WebGPU RT: port Sponza to wavefront (shadow ray in SHADE)

Restructure Sponza for the wavefront model: raygen emits the primary ray;
closesthit (in SHADE) gathers albedo/normal, accumulates ambient, and
emits a shadow ray carrying the pending direct term; miss adds the sky
(primary) or the direct term (shadow miss). resolve.wgsl applies the same
Reinhard+gamma the megakernel raygen did inline. User bindings moved to
group 3 (groups 0..2 reserved). RTPass maxDepth=2.

Renders the atrium correctly through the wavefront pipeline (textures,
two-sided shading, sun+ambient, shadows, tonemap).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
catbot 2026-05-31 20:16:04 +00:00
commit 376e66aeed
6 changed files with 70 additions and 134 deletions

View file

@ -1,12 +1,8 @@
// WebGPU raygen. Camera state comes from the host every frame via a
// storage buffer bound at @group(2) @binding(2); main.cpp drives that
// from WASD + mouse-delta through Crafter::Input.
//
// The shading + shadow trace all happens here because WGSL forbids
// recursive function call graphs closesthit_main can't call traceRay
// (that would loop closesthit traceRay runClosestHit closesthit).
// Raygen is the entry point and not called by anyone, so it can call
// traceRay twice (once primary, once shadow) without forming a cycle.
// Sponza raygen (runs in GENERATE). Emits the pixel's primary ray; all
// shading + the shadow trace now happen in SHADE (closesthit/miss). Camera
// state comes from the host each frame via a storage buffer at
// @group(3) @binding(2) (groups 0..2 are reserved by the wavefront
// pipeline). main.cpp drives it from WASD + mouse-delta.
struct Camera {
origin: vec3<f32>,
@ -18,92 +14,25 @@ struct Camera {
forward: vec3<f32>,
pad1: f32,
};
@group(2) @binding(2) var<storage, read> camera : Camera;
// Sun coming through Sponza's open roof. Y is up; this points "down and
// slightly along +X" so the light grazes the colonnades on one side.
const SUN_DIR_TO_LIGHT: vec3<f32> = vec3<f32>(-0.35, 1.00, -0.20);
const SUN_COLOR: vec3<f32> = vec3<f32>( 1.10, 1.00, 0.85);
const AMBIENT_COLOR: vec3<f32> = vec3<f32>( 0.18, 0.20, 0.28);
@group(3) @binding(2) var<storage, read> camera : Camera;
fn raygen_main(gid: vec3<u32>) {
if (gid.x >= hdr.surfaceW || gid.y >= hdr.surfaceH) { return; }
if (gid.x >= wfParams.surfaceW || gid.y >= wfParams.surfaceH) { return; }
let pixel = vec2<f32>(f32(gid.x), f32(gid.y));
let resolution = vec2<f32>(f32(hdr.surfaceW), f32(hdr.surfaceH));
let resolution = vec2<f32>(f32(wfParams.surfaceW), f32(wfParams.surfaceH));
let uv = (pixel + vec2<f32>(0.5)) / resolution;
let ndc = uv * 2.0 - vec2<f32>(1.0);
// Pinhole camera reconstructed from the host basis. ndc.x runs left-
// to-right across the screen +right; ndc.y is top-down so we
// negate before applying +up.
let direction = normalize(
camera.right * (ndc.x * camera.aspect * camera.tanHalf) +
camera.up * (-ndc.y * camera.tanHalf) +
camera.forward);
// Primary ray
var payload: Payload;
payload.color = vec3<f32>(0.0);
payload.shadowRay = 0u;
payload.hit = 0u;
traceRay(
0u, 0u, 0xFFu,
0u, 0u, 0u,
camera.origin, 0.001,
direction, 10000.0,
&payload);
var finalColor: vec3<f32>;
if (payload.hit == 1u) {
// Closesthit filled albedo/worldPos/worldNormal. Two-sided
// shading: flip the normal toward the camera if we hit the back
// face Sponza's curtains in particular have inconsistent
// winding, and without this half the surface would go black.
let albedo = payload.color;
let nFacing = select(-payload.worldNormal,
payload.worldNormal,
dot(payload.worldNormal, direction) < 0.0);
let lightDir = normalize(SUN_DIR_TO_LIGHT);
let nDotL = max(0.0, dot(nFacing, lightDir));
// Shadow ray
// Only worth tracing if the surface faces the sun at all.
var visibility = 0.0;
if (nDotL > 0.0) {
// Normal-offset bias on Sponza's units (~3700 wide atrium)
// is hefty; 0.5 keeps the shadow ray clear of the originating
// triangle without producing visible "floating" shadows.
let shadowOrigin = payload.worldPos + nFacing * 0.5;
var shadowPayload: Payload;
shadowPayload.color = vec3<f32>(0.0); // default: blocked
shadowPayload.shadowRay = 1u;
shadowPayload.hit = 0u;
traceRay(
0u,
RT_FLAG_SKIP_CLOSEST_HIT | RT_FLAG_TERMINATE_ON_FIRST_HIT,
0xFFu,
0u, 0u, 0u,
shadowOrigin, 0.001,
lightDir, 10000.0,
&shadowPayload);
visibility = shadowPayload.color.x;
}
let lit = AMBIENT_COLOR + SUN_COLOR * (nDotL * visibility);
finalColor = albedo * lit;
} else {
// Sky color was filled by miss_main.
finalColor = payload.color;
}
// Reinhard tonemap + gamma 2.2 so sun-lit albedos don't clip and
// shadow detail stays readable.
let mapped = finalColor / (finalColor + vec3<f32>(1.0));
let gamma = pow(mapped, vec3<f32>(1.0 / 2.2));
textureStore(outImage,
vec2<i32>(i32(gid.x), i32(gid.y)),
vec4<f32>(gamma, 1.0));
rtEmitPrimaryRay(camera.origin, 0.001, direction, 10000.0,
0u, 0xFFu, 0u, 0u, payload);
}