Part II · What light is · Chapter 6
Colour
Chapter 5 measured how bright light is. A light also has a colour, and a renderer has to carry it on every ray. ShadyTracer carries three numbers per ray, red, green and blue. This chapter is why three are enough for the eye, what they mean exactly, why one of them can be negative, and how a light given the way a lamp's datasheet gives it (a colour temperature, or a point on a chart) becomes those three.
1Three numbers for a colour
Light comes in wavelengths, from violet at about 380 nanometres to deep red at about 780, and a light's spectrum says how much of it there is at each. The eye does not see the spectrum. It has three kinds of cone cell, each sensitive to a broad range of wavelengths, and all it keeps of a light is how strongly each kind responds: three numbers. Two lights with different spectra that give the same three look exactly the same.
In 1931 the CIE, the body that sets these standards, fixed the three, from experiments in which people matched colours, as colour-matching functions: x̄, ȳ and z̄, the three curves in the figure. Weigh a spectrum by each curve and add up over the wavelengths, and you have its X, Y and Z, the numbers every colour standard since is built on. In the experiments a viewer matched a test light by mixing three lamps, and some colours could be matched only by adding a lamp to the test side instead: a negative amount, already in 1931. X, Y and Z are those results rearranged so that no curve is ever negative. They are not the cones' own curves, nor red, green and blue (x̄ even has a second bump in the blue), but two lights the cones cannot tell apart have the same X, Y and Z. ȳ is the eye's sensitivity to brightness, so Y is chapter 5's luminance (for a whole lamp rather than a surface, its lumens). Here the light is a narrow band of green at 530 nm (the dashed curve, its shape only): mostly ȳ, a little x̄. Slide its wavelength along by its handle.
Now a hot body, the glow of something heated (a lamp's filament, the sun), at 6500 K: a broad spectrum over every wavelength, and three numbers close to equal. That is about what daylight's white is. Its exact red, green and blue (the next section says what those are) come out 1.042, 0.984 and 1.034.
At 2700 K, a warm lamp, the spectrum leans to the red, and so does its colour: X is 1.11 of Y, Z only 0.32.
The curves here are not the CIE's table but a fit to it, a few bell curves added together, which ShadyTracer uses for its coloured coatings and prisms (chapters 10 and 11); its test holds it against published values of the table. A fit has errors: a hot body at 3000 K through these curves comes out a hair off, as if it were a hot body at 3024 K (section 2 says how a colour is given a temperature). A light given by its spectrum is coloured by the CIE's own table instead, every nanometre from 360 to 830 (), where the same hot body reads 3000 K within half a kelvin.
2A map of colours
X, Y and Z mix colour and brightness. Divide each by their sum, and the brightness drops out: x = X / (X + Y + Z) and y = Y / (X + Y + Z) are a light's chromaticity, and every colour is one point on this map. Round its curved edge are the single wavelengths of the first figure (460 to 620 nm labelled); every real light lies inside, since it is a sum of them. The straight edge closing it at the bottom, the line of purples, is no single wavelength: it is the mixes of the deepest violet and the deepest red. Two lights mixed lie on the straight line between their points: their X, Y and Z add, and the mix's point works out to an average of the two points, each counted by its own X + Y + Z (the fold-out shows it), and an average of two points always lies on the line between them.
A screen makes colours from three lights of its own, its primaries: the dashed triangle is those of Rec.709, the standard ordinary computer screens and television use, and the one ShadyTracer's red, green and blue are in. The screen can show only what lies inside its triangle, its gamut. The orange line is where the hot bodies lie, 1000 K at the red end to 15,000 K at the blue, and the ringed dot is D65, the standard daylight white a Rec.709 screen shows for equal red, green and blue (the small ringed dot labelled D65). Drag the point (the larger ring). On the right, its red, green and blue at luminance 1, and below them what the screen shows.
The point is now on the hot bodies' line at 3000 K. A light near the line has a colour temperature: the temperature of the nearest hot body. How far it is from the line is its Duv, measured at right angles to the line on an older map, CIE 1960's, where equal distances look more nearly equally different: positive towards green, negative towards pink. "Nearest" is judged on that map too. Its coordinates u and v are x and y redrawn, u = 4x / (−2x + 12y + 3), v = 6y / (the same); the readout also gives u′ and v′, a 1976 stretch of it (u′ = u, v′ = 1.5 v) that colour differences are quoted in. This is how lamps are specified: "3000 K, Duv 0".
Here, 4000 K at a Duv of +0.02: off the line towards green, a yellow-green that a lighting designer would call greenish and a standard would reject (they allow a few thousandths).
And here a royal-blue LED, a colour lamps really use, outside the triangle, just past its blue corner: small as the distance looks, near the edge of purples it takes very large numbers. Its red, green and blue at luminance 1 are 1.45, −2.57 and 35.0. "At luminance 1" fixes only the colour, its brightness set later, so a number above 1 is no problem here: only a screen has a ceiling. Blue counts for little brightness (0.0722 of it, the fold-out's middle row), so a lot of it is needed: 0.2126 × 1.45 − 0.7152 × 2.57 + 0.0722 × 35.0 is 1.
And a negative green: no amount of the screen's three lights mixes to this colour; the sum needs green taken away. ShadyTracer keeps that negative number on the ray. Colours add, and where the materials on the way treat the three channels alike (white, grey, clear glass), what arrives at a detector turns back into exactly the X, Y and Z the LED was given. Clipping earlier would break that: take two lights (0, −1, 3) and (0, 2, 0); added, (0, 1, 3); clipped first, (0, 0, 3) and (0, 2, 0), added (0, 2, 3), twice the green. So only the screen clips, once, at the very end, keeping the luminance: 1.13, 0 and 10.5. That clip only removes the negative channel; a number above 1 is for the exposure and the tone curve to bring down (chapter 7).
The mathsFrom a spectrum to the screen's red, green and blue
With S(λ) the spectrum, X, Y and Z are sums over the wavelengths, every nanometre here:
X = Σ S(λ) x̄(λ), Y = Σ S(λ) ȳ(λ), Z = Σ S(λ) z̄(λ)
Σ means "add up over every wavelength λ". Rec.709's red, green and blue are three particular X, Y, Z triples, the columns of a 3 × 3 matrix M, a table of nine numbers (M₁₂ is row 1, column 2). A colour R, G, B is M times (R, G, B) in XYZ, which means three weighted sums: X = M₁₁R + M₁₂G + M₁₃B, and the same with the second row for Y and the third for Z. M⁻¹, its inverse, is the table that undoes it. Going back, (R, G, B) = M⁻¹ (X, Y, Z), and nothing in that stops a channel being negative. The middle row of M is Y: 0.2126 R + 0.7152 G + 0.0722 B, chapter 5's luminance. From a chromaticity and a luminance Y back to XYZ: X = x Y / y, Z = (1 − x − y) Y / y (since X + Y + Z = Y / y).
Because all of it is weighted sums, the XYZ of two lights added is their XYZs added, and so are their R, G, B (it is linear). That is what makes a negative channel safe to carry, and mixing exact (section 3).
And why a mix lies on the line: write S₁ = X₁ + Y₁ + Z₁ for the first light, so its x₁ = X₁/S₁, and the same for the second. The mix's x is (X₁ + X₂)/(S₁ + S₂) = (S₁x₁ + S₂x₂)/(S₁ + S₂): an average of x₁ and x₂, weighted by S₁ and S₂. The same holds for y, with the same weights, so the mix's point is that weighted average of the two points: on the line between them, nearer the light with the larger S.
3Mixing lights
A white LED lamp is often three or more coloured LEDs mixed, and the question is how much of each. Because colours add as XYZ, it is one sum. Write Φ for each LED's lumens, the unknowns, and Xr for the red LED's X per lumen, and so on:
XrΦr + XgΦg + XbΦb = Xtarget, and the same for Y and for Z
Three equations, three unknowns, solved as at school by elimination; the code does the same in one step with M⁻¹ (the fold-out above). These are the LEDs of ShadyTracer's developer's own luminaire, a red, a green and a royal blue (the handles), mixed to 4000 K at 1000 lumens: 310.5 lumens of red, 673.0 of green and 16.5 of blue, the split the developer's own colour tool gave.
Cooler, 6500 K: more blue (30.8 lm) and green, less red. Drag an LED and the triangle and the split move with it.
Warmer, 1500 K: the target has moved just outside the triangle, past its edge from red to green, on the side away from blue. The solve still answers, with −0.6 lumens of blue. No LED gives negative light: three lights can mix only to colours inside their triangle.
mixToTarget. The dashed triangle is the LEDs' reach, the small dot the target.
The developer's LEDs puts them back.4How ShadyTracer does it
Why the renderer carries a colour this way, in the words of the file that does it:
A light can be given four ways, and each becomes the same thing: its exact red, green and blue, scaled so its luminance is 1 (its brightness is set separately, chapter 5):
Where light is added into a pixel, a negative channel is let through and only a negative luminance refused:
And the screen clips once, a pixel at a time, keeping the luminance (), with this:
The mix of section 3 is three lines: each LED's XYZ per lumen as a column, the target's XYZ, and the inverse:
A number in the comment, checked. The file's header gives its line of hot bodies as "within about 3e-4 of the true locus in xy". Against the seven tabulated points its own test holds it to (which allows 5e-4), the furthest is 3.8 × 10⁻⁴, at 2856 K: about right, a little over.
5Tricks and trade-offs
Each is stamped with how it is known; point at a stamp to see what it means.
Three channels, not a wavelength per ray. A renderer could trace one wavelength at a time and
turn it into a colour at the end. ShadyTracer chose not to: "a white source traced one wavelength at a time is
colour noise everywhere" (roadmap/PLAN.md, Colour, the hybrid, agreed 2026-09-27). Three
channels are exact for a light given by its chromaticity, through materials that treat the three alike. Only
where a material sorts wavelengths, a prism or a coloured coating, does a ray take a wavelength (chapter
11). The optics view (chapter 31) can be asked for more: three wavelengths a ray, drawn from the light's own
spectrum, in the same three numbers. The picture stays three channels.
Exact, measured. ShadyTracer's optics view traces rays from the lights themselves (photons,
chapter 18) into a detector, a surface that adds up the light it catches. A detector read back the colour each
light was given
(the optics-colour test, the user's Pascal card, 2026-09-27, docs/TESTING.md): a 3000 K
light to 9 × 10⁻¹¹ in u′v′, a 6500 K one through glass 5 × 10⁻⁹, the royal blue 8 × 10⁻⁹. The LED mixes read
0.0012 and 0.0005, the counting noise of about 1650 blue rays: each ray comes from one LED, picked at random,
and the blue one gives few.
A colour converges after its brightness. The same three LEDs mixed to 4000 K, lighting a
floor in the picture (the optics-light test, Pascal): the first press read the right lux but 4247
K, from a sensor 3 × 3 pixels wide that had caught about 17 blue photons, where one standard deviation is 250 K.
Read over the whole frame, about a million hits, it gave 3999.9 K.
6What went wrong before
- Every photon clipped. Light carried by photons was clipped into the screen's gamut as each one
was added to a pixel. A mix of saturated LEDs is white only if their exact colours add up; clipped one by one,
each lost its negative channel, and the mix shifted. Now only the screen clips, once
(
roadmap/PLAN.md, Colour, the hybrid). - A coloured light that lit its scene white. The table of light sources the optics trace reads
left the colour out (the comment in
builder/sources.js). - A Duv that was not one. The colour tool ShadyTracer's formulas came from measured Duv in u′v′,
the later map where v′ is 1.5 times the 1960 map's v, so its "0.02" was not a Duv of 0.02; ShadyTracer measures
it in 1960 uv, as the standard does (
colorimetry.js's header). - A colour that could not be specified. A light used to be a red, green and blue and a luminance: "fine for a picture and wrong for LED work", where a light is a chromaticity and a flux.
7Try it
- In the first figure, slide a narrow band from 450 to 650 nm. Where is its exact red negative, and where its blue? Widen the band: what happens to its colour, and why?
- In the second, drag the point along the edge of the triangle, then just outside it. Which channel goes negative outside each side?
- In the third, drag the green LED towards the hot bodies' line. Which targets can the three no longer reach?
- In ShadyTracer, give a light a colour temperature (a light's colour can be set by CCT and Duv) and light a white wall with it: 2700 K, then 6500 K.