sRGB vs linear colour in shaders
Shaders and game engines often represent the same colour in two different ways: display and linear. Display (sRGB) values are what you use in everyday work — websites, Photoshop, UI, and most colours you pick by eye. Linear values show up mainly in lighting maths, and when colours are blended or mixed. Use the wrong set and midtones often look too bright or washed out.
Jump to engines: Unity · Unreal · Godot · WebGL and WebGPU
What is a display colour?
A display colour — also called sRGB or gamma-space — is the number system screens and ordinary images use.
Human vision notices small differences in dark tones more than in bright ones. If brightness were stored in evenly spaced steps from black to white, you would waste detail on highlights you barely see and get banding in the shadows. So sRGB stores colour on a curve: more of the available codes go to the dark end, where the eye is picky.
That is why the familiar mid grey from a design tool sits near 128, 128, 128, or about 0.5 when written as floats from 0 to 1. Those numbers describe how bright the colour looks on a monitor, not how much light is in a lighting calculation.
// Display (sRGB): mid grey as it appears on screen
vec3 displayGrey = vec3(0.5, 0.5, 0.5);
What is a linear colour?
A linear colour is the number system lighting maths wants.
In the real world, light adds in a straight line. Two identical lamps produce twice the light of one. Display values do not work that way — they sit on the sRGB curve — so you cannot multiply, blend, or light with them and expect a physically sensible result.
For the same mid grey on screen, the linear value is about 0.214, not 0.5:
// Linear: same mid grey, for lighting and blending
vec3 linearGrey = vec3(0.214, 0.214, 0.214);
Put the display 0.5 into lighting code and the engine treats it as brighter light. Greys lift, contrast drops, and the result looks washed out.
The same idea shows up when mixing colours. A 50/50 blend of red and green done with display values comes out too dark. Done with linear values, it matches mixed light more closely. Lighting, alpha blending, mipmaps, bloom and anti-aliasing all want linear numbers for that reason.
Which should I use?
| Situation | Use |
|---|---|
| Lights, material multiply, blending, bloom, other lighting maths | Linear |
| Flat colour with no lighting maths | Display (sRGB) |
Non-colour data — normal maps, roughness maps, masks — is already in the form maths expects. Do not convert those as if they were sRGB colours.
Every colour on the picker shows both forms. Copy the one that matches the row above.
What each engine expects
Unity
Color holds display values as floats from 0–1. Color32 is the same kind of colour as whole numbers from 0 to 255. In a Linear colour-space project, use color.linear when you pass a value into lighting or a shader yourself. Assigning a colour to a material or UI Image is usually handled for you.
Full walkthrough: Unity Color vs Color32.
Unreal
FLinearColor expects linear values — not display (sRGB) floats from 0–1. FColor is whole numbers from 0 to 255 when you are not doing lighting maths. Put display values into FLinearColor and greys often look too bright and faded.
Full walkthrough: Unreal FLinearColor vs FColor.
Godot
Color holds display values in most contexts. Godot 4 exposes srgb_to_linear() when you need the linear form.
WebGL and WebGPU
Depends on your texture and surface formats. An sRGB texture is usually converted when sampled; one without that flag is not. Prefer an sRGB output surface when you can, so the GPU handles the final conversion to display values.
Advanced: the exact conversion
Most code approximates the curve as pow(2.2). The real sRGB function is slightly different — a short straight segment near black, then a 2.4 power. This site uses the real curve:
// Display (sRGB) -> linear
float srgbToLinear(float c) {
return c <= 0.04045
? c / 12.92
: pow((c + 0.055) / 1.055, 2.4);
}
// Linear -> display (sRGB)
float linearToSrgb(float c) {
return c <= 0.0031308
? c * 12.92
: 1.055 * pow(c, 1.0 / 2.4) - 0.055;
}
For mid grey, display 0.5 becomes linear 0.2140. The pow(0.5, 2.2) shortcut gives 0.2176 — close, but off where dark tones and banding matter.
Related guides:
- Unity Color vs Color32
- Unreal FLinearColor vs FColor
- OKLCh explained — for choosing colours and building scales, rather than storing them
Questions
Why does mid grey look wrong as 0.5, 0.5, 0.5 in a shader or FLinearColor?
0.5 is a display (sRGB) value — the usual 0–1 form of mid grey on screen. Lighting maths expects a linear value for the same grey, about 0.214. Put 0.5 into lighting code and the grey looks too bright and washed out.
Should I use sRGB or linear values in my shader?
Use linear for lighting, blending, and anything that mixes colours. Use display (sRGB) values when you only need the colour as it appears on screen and you are not doing lighting maths. When the colour goes into a light or a multiply, use linear.
Is sRGB the same as gamma 2.2?
Almost. The real sRGB curve is a bit more precise than a simple pow(2.2). For most game work the shortcut is fine; for dark tones and banding, use the real conversion (this site does).
Why does my colour look washed out in Unreal?
You almost certainly put display (sRGB) values into an FLinearColor. Unreal treats those numbers as linear light, so mid grey at 0.5 becomes far brighter than intended. Use about 0.214 instead, or copy FLinearColor from a tool that outputs linear values. See the Unreal FLinearColor vs FColor guide.
More guides
- OKLCh explained, and why HSL keeps letting you downHow OKLCh works, why its lightness matches what you see when HSL lightness does not, and how to use it to build tonal scales that actually look even.
- Choosing accessible text coloursHow WCAG 2 contrast ratios work, where they go wrong, what APCA does differently, and a practical method for picking text colours that pass and look right.
- Hex colour codes explainedWhat the six digits in a hex colour actually mean, how the shorthand and alpha forms work, and how to read or write one by hand.