RGB(A), luminance & blend modes
Color as a vec3 or vec4, what alpha means, how bright a color looks, and the math behind multiply, screen, overlay and add.
How it works
To a shader a color is a vector: red, green and blue in a Vector A group of two to four numbers handled as one value (vec2, vec3, vec4). The same type holds a position, a direction, a color or a UV. Linear interpolation (mix) Blends from a to b by a factor t, along a straight line. GLSL calls it mix; HLSL and most node editors call it lerp.vec3, from 0 (none) to 1 (full), plus an optional fourth
value, alpha, in a vec4. Everything from vector basicsA group of two to four numbers handled as one value (vec2, vec3, vec4). The same type holds a position, a direction, a color or a UV..rgb and .a are swizzles, and mixBlends from a to b by a factor t, along a straight line. GLSL calls it mix; HLSL and most node editors call it lerp.mix(a, b, t) = a (1 − t) + b t
Arithmetic as light
The basic operations each have a physical reading, which makes them easy to choose:
vec3 base = vec3(0.8, 0.6, 0.4);
vec3 tint = base * vec3(1.0, 0.5, 0.5); // multiply: a colored filter; only darkens
vec3 lit = base + vec3(0.2); // add: more light on top; only brightens
vec3 dim = base * 0.5; // scale: the same color, half the light
vec3 blend = mix(base, vec3(0.1, 0.2, 0.9), 0.3); // 30% of the way toward blue
Multiplying by white (vec3(1.0)) changes nothing and by black gives black, which is why textures and lighting are
combined by multiplying. Values above 1 are fine inside a shader, as bright light that hasn’t been mapped to the screen
yet; the output clamps them to 1 unless it is rendering to a floating-point target. Negative values have no meaning as
color, and usually come from a subtraction or a sharpen that needs a max(c, 0.0).
Alpha
Alpha is the fourth channel. By convention it means coverage or opacity: 1 is solid, 0 is invisible. The shader only
outputs it; whether it does anything depends on blending, which is set outside the shader. With blending off, alpha
does not mix this draw with what is already there; it is simply stored in the target. Most targets ignore it, but a
WebGL canvas with the default alpha: true uses it to composite over the page, so write 1.0 there unless you want the
page to show through.
There are two ways to store a color with alpha. Straight alpha keeps the color as it is, and opacity separately.
Premultiplied alphaA color stored already multiplied by its opacity. It filters and blends without dark fringes and can express additive light. Premultiplied alpha A color stored already multiplied by its opacity. It filters and blends without dark fringes and can express additive light.over: out = src + dst · (1 − src.a)vec4(0.5, 0.0, 0.0, 0.5). Layering a color over a background (“over”) reads:
vec3 overStraight = src.rgb * src.a + dst.rgb * (1.0 - src.a); // straight alpha
vec3 overPremul = src.rgb + dst.rgb * (1.0 - src.a); // premultiplied
Premultiplied colors filter and blur correctly (a fully transparent texel contributes nothing, whatever color it stores) and can express additive glow (alpha 0 with color above 0). Mixing the two conventions by accident gives dark or bright fringes around soft edges.
Luminance
The three channels don’t look equally bright. Eyes are most sensitive to green and least to blue, so full green looks far brighter than full blue. Luminance weights the channels by that sensitivity; for the sRGB and Rec. 709 primaries used by screens the weights are:
float luminance(vec3 c) { return dot(c, vec3(0.2126, 0.7152, 0.0722)); }
The weights sum to 1, so white stays 1. LuminanceHow bright a color looks, from its linear red, green and blue weighted by the eye's sensitivity to each. Luminance How bright a color looks, from its linear red, green and blue weighted by the eye's sensitivity to each.Y = 0.2126 R + 0.7152 G + 0.0722 Bvec3(luminance(c))), to adjust saturation (mix(vec3(luminance(c)), c, amount): 0 is gray, 1 the original, above 1
more vivid), to pick bright areas for a glow, or to run an edge detector on one channel instead of three. These weights
are meant for linear values; the next page, on gamma, explains the difference.
Blend modes
Image editors name a handful of ways to combine a base color a with a layer b. Each is a line of channel-wise
math on values from 0 to 1:
- Multiply
a * b: darkens, like stacking two slides. White is neutral. - Screen
1.0 - (1.0 - a) * (1.0 - b): brightens, the inverse of multiply. Black is neutral. - Add
a + b: brightens and can pass 1; glows, fire and light are added. - Overlay: multiply where the base is dark, screen where it is light, which raises contrast. 50% gray is neutral.
Overlay has a branch per channel, which step and mix write without an if:
vec3 overlay(vec3 a, vec3 b) {
vec3 dark = 2.0 * a * b; // multiply, doubled
vec3 light = 1.0 - 2.0 * (1.0 - a) * (1.0 - b); // screen, doubled
return mix(dark, light, step(0.5, a)); // pick per channel by the base
}
To apply any mode at partial strength, mix from the base to the result: mix(a, overlay(a, b), opacity).
The colors in this site’s cards follow the same math. A color you set in a card’s controls is written as you see it and arrives in the shader already converted to linear values, which is what makes the luminance weights and the light arithmetic above hold.