Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Post-Processing

Post-processing applies screen-space effects to the rendered scene - things like bloom, vignette, chromatic aberration, and CRT scanlines.

Basic Usage

#![allow(unused)]
fn main() {
.with_visuals(|v| {
    v.post_process(r#"
        let color = textureSample(scene, scene_sampler, in.uv);
        // Modify color here
        return color;
    "#);
})
}

Available Variables

VariableTypeDescription
in.uvvec2<f32>Screen coordinates (0 to 1, top-left is origin)
scenetexture_2d<f32>The rendered particle scene
scene_samplersamplerSampler for the scene texture
uniforms.timef32Seconds since simulation start
uniforms.delta_timef32Seconds since last frame
uniforms.*variesAny custom uniforms defined via .with_uniform()

How It Works

After all particles are rendered to an offscreen texture, your post-process shader runs once per screen pixel. You sample the scene texture and output a modified color.

#![allow(unused)]
fn main() {
// The identity post-process (does nothing)
v.post_process(r#"
    return textureSample(scene, scene_sampler, in.uv);
"#);
}

Common Effects

Vignette

Darken the edges of the screen:

#![allow(unused)]
fn main() {
v.post_process(r#"
    let color = textureSample(scene, scene_sampler, in.uv);
    let center = vec2<f32>(0.5, 0.5);
    let dist = length(in.uv - center);
    let vignette = 1.0 - smoothstep(0.3, 0.9, dist);
    return vec4<f32>(color.rgb * vignette, 1.0);
"#);
}

Chromatic Aberration

Separate RGB channels for a lens distortion effect:

#![allow(unused)]
fn main() {
v.post_process(r#"
    let aberration = 0.005;
    let r = textureSample(scene, scene_sampler, in.uv + vec2<f32>(aberration, 0.0)).r;
    let g = textureSample(scene, scene_sampler, in.uv).g;
    let b = textureSample(scene, scene_sampler, in.uv - vec2<f32>(aberration, 0.0)).b;
    return vec4<f32>(r, g, b, 1.0);
"#);
}

Radial Chromatic Aberration

Aberration that increases toward edges:

#![allow(unused)]
fn main() {
v.post_process(r#"
    let center = vec2<f32>(0.5, 0.5);
    let uv_centered = in.uv - center;
    let dist = length(uv_centered);
    let dir = normalize(uv_centered);

    let aberration = 0.003 + dist * 0.01;

    let r = textureSample(scene, scene_sampler, in.uv + dir * aberration).r;
    let g = textureSample(scene, scene_sampler, in.uv).g;
    let b = textureSample(scene, scene_sampler, in.uv - dir * aberration).b;

    return vec4<f32>(r, g, b, 1.0);
"#);
}

Film Grain

Add noise for a film-like quality:

#![allow(unused)]
fn main() {
v.post_process(r#"
    let color = textureSample(scene, scene_sampler, in.uv);

    // Hash-based noise
    let grain = fract(sin(dot(in.uv * 1000.0, vec2<f32>(12.9898, 78.233)) + uniforms.time) * 43758.5453);
    let noise = (grain - 0.5) * 0.03;

    return vec4<f32>(color.rgb + noise, 1.0);
"#);
}

CRT Scanlines

Classic CRT monitor effect:

#![allow(unused)]
fn main() {
v.post_process(r#"
    let color = textureSample(scene, scene_sampler, in.uv);

    let scanline_freq = 400.0;
    let scanline = sin(in.uv.y * scanline_freq) * 0.5 + 0.5;
    let scanline_intensity = 0.15;

    let result = color.rgb * (1.0 - scanline_intensity * (1.0 - scanline));
    return vec4<f32>(result, 1.0);
"#);
}

Barrel Distortion

CRT-style curved screen:

#![allow(unused)]
fn main() {
v.post_process(r#"
    let center = vec2<f32>(0.5, 0.5);
    let uv_centered = in.uv - center;
    let dist_sq = dot(uv_centered, uv_centered);
    let barrel = 0.1;
    let distorted_uv = center + uv_centered * (1.0 + barrel * dist_sq);

    let color = textureSample(scene, scene_sampler, distorted_uv);
    return color;
"#);
}

Bloom (Simple)

Boost bright areas:

#![allow(unused)]
fn main() {
v.post_process(r#"
    let color = textureSample(scene, scene_sampler, in.uv);
    let luminance = dot(color.rgb, vec3<f32>(0.299, 0.587, 0.114));
    let bloom = smoothstep(0.4, 1.0, luminance) * 0.4;
    return vec4<f32>(color.rgb + color.rgb * bloom, 1.0);
"#);
}

Color Grading

Adjust color balance:

#![allow(unused)]
fn main() {
v.post_process(r#"
    let color = textureSample(scene, scene_sampler, in.uv);

    // Warm tint (boost red, reduce blue)
    var graded = pow(color.rgb, vec3<f32>(0.95, 1.0, 1.05));

    // Contrast boost
    graded = (graded - 0.5) * 1.1 + 0.5;

    // Saturation boost
    let gray = dot(graded, vec3<f32>(0.3, 0.3, 0.3));
    graded = mix(vec3<f32>(gray), graded, 1.3);

    return vec4<f32>(clamp(graded, vec3<f32>(0.0), vec3<f32>(1.0)), 1.0);
"#);
}

Screen Flicker

Subtle brightness variation:

#![allow(unused)]
fn main() {
v.post_process(r#"
    let color = textureSample(scene, scene_sampler, in.uv);
    let flicker = sin(uniforms.time * 60.0) * 0.02 + 1.0;
    return vec4<f32>(color.rgb * flicker, 1.0);
"#);
}

Combining Effects

Chain multiple effects together:

#![allow(unused)]
fn main() {
v.post_process(r#"
    let center = vec2<f32>(0.5, 0.5);
    var uv = in.uv;

    // 1. Barrel distortion
    let uv_centered = uv - center;
    let dist_sq = dot(uv_centered, uv_centered);
    uv = center + uv_centered * (1.0 + 0.1 * dist_sq);

    // 2. Chromatic aberration
    let aberr = 0.004;
    let r = textureSample(scene, scene_sampler, uv + vec2<f32>(aberr, 0.0)).r;
    let g = textureSample(scene, scene_sampler, uv).g;
    let b = textureSample(scene, scene_sampler, uv - vec2<f32>(aberr, 0.0)).b;
    var color = vec3<f32>(r, g, b);

    // 3. Scanlines
    let scanline = sin(in.uv.y * 400.0) * 0.5 + 0.5;
    color *= 1.0 - 0.1 * (1.0 - scanline);

    // 4. Vignette
    let vignette_dist = length(in.uv - center);
    let vignette = 1.0 - smoothstep(0.4, 1.0, vignette_dist);
    color *= vignette;

    // 5. Flicker
    let flicker = sin(uniforms.time * 60.0) * 0.01 + 1.0;
    color *= flicker;

    return vec4<f32>(color, 1.0);
"#);
}

Performance Tips

  • Post-processing runs once per screen pixel
  • Texture samples are relatively expensive
  • Multiple samples (for blur) can add up quickly
  • Keep blur kernel sizes small (4-8 samples)