getJointsTexture method

Texture getJointsTexture()

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;
}