Generated LUT Color Grading
by GameDev.net · GLSL ES 3.00 (WebGL2) · 30 Aug 2026
Run the shader to adjust these controls.
What it demonstrates
A sampled three-dimensional colour transform can be arranged as a two-dimensional atlas and reconstructed with interpolation. This package generates both a colourful source scene and a viewport-sized atlas with sixteen blue slices, so the complete grading pipeline is visible without depending on an external image or authored lookup texture.
Shader inputs
void mainImage(out vec4 fragColor, in vec2 fragCoord)
Called once per pixel. Write the colour to fragColor.
-
iResolutionvec3 - Viewport size in pixels (z is the pixel aspect ratio).
-
iTimefloat - Seconds since the shader started.
-
iTimeDeltafloat - Seconds since the previous frame.
-
iFrameRatefloat - Frames per second, smoothed.
-
iFrameint - Frames rendered since the start.
-
iMousevec4 - Mouse position: xy while held, zw of the last click.
-
iDatevec4 - Year, month, day, and seconds within the day.
-
iChannel0sampler2D - Texture bound to channel 0.
-
iChannel1sampler2D - Texture bound to channel 1.
-
iChannel2sampler2D - Texture bound to channel 2.
-
iChannel3sampler2D - Texture bound to channel 3.
-
iChannelResolutionvec3[4] - Pixel size of each bound channel texture.
-
iChannelTimefloat[4] - Playback time of each channel, in seconds.
-
iSampleRatefloat - Audio sample rate, always 44100.
Common
uniform float uLutContrast; // @param 0.7..1.4 = 1.12 "LUT contrast"
uniform float uLutSaturation; // @param 0.0..1.5 = 0.88 "LUT saturation"
uniform float uLutWarmth; // @param -0.2..0.2 = 0.08 "LUT warmth"
vec3 applyGrade(vec3 color) {
color = (color - 0.5) * uLutContrast + 0.5;
float luma = dot(color, vec3(0.2126, 0.7152, 0.0722));
color = mix(vec3(luma), color, uLutSaturation);
color += vec3(uLutWarmth, uLutWarmth * 0.25, -uLutWarmth * 0.7);
color.r += color.b * color.b * 0.06;
return clamp(color, 0.0, 1.0);
}
void lutTilePixelBounds(vec2 tile, out vec2 firstPixel, out vec2 lastPixel) {
firstPixel = floor(tile * iResolution.xy / 4.0);
lastPixel = floor((tile + 1.0) * iResolution.xy / 4.0) - 1.0;
}
vec3 renderChart(vec2 p, float time) {
vec3 color = vec3(p.x + 0.5, p.y + 0.5, 0.5 + 0.25 * sin(time * 0.35));
color = clamp(color, 0.0, 1.0) * 0.55;
vec3 patches[6] = vec3[6](
vec3(0.90, 0.12, 0.08), vec3(0.95, 0.62, 0.08), vec3(0.12, 0.78, 0.25),
vec3(0.08, 0.62, 0.92), vec3(0.34, 0.18, 0.88), vec3(0.88, 0.16, 0.62)
);
for (int i = 0; i < 6; i++) {
float fi = float(i);
vec2 center = vec2(-0.39 + mod(fi, 3.0) * 0.39, 0.24 - floor(fi / 3.0) * 0.28);
vec2 q = abs(p - center) - vec2(0.14, 0.09);
float mask = 1.0 - smoothstep(0.0, 0.012, max(q.x, q.y));
color = mix(color, patches[i], mask);
}
float ramp = smoothstep(-0.42, 0.42, p.x);
float rampMask = 1.0 - smoothstep(0.0, 0.012, abs(p.y + 0.34) - 0.055);
color = mix(color, vec3(ramp), rampMask);
return color;
}
vec2 chartPoint(vec2 fragCoord) {
return (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
}
Buffer A
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
fragColor = vec4(renderChart(chartPoint(fragCoord), iTime), 1.0);
}
Buffer B2
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 pixel = floor(fragCoord);
vec2 tile = clamp(ceil((pixel + 1.0) * 4.0 / iResolution.xy) - 1.0, 0.0, 3.0);
vec2 firstPixel;
vec2 lastPixel;
lutTilePixelBounds(tile, firstPixel, lastPixel);
vec2 redGreen = (pixel - firstPixel) / max(lastPixel - firstPixel, vec2(1.0));
float blue = (tile.x + tile.y * 4.0) / 15.0;
vec3 sourceColor = vec3(redGreen, blue);
fragColor = vec4(applyGrade(sourceColor), 1.0);
}
Main Image3
uniform float uStrength; // @param 0.0..1.0 = 1.0 "Grade strength"
vec2 lutCoordinate(vec2 redGreen, float slice) {
vec2 tile = vec2(mod(slice, 4.0), floor(slice / 4.0));
vec2 firstPixel;
vec2 lastPixel;
lutTilePixelBounds(tile, firstPixel, lastPixel);
vec2 tileMin = (firstPixel + 0.5) / iResolution.xy;
vec2 tileMax = (lastPixel + 0.5) / iResolution.xy;
return mix(tileMin, tileMax, clamp(redGreen, 0.0, 1.0));
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
vec3 sourceColor = clamp(texture(iChannel0, uv).rgb, 0.0, 1.0);
float blueIndex = clamp(sourceColor.b, 0.0, 1.0) * 15.0;
float lowSlice = floor(blueIndex);
float highSlice = min(lowSlice + 1.0, 15.0);
vec3 lowSample = texture(iChannel1, lutCoordinate(sourceColor.rg, lowSlice)).rgb;
vec3 highSample = texture(iChannel1, lutCoordinate(sourceColor.rg, highSlice)).rgb;
vec3 graded = mix(lowSample, highSample, fract(blueIndex));
vec3 color = mix(sourceColor, graded, uStrength);
fragColor = vec4(color, 1.0);
}
Inputs for this pass
- iChannel0 Buffer A
- iChannel1 Buffer B
- iChannel2 Empty
- iChannel3 Empty
Learn from this shader
How it works
Buffer A renders neutral gradients, colour patches, and lit geometry. Buffer B lays sixteen blue slices in a four-by-four grid. Within each viewport-sized tile, the first and last texels map red and green exactly to zero and one; integer pixel bounds keep that mapping correct when dimensions are not divisible by four. Every coordinate is transformed by a warm cinematic grading function. The image pass uses those same bounds to sample neighbouring blue slices, then interpolates between them. Two bilinear reads reconstruct the sampled transform without implying that the atlas is a literal 16 by 16 by 16 cube. Strength blends the graded and original frames.
Try changing
Move Strength between zero and one to compare the source directly. Adjust LUT Contrast, Saturation, and Warmth to regenerate the buffer and observe how one lookup represents a combined transform. Push extreme settings and inspect saturated patches for clipping or hue shifts. The smooth gradient reveals interpolation seams if tile addressing is changed incorrectly.
Using it in a game
Replace Buffer A with scene colour and Buffer B with a flattened LUT exported by a colour-grading tool. Match its tile dimensions, texel-centre mapping, slice count, and filtering assumptions. Grade in a documented colour space; production HDR pipelines often apply a suitable transfer or log encoding before lookup. LUTs are efficient because multiple artistic operations collapse into two samples and interpolation, while blending Strength supports volumes, transitions, underwater zones, and temporary gameplay states.
Explore the techniques
Continue with curated explanations and progressively related examples.
Discussion