"""Backend-agnostic 3D-LUT colour-grading data generators.
Pure-numpy producers of ``(size, size, size, 4)`` uint8 identity/graded LUTs.
They live in core (no rendering deps) so both the Vulkan desktop backend and the
WebGPU web runtime can build the same LUT from one call: a game generates a LUT
here and registers it via ``App.register_lut`` / ``WebApp.register_lut``, and it
grades identically on either backend. The GPU upload + sampler live backend-side
(``simvx.graphics.renderer.colour_grading`` / the web ``KIND_LUT`` resource).
Axis order is ``[b, g, r]`` (numpy row-major: the trailing R axis is the fastest
stride = texture width), which maps to an ``rgba8`` 3D texture as X=R, Y=G, Z=B.
"""
import numpy as np
__all__ = [
"generate_neutral_lut",
"generate_warm_lut",
"generate_cool_lut",
"generate_vintage_lut",
]
[docs]
def generate_neutral_lut(size: int = 32) -> np.ndarray:
"""Generate an identity 3D LUT (no colour change)."""
lut = np.zeros((size, size, size, 4), dtype=np.uint8)
for b in range(size):
for g in range(size):
for r in range(size):
lut[b, g, r] = [
int(r / (size - 1) * 255),
int(g / (size - 1) * 255),
int(b / (size - 1) * 255),
255,
]
return lut
[docs]
def generate_warm_lut(size: int = 32) -> np.ndarray:
"""Generate a warm-toned 3D LUT (shifted toward orange/amber)."""
lut = generate_neutral_lut(size)
for b in range(size):
for g in range(size):
for r in range(size):
rf = r / (size - 1)
gf = g / (size - 1)
bf = b / (size - 1)
# Warm shift: boost reds, slight green reduction, reduce blues
rf = min(1.0, rf * 1.1 + 0.02)
gf = gf * 0.95 + 0.01
bf = bf * 0.8
lut[b, g, r] = [int(rf * 255), int(gf * 255), int(bf * 255), 255]
return lut
[docs]
def generate_cool_lut(size: int = 32) -> np.ndarray:
"""Generate a cool-toned 3D LUT (shifted toward blue/teal)."""
lut = generate_neutral_lut(size)
for b in range(size):
for g in range(size):
for r in range(size):
rf = r / (size - 1)
gf = g / (size - 1)
bf = b / (size - 1)
# Cool shift: reduce reds, boost greens slightly, boost blues
rf = rf * 0.85
gf = min(1.0, gf * 1.02 + 0.01)
bf = min(1.0, bf * 1.15 + 0.02)
lut[b, g, r] = [int(rf * 255), int(gf * 255), int(bf * 255), 255]
return lut
[docs]
def generate_vintage_lut(size: int = 32) -> np.ndarray:
"""Generate a vintage/desaturated warm 3D LUT."""
lut = generate_neutral_lut(size)
for b in range(size):
for g in range(size):
for r in range(size):
rf = r / (size - 1)
gf = g / (size - 1)
bf = b / (size - 1)
# Desaturate toward luminance
luma = 0.2126 * rf + 0.7152 * gf + 0.0722 * bf
sat = 0.6 # reduced saturation
rf = luma + (rf - luma) * sat
gf = luma + (gf - luma) * sat
bf = luma + (bf - luma) * sat
# Warm tint
rf = min(1.0, rf * 1.05 + 0.03)
gf = gf * 0.95
bf = bf * 0.75
# Lifted blacks (fade effect)
rf = rf * 0.9 + 0.05
gf = gf * 0.9 + 0.04
bf = bf * 0.9 + 0.06
lut[b, g, r] = [
int(min(1.0, rf) * 255),
int(min(1.0, gf) * 255),
int(min(1.0, bf) * 255),
255,
]
return lut