// 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<f32>,   // 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<f32>;
@group(3) @binding(1) var samp    : sampler;

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);

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<f32> {
    let base = meshRec.attribsOffset + vertexIdx * ATTRIB_STRIDE_U32 + ATTRIB_UV_OFFSET;
    return vec2<f32>(
        bitcast<f32>(vertexAttribs[base + 0u]),
        bitcast<f32>(vertexAttribs[base + 1u]),
    );
}

fn fetchNormal(meshRec: MeshRecord, vertexIdx: u32) -> vec3<f32> {
    let base = meshRec.attribsOffset + vertexIdx * ATTRIB_STRIDE_U32 + ATTRIB_NORMAL_OFFSET;
    return vec3<f32>(
        bitcast<f32>(vertexAttribs[base + 0u]),
        bitcast<f32>(vertexAttribs[base + 1u]),
        bitcast<f32>(vertexAttribs[base + 2u]),
    );
}

fn closesthit_main(ray: RayDesc, hit: HitInfo, payload: ptr<function, Payload>) {
    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<f32>(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<f32>(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<f32>(
        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);
    }
}
