Graphics › Basics
Color is numbers
This lesson is part of the course “The ABCs of graphics”.
Everything a material does is arithmetic on numbers. Color is written as fractions between 0 and 1, and three operations build almost every shader formula: multiplication, addition and blending by a mask. They are enough to read most graphs in Unreal and Substance Designer.
Why this matters
Open any material in Unreal or any graph in Substance Designer and you will find dozens of nodes. Each one seems to hide its own piece of knowledge. In fact, most of them do the same three things to numbers. In the materials of one popular stylized-environment course, the Multiply node appears in 25 of 27. Even the engine’s “magic” functions turn out to be a couple of multiplications and blends inside.
If you understand what each action does to the picture, the graph stops being an incantation. You can predict the result before you connect the wires, and you can read someone else’s material like a page of text.
Fractions instead of 0–255
In a graphics editor’s palette, a color is often given as three numbers from 0 to 255, for example (255, 128, 64). In shaders the same color is written as (1.0, 0.5, 0.25): each number is a fraction of the maximum, from 0 to 1. To convert one to the other, divide by 255.
- (1.0, 0.5, 0.25)
- (0.5, 0.5, 1.0)
- (0.5, 0.3, 0.15)
Fractions are easy to multiply. Half of a half is a quarter, and the result stays between 0 and 1. But 128 × 128 = 16,384, and as a color that number means nothing. Fractions also do not depend on how the color is stored. Whether it takes 8 bits per channel, 16 bits or floating-point numbers, the formula in the shader is the same.
How color sits in memory and what HDR is
An ordinary image stores each channel in 8 bits, which gives 28 = 256 steps, from 0 to 255. That is where the numbers from 0 to 255 in editor palettes come from. The number of bits is called bit depth. An RGBA pixel takes 4 bytes, so an uncompressed 1024 × 1024 texture weighs 4 MB, and a 4096 × 4096 one already weighs 64 MB. Game textures are therefore almost always compressed in special formats that the graphics card reads without unpacking. The cheatsheet on texture settings shows which format suits which map.
Inside the shader, numbers are fractional and are not capped at one. The sun in an engine can be thousands of times brighter than a light bulb, and a glow can be “brighter than white.” Such values are called HDR. The screen still shows only 0..1, so at the very end of the frame the picture is compressed into that range. Tone mapping does this, and a lesson on post-processing will cover it.
The rule that follows: intermediate values in a material can go above 1, and that is fine. What matters is what ends up on the screen and in a mask.
An image is a table of numbers
A 1024 × 1024 texture is a million pixels, each holding three numbers: R, G and B. Think of it as three stacked tables, one for each channel. Each such table is a black-and-white picture: where the channel is strong, it is bright.
A black-and-white mask works the same way, with a single table. Each pixel holds one number from 0 to 1: “how much.” How much moss, how much moisture, how much dirt. There is also a fourth channel, A (alpha), which usually holds transparency or another mask. Since the channels are independent, you can pack three or four different masks into one texture, one per channel. This is called channel packing.
Everything a material does with a texture comes down to one formula that the graphics card applies to each pixel separately, millions of times per frame. The lesson on the shader explains how. For now, all you need to know is that the formula works with the numbers of one pixel.
Three operations
Multiplication is a filter
Picture a piece of colored glass. Red glass lets through almost all the red light and almost none of the green and blue. Multiplying one color by another works the same way: each channel comes through at its own fraction.
A × B = (A.r × B.r, A.g × B.g, A.b × B.b)
- Multiplying by white (1, 1, 1) changes nothing: one is the neutral value of multiplication.
- Multiplying by black (0, 0, 0) turns everything off.
- Multiplying by a color whose numbers are between 0 and 1 can only darken: a fraction of a number is never more than the number itself.
Multiplication is how you tint textures: gray stone × warm color = warm stone, and all the light and dark places of the texture stay. Light lands on a surface the same way, by multiplication. The next lesson picks that up.
Addition is light
Two flashlights aimed at one wall make a spot brighter than either alone, because their light adds up. Adding colors works the same way. Its neutral value is zero: adding (0, 0, 0) changes nothing.
The sum quickly goes past 1. In a shader that is not an error, but on the screen everything above one shows as the same white. Addition makes glow (emissive), highlights and rim light: anything that adds light instead of coloring.
Blending by a mask (lerp)
Picture a cross-dissolve in a film. One scene flows smoothly into another, and at some point the screen shows three quarters of the first scene and a quarter of the second. Lerp (linear interpolation) does the same with numbers:
lerp(A, B, t) = A × (1 − t) + B × t
t is the fraction of the second input. At t = 0 you get pure A, at t = 1 pure B, and at 0.5 an equal share of both. A mask provides its own t for every pixel. So lerp is the standard tool for laying one material over another: moss on stone, a path on grass, snow on a roof.
Notice that lerp is built from the two previous operations: two multiplications and an addition. It is a weighted average, and the weights (1 − t) and t always sum to 1. So for t from 0 to 1 the result neither darkens nor blows out. It always lies somewhere between A and B.
Paints do not mix this way. Blue and yellow paint make green because each paint absorbs part of the light, and the mixture keeps what neither of them absorbed. A lerp of blue (0, 0, 1) and yellow (1, 1, 0) at t = 0.5, on the other hand, gives gray (0.5, 0.5, 0.5).
How to brighten
You can add, or multiply by a number greater than 1, because shaders do not limit numbers to one. The results differ, though. Addition raises every value by the same amount, shadows included, so the picture looks washed out, as if seen through fog, and loses depth. Multiplying by 1.5 brightens the light parts more than the dark ones: black stays black, and contrast is kept. The next section shows how this looks on a graph.
Try it
Choose an operation and hover over any image. The calculation for that pixel appears below the images.
B: the filter color, like colored glass
The curve: what an operation does to each value
You can picture an operation on a single channel as a graph. The horizontal axis is the value before, the vertical axis the value after. The dashed diagonal means “nothing changed.” This kind of graph is called a tone curve, and it shows the character of an operation at a glance.
- Multiplication rotates the line around zero. Black stays in place, and the rest is stretched or squeezed. With a multiplier above 1, the line hits 1 early, and the light areas merge into white.
- Addition shifts the line up or down. Shifting it up lifts the black, which is the “fog.”
- Power bends the line and leaves the ends in place: 0 raised to any power is 0, and 1 stays 1. With a power above 1 the midtones darken; with a power below 1 they lighten. So power is a favorite way to raise the contrast of a mask without cutting anything off. It bends in one direction only, though: with a power above 1, the light values spread apart while the dark ones are pressed toward zero and merge. If you need contrast in both the lights and the darks, use an S-shaped curve, such as smoothstep.
Try it
Choose an operation and drag the slider. On the left is the curve; on the right is the same texture before and after. Red on the curve marks where values hit 0 or 1.
Values that hit 0 or 1 are lost to clipping. Pixels that were different become identical, and no later operation can bring the difference back. So order matters: raising the contrast first and then adding light gives a different result from doing it the other way around.
Levels, Curves and CheapContrast are the same curves
Photoshop and Krita have Levels and Curves. Levels move the black and white points, which takes a subtraction and a multiplication: first subtract the black point, then multiply so that the white point becomes one. Curves let you draw the graph by hand.
Levels(x) = (x − black point) / (white point − black point)
Substance Designer does the same with the Levels and Histogram Scan nodes, and Unreal with CheapContrast. Inside, it is a single operation:
CheapContrast(x, c) = saturate(lerp(−c, 1 + c, x))
This is a lerp whose ends are moved outside 0..1: the line becomes steeper, and saturate cuts off everything that goes past the edges. The result is a curve with kinks, a cheap contrast that eats the edges. So you choose between two nodes: CheapContrast cuts off the darkest and the lightest values, while Power keeps them but bends in one direction only.
Where it breaks
- White patches with no detail. Addition, or multiplication by a number above 1, pushed the values past one, and they clip to white. A smaller addition, or a power instead of multiplication, helps.
- A dirty, too dark color. You multiplied several dark maps together: 0.5 × 0.5 × 0.5 = 0.125. Each multiplication by a number below 1 darkens, and the darkening accumulates. A common case is multiplying crevice shading (AO) by a color where the artist has already painted the shadows.
- A washed-out, flat picture. You brightened by addition, and black stopped being black.
- Moss grew everywhere except on the stone. The lerp inputs are swapped, or the mask is inverted. Swap A and B, or run the mask through 1 − x.
- Strange stripes at the blend boundary. The mask went outside 0..1, for example after an addition, and the lerp started to go past B: at t = 1.3 you get “more than B.” Put
Saturatebefore the lerp.
Connections: the same idea in other fields
- AnimationBlending poses is a lerp: between walking and running in proportion to speed, or between two animations during a transition. An animation weight is the same t. Animation: blending and IK
- SoundVolume multiplies the signal: a multiplier of 0.5 halves the amplitude. A smooth transition between two tracks is a lerp. Two sounds playing at once add up, and the sum can also “blow out.” That is overload, and you hear it as crackle. Sound as data
- Code and mechanicsA smooth camera follow is often coded as a lerp toward the target every frame. The catch: the speed then depends on the frame rate, and at 144 fps the camera catches up faster than at 30. The lesson on interpolation shows the fix. A change of time of day is a lerp between sky colors.
- Game designBonuses can be added (+10% and another +10%) or multiplied (×1.1 × 1.1). These are different economies: with many bonuses, the multiplied one grows noticeably faster. Progression and economy
- EffectsFire, sparks and magic are usually drawn with addition: the particle’s color is added to what is behind it. Many sparks in a row therefore make a white blob.
Key points
- In a shader, a color is three numbers from 0 to 1, fractions of the maximum. During calculations they can go past 1.
- An image is a table of numbers per channel; a mask is a table with one number per pixel.
- Multiplication is a filter: the neutral value is 1, and with numbers up to 1 it only darkens. Light on a surface is multiplication.
- Addition is light: the neutral value is 0, and it clips quickly. Light plus light is addition.
- Lerp(A, B, t) blends two inputs, and t is the fraction of the second. A mask provides t for every pixel.
- Any operation on a channel can be drawn as a curve. Whatever hits 0 or 1 is lost.
Test yourself
1. A gray stone texture (0.5, 0.5, 0.5) is multiplied by a tint (1.0, 0.6, 0.3). What do you get?
2. An artist wants to make a stone lighter. What will work?
3. In one pixel the moss mask equals 0.25. What will be there after lerp(stone, moss, mask)?
4. A red apple (1, 0, 0) is lit by pure blue light (0, 0, 1). What color will it be?
5. After an addition, white patches with no detail appeared on the stone. What happened?
6. Which operation will raise the contrast of a mask without moving pure black and pure white?
7. The moss grew everywhere except where the mask is white. What is the simplest way to fix it?
Mentioned in
Something still unclear? Select that piece of text and press “Ask about this”. The question goes to the author, and the place gets rewritten more clearly.