@forgeax/engine-gltf 0.1.29 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/engine-gltf",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -35,21 +35,21 @@
35
35
  "LICENSE"
36
36
  ],
37
37
  "dependencies": {
38
- "@forgeax/engine-animation": "0.1.29",
39
- "@forgeax/engine-geometry": "0.1.29",
40
- "@forgeax/engine-import": "0.1.29",
41
- "@forgeax/engine-math": "0.1.29",
42
- "@forgeax/engine-pack": "0.1.29",
43
- "@forgeax/engine-scene": "0.1.29",
44
- "@forgeax/engine-types": "0.1.29",
38
+ "@forgeax/engine-animation": "0.1.30",
39
+ "@forgeax/engine-geometry": "0.1.30",
40
+ "@forgeax/engine-import": "0.1.30",
41
+ "@forgeax/engine-math": "0.1.30",
42
+ "@forgeax/engine-pack": "0.1.30",
43
+ "@forgeax/engine-scene": "0.1.30",
44
+ "@forgeax/engine-types": "0.1.30",
45
45
  "@webgpu/types": "^0.1.71",
46
46
  "meshoptimizer": "^0.20.0"
47
47
  },
48
48
  "devDependencies": {
49
- "@forgeax/engine-assets-runtime": "0.1.29",
50
- "@forgeax/engine-picking": "0.1.29",
51
- "@forgeax/engine-render": "0.1.29",
52
- "@forgeax/engine-runtime": "0.1.29",
49
+ "@forgeax/engine-assets-runtime": "0.1.30",
50
+ "@forgeax/engine-picking": "0.1.30",
51
+ "@forgeax/engine-render": "0.1.30",
52
+ "@forgeax/engine-runtime": "0.1.30",
53
53
  "@types/node": "^20.14.0"
54
54
  },
55
55
  "forgeax": {
@@ -1399,6 +1399,73 @@ function unwrapReimport(result: ReturnType<typeof reimportReuseMeta>) {
1399
1399
  });
1400
1400
  });
1401
1401
 
