tiramisu/postprocessing

Post-processing effects for WebGL rendering using Three.js EffectComposer.

Post-processing allows you to apply visual effects to your rendered scene, such as bloom, pixelation, film grain, and more. Effects are applied in order and composed using Three.js’s EffectComposer system.

Camera-Based Architecture

Postprocessing in Tiramisu is camera-based, following industry standards from Unity and Unreal Engine. Each camera can have its own postprocessing pipeline, enabling split-screen with different effects, mini-maps without effects, or picture-in-picture with unique visual styles.

Dynamic Updates

Postprocessing configurations are automatically diffed each frame. When you change a camera’s postprocessing setup in your view function, the engine detects the change and recreates the effect composer. This allows for:

The diffing algorithm compares pass types and parameters, only recreating when actual changes are detected, minimizing performance impact.

Quick Example

import tiramisu/postprocessing as pp
import tiramisu/scene
import gleam/option.{None, Some}

// In your view function:
fn view(model: Model, _ctx) -> scene.Node(String) {
  let camera = scene.camera(
    id: "main",
    camera: cam,
    transform: transform.identity,
    look_at: option.None,
    active: True,
    viewport: option.None,
    postprocessing: option.Some(
      pp.new()
      |> pp.add_pass(pp.clear_pass(option.None))
      |> pp.add_pass(pp.render_pass())
      |> pp.add_pass(pp.bloom(strength: 1.5, threshold: 0.85, radius: 0.4))
      |> pp.add_pass(pp.pixelate(pixel_size: 4))
      |> pp.add_pass(pp.vignette(darkness: 1.0, offset: 1.0))
      |> pp.add_pass(pp.fxaa())
      |> pp.add_pass(pp.output_pass())
    ),
  )

  scene.empty(id: "root", transform: transform.identity, children: [
    camera,
    // ... your scene nodes
  ])
}

Multi-Camera Example

// Player 1: Retro pixel effect
let camera1 = scene.camera(
  id: "player1",
  viewport: option.Some(scene.Viewport(x: 0, y: 0, width: 400, height: 600)),
  postprocessing: option.Some(
    pp.new()
    |> pp.add_pass(pp.clear_pass(option.None))
    |> pp.add_pass(pp.render_pass())
    |> pp.add_pass(pp.pixelate(4))
    |> pp.add_pass(pp.output_pass())
  ),
  // ...
)

// Player 2: Cinematic bloom
let camera2 = scene.camera(
  id: "player2",
  viewport: option.Some(scene.Viewport(x: 400, y: 0, width: 400, height: 600)),
  postprocessing: option.Some(
    pp.new()
    |> pp.add_pass(pp.clear_pass(option.None))
    |> pp.add_pass(pp.render_pass())
    |> pp.add_pass(pp.bloom(strength: 1.5, threshold: 0.7, radius: 0.5))
    |> pp.add_pass(pp.vignette(darkness: 0.8, offset: 1.0))
    |> pp.add_pass(pp.output_pass())
  ),
  // ...
)

Effect Order

Effects are applied in the order you add them. A typical pipeline looks like:

  1. Bloom (brighten glowing areas)
  2. Pixelate (retro pixel effect)
  3. Film grain (add texture)
  4. Vignette (darken edges)
  5. FXAA (anti-aliasing for smooth final output)

Performance

Post-processing has a performance cost. Each effect adds a render pass. Each camera with postprocessing creates a separate EffectComposer. For mobile devices, consider using fewer effects or lower quality settings.

Types

Post-processing pass types.

Each pass represents a visual effect that will be applied to the scene. Passes are executed in the order they are added to the pipeline.

Pipeline Passes

Three.js postprocessing requires specific passes for proper rendering:

  • RenderPass: Renders the scene to the render target (usually first)
  • ClearPass: Clears render target with a color (for backgrounds)
  • OutputPass: Final tone mapping and output (usually last)

You must explicitly add these passes in the correct order.

