Skip to content

fix(loader): compute glTF skin bounds in rootBone space#3027

Merged
cptbtptpbcptdtptp merged 17 commits into
dev/2.0from
fix/gltf-loader-shaderlab-split
Jun 15, 2026
Merged

fix(loader): compute glTF skin bounds in rootBone space#3027
cptbtptpbcptdtptp merged 17 commits into
dev/2.0from
fix/gltf-loader-shaderlab-split

Conversation

@luzhuang

@luzhuang luzhuang commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

This is the glTF Loader split from #3014, narrowed to the skin/rootBone bounds path.

  • Preserve explicit skin.skeleton as the authoritative Skin.rootBone, including the valid glTF case where that root is not part of skin.joints.
  • Keep the existing conservative fallback for missing skin.skeleton: infer Skin.rootBone from the lowest common ancestor of skin.joints only. Skinned mesh nodes are owners/users of the skin, not skeleton roots, and are intentionally not used as LCA inputs.
  • Compute skinned mesh localBounds directly in rootBone space when the rootBone is not one of the skin bones.
  • Keep regression coverage through the public loader pipeline in tests/src/loader/GLTFLoader.test.ts.

Why

There are two separate cases in this loading path:

  1. Missing skin.skeleton: the loader needs a stable fallback rootBone. The safest fallback is the joints' own LCA. Including mesh nodes that merely reference the skin can promote the rootBone above the skeleton hierarchy and change renderer transform space for existing assets.
  2. Explicit skin.skeleton outside skin.joints: this is allowed by glTF and should be preserved. In that case the old bounds fallback averaged child joint inverse bind matrices, which is only an approximation and can shift SkinnedMeshRenderer.localBounds. The bounds should instead be transformed by inverse(rootBone.worldMatrix), matching the rootBone-space definition used by runtime skin matrix updates.

Changes

  • GLTFSceneParser

    • Keeps the existing path when rootBone is one of the skin bones.
    • Replaces the child-joint inverse-bind-matrix averaging fallback with inverse(rootBone.transform.worldMatrix), so mesh bounds are transformed into rootBone local space directly.
  • Tests

    • Keeps loader-level coverage for missing-skeleton joints-only LCA behavior.
    • Keeps loader-level coverage for unrelated scene siblings that must not promote rootBone to GLTF_ROOT.
    • Covers explicit rootBone outside joints with non-uniform rootBone scale, asserting both rootBone-space localBounds and final world bounds.

Verification

  • pnpm -F @galacean/engine-loader run b:types
  • pnpm vitest run tests/src/loader/GLTFLoader.test.ts --testNamePattern "Multi-root skins without skeleton|Skinned mesh bounds"
  • pnpm vitest run tests/src/loader/GLTFLoader.test.ts --testNamePattern "Skinned mesh bounds" --coverage
  • pnpm exec eslint --no-eslintrc --config .eslintrc.js --resolve-plugins-relative-to . tests/src/loader/GLTFLoader.test.ts
  • git diff --check

Summary by CodeRabbit

  • Bug Fixes

    • Improved accuracy of skinned mesh bounds calculations when explicit glTF skeleton roots are outside skin.joints.
  • Tests

    • Added loader-level test coverage for rootBone-space bounds, including non-uniform rootBone scale.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR refactors glTF skin root-bone resolution when explicit skeleton indices are absent, replacing an approximate heuristic with an LCA-based algorithm, and updates bounds computation to use the inverted rootBone world transform.

Changes

GLTF Skinning and Scene Loading

