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 ✨

renderling renders scenes in high dynamic range (HDR), where colors can be far brighter than a display can show. After the scene is rendered, two post-processing passes turn that HDR image into the final picture:

  1. Bloom - bright areas glow, by mixing a blurred copy of the bright parts back into the image.
  2. Tonemapping - HDR values are mapped into the range the display can show, using an exposure value and a tone mapping algorithm.

Example setup

We’ll use a night sky and a model full of glowing emissive objects, so there are bright areas for the effects to work on. Note that bloom is on by default - we turn it off here so we can show the difference:

    use renderling::{
        camera,
        context::Context,
        glam::{Vec3, Vec4},
        stage::Stage,
    };

    let width = 512;
    let height = 256;
    let ctx = Context::headless(width, height).await;
    // Bloom is on by default - we turn it off so we can show the
    // difference.
    let stage: Stage = ctx
        .new_stage()
        .with_bloom(false)
        .with_background_color(Vec4::new(0.25, 0.25, 0.25, 1.0));

    let projection = camera::perspective(width as f32, height as f32);
    let view = camera::look_at(Vec3::new(0.0, 2.0, 18.0), Vec3::ZERO, Vec3::Y);
    let _camera = stage
        .new_camera()
        .with_projection_and_view(projection, view);

    // A night sky and an emissive-strength test model full of glowing
    // objects, so there are bright areas for the bloom effect to pick up.
    let skybox = stage
        .new_skybox_from_path(workspace_dir().join("img/hdr/night.hdr"))
        .unwrap();
    stage.use_skybox(&skybox);
    let ibl = stage.new_ibl(&skybox);
    stage.use_ibl(&ibl);

    let _model = stage
        .load_gltf_document_from_path(workspace_dir().join("gltf/EmissiveStrengthTest.glb"))
        .unwrap();

    let frame = ctx.get_next_frame().unwrap();
    stage.render(&frame.view());
    let img_no_bloom = frame.read_image().await.unwrap();
    img_no_bloom.save("postprocessing/bloom-none.png").unwrap();
    frame.present();

a night scene with glowing objects and no bloom

Bloom

Bloom can be toggled at stage creation with Stage::with_bloom, or at runtime with Stage::set_has_bloom. Two parameters control the look:

    // Turn bloom on, either at stage creation with `.with_bloom(true)` or
    // at runtime.
    stage.set_has_bloom(true);
    // How much of the blurred bright areas to mix back into the image.
    stage.set_bloom_mix_strength(0.1);
    // The radius of the blur, in texels.
    stage.set_bloom_filter_radius(2.0);

    let frame = ctx.get_next_frame().unwrap();
    stage.render(&frame.view());
    let img_bloom = frame.read_image().await.unwrap();
    img_bloom.save("postprocessing/bloom.png").unwrap();
    frame.present();

the same night scene with bloom, glowing objects smeared with light

Tonemapping

Before the image is shown, it is multiplied by the exposure and passed through the tone mapping algorithm, which compresses HDR values into the display’s range.

The current configuration is available through the stage’s Tonemapping as a TonemapConstants - a small struct with two fields:

    // The stage renders in high dynamic range (HDR). Before the image is
    // shown it is multiplied by the exposure and run through the tone
    // mapping algorithm, which maps the HDR values into the display's
    // range.
    let mut config = stage.tonemapping().get_tonemapping_config();
    config.exposure = 3.0;
    stage.tonemapping().set_tonemapping_config(config);

    let frame = ctx.get_next_frame().unwrap();
    stage.render(&frame.view());
    let img_exposure = frame.read_image().await.unwrap();
    img_exposure.save("postprocessing/exposure.png").unwrap();
    frame.present();

the same night scene with 3x exposure, much brighter

By default no tone mapping algorithm is used (Tonemap::NONE) - HDR values outside the display range simply clip to white. The available algorithms are:

    use renderling::tonemapping::Tonemap;

    // By default no tone mapping algorithm is used (`Tonemap::NONE`), and
    // HDR values outside the display range simply clip. A filmic operator
    // like ACES compresses the highlights instead.
    config.tonemap = Tonemap::ACES_HILL_EXPOSURE_BOOST;
    config.exposure = 1.0;
    stage.tonemapping().set_tonemapping_config(config);

    let frame = ctx.get_next_frame().unwrap();
    stage.render(&frame.view());
    let img_aces = frame.read_image().await.unwrap();
    img_aces.save("postprocessing/tonemap-aces.png").unwrap();
    frame.present();

the same night scene tone mapped with ACES, softer highlights

Filmic operators like ACES trade a little saturation for smoother highlights - notice how the bright objects keep more of their color, instead of blowing out to white.