webgpu demo
All checks were successful
Deploy / build-deploy (push) Successful in 1m38s

This commit is contained in:
Jorijn van der Graaf 2026-07-19 01:13:30 +02:00
commit fb2f6079cc
16 changed files with 508 additions and 9 deletions

15
.claude/settings.json Normal file
View file

@ -0,0 +1,15 @@
{
"permissions": {
"allow": [
"Bash(cd ../Crafter/Crafter.Graphics && echo \"=== C++ files declaring wgpu* imports ===\"; grep -rln 'import_name\\(\"wgpu' implementations/ interfaces/; echo; echo \"=== sample: how a wgpu import + public wrapper is declared \\(canvas size + a string-taking one\\) ===\"; grep -rn -B2 -A2 -E 'import_name\\\\\\(\"wgpu\\(GetCanvasWidth|SurfaceWidth|LoadCustomShader|Init\\)\"' implementations/ interfaces/ | head -50)",
"Read(//home/jorijn/repos/Crafter/Crafter.Graphics/**)",
"Read(//home/jorijn/repos/Crafter/Crafter.Graphics/interfaces/**)",
"Read(//home/jorijn/repos/Crafter/Crafter.Graphics/additional/**)",
"Bash(crafter-build --local)"
],
"additionalDirectories": [
"/home/jorijn/repos/Crafter/Crafter.Graphics/additional",
"/home/jorijn/repos/Crafter/Crafter.Graphics/interfaces"
]
}
}

View file

@ -3,8 +3,7 @@
This is the source code for catcrafts.net, a website built entirely in C++ using the Crafter.Graphics library. This is the source code for catcrafts.net, a website built entirely in C++ using the Crafter.Graphics library.
```bash ```bash
crafter-build build executable crafter-build -r
./run.sh
``` ```
This will compile the project and serve it locally on `http:://localhost:8080`. This will compile the project and serve it locally on `http:://localhost:8080`.

View file

