@forgeax/engine-gltf 0.1.27 → 0.1.29
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/README.md +13 -11
- package/dist/bridge.d.ts +7 -0
- package/dist/bridge.d.ts.map +1 -1
- package/dist/cli-gltf.d.ts.map +1 -1
- package/dist/cli-gltf.mjs +19 -17
- package/dist/cli-gltf.mjs.map +1 -1
- package/dist/gltf-importer.d.ts.map +1 -1
- package/dist/importer-entry.mjs +47 -21
- package/dist/importer-entry.mjs.map +1 -1
- package/dist/index.mjs +47 -21
- package/dist/index.mjs.map +1 -1
- package/dist/node-file-entry.mjs +9 -7
- package/dist/node-file-entry.mjs.map +1 -1
- package/dist/parse-gltf.d.ts.map +1 -1
- package/dist/parse-skin.d.ts +8 -16
- package/dist/parse-skin.d.ts.map +1 -1
- package/package.json +13 -16
- package/src/__tests__/bridge.unit.test.ts +70 -51
- package/src/__tests__/cli-gltf.integration.test.ts +1 -1
- package/src/__tests__/gltf.unit.test.ts +96 -16
- package/src/__tests__/morph-import.integration.test.ts +1 -1
- package/src/bridge.ts +34 -10
- package/src/cli-gltf.ts +16 -18
- package/src/errors.ts +3 -3
- package/src/gltf-importer.ts +29 -11
- package/src/parse-gltf.ts +5 -0
- package/src/parse-skin.ts +14 -41
package/src/bridge.ts
CHANGED
|
@@ -36,7 +36,6 @@ import type {
|
|
|
36
36
|
RenderQueue,
|
|
37
37
|
Result,
|
|
38
38
|
SceneAsset,
|
|
39
|
-
SceneEntity,
|
|
40
39
|
Submesh,
|
|
41
40
|
VertexAttributeMap,
|
|
42
41
|
} from '@forgeax/engine-types';
|
|
@@ -459,6 +458,13 @@ export interface GltfBridgeContext {
|
|
|
459
458
|
* matching SkinAsset.jointPaths against the spawn subtree to fill Skin.joints[].
|
|
460
459
|
*/
|
|
461
460
|
readonly skeletonGuidBySkinIndex?: ReadonlyMap<number, string>;
|
|
461
|
+
/**
|
|
462
|
+
* glTF node index -> a live renderer-owned instance collection identity.
|
|
463
|
+
* The bridge is intentionally pure and cannot mint collection ids or copy
|
|
464
|
+
* matrix bytes into ECS. Runtime consumers create collections through their
|
|
465
|
+
* Renderer first, then provide this map when lowering the SceneAsset.
|
|
466
|
+
*/
|
|
467
|
+
readonly instanceCollectionIdsByNodeIndex?: ReadonlyMap<number, number>;
|
|
462
468
|
}
|
|
463
469
|
|
|
464
470
|
interface MutableSceneEntity {
|
|
@@ -547,7 +553,7 @@ function composeMat4(
|
|
|
547
553
|
export function gltfDocToSceneAsset(doc: GltfDoc, ctx: GltfBridgeContext): SceneAsset {
|
|
548
554
|
const sceneIr = doc.scenes[doc.defaultSceneIndex];
|
|
549
555
|
const resultNodes: MutableSceneEntity[] = [];
|
|
550
|
-
if (sceneIr === undefined) return { kind: 'scene', entities:
|
|
556
|
+
if (sceneIr === undefined) return { kind: 'scene', entities: {} } as unknown as SceneAsset;
|
|
551
557
|
const importedLights = doc.lights ?? doc.extensions?.KHR_lights_punctual?.lights ?? [];
|
|
552
558
|
const animationTargetIds = new Map<number, string>();
|
|
553
559
|
for (const clip of doc.animationClips) {
|
|
@@ -705,9 +711,16 @@ export function gltfDocToSceneAsset(doc: GltfDoc, ctx: GltfBridgeContext): Scene
|
|
|
705
711
|
components.MeshRenderer = { materials: [] };
|
|
706
712
|
}
|
|
707
713
|
|
|
708
|
-
// Instances on the same entity as MeshFilter/MeshRenderer.
|
|
714
|
+
// Instances on the same entity as MeshFilter/MeshRenderer. The bridge
|
|
715
|
+
// only carries the renderer-owned identity; IR matrix bytes stay in the
|
|
716
|
+
// caller's collection and never become an ECS managed array. Build-time
|
|
717
|
+
// consumers that do not have a Renderer omit this optional association and
|
|
718
|
+
// must materialise the collection at their runtime boundary.
|
|
709
719
|
if (ir.instancing !== undefined) {
|
|
710
|
-
|
|
720
|
+
const collectionId = ctx.instanceCollectionIdsByNodeIndex?.get(gltfNodeIdx);
|
|
721
|
+
if (collectionId !== undefined) {
|
|
722
|
+
components.Instances = { collectionId };
|
|
723
|
+
}
|
|
711
724
|
}
|
|
712
725
|
|
|
713
726
|
if (isCamera) {
|
|
@@ -751,10 +764,21 @@ export function gltfDocToSceneAsset(doc: GltfDoc, ctx: GltfBridgeContext): Scene
|
|
|
751
764
|
|
|
752
765
|
for (const rootIdx of sceneIr.nodes) visit(rootIdx, null);
|
|
753
766
|
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
767
|
+
const keyByLocalId = new Map<number, string>();
|
|
768
|
+
for (const node of resultNodes) keyByLocalId.set(node.localIdx, `node-${node.localIdx}`);
|
|
769
|
+
const entities: Record<string, { readonly components: Record<string, Record<string, unknown>> }> =
|
|
770
|
+
{};
|
|
771
|
+
for (const node of resultNodes) {
|
|
772
|
+
const components = { ...node.components };
|
|
773
|
+
const childOf = components.ChildOf;
|
|
774
|
+
if (childOf !== undefined && typeof childOf.parent === 'number') {
|
|
775
|
+
const parentKey = keyByLocalId.get(childOf.parent);
|
|
776
|
+
if (parentKey === undefined)
|
|
777
|
+
throw new Error(`gltfDocToSceneAsset: missing parent node ${childOf.parent}`);
|
|
778
|
+
components.ChildOf = { ...childOf, parent: parentKey };
|
|
779
|
+
}
|
|
780
|
+
entities[`node-${node.localIdx}`] = { components };
|
|
781
|
+
}
|
|
758
782
|
const lightFacts = importedLights.map((light) => ({
|
|
759
783
|
kind: light.type,
|
|
760
784
|
intensity: light.intensity,
|
|
@@ -763,9 +787,9 @@ export function gltfDocToSceneAsset(doc: GltfDoc, ctx: GltfBridgeContext): Scene
|
|
|
763
787
|
}));
|
|
764
788
|
return {
|
|
765
789
|
kind: 'scene',
|
|
766
|
-
entities
|
|
790
|
+
entities,
|
|
767
791
|
...(lightFacts.length === 0 ? {} : { lights: lightFacts }),
|
|
768
|
-
} as SceneAsset;
|
|
792
|
+
} as unknown as SceneAsset;
|
|
769
793
|
}
|
|
770
794
|
|
|
771
795
|
/** Internal helper: mark GltfNodeIr usable so future surface evolutions stay typed. */
|
package/src/cli-gltf.ts
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// @forgeax/engine-gltf/src/cli-gltf —
|
|
3
|
-
//
|
|
4
|
-
// 2.9). Discovered by the base bin via the kubectl 4th-path
|
|
5
|
-
// `forgeax-engine-remote-` prefix scanner.
|
|
2
|
+
// @forgeax/engine-gltf/src/cli-gltf — internal producer used by DevKit's
|
|
3
|
+
// unified `forgeax asset import` command. It is deliberately not a package bin.
|
|
6
4
|
//
|
|
7
5
|
// Single subcommand `import` for v1 (UX break vs the prior
|
|
8
|
-
// `forgeax
|
|
6
|
+
// `forgeax asset import`):
|
|
9
7
|
//
|
|
10
|
-
// write mode `forgeax
|
|
8
|
+
// write mode `forgeax asset import <gltf-or-glb> --root <project>`
|
|
11
9
|
// Parses the source via parseGlb / parseGltf and writes the
|
|
12
10
|
// sibling `<source>.meta.json` sidecar (sorted-keys, LF line
|
|
13
11
|
// ending — byte-stable so a clean reimport produces no diff).
|
|
14
12
|
//
|
|
15
|
-
// --check `forgeax
|
|
13
|
+
// --check `forgeax asset import <dir> --dry-run --root <project>`
|
|
16
14
|
// Dry-run: traverse <dir> reusing SCANNER_BLACKLIST from
|
|
17
15
|
// @forgeax/engine-pack/scanner and surface the first orphan
|
|
18
16
|
// .gltf / .glb whose `<source>.meta.json` is absent as
|
|
@@ -55,14 +53,14 @@ function emitError(ctx: AssetCtx, err: ErrShape): number {
|
|
|
55
53
|
|
|
56
54
|
function helpBody(): string {
|
|
57
55
|
return [
|
|
58
|
-
'forgeax
|
|
56
|
+
'forgeax asset import — glTF / GLB sidecar importer (internal producer)',
|
|
59
57
|
'',
|
|
60
58
|
'Usage:',
|
|
61
|
-
' forgeax
|
|
62
|
-
' forgeax
|
|
59
|
+
' forgeax asset import <path.gltf|path.glb> --root <project>',
|
|
60
|
+
' forgeax asset import <dir> --dry-run --root <project>',
|
|
63
61
|
'',
|
|
64
|
-
'
|
|
65
|
-
'
|
|
62
|
+
'produces texture, mesh, material, and scene sub-asset entries in a sibling',
|
|
63
|
+
'<source>.meta.json sidecar.',
|
|
66
64
|
'',
|
|
67
65
|
].join('\n');
|
|
68
66
|
}
|
|
@@ -77,7 +75,7 @@ export async function runCliGltf(rest: string[], ctx: AssetCtx): Promise<number>
|
|
|
77
75
|
return emitError(ctx, {
|
|
78
76
|
code: 'unknown-subcommand',
|
|
79
77
|
expected: "subcommand 'import'",
|
|
80
|
-
hint: "run 'forgeax
|
|
78
|
+
hint: "run 'forgeax help asset import' for usage",
|
|
81
79
|
detail: { subcommand: sub },
|
|
82
80
|
});
|
|
83
81
|
}
|
|
@@ -100,8 +98,8 @@ async function runImport(rest: string[], ctx: AssetCtx): Promise<number> {
|
|
|
100
98
|
const message = e instanceof Error ? e.message : String(e);
|
|
101
99
|
return emitError(ctx, {
|
|
102
100
|
code: 'cli-parse-error',
|
|
103
|
-
expected: 'forgeax
|
|
104
|
-
hint: "run 'forgeax
|
|
101
|
+
expected: 'forgeax asset import [--dry-run] <path> --root <project>',
|
|
102
|
+
hint: "run 'forgeax help asset import' for usage",
|
|
105
103
|
detail: { message },
|
|
106
104
|
});
|
|
107
105
|
}
|
|
@@ -110,8 +108,8 @@ async function runImport(rest: string[], ctx: AssetCtx): Promise<number> {
|
|
|
110
108
|
return emitError(ctx, {
|
|
111
109
|
code: 'cli-parse-error',
|
|
112
110
|
expected: check
|
|
113
|
-
? 'forgeax
|
|
114
|
-
: 'forgeax
|
|
111
|
+
? 'forgeax asset import <dir> --dry-run --root <project>'
|
|
112
|
+
: 'forgeax asset import <path.gltf|path.glb> --root <project>',
|
|
115
113
|
hint: 'pass a positional <gltf-or-glb> argument; with --check pass a directory',
|
|
116
114
|
});
|
|
117
115
|
}
|
|
@@ -192,7 +190,7 @@ async function runCheck(target: string, ctx: AssetCtx): Promise<number> {
|
|
|
192
190
|
const message = e instanceof Error ? e.message : String(e);
|
|
193
191
|
return emitError(ctx, {
|
|
194
192
|
code: 'cli-parse-error',
|
|
195
|
-
expected: 'forgeax
|
|
193
|
+
expected: 'forgeax asset import <dir> --dry-run --root <project>',
|
|
196
194
|
hint: 'pass a directory that exists and is readable',
|
|
197
195
|
detail: { path: target, message },
|
|
198
196
|
});
|
package/src/errors.ts
CHANGED
|
@@ -267,7 +267,7 @@ const gltfErrorPolicy = {
|
|
|
267
267
|
'gltf-malformed-header': {
|
|
268
268
|
expected:
|
|
269
269
|
'GLB 12-byte header (magic 0x46546C67 + version=2 + length) plus mandatory JSON chunk',
|
|
270
|
-
hint: 'verify .glb is not truncated; rerun: forgeax
|
|
270
|
+
hint: 'verify .glb is not truncated; rerun: forgeax asset import <path> --root <project> --json',
|
|
271
271
|
},
|
|
272
272
|
'gltf-version-unsupported': {
|
|
273
273
|
expected: 'asset.version === "2.0"',
|
|
@@ -305,7 +305,7 @@ const gltfErrorPolicy = {
|
|
|
305
305
|
},
|
|
306
306
|
'gltf-meta-missing': {
|
|
307
307
|
expected: "sidecar <source>.meta.json (importer: 'gltf') present in same directory",
|
|
308
|
-
hint: 'run: forgeax
|
|
308
|
+
hint: 'run: forgeax asset import <path> --root <project> --json',
|
|
309
309
|
},
|
|
310
310
|
'gltf-instancing-count-mismatch': {
|
|
311
311
|
expected: 'all instance attribute accessors share the same count',
|
|
@@ -334,7 +334,7 @@ const gltfErrorPolicy = {
|
|
|
334
334
|
'gltf-image-extract-failed': {
|
|
335
335
|
expected:
|
|
336
336
|
'image bytes extractable from bufferView / data-URI / external URI without corruption',
|
|
337
|
-
hint: 'verify the bufferView byte range / data: URI base64 / external URI sibling file is intact next to the .gltf source; rerun: forgeax
|
|
337
|
+
hint: 'verify the bufferView byte range / data: URI base64 / external URI sibling file is intact next to the .gltf source; rerun: forgeax asset import <path> --root <project> --json',
|
|
338
338
|
},
|
|
339
339
|
'gltf-skin-attr-asymmetric': {
|
|
340
340
|
expected:
|
package/src/gltf-importer.ts
CHANGED
|
@@ -51,6 +51,7 @@ import type {
|
|
|
51
51
|
import {
|
|
52
52
|
IMPORT_ERROR_HINTS,
|
|
53
53
|
ImportError,
|
|
54
|
+
readConservativeAnimatedBounds,
|
|
54
55
|
reconcileMeshMaterialSlotTopology,
|
|
55
56
|
resolveMeshMaterialSlotDefaultGuid,
|
|
56
57
|
toShared,
|
|
@@ -72,7 +73,6 @@ import { parseGlbForImporter, parseGltfForImporter } from './parse-gltf.js';
|
|
|
72
73
|
type ParseDocResult =
|
|
73
74
|
| { readonly ok: true; readonly value: GltfDoc }
|
|
74
75
|
| { readonly ok: false; readonly error: ImportError };
|
|
75
|
-
|
|
76
76
|
function isGlbBytes(source: string): boolean {
|
|
77
77
|
return source.toLowerCase().endsWith('.glb');
|
|
78
78
|
}
|
|
@@ -83,6 +83,23 @@ function publishesCatalogProduct(input: {
|
|
|
83
83
|
return input.importSettings.geometry !== 'procedural';
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
function applyImportSettingsBounds(
|
|
87
|
+
doc: GltfDoc,
|
|
88
|
+
importSettings: Readonly<Record<string, unknown>>,
|
|
89
|
+
): GltfDoc {
|
|
90
|
+
let changed = false;
|
|
91
|
+
const skeletons = doc.skeletons.map((record, sourceIndex) => {
|
|
92
|
+
// Source extras are the primary producer path. The sidecar row is an
|
|
93
|
+
// explicit external-producer override for sources whose authoring tool
|
|
94
|
+
// cannot carry ForgeaX extras; neither path derives a bind-pose AABB.
|
|
95
|
+
const bounds = readConservativeAnimatedBounds(importSettings, sourceIndex);
|
|
96
|
+
if (bounds === undefined || record.bounds !== undefined) return record;
|
|
97
|
+
changed = true;
|
|
98
|
+
return { ...record, bounds };
|
|
99
|
+
});
|
|
100
|
+
return changed ? { ...doc, skeletons } : doc;
|
|
101
|
+
}
|
|
102
|
+
|
|
86
103
|
function previousMaterialSlotTopology(
|
|
87
104
|
ctx: ImportContext,
|
|
88
105
|
meshSourceKey: string | undefined,
|
|
@@ -527,7 +544,7 @@ async function importGltf(
|
|
|
527
544
|
}
|
|
528
545
|
const parsed = await parseDoc(ctx.source, read.value, ctx, meshopt);
|
|
529
546
|
if (!parsed.ok) return parsed;
|
|
530
|
-
const doc = parsed.value;
|
|
547
|
+
const doc = applyImportSettingsBounds(parsed.value, ctx.importSettings);
|
|
531
548
|
const maps = buildHandleMaps(ctx.subAssets, doc);
|
|
532
549
|
|
|
533
550
|
// Pre-derive each images[] row's colorSpace from material slot bindings
|
|
@@ -921,30 +938,30 @@ async function importGltf(
|
|
|
921
938
|
// SkinAsset.jointPaths.
|
|
922
939
|
//
|
|
923
940
|
// D-2 / D-3: refs carries structured edge metadata (AssetRef[]).
|
|
924
|
-
// Walk scene entities to build a handle-value -> (
|
|
941
|
+
// Walk keyed scene entities to build a handle-value -> (entityKey,
|
|
925
942
|
// componentName, fieldName, arrayIndex?) provenance map, then
|
|
926
|
-
// produce AssetRef[] with sourceField /
|
|
943
|
+
// produce AssetRef[] with sourceField / sceneEntityKey filled for mesh
|
|
927
944
|
// handle-field edges. Skeleton edges: sourceField from Skin.skeleton if entity
|
|
928
945
|
// carries that GUID. Skin edges: sourceField=undefined (cross-edge
|
|
929
946
|
// with no entity-component representation).
|
|
930
947
|
const handleValueProvenance = new Map<
|
|
931
948
|
number,
|
|
932
|
-
{
|
|
949
|
+
{ sceneEntityKey: string; componentName: string; fieldName: string; arrayIndex?: number }
|
|
933
950
|
>();
|
|
934
|
-
const skeletonGuidProvenance = new Map<string, {
|
|
935
|
-
for (const entity of scene.entities) {
|
|
951
|
+
const skeletonGuidProvenance = new Map<string, { sceneEntityKey: string }>();
|
|
952
|
+
for (const [sceneEntityKey, entity] of Object.entries(scene.entities)) {
|
|
936
953
|
const comps = entity.components as Record<string, Record<string, unknown>>;
|
|
937
954
|
const mf = comps.MeshFilter;
|
|
938
955
|
if (mf !== undefined && typeof mf.assetHandle === 'number') {
|
|
939
956
|
handleValueProvenance.set(mf.assetHandle, {
|
|
940
|
-
|
|
957
|
+
sceneEntityKey,
|
|
941
958
|
componentName: 'MeshFilter',
|
|
942
959
|
fieldName: 'assetHandle',
|
|
943
960
|
});
|
|
944
961
|
}
|
|
945
962
|
const skin = comps.Skin;
|
|
946
963
|
if (skin !== undefined && typeof skin.skeleton === 'string') {
|
|
947
|
-
skeletonGuidProvenance.set(skin.skeleton, {
|
|
964
|
+
skeletonGuidProvenance.set(skin.skeleton, { sceneEntityKey });
|
|
948
965
|
}
|
|
949
966
|
}
|
|
950
967
|
|
|
@@ -960,7 +977,7 @@ async function importGltf(
|
|
|
960
977
|
fieldName: prov.fieldName,
|
|
961
978
|
...(prov.arrayIndex !== undefined ? { arrayIndex: prov.arrayIndex } : {}),
|
|
962
979
|
},
|
|
963
|
-
|
|
980
|
+
sceneEntityKey: prov.sceneEntityKey,
|
|
964
981
|
};
|
|
965
982
|
}
|
|
966
983
|
return { guid };
|
|
@@ -981,7 +998,7 @@ async function importGltf(
|
|
|
981
998
|
? {
|
|
982
999
|
guid,
|
|
983
1000
|
sourceField: { componentName: 'Skin', fieldName: 'skeleton' },
|
|
984
|
-
|
|
1001
|
+
sceneEntityKey: skProv.sceneEntityKey,
|
|
985
1002
|
}
|
|
986
1003
|
: { guid },
|
|
987
1004
|
);
|
|
@@ -1025,6 +1042,7 @@ async function importGltf(
|
|
|
1025
1042
|
kind: 'skeleton' as const,
|
|
1026
1043
|
inverseBindMatrices: rec.inverseBindMatrices,
|
|
1027
1044
|
jointCount: rec.jointCount,
|
|
1045
|
+
...(rec.bounds === undefined ? {} : { bounds: rec.bounds }),
|
|
1028
1046
|
};
|
|
1029
1047
|
out.push({ guid: sub.guid, kind: 'skeleton', payload, refs: [], artifacts: {} });
|
|
1030
1048
|
} else if (sub.kind === 'skin') {
|
package/src/parse-gltf.ts
CHANGED
|
@@ -400,6 +400,11 @@ interface RootGltfJson extends GltfExtensionsJson {
|
|
|
400
400
|
readonly name?: string;
|
|
401
401
|
readonly joints: readonly number[];
|
|
402
402
|
readonly inverseBindMatrices?: number;
|
|
403
|
+
readonly extras?: {
|
|
404
|
+
readonly forgeax?: {
|
|
405
|
+
readonly conservativeAnimatedBounds?: unknown;
|
|
406
|
+
};
|
|
407
|
+
};
|
|
403
408
|
}>;
|
|
404
409
|
readonly meshes?: readonly MeshJson[];
|
|
405
410
|
readonly materials?: ReadonlyArray<GltfMaterialJson>;
|
package/src/parse-skin.ts
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
// - plan-strategy D-1 (3-asset separation: IBM / skin binding / animation clip)
|
|
9
9
|
// - plan-strategy D-2 (skin index dedupe via reimport reuse managed by toAssetPack)
|
|
10
10
|
// - requirements AC-03 (skin index dedupe), AC-05 (jointPaths + Name missing fail-fast)
|
|
11
|
-
// - requirements AC-10 (IR extension)
|
|
12
|
-
// - plan-strategy D-11 (BindPose AABB importer-phase, per-frame zero cost)
|
|
11
|
+
// - requirements AC-10 (IR extension)
|
|
13
12
|
// - charter P3 (fail-fast on invalid data)
|
|
14
13
|
|
|
14
|
+
import { parseConservativeAnimatedBounds } from '@forgeax/engine-types';
|
|
15
15
|
import { decodeF32Accessor } from './accessor/decode-accessor.js';
|
|
16
16
|
import { err, type GltfError, gltfErr, ok, type Result } from './errors.js';
|
|
17
17
|
import { buildNodeParentMap, resolveNamedNodePath } from './node-path.js';
|
|
@@ -33,12 +33,20 @@ export interface GltfSkeletonRecord {
|
|
|
33
33
|
readonly inverseBindMatrices: Float32Array;
|
|
34
34
|
/** Per-joint Name path from scene root (parallel to joints array). */
|
|
35
35
|
readonly jointPaths: readonly string[];
|
|
36
|
+
/** Producer-authored conservative animated local bounds when supplied. */
|
|
37
|
+
readonly bounds?: Float32Array;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
interface SkinJson {
|
|
39
41
|
readonly name?: string;
|
|
40
42
|
readonly joints: readonly number[];
|
|
41
43
|
readonly inverseBindMatrices?: number;
|
|
44
|
+
/** glTF source metadata authored by the ForgeaX asset producer. */
|
|
45
|
+
readonly extras?: {
|
|
46
|
+
readonly forgeax?: {
|
|
47
|
+
readonly conservativeAnimatedBounds?: unknown;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
interface NodeJson {
|
|
@@ -185,54 +193,19 @@ export function parseSkin(
|
|
|
185
193
|
jointPaths.push(pathResult.value.join('/'));
|
|
186
194
|
}
|
|
187
195
|
|
|
196
|
+
const bounds = parseConservativeAnimatedBounds(
|
|
197
|
+
skin.extras?.forgeax?.conservativeAnimatedBounds,
|
|
198
|
+
);
|
|
188
199
|
records.push({
|
|
189
200
|
jointCount: joints.length,
|
|
190
201
|
inverseBindMatrices: ibm,
|
|
191
202
|
jointPaths,
|
|
203
|
+
...(bounds === undefined ? {} : { bounds }),
|
|
192
204
|
});
|
|
193
205
|
}
|
|
194
206
|
|
|
195
207
|
return ok(records);
|
|
196
208
|
}
|
|
197
209
|
|
|
198
|
-
/**
|
|
199
|
-
* Compute the BindPose static AABB for a skinned mesh's vertex positions.
|
|
200
|
-
*
|
|
201
|
-
* At bind pose, joint_bind = IBM^{-1}, so skinning collapses:
|
|
202
|
-
* world_pos = Sum(w_i * joint_bind_i * IBM_i * local_pos) = local_pos
|
|
203
|
-
* Therefore the BindPose AABB is simply the local position bounds.
|
|
204
|
-
*
|
|
205
|
-
* Per-frame zero cost: stored in mesh asset metadata at importer time.
|
|
206
|
-
* Dynamic AABB for animated poses is deferred to OOS-skin-dyn-bounds.
|
|
207
|
-
*
|
|
208
|
-
* Returns { min: [x,y,z], max: [x,y,z] } or undefined if positions is empty.
|
|
209
|
-
*/
|
|
210
|
-
export function computeBindPoseAABB(positions: Float32Array):
|
|
211
|
-
| {
|
|
212
|
-
readonly min: readonly [number, number, number];
|
|
213
|
-
readonly max: readonly [number, number, number];
|
|
214
|
-
}
|
|
215
|
-
| undefined {
|
|
216
|
-
if (positions.length < 3) return undefined;
|
|
217
|
-
let minX = positions[0] ?? 0;
|
|
218
|
-
let minY = positions[1] ?? 0;
|
|
219
|
-
let minZ = positions[2] ?? 0;
|
|
220
|
-
let maxX = minX;
|
|
221
|
-
let maxY = minY;
|
|
222
|
-
let maxZ = minZ;
|
|
223
|
-
for (let i = 3; i < positions.length; i += 3) {
|
|
224
|
-
const x = positions[i] ?? 0;
|
|
225
|
-
const y = positions[i + 1] ?? 0;
|
|
226
|
-
const z = positions[i + 2] ?? 0;
|
|
227
|
-
if (x < minX) minX = x;
|
|
228
|
-
if (y < minY) minY = y;
|
|
229
|
-
if (z < minZ) minZ = z;
|
|
230
|
-
if (x > maxX) maxX = x;
|
|
231
|
-
if (y > maxY) maxY = y;
|
|
232
|
-
if (z > maxZ) maxZ = z;
|
|
233
|
-
}
|
|
234
|
-
return { min: [minX, minY, minZ], max: [maxX, maxY, maxZ] };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
210
|
/** Re-export MAX_JOINTS for use by downstream modules. */
|
|
238
211
|
export { MAX_JOINTS };
|