How GLSL unpackUnorm4x8 Decompresses Integers

This article provides an overview of how the GLSL built-in function unpackUnorm4x8 unpacks a single 32-bit unsigned integer into a four-component floating-point vector (vec4). It covers the step-by-step mathematical process of extracting individual 8-bit sequences, converting them from fixed-point integers to normalized floating-point numbers in the range \([0.0, 1.0]\), and mapping them to their corresponding vector channels.

The Purpose of unpackUnorm4x8

In graphics pipelines, packing four 8-bit color or attribute channels (such as RGBA) into a single 32-bit unsigned integer (uint) is a standard technique to minimize memory bandwidth and storage overhead. The GLSL function unpackUnorm4x8 reverses this process, taking a packed uint and expanding it into a normalized vec4. The "Unorm" designation indicates that the resulting floats are unsigned and normalized between 0.0 and 1.0.

Step 1: Bitwise Extraction

A 32-bit unsigned integer consists of 4 bytes (8 bits each). When unpackUnorm4x8 executes, it treats the integer as little-endian with respect to vector indexing and extracts four separate 8-bit unsigned integer values:

  • Component 0 (x / r): Bits 0 through 7
  • Component 1 (y / g): Bits 8 through 15
  • Component 2 (z / b): Bits 16 through 23
  • Component 3 (w / a): Bits 24 through 31

Using standard bitwise operations, extracting these values is equivalent to shifting and masking:

uint c0 = (packedVal      ) & 0xFFu;
uint c1 = (packedVal >>  8) & 0xFFu;
uint c2 = (packedVal >> 16) & 0xFFu;
uint c3 = (packedVal >> 24) & 0xFFu;

Each resulting component c0 through c3 is an integer ranging from 0 to 255 (\(2^8 - 1\)).

Step 2: Floating-Point Normalization

After isolation, each 8-bit integer is converted to an unsigned normalized float. Normalization maps the discrete integer range \([0, 255]\) linearly onto the continuous floating-point interval \([0.0, 1.0]\).

To achieve this, the integer value is divided by the maximum possible value of an 8-bit unsigned integer (\(255.0\)):

\[\text{float\_val} = \frac{\text{uint\_val}}{255.0}\]

  • An integer value of 0 becomes 0.0 / 255.0 = 0.0.
  • An integer value of 128 becomes 128.0 / 255.0 ≈ 0.50196.
  • An integer value of 255 becomes 255.0 / 255.0 = 1.0.

Step 3: Vector Assembly

The final step is packing these four normalized floating-point numbers into a single vec4.

In pure GLSL, the exact programmatic equivalent of unpackUnorm4x8(packedVal) can be represented as:

vec4 manualUnpackUnorm4x8(uint p) {
    return vec4(
        float(p & 0xFFu) / 255.0,
        float((p >> 8) & 0xFFu) / 255.0,
        float((p >> 16) & 0xFFu) / 255.0,
        float((p >> 24) & 0xFFu) / 255.0
    );
}

Hardware GPUs implement this operation directly at the instruction level, allowing this extraction and normalization to occur in a single cycle without software-level bit-shifting or software division.