myth_render 0.3.0

Core rendering system for the Myth engine (render graph, pipeline, GPU management).
Documentation
{$ include 'core/full_screen_vertex' $}

{{ struct_definitions }}
{{ binding_code }}
{{ scene_lighting_structs }}

@group(1) @binding(0) var t_raw_reflection: texture_2d<f32>;
@group(1) @binding(1) var t_history_reflection: texture_2d<f32>;
@group(1) @binding(2) var t_depth: texture_depth_2d;
@group(1) @binding(3) var t_normal: texture_2d<f32>;
@group(1) @binding(4) var t_history_meta: texture_2d<f32>;
@group(1) @binding(5) var t_velocity: texture_2d<f32>;
@group(1) @binding(6) var t_material_data: texture_2d<f32>;
@group(1) @binding(7) var s_linear: sampler;
@group(1) @binding(8) var s_point: sampler;
@group(1) @binding(9) var<uniform> u_ssr: SsrUniforms;

struct TemporalOutput {
    @location(0) reflection: vec4<f32>,
    @location(1) history_meta: vec4<f32>,
};

struct HistorySample {
    color: vec3<f32>,
    confidence: f32,
    valid: bool,
};

fn saturate(v: f32) -> f32 {
    return clamp(v, 0.0, 1.0);
}

fn luminance(color: vec3<f32>) -> f32 {
    return dot(max(color, vec3<f32>(0.0)), vec3<f32>(0.2126, 0.7152, 0.0722));
}

fn perceptual_luma(color: vec3<f32>) -> f32 {
    return log2(1.0 + luminance(color));
}

fn unpack_view_normal(packed: vec4<f32>) -> vec3<f32> {
    let raw = packed.xyz * 2.0 - 1.0;
    return normalize(select(vec3<f32>(0.0, 0.0, 1.0), raw, dot(raw, raw) > 1e-5));
}

fn sign_not_zero(v: f32) -> f32 {
    return select(-1.0, 1.0, v >= 0.0);
}

fn oct_encode(normal: vec3<f32>) -> vec2<f32> {
    let inv_l1 = 1.0 / max(abs(normal.x) + abs(normal.y) + abs(normal.z), 1e-4);
    var encoded = normal.xy * inv_l1;

    if (normal.z < 0.0) {
        encoded = vec2<f32>(
            (1.0 - abs(encoded.y)) * sign_not_zero(encoded.x),
            (1.0 - abs(encoded.x)) * sign_not_zero(encoded.y)
        );
    }

    return encoded * 0.5 + 0.5;
}

fn oct_decode(encoded: vec2<f32>) -> vec3<f32> {
    let f = encoded * 2.0 - 1.0;
    var normal = vec3<f32>(f.x, f.y, 1.0 - abs(f.x) - abs(f.y));

    if (normal.z < 0.0) {
        let old_xy = normal.xy;
        normal.x = (1.0 - abs(old_xy.y)) * sign_not_zero(old_xy.x);
        normal.y = (1.0 - abs(old_xy.x)) * sign_not_zero(old_xy.y);
    }

    return normalize(normal);
}

fn linearize_depth(z: f32) -> f32 {
    return u_ssr.temporal_params.z / max(z, 0.0001);
}

fn jitter_uv_offset() -> vec2<f32> {
    return vec2<f32>(0.5, -0.5) * u_render_state.jitter;
}

fn prev_jitter_uv_offset() -> vec2<f32> {
    return vec2<f32>(0.5, -0.5) * u_render_state.prev_jitter;
}

fn jitter_history_uv(uv: vec2<f32>) -> vec2<f32> {
    return uv + prev_jitter_uv_offset() - jitter_uv_offset();
}

fn resolve_full_res_coord(pixel: vec2<i32>) -> vec2<i32> {
    let full_extent = vec2<i32>(i32(u_ssr.full_resolution.x), i32(u_ssr.full_resolution.y));
    let scale = select(vec2<i32>(1, 1), vec2<i32>(2, 2), u_ssr.denoise_params.y != 0u);
    return clamp(pixel * scale, vec2<i32>(0, 0), full_extent - vec2<i32>(1, 1));
}

fn full_res_coord_to_uv(coord: vec2<i32>) -> vec2<f32> {
    return (vec2<f32>(coord) + vec2<f32>(0.5, 0.5)) / u_ssr.full_resolution.xy;
}

