Orrery
A GPU-accelerated N-body gravitational simulator
Loading...
Searching...
No Matches
tone_map.hpp File Reference

Turning accumulated light into pixels. More...

#include <array>
#include <cstdint>
#include <string_view>

Go to the source code of this file.

Classes

struct  orrery::viz::ToneMapping
 The two numbers that decide how bright the picture is. More...

Functions

float orrery::viz::tone_curve (float radiance, ToneMapping mapping) noexcept
 One channel through the curve, clamped to [0, 1].
std::uint8_t orrery::viz::encode_srgb (float linear) noexcept
 The sRGB transfer function, from a linear value in [0, 1] to a byte.
std::array< std::uint8_t, 3 > orrery::viz::to_display (float red, float green, float blue, ToneMapping mapping) noexcept
 A linear radiance triple through the curve and the encoding, ready to store.
constexpr std::string_view orrery::viz::tone_curve_glsl () noexcept
 The same curve as GLSL, for the shader that applies it on the device.

Detailed Description

Turning accumulated light into pixels.

A star field rendered with additive blending has no upper bound on brightness. The centre of a galactic bulge may hold a thousand times the light of its outskirts, and a display holds two hundred and fifty-six values per channel. Writing the accumulated value straight to the screen gives a picture that is a solid white disc surrounded by black, which is not a failure of the renderer but of the last step in it.

So the accumulated radiance is compressed by a curve that is close to linear where the image is dark and flattens where it is bright, and only then encoded for display. That is what a camera's response curve does and why a photograph of a night sky shows both the bright core and the faint arms.

Which curve

The extended Reinhard operator: L (1 + L / W^2) / (1 + L), where L is the exposed radiance and W the value chosen to map exactly to white. It is the simplest curve with the two properties this needs. It is linear at the dark end, so faint structure keeps its relative brightness and the outer disc does not turn grey. And it reaches exactly one at W rather than approaching it, so there is a stated brightness at which the image saturates rather than an asymptote that never quite gets there and leaves the brightest pixel at 254.

The filmic curves used in games are better at making an image look photographic and worse here for exactly that reason: they apply a toe that crushes the darkest values, and the faintest parts of a tidal tail are the thing the picture exists to show.

The curve is applied to each channel rather than to a luminance. That is what makes a bright region desaturate towards white as it saturates, which is how an overexposed light behaves and what makes the core of a galaxy read as bright rather than as intensely coloured.

Two spellings of one curve

The interactive renderer applies this in a fragment shader and the offline export applies it here, on the host. The reason is in ADR-0036: what is exported has to be reproducible from the accumulated buffer alone, and a fragment shader's arithmetic is the driver's business. So the curve is written twice, once in C++ below and once in the GLSL beside it. They are kept in the same file, adjacent, because that is the only real guard against them drifting apart; the properties both must have are what tests/viz/tone_map_test.cpp checks of the one it can reach.

Function Documentation

◆ tone_curve()

float orrery::viz::tone_curve ( float radiance,
ToneMapping mapping )
nodiscardnoexcept

One channel through the curve, clamped to [0, 1].

The clamp matters above white_point: the extended Reinhard curve continues past one rather than saturating at it, and a pixel that came out at 1.4 would wrap to a dark value when it was converted to a byte.

◆ encode_srgb()

std::uint8_t orrery::viz::encode_srgb ( float linear)
nodiscardnoexcept

The sRGB transfer function, from a linear value in [0, 1] to a byte.

Separate from the curve because it is not a choice. Displays are sRGB, the encoding is defined by a standard, and the interactive path does not call this at all: it writes linear values into an sRGB framebuffer and lets the hardware apply exactly this function. Doing it here by hand for the export path is therefore not a second implementation of a decision but the same standard function, applied where no hardware is doing it.

◆ tone_curve_glsl()

std::string_view orrery::viz::tone_curve_glsl ( )
nodiscardconstexprnoexcept

The same curve as GLSL, for the shader that applies it on the device.

A function vec3 orrery_tone_curve(vec3, float, float) and nothing else, so that the shader that includes it can be read beside this file and compared line for line. It does not encode for display: the interactive path renders into an sRGB framebuffer and the hardware does that.