webgpu sponza

This commit is contained in:
Jorijn van der Graaf 2026-05-19 00:27:09 +02:00
commit b5d0f52da0
21 changed files with 1426 additions and 58 deletions

109
examples/Sponza/raygen.wgsl Normal file
View file

@ -0,0 +1,109 @@
// 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.
struct Camera {
origin: vec3<f32>,
pad0: f32,
right: vec3<f32>,
tanHalf: f32,
up: vec3<f32>,
aspect: f32,
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);
fn raygen_main(gid: vec3<u32>) {
if (gid.x >= hdr.surfaceW || gid.y >= hdr.surfaceH) { return; }
let pixel = vec2<f32>(f32(gid.x), f32(gid.y));
let resolution = vec2<f32>(f32(hdr.surfaceW), f32(hdr.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));
}