|
Orrery
A GPU-accelerated N-body gravitational simulator
|
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. | |
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.
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.
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.
|
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.
|
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.
|
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.