viewport-lib 0.19.0

3D viewport rendering library
Documentation
// Shared scene-lighting evaluation for every lit pipeline.
//
// Defines the canonical `SingleLight` struct, the `Lights` header struct
// (binding 3 of group 0), the dynamically-sized storage buffer of lights
// (binding 13 of group 0), the cluster grid bindings (14, 15, 16), and the
// per-fragment helper:
//
//   apply_scene_lighting(N, base_colour, two_sided, world_pos, lights) -> vec3<f32>
//
// The helper returns `base_colour * (hemisphere_ambient + light_sum)` where
// `hemisphere_ambient` is the sky/ground blend on `N.z` and `light_sum`
// accumulates direct contribution from the per-cluster light list. When the
// active light count is small enough that cluster overhead would dominate,
// the host sets `cluster_grid_uniform.screen.z != 0` and the helper iterates
// the full `lights_storage` array directly.
//
// The world-to-view matrix is carried inside the cluster grid uniform so the
// helper can derive the view-space fragment position itself; consumers don't
// have to extend their per-shader `Camera` struct to expose it.
//
// Consumers must:
//   - Bind the `Lights` header uniform at `@group(0) @binding(3)`.
//   - Bind the per-light storage buffer at `@group(0) @binding(13)`.
//   - Bind the cluster grid uniform / cells / index list at bindings 14, 15,
//     and 16 (all on group 0).
//   - Remove any local `SingleLight` and `Lights` struct definitions and
//     `// #include "scene_lighting.wgsl"` near the top of the file instead.

struct SingleLight {
    light_view_proj:   mat4x4<f32>,
    pos_or_dir:        vec3<f32>,
    light_type:        u32,
    colour:            vec3<f32>,
    intensity:         f32,
    range:             f32,
    inner_angle:       f32,
    outer_angle:       f32,
    spot_direction:    vec3<f32>,
    point_shadow_slot: i32,
    point_shadow_near: f32,
    _pad0:             f32,
    _pad1:             f32,
};

struct Lights {
    count:                u32,
    shadow_bias:          f32,
    shadows_enabled:      u32,
    debug_vis_mode:       u32,
    sky_colour:            vec3<f32>,
    hemisphere_intensity: f32,
    ground_colour:         vec3<f32>,
    debug_vis_scale:      f32,
    ibl_enabled:          u32,
    ibl_intensity:        f32,
    ibl_rotation:         f32,
    show_skybox:          u32,
    debug_vis_split_x:    f32,
    _pad_dbg_a:           u32,
    _pad_dbg_b:           u32,
    _pad_dbg_c:           u32,
};

struct ClusterGrid {
    dimensions: vec4<u32>,
    depth:      vec4<f32>,
    screen:     vec4<f32>,
    proj_scale: vec4<f32>,
    view:       mat4x4<f32>,
};

struct ClusterCell {
    offset:         u32,
    count:          u32,
    punctual_count: u32,
    _pad:           u32,
};

@group(0) @binding(13) var<storage, read> lights_storage:        array<SingleLight>;
@group(0) @binding(14) var<uniform>       cluster_grid_uniform:  ClusterGrid;
@group(0) @binding(15) var<storage, read> cluster_cells:         array<ClusterCell>;
@group(0) @binding(16) var<storage, read> cluster_light_indices: array<u32>;
@group(0) @binding(17) var                point_shadow_cube_tex: texture_depth_cube_array;


fn cluster_index_for(view_pos: vec3<f32>) -> u32 {
    let grid = cluster_grid_uniform;
    let near = grid.depth.x;
    let far  = grid.depth.y;
    let log_ratio = grid.depth.z;

    // View-space z is negative looking down -Z. Distance from camera.
    let z_abs = clamp(-view_pos.z, near, far);
    let nz = f32(grid.dimensions.z);
    let zi = u32(clamp(log(z_abs / near) / log_ratio * nz, 0.0, nz - 1.0));

    // Perspective-divide x, y into NDC space.
    let denom = max(z_abs, 1e-5);
    let x_ndc = view_pos.x / (denom * grid.proj_scale.x);
    let y_ndc = view_pos.y / (denom * grid.proj_scale.y);
    let nx = f32(grid.dimensions.x);
    let ny = f32(grid.dimensions.y);
    let xi = u32(clamp((x_ndc + 1.0) * 0.5 * nx, 0.0, nx - 1.0));
    let yi = u32(clamp((y_ndc + 1.0) * 0.5 * ny, 0.0, ny - 1.0));

    return zi * (grid.dimensions.x * grid.dimensions.y)
         + yi *  grid.dimensions.x
         + xi;
}