Layer / File(s) Summary
Skin Root-Bone Resolution Refactoring
packages/loader/src/gltf/parser/GLTFSkinParser.ts
GLTFSkinParser.parse now resolves missing skin.skeleton via _findSkinRootBoneByLCA, which computes the lowest common ancestor of joint indices by building parent-chain paths and walking shared prefixes.
Scene Bounds Computation Update
packages/loader/src/gltf/parser/GLTFSceneParser.ts
When rootBone is not in the bones list, _computeLocalBounds computes the inverse of rootBone.transform.worldMatrix and applies it to mesh.bounds to set skinnedMeshRenderer.localBounds.
Test Fixtures and Integration Tests
tests/src/loader/GLTFLoader.test.ts
Fixture buffer layouts are realigned (128 bytes for testSkinRoot.gltf, 152 bytes for new testSkinRootBounds.gltf), Character_Man is assigned a skin reference, and test assertions verify skins[0].rootBone selection and bounds remain in rootBone space.
Regression Note and Verification
notes/loader/2026-06-11-gltf-skin-rootbone-visual-ci.md
Documents the visual CI failure (tiny e2e image diffs), attributes it to GLTFSkinParser including skin-referencing nodes in LCA inference, describes the fix (joints-only LCA excluding mesh owners), and confirms resolution via vitest and Playwright verification (8/8 passing).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A rabbit digs through skinning code so deep,
LCA paths and bone transforms to keep,
Bounds in rootBone space, inverse math neat—
Fixtures aligned, tests all compete! 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main change: fixing how glTF skin bounds are computed in the rootBone space when rootBone is outside the joints list.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/gltf-loader-shaderlab-split

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/loader/src/gltf/parser/GLTFSkinParser.test.ts`:
- Around line 4-8: The test mutates globalThis.window inside createParser which
can leak state; update tests to save the original value (const _origWindow =
globalThis.window) and set the stubbed value in a beforeEach hook (or inside
createParser but only after saving), then restore the original in an afterEach
hook (globalThis.window = _origWindow). Refer to createParser and
globalThis.window when locating the change and ensure restoration occurs even if
createParser throws.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9fae284d-d92d-4bbc-a0ea-a2592e03f586

📥 Commits

Reviewing files that changed from the base of the PR and between de75496 and 326e183.

📒 Files selected for processing (5)
  • packages/loader/src/gltf/extensions/GLTFExtensionSchema.ts
  • packages/loader/src/gltf/parser/GLTFSceneParser.ts
  • packages/loader/src/gltf/parser/GLTFSkinParser.test.ts
  • packages/loader/src/gltf/parser/GLTFSkinParser.ts
  • tests/src/loader/GLTFLoader.test.ts

Comment thread packages/loader/src/gltf/parser/GLTFSkinParser.test.ts Outdated
@luzhuang

luzhuang commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

补充说明 packages/loader/src/gltf/parser/GLTFSceneParser.ts 的改动目的(已按 02542af 后的当前实现更新):

当前 PR 里这个文件最终只保留 _computeLocalBounds 的修正,已经去掉了之前带回来的 _sceneRoots[index] = sceneRoot 同步写入。

这里要解决的是:Skin.rootBone 可能合法地不在 skin.joints 里。典型来源包括:

  1. glTF 显式写了 skin.skeleton,且这个 skeleton root 是 joints 的父节点;
  2. skin.skeleton 缺失时,多个 joints 的 LCA 本身不在 joints 列表里。

旧逻辑在 rootBone 不属于 joints 时,会递归找它下面的 joint,然后平均这些 joint 的 inverseBindMatrices 来近似 rootBone 的 inverse bind matrix。这个近似没有稳定的空间语义:joint 的 IBM 表达的是各自 joint bind pose 的逆矩阵,多个 joint 的 IBM 平均并不等价于 rootBone 的逆世界矩阵,容易导致 SkinnedMeshRenderer.localBounds 偏移。

新逻辑改成直接用 inverse(rootBone.transform.worldMatrix) 把 mesh bounds 转到 rootBone local space:

const inverseRootBoneWorld = new Matrix();
Matrix.invert(rootBone.transform.worldMatrix, inverseRootBoneWorld);
BoundingBox.transform(mesh.bounds, inverseRootBoneWorld, skinnedMeshRenderer.localBounds);

这和 SkinnedMeshRenderer/Skin 的空间语义一致:有 rootBone 时,localBounds 应该处在 rootBone space,后续再由 renderer 的 transform entity 推到 world bounds。对应测试 testSkinRootBounds.gltf 现在使用显式 skin.skeleton 指向 non-joint rootBone,验证 localBounds 和最终 bounds 都落在预期范围。

@luzhuang

luzhuang commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

补充说明 packages/loader/src/gltf/parser/GLTFSkinParser.ts 的当前行为(已按 02542af 后的结论更新):

这里现在不是要扩展 skin.skeleton 缺失时的推断范围,而是保持一个更保守的 fallback:

  • 如果 glTF 显式写了 skin.skeleton,优先使用该节点作为 Skin.rootBone
  • 如果 skin.skeleton 缺失,只基于 skin.joints 的 parent chain 求 LCA;
  • 使用该 skin 的 mesh node 不参与 LCA,因为 node.skin === skinIndex 只表示这个 mesh 使用该 skin,不表示它属于 skeleton hierarchy。

这个边界来自 CI visual diff 的 RCA:之前把 mesh node 纳入 LCA,会把某些现有资产的 rootBone 从 mixamorig:Hips 提升到外层 Armature。该外层节点带有额外 transform(实际 case 里有 scale: 0.01),会改变 renderer transform/root space,导致渲染结果变化。

因此当前代码有意保留 joints-only fallback:

const rootBone = this._findSkinRootBoneByLCA(joints, entities);

如果资产需要表达 “rootBone 是 joints 外面的某个父节点”,正确来源应是显式 skin.skeleton。PR 保留并修复的是这类 explicit non-joint rootBone 的 bounds 计算:GLTFSceneParser 会用 inverse(rootBone.worldMatrix) 把 mesh bounds 转到 rootBone local space,避免旧的平均 joint inverse bind matrix 近似导致 bounds 偏移。

@luzhuang

luzhuang commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

补充整理了测试覆盖(已按 56cf6ba1 后的当前实现更新):

  1. tests/src/loader/GLTFLoader.test.ts 继续覆盖真实 loader pipeline:
  • testSkinRoot.gltf 覆盖 missing skin.skeleton 的保守 fallback:只用 skin.joints 的 LCA 推断 rootBone,不把 mesh owner 节点纳入 LCA;断言 rootBone 是 mixamorig:Hips,不是 GLTF_ROOT
  • testSingleSkeleton.gltf 验证当 joints 收敛到同一个角色根节点时,即使 scene 里有无关 sibling,也不应该把 rootBone 提升到整个 GLTF_ROOT wrapper。
  • testSkinRootBounds.gltf 验证 explicit skin.skeleton 可以指向不在 skin.joints 里的 rootBone,并且 GLTFSceneParserinverse(rootBone.worldMatrix) 计算出的 localBounds 和最终 bounds 都落在预期空间。
  1. packages/loader/tests/GLTFSkinParser.test.ts 保留 parser helper 的 focused coverage,但不放在 packages/loader/src 下。这样 coverage 仍能覆盖 GLTFSkinParser 的 changed lines,同时不会被 packages/loader/tsconfig.jsonsrc/**/* 收进 declaration build,也不会进入发布包的 types/**/* artifact。

本次整理后的当前 head:56cf6ba1d test(loader): cover gltf skin parser outside src

验证:

  • pnpm -F @galacean/engine-loader run b:types passed
  • pnpm vitest run packages/loader/tests/GLTFSkinParser.test.ts passed
  • pnpm vitest run packages/loader/tests/GLTFSkinParser.test.ts --coverage passed
  • pnpm vitest run tests/src/loader/GLTFLoader.test.ts --testNamePattern "Multi-root skins without skeleton|Skinned mesh bounds" passed
  • targeted eslint on both changed test files passed
  • git diff --check passed
  • npm pack --dry-run from packages/loader no longer includes GLTFSkinParser.test.d.ts or packages/loader/tests
  • GitHub checks are green on 56cf6ba1d39c6b2c19c99ff64bb3468b12a066a0, including codecov/patch

@luzhuang luzhuang changed the title fix(loader): split shaderlab glTF fixes fix(loader): correct glTF skin rootBone inference and bounds Jun 11, 2026
@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 33.33333% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.52%. Comparing base (de75496) to head (c3f279d).

Files with missing lines Patch % Lines
packages/loader/src/gltf/parser/GLTFSceneParser.ts 33.33% 4 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           dev/2.0    #3027      +/-   ##
===========================================
+ Coverage    77.48%   77.52%   +0.03%     
===========================================
  Files          914      914              
  Lines       101783   101678     -105     
  Branches     10430    10431       +1     
===========================================
- Hits         78862    78821      -41     
+ Misses       22738    22673      -65     
- Partials       183      184       +1     
Flag Coverage Δ
unittests 77.52% <33.33%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/src/loader/GLTFLoader.test.ts (1)

97-149: ⚡ Quick win

Consider adding a mesh node that uses the skin to fully exercise the updated LCA algorithm.

The fixture defines joints [0, 1] but no node has a skin property, meaning no mesh uses this skin. According to the PR objectives and context snippet 1, the updated LCA algorithm collects "joint indices plus nodes whose node.skin === skinIndex". Without a mesh node referencing the skin, the test validates only the joints-only path, not the full algorithm.

For comparison:

  • testSkinRoot.gltf (line 57): node 0 has skin: 0
  • testSkinRootBounds.gltf (line 179): node 1 has mesh: 0, skin: 0
🔧 Suggested enhancement

Add a mesh reference and skin property to one of the nodes, for example:

 nodes: [
   {
     name: "Character_Root",
-    children: [1]
+    children: [1, 3]
   },
   {
     name: "mixamorig:Hips"
   },
   {
     name: "Light"
+  },
+  {
+    name: "Character_Man",
+    mesh: 0,
+    skin: 0
   }
 ],
+meshes: [
+  {
+    primitives: [
+      {
+        attributes: { POSITION: 1 },
+        mode: 4
+      }
+    ]
+  }
+],

And add a position accessor pointing to the buffer. This would ensure the test exercises the full LCA calculation including mesh nodes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/src/loader/GLTFLoader.test.ts` around lines 97 - 149, The test fixture
for context.glTFResource.url.endsWith("testSingleSkeleton.gltf") defines skins
with joints [0,1] but no node references that skin, so update the mocked glTF to
include a mesh node that uses the skin (e.g., set one of the nodes to include
mesh: 0 and skin: 0), add a corresponding meshes entry with a primitive that
references a POSITION accessor, and add a POSITION accessor (and any necessary
bufferView) pointing into the existing buffer so the loader's LCA logic (which
looks for joint indices plus nodes where node.skin === skinIndex) is exercised
fully; modify the nodes array, meshes array, and accessors/bufferViews to
reflect the mesh+accessor relationship.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/src/loader/GLTFLoader.test.ts`:
- Around line 97-149: The test fixture for
context.glTFResource.url.endsWith("testSingleSkeleton.gltf") defines skins with
joints [0,1] but no node references that skin, so update the mocked glTF to
include a mesh node that uses the skin (e.g., set one of the nodes to include
mesh: 0 and skin: 0), add a corresponding meshes entry with a primitive that
references a POSITION accessor, and add a POSITION accessor (and any necessary
bufferView) pointing into the existing buffer so the loader's LCA logic (which
looks for joint indices plus nodes where node.skin === skinIndex) is exercised
fully; modify the nodes array, meshes array, and accessors/bufferViews to
reflect the mesh+accessor relationship.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 733075d1-1a8a-4cc4-b892-1fef080893f1

📥 Commits

Reviewing files that changed from the base of the PR and between 010262e and 884b857.

📒 Files selected for processing (1)
  • tests/src/loader/GLTFLoader.test.ts

GuoLei1990

This comment was marked as outdated.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/loader/src/gltf/parser/GLTFSkinParser.ts`:
- Line 45: The LCA fallback currently calls _findSkinRootBoneByLCA(joints,
entities) using only the joints list, which drops mesh nodes that reference this
skin; update the input to _findSkinRootBoneByLCA to include both the joint nodes
and any entity nodes whose glTF node has node.skin === skinIndex (i.e. merge
joints with entities.filter(e => e.gltfNode?.skin === skinIndex)) so the LCA
calculation considers mesh participants as well; apply the same change to the
other occurrence that computes the no-skeleton LCA (the other call site around
the current fallback logic) so both rootBone computations include
skin-referencing nodes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 29c8e4a4-950e-460e-833d-c04f9658ecef

