Repository navigation
Why color is linear
Every color in the renderer is decoded to linear light on the way in, mixed there, and encoded to sRGB once at the pixel. Arithmetic on encoded bytes gives shaded midtones that are far too dark.

A byte value of 128 is not half the light of 255. It is about 21.6 percent of it. sRGB spends more of its 256 steps on dark values, where the eye can tell them apart. Every image file and every authored color is encoded that way.
Filtering blends texels. Shading multiplies by a Lambert term. Blending mixes a translucent surface with what is behind it. Each is arithmetic on light, and it is only right where the numbers are proportional to light.
A white surface at a Lambert level of 0.4 used to come out as byte 102. The right answer is 170.
| Lambert level | Encoded bytes | Linear light |
|---|---|---|
| 0.25 | 64 | 137 |
| 0.40 | 102 | 170 |
| 0.60 | 153 | 203 |
| 0.80 | 204 | 231 |
- A material color is decoded once in
Renderer.prepare. - A texture is decoded through a 256-entry table built once per texture. The same table goes to the GPU.
- Alpha is never decoded. It is coverage, not color.
- A
LINEARtexture holds data and is not decoded.
The RenderTarget holds premultiplied linear RGBA at float precision. Compositing and filtering are both weighted sums, and a hidden color must weigh nothing:
out.rgb = src.rgb + dst.rgb * (1 - src.a)
out.a = src.a + dst.a * (1 - src.a)
Two bugs forced this. Rounding to a byte after every translucent layer lost a hundred faint layers entirely. And blending half-transparent red over fully transparent blue gave opaque purple, because the invisible blue contributed color.
resolve unpremultiplies and encodes each pixel once. PNG stores unassociated alpha, and alpha does not go through the sRGB curve. Texture mip levels are built the same way and re-encoded to bytes, as a GPU stores them.
Two white lamps at half strength make one at full strength. That is true of linear numbers and false of bytes, where half plus half comes to 128 rather than 255. Nothing is clamped until resolve, so two lamps can overexpose a white surface and the headroom survives every step.
Tutorials
How-to
Reference
- Scene graph
- Rotations
- Cameras
- Geometry
- Geometry addons
- Lofts and clipping groups
- Sculptor
- Procedural generators
- Meshes and assets
- Lines
- Points and sprites
- Helpers
- Scene objects
- Animated objects
- Raycasting
- Curves and paths
- Animation
- Skinning
- Materials
- Node materials
- TSL functions
- Lights
- Lighting addons
- Fog
- Textures
- Renderer
- Renderer hooks and material flags
- Rasterization
- GPU backend
- Render target
- Post-processing
- GPU computation
- Compute nodes
- Progressive light map
- Gaussian splats
- Voxel global illumination
- SVG renderer
- Image files
- Image decode queue
- Model files
- More model files
- Exporters
- Scene JSON
- Windowing and controls
- Math
- Norm consumers
- Math addons
- Units
- Coverage tool
- Commands
- Examples
- Benchmarks
- Extensions
- Shared physics
- Physics query snapshots
- Continuous collision
- Tick force restoration
- Water
- Animals
- Buildings
- CARLA
- Femur
- Tibia
- Fibula
- Patella
- Knee
- Muscles
- Vessels
- Lymph
- Nerves
- Integument
- Leg
- Foot
- Pelvis
- Torso
- Arm
- Hand
- Head
- Genome
- Game humanoid
- Humanoid fidelity
- Audio-aligned game faces
- Converted assets
- Mesh quality
- Segment inertia
Explanation
- Software rasterizer
- Scene graph as an array
- Linear color
- Normals are not colors
- Perspective correction
- Fixed-point coverage
- Per-fragment shading
- Mipmaps
- Sorted transparency
- Types and checks
- Render hooks as a trait
- Shared CPU and GPU code
- Bands and threads
- PNG structure
- Extensions beside the port
- CUDA teardown hang
- Mojo compiler hang