Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 6 additions & 43 deletions packages/loader/src/gltf/parser/GLTFSceneParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { GLTFParserContext, GLTFParserType, registerGLTFParser } from "./GLTFPar

@registerGLTFParser(GLTFParserType.Scene)
export class GLTFSceneParser extends GLTFParser {
private static _tempMatrix: Matrix = new Matrix();

parse(context: GLTFParserContext, index: number): AssetPromise<Entity> {
const {
glTF: { scenes, scene = 0 },
Expand Down Expand Up @@ -195,49 +197,10 @@ export class GLTFSceneParser extends GLTFParser {
if (rootBoneIndex !== -1) {
BoundingBox.transform(mesh.bounds, inverseBindMatrices[rootBoneIndex], skinnedMeshRenderer.localBounds);
} else {
// Root bone is not in joints list, we can only compute approximate inverse bind matrix
// Average all root bone's children inverse bind matrix
const approximateBindMatrix = new Matrix(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
let subRootBoneCount = this._computeApproximateBindMatrix(
bones,
inverseBindMatrices,
rootBone,
approximateBindMatrix
);

if (subRootBoneCount !== 0) {
Matrix.multiplyScalar(approximateBindMatrix, 1.0 / subRootBoneCount, approximateBindMatrix);
BoundingBox.transform(mesh.bounds, approximateBindMatrix, skinnedMeshRenderer.localBounds);
} else {
skinnedMeshRenderer.localBounds.copyFrom(mesh.bounds);
}
// rootBone can be outside skin.joints, so it has no inverse bind matrix
const inverseRootBoneWorld = GLTFSceneParser._tempMatrix;
Matrix.invert(rootBone.transform.worldMatrix, inverseRootBoneWorld);
BoundingBox.transform(mesh.bounds, inverseRootBoneWorld, skinnedMeshRenderer.localBounds);
}
}

private _computeApproximateBindMatrix(
jointEntities: ReadonlyArray<Entity>,
inverseBindMatrices: Matrix[],
rootEntity: Entity,
approximateBindMatrix: Matrix
): number {
let subRootBoneCount = 0;
const children = rootEntity.children;
for (let i = 0, n = children.length; i < n; i++) {
const rootChild = children[i];
const index = jointEntities.indexOf(rootChild);
if (index !== -1) {
Matrix.add(approximateBindMatrix, inverseBindMatrices[index], approximateBindMatrix);
subRootBoneCount++;
} else {
subRootBoneCount += this._computeApproximateBindMatrix(
jointEntities,
inverseBindMatrices,
rootChild,
approximateBindMatrix
);
}
}

return subRootBoneCount;
}
}
139 changes: 127 additions & 12 deletions tests/src/loader/GLTFLoader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ beforeAll(async function () {
class GLTFCustomJSONParser extends GLTFParser {
parse(context: GLTFParserContext) {
if (context.glTFResource.url.endsWith("testSkinRoot.gltf")) {
context.buffers = [new ArrayBuffer(192)];
context.buffers = [new ArrayBuffer(128)];
return Promise.resolve({
asset: {
version: "2.0"
Expand All @@ -66,29 +66,28 @@ beforeAll(async function () {
skins: [
{
inverseBindMatrices: 0,
// Joints span both top-level scene roots: Character_Man (0) and Hips (1)/Spine (2).
joints: [0, 1, 2]
joints: [1, 2]
}
],
accessors: [
{
bufferView: 0,
byteOffset: 0,
componentType: 5126,
count: 3,
count: 2,
type: "MAT4"
}
],
bufferViews: [
{
buffer: 0,
byteOffset: 0,
byteLength: 192
byteLength: 128
}
],
buffers: [
{
byteLength: 192
byteLength: 128
}
]
});
Expand All @@ -103,7 +102,6 @@ beforeAll(async function () {
scene: 0,
scenes: [
{
// Two top-level roots: a character skeleton and an unrelated sibling (e.g., a light).
nodes: [0, 2]
}
],
Expand All @@ -122,7 +120,6 @@ beforeAll(async function () {
skins: [
{
inverseBindMatrices: 0,
// All joints converge to a single top-level root (Character_Root).
joints: [0, 1]
}
],
Expand Down Expand Up @@ -150,6 +147,103 @@ beforeAll(async function () {
});
}

if (context.glTFResource.url.endsWith("testSkinRootBounds.gltf")) {
const buffer = new ArrayBuffer(152);
const floats = new Float32Array(buffer);
// Inverse bind matrices for Hips and Spine; Character_Group scales by
// (2, 3, 1), then offsets Hips by 10 on x, so Hips world x is 23.
floats.set([0.5, 0, 0, 0, 0, 1 / 3, 0, 0, 0, 0, 1, 0, -11.5, 0, 0, 1], 0);
floats.set([0.5, 0, 0, 0, 0, 1 / 3, 0, 0, 0, 0, 1, 0, -11.5, 0, 0, 1], 16);
floats.set([9, -3, -1, 11, 3, 1], 32);
context.buffers = [buffer];
return Promise.resolve({
asset: {
version: "2.0"
},
scene: 0,
scenes: [
{
nodes: [0]
}
],
nodes: [
{
name: "Character_Group",
translation: [3, 0, 0],
scale: [2, 3, 1],
children: [1, 2]
},
{
name: "Character_Man",
mesh: 0,
skin: 0
},
{
name: "mixamorig:Hips",
translation: [10, 0, 0],
children: [3]
},
{
name: "mixamorig:Spine"
}
],
skins: [
{
inverseBindMatrices: 0,
skeleton: 0,
joints: [2, 3]
}
],
meshes: [
{
primitives: [
{
attributes: {
POSITION: 1
},
mode: 4
}
]
}
],
accessors: [
{
bufferView: 0,
byteOffset: 0,
componentType: 5126,
count: 2,
type: "MAT4"
},
{
bufferView: 1,
byteOffset: 0,
componentType: 5126,
count: 2,
type: "VEC3",
min: [9, -3, -1],
max: [11, 3, 1]
}
],
bufferViews: [
{
buffer: 0,
byteOffset: 0,
byteLength: 128
},
{
buffer: 0,
byteOffset: 128,
byteLength: 24
}
],
buffers: [
{
byteLength: 152
}
]
});
}

const glTF = <any>{
buffers: [
{
Expand Down Expand Up @@ -653,7 +747,7 @@ describe("glTF scene root structure", function () {
expect(defaultSceneRoot.children[0].name).to.equal("entity1");
});

it("Multi-root skins without skeleton should use the scene wrapper as rootBone", async () => {
it("Multi-root skins without skeleton should use the joints LCA as rootBone", async () => {
const glTFResource: GLTFResource = await engine.resourceManager.load({
type: AssetType.GLTF,
url: "mock/path/testSkinRoot.gltf"
Expand All @@ -662,7 +756,8 @@ describe("glTF scene root structure", function () {

expect(defaultSceneRoot.name).to.equal("GLTF_ROOT");
expect(defaultSceneRoot.children.length).to.equal(2);
expect(skins[0].rootBone).to.equal(defaultSceneRoot);
expect(skins[0].rootBone).to.not.equal(defaultSceneRoot);
expect(skins[0].rootBone.name).to.equal("mixamorig:Hips");
});

it("Multi-root scenes whose joints converge to a single top-level root should not use the scene wrapper", async () => {
Expand All @@ -673,12 +768,32 @@ describe("glTF scene root structure", function () {
const { defaultSceneRoot, skins } = glTFResource;

expect(defaultSceneRoot.name).to.equal("GLTF_ROOT");
// Scene has two top-level roots, but all joints converge to "Character_Root".
expect(defaultSceneRoot.children.length).to.equal(2);
expect(skins[0].rootBone).to.not.equal(defaultSceneRoot);
// rootBone should be inside the Character_Root subtree (LCA = Character_Root).
expect(skins[0].rootBone.name).to.equal("Character_Root");
});

it("Skinned mesh bounds should stay in scaled rootBone space when explicit rootBone is outside joints", async () => {
const glTFResource: GLTFResource = await engine.resourceManager.load({
type: AssetType.GLTF,
url: "mock/path/testSkinRootBounds.gltf"
});
const { defaultSceneRoot, skins } = glTFResource;
const characterGroup = defaultSceneRoot.children[0];
const characterMesh = characterGroup.children[0];
const renderer = characterMesh.getComponent(SkinnedMeshRenderer);

expect(skins[0].rootBone).to.equal(characterGroup);
expect(renderer).to.exist;
expect(renderer.localBounds.min.x).to.be.closeTo(3, 1e-5);
expect(renderer.localBounds.max.x).to.be.closeTo(4, 1e-5);
expect(renderer.localBounds.min.y).to.be.closeTo(-1, 1e-5);
expect(renderer.localBounds.max.y).to.be.closeTo(1, 1e-5);
expect(renderer.bounds.min.x).to.be.closeTo(9, 1e-5);
expect(renderer.bounds.max.x).to.be.closeTo(11, 1e-5);
expect(renderer.bounds.min.y).to.be.closeTo(-3, 1e-5);
expect(renderer.bounds.max.y).to.be.closeTo(3, 1e-5);
});
});

describe("glTF instance test", function () {
Expand Down
Loading