@expofp/renderer 3.45.7 → 3.45.9

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.
@@ -32,6 +32,12 @@ export interface ModelAsset {
32
32
  * the merge happen at all. Then it is filed under its material, sub-keyed by {@link mergeSignature},
33
33
  * because `mergeGeometries` requires its inputs to agree about their attributes.
34
34
  *
35
+ * Two shapes compressors emit are unpacked on the way in. An `EXT_mesh_gpu_instancing` node (an
36
+ * `InstancedMesh`, from gltfpack `-mi`) is baked once per instance, since the def is our only
37
+ * instanced unit; its per-instance colors and custom attributes are dropped with a warning. Quantized
38
+ * attributes (`KHR_mesh_quantization`, from gltfpack) are widened to floats first — see
39
+ * {@link toFloatGeometry}.
40
+ *
35
41
  * **Nothing here knows about the plan.** The geometry stays in the model's own space, glTF's y-up,
36
42
  * and the mapping onto the plan is a transform the *mesh* carries — see `ModelContainer`. That is
37
43
  * not a tidiness argument: a mapping baked into vertices is one three cannot see when it decides
@@ -1,4 +1,4 @@
1
- import { Box3, Mesh } from 'three';
1
+ import { Box3, BufferAttribute, InstancedMesh, Matrix4, Mesh, } from 'three';
2
2
  import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
3
3
  import { createLogger } from '../../utils/logging';
4
4
  const logger = createLogger('model');
@@ -10,6 +10,12 @@ const logger = createLogger('model');
10
10
  * the merge happen at all. Then it is filed under its material, sub-keyed by {@link mergeSignature},
11
11
  * because `mergeGeometries` requires its inputs to agree about their attributes.
12
12
  *
13
+ * Two shapes compressors emit are unpacked on the way in. An `EXT_mesh_gpu_instancing` node (an
14
+ * `InstancedMesh`, from gltfpack `-mi`) is baked once per instance, since the def is our only
15
+ * instanced unit; its per-instance colors and custom attributes are dropped with a warning. Quantized
16
+ * attributes (`KHR_mesh_quantization`, from gltfpack) are widened to floats first — see
17
+ * {@link toFloatGeometry}.
18
+ *
13
19
  * **Nothing here knows about the plan.** The geometry stays in the model's own space, glTF's y-up,
14
20
  * and the mapping onto the plan is a transform the *mesh* carries — see `ModelContainer`. That is
15
21
  * not a tidiness argument: a mapping baked into vertices is one three cannot see when it decides
@@ -38,16 +44,36 @@ export function bakeGltf(scene, url) {
38
44
  const material = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
39
45
  if (!material)
40
46
  return;
41
- const baked = mesh.geometry.clone();
42
- baked.applyMatrix4(mesh.matrixWorld);
43
- baked.computeBoundingBox();
44
- if (baked.boundingBox)
45
- extents.union(baked.boundingBox);
46
- const signature = mergeSignature(baked);
47
- const id = `${material.uuid}|${signature}`;
48
- const bucket = buckets.get(id) ?? { material, signature, geometries: [] };
49
- bucket.geometries.push(baked);
50
- buckets.set(id, bucket);
47
+ const source = toFloatGeometry(mesh.geometry);
48
+ // Per-instance attributes three parked on the shared geometry; each baked copy sheds them.
49
+ const dropped = mesh instanceof InstancedMesh
50
+ ? Object.keys(source.attributes).filter((name) => name.startsWith('_'))
51
+ : [];
52
+ const bake = (matrix) => {
53
+ const baked = source.clone();
54
+ for (const name of dropped)
55
+ baked.deleteAttribute(name);
56
+ baked.applyMatrix4(matrix);
57
+ baked.computeBoundingBox();
58
+ if (baked.boundingBox)
59
+ extents.union(baked.boundingBox);
60
+ const signature = mergeSignature(baked);
61
+ const id = `${material.uuid}|${signature}`;
62
+ const bucket = buckets.get(id) ?? { material, signature, geometries: [] };
63
+ bucket.geometries.push(baked);
64
+ buckets.set(id, bucket);
65
+ };
66
+ if (!(mesh instanceof InstancedMesh)) {
67
+ bake(mesh.matrixWorld);
68
+ return;
69
+ }
70
+ const ignored = mesh.instanceColor ? [...dropped, '_COLOR_0'] : dropped;
71
+ if (ignored.length > 0) {
72
+ logger.warn('model %s: ignored per-instance attributes %s on mesh "%s"', url, ignored.join(', '), mesh.name);
73
+ }
74
+ for (let i = 0; i < mesh.count; i++) {
75
+ bake(_matrix.multiplyMatrices(mesh.matrixWorld, mesh.getMatrixAt(i, _instanceMatrix)));
76
+ }
51
77
  });
