Repository navigation
feat(core): bake a colour grade into a 3D LUT that matches the shader - #6679
Conversation
bakeHfColorGradingCube evaluates the runtime shader's colour path on the CPU (primary, tonal wheels, RGB and hue curves, secondaries, output LUT, intensity mix) through the same 8-bit texture quantisation, and writes a red-fastest .cube for ffmpeg's lut3d. Grades with spatial stages (grain, vignette, effects) throw, as does a grade whose LUT was not passed in. A Chromium test draws a 4,096-colour lattice through the real runtime and compares every tile with the bake. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Edit accuracy: accurate 271 (base branch 271), smooth 142 of thoseThe gate passes. Quarantined, measured but not gated (0) Unstable (2)
|
There was a problem hiding this comment.
Verdict: APPROVE · 0 blocker · 1 important · 1 nit · head 75fbb10
Required checks are green. Nothing consumes this yet: hyperframes-internal#6729 bakes with its own primary-only port.
- nit: the PR body says the bake returns a
CubeLut3D, but it returns the.cubetext.
Scope
Compared the bake stage by stage with the shader in runtime/colorGrading.ts (applyPrimaryGrade, applyTonalWheels, curves, hue curves, applySecondary, applyLut, the clamps, the intensity mix) and found no divergence. Probe for the finding: bun, bakeHfColorGradingCube at sizes 33/65 against size 256, trilinear interpolation, 200k random 8-bit colours.
— Claude (Genesis)
| } | ||
|
|
||
| function compareWithBake(pixels: readonly number[], grading: HfColorGrading): Difference { | ||
| const rows = bakeHfColorGradingCube(grading, { size: STEPS }) |
There was a problem hiding this comment.
important: this parity test only checks colours that sit exactly on cube nodes (size 16 = the lattice), so it can't see interpolation error. ffmpeg's lut3d interpolates every other colour. I baked at size 256 (exact at every 8-bit code) and compared 200k random colours against trilinear lookups in smaller cubes. The ADVANCED grade misses by up to 11 codes at the default size 33 (mean 0.33, worst at 48,44,32, a dark skin tone near the secondary's key) and by up to 7 codes at 65. PRIMARY misses by up to 3 codes at 33. So "matches the shader exactly" only holds at the nodes. Please add an off-node case (e.g. bake at 33 and probe the 16-step lattice through tetrahedral interpolation) and state the bound in the docblock. A larger default size for grades with secondaries or hue curves would also help.
What
bakeHfColorGradingCube(grading, { size = 33, lut })turns adata-color-gradingvalue into the text of a 3D LUT.cubefile. It uses the same stages as the runtime shader: primary, wheels, RGB curves, hue curves, secondaries, then the LUT and overall intensity. Every one of these is pointwise, so a colour-only grade is exactly a cube.Why: a host that grades live in preview and bakes only at render (the Desktop podcast edit: grade a camera's lane, flatten for export) needs ffmpeg's
lut3dto give the same picture as the shader.Behaviour
enabled: falsereturns the identity cube, matching the shader, which doesn't draw it either.options.lutis missing, the bake throws. The preview would skip that LUT silently and the export would be wrong.sizemust be a whole number from 2 to 256.lut3dafter converting to full-range RGB, never on video-range or linear data.Tests
main.Not covered
Interpolation error between nodes is not bounded yet: Genesis measured up to 11 codes at size 33 for a grade with a secondary (3 for primary-only). A follow-up adds an off-node test and states the bound.
The LUT stage is unit-tested only, against an inverting LUT. The runtime loads LUTs by URL, so the browser parity test does not include one.
🤖 Generated with Claude Code