📥 Commits

Reviewing files that changed from the base of the PR and between 884b857 and 02542af.

📒 Files selected for processing (4)
  • notes/loader/2026-06-11-gltf-skin-rootbone-visual-ci.md
  • packages/loader/src/gltf/parser/GLTFSkinParser.test.ts
  • packages/loader/src/gltf/parser/GLTFSkinParser.ts
  • tests/src/loader/GLTFLoader.test.ts
✅ Files skipped from review due to trivial changes (1)
  • notes/loader/2026-06-11-gltf-skin-rootbone-visual-ci.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/src/loader/GLTFLoader.test.ts
  • packages/loader/src/gltf/parser/GLTFSkinParser.test.ts

Comment thread packages/loader/src/gltf/parser/GLTFSkinParser.ts Outdated
@luzhuang luzhuang changed the title fix(loader): correct glTF skin rootBone inference and bounds fix(loader): compute glTF skin bounds in rootBone space Jun 11, 2026
@luzhuang

Copy link
Copy Markdown
Contributor Author

Update after CI RCA on 02542af:

The PR no longer expands the missing-skin.skeleton fallback to joints + mesh nodes using the skin. That intermediate approach was too broad: node.skin === skinIndex only means the mesh uses the skin, not that the mesh node is part of the skeleton hierarchy. Including it in LCA can promote rootBone above the actual skeleton root and change renderer transform space; this was the cause of the visual snapshot diffs.

