GLSL packUnorm4x8 Explained: Pack Floats to Uint
The packUnorm4x8 function in GLSL is a built-in utility
designed to serialize a four-component floating-point vector
(vec4) into a single 32-bit unsigned integer
(uint). It achieves this by treating each vector component
as an unsigned normalized value in the range \([0.0, 1.0]\), converting each component
into an 8-bit integer (\(0\) to \(255\)), and packing those four bytes into a
single scalar value. This conversion is widely used to reduce memory
bandwidth, optimize storage in Shader Storage Buffer Objects (SSBOs),
and store color data efficiently.
The Mathematical Conversion
To understand how packUnorm4x8 compresses values,
consider an input vector vec4 v. The function processes
each component (\(x, y, z, w\))
individually through three mathematical stages:
Clamping: Each component is clamped to the normalized range \([0.0, 1.0]\) to prevent underflow or overflow: \[\text{clamped} = \min(\max(v_i, 0.0), 1.0)\]
Scaling: The normalized value is scaled by \(255.0\), mapping the \([0.0, 1.0]\) range onto \([0.0, 255.0]\): \[\text{scaled} = \text{clamped} \times 255.0\]
Rounding: The scaled floating-point value is rounded to the nearest integer to yield an 8-bit representation: \[\text{byte}_i = \text{uint}(\text{round}(\text{scaled}))\]
Bitwise Packing Layout
Once each of the four components is converted into an 8-bit byte (\([0, 255]\)), the function places them into specific bit positions of the final 32-bit unsigned integer using bitwise shift and OR operations:
- \(v.x\) (First byte): Occupies bits \(0\) through \(7\) (shifted by \(0\)).
- \(v.y\) (Second byte): Occupies bits \(8\) through \(15\) (shifted left by \(8\)).
- \(v.z\) (Third byte): Occupies bits \(16\) through \(23\) (shifted left by \(16\)).
- \(v.w\) (Fourth byte): Occupies bits \(24\) through \(31\) (shifted left by \(24\)).
Expressed as a bitwise formula:
\[\text{result} = (\text{byte}_x) \mid (\text{byte}_y \ll 8) \mid (\text{byte}_z \ll 16) \mid (\text{byte}_w \ll 24)\]
Concrete Example
Consider encoding the color vector
vec4(1.0, 0.0, 0.5, 1.0):
- \(v.x = 1.0\)
\(\rightarrow \text{round}(1.0 \times 255) =
255\) (
0xFF) - \(v.y = 0.0\)
\(\rightarrow \text{round}(0.0 \times 255) =
0\) (
0x00) - \(v.z = 0.5\)
\(\rightarrow \text{round}(0.5 \times 255) =
128\) (
0x80) - \(v.w = 1.0\)
\(\rightarrow \text{round}(1.0 \times 255) =
255\) (
0xFF)
Assembling the bytes into a 32-bit integer:
0xFF\(\ll 0 = \text{0x000000FF}\)0x00\(\ll 8 = \text{0x00000000}\)0x80\(\ll 16 = \text{0x00800000}\)0xFF\(\ll 24 = \text{0xFF000000}\)
Combining with bitwise OR yields 0xFF8000FF (\(4,286,578,943\) in decimal).
Decompression with unpackUnorm4x8
The inverse operation is performed using
unpackUnorm4x8(uint p). It extracts each 8-bit slice using
bit masks and shifts, then divides each integer value by \(255.0\) to reconstruct the original
vec4 within normalized bounds.