fn sample_dilated_velocity(full_res_coord: vec2<i32>) -> vec2<f32> {
    let full_extent = vec2<i32>(i32(u_ssr.full_resolution.x), i32(u_ssr.full_resolution.y));
    let max_coord = full_extent - vec2<i32>(1, 1);

    var closest_depth = textureLoad(t_depth, clamp(full_res_coord, vec2<i32>(0, 0), max_coord), 0);
    var closest_coord = clamp(full_res_coord, vec2<i32>(0, 0), max_coord);

    for (var y: i32 = -1; y <= 1; y++) {
        for (var x: i32 = -1; x <= 1; x++) {
            let coord = clamp(full_res_coord + vec2<i32>(x, y), vec2<i32>(0, 0), max_coord);
            let depth = textureLoad(t_depth, coord, 0);
            if (depth > closest_depth) {
                closest_depth = depth;
                closest_coord = coord;
            }
        }
    }

    return textureLoad(t_velocity, closest_coord, 0).rg;
}

fn get_safe_raw_reflection(pixel: vec2<i32>, extent: vec2<i32>) -> vec4<f32> {
    let coord = clamp(pixel, vec2<i32>(0, 0), extent - vec2<i32>(1, 1));
    let raw = textureLoad(t_raw_reflection, coord, 0);
    let luma_limit = u_ssr.temporal_params.y;
    let raw_luma = luminance(raw.rgb);
    if (luma_limit <= 0.0 || raw_luma <= luma_limit) {
        return raw;
    }

    return vec4<f32>(raw.rgb * (luma_limit / raw_luma), raw.a);
}

fn clip_towards_aabb_center(
    history_color: vec3<f32>,
    box_center: vec3<f32>,
    box_extent: vec3<f32>,
) -> vec3<f32> {
    let diff = history_color - box_center;
    let safe_extent = max(box_extent, vec3<f32>(1e-4));
    let ratio = max(
        abs(diff.x) / safe_extent.x,
        max(abs(diff.y) / safe_extent.y, abs(diff.z) / safe_extent.z)
    );

    if (ratio > 1.0) {
        return box_center + diff / ratio;
    }

    return history_color;
}

fn sample_valid_history(
    validation_world_normal: vec3<f32>,
    expected_prev_linear: f32,
    current_roughness: f32,
    history_uv: vec2<f32>,
) -> HistorySample {
    let history_extent = vec2<i32>(textureDimensions(t_history_reflection));
    let max_coord = history_extent - vec2<i32>(1, 1);
    let sample_pos = history_uv * vec2<f32>(history_extent) - vec2<f32>(0.5, 0.5);
    let base_coord = vec2<i32>(floor(sample_pos));
    let frac = fract(sample_pos);

    let normal_threshold = u_ssr.reprojection_params.y;
    let depth_threshold = u_ssr.reprojection_params.z;

    var color_sum = vec3<f32>(0.0);
    var confidence_sum = 0.0;
    var weight_sum = 0.0;

    for (var y: i32 = 0; y <= 1; y++) {
        for (var x: i32 = 0; x <= 1; x++) {
            let coord = clamp(base_coord + vec2<i32>(x, y), vec2<i32>(0, 0), max_coord);
            let hist_meta = textureLoad(t_history_meta, coord, 0);

            let hist_normal = oct_decode(hist_meta.xy);
            let hist_linear = hist_meta.z;
            let hist_roughness = hist_meta.w;

            if (dot(validation_world_normal, hist_normal) < normal_threshold
                || abs(expected_prev_linear - hist_linear)
                    > depth_threshold * max(expected_prev_linear, 1.0)
                || abs(current_roughness - hist_roughness) > u_ssr.reprojection_params.w) {
                continue;
            }

            let hist_reflection = textureLoad(t_history_reflection, coord, 0);
            if (hist_reflection.a <= 1e-4) {
                continue;
            }

            let bilinear_weight_x = select(1.0 - frac.x, frac.x, x == 1);
            let bilinear_weight_y = select(1.0 - frac.y, frac.y, y == 1);
            let weight = bilinear_weight_x * bilinear_weight_y;

            color_sum += hist_reflection.rgb * weight;
            confidence_sum += hist_reflection.a * weight;
            weight_sum += weight;
        }
    }

    if (weight_sum <= 1e-4) {
        return HistorySample(vec3<f32>(0.0), 0.0, false);
    }

    return HistorySample(color_sum / weight_sum, confidence_sum / weight_sum, true);
}

