From ff7b940e86d2ac56317354ef45b45cfb3a568194 Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Mon, 20 Apr 2026 19:32:15 +0100 Subject: [PATCH 1/4] fix: properly dump gltf with root node in models with more than one root bone --- src/ObjLoading/XModel/Gltf/GltfLoader.cpp | 7 +++---- src/ObjWriting/XModel/Gltf/GltfWriter.cpp | 13 ++++++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/ObjLoading/XModel/Gltf/GltfLoader.cpp b/src/ObjLoading/XModel/Gltf/GltfLoader.cpp index 0adba623..cc7f79b0 100644 --- a/src/ObjLoading/XModel/Gltf/GltfLoader.cpp +++ b/src/ObjLoading/XModel/Gltf/GltfLoader.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include #include @@ -451,7 +450,7 @@ namespace return std::nullopt; } - void ApplyNodeMatrixTRS(const JsonNode& node, float (&localOffsetRhc)[3], float (&localRotationRhc)[4], float (&scaleRhc)[3]) + void ApplyNodeMatrixTRS(const JsonNode& node, float (&localOffsetRhc)[3], float (&localRotationRhc)[4], float (&scaleRhc)[3]) const { const auto matrix = Eigen::Matrix4f({ {(*node.matrix)[0], (*node.matrix)[4], (*node.matrix)[8], (*node.matrix)[12]}, @@ -492,7 +491,7 @@ namespace scaleRhc[2] = matrix.block<3, 1>(0, 2).norm(); } - void ApplyNodeSeparateTRS(const JsonNode& node, float (&localOffsetRhc)[3], float (&localRotationRhc)[4], float (&scaleRhc)[3]) + void ApplyNodeSeparateTRS(const JsonNode& node, float (&localOffsetRhc)[3], float (&localRotationRhc)[4], float (&scaleRhc)[3]) const { if (node.translation) { @@ -630,7 +629,7 @@ namespace const auto skinBoneOffset = static_cast(common.m_bones.size()); common.m_bones.resize(skinBoneOffset + skin.joints.size()); - const Eigen::Vector3f defaultTranslation(0.0f, 0.0f, 0.0f); + constexpr Eigen::Vector3f defaultTranslation(0.0f, 0.0f, 0.0f); const Eigen::Quaternionf defaultRotation(1.0f, 0.0f, 0.0f, 0.0f); constexpr float defaultScale[3]{1.0f, 1.0f, 1.0f}; diff --git a/src/ObjWriting/XModel/Gltf/GltfWriter.cpp b/src/ObjWriting/XModel/Gltf/GltfWriter.cpp index ab81bbc1..ecac06eb 100644 --- a/src/ObjWriting/XModel/Gltf/GltfWriter.cpp +++ b/src/ObjWriting/XModel/Gltf/GltfWriter.cpp @@ -187,10 +187,10 @@ namespace const auto meshCount = xmodel.m_objects.size(); for (auto meshIndex = 0u; meshIndex < meshCount; meshIndex++) - rootNode.children->push_back(m_first_mesh_node + meshIndex); + rootNode.children->emplace_back(m_first_mesh_node + meshIndex); - if (!xmodel.m_bones.empty()) - rootNode.children->push_back(m_first_bone_node); + for (auto rootBoneIndex = 0u; rootBoneIndex < m_root_bone_count; rootBoneIndex++) + rootNode.children->emplace_back(m_first_bone_node + rootBoneIndex); m_root_node = static_cast(gltf.nodes->size()); gltf.nodes->emplace_back(std::move(rootNode)); @@ -303,6 +303,7 @@ namespace const auto boneCount = common.m_bones.size(); m_first_bone_node = static_cast(gltf.nodes->size()); + m_root_bone_count = 0; for (auto boneIndex = 0u; boneIndex < boneCount; boneIndex++) { JsonNode boneNode; @@ -335,6 +336,11 @@ namespace translation = inverseParentRotation * translation; rotation = inverseParentRotation * rotation; } + else + { + assert(m_root_bone_count == boneIndex); + m_root_bone_count++; + } rotation.normalize(); boneNode.name = bone.name; @@ -745,6 +751,7 @@ namespace unsigned m_first_mesh_node = 0u; unsigned m_root_node = 0u; unsigned m_first_bone_node = 0u; + unsigned m_root_bone_count = 0u; unsigned m_position_accessor = 0u; unsigned m_normal_accessor = 0u; unsigned m_color_accessor = 0u; From 875e1fb36096381d82585c1a9c2e4e581a0e3e0d Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Mon, 20 Apr 2026 22:36:31 +0100 Subject: [PATCH 2/4] fix: properly load all root bones from gltf --- src/ObjLoading/XModel/Gltf/GltfLoader.cpp | 31 ++++++++++++++--------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/ObjLoading/XModel/Gltf/GltfLoader.cpp b/src/ObjLoading/XModel/Gltf/GltfLoader.cpp index cc7f79b0..10bf9319 100644 --- a/src/ObjLoading/XModel/Gltf/GltfLoader.cpp +++ b/src/ObjLoading/XModel/Gltf/GltfLoader.cpp @@ -405,13 +405,12 @@ namespace return true; } - static std::optional GetRootNodeForSkin(const JsonRoot& jRoot, const JsonSkin& skin) + static std::vector GetRootNodesForSkin(const JsonRoot& jRoot, const JsonSkin& skin) { if (!jRoot.nodes || skin.joints.empty()) - return std::nullopt; + return {}; const auto jointCount = skin.joints.size(); - auto rootCount = jointCount; std::vector isRoot(jointCount, true); for (const auto joint : skin.joints) @@ -431,23 +430,20 @@ namespace if (isRoot[foundChildJointIndex]) { isRoot[foundChildJointIndex] = false; - rootCount--; } } } } } - if (rootCount != 1) - throw GltfLoadException("Skins must have exactly one common root node"); - + std::vector result; for (auto index = 0u; index < jointCount; index++) { if (isRoot[index]) - return skin.joints[index]; + result.emplace_back(skin.joints[index]); } - return std::nullopt; + return std::move(result); } void ApplyNodeMatrixTRS(const JsonNode& node, float (&localOffsetRhc)[3], float (&localRotationRhc)[4], float (&scaleRhc)[3]) const @@ -611,7 +607,9 @@ namespace { if (!ConvertJoint( jRoot, skin, common, skinBoneOffset, childIndex, commonBoneOffset, globalTranslationEigenRhc, globalRotationEigenRhc, bone.scale)) + { return false; + } } } @@ -625,7 +623,10 @@ namespace if (!jRoot.nodes) return false; - const auto rootNode = GetRootNodeForSkin(jRoot, skin).value_or(skin.joints[0]); + auto rootNodes = GetRootNodesForSkin(jRoot, skin); + if (rootNodes.empty()) + rootNodes.emplace_back(skin.joints[0]); + const auto skinBoneOffset = static_cast(common.m_bones.size()); common.m_bones.resize(skinBoneOffset + skin.joints.size()); @@ -633,10 +634,16 @@ namespace const Eigen::Quaternionf defaultRotation(1.0f, 0.0f, 0.0f, 0.0f); constexpr float defaultScale[3]{1.0f, 1.0f, 1.0f}; - if (!ConvertJoint(jRoot, skin, common, skinBoneOffset, rootNode, std::nullopt, defaultTranslation, defaultRotation, defaultScale)) - return false; + for (const auto rootNode : rootNodes) + { + if (!ConvertJoint(jRoot, skin, common, skinBoneOffset, rootNode, std::nullopt, defaultTranslation, defaultRotation, defaultScale)) + return false; + } common.CalculateBoneLocalsFromGlobals(); + + // TODO: Reorder bones if necessary and prepare lookup + return true; } From 92e236b1fe4d4197aea61233a8f08140f419cc39 Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Sun, 3 May 2026 17:19:48 +0200 Subject: [PATCH 3/4] chore: only allocate 3 floats per bone for applicable games --- src/ObjLoading/XModel/LoaderXModel.cpp.template | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/ObjLoading/XModel/LoaderXModel.cpp.template b/src/ObjLoading/XModel/LoaderXModel.cpp.template index b23fbd2b..018ada36 100644 --- a/src/ObjLoading/XModel/LoaderXModel.cpp.template +++ b/src/ObjLoading/XModel/LoaderXModel.cpp.template @@ -323,8 +323,12 @@ namespace { xmodel.parentList = m_memory.Alloc(xmodel.numBones - xmodel.numRootBones); - // For some reason Treyarch games allocate for a vec4 here. it is treated as a vec3 though? +#if defined(FEATURE_IW3) || defined(FEATURE_T5) || defined(FEATURE_T6) + // For some reason some games allocate for a vec4 here. it is treated as a vec3 though? xmodel.trans = m_memory.Alloc((xmodel.numBones - xmodel.numRootBones) * 4u); +#else + xmodel.trans = m_memory.Alloc((xmodel.numBones - xmodel.numRootBones) * 3u); +#endif xmodel.quats = m_memory.Alloc(xmodel.numBones - xmodel.numRootBones); } else @@ -373,7 +377,11 @@ namespace // Viewhands seem to have nulled trans for some reason? if (jXModel.type.value_or(JsonXModelType::RIGID) == JsonXModelType::VIEWHANDS) { +#if defined(FEATURE_IW3) || defined(FEATURE_T5) || defined(FEATURE_T6) memset(xmodel.trans, 0, sizeof(float) * 4 * (xmodel.numBones - xmodel.numRootBones)); +#else + memset(xmodel.trans, 0, sizeof(float) * 3 * (xmodel.numBones - xmodel.numRootBones)); +#endif } return true; From 582d8abbbf73a2bb35fe04ca5c871cbe65e8a3c1 Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Sun, 3 May 2026 20:26:13 +0200 Subject: [PATCH 4/4] feat: reorder bones when their order is not corresponding to xmodel constraints --- src/ObjLoading/XModel/Gltf/GltfLoader.cpp | 80 +++++++++++++++++++++-- 1 file changed, 73 insertions(+), 7 deletions(-) diff --git a/src/ObjLoading/XModel/Gltf/GltfLoader.cpp b/src/ObjLoading/XModel/Gltf/GltfLoader.cpp index 10bf9319..79a1122d 100644 --- a/src/ObjLoading/XModel/Gltf/GltfLoader.cpp +++ b/src/ObjLoading/XModel/Gltf/GltfLoader.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include using namespace gltf; @@ -315,7 +316,7 @@ namespace || !colorAccessor->GetFloatVec4(vertexIndex, vertex.color) || !uvAccessor->GetFloatVec2(vertexIndex, vertex.uv) || !jointsAccessor->GetUnsignedVec4(vertexIndex, joints) || !weightsAccessor->GetFloatVec4(vertexIndex, weights)) { - return false; + throw GltfLoadException("Failed to load vertex data from accessors"); } RhcToLhcCoordinates(vertex.coordinates); @@ -329,7 +330,16 @@ namespace if (std::abs(weights[i]) < std::numeric_limits::epsilon()) continue; - common.m_bone_weight_data.weights.emplace_back(joints[i], weights[i]); + assert(joints[i] < m_gltf_to_common_joint_index_lookup.size()); + if (joints[i] >= m_gltf_to_common_joint_index_lookup.size()) + { + throw GltfLoadException(std::format( + "Vertex weight referenced joint {} (there are only {} joints in skin)", joints[i], m_gltf_to_common_joint_index_lookup.size())); + } + + const auto xmodelBoneIndex = m_gltf_to_common_joint_index_lookup[joints[i]]; + + common.m_bone_weight_data.weights.emplace_back(xmodelBoneIndex, weights[i]); vertexWeights.weightCount++; } @@ -616,6 +626,54 @@ namespace return true; } + void ReorderBonesForXModels(XModelCommon& common) + { + const auto boneCount = common.m_bones.size(); + m_gltf_to_common_joint_index_lookup.resize(boneCount); + auto reorderedBoneIndex = 0u; + + std::deque parentIndicesToTraverse; + for (auto boneIndex = 0u; boneIndex < boneCount; ++boneIndex) + { + const auto& bone = common.m_bones[boneIndex]; + if (!bone.parentIndex.has_value()) + { + parentIndicesToTraverse.emplace_back(boneIndex); + m_gltf_to_common_joint_index_lookup[boneIndex] = reorderedBoneIndex++; + } + } + + while (!parentIndicesToTraverse.empty()) + { + const auto parentIndex = parentIndicesToTraverse.front(); + parentIndicesToTraverse.pop_front(); + + for (auto boneIndex = 0u; boneIndex < boneCount; ++boneIndex) + { + const auto& bone = common.m_bones[boneIndex]; + if (bone.parentIndex.has_value() && *bone.parentIndex == parentIndex) + { + parentIndicesToTraverse.emplace_back(boneIndex); + m_gltf_to_common_joint_index_lookup[boneIndex] = reorderedBoneIndex++; + } + } + } + + assert(reorderedBoneIndex == boneCount); + + std::vector reorderedBones(boneCount); + for (size_t boneIndex = 0; boneIndex < boneCount; ++boneIndex) + { + auto& reorderedBone = reorderedBones[m_gltf_to_common_joint_index_lookup[boneIndex]]; + reorderedBone = std::move(common.m_bones[boneIndex]); + + if (reorderedBone.parentIndex.has_value()) + reorderedBone.parentIndex = m_gltf_to_common_joint_index_lookup[*reorderedBone.parentIndex]; + } + + common.m_bones = std::move(reorderedBones); + } + bool ConvertSkin(const JsonRoot& jRoot, const JsonSkin& skin, XModelCommon& common) { if (skin.joints.empty()) @@ -627,8 +685,9 @@ namespace if (rootNodes.empty()) rootNodes.emplace_back(skin.joints[0]); - const auto skinBoneOffset = static_cast(common.m_bones.size()); - common.m_bones.resize(skinBoneOffset + skin.joints.size()); + // Only one skin per GLTF allowed, more would require more complex mapping and reordering + assert(common.m_bones.empty()); + common.m_bones.resize(skin.joints.size()); constexpr Eigen::Vector3f defaultTranslation(0.0f, 0.0f, 0.0f); const Eigen::Quaternionf defaultRotation(1.0f, 0.0f, 0.0f, 0.0f); @@ -636,14 +695,13 @@ namespace for (const auto rootNode : rootNodes) { - if (!ConvertJoint(jRoot, skin, common, skinBoneOffset, rootNode, std::nullopt, defaultTranslation, defaultRotation, defaultScale)) + if (!ConvertJoint(jRoot, skin, common, 0, rootNode, std::nullopt, defaultTranslation, defaultRotation, defaultScale)) return false; } + ReorderBonesForXModels(common); common.CalculateBoneLocalsFromGlobals(); - // TODO: Reorder bones if necessary and prepare lookup - return true; } @@ -838,7 +896,15 @@ namespace std::vector> m_buffer_views; std::vector> m_buffers; + // Old gltf support for OAT used bad formulas to calculate right-handed coordinate system rotations + // To make the fixed code be backwards compatible, old behaviour can be restored with this setting. bool m_bad_rotation_formulas; + + // We may need to reorder bones to account for the constraints of xmodels: + // Root bones have the lowest indices and the index of the parent of each bone must be lower than its own. + // The index in this vector is the joint index of the gltf. + // The value is the index in the common xmodel. + std::vector m_gltf_to_common_joint_index_lookup; }; } // namespace