Skip to content

Commit 81c7cb6

Browse files
authored
[rmodels] Fix glTF skinning when joints have non-joint parent nodes (#5876)
* [rmodels] Fix glTF skinning when joints have non-joint parent nodes Some glTF exporters (notably wow.export, but also various other DCC pipelines) place skin joints under intermediate non-joint transform nodes that carry part of the bind-pose offset. raylib's existing LoadBoneInfoGLTF and LoadModelAnimationsGLTF only inspected a joint's immediate parent and only sampled joint-local TRS, so any transform stored on an intermediate non-joint ancestor was silently dropped, producing exploded or stretched meshes at runtime. Two surgical changes: LoadBoneInfoGLTF: walk the parent chain past any non-joint ancestors when looking up parentIndex, instead of comparing only against node.parent. Joints whose direct parent is a non-joint were previously treated as skeleton roots. LoadModelAnimationsGLTF: precompute a per-joint extOffset matrix that bakes in the static TRS contribution of any intermediate non-joint nodes between the joint and its nearest joint ancestor. Apply it to each frame's joint TRS before BuildPoseFromParentJoints so the per-frame world transforms match the bind-pose world transforms (LoadGLTF already used cgltf_node_transform_world for bindPose, so this aligns the two code paths). The replaced root-only worldTransform adjustment is a strict subset of the new per-joint extOffset machinery, so it has been removed. Spec-compliant files (the six skeletal-skinning .glb examples shipped with raylib) render bit-identically before and after; previously broken files (e.g. wow.export's babyoctopus.gltf) now match the reference rendering from f3d, the Khronos sample viewer, and three.js. * Resolve review: NULL-check joint offset allocation, fail fast [rmodels] Address review feedback on glTF joint offset handling Resolve the open review points raised on PR #5876 for the per-joint extOffset precompute in LoadModelAnimationsGLTF. * NULL check: validate the extOffset RL_MALLOC result before use, which was previously missing. * Fail-fast handling: detect the allocation failure at its source and abort animation loading (log a warning, free resources, return NULL) instead of propagating a NULL pointer into the per-frame loop. * Brace formatting: expand the collapsed one-line joint-match check (if (...) { isJoint = true; break; }) into raylib's standard Allman brace style. * Readability: break the deeply nested MatrixMultiply(MatrixMultiply(...)) into named nodeScale/nodeRotation/nodeTranslation/nodeTransform locals, mirroring the existing S/R/T composition later in the function. * Spacing/line breaks: add blank lines within the precompute block to match the surrounding code style.
1 parent 186d3ce commit 81c7cb6

1 file changed

Lines changed: 71 additions & 29 deletions

File tree

src/rmodels.c

Lines changed: 71 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5375,16 +5375,18 @@ static BoneInfo *LoadBoneInfoGLTF(cgltf_skin skin, int *boneCount)
53755375
cgltf_node node = *skin.joints[i];
53765376
if (node.name != NULL) strncpy(bones[i].name, node.name, sizeof(bones[i].name) - 1);
53775377

5378-
// Find parent bone index
5378+
// Find parent bone index by walking up the node tree past any
5379+
// non-joint ancestors (intermediate transform nodes used by some
5380+
// DCC exporters), until we hit a node that is also in skin.joints.
53795381
int parentIndex = -1;
5380-
5381-
for (unsigned int j = 0; j < skin.joints_count; j++)
5382+
cgltf_node *ancestor = node.parent;
5383+
while (ancestor != NULL && parentIndex == -1)
53825384
{
5383-
if (skin.joints[j] == node.parent)
5385+
for (unsigned int j = 0; j < skin.joints_count; j++)
53845386
{
5385-
parentIndex = (int)j;
5386-
break;
5387+
if (skin.joints[j] == ancestor) { parentIndex = (int)j; break; }
53875388
}
5389+
if (parentIndex == -1) ancestor = ancestor->parent;
53885390
}
53895391

