glTF Normalized Integers to Float Conversion

In glTF assets, vertex attributes can be stored as integers to save bandwidth and memory, then converted to floating-point values during processing. When an accessor sets the normalized boolean property to true, runtime parsers and graphics hardware automatically convert these signed or unsigned integer components into normalized floating-point numbers. This article explains how glTF maps these raw integer components to the standard [0.0, 1.0] and [-1.0, 1.0] floating-point ranges.

The Role of the Accessor

In the glTF 2.0 specification, a binary buffer is interpreted through an accessor object. The accessor defines:

  • componentType: The integer data type (such as byte, unsigned byte, short, or unsigned short).
  • type: The element structure (e.g., SCALAR, VEC2, VEC3, VEC4).
  • normalized: A boolean indicating whether the integer values represent normalized floats.

If normalized is false, the values remain raw integers (used primarily for vertex indices or joint IDs). If normalized is true, parsers and rendering APIs map the raw integer bits to continuous floating-point ranges.

Unsigned Integer Conversion

Unsigned integers represent numbers from zero up to the maximum value that the bit width can hold (\(2^b - 1\)). glTF maps unsigned integers uniformly across the range [0.0, 1.0].

The conversion formula divides the raw integer \(c\) by the maximum representable value of that type:

\[f = \frac{c}{2^b - 1}\]

Supported Unsigned Types

  • UNSIGNED_BYTE (5121, 8-bit): \[f = \frac{c}{255.0}\] A value of 0 maps to 0.0, and 255 maps to 1.0.

  • UNSIGNED_SHORT (5123, 16-bit): \[f = \frac{c}{65535.0}\] A value of 0 maps to 0.0, and 65535 maps to 1.0.

  • UNSIGNED_INT (5125, 32-bit): \[f = \frac{c}{4294967295.0}\]

Signed Integer Conversion

Signed integers represent values ranging from \(-2^{b-1}\) to \(2^{b-1} - 1\). glTF maps these integers to the range [-1.0, 1.0].

Because two's complement representation has one more negative value than positive values (for example, -128 to 127 in an 8-bit signed byte), dividing directly by the negative maximum would make zero asymmetric. To maintain symmetry around zero, the divisor is \(2^{b-1} - 1\), and the result is clamped to -1.0 to handle the extra negative integer.

The conversion formula is:

\[f = \max\left(\frac{c}{2^{b-1} - 1}, -1.0\right)\]

Supported Signed Types

  • BYTE (5120, 8-bit signed): \[f = \max\left(\frac{c}{127.0}, -1.0\right)\] Both -128 and -127 map to -1.0, 0 maps to 0.0, and 127 maps to 1.0.

  • SHORT (5122, 16-bit signed): \[f = \max\left(\frac{c}{32767.0}, -1.0\right)\] Both -32768 and -32767 map to -1.0, 0 maps to 0.0, and 32767 maps to 1.0.

  • INT (5124, 32-bit signed): \[f = \max\left(\frac{c}{2147483647.0}, -1.0\right)\]

Hardware and API Handling

When passing glTF data to modern graphics APIs (WebGL, OpenGL, Vulkan, DirectX, or Metal), custom software parsing is often unnecessary:

  • WebGL / OpenGL: Passing normalized = true to glVertexAttribPointer instructs the GPU to execute these formulas directly in hardware when loading vertex attributes into the vertex shader.
  • Vulkan / DirectX: Using normalized formats (such as VK_FORMAT_R8G8B8A8_UNORM or DXGI_FORMAT_R16G16_SNORM) causes the input assembler to automatically decode the integers into floating-point vectors before shader execution.