1402
+ it('derives animated bounds through the real glTF import path without metadata', async () => {
1403
+ const binary = new Uint8Array(72);
1404
+ new Float32Array(binary.buffer, 0, 3).set([1, 0, 0]);
1405
+ new Uint16Array(binary.buffer, 12, 4).set([0, 0, 0, 0]);
1406
+ new Float32Array(binary.buffer, 20, 4).set([1, 0, 0, 0]);
1407
+ new Float32Array(binary.buffer, 36, 2).set([0, 1]);
1408
+ new Float32Array(binary.buffer, 44, 6).set([0, 0, 0, 4, 2, 0]);
1409
+ const guid = '019e2cc6-0c86-79da-aa76-b0984c86d493';
1410
+ const source = {
1411
+ asset: { version: '2.0' },
1412
+ buffers: [
1413
+ {
1414
+ byteLength: 72,
1415
+ uri: `data:application/octet-stream;base64,${Buffer.from(binary).toString('base64')}`,
1416
+ },
1417
+ ],
1418
+ bufferViews: [
1419
+ [0, 12],
1420
+ [12, 8],
1421
+ [20, 16],
1422
+ [36, 8],
1423
+ [44, 24],
1424
+ ].map(([byteOffset, byteLength]) => ({ buffer: 0, byteOffset, byteLength })),
1425
+ accessors: [
1426
+ { bufferView: 0, componentType: 5126, count: 1, type: 'VEC3' },
1427
+ { bufferView: 1, componentType: 5123, count: 1, type: 'VEC4' },
1428
+ { bufferView: 2, componentType: 5126, count: 1, type: 'VEC4' },
1429
+ { bufferView: 3, componentType: 5126, count: 2, type: 'SCALAR' },
1430
+ { bufferView: 4, componentType: 5126, count: 2, type: 'VEC3' },
1431
+ ],
1432
+ meshes: [
1433
+ { primitives: [{ mode: 0, attributes: { POSITION: 0, JOINTS_0: 1, WEIGHTS_0: 2 } }] },
1434
+ ],
1435
+ nodes: [
1436
+ { name: 'root', children: [1, 2] },
1437
+ { name: 'mesh', mesh: 0, skin: 0 },
1438
+ { name: 'joint' },
1439
+ ],
1440
+ scenes: [{ nodes: [0] }],
1441
+ scene: 0,
1442
+ skins: [{ joints: [2] }],
1443
+ animations: [
1444
+ {
1445
+ channels: [{ sampler: 0, target: { node: 2, path: 'translation' } }],
1446
+ samplers: [{ input: 3, output: 4, interpolation: 'LINEAR' }],
1447
+ },
1448
+ ],
1449
+ };
1450
+ const produced = unwrap(
1451
+ await gltfImporter.import(
1452
+ makeDataUriCtx({
1453
+ bytes: new TextEncoder().encode(JSON.stringify(source)),
1454
+ subAssets: [{ guid, kind: 'skeleton', sourceIndex: 0 }],
1455
+ decodeCalls: [],
1456
+ }),
1457
+ ),
1458
+ );
1459
+ const skeleton = produced.find((asset) => asset.guid === guid);
1460
+ if (skeleton?.payload.kind !== 'skeleton') throw new Error('missing skeleton');
1461
+ const bounds = skeleton.payload.bounds;
1462
+ if (bounds === undefined) throw new Error('missing imported animation bounds');
1463
+ expect(bounds[0]).toBeLessThanOrEqual(1);
1464
+ expect(bounds[3]).toBeGreaterThanOrEqual(5);
1465
+ expect(bounds[4]).toBeGreaterThanOrEqual(2);
1466
+ expect((bounds[3] ?? NaN) - (bounds[0] ?? NaN)).toBeLessThan(4.01);
1467
+ });
1468
+
1402
1469
  it('imports producer-authored animated skin bounds from importSettings with the SkinAsset', async () => {
1403
1470
  const skeletonGuid = '019e2cc6-0c86-79da-aa76-b0984c86d490';
1404
1471
  const skinGuid = '019e2cc6-0c86-79da-aa76-b0984c86d491';
@@ -0,0 +1,94 @@
1
+ import {
2
+ type AnimatedBoundsMesh,
3
+ deriveConservativeAnimatedBounds,
4
+ } from '@forgeax/engine-animation/animated-bounds';
5
+ import { buildNodeParentMap, resolveNamedNodePath } from './node-path';
6
+ import type { GltfDoc } from './parse-gltf';
7
+
8
+ /** Source extras and sidecar bounds have already been applied before this producer runs. */
9
+ export function deriveGltfAnimatedBounds(doc: GltfDoc): GltfDoc {
10
+ const parents = buildNodeParentMap(doc.nodes);
11
+ const paths = doc.nodes.map((_, index) => {
12
+ const path = resolveNamedNodePath(doc.nodes, parents, index);
13
+ return path.ok ? path.value.join('/') : undefined;
14
+ });
15
+ const nodes = doc.nodes.map((node, index) => ({
16
+ ...node.transform,
17
+ parent: parents.get(index) ?? null,
18
+ }));
19
+ const channels = doc.animationClips.flatMap((clip) =>
20
+ clip.channels.flatMap((channel) =>
21
+ channel.property === 'weights'
22
+ ? []
23
+ : [
24
+ {
25
+ node: channel.targetNodeIndex,
26
+ property: channel.property,
27
+ values: channel.sampler.output,
28
+ interpolation: channel.sampler.interpolation,
29
+ },
30
+ ],
31
+ ),
32
+ );
33
+ const primitiveStarts: number[] = [];
34
+ let offset = 0;
35
+ for (const count of doc.meshPrimitiveCount?.values() ?? doc.meshes.map(() => 1)) {
36
+ primitiveStarts.push(offset);
37
+ offset += count;
38
+ }
39
+ const skeletons = doc.skeletons.map((skeleton, skinIndex) => {
40
+ if (skeleton.bounds !== undefined) return skeleton;
41
+ const meshes: AnimatedBoundsMesh[] = [];
42
+ for (let index = 0; index < doc.nodes.length; index++) {
43
+ const node = doc.nodes[index];
44
+ if (node === undefined) return skeleton;
45
+ if (node.skinIndex !== skinIndex || node.meshIndex === null) continue;
46
+ const start = primitiveStarts[node.meshIndex];
47
+ if (start === undefined) return skeleton;
48
+ for (const mesh of doc.meshes.slice(
49
+ start,
50
+ start + (doc.meshPrimitiveCount?.get(node.meshIndex) ?? 1),
51
+ )) {
52
+ if (mesh.joints0 === undefined || mesh.weights0 === undefined) return skeleton;
53
+ let maxMorphWeight = Math.max(
54
+ 0,
55
+ ...Array.from(node.morphWeights ?? mesh.morphWeights ?? [], Math.abs),
56
+ );
57
+ for (const clip of doc.animationClips)
58
+ for (const channel of clip.channels)
59
+ if (channel.targetNodeIndex === index && channel.property === 'weights')
60
+ for (const weight of channel.sampler.output)
61
+ maxMorphWeight = Math.max(maxMorphWeight, Math.abs(weight));
62
+ const morphExtent = Float32Array.from(mesh.positions, (_, component) =>
63
+ (mesh.morphTargets ?? []).reduce(
64
+ (sum, target) => sum + Math.abs(target.position?.[component] ?? 0) * maxMorphWeight,
65
+ 0,
66
+ ),
67
+ );
68
+ meshes.push({
69
+ node: index,
70
+ positions: mesh.positions,
71
+ joints: mesh.joints0,
72
+ weights: mesh.weights0,
73
+ morphExtent,
74
+ });
75
+ }
76
+ }
77
+ const jointNodes = skeleton.jointPaths.map((path) => paths.indexOf(path));
78
+ if (
79
+ jointNodes.some(
80
+ (node, index) => node < 0 || paths.lastIndexOf(skeleton.jointPaths[index]) !== node,
81
+ )
82
+ )
83
+ return skeleton;
84
+ const bounds = deriveConservativeAnimatedBounds({
85
+ nodes,
86
+ channels,
87
+ jointNodes,
88
+ inverseBindMatrices: skeleton.inverseBindMatrices,
89
+ meshes,
90
+ });
91
+ return bounds === undefined ? skeleton : { ...skeleton, bounds };
92
+ });
93
+ return { ...doc, skeletons };
94
+ }
@@ -56,6 +56,7 @@ import {
56
56
  resolveMeshMaterialSlotDefaultGuid,
57
57
  toShared,
58
58
  } from '@forgeax/engine-types';
59
+ import { deriveGltfAnimatedBounds } from './animated-bounds';
59
60
  import {
60
61
  gltfDocToSceneAsset,
61
62
  meshIrToMeshAsset,
@@ -544,7 +545,7 @@ async function importGltf(
544
545
  }
545
546
  const parsed = await parseDoc(ctx.source, read.value, ctx, meshopt);
546
547
  if (!parsed.ok) return parsed;
547
- const doc = applyImportSettingsBounds(parsed.value, ctx.importSettings);
548
+ const doc = deriveGltfAnimatedBounds(applyImportSettingsBounds(parsed.value, ctx.importSettings));
548
549
  const maps = buildHandleMaps(ctx.subAssets, doc);
549
550
 
550
551
  // Pre-derive each images[] row's colorSpace from material slot bindings