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:
parent
1d2e12dbc9
commit
376e66aeed
6 changed files with 70 additions and 134 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue