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

@group(0) @binding(0) var t_raw_indirect: texture_2d<f32>;
@group(0) @binding(1) var t_history_indirect: texture_2d<f32>;
@group(0) @binding(2) var t_depth: texture_depth_2d;
@group(0) @binding(3) var t_normal: texture_2d<f32>;
@group(0) @binding(4) var t_history_meta: texture_2d<f32>;
@group(0) @binding(5) var t_velocity: texture_2d<f32>;
@group(0) @binding(6) var s_linear: sampler;
@group(0) @binding(7) var s_point: sampler;
@group(0) @binding(8) var<uniform> u_ssgi: SsgiUniforms;

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

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

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 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 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_ssgi.temporal_params.z / max(z, 0.0001);
}

fn get_safe_raw_color(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_indirect, coord, 0);
    let luma_limit = u_ssgi.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(
    current_normal: vec3<f32>,
    current_linear: f32,
    history_uv: vec2<f32>,
) -> HistorySample {
    let history_extent = vec2<i32>(textureDimensions(t_history_indirect));
    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);

    var color_sum = vec3<f32>(0.0);
    var history_len_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;

            if (dot(current_normal, hist_normal) < u_ssgi.reprojection_params.y
                || abs(current_linear - hist_linear) > u_ssgi.reprojection_params.z * max(current_linear, 1.0)) {
                continue;
            }

            let hist_indirect = textureLoad(t_history_indirect, coord, 0);
            if (hist_indirect.a <= 0.0) {
                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_indirect.rgb * weight;
            history_len_sum += hist_indirect.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, history_len_sum / weight_sum, true);
}

fn resolve_full_uv(half_pixel: vec2<u32>) -> vec2<f32> {
    if (u_ssgi.frame_params.z == 0u) {
        return (vec2<f32>(half_pixel) + vec2<f32>(0.5, 0.5)) * u_ssgi.half_resolution.zw;
    }

    let frame = u_ssgi.frame_params.x;
    let base = vec2<f32>(half_pixel * 2u);
    let offset_x = f32((frame & 1u) ^ ((frame & 2u) >> 1u));
    let offset_y = f32((frame & 2u) >> 1u);
    let offset = vec2<f32>(offset_x + 0.5, offset_y + 0.5);
    return (base + offset) * u_ssgi.full_resolution.zw;
}

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

    let half_pixel = vec2<u32>(in.position.xy);
    let raw_extent = vec2<i32>(textureDimensions(t_raw_indirect));
    let raw_pixel = clamp(vec2<i32>(half_pixel), vec2<i32>(0, 0), raw_extent - vec2<i32>(1, 1));
    let full_uv = resolve_full_uv(half_pixel);
    let current_depth = textureSampleLevel(t_depth, s_point, full_uv, 0u);
    let current_normal_packed = textureSampleLevel(t_normal, s_point, full_uv, 0.0);

    if (current_depth <= 0.0 || current_normal_packed.a < 0.5) {
        return out;
    }

    let current_normal = unpack_view_normal(current_normal_packed);
    let current_linear = linearize_depth(current_depth);
    let camera_cut = (u_ssgi.frame_params.w & 4u) != 0u;

    let current_raw = get_safe_raw_color(raw_pixel, raw_extent);

    var moment1 = vec3<f32>(0.0);
    var moment2 = vec3<f32>(0.0);
    var luma_m1 = 0.0;
    var luma_m2 = 0.0;

    for (var y: i32 = -1; y <= 1; y++) {
        for (var x: i32 = -1; x <= 1; x++) {
            let sample_coord = clamp(raw_pixel + vec2<i32>(x, y), vec2<i32>(0, 0), raw_extent - vec2<i32>(1, 1));
            let sample_value = get_safe_raw_color(sample_coord, raw_extent).rgb;
            moment1 += sample_value;
            moment2 += sample_value * sample_value;
        }
    }

    for (var y: i32 = -2; y <= 2; y++) {
        for (var x: i32 = -2; x <= 2; x++) {
            let sample_coord = clamp(raw_pixel + vec2<i32>(x, y), vec2<i32>(0, 0), raw_extent - vec2<i32>(1, 1));
            let sample_luma = perceptual_luma(get_safe_raw_color(sample_coord, raw_extent).rgb);
            luma_m1 += sample_luma;
            luma_m2 += sample_luma * sample_luma;
        }
    }

    let mean = moment1 / 9.0;
    let variance = max(moment2 / 9.0 - mean * mean, vec3<f32>(0.0));
    let std_dev = sqrt(variance);
    let luma_mean = luma_m1 / 25.0;
    let spatial_variance = max(luma_m2 / 25.0 - luma_mean * luma_mean, 1e-4);

    var indirect = vec4<f32>(current_raw.rgb, max(current_raw.a, 1.0));
    var accepted_history = false;

    if (!camera_cut && (u_ssgi.frame_params.w & 1u) != 0u) {
        let velocity = textureSampleLevel(t_velocity, s_point, full_uv, 0.0).rg;
        let history_uv = in.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, history_uv);
            if (history.valid) {
                let current_weight = max(u_ssgi.reprojection_params.x, 1.0 / (history.history_len + 1.0));
                let dynamic_gamma = u_ssgi.temporal_params.x * mix(2.0, 1.0, clamp(current_weight, 0.0, 1.0));
                let box_extent = max(std_dev * dynamic_gamma, vec3<f32>(1e-4));
                let clipped_history = clip_towards_aabb_center(history.color, mean, box_extent);
                indirect = vec4<f32>(
                    mix(clipped_history, current_raw.rgb, current_weight),
                    min(history.history_len + 1.0, 32.0)
                );
                accepted_history = true;
            }
        }
    }

    if (!accepted_history) {
        indirect = vec4<f32>(current_raw.rgb, max(current_raw.a, 1.0));
    }

    out.indirect = indirect;
    out.history_meta = vec4<f32>(oct_encode(current_normal), current_linear, spatial_variance);
    return out;
}