# 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.**

- Repository: [Alexander/equilibriumengine](https://github.com/clibequilibrium/equilibriumengine)
- Tags: 
- Published: 2026-02-27

---

**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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/equilibrium/systems/rendering/base_rendering_system.c), the engine selects `BGFX_TEXTURE_FORMAT_RGBA16F` when HDR mode is enabled:

```c
// 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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/base_rendering_system.c), the system creates `vec4` uniforms during initialization and updates them each frame before the blit operation:

```c
// 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:

```glsl
// 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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/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`:

```c
// 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:

```c
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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/launcher/sandbox/shaders/shading/tonemapping.sh):

```glsl
vec3 tonemap_mycustom(vec3 c) {
    // Simple filmic approximation
    return c / (c + vec3_splat(0.85));
}

```

2. **Add the case** in `launcher/sandbox/shaders/shading/fs_tonemap.sc`:

```glsl
#define TONEMAP_MY_CUSTOM 8

// In the switch statement:
case TONEMAP_MY_CUSTOM: 
    result.rgb = tonemap_mycustom(result.rgb); 
    break;

```

3. **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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/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/`.