53905392
bones[i].parent = parentIndex;
@@ -6515,20 +6517,58 @@ static ModelAnimation *LoadModelAnimationsGLTF(const char *fileName, int *animCo
65156517
if (data->skins_count > 0)
65166518
{
65176519
cgltf_skin skin = data->skins[0];
6520+
6521+
// Precompute, per joint, the static transform contributed by any
6522+
// intermediate non-joint nodes between the joint and its nearest
6523+
// joint ancestor. This handles exporters (e.g. wow.export) that
6524+
// store bone offsets on dummy parent nodes rather than on the
6525+
// joints themselves. Depends only on the skin, not the animation.
6526+
int jointCount = (int)skin.joints_count;
6527+
Matrix *extOffset = (Matrix *)RL_MALLOC(jointCount*sizeof(Matrix));
6528+
6529+
if (extOffset == NULL)
6530+
{
6531+
// Allocation failed: abort animation loading at the source rather than
6532+
// propagating a NULL pointer into the per-frame transform loop below
6533+
TRACELOG(LOG_WARNING, "MODEL: [%s] Failed to allocate joint offset buffer", fileName);
6534+
cgltf_free(data);
6535+
UnloadFileData(fileData);
6536+
*animCount = 0;
6537+
return NULL;
6538+
}
6539+
65186540
*animCount = (int)data->animations_count;
65196541
animations = (ModelAnimation *)RL_CALLOC(data->animations_count, sizeof(ModelAnimation));
65206542

6521-
Transform worldTransform = { 0 };
6522-
cgltf_float cgltf_worldTransform[16] = { 0 };
6523-
cgltf_node *node = skin.joints[0];
6524-
cgltf_node_transform_world(node->parent, cgltf_worldTransform);
6525-
Matrix worldMatrix = {
6526-
cgltf_worldTransform[0], cgltf_worldTransform[4], cgltf_worldTransform[8], cgltf_worldTransform[12],
6527-
cgltf_worldTransform[1], cgltf_worldTransform[5], cgltf_worldTransform[9], cgltf_worldTransform[13],
6528-
cgltf_worldTransform[2], cgltf_worldTransform[6], cgltf_worldTransform[10], cgltf_worldTransform[14],
6529-
cgltf_worldTransform[3], cgltf_worldTransform[7], cgltf_worldTransform[11], cgltf_worldTransform[15]
6530-
};
6531-
MatrixDecompose(worldMatrix, &worldTransform.translation, &worldTransform.rotation, &worldTransform.scale);
6543+
for (int k = 0; k < jointCount; k++)
6544+
{
6545+
extOffset[k] = MatrixIdentity();
6546+
cgltf_node *n = skin.joints[k]->parent;
6547+
6548+
while (n != NULL)
6549+
{
6550+
bool isJoint = false;
6551+
for (int jj = 0; jj < jointCount; jj++)
6552+
{
6553+
if (skin.joints[jj] == n)
6554+
{
6555+
isJoint = true;
6556+
break;
6557+
}
6558+
}
6559+
6560+
if (isJoint) break;
6561+
6562+
// Compose the intermediate node's local TRS (scale, then rotation, then translation)
6563+
Matrix nodeScale = MatrixScale(n->scale[0], n->scale[1], n->scale[2]);
6564+
Matrix nodeRotation = QuaternionToMatrix((Quaternion){ n->rotation[0], n->rotation[1], n->rotation[2], n->rotation[3] });
6565+
Matrix nodeTranslation = MatrixTranslate(n->translation[0], n->translation[1], n->translation[2]);
6566+
Matrix nodeTransform = MatrixMultiply(MatrixMultiply(nodeScale, nodeRotation), nodeTranslation);
6567+
6568+
extOffset[k] = MatrixMultiply(extOffset[k], nodeTransform);
6569+
n = n->parent;
6570+
}
6571+
}
65326572

65336573
for (unsigned int a = 0; a < data->animations_count; a++)
65346574
{
@@ -6637,27 +6677,29 @@ static ModelAnimation *LoadModelAnimationsGLTF(const char *fileName, int *animCo
66376677
}
66386678
}
66396679

6640-
animations[a].keyframePoses[j][k] = (Transform){
6641-
.translation = translation,
6642-
.rotation = rotation,
6643-
.scale = scale
6644-
};
6680+
// Compose joint local TRS, then prepend the static
6681+
// intermediate non-joint offsets so the final TRS is
6682+
// expressed relative to the joint's skeleton parent.
6683+
Matrix S = MatrixScale(scale.x, scale.y, scale.z);
6684+
Matrix R = QuaternionToMatrix(rotation);
6685+
Matrix T = MatrixTranslate(translation.x, translation.y, translation.z);
6686+
Matrix jointLocal = MatrixMultiply(MatrixMultiply(S, R), T);
6687+
Matrix combined = MatrixMultiply(jointLocal, extOffset[k]);
6688+
6689+
Transform tr;
6690+
MatrixDecompose(combined, &tr.translation, &tr.rotation, &tr.scale);
6691+
animations[a].keyframePoses[j][k] = tr;
66456692
}
66466693

6647-
Transform *root = &animations[a].keyframePoses[j][0];
6648-
root->rotation = QuaternionMultiply(worldTransform.rotation, root->rotation);
6649-
root->scale = Vector3Multiply(root->scale, worldTransform.scale);
6650-
root->translation = Vector3Multiply(root->translation, worldTransform.scale);
6651-
root->translation = Vector3RotateByQuaternion(root->translation, worldTransform.rotation);
6652-
root->translation = Vector3Add(root->translation, worldTransform.translation);
6653-
66546694
BuildPoseFromParentJoints(bones, animations[a].boneCount, animations[a].keyframePoses[j]);
66556695
}
66566696

66576697
TRACELOG(LOG_INFO, "MODEL: [%s] Loaded animation: %s | Frames: %d | Duration: %fs", fileName, (animData.name != NULL)? animData.name : "NULL", animations[a].keyframeCount, animDuration);
66586698
RL_FREE(boneChannels);
66596699
RL_FREE(bones);
66606700
}
6701+
6702+
RL_FREE(extOffset);
66616703
}
66626704

66636705
if (data->skins_count > 1)

0 commit comments

Comments
 (0)