awsm-renderer 0.3.0

awsm-renderer
Documentation
// light_access.wgsl — light-data access shared by every lit shading model
// (PBR + toon). Split out of the former lights.wgsl so non-PBR materials can
// walk the light list WITHOUT pulling in apply_lighting + the PBR BRDF.
//
// DELIBERATELY NOT skinny-gated, and included in every opaque pipeline. The
// packed structs (LightsInfoPacked/LightPacked) are part of the bind-group ABI
// (bind_groups.wgsl declares the bindings with these types), so — exactly like
// the bindings themselves — they must always be present. The accessor functions
// below are ~80 lines of trivial unpack/switch code: their compile cost is
// negligible next to the gated brdf.wgsl (889 lines of GGX/Fresnel/IBL math),
// and gating them would only entangle the per-pixel shade entry points (which
// take `LightsInfo` for every material) for no real win. So this whole file is
// cheap, always-present shared infrastructure.

// `data`: x = n_lights, y = prefiltered-env mip count, z = irradiance mip
// count, w = n_directional (count of directional lights this frame, ≤ 8).
// `directional`: packed-array indices of the (≤ 8) directional lights.
// The shading paths use these to walk *only* the directionals in
// O(n_directional) instead of scanning all `n_lights` per pixel — the
// latter is catastrophic when a scene has hundreds/thousands of punctuals
// (each pixel would skip over every punctual just to find the sun).
struct LightsInfoPacked {
    data: vec4<u32>,
    directional: array<vec4<u32>, 2>,
}

struct LightsInfo {
    n_lights: u32,
    ibl: IblInfo
}

struct IblInfo {
    prefiltered_env_mip_count: u32,
    irradiance_mip_count: u32,
}

struct LightPacked {
  // pos.xyz + range
  pos_range: vec4<f32>,
  // dir.xyz + inner_cone
  dir_inner: vec4<f32>,
  // color.rgb + intensity
  color_intensity: vec4<f32>,
  // kind (as uint) + outer_cone + shadow_index (bit-cast u32) + 1 pad
  kind_outer_pad: vec4<f32>,
};

struct Light {
    kind: u32,
    color: vec3<f32>,
    intensity: f32,
    position: vec3<f32>,
    range: f32,
    direction: vec3<f32>,
    inner_cone: f32,
    outer_cone: f32,
    // Index into `shadow_descriptors`. `0xFFFFFFFF` = no shadow.
    shadow_index: u32,
};

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

// The result of sampling one light at a surface point — the generic,
// shading-model-agnostic lighting primitive. `light_sample()` (below) computes
// it for any light kind (directional/point/spot) with NO PBR/BRDF math, so
// custom materials can do Lambert / Phong / toon / whatever WITHOUT pulling in
// brdf.wgsl or apply_lighting.wgsl (this file is always present in every opaque
// + transparent pipeline). The PBR path (`brdf_direct`) consumes the same
// struct; it's just one consumer.
//   - light_dir : normalized surface->light direction
//   - radiance  : color * intensity * attenuation (spot/range already applied)
//   - n_dot_l   : saturate(dot(normal, light_dir)) — the Lambert term
// Lambert diffuse:  radiance * n_dot_l
// Phong specular:   pow(max(dot(reflect(-light_dir, normal), view_dir), 0), s) * radiance
struct LightSample {
    normal: vec3<f32>,
    n_dot_l: f32,
    light_dir: vec3<f32>,
    radiance: vec3<f32>,
};

// 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;
}