How EquilibriumEngine Implements HDR Tonemapping for Photorealistic Rendering

EquilibriumEngine renders scenes to a floating-point HDR framebuffer and applies selectable tonemapping operators—including ACES, Hable, and Reinhard variants—in a post-process shader before final gamma correction.

The clibequilibrium/equilibriumengine repository provides a real-time rendering pipeline that preserves high-dynamic-range lighting data throughout the scene pass, then compresses it to displayable range using HDR tonemapping. This approach maintains detail in both shadows and highlights while giving developers precise control over the final visual output through shader-based configuration.

HDR Framebuffer Configuration

Floating-Point Render Target Creation

When the base rendering system initializes, it conditionally creates a render target using a 16-bit floating-point color format to store HDR values. In equilibrium/systems/rendering/base_rendering_system.c, the engine selects BGFX_TEXTURE_FORMAT_RGBA16F when HDR mode is enabled:

// equilibrium/systems/rendering/base_rendering_system.c (lines 36-44)
bgfx_texture_format_t format =
    hdr ? BGFX_TEXTURE_FORMAT_RGBA16F : BGFX_TEXTURE_FORMAT_BGRA8;

textures[attachments++] = create_texture_2d_scaled(
    world, BGFX_BACKBUFFER_RATIO_EQUAL, false, 1,
    format, BGFX_TEXTURE_RT | samplerFlags);

This framebuffer includes a depth buffer and is stored in frame_data->frame_buffer. The system later samples this as s_texColor during the tonemapping pass, ensuring all lighting calculations remain in linear HDR space until the final post-process stage.

Runtime Tonemapping Controls

The engine exposes two critical uniforms that drive the tonemapping behavior, allowing runtime adjustment without shader recompilation.

Exposure and Mode Uniforms

In base_rendering_system.c, the system creates vec4 uniforms during initialization and updates them each frame before the blit operation:

// Uniform creation (lines 73-80)
frame_data->exposure_vec_uniform = create_uniform(it->world,
    "u_exposureVec", BGFX_UNIFORM_TYPE_VEC4);
frame_data->tonemapping_mode_vec_uniform = create_uniform(it->world,
    "u_tonemappingModeVec", BGFX_UNIFORM_TYPE_VEC4);

// Per-frame value setting (lines 48-56)
float exposure_vec[4] = { 1.0f };
bgfx_set_uniform(frame_data[i].exposure_vec_uniform,
    &exposure_vec[0], UINT16_MAX);

float tonemapping_mode_vec[4] = { (float)7 }; // default = ACES_LUM
bgfx_set_uniform(frame_data[i].tonemapping_mode_vec_uniform,
    &tonemapping_mode_vec[0], UINT16_MAX);
  • u_exposureVec: Scales HDR color values before tonemapping (default 1.0)
  • u_tonemappingModeVec: Selects the operator via integer ID (default 7 for ACES luminance)

Shader-Based Tonemapping Pipeline

Fragment Shader Implementation

The full-screen pass is implemented in launcher/sandbox/shaders/shading/fs_tonemap.sc. This shader samples the HDR texture, applies exposure scaling, executes the selected tonemapping operator, and performs accurate sRGB gamma correction:

// launcher/sandbox/shaders/shading/fs_tonemap.sc
uniform vec4 u_exposureVec;
#define u_exposure u_exposureVec.x

uniform vec4 u_tonemappingModeVec;
#define u_tonemappingMode int(u_tonemappingModeVec.x)

vec4 result = texture2D(s_texColor, texcoord);
result.rgb *= u_exposure;          // exposure scaling

switch(u_tonemappingMode) {
    case TONEMAP_EXPONENTIAL:   result.rgb = tonemap_exponential(result.rgb); break;
    case TONEMAP_REINHARD:      result.rgb = tonemap_reinhard(result.rgb); break;
    case TONEMAP_REINHARD_LUM:  result.rgb = tonemap_reinhard_luminance(result.rgb); break;
    case TONEMAP_HABLE:         result.rgb = tonemap_hable(result.rgb); break;
    case TONEMAP_DUIKER:        result.rgb = tonemap_duiker(result.rgb); break;
    case TONEMAP_ACES:          result.rgb = tonemap_aces(result.rgb); break;
    case TONEMAP_ACES_LUM:      result.rgb = tonemap_aces_luminance(result.rgb); break;
    default: case TONEMAP_NONE: result.rgb = saturate(result.rgb); break;
}

// accurate sRGB gamma correction
result.rgb = toGammaAccurate(result.rgb);
gl_FragColor = result;

