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
| Variable | Type | Description |
|---|---|---|
in.uv | vec2<f32> | Screen coordinates (0 to 1, top-left is origin) |
scene | texture_2d<f32> | The rendered particle scene |
scene_sampler | sampler | Sampler for the scene texture |
uniforms.time | f32 | Seconds since simulation start |
uniforms.delta_time | f32 | Seconds since last frame |
uniforms.* | varies | Any 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)
Related
- Visual Configuration - Blend modes, trails, connections
- Fragment Shaders - Per-particle appearance