Current boundary:

  • explicit skin.skeleton remains authoritative, including the valid case where it points to a root outside skin.joints;
  • missing skin.skeleton uses joints-only LCA as the conservative fallback;
  • GLTFSceneParser still fixes the real bounds issue for non-joint rootBone by using inverse(rootBone.worldMatrix) instead of averaging child joint IBM.

I updated the PR title/body and corrected the earlier explanatory comments to match this current behavior. CI is green on head 02542af8869699b1588bb38ed63e73904fdf19d5.

@luzhuang

luzhuang commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up cleanup across 0c3b250f4 and 56cf6ba1d:

  • removed packages/loader/src/gltf/parser/GLTFSkinParser.test.ts so package-local test code no longer participates in the loader declaration build or published types/**/* artifact;
  • moved the parser helper coverage to packages/loader/tests/GLTFSkinParser.test.ts, which is outside package src but still covered by the package Vitest project and Codecov;
  • kept behavior-level coverage in tests/src/loader/GLTFLoader.test.ts through the real loader pipeline;
  • removed the stale skin: 0 field from the missing-skeleton fixture because mesh-owner nodes are intentionally ignored by the current joints-only LCA fallback.

Local verification for this cleanup:

  • pnpm -F @galacean/engine-loader run b:types
  • pnpm vitest run packages/loader/tests/GLTFSkinParser.test.ts
  • pnpm vitest run packages/loader/tests/GLTFSkinParser.test.ts --coverage
  • pnpm vitest run tests/src/loader/GLTFLoader.test.ts --testNamePattern "Multi-root skins without skeleton|Skinned mesh bounds"
  • targeted eslint on tests/src/loader/GLTFLoader.test.ts and packages/loader/tests/GLTFSkinParser.test.ts
  • git diff --check
  • npm pack --dry-run from packages/loader, confirming GLTFSkinParser.test.d.ts and packages/loader/tests are not included.

