Add light in linear, not in gamma

Photons add. The numbers in an image file do not, because they carry a gamma curve baked in so that 8 bits are spent where the eye can see them. Add two of those numbers together and you have performed an operation that corresponds to nothing physical.

Three colored lights, same values, same falloff. Left: added in display space. Right: added in linear, encoded once at the end.

The place to look is the dim outer falloff, not the bright centers. On the left the whole surround is lifted into a broad, pale, washed-out haze; on the right it stays dark and the colors stay separate. Same numbers, same glow function, same output space. The only difference is where the addition happened.

Why the error is always in the same direction

The sRGB curve is concave: it lifts dark values a long way and bright values very little. So encoded numbers are systematically larger than the linear light they represent, and adding two of them overshoots. Gamma-space additive blending always errs toward too bright, never too dark.

And because the lift is largest at the bottom of the range, the error is worst in the dim regions, which is the opposite of where most people look for it. An encoded 0.1 represents about 0.006 of linear light, roughly sixteen times less than the number suggests. Add a few of those together in the wrong space and a falloff that should be nearly black becomes a visible gray wash. That is why the symptom usually shows up as "my glows are muddy" rather than as blown-out cores.

It also explains the hue shift. The three channels are lifted by different amounts depending on their values, so the ratio between them, which is what hue is, changes as you add.

Top: a 0 to 1 ramp shown raw. Bottom: the same values treated as linear and encoded. The mark is at 0.5.

The two strips are the same numbers. Half-way along, the top strip is mid-gray to the eye, while the bottom is much darker, because linear 0.5 is genuinely half the light and half the light does not look half as bright. That gap is the entire problem in one picture.

The rule

// decode anything authored as sRGB
vec3 lin = pow(srgbColor, vec3(2.2));

// do ALL the work here: add, multiply, blur, blend, tonemap
lin = lightA + lightB + lightC;

// encode exactly once, at the very end
vec3 out = pow(lin, vec3(1.0/2.2));

Once, at the end. Every extra round trip costs precision, and every operation done on the wrong side of it is wrong in a way that looks almost right, which is what makes this bug so durable.

What it breaks besides blending

Rules of thumb

  1. Decode on input, encode once on output. Everything between is linear.
  2. If overlaps look too bright and too pale, suspect this before you touch the values.
  3. 2.2 is a fine approximation. The exact sRGB piecewise curve matters near black.
  4. Your framebuffer format may already be doing the conversion. Doing it twice is its own bug.
  5. Middle gray is not 0.5. Authored 0.5 is roughly 0.21 in linear.

All 61 notes How to use them Credits