Source code for simvx.core.colour
"""Backend-agnostic sRGB colour-space conversion helpers.
Pure-numpy implementations of the exact piecewise sRGB electro-optical transfer
function (EOTF) and its inverse. They live in core (no rendering deps) so the
Vulkan desktop backend and the WebGPU web runtime decode *constant* material
colours identically, matching the hardware sRGB texture decode bit-for-bit.
Use these for COLOUR inputs (albedo / base-colour, emissive). Data maps (normal,
metallic-roughness, ambient occlusion) carry no colour and must stay linear.
The piecewise curve (not the ``pow(2.2)`` approximation) is required so a flat
constant colour matches a same-valued sRGB-decoded texture sample exactly.
"""
import numpy as np
__all__ = [
"srgb_to_linear",
"linear_to_srgb",
"srgb_to_linear_rgb",
]
[docs]
def srgb_to_linear(c: np.ndarray | float) -> np.ndarray | float:
"""Decode sRGB-encoded values in [0, 1] to linear light (exact piecewise EOTF).
Scalar in / scalar out, or numpy array in / numpy array out (element-wise).
"""
arr = np.asarray(c, dtype=np.float64)
out = np.where(arr <= 0.04045, arr / 12.92, ((arr + 0.055) / 1.055) ** 2.4)
return float(out) if np.isscalar(c) or np.ndim(c) == 0 else out
[docs]
def linear_to_srgb(c: np.ndarray | float) -> np.ndarray | float:
"""Encode linear-light values in [0, 1] to sRGB (exact piecewise inverse EOTF)."""
arr = np.asarray(c, dtype=np.float64)
out = np.where(arr <= 0.0031308, arr * 12.92, 1.055 * np.power(arr, 1.0 / 2.4) - 0.055)
return float(out) if np.isscalar(c) or np.ndim(c) == 0 else out
[docs]
def srgb_to_linear_rgb(colour: tuple[float, ...]) -> tuple[float, ...]:
"""Decode the RGB channels of an (R, G, B[, A]) tuple, leaving any 4th component as-is.
The 4th component is alpha (linear) for albedo or emissive *intensity* for
emissive packing: neither is a colour and both pass through untouched.
"""
rgb = srgb_to_linear(np.asarray(colour[:3], dtype=np.float64))
return (*(float(x) for x in rgb), *colour[3:])