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:
- Allocate a standard OpenGL Buffer Object
(
GL_TEXTURE_BUFFER) usingglBufferDataorglBufferStorage. - Generate a texture handle with
glGenTexturesand bind it toGL_TEXTURE_BUFFER. - Associate the buffer with the texture using
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, bufferID). - Update the buffer memory each frame with your flattened matrix array
using
glBufferSubDataor 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:
texelFetchtakes an integer texel coordinate. There is no normalized coordinate mapping or filtering (such as linear interpolation) applied tosamplerBufferlookups. - 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
boneIndexto 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.