// Bloom/Glow post-processing shader for lightsaber waveform effect
//
// This shader applies a multi-pass gaussian blur and additive blending
// to create a glowing effect around bright elements.
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
};
// Uniforms
struct BloomParams {
intensity: f32, // Glow intensity (0.0 - 2.0)
threshold: f32, // Brightness threshold for bloom
radius: f32, // Blur radius
_padding: f32,
};
@group(0) @binding(0) var input_texture: texture_2d<f32>;
@group(0) @binding(1) var input_sampler: sampler;
@group(0) @binding(2) var<uniform> params: BloomParams;
// Lightsaber color palette
const LAPIS: vec3<f32> = vec3<f32>(0.149, 0.349, 0.647); // #2659A5
const PURPLE: vec3<f32> = vec3<f32>(0.502, 0.200, 0.800); // #8033CC
const EMERALD: vec3<f32> = vec3<f32>(0.200, 0.698, 0.302); // #33B24D
const ORANGE: vec3<f32> = vec3<f32>(0.800, 0.400, 0.102); // #CC661A
const RED: vec3<f32> = vec3<f32>(0.902, 0.102, 0.102); // #E61A1A
// Gaussian weights for 9-tap blur
const WEIGHTS: array<f32, 5> = array<f32, 5>(
0.227027,
0.1945946,
0.1216216,
0.054054,
0.016216
);
// Full-screen triangle vertex shader
@vertex
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
var output: VertexOutput;
// Generate full-screen triangle
let x = f32(vertex_index & 1u) * 4.0 - 1.0;
let y = f32((vertex_index >> 1u) & 1u) * 4.0 - 1.0;
output.position = vec4<f32>(x, y, 0.0, 1.0);
output.uv = vec2<f32>((x + 1.0) * 0.5, (1.0 - y) * 0.5);
return output;
}
// Extract bright areas for bloom
@fragment
fn fs_threshold(in: VertexOutput) -> @location(0) vec4<f32> {
let color = textureSample(input_texture, input_sampler, in.uv);
// Calculate luminance
let luminance = dot(color.rgb, vec3<f32>(0.2126, 0.7152, 0.0722));
// Only keep pixels above threshold
if (luminance > params.threshold) {
return color;
}
return vec4<f32>(0.0, 0.0, 0.0, 0.0);
}
// Horizontal blur pass
@fragment
fn fs_blur_horizontal(in: VertexOutput) -> @location(0) vec4<f32> {
let tex_size = vec2<f32>(textureDimensions(input_texture));
let texel_size = 1.0 / tex_size.x;
var result = textureSample(input_texture, input_sampler, in.uv).rgb * WEIGHTS[0];
for (var i = 1; i < 5; i++) {
let offset = texel_size * f32(i) * params.radius;
result += textureSample(input_texture, input_sampler, in.uv + vec2<f32>(offset, 0.0)).rgb * WEIGHTS[i];
result += textureSample(input_texture, input_sampler, in.uv - vec2<f32>(offset, 0.0)).rgb * WEIGHTS[i];
}
return vec4<f32>(result, 1.0);
}
// Vertical blur pass
@fragment
fn fs_blur_vertical(in: VertexOutput) -> @location(0) vec4<f32> {
let tex_size = vec2<f32>(textureDimensions(input_texture));
let texel_size = 1.0 / tex_size.y;
var result = textureSample(input_texture, input_sampler, in.uv).rgb * WEIGHTS[0];
for (var i = 1; i < 5; i++) {
let offset = texel_size * f32(i) * params.radius;
result += textureSample(input_texture, input_sampler, in.uv + vec2<f32>(0.0, offset)).rgb * WEIGHTS[i];
result += textureSample(input_texture, input_sampler, in.uv - vec2<f32>(0.0, -offset)).rgb * WEIGHTS[i];
}
return vec4<f32>(result, 1.0);
}
// Final composite: blend original + bloom
@fragment
fn fs_composite(in: VertexOutput) -> @location(0) vec4<f32> {
let original = textureSample(input_texture, input_sampler, in.uv);
// Bloom texture would be bound separately in practice
// For this example, we apply a simple glow approximation
var bloom = vec3<f32>(0.0);
let blur_size = 0.003 * params.radius;
// Multi-sample blur for glow
for (var x = -3; x <= 3; x++) {
for (var y = -3; y <= 3; y++) {
let offset = vec2<f32>(f32(x), f32(y)) * blur_size;
let sample = textureSample(input_texture, input_sampler, in.uv + offset);
let weight = 1.0 / (1.0 + f32(x*x + y*y));
bloom += sample.rgb * weight;
}
}
bloom /= 16.0;
// Additive blend with intensity
let final_color = original.rgb + bloom * params.intensity;
// Tone mapping to prevent clipping
let mapped = final_color / (final_color + vec3<f32>(1.0));
return vec4<f32>(mapped, original.a);
}
// Lightsaber-specific glow with color fringing
@fragment
fn fs_lightsaber_glow(in: VertexOutput) -> @location(0) vec4<f32> {
let original = textureSample(input_texture, input_sampler, in.uv);
// Create multi-layered glow
var glow = vec3<f32>(0.0);
// Inner glow (tight, bright)
for (var i = 0; i < 8; i++) {
let angle = f32(i) * 0.785398; // PI/4
let offset = vec2<f32>(cos(angle), sin(angle)) * 0.002 * params.radius;
glow += textureSample(input_texture, input_sampler, in.uv + offset).rgb * 0.15;
}
// Outer glow (diffuse)
for (var i = 0; i < 16; i++) {
let angle = f32(i) * 0.392699; // PI/8
let offset = vec2<f32>(cos(angle), sin(angle)) * 0.006 * params.radius;
glow += textureSample(input_texture, input_sampler, in.uv + offset).rgb * 0.05;
}
// Color fringing (chromatic aberration for extra glow effect)
let fringe_offset = 0.001 * params.radius;
let r = textureSample(input_texture, input_sampler, in.uv + vec2<f32>(fringe_offset, 0.0)).r;
let b = textureSample(input_texture, input_sampler, in.uv - vec2<f32>(fringe_offset, 0.0)).b;
// Combine
var final_color = original.rgb;
final_color += glow * params.intensity;
final_color.r = mix(final_color.r, r, 0.1);
final_color.b = mix(final_color.b, b, 0.1);
// Subtle vignette
let vignette = 1.0 - length(in.uv - vec2<f32>(0.5)) * 0.5;
final_color *= vignette;
return vec4<f32>(final_color, original.a);
}