// Iteration bounds for the per-fragment light loop. `fallback` is true when
// the host short-circuited the cluster build : in that case `start` is 0 and
// `count` is the total active light count, and `cluster_light_global` returns
// the loop index directly.
struct LightRange {
    fallback: bool,
    start:    u32,
    count:    u32,
};

fn cluster_light_range(world_pos: vec3<f32>, total_count: u32) -> LightRange {
    var r: LightRange;
    r.fallback = cluster_grid_uniform.screen.z != 0.0;
    if r.fallback {
        r.start = 0u;
        r.count = total_count;
    } else {
        let view_pos = (cluster_grid_uniform.view * vec4<f32>(world_pos, 1.0)).xyz;
        let cid = cluster_index_for(view_pos);
        let cell = cluster_cells[cid];
        r.start = cell.offset;
        r.count = cell.count;
    }
    return r;
}

fn cluster_light_global(range: LightRange, j: u32) -> u32 {
    if range.fallback {
        return j;
    }
    return cluster_light_indices[range.start + j];
}

fn evaluate_light(l: SingleLight, world_pos: vec3<f32>) -> array<vec3<f32>, 2> {
    var L: vec3<f32>;
    var radiance: vec3<f32>;
    if l.light_type == 0u {
        L = normalize(l.pos_or_dir);
        radiance = l.colour * l.intensity;
    } else if l.light_type == 1u {
        let to_light = l.pos_or_dir - world_pos;
        let dist = length(to_light);
        L = to_light / max(dist, 0.0001);
        let falloff = clamp(1.0 - dist / l.range, 0.0, 1.0);
        radiance = l.colour * l.intensity * falloff * falloff;
    } else {
        let to_light = l.pos_or_dir - world_pos;
        let dist = length(to_light);
        L = to_light / max(dist, 0.0001);
        let dist_falloff = clamp(1.0 - dist / l.range, 0.0, 1.0);
        let spot_dir = normalize(l.spot_direction);
        let cos_angle = dot(-L, spot_dir);
        let cos_outer = cos(l.outer_angle);
        let cos_inner = cos(l.inner_angle);
        let cone_att = clamp(
            (cos_angle - cos_outer) / max(cos_inner - cos_outer, 0.0001),
            0.0, 1.0,
        );
        radiance = l.colour * l.intensity * dist_falloff * dist_falloff * cone_att;
    }
    return array<vec3<f32>, 2>(L, radiance);
}

fn apply_scene_lighting(
    normal: vec3<f32>,
    base_colour: vec3<f32>,
    two_sided: bool,
    world_pos: vec3<f32>,
    lights: Lights,
) -> vec3<f32> {
    // Hemisphere ambient. Z-up world: N.z=+1 reads pure sky, N.z=-1 reads pure
    // ground. Scaled by `hemisphere_intensity` so `hemisphere_intensity = 0`
    // disables the term and items rely entirely on direct lights.
    let up_weight = clamp(normal.z * 0.5 + 0.5, 0.0, 1.0);
    let ambient = mix(lights.ground_colour, lights.sky_colour, up_weight)
                  * lights.hemisphere_intensity;

    var direct = vec3<f32>(0.0);
    let range = cluster_light_range(world_pos, lights.count);
    for (var j: u32 = 0u; j < range.count; j = j + 1u) {
        let idx = cluster_light_global(range, j);
        let r = evaluate_light(lights_storage[idx], world_pos);
        let raw = dot(normal, r[0]);
        let n_dot_l = select(max(raw, 0.0), abs(raw), two_sided);
        direct = direct + r[1] * n_dot_l;
    }
    return base_colour * (ambient + direct);
}