gizmo-renderer 0.9.1

A custom ECS and physics engine aimed for realistic simulations.
Documentation
// SSGI Temporal Accumulation
//
// Denoises the 1-spp raymarched GI by blending the current (frame-varying) noisy
// estimate with a world-reprojected history — the same scheme as taa.wgsl, but run
// at SSGI's HALF resolution and on the raw GI signal (before the spatial blur).
//
// The raymarch (ssgi.wgsl) now rotates its ray seed every frame, so each frame is an
// independent Monte-Carlo sample of the same lighting; averaging ~1/alpha frames of
// reprojected history converges the salt-and-pepper grain to a smooth solution.

struct SsgiTemporalParams {
    prev_view_proj: mat4x4<f32>,  // previous frame's UNJITTERED view-projection
    alpha:          f32,          // blend weight: 0 = full history, 1 = full current
    // NOTE: three scalar pads (NOT a vec3) — a vec3 has align-16 and would push the
    // struct to 96 bytes while the Rust `[f32; 3]` mirror is 80, tripping the uniform
    // min_binding_size validation. Scalars keep both sides at 80 bytes.
    // Was three pads. They now carry the camera position, because the world-position G-buffer is
    // stored relative to it (see gbuffer.wgsl) and the reprojection below multiplies by a *world*
    // matrix. Still three scalars for the reason above: a `vec3` would align to 16 and break the
    // 80-byte mirror.
    camera_x:       f32,
    camera_y:       f32,
    camera_z:       f32,
};

@group(0) @binding(0) var<uniform> params: SsgiTemporalParams;
@group(0) @binding(1) var t_current:  texture_2d<f32>;  // raw SSGI this frame     (half-res)
@group(0) @binding(2) var t_history:  texture_2d<f32>;  // accumulated SSGI last frame (half-res)
@group(0) @binding(3) var t_position_rel_camera: texture_2d<f32>;  // world-position G-buffer (full-res)
@group(0) @binding(4) var s_linear:   sampler;          // bilinear — for history reprojection

@vertex
fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4<f32> {
    var p = array<vec2<f32>, 3>(vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
    return vec4(p[vi], 0.0, 1.0);
}

@fragment
fn fs_resolve(@builtin(position) frag_coord: vec4<f32>) -> @location(0) vec4<f32> {
    let half_dims = vec2<f32>(textureDimensions(t_current));
    let iuv_half  = vec2<i32>(i32(frag_coord.x), i32(frag_coord.y));

    let current = textureLoad(t_current, iuv_half, 0).rgb;

    // World position for THIS half-res pixel comes from the full-res G-buffer at the
    // matching texel — the raymarch reads it the same way (iuv = frag_coord * 2).
    let iuv_full = iuv_half * 2;
    let pos_samp = textureLoad(t_position_rel_camera, iuv_full, 0);

    // ── Reproject: where did this world point project last frame? ────────────────
    // NO early returns before the textureSample below — WGSL requires derivative-taking
    // samples to run in uniform control flow. Track validity in a flag instead and fold
    // it into the blend weight at the end.
    var history_uv = frag_coord.xy / half_dims;   // fallback: same pixel
    var history_valid = pos_samp.w >= 0.5;        // sky / unwritten → no GI history
    let camera_pos = vec3<f32>(params.camera_x, params.camera_y, params.camera_z);
    let prev_clip = params.prev_view_proj * vec4(pos_samp.xyz + camera_pos, 1.0);
    if (prev_clip.w > 0.001) {
        let ndc = prev_clip.xy / prev_clip.w;
        history_uv = vec2(ndc.x * 0.5 + 0.5, ndc.y * -0.5 + 0.5);
    } else {
        history_valid = false;                    // behind previous camera
    }
    if (history_uv.x < 0.0 || history_uv.x > 1.0 || history_uv.y < 0.0 || history_uv.y > 1.0) {
        history_valid = false;                    // off-screen last frame (disocclusion)
    }

    // Re-center the reprojected UV onto the half-res write grid. World position is read
    // at the 2×2 block's top-left full-res texel (iuv_full = iuv_half*2), so its back-
    // projection lands a quarter-texel toward top-left of where THIS half-res pixel writes;
    // without this the feedback loop bakes in a permanent quarter-texel blur/shift.
    history_uv += 0.25 / half_dims;

    // Unconditional sample (uniform control flow); uv is clamped so an invalid reproject
    // reads a safe edge texel — its contribution is discarded via history_valid below.
    var history = textureSample(t_history, s_linear, clamp(history_uv, vec2(0.0), vec2(1.0))).rgb;

    // ── History rejection ────────────────────────────────────────────────────────
    // A neighbourhood colour-clamp (TAA-style) actively FIGHTS convergence for a 1-spp
    // GI signal: every frame it drags the accumulated (smooth) history back toward the
    // current frame's biased local diagonal band, so the bands never average out. For a
    // primarily-static scene the correct denoiser is a straight exponential accumulation,
    // rejecting history ONLY on true disocclusion (reprojection off-screen / behind
    // camera), which `history_valid` already encodes. Motion ghosting on the low-frequency
    // half-res GI is minor and is the acceptable trade for actually converging.
    //
    // (Deliberately NO colour clamp here — see git history for the variance-clip variant.)

    // Invalid history → blend weight 1.0 (take the fresh estimate, ignore history).
    let a = select(1.0, params.alpha, history_valid);
    let resolved = mix(history, current, a);
    return vec4(resolved, 1.0);
}