Skip to main content

LIGHT_CULL

Constant LIGHT_CULL 

Source
pub const LIGHT_CULL: &str = "// Clustered light-binning compute kernel. One thread per froxel cluster: builds\n// the cluster\'s world-space AABB from its screen tile + exponential depth slice,\n// tests every local light\'s bounding sphere against it, and writes the surviving\n// light indices into the per-cluster list the forward pass reads.\n//\n// Single source for every backend. Vulkan binds set 0 bindings 0-2; Metal and\n// DXIL assign the same slots from declaration order (params 0, lights 1,\n// list 2), matching the hand-assigned host bindings on all three.\n//\n// Layouts must match render_types.rs: ClusterParams (128 B) and GpuLight (64 B).\n// CLUSTER_LIGHT_LIST_STRIDE mirrors MAX_LIGHTS_PER_CLUSTER + 1.\n\nstatic const uint CLUSTER_LIGHT_LIST_STRIDE = 64u;\nstatic const uint MAX_LIGHTS_PER_CLUSTER = 63u;\n\n// Each (vec3, scalar) pair is spelled as one float4: MSL sizes a float3 at 16\n// bytes in a structured buffer as well as in a constant buffer, so a literal\n// transcription pushes every following field four bytes late on Metal alone.\nstruct GpuLight\n{\n    // xyz = world-space position, w = range.\n    float4 position_range;\n    // xyz = linear RGB, w = intensity.\n    float4 color_intensity;\n    // xyz = direction, w = the LIGHT_KIND_* discriminant\'s bits.\n    float4 direction_kind;\n    float  cos_inner;\n    float  cos_outer;\n    int    shadow_index;\n    // Index into the AreaLightData table for an area light, else -1. Unused by\n    // the cull (the bounding sphere covers every light kind) but kept so this\n    // declaration matches the forward pass\'s byte for byte.\n    int    data_index;\n};\n\nstruct ClusterParams\n{\n    float4x4 inv_view_proj;\n    // xyz = camera position, w = z_near. float4 pairs rather than float3 +\n    // scalar: MSL sizes a constant-buffer float3 at 16 bytes, so the packed\n    // 128-byte CPU layout only survives on every target without vec3 fields.\n    float4   cam_pos_znear;\n    // xyz = view forward, w = z_far.\n    float4   view_forward_zfar;\n    uint     grid_x;\n    uint     grid_y;\n    uint     grid_z;\n    uint     num_lights;\n    float    screen_w;\n    float    screen_h;\n    uint     use_clusters;\n    uint     _pad;\n};\n\n[[vk::binding(0, 0)]]\nConstantBuffer<ClusterParams> cluster;\n\n[[vk::binding(1, 0)]]\nStructuredBuffer<GpuLight> lights;\n\n[[vk::binding(2, 0)]]\nRWStructuredBuffer<uint> cluster_list;\n\n// Direction of the camera ray through a screen-NDC point. Unprojects the far\n// plane (z = 1) to world space, then normalises from the camera: for a\n// perspective projection every ray through a screen point passes through the\n// eye, so the far-plane unprojection gives the direction.\nfloat3 cluster_corner_ray(float2 ndc)\n{\n    float4 clip = float4(ndc, 1.0, 1.0);\n    float4 world = mul(cluster.inv_view_proj, clip);\n    world /= world.w;\n    return normalize(world.xyz - cluster.cam_pos_znear.xyz);\n}\n\n[shader(\"compute\")]\n[numthreads(64, 1, 1)]\nvoid light_cull_kernel(uint3 tid : SV_DispatchThreadID)\n{\n    uint cid = tid.x;\n    uint cluster_count = cluster.grid_x * cluster.grid_y * cluster.grid_z;\n    if (cid >= cluster_count)\n    {\n        return;\n    }\n\n    uint cx = cid % cluster.grid_x;\n    uint cy = (cid / cluster.grid_x) % cluster.grid_y;\n    uint cz = cid / (cluster.grid_x * cluster.grid_y);\n\n    // Screen-tile NDC bounds (y flipped: screen y-down to NDC y-up).\n    float2 lo = float2(float(cx), float(cy)) / float2(float(cluster.grid_x), float(cluster.grid_y));\n    float2 hi = float2(float(cx + 1u), float(cy + 1u)) / float2(float(cluster.grid_x), float(cluster.grid_y));\n    float2 ndcs[4] = {\n        float2(lo.x * 2.0 - 1.0, -(lo.y * 2.0 - 1.0)),\n        float2(hi.x * 2.0 - 1.0, -(lo.y * 2.0 - 1.0)),\n        float2(lo.x * 2.0 - 1.0, -(hi.y * 2.0 - 1.0)),\n        float2(hi.x * 2.0 - 1.0, -(hi.y * 2.0 - 1.0)),\n    };\n\n    // Exponential depth slice: near/far view-space distances for this Z band.\n    float ratio  = cluster.view_forward_zfar.w / cluster.cam_pos_znear.w;\n    float near_d = cluster.cam_pos_znear.w * pow(ratio, float(cz) / float(cluster.grid_z));\n    float far_d  = cluster.cam_pos_znear.w * pow(ratio, float(cz + 1u) / float(cluster.grid_z));\n\n    // World-space AABB over the tile frustum clamped to [near_d, far_d].\n    float3 aabb_min = float3(1e30);\n    float3 aabb_max = float3(-1e30);\n    for (uint i = 0u; i < 4u; ++i)\n    {\n        float3 ray = cluster_corner_ray(ndcs[i]);\n        float  fdot = max(dot(ray, cluster.view_forward_zfar.xyz), 1e-4);\n        float3 p_near = cluster.cam_pos_znear.xyz + ray * (near_d / fdot);\n        float3 p_far  = cluster.cam_pos_znear.xyz + ray * (far_d / fdot);\n        aabb_min = min(aabb_min, min(p_near, p_far));\n        aabb_max = max(aabb_max, max(p_near, p_far));\n    }\n\n    uint base  = cid * CLUSTER_LIGHT_LIST_STRIDE;\n    uint count = 0u;\n    for (uint li = 0u; li < cluster.num_lights; ++li)\n    {\n        float3 lp = lights[li].position_range.xyz;\n        float  r  = lights[li].position_range.w;\n        // Sphere vs AABB: distance from the light centre to the clamped point.\n        float3 d = lp - clamp(lp, aabb_min, aabb_max);\n        if (dot(d, d) <= r * r)\n        {\n            if (count < MAX_LIGHTS_PER_CLUSTER)\n            {\n                cluster_list[base + 1u + count] = li;\n                count += 1u;\n            }\n        }\n    }\n    cluster_list[base] = count;\n}\n";
Expand description

light_cull.slang.