52
78
  const parts = [];
53
79
  for (const [id, { material, signature, geometries }] of buckets) {
@@ -75,3 +101,44 @@ function mergeSignature(geometry) {
75
101
  const indexed = geometry.getIndex() ? 'indexed' : 'non-indexed';
76
102
  return `${indexed} [${Object.keys(geometry.attributes).sort().join(', ')}]`;
77
103
  }
104
+ /**
105
+ * A copy of the geometry whose every attribute is a plain, non-normalized `Float32Array`.
106
+ *
107
+ * `KHR_mesh_quantization` stores attributes as (possibly normalized) integers, with the scale moved
108
+ * into the node transform. `applyMatrix4` writes its result back into the same array, so baking a
109
+ * world transform into an `Int16Array` rounds every coordinate to a whole unit. Widening first also
110
+ * gives `mergeGeometries` matching array types, which it requires but {@link mergeSignature} does not
111
+ * check. A geometry that is already all floats is returned as is, uncopied: the caller clones it.
112
+ * @param geometry - The geometry to widen; left untouched.
113
+ * @returns The geometry itself, or a widened copy.
114
+ */
115
+ function toFloatGeometry(geometry) {
116
+ const narrow = Object.entries(geometry.attributes).filter(([, a]) => !isPlainFloat(a));
117
+ if (narrow.length === 0)
118
+ return geometry;
119
+ const widened = geometry.clone();
120
+ for (const [name, attribute] of narrow) {
121
+ const { count, itemSize } = attribute;
122
+ const array = new Float32Array(count * itemSize);
123
+ for (let i = 0; i < count; i++) {
124
+ for (let component = 0; component < itemSize; component++) {
125
+ // `getComponent` denormalizes, and reads through an interleaved buffer's stride.
126
+ array[i * itemSize + component] = attribute.getComponent(i, component);
127
+ }
128
+ }
129
+ widened.setAttribute(name, new BufferAttribute(array, itemSize));
130
+ }
131
+ return widened;
132
+ }
133
+ /**
134
+ * Whether an attribute is already what {@link toFloatGeometry} produces.
135
+ * @param attribute - The attribute to test.
136
+ * @returns `true` for a plain, non-normalized `Float32Array` attribute.
137
+ */
138
+ function isPlainFloat(attribute) {
139
+ return (attribute instanceof BufferAttribute &&
140
+ attribute.array instanceof Float32Array &&
141
+ !attribute.normalized);
142
+ }
143
+ const _matrix = new Matrix4();
144
+ const _instanceMatrix = new Matrix4();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expofp/renderer",
3
- "version": "3.45.7",
3
+ "version": "3.45.9",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/expofp/efp-app.git",
@@ -42,7 +42,7 @@
42
42
  "troika-three-text": "^0.52.5",
43
43
  "tslib": "^2.3.0",
44
44
  "webgl-memory": "^1.1.3",
45
- "@expofp/geometry": "3.45.7"
45
+ "@expofp/geometry": "3.45.9"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@react-three/test-renderer": "^9.1.0",