@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/README.md +1 -1
- package/dist/animated-bounds.d.ts +4 -0
- package/dist/animated-bounds.d.ts.map +1 -0
- package/dist/gltf-importer.d.ts.map +1 -1
- package/dist/importer-entry.mjs +114 -30
- package/dist/importer-entry.mjs.map +1 -1
- package/dist/index.mjs +114 -30
- package/dist/index.mjs.map +1 -1
- package/package.json +12 -12
- package/src/__tests__/gltf.unit.test.ts +67 -0
- package/src/animated-bounds.ts +94 -0
- package/src/gltf-importer.ts +2 -1
package/README.md
CHANGED
|
@@ -228,7 +228,7 @@ When `ImporterRegistry` + `runImport` consumes a glTF source, malformed base64 i
|
|
|
228
228
|
> Two submodules: `parse-skin.ts` (skin index dedupe via UUIDv5 + IBM decoding + jointPath derivation) + `parse-animation.ts` (LINEAR/STEP samplers, CUBICSPLINE/morph fail-fast). Called inside `parseGltfWithBin` -> `toAssetPack` which extends sub-asset output from 3 kinds (mesh/material/scene) to 6 (+ skeleton + skin + animation-clip).
|
|
229
229
|
|
|
230
230
|
- **Limitations**: CUBICSPLINE interpolation not supported; morph weight animation not supported; jointPath resolution uses leaf-name first-match (same-name sibling is warn-only).
|
|
231
|
-
- **Animated skin bounds** are producer-authored facts. The primary source is `skins[].extras.forgeax.conservativeAnimatedBounds`; `importSettings.conservativeAnimatedBounds[sourceIndex]` is accepted only when source extras do not provide bounds. The importer publishes the six-float bounds on `SkeletonAsset`.
|
|
231
|
+
- **Animated skin bounds** are producer-authored facts. The primary source is `skins[].extras.forgeax.conservativeAnimatedBounds`; `importSettings.conservativeAnimatedBounds[sourceIndex]` is accepted only when source extras do not provide bounds. The importer publishes the six-float bounds on `SkeletonAsset`. When both sources are absent, the import producer derives a conservative enclosure from mesh influences, inverse bind matrices, node TRS, morph deltas, and every imported animation clip. The shared [animation enclosure contract](../animation/README.md#imported-animation-bounds) covers intervals between keys and convex clip blends. Incomplete or singular inputs keep bounds absent and retain the `cpu-deformation` lane. The raw parser remains a source IR; the importer publishes the derived metadata.
|
|
232
232
|
|
|
233
233
|
## 4-step runtime recipe (apps/hello/gltf, M5)
|
|
234
234
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"animated-bounds.d.ts","sourceRoot":"","sources":["../src/animated-bounds.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C,4FAA4F;AAC5F,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAqF9D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gltf-importer.d.ts","sourceRoot":"","sources":["../src/gltf-importer.ts"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EAEV,QAAQ,EAIR,QAAQ,EAMT,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"gltf-importer.d.ts","sourceRoot":"","sources":["../src/gltf-importer.ts"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EAEV,QAAQ,EAIR,QAAQ,EAMT,MAAM,uBAAuB,CAAC;AAoB/B,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,qBAAqB,CAAC;AAC1E,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAqB,MAAM,iBAAiB,CAAC;AAwUlF,iFAAiF;AACjF,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,OAAO,EACZ,kBAAkB,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/C,kBAAkB,GAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAa,GAC1D,SAAS,QAAQ,EAAE,CA2CrB;AA6oBD;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,CAAC,EAAE,8BAA8B,GAAG,QAAQ,CAMrF;AAED,8EAA8E;AAC9E,eAAO,MAAM,YAAY,EAAE,QAA+B,CAAC"}
|
package/dist/importer-entry.mjs
CHANGED
|
@@ -3,11 +3,124 @@ import { deriveDefaultLodScreenCoverages } from '@forgeax/engine-import';
|
|
|
3
3
|
import { packMeshBinV4 } from '@forgeax/engine-import/mesh-bin';
|
|
4
4
|
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
5
5
|
import { ImportError, readConservativeAnimatedBounds, toShared, err, ok, reconcileMeshMaterialSlotTopology, IMPORT_ERROR_HINTS, resolveMeshMaterialSlotDefaultGuid, createMaterialError, standardMaterialParameters, STANDARD_MATERIAL_PARAM_SCHEMA, STANDARD_PHYSICAL_PARAMETER_NAMES, STANDARD_TRANSMISSION_PARAMETER_NAMES, STANDARD_LAYER_PARAMETER_GROUPS, parseConservativeAnimatedBounds } from '@forgeax/engine-types';
|
|
6
|
+
import { deriveConservativeAnimatedBounds } from '@forgeax/engine-animation/animated-bounds';
|
|
6
7
|
import { computeTangentVec4, packInterleavedVertexAttributes } from '@forgeax/engine-geometry';
|
|
7
8
|
import { box3, mat4, vec3, quat } from '@forgeax/engine-math';
|
|
8
9
|
import { deriveAnimationTargetId } from '@forgeax/engine-animation/target-id';
|
|
9
10
|
|
|
10
11
|
// src/importer-entry.ts
|
|
12
|
+
|
|
13
|
+
// src/node-path.ts
|
|
14
|
+
function buildNodeParentMap(nodes) {
|
|
15
|
+
const parents = /* @__PURE__ */ new Map();
|
|
16
|
+
for (let index = 0; index < nodes.length; index++) {
|
|
17
|
+
for (const child of nodes[index]?.children ?? []) parents.set(child, index);
|
|
18
|
+
}
|
|
19
|
+
return parents;
|
|
20
|
+
}
|
|
21
|
+
function resolveNamedNodePath(nodes, parents, nodeIndex) {
|
|
22
|
+
const reversed = [];
|
|
23
|
+
const visited = /* @__PURE__ */ new Set();
|
|
24
|
+
let current = nodeIndex;
|
|
25
|
+
while (current !== void 0) {
|
|
26
|
+
if (visited.has(current)) {
|
|
27
|
+
return { ok: false, reason: "hierarchy-cycle", nodeIndex: current };
|
|
28
|
+
}
|
|
29
|
+
visited.add(current);
|
|
30
|
+
const name = nodes[current]?.name;
|
|
31
|
+
if (name === void 0 || name.length === 0) {
|
|
32
|
+
return { ok: false, reason: "name-missing", nodeIndex: current };
|
|
33
|
+
}
|
|
34
|
+
reversed.push(name);
|
|
35
|
+
current = parents.get(current);
|
|
36
|
+
}
|
|
37
|
+
return { ok: true, value: reversed.reverse() };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/animated-bounds.ts
|
|
41
|
+
function deriveGltfAnimatedBounds(doc) {
|
|
42
|
+
const parents = buildNodeParentMap(doc.nodes);
|
|
43
|
+
const paths = doc.nodes.map((_, index) => {
|
|
44
|
+
const path = resolveNamedNodePath(doc.nodes, parents, index);
|
|
45
|
+
return path.ok ? path.value.join("/") : void 0;
|
|
46
|
+
});
|
|
47
|
+
const nodes = doc.nodes.map((node, index) => ({
|
|
48
|
+
...node.transform,
|
|
49
|
+
parent: parents.get(index) ?? null
|
|
50
|
+
}));
|
|
51
|
+
const channels = doc.animationClips.flatMap(
|
|
52
|
+
(clip) => clip.channels.flatMap(
|
|
53
|
+
(channel) => channel.property === "weights" ? [] : [
|
|
54
|
+
{
|
|
55
|
+
node: channel.targetNodeIndex,
|
|
56
|
+
property: channel.property,
|
|
57
|
+
values: channel.sampler.output,
|
|
58
|
+
interpolation: channel.sampler.interpolation
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
)
|
|
62
|
+
);
|
|
63
|
+
const primitiveStarts = [];
|
|
64
|
+
let offset = 0;
|
|
65
|
+
for (const count of doc.meshPrimitiveCount?.values() ?? doc.meshes.map(() => 1)) {
|
|
66
|
+
primitiveStarts.push(offset);
|
|
67
|
+
offset += count;
|
|
68
|
+
}
|
|
69
|
+
const skeletons = doc.skeletons.map((skeleton, skinIndex) => {
|
|
70
|
+
if (skeleton.bounds !== void 0) return skeleton;
|
|
71
|
+
const meshes = [];
|
|
72
|
+
for (let index = 0; index < doc.nodes.length; index++) {
|
|
73
|
+
const node = doc.nodes[index];
|
|
74
|
+
if (node === void 0) return skeleton;
|
|
75
|
+
if (node.skinIndex !== skinIndex || node.meshIndex === null) continue;
|
|
76
|
+
const start = primitiveStarts[node.meshIndex];
|
|
77
|
+
if (start === void 0) return skeleton;
|
|
78
|
+
for (const mesh of doc.meshes.slice(
|
|
79
|
+
start,
|
|
80
|
+
start + (doc.meshPrimitiveCount?.get(node.meshIndex) ?? 1)
|
|
81
|
+
)) {
|
|
82
|
+
if (mesh.joints0 === void 0 || mesh.weights0 === void 0) return skeleton;
|
|
83
|
+
let maxMorphWeight = Math.max(
|
|
84
|
+
0,
|
|
85
|
+
...Array.from(node.morphWeights ?? mesh.morphWeights ?? [], Math.abs)
|
|
86
|
+
);
|
|
87
|
+
for (const clip of doc.animationClips)
|
|
88
|
+
for (const channel of clip.channels)
|
|
89
|
+
if (channel.targetNodeIndex === index && channel.property === "weights")
|
|
90
|
+
for (const weight of channel.sampler.output)
|
|
91
|
+
maxMorphWeight = Math.max(maxMorphWeight, Math.abs(weight));
|
|
92
|
+
const morphExtent = Float32Array.from(
|
|
93
|
+
mesh.positions,
|
|
94
|
+
(_, component) => (mesh.morphTargets ?? []).reduce(
|
|
95
|
+
(sum, target) => sum + Math.abs(target.position?.[component] ?? 0) * maxMorphWeight,
|
|
96
|
+
0
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
meshes.push({
|
|
100
|
+
node: index,
|
|
101
|
+
positions: mesh.positions,
|
|
102
|
+
joints: mesh.joints0,
|
|
103
|
+
weights: mesh.weights0,
|
|
104
|
+
morphExtent
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const jointNodes = skeleton.jointPaths.map((path) => paths.indexOf(path));
|
|
109
|
+
if (jointNodes.some(
|
|
110
|
+
(node, index) => node < 0 || paths.lastIndexOf(skeleton.jointPaths[index]) !== node
|
|
111
|
+
))
|
|
112
|
+
return skeleton;
|
|
113
|
+
const bounds = deriveConservativeAnimatedBounds({
|
|
114
|
+
nodes,
|
|
115
|
+
channels,
|
|
116
|
+
jointNodes,
|
|
117
|
+
inverseBindMatrices: skeleton.inverseBindMatrices,
|
|
118
|
+
meshes
|
|
119
|
+
});
|
|
120
|
+
return bounds === void 0 ? skeleton : { ...skeleton, bounds };
|
|
121
|
+
});
|
|
122
|
+
return { ...doc, skeletons };
|
|
123
|
+
}
|
|
11
124
|
var GLTF_MESHOPT_MODES = ["ATTRIBUTES", "TRIANGLES", "INDICES"];
|
|
12
125
|
var GLTF_MESHOPT_FILTERS = ["NONE", "OCTAHEDRAL", "QUATERNION", "EXPONENTIAL"];
|
|
13
126
|
var gltfErrorPolicy = {
|
|
@@ -2102,35 +2215,6 @@ async function projectMeshoptBufferViews(inputViews, inputBuffers, extensionsReq
|
|
|
2102
2215
|
}
|
|
2103
2216
|
return ok({ bufferViews, buffers, decodedCount });
|
|
2104
2217
|
}
|
|
2105
|
-
|
|
2106
|
-
// src/node-path.ts
|
|
2107
|
-
function buildNodeParentMap(nodes) {
|
|
2108
|
-
const parents = /* @__PURE__ */ new Map();
|
|
2109
|
-
for (let index = 0; index < nodes.length; index++) {
|
|
2110
|
-
for (const child of nodes[index]?.children ?? []) parents.set(child, index);
|
|
2111
|
-
}
|
|
2112
|
-
return parents;
|
|
2113
|
-
}
|
|
2114
|
-
function resolveNamedNodePath(nodes, parents, nodeIndex) {
|
|
2115
|
-
const reversed = [];
|
|
2116
|
-
const visited = /* @__PURE__ */ new Set();
|
|
2117
|
-
let current = nodeIndex;
|
|
2118
|
-
while (current !== void 0) {
|
|
2119
|
-
if (visited.has(current)) {
|
|
2120
|
-
return { ok: false, reason: "hierarchy-cycle", nodeIndex: current };
|
|
2121
|
-
}
|
|
2122
|
-
visited.add(current);
|
|
2123
|
-
const name = nodes[current]?.name;
|
|
2124
|
-
if (name === void 0 || name.length === 0) {
|
|
2125
|
-
return { ok: false, reason: "name-missing", nodeIndex: current };
|
|
2126
|
-
}
|
|
2127
|
-
reversed.push(name);
|
|
2128
|
-
current = parents.get(current);
|
|
2129
|
-
}
|
|
2130
|
-
return { ok: true, value: reversed.reverse() };
|
|
2131
|
-
}
|
|
2132
|
-
|
|
2133
|
-
// src/parse-animation.ts
|
|
2134
2218
|
var ANIMATION_ACCESSOR_TYPES = ["SCALAR", "VEC2", "VEC3", "VEC4"];
|
|
2135
2219
|
function parseAnimation(animationsJson, nodesJson, accessors, bufferViews, buffers) {
|
|
2136
2220
|
if (animationsJson === void 0 || animationsJson.length === 0) {
|
|
@@ -3564,7 +3648,7 @@ async function importGltf(ctx, meshopt) {
|
|
|
3564
3648
|
}
|
|
3565
3649
|
const parsed = await parseDoc(ctx.source, read.value, ctx, meshopt);
|
|
3566
3650
|
if (!parsed.ok) return parsed;
|
|
3567
|
-
const doc = applyImportSettingsBounds(parsed.value, ctx.importSettings);
|
|
3651
|
+
const doc = deriveGltfAnimatedBounds(applyImportSettingsBounds(parsed.value, ctx.importSettings));
|
|
3568
3652
|
const maps = buildHandleMaps(ctx.subAssets, doc);
|
|
3569
3653
|
const imageColorSpaces = deriveTextureColorSpace({
|
|
3570
3654
|
imageCount: (doc.images ?? []).length,
|