@fragment
fn fs_main(in: VertexOutput) -> TemporalOutput {
    var out: TemporalOutput;
    out.reflection = vec4<f32>(0.0);
    out.history_meta = vec4<f32>(0.0);

    let pixel = vec2<i32>(in.position.xy);
    let raw_extent = vec2<i32>(textureDimensions(t_raw_reflection));
    let full_res_coord = resolve_full_res_coord(pixel);
    let surface_uv = full_res_coord_to_uv(full_res_coord);
    let current_depth = textureLoad(t_depth, full_res_coord, 0);
    let current_normal_packed = textureLoad(t_normal, full_res_coord, 0);
    if (current_depth <= 0.0 || current_normal_packed.a < 0.5) {
        return out;
    }

    let current_material = textureLoad(t_material_data, full_res_coord, 0);
    let current_roughness = current_material.a;
    let current_normal = unpack_view_normal(current_normal_packed);
    let current_linear = linearize_depth(current_depth);
    out.history_meta = vec4<f32>(oct_encode(current_normal), current_linear, current_roughness);

    if (current_roughness > u_ssr.shading_params.x) {
        return out;
    }

    let current_raw = get_safe_raw_reflection(pixel, raw_extent);

    var moment1 = vec3<f32>(0.0);
    var moment2 = vec3<f32>(0.0);
    var valid_weight = 0.0;
    for (var y: i32 = -1; y <= 1; y++) {
        for (var x: i32 = -1; x <= 1; x++) {
            let sample_coord = clamp(pixel + vec2<i32>(x, y), vec2<i32>(0, 0), raw_extent - vec2<i32>(1, 1));
            let sample_value = get_safe_raw_reflection(sample_coord, raw_extent);
            if (sample_value.a <= 1e-4) {
                continue;
            }

            moment1 += sample_value.rgb;
            moment2 += sample_value.rgb * sample_value.rgb;
            valid_weight += 1.0;
        }
    }

    var mean = current_raw.rgb;
    var std_dev = vec3<f32>(0.0);
    if (valid_weight > 0.0) {
        mean = moment1 / valid_weight;
        let variance = max(moment2 / valid_weight - mean * mean, vec3<f32>(0.0));
        // Add a small absolute variance floor so the AABB never collapses to a
        // point at reflection edges where only 1-2 of the 9 neighbourhood pixels
        // have a valid hit.  Without this the AABB box can be far too tight and
        // the temporal clipper aggressively rejects any history, producing single-
        // pixel edge flicker during camera motion.
        std_dev = sqrt(variance) + max(mean * 0.05, vec3<f32>(0.005));
    }

    let camera_cut = (u_ssr.frame_params.w & 2u) != 0u;

    var reflection = current_raw;
    var accepted_history = false;

    if (!camera_cut && (u_ssr.frame_params.w & 1u) != 0u) {
        let velocity = sample_dilated_velocity(full_res_coord);
        let history_uv = jitter_history_uv(surface_uv - velocity);
        if (history_uv.x >= 0.0 && history_uv.x <= 1.0
            && history_uv.y >= 0.0 && history_uv.y <= 1.0) {
            let history = sample_valid_history(
                current_normal,
                current_linear,
                current_roughness,
                history_uv,
            );

            if (history.valid) {
                let roughness_ratio = clamp(
                    current_roughness / max(u_ssr.shading_params.x, 1e-4),
                    0.0,
                    1.0
                );
                let current_weight = mix(
                    u_ssr.reprojection_params.x,
                    max(u_ssr.reprojection_params.x * 0.45, 0.04),
                    roughness_ratio
                );
                let box_extent = max(
                    std_dev * u_ssr.temporal_params.x,
                    vec3<f32>(1e-4)
                );
                let clipped_history = clip_towards_aabb_center(history.color, mean, box_extent);
                reflection = vec4<f32>(
                    mix(clipped_history, current_raw.rgb, current_weight),
                    mix(history.confidence, current_raw.a, current_weight)
                );
                accepted_history = true;
            }
        }
    }

    if (!accepted_history && current_raw.a <= 1e-4) {
        reflection = vec4<f32>(0.0);
    }

    out.reflection = reflection;
    return out;
}