Storing Bone Matrices in GLSL Texture Buffers

Skeletal animation requires passing extensive arrays of transform matrices to vertex shaders, which often exceeds the size limits of standard Uniform Buffer Objects (UBOs). Texture Buffer Objects (TBOs) solve this limitation by offering a high-capacity, one-dimensional array accessible directly in GLSL via texel fetch operations. This article covers the memory layout required to store bone matrices in a TBO and the GLSL implementation necessary to fetch and reconstruct them during skinning.

Memory Layout and CPU-Side Setup

A standard transform matrix is a \(4 \times 4\) matrix composed of 16 32-bit floating-point values. When using a Texture Buffer Object, the buffer is commonly backed by the GL_RGBA32F internal texture format.

Because each GL_RGBA32F texel holds four 32-bit floats (vec4), a single \(4 \times 4\) matrix occupies exactly four consecutive texels:

  • Texel 0: Column 0 (or Row 0) of the matrix (vec4)
  • Texel 1: Column 1 (or Row 1) of the matrix (vec4)
  • Texel 2: Column 2 (or Row 2) of the matrix (vec4)
  • Texel 3: Column 3 (or Row 3) of the matrix (vec4)

To upload the bone data in OpenGL:

  1. Allocate a standard OpenGL Buffer Object (GL_TEXTURE_BUFFER) using glBufferData or glBufferStorage.
  2. Generate a texture handle with glGenTextures and bind it to GL_TEXTURE_BUFFER.
  3. Associate the buffer with the texture using glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, bufferID).
  4. Update the buffer memory each frame with your flattened matrix array using glBufferSubData or memory mapping.

If using affine transforms that omit projective coordinates, you can optimize memory by storing \(4 \times 3\) matrices across three texels instead of four.

Fetching Matrices in GLSL

In GLSL, the buffer is declared using the samplerBuffer uniform type. Rather than standard UV texture coordinates and filtering, data is retrieved using integer indices via the texelFetch function.

Since GLSL constructs matrices in column-major order by default, ensure the data packed on the CPU corresponds to columns, or transpose the matrix upon reconstruction.

#version 330 core

// The bone transform buffer
uniform samplerBuffer u_BoneMatrixBuffer;

// Attributes for vertex skinning
layout(location = 0) in vec3 a_Position;
layout(location = 1) in vec4 a_BoneIndices;
layout(location = 2) in vec4 a_BoneWeights;

// Helper function to reconstruct a 4x4 matrix from 4 consecutive texels
mat4 getBoneMatrix(int boneIndex) 
{
    int baseIndex = boneIndex * 4;

    vec4 col0 = texelFetch(u_BoneMatrixBuffer, baseIndex + 0);
    vec4 col1 = texelFetch(u_BoneMatrixBuffer, baseIndex + 1);
    vec4 col2 = texelFetch(u_BoneMatrixBuffer, baseIndex + 2);
    vec4 col3 = texelFetch(u_BoneMatrixBuffer, baseIndex + 3);

    return mat4(col0, col1, col2, col3);
}

void main() 
{
    // Accumulate the weighted skinning matrix
    mat4 skinMatrix = 
        getBoneMatrix(int(a_BoneIndices.x)) * a_BoneWeights.x +
        getBoneMatrix(int(a_BoneIndices.y)) * a_BoneWeights.y +
        getBoneMatrix(int(a_BoneIndices.z)) * a_BoneWeights.z +
        getBoneMatrix(int(a_BoneIndices.w)) * a_BoneWeights.w;

    vec4 skinnedPosition = skinMatrix * vec4(a_Position, 1.0);

    gl_Position = skinnedPosition; // Multiply by View/Projection matrices as needed
}

Key Considerations

  • Integer Indexing: texelFetch takes an integer texel coordinate. There is no normalized coordinate mapping or filtering (such as linear interpolation) applied to samplerBuffer lookups.
  • Instanced Rendering Offset: When rendering multiple animated instances, multiply the instance ID by the total number of bones per skeleton, then add this offset to the boneIndex to reference instance-specific poses within a single global buffer.
  • Cache Coherency: Storing all four vectors for a single bone contiguously maximizes L1/L2 texture cache hits on modern hardware, minimizing memory latency during skinning.