awsm-renderer 0.4.1

awsm-renderer
Documentation
// light_access.wgsl — light-data ACCESSOR FUNCTIONS (get_lights_info / get_light
// / light_sample / …). The STRUCTS they return live in `light_access_types.wgsl`
// (always included — bind-group ABI).
//
// GATED on `inc.light_access` (Phase 4 of docs/plans/material-optimizations.md).
// A material — or a whole scene known to have no lights — can opt out of lighting
// completely: these accessors are dropped, and the per-pixel shade entry points
// gate their `get_lights_info()` calls + `lights_info` params to match. PBR/Toon
// declare LIGHT_ACCESS so they keep them; an unlit/flipbook/no-light custom
// material does not.
//
// (Historical note: this file was previously "deliberately not skinny-gated" on
// the reasoning that the structs are ABI and the accessors are cheap. That
// predated the granular include splits + the explicit lighting opt-out; the
// structs are now split into light_access_types.wgsl so the ABI stays intact
// while the accessor bodies gate out.)

fn get_lights_info() -> LightsInfo {
    // expects `lights_info` is global LightsInfoPacked
    return LightsInfo(
        lights_info.data.x,
        IblInfo(
            lights_info.data.y,
            lights_info.data.z
        )
    );
}

fn get_light(i: u32) -> Light {
    // expects `lights` is global array<LightPacked>
    let p = lights[i];
    return Light(
        u32(p.kind_outer_pad.x),
        p.color_intensity.xyz,
        p.color_intensity.w,
        p.pos_range.xyz,
        p.pos_range.w,
        p.dir_inner.xyz,
        p.dir_inner.w,
        p.kind_outer_pad.y,
        bitcast<u32>(p.kind_outer_pad.z),
    );
}

// Count of directional lights this frame (≤ 8). Reads the global
// `lights_info` uniform directly so the shading functions (whose
// `lights_info` *parameter* shadows the global) can still reach it.
fn get_n_directional() -> u32 {
    return lights_info.data.w;
}

// Packed-array index of the `d`-th directional light (`d < get_n_directional()`).
// Pair with `get_light` to walk only directionals instead of scanning all
// `n_lights` per pixel.
fn get_directional_light_index(d: u32) -> u32 {
    return lights_info.directional[d >> 2u][d & 3u];
}

// Debug: map an applied-punctual-light count to a jet colormap
// (black = 0, blue = few, green = mid, red = many; 64+ clamps to red).
// Used by the `cull_params.debug_light_heatmap` visualization to inspect
// froxel occupancy / cull behaviour. Tune the 64.0 reference to taste.
fn light_count_heatmap(count: u32) -> vec3<f32> {
    if (count == 0u) {
        return vec3<f32>(0.0, 0.0, 0.0);
    }
    let t = saturate(f32(count) / 64.0);
    let r = saturate(1.5 - abs(4.0 * t - 3.0));
    let g = saturate(1.5 - abs(4.0 * t - 2.0));
    let b = saturate(1.5 - abs(4.0 * t - 1.0));
    return vec3<f32>(r, g, b);
}

// KHR_lights_punctual unit-handling note:
//   * Directional intensity is `lux` (lm/m²).
//   * Point/spot intensity is `candela` (lm/sr).
// We treat both as already-radiometric: `radiance = color * intensity * attenuation`.
// That matches what the glTF Sample Renderer does and is what most assets
// are authored for, but it is NOT a proper photometric → radiometric
// conversion. A tonemapped, exposure-aware renderer can hide the
// difference; a strictly physical pipeline would multiply each kind by
// the appropriate `683 lm/W` luminous-efficacy scale and divide by the
// shaded surface's projected area.
fn light_sample(light:Light, normal: vec3<f32>, world_position: vec3<f32>) -> LightSample {
    var light_dir: vec3<f32> = vec3<f32>(0.0, 0.0, 0.0);
    var radiance: vec3<f32> = vec3<f32>(0.0, 0.0, 0.0);
    var n_dot_l: f32 = 0.0;

    switch (light.kind) {
        case 0u: {
            // no light, skip
        }
        case 1u: { // Directional
            light_dir = normalize(-light.direction); // surface -> light
            radiance = light.color * light.intensity;
            n_dot_l = max(dot(normal, light_dir), 0.0);
        }
        case 2u: { // Point
            let surface_to_light = light.position - world_position;
            let dist = length(surface_to_light);
            light_dir = surface_to_light / max(dist, 1e-6); // surface -> light (guard dist==0)
            let attenuation = inverse_square(light.range, dist);
            radiance = light.color * light.intensity * attenuation;
            n_dot_l = max(dot(normal, light_dir), 0.0);
        }
        case 3u: { // Spot
            let surface_to_light = light.position - world_position;
            let dist = length(surface_to_light);
            light_dir = surface_to_light / max(dist, 1e-6); // surface -> light (guard dist==0)
            let cos_l = dot(light_dir, -normalize(light.direction));
            let spot = spot_falloff(light.inner_cone, light.outer_cone, cos_l);
            let attenuation = inverse_square(light.range, dist) * spot;
            radiance = light.color * light.intensity * attenuation;
            n_dot_l = max(dot(normal, light_dir), 0.0);
        }
        default: { // unexpected
        }
    }

    return LightSample(
        normal,
        n_dot_l,
        light_dir,
        radiance,
    );
}

// Orient the surface normal toward the light for shadow-bias purposes.
// The shadow normal-offset bias (`world_pos + normal * normal_bias`) assumes
// the lit side faces the light. For a diffuse-transmissive surface lit from
// BEHIND (n·l < 0, driving the back-transmission lobe), the front normal
// would push the sample into self-shadow and extinguish the transmission.
// Flipping the normal to face the light fixes the bias for that lobe. For
// every other case this is a no-op: front-lit fragments keep the front
// normal, and a back-lit *non*-transmissive fragment has `brdf_direct == 0`,
// so the (changed) visibility multiplies zero — bit-identical result.
fn shadow_normal_toward_light(surface_normal: vec3<f32>, light_dir: vec3<f32>) -> vec3<f32> {
    return surface_normal * select(1.0, -1.0, dot(surface_normal, light_dir) < 0.0);
}

// spot light mask (smooth edge)
fn spot_falloff(inner_cos: f32, outer_cos: f32, cos_l: f32) -> f32 {
    let smoothed = saturate((cos_l - outer_cos) / (inner_cos - outer_cos));
    return smoothed * smoothed;
}