Operator Library

The actual mathematical implementations reside in launcher/sandbox/shaders/shading/tonemapping.sh. The engine provides eight distinct operators ranging from simple curves to filmic approximations:

  • tonemap_exponential: Simple exponential curve (1.0 - exp(-c))
  • tonemap_reinhard: Classic Reinhard operator (c / (c + 1))
  • tonemap_reinhard_luminance: Reinhard applied to luminance preserving hue
  • tonemap_hable: Uncharted 2 filmic curve (high contrast preservation)
  • tonemap_duiker: Haarm-Peter Duiker filmic curve
  • tonemap_aces: Academy Color Encoding System (RGB)
  • tonemap_aces_luminance: ACES applied to luminance channel (default)

Practical Implementation Examples

Switching Tonemap Modes at Runtime

To change the operator during execution, update the uniform value using the predefined constants from fs_tonemap.sc:

// Switch to Hable filmic curve
#define TONEMAP_HABLE 4
float mode = (float)TONEMAP_HABLE;
bgfx_set_uniform(frame_data->tonemapping_mode_vec_uniform,
                 &mode, UINT16_MAX);

Available mode values:

  • 0: TONEMAP_NONE
  • 1: TONEMAP_EXPONENTIAL
  • 2: TONEMAP_REINHARD
  • 3: TONEMAP_REINHARD_LUM
  • 4: TONEMAP_HABLE
  • 5: TONEMAP_DUIKER
  • 6: TONEMAP_ACES
  • 7: TONEMAP_ACES_LUM (default)

Adjusting Exposure Values

Control scene brightness before tonemapping by modifying the exposure uniform:

float exposure = 2.0f;  // Increase to brighten, decrease to darken
float exposure_vec[4] = { exposure };
bgfx_set_uniform(frame_data->exposure_vec_uniform,
                 &exposure_vec[0], UINT16_MAX);

Adding Custom Tonemapping Curves

Extend the system without modifying core engine code:

  1. Implement the curve in launcher/sandbox/shaders/shading/tonemapping.sh:
vec3 tonemap_mycustom(vec3 c) {
    // Simple filmic approximation
    return c / (c + vec3_splat(0.85));
}
  1. Add the case in launcher/sandbox/shaders/shading/fs_tonemap.sc:
#define TONEMAP_MY_CUSTOM 8

// In the switch statement:
case TONEMAP_MY_CUSTOM: 
    result.rgb = tonemap_mycustom(result.rgb); 
    break;
  1. Update host code to send the new ID (8) when setting u_tonemappingModeVec.

Summary

  • HDR preservation: The engine uses RGBA16F framebuffers in base_rendering_system.c to maintain high-dynamic-range data throughout the rendering pass.
  • Flexible operators: Eight tonemapping algorithms—including ACES, Hable, and Reinhard variants—are implemented in tonemapping.sh and selectable via u_tonemappingModeVec.
  • Runtime control: Exposure and tonemapping mode are adjustable per-frame through bgfx uniforms without shader recompilation.
  • Linear workflow: The pipeline maintains linear color space until the final toGammaAccurate call in fs_tonemap.sc, ensuring physically correct lighting calculations.

Frequently Asked Questions

What is the default HDR tonemapping mode in EquilibriumEngine?

The default mode is ACES_LUM (value 7), which applies the Academy Color Encoding System curve to the luminance channel while preserving hue. This is hardcoded in base_rendering_system.c where tonemapping_mode_vec is initialized to (float)7 before being sent to the shader.

How do I switch between tonemapping operators at runtime?

Update the u_tonemappingModeVec uniform with a float value corresponding to the desired operator ID (0-7). Use bgfx_set_uniform to push the new value before the blit pass executes. The shader will select the corresponding case in the switch statement during the next frame.

What is the difference between RGB and luminance-based tonemappers?

RGB tonemappers (like tonemap_aces and tonemap_reinhard) apply the compression curve to each color channel independently, which can desaturate highlights. Luminance variants (like tonemap_aces_luminance and tonemap_reinhard_luminance) calculate the curve against the perceived brightness (luma) and scale the original RGB values proportionally, preserving color saturation in bright areas.

Can I implement a custom tonemapping curve without modifying the engine core?

Yes. Since the tonemapping logic resides in shader files within the launcher/sandbox/shaders/shading/ directory, you can add new functions to tonemapping.sh and extend the switch statement in fs_tonemap.sc. These changes take effect immediately upon shader recompilation without requiring modifications to the C rendering system in equilibrium/systems/rendering/.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →