getJointsTexture method
Computes the joint matrices for the current frame and uploads them as
a square RGBA32F GPU texture.
Each joint occupies four texels (one matrix). The texture's edge length is rounded up to the next power of two to satisfy GPU sampling requirements; unused slots are initialized to identity.
The companion getTextureWidth returns the same edge length so the vertex shader can index into the texture.
Implementation
gpu.Texture getJointsTexture() {
// Each joint has a matrix. 1 matrix = 16 floats. 1 pixel = 4 floats.
// Therefore, each joint needs 4 pixels.
int requiredPixels = joints.length * 4;
int dimensionSize = max(
2,
_getNextPowerOfTwoSize(sqrt(requiredPixels).ceil()),
);
// Drop the ring if the texture size changed (joint count is fixed
// after construction, so this normally never triggers).
if (dimensionSize != _jointsTextureDimension) {
_jointsTextureRing.fillRange(0, _jointsTextureRing.length, null);
_jointsTextureDimension = dimensionSize;
}
// Advance to the next ring slot, allocating it on first use.
_jointsTextureRingCursor =
(_jointsTextureRingCursor + 1) % _jointsTextureRingSize;
final gpu.Texture texture =
_jointsTextureRing[_jointsTextureRingCursor] ??= gpu.gpuContext
.createTexture(
gpu.StorageMode.hostVisible,
dimensionSize,
dimensionSize,
format: gpu.PixelFormat.r32g32b32a32Float,
);
// 64 bytes per matrix. 4 bytes per pixel.
Float32List jointMatrixFloats = Float32List(
dimensionSize * dimensionSize * 4,
);
// Initialize with identity matrices.
for (int i = 0; i < jointMatrixFloats.length; i += 16) {
jointMatrixFloats[i] = 1.0;
jointMatrixFloats[i + 5] = 1.0;
jointMatrixFloats[i + 10] = 1.0;
jointMatrixFloats[i + 15] = 1.0;
}
for (int jointIndex = 0; jointIndex < joints.length; jointIndex++) {
final Node? joint = joints[jointIndex];
// A null joint (Node.clone couldn't relocate it) keeps the
// pre-initialized identity slot.
if (joint == null) continue;
// glTF skinning: the joint matrix is the joint's full global
// transform times its inverse bind matrix. globalTransform walks
// every ancestor, so transforms on non-joint nodes between the
// joints and the scene root (e.g. a skeleton root carrying the
// model's Z-up-to-Y-up correction) are included, as is the
// scene-root flip. The inverse bind matrix takes a vertex from
// model space into the joint's rest-pose space; the global
// transform then places it by the joint's current pose.
//
// The shader applies this matrix directly, so the mesh node's own
// transform must not be applied again -- SkinnedGeometry.bind
// passes an identity model transform.
final Matrix4 matrix =
joint.globalTransform * inverseBindMatrices[jointIndex];
final floatOffset = jointIndex * 16;
jointMatrixFloats.setRange(floatOffset, floatOffset + 16, matrix.storage);
}
texture.overwrite(jointMatrixFloats.buffer.asByteData());
return texture;
}