GitHub checks are green on head 56cf6ba1d39c6b2c19c99ff64bb3468b12a066a0, including codecov/patch.

GuoLei1990

This comment was marked as outdated.

@luzhuang

luzhuang commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

已按这轮 review 收敛到当前 head 2e8696958

  • 删除 tracked notes,并在 /.gitignore 加了 /notes/,避免后续本地排障 notes 进入 PR diff;
  • GLTFSkinParser.ts 已还原到 dev/2.0 基线。重新核对后发现 dev/2.0 里的缺失 skin.skeleton fallback 本来就只基于 skin.joints 做 LCA,并不会把使用该 skin 的 mesh node 纳入 LCA;之前剩下的源码改动只是注释和 helper 命名调整,没有必要保留,也会扩大无意义 patch 面;
  • 保留的真实 runtime 修复是 GLTFSceneParser:当显式 skin.skeleton 指向不在 skin.joints 内的 rootBone 时,local bounds 改为用 inverse(rootBone.worldMatrix) 转到 rootBone space,不再平均子 joint 的 inverse bind matrix;
  • tests/src/loader/GLTFLoader.test.ts 继续只通过公开 loader 链路覆盖两个行为:缺失 skeleton 时 joints-only LCA,以及 rootBone outside joints 的 skinned bounds。

