// Sponza closest-hit (runs in SHADE). In the wavefront model the lighting // + shadow trace that used to live in raygen happens here: gather surface // data, accumulate ambient, and emit a shadow ray toward the sun carrying // the pending direct contribution. The shadow ray's miss adds that // contribution (sun visible); its hit adds nothing (occluded), since // RT_FLAG_SKIP_CLOSEST_HIT suppresses closesthit on the shadow ray. // // Payload declared here so the assembler sees it before wfPayload / SHADE. struct Payload { color: vec3, // shadow ray: pending albedo·sun·nDotL shadowRay: u32, // 0 primary, 1 shadow }; // User resources at @group(3) (0..2 are the wavefront pipeline's reserved // groups). binding 0 albedo array, 1 sampler, 2 camera (raygen only). @group(3) @binding(0) var albedos : texture_2d_array; @group(3) @binding(1) var samp : sampler; const SUN_DIR_TO_LIGHT: vec3 = vec3(-0.35, 1.00, -0.20); const SUN_COLOR: vec3 = vec3( 1.10, 1.00, 0.85); const AMBIENT_COLOR: vec3 = vec3( 0.18, 0.20, 0.28); const ATTRIB_STRIDE_U32: u32 = 12u; const ATTRIB_NORMAL_OFFSET: u32 = 0u; const ATTRIB_UV_OFFSET: u32 = 8u; fn fetchUV(meshRec: MeshRecord, vertexIdx: u32) -> vec2 { let base = meshRec.attribsOffset + vertexIdx * ATTRIB_STRIDE_U32 + ATTRIB_UV_OFFSET; return vec2( bitcast(vertexAttribs[base + 0u]), bitcast(vertexAttribs[base + 1u]), ); } fn fetchNormal(meshRec: MeshRecord, vertexIdx: u32) -> vec3 { let base = meshRec.attribsOffset + vertexIdx * ATTRIB_STRIDE_U32 + ATTRIB_NORMAL_OFFSET; return vec3( bitcast(vertexAttribs[base + 0u]), bitcast(vertexAttribs[base + 1u]), bitcast(vertexAttribs[base + 2u]), ); } fn closesthit_main(ray: RayDesc, hit: HitInfo, payload: ptr) { let meshIdx = tlasEntries[hit.instanceId].blasMeshIdx; let meshRec = meshRecords[meshIdx]; let baseIdx = meshRec.indexOffset + hit.primitiveId * 3u; let i0 = indices[baseIdx + 0u]; let i1 = indices[baseIdx + 1u]; let i2 = indices[baseIdx + 2u]; let bary = vec3(1.0 - hit.attribs.x - hit.attribs.y, hit.attribs.x, hit.attribs.y); let uv0 = fetchUV(meshRec, i0); let uv1 = fetchUV(meshRec, i1); let uv2 = fetchUV(meshRec, i2); let uv = uv0 * bary.x + uv1 * bary.y + uv2 * bary.z; let uvTiled = vec2(fract(uv.x), fract(1.0 - uv.y)); let layer = i32(hit.customIndex); let albedo = textureSampleLevel(albedos, samp, uvTiled, layer, 0.0).rgb; let n0 = fetchNormal(meshRec, i0); let n1 = fetchNormal(meshRec, i1); let n2 = fetchNormal(meshRec, i2); let nObj = normalize(n0 * bary.x + n1 * bary.y + n2 * bary.z); let nWorld = normalize(vec3( dot(hit.objectToWorldR0.xyz, nObj), dot(hit.objectToWorldR1.xyz, nObj), dot(hit.objectToWorldR2.xyz, nObj))); // Two-sided: flip the normal toward the camera (Sponza curtains have // inconsistent winding). let nFacing = select(-nWorld, nWorld, dot(nWorld, ray.direction) < 0.0); let lightDir = normalize(SUN_DIR_TO_LIGHT); let nDotL = max(0.0, dot(nFacing, lightDir)); let worldPos = ray.origin + ray.direction * hit.t; // Ambient is unconditional; direct light is gated behind the shadow ray. rtAccumulate(albedo * AMBIENT_COLOR); if (nDotL > 0.0) { let shadowOrigin = worldPos + nFacing * 0.5; var sp: Payload; sp.color = albedo * SUN_COLOR * nDotL; sp.shadowRay = 1u; rtEmitRay(shadowOrigin, 0.001, lightDir, 10000.0, RT_FLAG_SKIP_CLOSEST_HIT | RT_FLAG_TERMINATE_ON_FIRST_HIT, 0xFFu, 0u, 0u, sp); } }