getJointsTexture method
Computes the joint matrices for the current frame into the next ring
slot and returns that slot's square RGBA32F GPU texture. The upload
waits for flushJointsUpload, so a frame that draws nothing (one that
re-presents its previous image) never pays for it.
Each joint occupies four texels (one matrix). The texture's edge length is rounded up to the next power of two, with a floor of four so a matrix never straddles a row; 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() {
final int dimensionSize = _jointsTextureEdge(joints.length);
// 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;
_jointMatrixFloats = null;
_uploadPending = false;
}
// Advance to the next ring slot, allocating it on first use. A slot whose
// upload never ran is reused, so the previous slot stays the last one the
// GPU was given.
if (!_uploadPending) {
_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.
final floatCount = dimensionSize * dimensionSize * 4;
var jointMatrixFloats = _jointMatrixFloats;
if (jointMatrixFloats == null) {
jointMatrixFloats = _jointMatrixFloats = Float32List(floatCount);
// Identity in every slot; a null joint keeps it.
for (int i = 0; i < floatCount; 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);
}
_uploadPending = true;
return texture;
}