Source code for simvx.core.colour_grading

"""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