fluid_core 0.1.1

GPU-accelerated fluid simulation core library (wgpu, no winit dependency)
Documentation
// bloom.wgsl - Bloom prefilter, blur, and final shaders
// Bloom creates a "glow" effect around bright areas.
// It works by:
// 1. Extracting pixels brighter than a threshold (Prefilter).
// 2. Blurring these pixels (often multiple times at different scales).
// 3. Adding the blurred result back to the original image.

struct VertexOutput {
    @builtin(position) clip_position: vec4<f32>,
    @location(0) uv: vec2<f32>,
    @location(1) v_l: vec2<f32>,
    @location(2) v_r: vec2<f32>,
    @location(3) v_t: vec2<f32>,
    @location(4) v_b: vec2<f32>,
};

// ===== PREFILTER =====
// Extracts bright pixels to be blurred. Uses a "soft knee" threshold curve
// to avoid harsh transitions where the glow starts.

struct BloomPrefilterUniforms {
    texel_size: vec2<f32>,
    curve: vec4<f32>, // x: threshold - knee, y: knee * 2, z: 0.25 / knee
    threshold: f32,
    _pad: f32,
};

@group(0) @binding(0)
var<uniform> prefilter_uniforms: BloomPrefilterUniforms;

@group(0) @binding(1)
var u_texture: texture_2d<f32>;
@group(0) @binding(2)
var u_sampler: sampler;

@fragment
fn fs_prefilter(in: VertexOutput) -> @location(0) vec4<f32> {
    // 1. Downsample: Sample 4 neighbors to get average brightness of the area
    let dt = prefilter_uniforms.texel_size * 2.0;
    var s = vec3<f32>(0.0);
    s += textureSample(u_texture, u_sampler, in.uv - dt).xyz;
    s += textureSample(u_texture, u_sampler, in.uv + vec2<f32>(dt.x, -dt.y)).xyz;
    s += textureSample(u_texture, u_sampler, in.uv + dt).xyz;
    s += textureSample(u_texture, u_sampler, in.uv + vec2<f32>(-dt.x, dt.y)).xyz;
    s *= 0.25;
    
    // 2. Thresholding with Soft Knee
    // Standard threshold: max(brightness - threshold, 0)
    // Soft knee: smooths the transition around the threshold
    let brightness = max(s.x, max(s.y, s.z));
    
    // Calculate contribution curve
    var rq = clamp(brightness - prefilter_uniforms.curve.x, 0.0, prefilter_uniforms.curve.y);
    rq = prefilter_uniforms.curve.z * rq * rq;
    
    // Apply threshold
    s *= max(rq, brightness - prefilter_uniforms.threshold) / max(brightness, 0.0001);
    
    return vec4<f32>(s, 1.0);
}

// ===== BLUR (used for both downsample and upsample) =====
// Simple 4-tap box filter/tent filter.
// When applied repeatedly in a mipmap chain, it approximates a Gaussian blur efficiently.

struct BloomBlurUniforms {
    texel_size: vec2<f32>,
    _pad: vec2<f32>,
};

@group(0) @binding(0)
var<uniform> blur_uniforms: BloomBlurUniforms;

@fragment
fn fs_blur(in: VertexOutput) -> @location(0) vec4<f32> {
    let ts = blur_uniforms.texel_size;
    var s = vec4<f32>(0.0);
    s += textureSample(u_texture, u_sampler, in.uv - ts) * 0.25;
    s += textureSample(u_texture, u_sampler, in.uv + vec2<f32>(ts.x, -ts.y)) * 0.25;
    s += textureSample(u_texture, u_sampler, in.uv + ts) * 0.25;
    s += textureSample(u_texture, u_sampler, in.uv + vec2<f32>(-ts.x, ts.y)) * 0.25;
    return s;
}

// ===== FINAL =====
// Upsamples the bloom texture and blends it.

struct BloomFinalUniforms {
    texel_size: vec2<f32>,
    intensity: f32,
    _pad: f32,
};

@group(0) @binding(0)
var<uniform> final_uniforms: BloomFinalUniforms;

@fragment
fn fs_final(in: VertexOutput) -> @location(0) vec4<f32> {
    let ts = final_uniforms.texel_size;
    var s = vec4<f32>(0.0);
    s += textureSample(u_texture, u_sampler, in.uv - ts) * 0.25;
    s += textureSample(u_texture, u_sampler, in.uv + vec2<f32>(ts.x, -ts.y)) * 0.25;
    s += textureSample(u_texture, u_sampler, in.uv + ts) * 0.25;
    s += textureSample(u_texture, u_sampler, in.uv + vec2<f32>(-ts.x, ts.y)) * 0.25;
    return s * final_uniforms.intensity;
}