验证:

  • pnpm -F @galacean/engine-loader run b:types
  • pnpm vitest run tests/src/loader/GLTFLoader.test.ts --testNamePattern "Multi-root skins without skeleton|Skinned mesh bounds"
  • pnpm vitest run tests/src/loader/GLTFLoader.test.ts --testNamePattern "Skinned mesh bounds" --coverage
  • CI=true npm run coverage
  • targeted eslint + git diff --check

远端状态:GitHub Actions 全绿,codecov/project 也绿;codecov/patch 仍把 GLTFSceneParser.ts 的两行报成 0% patch。这个分支本地按 CI 顺序 npm run build 后跑 targeted coverage 时,coverage 表里 GLTFSceneParser.ts 只缺 196-197(rootBone 已在 joints 内的旧分支),新分支 198-200 是命中的。这里我没有为了 Codecov patch 再加 private method 调用或 coverage ignore,避免把测试变成覆盖率补丁。

@luzhuang

Copy link
Copy Markdown
Contributor Author

已处理这两条 non-blocking 建议,当前 head fdae6cf08

  • testSkinRootBounds.gltf 的 explicit rootBone case 从纯 translation 扩成了 non-uniform scale:Character_Group 现在带 scale: [2, 3, 1],测试同时断言 rootBone-space localBounds 和最终 world bounds 的 x/y 结果,用来覆盖 inverse 方向和 scale 维度。
  • PR description 已更新,verification 路径改成当前实际的 tests/src/loader/GLTFLoader.test.ts,并移除了旧的 packages/loader/tests/GLTFSkinParser.test.ts 描述。

本地验证:

  • pnpm -F @galacean/engine-loader run b:types
  • pnpm vitest run tests/src/loader/GLTFLoader.test.ts --testNamePattern "Multi-root skins without skeleton|Skinned mesh bounds"
  • pnpm vitest run tests/src/loader/GLTFLoader.test.ts --testNamePattern "Skinned mesh bounds" --coverage
  • pnpm exec eslint --no-eslintrc --config .eslintrc.js --resolve-plugins-relative-to . tests/src/loader/GLTFLoader.test.ts
  • git diff --check

远端 fdae6cf08 上 GitHub Actions 全部通过,codecov/project 通过;codecov/patch 仍是之前同一个 GLTFSceneParser.ts 两行 0% patch 判定。

GuoLei1990

This comment was marked as outdated.

@GuoLei1990 GuoLei1990 force-pushed the fix/gltf-loader-shaderlab-split branch from d9614ca to 12757c3 Compare June 13, 2026 12:31
GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

总结

复审 HEAD c3f279dfc,自上轮 approve 后无新提交、作者无新增回复。核心修复不变:GLTFSceneParser._computeLocalBounds else 分支(rootBone ∉ skin.joints)用 inverse(rootBone.worldMatrix) 把 mesh bounds 映射到 rootBone 空间,替代旧的"平均子 joint IBM"近似。

核心不变量再确认成立:运行时出口 SkinnedMeshRenderer._updateBounds 固定乘 rootBone.transform.worldMatrix,所以加载入口必须用同一根 rootBone 的逆世界矩阵归零——rootBone ∈ jointsIBM_rootrootBone ∉ joints 现场算 inverse(rootBone.worldMatrix),两者同一公式,一进一出精确抵消。对规范资产(导出器生成的 IBM 自洽)这是 bind-pose 下的精确解而非近似。测试覆盖了"显式 rootBone 在 joints 外 + 非均匀 rootBone 缩放",断言 rootBone-space localBounds 与最终 world bounds,能守住对称性。

无新问题,维持 approve。

@cptbtptpbcptdtptp cptbtptpbcptdtptp left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@cptbtptpbcptdtptp cptbtptpbcptdtptp merged commit 2419929 into dev/2.0 Jun 15, 2026
11 of 12 checks passed
@GuoLei1990 GuoLei1990 deleted the fix/gltf-loader-shaderlab-split branch June 29, 2026 08:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants