Grade through a LUT
Open the live demo · Read the source · View on GitHub
A look-up table, or LUT, is a grade written down as data. Instead of a dozen sliders you keep a small picture that says, for every colour that can come in, which colour goes out. Colourists export them from grading tools, and the renderer applies one in its last pass.
This page builds three tables in code, so there is no file to load, and lets you switch between them.
Step 1: Build a table #
A table is a strip of N squares side by side, each N by N pixels. Blue picks the square, red runs across it and green runs down it. buildIdentityLut makes the table that changes nothing: the colour stored at each place is the colour that place stands for. Every other table is a departure from it, so the simplest way to make a look is to start from the identity and change each entry.
Here each entry is passed through a small function, once for a sepia brown, once with red and blue swapped and once for a dark night look. The bytes then become a texture.
final Uint8List identity = buildIdentityLut(size: _size);
final List<Uint8List> looks = <Uint8List>[
// Sepia: every colour becomes a brown of the same brightness.
_remap(identity, (double r, double g, double b) {
final double l = 0.3 * r + 0.59 * g + 0.11 * b;
return <double>[l * 1.15, l * 0.95, l * 0.7];
}),
// Red and blue swapped, which nobody would mistake for the original.
_remap(identity, (double r, double g, double b) => <double>[b, g, r]),
// Night: dark, with the red taken out.
_remap(identity, (double r, double g, double b) {
return <double>[r * 0.3, g * 0.55, b * 0.9];
}),
];
_tables = <TextureHandle>[
for (final Uint8List pixels in looks)
context.device.createTextureFromPixels(
width: _size * _size,
height: _size,
format: TextureFormat.r8g8b8a8UNormInt,
pixels: ByteData.sublistView(pixels),
)!,
];
Step 2: Hand it to the look #
LookSettings.lut takes the texture. lutStrength says how much of the result
to use, from 0, where the table is never read, to 1, where the picture is fully
regraded.
_look = LookSettings(lut: _tables[table], lutStrength: strength);
Step 3: Switch tables and blend #
Use the Table choice to switch between the three built in Step 1. Each is the same size, so switching costs nothing. Drag Strength from 1 down to 0 and watch the picture come back from brown to its own colours.
final Uint8List identity = buildIdentityLut(size: _size);
final List<Uint8List> looks = <Uint8List>[
// Sepia: every colour becomes a brown of the same brightness.
_remap(identity, (double r, double g, double b) {
final double l = 0.3 * r + 0.59 * g + 0.11 * b;
return <double>[l * 1.15, l * 0.95, l * 0.7];
}),
// Red and blue swapped, which nobody would mistake for the original.
_remap(identity, (double r, double g, double b) => <double>[b, g, r]),
// Night: dark, with the red taken out.
_remap(identity, (double r, double g, double b) {
return <double>[r * 0.3, g * 0.55, b * 0.9];
}),
];
_tables = <TextureHandle>[
for (final Uint8List pixels in looks)
context.device.createTextureFromPixels(
width: _size * _size,
height: _size,
format: TextureFormat.r8g8b8a8UNormInt,
pixels: ByteData.sublistView(pixels),
)!,
];
Tip. A table only sees colours, not places. It cannot darken a corner or tint the sky differently from the floor. For that, use the grade sliders on the colour grading page.