cvkg-render-gpu 0.1.4

Cyberpunk Viking Knowledge Graph (CVKG) - High-fidelity agentic UI framework
Documentation
//! # Niflheim Shader Suite (v2.0 — Muspelheim Multi-pass)
//!
//! Atmosphere and effect shaders for the CVKG "Surtr" render pipeline.
//! Implements layered Cyberpunk Viking Berserker aesthetics:
//!   - Base pass: solid geometry with vertex colour
//!   - Bloom pass: extracts bright pixels for the Gungnir glow
//!   - Blur H/V passes: separable Gaussian for Niflheim mist
//!   - Composite pass: additively blends blurred glow onto the base scene

// ---------------------------------------------------------------------------
// Shared types
// ---------------------------------------------------------------------------

struct VertexInput {
    @location(0) position: vec2<f32>,
    @location(1) uv:       vec2<f32>,
    @location(2) color:    vec4<f32>,
    @location(3) mode:     u32,
};

struct VertexOutput {
    @builtin(position) clip_position: vec4<f32>,
    @location(0) uv:    vec2<f32>,
    @location(1) color: vec4<f32>,
    @location(2) @interpolate(flat) mode: u32,
};

@group(0) @binding(0) var t_diffuse: texture_2d<f32>;
@group(0) @binding(1) var s_diffuse: sampler;

// ---------------------------------------------------------------------------
// Vertex shaders
// ---------------------------------------------------------------------------

/// Standard per-vertex stage — used by the base geometry pass.
@vertex
fn vs_main(model: VertexInput) -> VertexOutput {
    var out: VertexOutput;
    out.clip_position = vec4<f32>(model.position, 0.0, 1.0);
    out.uv    = model.uv;
    out.color = model.color;
    out.mode  = model.mode;
    return out;
}

/// Full-screen triangle trick — used by all post-process passes.
/// Emits a triangle whose verts lie outside [-1,1] so the rasteriser
/// clips it to exactly the viewport without any vertex buffer needed.
/// Vertex positions: (0,-1,-1), (1,3,-1), (2,-1,3)
@vertex
fn vs_fullscreen(@builtin(vertex_index) vi: u32) -> VertexOutput {
    var out: VertexOutput;
    let x = f32((vi << 1u) & 2u) * 2.0 - 1.0;  // 0→-1, 1→-1+2=1→ err, let's do explicit
    let y = f32(vi & 2u) * (-2.0) + 1.0;
    // vertex 0: vi=0  → x=2*0-1=-1, y=1-2*0=1    → (-1, 1)
    // vertex 1: vi=1  → x=2*2-1=3,  y=1-2*0=1    → ( 3, 1)
    // vertex 2: vi=2  → x=2*0-1=-1, y=1-2*2=-3   → (-1,-3)
    let nx = f32((vi << 1u) & 2u) * 2.0 - 1.0;
    let ny = 1.0 - f32(vi & 2u) * 2.0;
    out.clip_position = vec4<f32>(nx, ny, 0.0, 1.0);
    out.uv    = vec2<f32>(nx * 0.5 + 0.5, 0.5 - ny * 0.5);
    out.color = vec4<f32>(0.0);
    out.mode  = 0u;
    return out;
}

// ---------------------------------------------------------------------------
// Fragment shaders — base geometry
// ---------------------------------------------------------------------------

/// Mode 0 — solid fill, direct vertex colour.
/// Mode 1 — Gungnir Glow: amplify colour for the bloom extraction step.
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    switch in.mode {
        case 0u: {
            // Plain solid rect
            return in.color;
        }
        case 1u: {
            // Gungnir Glow source — output amplified colour so the bloom
            // extractor picks it up strongly.
            let amp = in.color.rgb * 2.0;
            return vec4<f32>(amp, in.color.a);
        }
        default: {
            return vec4<f32>(1.0, 0.0, 1.0, 1.0); // Debug magenta
        }
    }
}

// ---------------------------------------------------------------------------
// Fragment shaders — bloom extraction
// ---------------------------------------------------------------------------

/// Pull out pixels brighter than a threshold (luma > 0.4).
/// These feed the Muspelheim blur pipeline.
@fragment
fn fs_bloom_extract(in: VertexOutput) -> @location(0) vec4<f32> {
    let c = textureSample(t_diffuse, s_diffuse, in.uv);
    let luma = dot(c.rgb, vec3<f32>(0.299, 0.587, 0.114));
    let knee = smoothstep(0.3, 0.7, luma);
    return vec4<f32>(c.rgb * knee, c.a * knee);
}

// ---------------------------------------------------------------------------
// Fragment shaders — Niflheim separable Gaussian blur (5-tap, unrolled)
// WGSL requires constant array indices, so we unroll all taps explicitly.
// ---------------------------------------------------------------------------

/// Mist of Niflheim — horizontal blur pass.
@fragment
fn fs_blur_h(in: VertexOutput) -> @location(0) vec4<f32> {
    let tex_size = vec2<f32>(textureDimensions(t_diffuse));
    let s = 1.0 / tex_size.x;
    return
        textureSample(t_diffuse, s_diffuse, in.uv + vec2<f32>(-2.0 * s, 0.0)) * 0.06136 +
        textureSample(t_diffuse, s_diffuse, in.uv + vec2<f32>(-1.0 * s, 0.0)) * 0.24477 +
        textureSample(t_diffuse, s_diffuse, in.uv)                             * 0.38774 +
        textureSample(t_diffuse, s_diffuse, in.uv + vec2<f32>( 1.0 * s, 0.0)) * 0.24477 +
        textureSample(t_diffuse, s_diffuse, in.uv + vec2<f32>( 2.0 * s, 0.0)) * 0.06136;
}

/// Mist of Niflheim — vertical blur pass.
@fragment
fn fs_blur_v(in: VertexOutput) -> @location(0) vec4<f32> {
    let tex_size = vec2<f32>(textureDimensions(t_diffuse));
    let s = 1.0 / tex_size.y;
    return
        textureSample(t_diffuse, s_diffuse, in.uv + vec2<f32>(0.0, -2.0 * s)) * 0.06136 +
        textureSample(t_diffuse, s_diffuse, in.uv + vec2<f32>(0.0, -1.0 * s)) * 0.24477 +
        textureSample(t_diffuse, s_diffuse, in.uv)                             * 0.38774 +
        textureSample(t_diffuse, s_diffuse, in.uv + vec2<f32>(0.0,  1.0 * s)) * 0.24477 +
        textureSample(t_diffuse, s_diffuse, in.uv + vec2<f32>(0.0,  2.0 * s)) * 0.06136;
}

// ---------------------------------------------------------------------------
// Fragment shader — final Muspelheim composite
// ---------------------------------------------------------------------------

/// Additively blend the blurred Gungnir glow on top of the base frame.
/// Intensity 2.5x makes the neon halo clearly visible on thin bright elements.
@fragment
fn fs_composite(in: VertexOutput) -> @location(0) vec4<f32> {
    let bloom = textureSample(t_diffuse, s_diffuse, in.uv);
    return vec4<f32>(bloom.rgb * 2.5, bloom.a);
}