@ -10,11 +10,34 @@ export module Catcrafts:Blog_impl;
import :Blog; import :Blog;
import :Root; import :Root;
import :Views; import :Views;
import :Demo;
import Crafter.Graphics; import Crafter.Graphics;
import std; import std;
using namespace Crafter; using namespace Crafter;
namespace Catcrafts {
// Post that hosts the live ray-traced WebGPU demo. RenderBlogPost
// injects the mount container into this post and mounts the render
// canvas into it once the DOM is in place.
constexpr std::string_view kDemoPostSlug = "hello-world-2";
// Markup for the embedded demo: a fixed-height host box the render
// canvas is reparented into (see Catcrafts:Demo / WebGPU::SetCanvasMount)
// plus a caption. The id must match kDemoMountId.
constexpr std::string_view kDemoCardHtml = R"(
<div class="webgpu-demo">
<div id="webgpu-demo" class="webgpu-demo-canvas"></div>
<p class="webgpu-demo-caption">
Live above: a scene ray-traced in real time &mdash; four coloured
point lights, one soft shadow per light &mdash; running through the
Crafter.Graphics WebGPU wavefront tracer, driven from this page's
C++ compiled to WebAssembly. Not a video, not an iframe: the same
WASM module that rendered this text is tracing those pixels.
</p>
</div>)";
}
export namespace Catcrafts { export namespace Catcrafts {
// Persistent storage for the per-post card click handlers. HtmlElementPtr // Persistent storage for the per-post card click handlers. HtmlElementPtr
// unregisters its listeners on destruction, so the elements have to // unregisters its listeners on destruction, so the elements have to
@ -63,6 +86,7 @@ export namespace Catcrafts {
void RenderBlogPost(const std::string_view slug) { void RenderBlogPost(const std::string_view slug) {
for(const BlogPost& post : posts) { for(const BlogPost& post : posts) {
if(post.slug == slug) { if(post.slug == slug) {
const bool isDemo = post.slug == kDemoPostSlug;
MainContent().SetInnerHTML(std::format(R"( MainContent().SetInnerHTML(std::format(R"(
<div class="blog-post-page"> <div class="blog-post-page">
<div class="post-header"> <div class="post-header">
@ -72,7 +96,13 @@ export namespace Catcrafts {
<div class="post-content"> <div class="post-content">
{} {}
</div> </div>
</div>)", post.name, post.date, post.content)); {}
</div>)", post.name, post.date, post.content, isDemo ? kDemoCardHtml : std::string_view{}));
// The #webgpu-demo container now exists in the DOM, so the
// render canvas can be reparented into it and tracing can
// start. RenderRoot() already called UnmountDemo() for us.
if(isDemo) MountDemo();
return; return;
} }
} }

View file

@ -0,0 +1,214 @@
/*
catcrafts.net
Copyright (C) 2026 Catcrafts
The source code of this website is made available for viewing purposes only.
No permission is granted to copy, modify, distribute, or create derivative works.
Ray-traced WebGPU demo embedded in the blog. The scene (a checkered floor
with five pillars lit by four coloured point lights, each pillar casting
four separable coloured shadows) and its four WGSL stages are adapted from
Crafter.Graphics' RTMultiShadow example the wavefront software ray tracer
that is the WebGPU/DOM RT path. Differences from the example:
* No input: the camera auto-orbits the scene, driven by a frame counter,
so the demo just plays. The embedded canvas is offset from the viewport
origin, which the mouse-coordinate bridge doesn't account for, so an
interactive camera would be misaligned anyway.
* The whole StartInit()/FinishInit() init lives in SetupDemo(); the render
pass is only attached to the window while the demo is mounted into
#webgpu-demo, so the rest of the site stays a plain DOM document.
*/
export module Catcrafts:Demo_impl;
import :Demo;
import Crafter.Graphics;
import Crafter.Math;
import Crafter.Event;
import std;
using namespace Crafter;
namespace fs = std::filesystem;
namespace {
// Matches LIGHT_COUNT in shaders/closesthit.wgsl and drives the
// per-pixel shadow-ray budget (one shadow ray per light).
constexpr std::uint32_t kLightCount = 4;
// Mirrors `struct Camera` in shaders/raygen.wgsl byte-for-byte.
struct CameraGPU {
float origin[3]; float pad0;
float right[3]; float tanHalf;
float up[3]; float aspect;
float forward[3]; float pad1;
};
static_assert(sizeof(CameraGPU) == 64);
// Axis-aligned box: 8 corners between mn and mx.
std::array<Vector<float, 3, 3>, 8> BoxVerts(float mnx, float mny, float mnz,
float mxx, float mxy, float mxz) {
return {{
{mnx, mny, mnz}, {mxx, mny, mnz}, {mxx, mxy, mnz}, {mnx, mxy, mnz},
{mnx, mny, mxz}, {mxx, mny, mxz}, {mxx, mxy, mxz}, {mnx, mxy, mxz},
}};
}
// Mesh::Build takes mutable spans, so this can't be constexpr.
std::array<std::uint32_t, 36> kBoxIndices {{
0,1,2, 0,2,3, 5,4,7, 5,7,6, 4,0,3, 4,3,7,
1,5,6, 1,6,2, 4,5,1, 4,1,0, 3,2,6, 3,6,7,
}};
// Cross-module demo state. Raw pointers into function-local statics in
// SetupDemo() (valid for the program's lifetime); the site builds with
// -fno-c++-static-destructors so none of it is torn down at exit.
Window* gWindow = nullptr;
RTPass* gRtPass = nullptr;
bool gActive = false;
std::uint32_t gFrame = 0;
}
namespace Catcrafts {
void SetupDemo(Window& window) {
gWindow = &window;
auto cmd = window.StartInit();
static DescriptorHeapWebGPU heap;
heap.Initialize(/*images*/ 1, /*buffers*/ 2, /*samplers*/ 1);
static std::array<WebGPUShader, 4> shaders {{
WebGPUShader(fs::path("raygen.wgsl"), "raygen_main", WebGPURTStage::Raygen),
WebGPUShader(fs::path("miss.wgsl"), "miss_main", WebGPURTStage::Miss),
WebGPUShader(fs::path("closesthit.wgsl"), "closesthit_main", WebGPURTStage::ClosestHit),
WebGPUShader(fs::path("resolve.wgsl"), "resolve_main", WebGPURTStage::Resolve),
}};
static ShaderBindingTableWebGPU sbt;
sbt.Init(shaders);
static std::array<RTShaderGroup, 1> raygenGroups {{ { .type = RTShaderGroupType::General, .generalShader = 0 } }};
static std::array<RTShaderGroup, 1> missGroups {{ { .type = RTShaderGroupType::General, .generalShader = 1 } }};
static std::array<RTShaderGroup, 1> hitGroups {{ { .type = RTShaderGroupType::TrianglesHitGroup, .closestHitShader = 2 } }};
// One user binding: the camera storage buffer at @group(3).
static std::array<UICustomBinding, 1> bindings {{
{ .group = 3, .binding = 0, .kind = UICustomBindingKind::Buffer, .pushOffset = 0 },
}};
static PipelineRTWebGPU pipeline;
pipeline.Init(cmd, raygenGroups, missGroups, hitGroups, sbt, bindings);
// Meshes: a large ground slab and a pillar (origin at its base).
static auto groundVerts = BoxVerts(-30.0f, -1.0f, -30.0f, 30.0f, 0.0f, 30.0f);
static auto pillarVerts = BoxVerts(-0.8f, 0.0f, -0.8f, 0.8f, 6.0f, 0.8f);
static Mesh ground, pillar;
ground.Build(groundVerts, kBoxIndices, cmd);
pillar.Build(pillarVerts, kBoxIndices, cmd);
static WebGPUBuffer<CameraGPU, true> cameraBuf;
cameraBuf.Create(1);
static std::array<std::uint32_t, 1> userHandles { cameraBuf.handle };
// Instances: ground (customIndex 0) + five pillars.
struct Placement { float x, z; };
static constexpr std::array<Placement, 5> kPillars {{
{ 0.0f, 0.0f }, { 5.0f, 5.0f }, { -5.0f, 5.0f }, { 5.0f, -5.0f }, { -5.0f, -5.0f },
}};
static std::vector<RenderingElement3D> renderers;
renderers.reserve(1 + kPillars.size());
auto addInstance = [&](std::uint64_t blasAddr, float x, float z) {
renderers.emplace_back();
RenderingElement3D& r = renderers.back();
auto& tx = r.instance.transform.matrix;
tx[0][0] = 1; tx[0][1] = 0; tx[0][2] = 0; tx[0][3] = x;
tx[1][0] = 0; tx[1][1] = 1; tx[1][2] = 0; tx[1][3] = 0;
tx[2][0] = 0; tx[2][1] = 0; tx[2][2] = 1; tx[2][3] = z;
r.instance.instanceCustomIndex = static_cast<std::uint32_t>(renderers.size() - 1);
r.instance.mask = 0xFF;
r.instance.instanceShaderBindingTableRecordOffset = 0;
r.instance.flags = kRTGeometryInstanceForceOpaque;
r.instance.accelerationStructureReference = blasAddr;
RenderingElement3D::Add(&r);
};
addInstance(ground.blasAddr, 0.0f, 0.0f);
for (const auto& p : kPillars) addInstance(pillar.blasAddr, p.x, p.z);
RenderingElement3D::BuildTLAS(cmd, 0);
window.descriptorHeap = &heap;
window.FinishInit();
static RTPass rtPass(&pipeline);
rtPass.handlesPtr = userHandles.data();
rtPass.handlesCount = static_cast<std::uint32_t>(userHandles.size());
rtPass.maxDepth = 2; // primary + shadow
rtPass.raysPerPixel = kLightCount; // one shadow ray per light per pixel
gRtPass = &rtPass;
// Auto-orbit camera. Only does work (and touches the GPU buffer)
// while the demo is mounted; otherwise the tick is a cheap no-op.
// Capture nothing: cameraBuf is a static local (usable directly),
// and the window is reached through the stable gWindow pointer —
// capturing the reference parameter into this static lambda would
// dangle once SetupDemo() returns.
static EventListener<void> camTick(&window.onBeforeUpdate, []() {
if (!gActive || gWindow == nullptr || gWindow->height == 0) return;
const float t = static_cast<float>(gFrame) * 0.006f;
++gFrame;
const float radius = 24.0f;
const float px = std::cos(t) * radius;
const float pz = std::sin(t) * radius;
const float py = 12.0f;
// forward = normalize(centre - eye), centre ≈ (0, 2, 0)
float fx = -px, fy = 2.0f - py, fz = -pz;
const float fl = std::sqrt(fx*fx + fy*fy + fz*fz);
fx /= fl; fy /= fl; fz /= fl;
// right = normalize(cross(forward, worldUp)), worldUp = (0,1,0)
float rx = fy*0.0f - fz*1.0f;
float ry = fz*0.0f - fx*0.0f;
float rz = fx*1.0f - fy*0.0f;
const float rl = std::sqrt(rx*rx + ry*ry + rz*rz);
rx /= rl; ry /= rl; rz /= rl;
// up = cross(right, forward)
const float ux = ry*fz - rz*fy;
const float uy = rz*fx - rx*fz;
const float uz = rx*fy - ry*fx;
CameraGPU& g = cameraBuf.value[0];
g.origin[0]=px; g.origin[1]=py; g.origin[2]=pz; g.pad0=0;
g.right[0]=rx; g.right[1]=ry; g.right[2]=rz;
g.up[0]=ux; g.up[1]=uy; g.up[2]=uz;
g.forward[0]=fx; g.forward[1]=fy; g.forward[2]=fz;
g.aspect = static_cast<float>(gWindow->width) / static_cast<float>(gWindow->height);
g.tanHalf = std::tan(70.0f * 3.14159265f / 360.0f);
g.pad1 = 0;
cameraBuf.FlushDevice();
});
// Start as a plain DOM page: canvas detached + hidden, no RT pass.
WebGPU::SetCanvasMount("");
}
void MountDemo() {
if (!gWindow || !gRtPass) return;
if (!gActive) {
gWindow->passes.push_back(gRtPass);
gActive = true;
}
WebGPU::SetCanvasMount(kDemoMountId);
}
void UnmountDemo() {
if (!gWindow || !gRtPass) return;
if (gActive) {
auto& p = gWindow->passes;
std::erase(p, gRtPass);
gActive = false;
}
WebGPU::SetCanvasMount("");
}
}

View file

@ -10,6 +10,7 @@ export module Catcrafts:Root_impl;
import :Root; import :Root;
import :Views; import :Views;
import :Blog; import :Blog;
import :Demo;
import Crafter.Graphics; import Crafter.Graphics;
import std; import std;
@ -17,6 +18,11 @@ using namespace Crafter;
namespace Catcrafts { namespace Catcrafts {
void RenderRoot(const std::string_view route) { void RenderRoot(const std::string_view route) {
// Every route change replaces <main>'s innerHTML, which would
// orphan the demo canvas if it's currently mounted inside a post.
// Detach + hide it first; the post renderer re-mounts if needed.
UnmountDemo();
std::string currentRoute = std::string(route); std::string currentRoute = std::string(route);
if(currentRoute == "/blog" || currentRoute == "/") { if(currentRoute == "/blog" || currentRoute == "/") {

View file

@ -18,7 +18,14 @@ int main() {
// plus the rAF driver; even without subscribing to its events we need // plus the rAF driver; even without subscribing to its events we need
// it so StartSync() can keep the wasm module alive past main() (the // it so StartSync() can keep the wasm module alive past main() (the
// bridge stashes a raw pointer, so the storage has to outlive main). // bridge stashes a raw pointer, so the storage has to outlive main).
static Window window(0, 0, "Catcrafts"); // The dimensions are a hint only — the JS bridge sizes the render
// surface to the canvas (full viewport, or the mount element).
static Window window(1280, 720, "Catcrafts");
// Build the ray-tracing pipeline + scene now (runs StartInit/FinishInit).
// The render canvas starts hidden; the demo only traces once a route
// mounts it into #webgpu-demo (see Catcrafts:Demo).
SetupDemo(window);
InitializePage(); InitializePage();
@ -34,6 +41,7 @@ int main() {
RenderRoot(Router::GetPath()); RenderRoot(Router::GetPath());
window.Render();
window.StartUpdate(); window.StartUpdate();
window.StartSync(); window.StartSync();
return 0; return 0;

View file

@ -19,10 +19,23 @@ export namespace Catcrafts {
std::string content; std::string content;
}; };
std::vector<BlogPost> posts { std::vector<BlogPost> posts {
{
"Hello World! 2?",
"hello-world-2",
"2026-07-18",
R"(So this blog has been mega dead but today i bring something exciting that was already released months ago but never deployed xd.
Crafter.CppDOM is dead, long live Crafter.Graphics!
This website is now updated to use the new Crafter.Graphics library, which allow for C++ manpiulation of the DOM, and as a new feature WebGPU!
And what better way to flex WebGPU than ray tracing? Here's a little scene traced live in your browser, shadows and all, straight from the same C++ compiled to WASM that rendered this very post:
)"
},
{ {
"In WASM, Exit doesn't mean done.", "In WASM, Exit doesn't mean done.",
"in-wasm-exit-doesnt-mean-done", "in-wasm-exit-doesnt-mean-done",
"2026-11-14", "2025-11-14",
R"(So if anyone looked at the source code for this website pefore this post you would have seen that everything was allocated with new, for example this very blog was defined as <code>std::vector&lt;BlogPost&gt; posts = new std::vector&lt;BlogPost&gt;{...};</code><br><br> R"(So if anyone looked at the source code for this website pefore this post you would have seen that everything was allocated with new, for example this very blog was defined as <code>std::vector&lt;BlogPost&gt; posts = new std::vector&lt;BlogPost&gt;{...};</code><br><br>
Reason for this was that everything became corrupted when callbacking from JS, not knowing sure as to why this was the fastest solution, but after debugging the problem became clear.<br><br> Reason for this was that everything became corrupted when callbacking from JS, not knowing sure as to why this was the fastest solution, but after debugging the problem became clear.<br><br>
@ -40,7 +53,7 @@ export namespace Catcrafts {
{ {
"Hello World!", "Hello World!",
"hello-world", "hello-world",
"2026-11-12", "2025-11-12",
R"(Welcome to catcrafts.net!<br><br> R"(Welcome to catcrafts.net!<br><br>
Here we believe optimization is everything and C++ is a gift from god.<br> Here we believe optimization is everything and C++ is a gift from god.<br>
This blog will mostly be dedicated to random tidbits i come across while working on my Crafter series of libraries.<br><br> This blog will mostly be dedicated to random tidbits i come across while working on my Crafter series of libraries.<br><br>

View file

@ -0,0 +1,36 @@
/*
catcrafts.net
Copyright (C) 2026 Catcrafts
The source code of this website is made available for viewing purposes only.
No permission is granted to copy, modify, distribute, or create derivative works.
*/
export module Catcrafts:Demo;
import Crafter.Graphics;
import std;
using namespace Crafter;
export namespace Catcrafts {
// DOM element id that the ray-traced WebGPU demo renders into. The
// blog post embeds a <div> with this id; MountDemo() reparents the
// Crafter.Graphics render canvas into it via WebGPU::SetCanvasMount.
inline constexpr std::string_view kDemoMountId = "webgpu-demo";
// Build the ray-tracing pipeline + scene once and start the render
// loop plumbing. Runs the window's StartInit()/FinishInit(), so call
// it from main() right after the Window is constructed and before the
// page chrome / routes are built. The canvas starts detached + hidden;
// nothing is traced until MountDemo() is called.
void SetupDemo(Window& window);
// Reparent the render canvas into #webgpu-demo and begin tracing. Call
// only after the element exists in the DOM (i.e. right after the post's
// innerHTML has been set). Idempotent.
void MountDemo();
// Stop tracing and detach + hide the canvas, leaving a plain DOM page.
// Call before any route re-render that would replace #webgpu-demo.
// Safe to call when the demo is not mounted.
void UnmountDemo();
}

View file

@ -9,4 +9,5 @@ No permission is granted to copy, modify, distribute, or create derivative works
export module Catcrafts; export module Catcrafts;
export import :Views; export import :Views;
export import :Blog; export import :Blog;
export import :Root; export import :Root;
export import :Demo;

View file

@ -46,17 +46,19 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
ApplyStandardArgs(cfg, args); ApplyStandardArgs(cfg, args);
cfg.dependencies = { graphics }; cfg.dependencies = { graphics };
std::array<fs::path, 4> ifaces = { std::array<fs::path, 5> ifaces = {
"interfaces/Catcrafts", "interfaces/Catcrafts",
"interfaces/Catcrafts-Views", "interfaces/Catcrafts-Views",
"interfaces/Catcrafts-Blog", "interfaces/Catcrafts-Blog",
"interfaces/Catcrafts-Root", "interfaces/Catcrafts-Root",
"interfaces/Catcrafts-Demo",
}; };
std::array<fs::path, 4> impls = { std::array<fs::path, 5> impls = {
"implementations/main", "implementations/main",
"implementations/Catcrafts-Blog", "implementations/Catcrafts-Blog",
"implementations/Catcrafts-Root", "implementations/Catcrafts-Root",
"implementations/Catcrafts-Views", "implementations/Catcrafts-Views",
"implementations/Catcrafts-Demo",
}; };
cfg.GetInterfacesAndImplementations(ifaces, impls); cfg.GetInterfacesAndImplementations(ifaces, impls);
@ -64,6 +66,12 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
cfg.files.emplace_back(fs::path("robots.txt")); cfg.files.emplace_back(fs::path("robots.txt"));
cfg.files.emplace_back(fs::path("sitemap.xml")); cfg.files.emplace_back(fs::path("sitemap.xml"));
cfg.files.emplace_back(fs::path("favicon.svg")); cfg.files.emplace_back(fs::path("favicon.svg"));
// WGSL for the ray-traced WebGPU demo embedded in the blog (see
// interfaces/Catcrafts-Demo.cppm). Fetched at runtime by WebGPUShader.
cfg.files.emplace_back(fs::path("shaders/raygen.wgsl"));
cfg.files.emplace_back(fs::path("shaders/miss.wgsl"));
cfg.files.emplace_back(fs::path("shaders/closesthit.wgsl"));
cfg.files.emplace_back(fs::path("shaders/resolve.wgsl"));
// Loaded as a <script type="module"> before runtime.js by // Loaded as a <script type="module"> before runtime.js by
// EnableWasiBrowserRuntime — sets <title>/<link> tags since the // EnableWasiBrowserRuntime — sets <title>/<link> tags since the
// Dom partition has no head-element access. // Dom partition has no head-element access.

82
shaders/closesthit.wgsl Normal file
View file

@ -0,0 +1,82 @@
// RTMultiShadow closest-hit (runs in SHADE). The multi-light counterpart
// of RTStress: EVERY light emits its own shadow ray from this single
// invocation, so several rays for the same pixel resolve in the next SHADE
// pass exactly the contention the atomic rtAccumulate exists for (#30).
// Before the atomic accumulator their rtAccumulate calls raced (lost
// updates flickering dark noise); before RTPass::raysPerPixel the extra
// rays were silently dropped by the capacity guard. The host sets
// raysPerPixel = LIGHT_COUNT so every emit fits the bounce.
//
// Payload declared here so the assembler sees it before wfPayload / SHADE.
struct Payload {
color: vec3<f32>, // shadow ray: pending direct contribution
shadowRay: u32, // 0 primary, 1 shadow
};
// Point lights, color premultiplied with intensity; 1/d² falloff at shade
// time. Four distinct hues so each occluder casts four separable shadows
// any accumulator race or dropped shadow ray is immediately visible as
// noise / a missing color in the overlap regions.
const LIGHT_COUNT: u32 = 4u;
struct Light {
pos: vec3<f32>,
color: vec3<f32>,
};
var<private> LIGHTS: array<Light, 4> = array<Light, 4>(
Light(vec3<f32>( 14.0, 9.0, 2.0), vec3<f32>(250.0, 205.0, 140.0)), // warm white
Light(vec3<f32>(-13.0, 8.0, 7.0), vec3<f32>(235.0, 45.0, 30.0)), // red
Light(vec3<f32>( 3.0, 8.0, -14.0), vec3<f32>( 55.0, 225.0, 105.0)), // green
Light(vec3<f32>( -5.0, 10.0, 13.0), vec3<f32>( 65.0, 105.0, 250.0)), // blue
);
const AMBIENT_COLOR: vec3<f32> = vec3<f32>(0.030, 0.034, 0.045);
// Ground (customIndex 0) is a subtle checker so the colored shadows read;
// pillars hash their instance index like RTStress.
fn surfaceAlbedo(customIndex: u32, worldPos: vec3<f32>) -> vec3<f32> {
if (customIndex == 0u) {
let cx = u32(floor(worldPos.x * 0.25 + 100.0));
let cz = u32(floor(worldPos.z * 0.25 + 100.0));
return mix(vec3<f32>(0.60), vec3<f32>(0.76), f32((cx + cz) & 1u));
}
let h = customIndex * 2654435761u;
return vec3<f32>(
0.45 + 0.5 * f32((h >> 0u) & 255u) / 255.0,
0.45 + 0.5 * f32((h >> 8u) & 255u) / 255.0,
0.45 + 0.5 * f32((h >> 16u) & 255u) / 255.0);
}
fn closesthit_main(ray: RayDesc, hit: HitInfo, payload: ptr<function, Payload>) {
let meshRec = meshRecords[tlasEntries[hit.instanceId].blasMeshIdx];
let verts = _rtFetchTri(meshRec, hit.primitiveId);
let nObj = normalize(cross(verts[1] - verts[0], verts[2] - verts[0]));
let nWorld = normalize(vec3<f32>(
dot(hit.objectToWorldR0.xyz, nObj),
dot(hit.objectToWorldR1.xyz, nObj),
dot(hit.objectToWorldR2.xyz, nObj)));
let worldPos = ray.origin + ray.direction * hit.t;
let nFacing = select(-nWorld, nWorld, dot(nWorld, -ray.direction) > 0.0);
let albedo = surfaceAlbedo(hit.customIndex, worldPos);
rtAccumulate(albedo * AMBIENT_COLOR);
// One shadow ray PER LIGHT from this one closest-hit invocation. All of
// them carry the same pixel; the ones that miss (light visible) each
// rtAccumulate their light's contribution in the same SHADE pass.
let shadowOrigin = worldPos + nFacing * 0.05;
for (var i: u32 = 0u; i < LIGHT_COUNT; i = i + 1u) {
let toLight = LIGHTS[i].pos - shadowOrigin;
let dist = length(toLight);
let dir = toLight / dist;
let nDotL = dot(nFacing, dir);
if (nDotL <= 0.0) { continue; }
var sp: Payload;
sp.color = albedo * LIGHTS[i].color * (nDotL / (dist * dist));
sp.shadowRay = 1u;
// tMax stops at the light so geometry beyond it can't occlude.
rtEmitRay(shadowOrigin, 0.01, dir, dist,
RT_FLAG_SKIP_CLOSEST_HIT | RT_FLAG_TERMINATE_ON_FIRST_HIT,
0xFFu, 0u, 0u, sp);
}
}

14
shaders/miss.wgsl Normal file
View file

@ -0,0 +1,14 @@
// RTMultiShadow miss (runs in SHADE). Shadow miss that light is visible
// from the surface, so add its pending contribution; up to LIGHT_COUNT of
// these resolve for the same pixel in one pass (atomic rtAccumulate, #30).
// Primary miss near-black night sky so the colored lighting carries the
// frame.
fn miss_main(ray: RayDesc, payload: ptr<function, Payload>) {
if ((*payload).shadowRay == 1u) {
rtAccumulate((*payload).color);
return;
}
let t = clamp(ray.direction.y * 0.5 + 0.5, 0.0, 1.0);
rtAccumulate(mix(vec3<f32>(0.010, 0.012, 0.022),
vec3<f32>(0.030, 0.040, 0.075), t));
}

35
shaders/raygen.wgsl Normal file
View file

@ -0,0 +1,35 @@
// RTMultiShadow raygen (runs in GENERATE). Host-driven pinhole camera at
// @group(3) (groups 0..2 are reserved by the wavefront pipeline:
// 0 = WfParams, 1 = data heaps, 2 = indirect args).
struct Camera {
origin: vec3<f32>,
pad0: f32,
right: vec3<f32>,
tanHalf: f32,
up: vec3<f32>,
aspect: f32,
forward: vec3<f32>,
pad1: f32,
};
@group(3) @binding(0) var<storage, read> camera : Camera;
fn raygen_main(gid: vec3<u32>) {
if (gid.x >= wfParams.surfaceW || gid.y >= wfParams.surfaceH) { return; }
let pixelf = vec2<f32>(f32(gid.x), f32(gid.y));
let res = vec2<f32>(f32(wfParams.surfaceW), f32(wfParams.surfaceH));
let uv = (pixelf + vec2<f32>(0.5)) / res;
let ndc = uv * 2.0 - vec2<f32>(1.0);
let direction = normalize(
camera.right * (ndc.x * camera.aspect * camera.tanHalf) +
camera.up * (-ndc.y * camera.tanHalf) +
camera.forward);
var p: Payload;
p.color = vec3<f32>(0.0);
p.shadowRay = 0u;
rtEmitPrimaryRay(camera.origin, 0.01, direction, 100000.0,
0u, 0xFFu, 0u, 0u, p);
}

7
shaders/resolve.wgsl Normal file
View file

@ -0,0 +1,7 @@
// RTMultiShadow RESOLVE-stage tonemap: Reinhard + gamma 2.2 over the
// linear accumulator. Registered as a WebGPURTStage::Resolve shader.
fn resolve_main(coord: vec2<u32>, hdr: vec4<f32>) -> vec4<f32> {
let mapped = hdr.rgb / (hdr.rgb + vec3<f32>(1.0));
let g = pow(mapped, vec3<f32>(1.0 / 2.2));
return vec4<f32>(g, 1.0);
}

View file

@ -12,4 +12,7 @@
<url> <url>
<loc>https://catcrafts.net/blog/in-wasm-exit-doesnt-mean-done</loc> <loc>https://catcrafts.net/blog/in-wasm-exit-doesnt-mean-done</loc>
</url> </url>
<url>
<loc>https://catcrafts.net/blog/hello-world-2</loc>
</url>
</urlset> </urlset>

View file

@ -282,6 +282,34 @@ main {
opacity: 1; opacity: 1;
} }
/* Embedded ray-traced WebGPU demo (see Catcrafts:Demo). */
.webgpu-demo {
margin: 2.5rem 0 1rem;
}
/* Host box the Crafter.Graphics render canvas is reparented into. It must
have a resolved height for getBoundingClientRect() (the canvas is
absolutely positioned inside it), so the size comes from aspect-ratio.
The dark fill + border frame it before the first traced frame lands. */
.webgpu-demo-canvas {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
border-radius: var(--border-radius);
overflow: hidden;
background: #05060a;
border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: var(--box-shadow);
}
.webgpu-demo-caption {
margin-top: 0.85rem;
font-size: 0.95rem;
line-height: 1.6;
color: var(--text-secondary);
text-align: center;
}
/* Footer */ /* Footer */
footer { footer {
background-color: var(--dark-color); background-color: var(--dark-color);