@forgeax/engine-gltf 0.1.29 → 0.1.31

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 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`. If both sources are absent, bounds remain absent and Render keeps the skin on the `cpu-deformation` lane; no BindPose AABB is derived as an animation-bound fallback.
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,4 @@
1
+ import type { GltfDoc } from './parse-gltf';
2
+ /** Source extras and sidecar bounds have already been applied before this producer runs. */
3
+ export declare function deriveGltfAnimatedBounds(doc: GltfDoc): GltfDoc;
4
+ //# sourceMappingURL=animated-bounds.d.ts.map
@@ -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;AAmB/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"}
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"}
@@ -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,