awsm-renderer 0.3.1

awsm-renderer
Documentation
// froxel_walk.wgsl — the SINGLE SOURCE OF TRUTH for how a pixel enumerates the
// lights (and therefore the shadow casters) that affect it.
//
// Included by BOTH `apply_lighting.wgsl` (per-material shading) and the Plan B
// prep pass (`docs/plans/deferred-shared-prep-pass.md`). The deferred-shadow
// stage relies on this: the prep pass writes shadow visibility for the j-th
// shadowed caster, and the per-material lighting loop reads layer j for its j-th
// shadowed caster — they MUST enumerate in the identical order, so that order is
// defined here, once.
//
// CANONICAL SHADOW-CASTER ENUMERATION ORDER (slot j increments per shadowed light):
//   1. Directional prefix — flat, NOT froxel-binned (directionals hit every
//      pixel): for d in 0..get_n_directional(): light = get_light(get_directional_light_index(d)).
//   2. Per-froxel punctual — for i in 0..froxel_light_count(base): light =
//      get_light(lights_storage[base + 1u + i]), base = froxel_base_for_pixel(...).
//   A light is a shadow caster iff `light.shadow_index != SHADOW_INDEX_NONE`.
//   Both passes must apply this exact predicate in this exact order.
//
// Per-froxel slice layout (see light_culling_wgsl/bind_groups.wgsl):
//   stride = cull_params.max_per_froxel_capacity + 1; slot 0 = count (clamp at
//   read time); slots 1..1+count = light indices.

const FROXEL_TILE_PIXEL_SIZE: u32 = 16u;
const FROXEL_SLICE_COUNT: u32 = {{ froxel_slice_count }}u;
// `max_per_froxel_capacity` is a runtime field on `cull_params` so the
// auto-grow path can bump the budget without recompiling.

// Maps a fragment's screen-space pixel coordinates + view-space depth
// (positive forward) into a froxel base index in `lights_storage`. The
// returned index already accounts for the head-region offset
// (`cull_params.mesh_indices_capacity_u32`) so callers can read
// `lights_storage[base]` for the count and `lights_storage[base + 1u + i]`
// for the i-th light index.
fn froxel_base_for_pixel(pixel_xy: vec2<f32>, view_z: f32) -> u32 {
    let tile_x = u32(pixel_xy.x) / FROXEL_TILE_PIXEL_SIZE;
    let tile_y = u32(pixel_xy.y) / FROXEL_TILE_PIXEL_SIZE;
    let tile_x_clamped = min(tile_x, max(cull_params.tiles_x, 1u) - 1u);
    let tile_y_clamped = min(tile_y, max(cull_params.tiles_y, 1u) - 1u);
    // Exponential z-slice mapping inverse:
    //   s = log(z / z_near) / log(z_far / z_near)
    let z = max(view_z, cull_params.z_near);
    let s = log(z / cull_params.z_near) / max(cull_params.log_far_over_near, 1e-6);
    let z_slice = clamp(u32(s * f32(FROXEL_SLICE_COUNT)), 0u, FROXEL_SLICE_COUNT - 1u);
    let tiles_per_layer = cull_params.tiles_x * cull_params.tiles_y;
    let froxel_idx = z_slice * tiles_per_layer + tile_y_clamped * cull_params.tiles_x + tile_x_clamped;
    let stride = cull_params.max_per_froxel_capacity + 1u;
    return cull_params.mesh_indices_capacity_u32 + froxel_idx * stride;
}

// The clamped per-froxel light count for `base` (slot 0, capped at the runtime
// per-froxel capacity). Centralized so prep + shading read it identically.
fn froxel_light_count(base: u32) -> u32 {
    return min(lights_storage[base], cull_params.max_per_froxel_capacity);
}