pub type Pass {
  RenderPass
  ClearPass(color: option.Option(Int))
  OutputPass
  PixelatePass(
    pixel_size: Int,
    normal_edge_strength: Float,
    depth_edge_strength: Float,
  )
  BloomPass(strength: Float, threshold: Float, radius: Float)
  FilmPass(
    noise_intensity: Float,
    scanline_intensity: Float,
    scanline_count: Int,
    grayscale: Bool,
  )
  VignettePass(darkness: Float, offset: Float)
  FXAAPass
  GlitchPass(dt_size: Int)
  ColorCorrectionPass(
    brightness: Float,
    contrast: Float,
    saturation: Float,
  )
  CustomShaderPass(
    vertex_shader: String,
    fragment_shader: String,
    uniforms: List(#(String, UniformValue)),
  )
}

Constructors

  • RenderPass

    Render pass - renders the scene to the render target.

    This pass actually draws your 3D scene. It should typically be one of the first passes in your pipeline. If you need the scene background to work, add a ClearPass before this.

  • ClearPass(color: option.Option(Int))

    Clear pass - clears the render target with a color.

    Use this before RenderPass to make scene backgrounds work correctly. The color parameter overrides the scene background if provided.

    • None: Uses the scene’s background color
    • Some(color): Uses the specified hex color
  • OutputPass

    Output pass - applies tone mapping and outputs to screen.

    This should typically be the last pass in your pipeline. It applies final color corrections and tone mapping before displaying the result.

  • PixelatePass(
      pixel_size: Int,
      normal_edge_strength: Float,
      depth_edge_strength: Float,
    )

    Pixelation effect with optional edge detection.

    Creates a retro pixel-art aesthetic by reducing the resolution of the image. Edge detection can add outlines based on surface normals and depth.

  • BloomPass(strength: Float, threshold: Float, radius: Float)

    Bloom effect (glow for bright areas).

    Makes bright areas of the scene glow and bleed into surrounding pixels. Great for emissive materials, lights, and sci-fi aesthetics.

  • FilmPass(
      noise_intensity: Float,
      scanline_intensity: Float,
      scanline_count: Int,
      grayscale: Bool,
    )

    Film grain effect.

    Adds analog film texture with grain noise and optional scanlines. Can create a retro or cinematic look.

  • VignettePass(darkness: Float, offset: Float)

    Vignette effect (darkened edges).

    Darkens the edges of the screen, focusing attention on the center.

  • FXAAPass

    FXAA anti-aliasing.

    Fast approximate anti-aliasing that smooths jagged edges. Usually added as the last pass for a polished final output.

  • GlitchPass(dt_size: Int)

    Glitch effect.

    Creates digital corruption artifacts with RGB channel offsets. Great for cyberpunk or error state aesthetics.

  • ColorCorrectionPass(
      brightness: Float,
      contrast: Float,
      saturation: Float,
    )

    Color correction.

    Adjust brightness, contrast, and saturation of the final image.

  • CustomShaderPass(
      vertex_shader: String,
      fragment_shader: String,
      uniforms: List(#(String, UniformValue)),
    )

    Custom shader pass.

    Apply a custom GLSL shader for advanced effects.

Opaque post-processing composer type.

Contains a list of passes that will be applied in order to the rendered scene.

pub opaque type PostProcessing

Uniform values for custom shaders.

When creating custom shader passes, you can pass uniform values of different types.

pub type UniformValue {
  FloatUniform(Float)
  IntUniform(Int)
  Vec2Uniform(Float, Float)
  Vec3Uniform(Float, Float, Float)
  ColorUniform(Int)
}

Constructors

  • FloatUniform(Float)
  • IntUniform(Int)
  • Vec2Uniform(Float, Float)
  • Vec3Uniform(Float, Float, Float)
  • ColorUniform(Int)

Values

pub fn add_pass(pp: PostProcessing, pass: Pass) -> PostProcessing

Add a pass to the pipeline.

Passes are executed in the order they are added.

Example

postprocessing.new()
|> postprocessing.add_pass(postprocessing.bloom(
  strength: 1.5,
  threshold: 0.85,
  radius: 0.4,
))
|> postprocessing.add_pass(postprocessing.fxaa())
pub fn bloom(
  strength strength: Float,
  threshold threshold: Float,
  radius radius: Float,
) -> Pass

Create a bloom effect pass.

Bloom makes bright areas glow and bleed into surrounding pixels.

Parameters

  • strength: How much bloom to apply (0.0 - 3.0, typically 0.5 - 2.0)
  • threshold: Brightness threshold for bloom (0.0 - 1.0, typically 0.5 - 0.9)
  • radius: How far the bloom spreads (0.0 - 1.0, typically 0.4 - 0.8)

Example

// Subtle bloom for realistic glow
pp.bloom(strength: 0.8, threshold: 0.85, radius: 0.4)

// Intense bloom for sci-fi effect
pp.bloom(strength: 2.0, threshold: 0.5, radius: 0.8)
pub fn clear_pass(color color: option.Option(Int)) -> Pass

Create a clear pass.

Clears the render target with a color. Use this before RenderPass to make scene backgrounds work correctly with postprocessing.

Parameters

  • color: Optional hex color to clear with
    • None: Uses the scene’s background color
    • Some(0xff0000): Clears with the specified color

Example

// Use scene background
pp.clear_pass(option.None)

// Use custom color
pp.clear_pass(option.Some(0x000000))  // Black
pub fn color_correction(
  brightness brightness: Float,
  contrast contrast: Float,
  saturation saturation: Float,
) -> Pass

Create a color correction pass.

Adjust brightness, contrast, and saturation of the final image.

Parameters

  • brightness: Brightness adjustment (-1.0 to 1.0, 0.0 is no change)
  • contrast: Contrast adjustment (-1.0 to 1.0, 0.0 is no change)
  • saturation: Saturation adjustment (-1.0 to 1.0, 0.0 is no change, -1.0 is grayscale)

Example

// Brighten and increase saturation
pp.color_correction(
  brightness: 0.2,
  contrast: 0.1,
  saturation: 0.3,
)

// Desaturated look
pp.color_correction(
  brightness: 0.0,
  contrast: 0.2,
  saturation: -0.5,
)
pub fn custom_shader(
  vertex_shader vertex_shader: String,
  fragment_shader fragment_shader: String,
  uniforms uniforms: List(#(String, UniformValue)),
) -> Pass

Create a custom shader pass.

Apply custom GLSL shaders for advanced effects.

Parameters

  • vertex_shader: GLSL vertex shader code
  • fragment_shader: GLSL fragment shader code
  • uniforms: List of uniform name/value pairs

Example

pp.custom_shader(
  vertex_shader: "
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  ",
  fragment_shader: "
    uniform sampler2D tDiffuse;
    uniform float intensity;
    varying vec2 vUv;
    void main() {
      vec4 color = texture2D(tDiffuse, vUv);
      gl_FragColor = color * intensity;
    }
  ",
  uniforms: [
    #("intensity", pp.FloatUniform(1.5)),
  ],
)
pub fn film_grain(
  noise_intensity noise_intensity: Float,
  scanline_intensity scanline_intensity: Float,
  scanline_count scanline_count: Int,
  grayscale grayscale: Bool,
) -> Pass

Create a film grain effect.

Adds analog film texture with grain noise and optional scanlines.

Parameters

  • noise_intensity: Amount of grain (0.0 - 1.0, typically 0.2 - 0.5)
  • scanline_intensity: Strength of scanlines (0.0 - 1.0, typically 0.0 - 0.3)
  • scanline_count: Number of scanlines (e.g., 512, 1024)
  • grayscale: Convert to black and white

Example

// Subtle film grain
pp.film_grain(
  noise_intensity: 0.2,
  scanline_intensity: 0.0,
  scanline_count: 0,
  grayscale: False,
)

// Retro CRT monitor effect
pp.film_grain(
  noise_intensity: 0.4,
  scanline_intensity: 0.3,
  scanline_count: 512,
  grayscale: True,
)
pub fn fxaa() -> Pass

Create an FXAA anti-aliasing pass.

Fast approximate anti-aliasing that smooths jagged edges. Usually added as the last pass in the pipeline.

Example

postprocessing.new()
|> postprocessing.add_pass(postprocessing.bloom(...))
|> postprocessing.add_pass(postprocessing.fxaa())  // Last pass
pub fn glitch(dt_size dt_size: Int) -> Pass

Create a glitch effect pass.

Creates digital corruption artifacts with RGB channel offsets.

Parameters

  • dt_size: Size of distortion blocks (typically 32 - 128)

Example

pp.glitch(dt_size: 64)
pub fn new() -> PostProcessing

Create a new empty post-processing pipeline.

Start with this and add passes using add_pass.

Example

let pp = postprocessing.new()
  |> postprocessing.add_pass(postprocessing.clear_pass(option.None))
  |> postprocessing.add_pass(postprocessing.render_pass())
  |> postprocessing.add_pass(postprocessing.bloom(...))
  |> postprocessing.add_pass(postprocessing.fxaa())
  |> postprocessing.add_pass(postprocessing.output_pass())
pub fn output_pass() -> Pass

Create an output pass.

Applies final tone mapping and outputs to the screen. This should typically be the last pass in your pipeline.

Example

pp.new()
|> pp.add_pass(pp.render_pass())
|> pp.add_pass(pp.bloom(...))
|> pp.add_pass(pp.output_pass())  // Last pass
pub fn pixelate(pixel_size pixel_size: Int) -> Pass

Create a simple pixelation effect without edge detection.

Parameters

  • pixel_size: Size of pixels (1 - 16, where 1 is no effect)

Example

// Subtle pixelation
pp.pixelate(pixel_size: 2)

// Strong retro effect
pp.pixelate(pixel_size: 8)
pub fn pixelate_with_edges(
  pixel_size pixel_size: Int,
  normal_edge_strength normal_edge_strength: Float,
  depth_edge_strength depth_edge_strength: Float,
) -> Pass

Create a pixelation effect with edge detection.

Edge detection adds outlines based on surface normals and depth changes.

Parameters

  • pixel_size: Size of pixels (1 - 16)
  • normal_edge_strength: Strength of normal-based edges (0.0 - 2.0)
  • depth_edge_strength: Strength of depth-based edges (0.0 - 1.0)

Example

pp.pixelate_with_edges(
  pixel_size: 4,
  normal_edge_strength: 1.0,
  depth_edge_strength: 0.5,
)
pub fn render_pass() -> Pass

Create a render pass.

This pass renders your 3D scene to the render target. It should typically be one of the first passes in your pipeline (after ClearPass if you need background rendering).

Example

pp.new()
|> pp.add_pass(pp.render_pass())
|> pp.add_pass(pp.bloom(...))
pub fn vignette(
  darkness darkness: Float,
  offset offset: Float,
) -> Pass

Create a vignette effect.

Darkens the edges of the screen, focusing attention on the center.

Parameters

  • darkness: How dark the edges are (0.0 - 2.0, typically 0.5 - 1.5)
  • offset: How far from center the vignette starts (0.0 - 2.0, typically 0.8 - 1.2)

Example

// Subtle vignette
pp.vignette(darkness: 0.5, offset: 1.0)

// Dramatic vignette
pp.vignette(darkness: 1.5, offset: 0.8)
Search Document