//! # 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);
}