How GLSL packSnorm4x8 Converts Floats to uint

The packSnorm4x8 function in GLSL converts a four-component floating-point vector (vec4) with values normalized between -1.0 and 1.0 into an unpacked 32-bit unsigned integer (uint). It achieves this by clamping each float to the valid range, scaling the values by 127.0, rounding them to the nearest integer, and packing each resulting 8-bit signed integer into consecutive byte positions within a single 32-bit scalar.

1. Clamping the Input Values

Before conversion, each component of the input vector v = (v.x, v.y, v.z, v.w) is clamped to the range \([-1.0, 1.0]\). This ensures that values outside the normalized range do not overflow the 8-bit signed integer format.

\[\text{clamped}_i = \min(\max(v_i, -1.0), 1.0)\]

2. Scaling and Rounding to 8-Bit Integers

The clamped floating-point values are multiplied by \(127.0\) and rounded to the nearest integer:

\[i_k = \text{round}(\text{clamped}_k \times 127.0)\]

The multiplier \(127.0\) is used because standard signed normalized (snorm) representations in OpenGL map the range \([-1.0, 1.0]\) symmetrically to \([-127, 127]\). The minimum value of an 8-bit two's-complement integer, \(-128\), is intentionally unused to preserve symmetry around zero.

  • \(1.0\) scales to \(+127\) (0x7F)
  • \(0.0\) scales to \(0\) (0x00)
  • \(-1.0\) scales to \(-127\) (0x81 in two's complement)

3. Bitwise Packing into 32 Bits

Each resulting 8-bit signed value is interpreted as an unsigned 8-bit byte (0x00 to 0xFF). These four bytes are positioned into a single 32-bit integer according to their vector component order:

  • Bits 0–7: The x component (byte 0)
  • Bits 8–15: The y component (byte 1)
  • Bits 16–23: The z component (byte 2)
  • Bits 24–31: The w component (byte 3)

Equivalent Mathematical Logic

The entire operation performs the equivalent of the following GLSL logic:

uint packSnorm4x8_equivalent(vec4 v) {
    ivec4 iv = ivec4(round(clamp(v, -1.0, 1.0) * 127.0));
    uvec4 uv = uvec4(iv) & 0xFFu;
    return uv.x | (uv.y << 8u) | (uv.z << 16u) | (uv.w << 24u);
}

The resulting uint contains the packed signed normalized data ready for memory-efficient storage, buffer packing, or transmission across pipeline stages.