Octahedral-encodes an array of unit-length 3D normals into a flat array of
16-bit unsigned integers (two values per normal).
The signed-zero octahedral form maps a unit normal to a 2D coordinate in
[-1, 1]; this encoder remaps that range to [0, 65535] so the output
fits a single RG16UI texel per normal — half the memory cost of three
unquantised floats while retaining sub-degree precision (worst-case angular
error is roughly 0.001 rad), which is comfortably below the perceptual
threshold for diffuse Lambert and the lower-frequency PBR terms.
Input is read three components at a time. Non-unit inputs are normalised
before encoding. The output length is two-thirds of the input length.
The matching shader-side decode is:
vec2 e = vec2(packed) / 65535.0 * 2.0 - 1.0;
vec3 n = vec3(e.xy, 1.0 - abs(e.x) - abs(e.y));
if (n.z < 0.0) n.xy = (1.0 - abs(n.yx)) * sign(n.xy);
n = normalize(n);
Octahedral-encodes an array of unit-length 3D normals into a flat array of 16-bit unsigned integers (two values per normal).
The signed-zero octahedral form maps a unit normal to a 2D coordinate in
[-1, 1]; this encoder remaps that range to[0, 65535]so the output fits a single RG16UI texel per normal — half the memory cost of three unquantised floats while retaining sub-degree precision (worst-case angular error is roughly 0.001 rad), which is comfortably below the perceptual threshold for diffuse Lambert and the lower-frequency PBR terms.Input is read three components at a time. Non-unit inputs are normalised before encoding. The output length is two-thirds of the input length.
The matching shader-side decode is: