@needle-tools/gltf-build-pipeline 2.13.0 → 2.13.1

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.
Files changed (103) hide show
  1. package/CHANGELOG.md +520 -517
  2. package/README.md +38 -38
  3. package/dist/cache/cache.d.ts +55 -55
  4. package/dist/cache/cache.js +326 -326
  5. package/dist/cache/index.d.ts +1 -1
  6. package/dist/cache/index.js +1 -1
  7. package/dist/cli/index.d.ts +2 -2
  8. package/dist/cli/index.js +198 -198
  9. package/dist/config/index.d.ts +53 -53
  10. package/dist/config/index.js +152 -152
  11. package/dist/constants.d.ts +7 -7
  12. package/dist/constants.js +7 -7
  13. package/dist/extensions/NEEDLE_compression_texture/NEEDLE_compression_texture.d.ts +24 -24
  14. package/dist/extensions/NEEDLE_compression_texture/NEEDLE_compression_texture.js +54 -54
  15. package/dist/extensions/NEEDLE_compression_texture/index.d.ts +1 -1
  16. package/dist/extensions/NEEDLE_compression_texture/index.js +1 -1
  17. package/dist/extensions/NEEDLE_mesh_compression/NEEDLE_mesh_compression.d.ts +27 -27
  18. package/dist/extensions/NEEDLE_mesh_compression/NEEDLE_mesh_compression.js +88 -88
  19. package/dist/extensions/NEEDLE_mesh_compression/index.d.ts +1 -1
  20. package/dist/extensions/NEEDLE_mesh_compression/index.js +1 -1
  21. package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.d.ts +27 -27
  22. package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.js +980 -980
  23. package/dist/extensions/NEEDLE_opaque/index.d.ts +3 -3
  24. package/dist/extensions/NEEDLE_opaque/index.js +30 -30
  25. package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.d.ts +48 -48
  26. package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.js +69 -69
  27. package/dist/extensions/NEEDLE_progressive/index.d.ts +1 -1
  28. package/dist/extensions/NEEDLE_progressive/index.js +1 -1
  29. package/dist/extensions/NEEDLE_progressive_mesh_settings/index.d.ts +23 -23
  30. package/dist/extensions/NEEDLE_progressive_mesh_settings/index.js +41 -41
  31. package/dist/extensions/NEEDLE_progressive_texture_settings/index.d.ts +25 -25
  32. package/dist/extensions/NEEDLE_progressive_texture_settings/index.js +38 -38
  33. package/dist/extensions/index.d.ts +7 -7
  34. package/dist/extensions/index.js +17 -17
  35. package/dist/extensions/utils.d.ts +6 -6
  36. package/dist/extensions/utils.js +72 -72
  37. package/dist/functions/weld.3.d.ts +76 -76
  38. package/dist/functions/weld.3.js +414 -414
  39. package/dist/index.d.ts +8 -8
  40. package/dist/index.js +8 -8
  41. package/dist/scripts/index.d.ts +2 -2
  42. package/dist/scripts/index.js +2 -2
  43. package/dist/scripts/pack-gltf.d.ts +15 -15
  44. package/dist/scripts/pack-gltf.js +135 -133
  45. package/dist/scripts/stats.d.ts +2 -2
  46. package/dist/scripts/stats.js +19 -19
  47. package/dist/transforms/index.d.ts +9 -9
  48. package/dist/transforms/index.js +9 -9
  49. package/dist/transforms/needle_animation_transform.d.ts +2 -2
  50. package/dist/transforms/needle_animation_transform.js +5 -5
  51. package/dist/transforms/needle_asset.d.ts +3 -3
  52. package/dist/transforms/needle_asset.js +50 -50
  53. package/dist/transforms/needle_common.d.ts +17 -17
  54. package/dist/transforms/needle_common.js +1 -1
  55. package/dist/transforms/needle_mesh_transform.d.ts +15 -15
  56. package/dist/transforms/needle_mesh_transform.js +223 -223
  57. package/dist/transforms/needle_progressive.d.ts +45 -45
  58. package/dist/transforms/needle_progressive.js +1043 -1043
  59. package/dist/transforms/needle_texture_transform.d.ts +13 -13
  60. package/dist/transforms/needle_texture_transform.js +257 -257
  61. package/dist/transforms/needle_toktx.d.ts +26 -26
  62. package/dist/transforms/needle_toktx.js +243 -243
  63. package/dist/transforms/needle_webp.d.ts +15 -15
  64. package/dist/transforms/needle_webp.js +53 -53
  65. package/dist/transforms/toktx.d.ts +52 -52
  66. package/dist/transforms/toktx.js +221 -221
  67. package/dist/transforms/toktx_types.d.ts +78 -78
  68. package/dist/transforms/toktx_types.js +62 -62
  69. package/dist/transforms/util.d.ts +21 -21
  70. package/dist/transforms/util.js +90 -90
  71. package/dist/utils/compression_utils.d.ts +2 -2
  72. package/dist/utils/compression_utils.js +6 -6
  73. package/dist/utils/fileutils.d.ts +19 -19
  74. package/dist/utils/fileutils.js +177 -177
  75. package/dist/utils/guid.d.ts +1 -1
  76. package/dist/utils/guid.js +5 -5
  77. package/dist/utils/index.d.ts +10 -10
  78. package/dist/utils/index.js +10 -10
  79. package/dist/utils/merge.d.ts +12 -12
  80. package/dist/utils/merge.js +31 -31
  81. package/dist/utils/mesh.d.ts +13 -13
  82. package/dist/utils/mesh.js +124 -124
  83. package/dist/utils/nodeio.d.ts +6 -6
  84. package/dist/utils/nodeio.js +86 -86
  85. package/dist/utils/stats.d.ts +70 -70
  86. package/dist/utils/stats.js +258 -258
  87. package/dist/utils/texture.d.ts +5 -5
  88. package/dist/utils/texture.js +14 -14
  89. package/dist/utils/validate.d.ts +5 -5
  90. package/dist/utils/validate.js +17 -17
  91. package/dist/utils/version.d.ts +1 -1
  92. package/dist/utils/version.gen.d.ts +1 -1
  93. package/dist/utils/version.gen.js +1 -1
  94. package/dist/utils/version.js +4 -4
  95. package/package.json +79 -79
  96. package/scripts/clear-caches.mjs +3 -3
  97. package/scripts/defaults.mjs +11 -11
  98. package/scripts/fileutils.mjs +5 -5
  99. package/scripts/index.mjs +41 -41
  100. package/scripts/limit-caches.mjs +3 -3
  101. package/scripts/make-progressive.mjs +16 -16
  102. package/scripts/pack-gltf.mjs +106 -106
  103. package/tsconfig.json +19 -19
@@ -1,414 +1,414 @@
1
- /*
2
-
3
-
4
- COPY OF weld() of gltf-transform 3.10.1
5
- 2aec33bf432a354275fb5066d1ddf5840f2d8bb4
6
-
7
-
8
-
9
- */
10
- import { Accessor, Document, Primitive, PropertyType, } from '@gltf-transform/core';
11
- import { createTransform, prune, dedup } from '@gltf-transform/functions';
12
- /** @hidden */
13
- export function createIndices(count, maxIndex = count) {
14
- const array = maxIndex <= 65534 ? new Uint16Array(count) : new Uint32Array(count);
15
- for (let i = 0; i < array.length; i++)
16
- array[i] = i;
17
- return array;
18
- }
19
- export function formatLong(x) {
20
- return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
21
- }
22
- export function formatDelta(a, b, decimals = 2) {
23
- const prefix = a > b ? '–' : '+';
24
- const suffix = '%';
25
- return prefix + ((Math.abs(a - b) / a) * 100).toFixed(decimals) + suffix;
26
- }
27
- export function formatDeltaOp(a, b) {
28
- return `${formatLong(a)} → ${formatLong(b)} (${formatDelta(a, b)})`;
29
- }
30
- export function cleanPrimitive(prim) {
31
- const indices = prim.getIndices();
32
- if (!indices)
33
- return;
34
- const tmpIndicesArray = [];
35
- let maxIndex = -Infinity;
36
- for (let i = 0, il = indices.getCount(); i < il; i += 3) {
37
- const a = indices.getScalar(i);
38
- const b = indices.getScalar(i + 1);
39
- const c = indices.getScalar(i + 2);
40
- if (a === b || a === c || b === c)
41
- continue;
42
- // @ts-ignore
43
- tmpIndicesArray.push(a, b, c);
44
- maxIndex = Math.max(maxIndex, a, b, c);
45
- }
46
- const dstIndicesArray = createIndices(tmpIndicesArray.length, maxIndex);
47
- dstIndicesArray.set(tmpIndicesArray);
48
- indices.setArray(dstIndicesArray);
49
- }
50
- // DEVELOPER NOTES: Ideally a weld() implementation should be fast, robust,
51
- // and tunable. The writeup below tracks my attempts to solve for these
52
- // constraints.
53
- //
54
- // (Approach #1) Follow the mergeVertices() implementation of three.js,
55
- // hashing vertices with a string concatenation of all vertex attributes.
56
- // The approach does not allow per-attribute tolerance in local units.
57
- //
58
- // (Approach #2) Sort points along the X axis, then make cheaper
59
- // searches up/down the sorted list for merge candidates. While this allows
60
- // simpler comparison based on specified tolerance, it's much slower, even
61
- // for cases where choice of the X vs. Y or Z axes is reasonable.
62
- //
63
- // (Approach #3) Attempted a Delaunay triangulation in three dimensions,
64
- // expecting it would be an n * log(n) algorithm, but the only implementation
65
- // I found (with delaunay-triangulate) appeared to be much slower than that,
66
- // and was notably slower than the sort-based approach, just building the
67
- // Delaunay triangulation alone.
68
- //
69
- // (Approach #4) Hybrid of (1) and (2), assigning vertices to a spatial
70
- // grid, then searching the local neighborhood (27 cells) for weld candidates.
71
- //
72
- // RESULTS: For the "Lovecraftian" sample model, after joining, a primitive
73
- // with 873,000 vertices can be welded down to 230,000 vertices. Results:
74
- // - (1) Not tested, but prior results suggest not robust enough.
75
- // - (2) 30 seconds
76
- // - (3) 660 seconds
77
- // - (4) 5 seconds exhaustive, 1.5s non-exhaustive
78
- const NAME = 'weld';
79
- const Tolerance = {
80
- DEFAULT: 0.0001,
81
- TEXCOORD: 0.0001,
82
- COLOR: 0.01,
83
- NORMAL: 0.05,
84
- JOINTS: 0.0,
85
- WEIGHTS: 0.01, // [0, ∞]
86
- };
87
- export const WELD_DEFAULTS = {
88
- tolerance: Tolerance.DEFAULT,
89
- toleranceNormal: Tolerance.NORMAL,
90
- overwrite: true,
91
- exhaustive: false, // donmccurdy/glTF-Transform#886
92
- };
93
- /**
94
- * Index {@link Primitive Primitives} and (optionally) merge similar vertices. When merged
95
- * and indexed, data is shared more efficiently between vertices. File size can
96
- * be reduced, and the GPU can sometimes use the vertex cache more efficiently.
97
- *
98
- * When welding, the 'tolerance' threshold determines which vertices qualify for
99
- * welding based on distance between the vertices as a fraction of the primitive's
100
- * bounding box (AABB). For example, tolerance=0.01 welds vertices within +/-1%
101
- * of the AABB's longest dimension. Other vertex attributes are also compared
102
- * during welding, with attribute-specific thresholds. For `tolerance=0`, geometry
103
- * is indexed in place, without merging.
104
- *
105
- * To preserve visual appearance consistently, use low `toleranceNormal` thresholds
106
- * around 0.1 (±3º). To pre-processing a scene before simplification or LOD creation,
107
- * use higher thresholds around 0.5 (±30º).
108
- *
109
- * Example:
110
- *
111
- * ```javascript
112
- * import { weld } from '@gltf-transform/functions';
113
- *
114
- * await document.transform(
115
- * weld({ tolerance: 0.001, toleranceNormal: 0.5 })
116
- * );
117
- * ```
118
- *
119
- * @category Transforms
120
- */
121
- export function weld(_options = WELD_DEFAULTS) {
122
- const options = expandWeldOptions(_options);
123
- return createTransform(NAME, async (doc) => {
124
- const logger = doc.getLogger();
125
- for (const mesh of doc.getRoot().listMeshes()) {
126
- for (const prim of mesh.listPrimitives()) {
127
- weldPrimitive(doc, prim, options);
128
- if (isPrimEmpty(prim))
129
- prim.dispose();
130
- }
131
- if (mesh.listPrimitives().length === 0)
132
- mesh.dispose();
133
- }
134
- if (options.tolerance > 0) {
135
- // If tolerance is greater than 0, welding may remove a mesh, so we prune
136
- await doc.transform(prune({
137
- propertyTypes: [PropertyType.ACCESSOR, PropertyType.NODE],
138
- keepAttributes: true,
139
- keepIndices: true,
140
- keepLeaves: false,
141
- }));
142
- }
143
- await doc.transform(dedup({ propertyTypes: [PropertyType.ACCESSOR] }));
144
- logger.debug(`${NAME}: Complete.`);
145
- });
146
- }
147
- /**
148
- * Index a {@link Primitive} and (optionally) weld similar vertices. When merged
149
- * and indexed, data is shared more efficiently between vertices. File size can
150
- * be reduced, and the GPU can sometimes use the vertex cache more efficiently.
151
- *
152
- * When welding, the 'tolerance' threshold determines which vertices qualify for
153
- * welding based on distance between the vertices as a fraction of the primitive's
154
- * bounding box (AABB). For example, tolerance=0.01 welds vertices within +/-1%
155
- * of the AABB's longest dimension. Other vertex attributes are also compared
156
- * during welding, with attribute-specific thresholds. For tolerance=0, geometry
157
- * is indexed in place, without merging.
158
- *
159
- * Example:
160
- *
161
- * ```javascript
162
- * import { weldPrimitive } from '@gltf-transform/functions';
163
- *
164
- * const mesh = document.getRoot().listMeshes()
165
- * .find((mesh) => mesh.getName() === 'Gizmo');
166
- *
167
- * for (const prim of mesh.listPrimitives()) {
168
- * weldPrimitive(prim, {tolerance: 0.0001});
169
- * }
170
- * ```
171
- *
172
- * @privateRemarks TODO(v4): Remove the "Document" parameter.
173
- */
174
- export function weldPrimitive(a, b = WELD_DEFAULTS, c = WELD_DEFAULTS) {
175
- let _document;
176
- let _prim;
177
- let _options;
178
- if (a instanceof Primitive) {
179
- const graph = a.getGraph();
180
- _document = Document.fromGraph(graph);
181
- _prim = a;
182
- _options = expandWeldOptions(b);
183
- }
184
- else {
185
- _document = a;
186
- _prim = b;
187
- _options = expandWeldOptions(c);
188
- }
189
- if (_prim.getIndices() && !_options.overwrite)
190
- return;
191
- if (_prim.getMode() === Primitive.Mode.POINTS)
192
- return;
193
- if (_options.tolerance === 0) {
194
- _indexPrimitive(_document, _prim);
195
- }
196
- else {
197
- _weldPrimitive(_document, _prim, _options);
198
- }
199
- }
200
- /** @internal Adds indices, if missing. Does not merge vertices. */
201
- function _indexPrimitive(doc, prim) {
202
- // No need to overwrite here, even if options.overwrite=true.
203
- if (prim.getIndices())
204
- return;
205
- const attr = prim.listAttributes()[0];
206
- const numVertices = attr.getCount();
207
- const buffer = attr.getBuffer();
208
- const indices = doc
209
- .createAccessor()
210
- .setBuffer(buffer)
211
- .setType(Accessor.Type.SCALAR)
212
- .setArray(createIndices(numVertices));
213
- prim.setIndices(indices);
214
- }
215
- /** @internal Weld and merge, combining vertices that are similar on all vertex attributes. */
216
- function _weldPrimitive(doc, prim, options) {
217
- const logger = doc.getLogger();
218
- const srcPosition = prim.getAttribute('POSITION');
219
- const srcIndices = prim.getIndices() || doc.createAccessor().setArray(createIndices(srcPosition.getCount()));
220
- const uniqueIndices = new Uint32Array(new Set(srcIndices.getArray())).sort();
221
- // (1) Compute per-attribute tolerance and spatial grid for vertices.
222
- const attributeTolerance = {};
223
- for (const semantic of prim.listSemantics()) {
224
- const attribute = prim.getAttribute(semantic);
225
- attributeTolerance[semantic] = getAttributeTolerance(semantic, attribute, options);
226
- }
227
- logger.debug(`${NAME}: Tolerance thresholds: ${formatKV(attributeTolerance)}`);
228
- // (2) Compare and identify vertices to weld.
229
- const posA = [0, 0, 0];
230
- const posB = [0, 0, 0];
231
- const grid = {};
232
- const cellSize = attributeTolerance.POSITION;
233
- for (let i = 0; i < uniqueIndices.length; i++) {
234
- srcPosition.getElement(uniqueIndices[i], posA);
235
- const key = getGridKey(posA, cellSize);
236
- grid[key] = grid[key] || [];
237
- grid[key].push(uniqueIndices[i]);
238
- }
239
- // (2) Compare and identify vertices to weld.
240
- const srcMaxIndex = uniqueIndices[uniqueIndices.length - 1];
241
- const weldMap = createIndices(srcMaxIndex + 1); // oldIndex → oldCommonIndex
242
- const writeMap = new Array(uniqueIndices.length).fill(-1); // oldIndex → newIndex
243
- const srcVertexCount = srcPosition.getCount();
244
- let dstVertexCount = 0;
245
- for (let i = 0; i < uniqueIndices.length; i++) {
246
- const a = uniqueIndices[i];
247
- srcPosition.getElement(a, posA);
248
- const cellKeys = options.exhaustive ? getGridNeighborhoodKeys(posA, cellSize) : [getGridKey(posA, cellSize)];
249
- cells: for (const cellKey of cellKeys) {
250
- if (!grid[cellKey])
251
- continue cells; // May occur in exhaustive search.
252
- neighbors: for (const j of grid[cellKey]) {
253
- const b = weldMap[j];
254
- // Only weld to lower indices, preventing two-way match.
255
- if (a <= b)
256
- continue neighbors;
257
- srcPosition.getElement(b, posB);
258
- // Weld if base attributes and morph target attributes match.
259
- const isBaseMatch = prim.listSemantics().every((semantic) => {
260
- const attribute = prim.getAttribute(semantic);
261
- const tolerance = attributeTolerance[semantic];
262
- return compareAttributes(attribute, a, b, tolerance, semantic);
263
- });
264
- const isTargetMatch = prim.listTargets().every((target) => {
265
- return target.listSemantics().every((semantic) => {
266
- const attribute = target.getAttribute(semantic);
267
- const tolerance = attributeTolerance[semantic];
268
- return compareAttributes(attribute, a, b, tolerance, semantic);
269
- });
270
- });
271
- if (isBaseMatch && isTargetMatch) {
272
- weldMap[a] = b;
273
- break cells;
274
- }
275
- }
276
- }
277
- // Output the vertex if we didn't find a match, else record the index of the match. Because
278
- // we iterate vertices in ascending order, and only match to lower indices, we're
279
- // guaranteed the source vertex for a weld has already been marked for output.
280
- if (weldMap[a] === a) {
281
- writeMap[a] = dstVertexCount++;
282
- }
283
- else {
284
- writeMap[a] = writeMap[weldMap[a]];
285
- }
286
- }
287
- logger.debug(`${NAME}: ${formatDeltaOp(srcVertexCount, dstVertexCount)} vertices.`);
288
- // (3) Update indices.
289
- const dstIndicesCount = srcIndices.getCount(); // # primitives does not change.
290
- const dstIndicesArray = createIndices(dstIndicesCount, uniqueIndices.length);
291
- for (let i = 0; i < dstIndicesCount; i++) {
292
- dstIndicesArray[i] = writeMap[srcIndices.getScalar(i)];
293
- }
294
- prim.setIndices(srcIndices.clone().setArray(dstIndicesArray));
295
- if (srcIndices.listParents().length === 1)
296
- srcIndices.dispose();
297
- // (4) Update vertex attributes.
298
- for (const srcAttr of prim.listAttributes()) {
299
- swapAttributes(prim, srcAttr, writeMap, dstVertexCount);
300
- }
301
- for (const target of prim.listTargets()) {
302
- for (const srcAttr of target.listAttributes()) {
303
- swapAttributes(target, srcAttr, writeMap, dstVertexCount);
304
- }
305
- }
306
- // (5) Clean up degenerate triangles.
307
- cleanPrimitive(prim);
308
- }
309
- /** Creates a new TypedArray of the same type as an original, with a new length. */
310
- function createArrayOfType(array, length) {
311
- const ArrayCtor = array.constructor;
312
- return new ArrayCtor(length);
313
- }
314
- /** Replaces an {@link Attribute}, creating a new one with the given elements. */
315
- function swapAttributes(parent, srcAttr, reorder, dstCount) {
316
- const dstAttrArray = createArrayOfType(srcAttr.getArray(), dstCount * srcAttr.getElementSize());
317
- const dstAttr = srcAttr.clone().setArray(dstAttrArray);
318
- const done = new Uint8Array(dstCount);
319
- for (let i = 0, el = []; i < reorder.length; i++) {
320
- if (!done[reorder[i]]) {
321
- dstAttr.setElement(reorder[i], srcAttr.getElement(i, el));
322
- done[reorder[i]] = 1;
323
- }
324
- }
325
- parent.swap(srcAttr, dstAttr);
326
- // Clean up.
327
- if (srcAttr.listParents().length === 1)
328
- srcAttr.dispose();
329
- }
330
- const _a = [];
331
- const _b = [];
332
- /** Computes a per-attribute tolerance, based on domain and usage of the attribute. */
333
- function getAttributeTolerance(semantic, attribute, options) {
334
- // Attributes like NORMAL and COLOR_# do not vary in range like POSITION,
335
- // so do not apply the given tolerance factor to these attributes.
336
- if (semantic === 'NORMAL' || semantic === 'TANGENT')
337
- return options.toleranceNormal;
338
- if (semantic.startsWith('COLOR_'))
339
- return Tolerance.COLOR;
340
- if (semantic.startsWith('TEXCOORD_'))
341
- return Tolerance.TEXCOORD;
342
- if (semantic.startsWith('JOINTS_'))
343
- return Tolerance.JOINTS;
344
- if (semantic.startsWith('WEIGHTS_'))
345
- return Tolerance.WEIGHTS;
346
- _a.length = _b.length = 0;
347
- attribute.getMinNormalized(_a);
348
- attribute.getMaxNormalized(_b);
349
- const diff = _b.map((bi, i) => bi - _a[i]);
350
- const range = Math.max(...diff);
351
- return options.tolerance * range;
352
- }
353
- /** Compares two vertex attributes against a tolerance threshold. */
354
- function compareAttributes(attribute, a, b, tolerance, _semantic) {
355
- attribute.getElement(a, _a);
356
- attribute.getElement(b, _b);
357
- for (let i = 0, il = attribute.getElementSize(); i < il; i++) {
358
- if (Math.abs(_a[i] - _b[i]) > tolerance) {
359
- return false;
360
- }
361
- }
362
- return true;
363
- }
364
- function formatKV(kv) {
365
- return Object.entries(kv)
366
- .map(([k, v]) => `${k}=${v}`)
367
- .join(', ');
368
- }
369
- // Order to search nearer cells first.
370
- const CELL_OFFSETS = [0, -1, 1];
371
- function getGridNeighborhoodKeys(p, cellSize) {
372
- const keys = [];
373
- const _p = [0, 0, 0];
374
- for (const i of CELL_OFFSETS) {
375
- for (const j of CELL_OFFSETS) {
376
- for (const k of CELL_OFFSETS) {
377
- _p[0] = p[0] + i * cellSize;
378
- _p[1] = p[1] + j * cellSize;
379
- _p[2] = p[2] + k * cellSize;
380
- keys.push(getGridKey(_p, cellSize));
381
- }
382
- }
383
- }
384
- return keys;
385
- }
386
- function getGridKey(p, cellSize) {
387
- const cellX = Math.round(p[0] / cellSize);
388
- const cellY = Math.round(p[1] / cellSize);
389
- const cellZ = Math.round(p[2] / cellSize);
390
- return cellX + ':' + cellY + ':' + cellZ;
391
- }
392
- function expandWeldOptions(_options) {
393
- const options = { ...WELD_DEFAULTS, ..._options };
394
- if (options.tolerance < 0 || options.tolerance > 0.1) {
395
- throw new Error(`${NAME}: Requires 0 <= tolerance <= 0.1`);
396
- }
397
- if (options.toleranceNormal < 0 || options.toleranceNormal > Math.PI / 2) {
398
- throw new Error(`${NAME}: Requires 0 <= toleranceNormal <= ${(Math.PI / 2).toFixed(2)}`);
399
- }
400
- if (options.tolerance > 0) {
401
- options.tolerance = Math.max(options.tolerance, Number.EPSILON);
402
- options.toleranceNormal = Math.max(options.toleranceNormal, Number.EPSILON);
403
- }
404
- return options;
405
- }
406
- /**
407
- * For purposes of welding, we consider a primitive to be 'empty' or degenerate
408
- * if (1) it has an index, and (2) that index is empty. In some cases
409
- * (mode=POINTS) the index may be missing — this is outside the scope of welding.
410
- */
411
- function isPrimEmpty(prim) {
412
- const indices = prim.getIndices();
413
- return !!indices && indices.getCount() === 0;
414
- }
1
+ /*
2
+
3
+
4
+ COPY OF weld() of gltf-transform 3.10.1
5
+ 2aec33bf432a354275fb5066d1ddf5840f2d8bb4
6
+
7
+
8
+
9
+ */
10
+ import { Accessor, Document, Primitive, PropertyType, } from '@gltf-transform/core';
11
+ import { createTransform, prune, dedup } from '@gltf-transform/functions';
12
+ /** @hidden */
13
+ export function createIndices(count, maxIndex = count) {
14
+ const array = maxIndex <= 65534 ? new Uint16Array(count) : new Uint32Array(count);
15
+ for (let i = 0; i < array.length; i++)
16
+ array[i] = i;
17
+ return array;
18
+ }
19
+ export function formatLong(x) {
20
+ return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
21
+ }
22
+ export function formatDelta(a, b, decimals = 2) {
23
+ const prefix = a > b ? '–' : '+';
24
+ const suffix = '%';
25
+ return prefix + ((Math.abs(a - b) / a) * 100).toFixed(decimals) + suffix;
26
+ }
27
+ export function formatDeltaOp(a, b) {
28
+ return `${formatLong(a)} → ${formatLong(b)} (${formatDelta(a, b)})`;
29
+ }
30
+ export function cleanPrimitive(prim) {
31
+ const indices = prim.getIndices();
32
+ if (!indices)
33
+ return;
34
+ const tmpIndicesArray = [];
35
+ let maxIndex = -Infinity;
36
+ for (let i = 0, il = indices.getCount(); i < il; i += 3) {
37
+ const a = indices.getScalar(i);
38
+ const b = indices.getScalar(i + 1);
39
+ const c = indices.getScalar(i + 2);
40
+ if (a === b || a === c || b === c)
41
+ continue;
42
+ // @ts-ignore
43
+ tmpIndicesArray.push(a, b, c);
44
+ maxIndex = Math.max(maxIndex, a, b, c);
45
+ }
46
+ const dstIndicesArray = createIndices(tmpIndicesArray.length, maxIndex);
47
+ dstIndicesArray.set(tmpIndicesArray);
48
+ indices.setArray(dstIndicesArray);
49
+ }
50
+ // DEVELOPER NOTES: Ideally a weld() implementation should be fast, robust,
51
+ // and tunable. The writeup below tracks my attempts to solve for these
52
+ // constraints.
53
+ //
54
+ // (Approach #1) Follow the mergeVertices() implementation of three.js,
55
+ // hashing vertices with a string concatenation of all vertex attributes.
56
+ // The approach does not allow per-attribute tolerance in local units.
57
+ //
58
+ // (Approach #2) Sort points along the X axis, then make cheaper
59
+ // searches up/down the sorted list for merge candidates. While this allows
60
+ // simpler comparison based on specified tolerance, it's much slower, even
61
+ // for cases where choice of the X vs. Y or Z axes is reasonable.
62
+ //
63
+ // (Approach #3) Attempted a Delaunay triangulation in three dimensions,
64
+ // expecting it would be an n * log(n) algorithm, but the only implementation
65
+ // I found (with delaunay-triangulate) appeared to be much slower than that,
66
+ // and was notably slower than the sort-based approach, just building the
67
+ // Delaunay triangulation alone.
68
+ //
69
+ // (Approach #4) Hybrid of (1) and (2), assigning vertices to a spatial
70
+ // grid, then searching the local neighborhood (27 cells) for weld candidates.
71
+ //
72
+ // RESULTS: For the "Lovecraftian" sample model, after joining, a primitive
73
+ // with 873,000 vertices can be welded down to 230,000 vertices. Results:
74
+ // - (1) Not tested, but prior results suggest not robust enough.
75
+ // - (2) 30 seconds
76
+ // - (3) 660 seconds
77
+ // - (4) 5 seconds exhaustive, 1.5s non-exhaustive
78
+ const NAME = 'weld';
79
+ const Tolerance = {
80
+ DEFAULT: 0.0001,
81
+ TEXCOORD: 0.0001,
82
+ COLOR: 0.01,
83
+ NORMAL: 0.05,
84
+ JOINTS: 0.0,
85
+ WEIGHTS: 0.01, // [0, ∞]
86
+ };
87
+ export const WELD_DEFAULTS = {
88
+ tolerance: Tolerance.DEFAULT,
89
+ toleranceNormal: Tolerance.NORMAL,
90
+ overwrite: true,
91
+ exhaustive: false, // donmccurdy/glTF-Transform#886
92
+ };
93
+ /**
94
+ * Index {@link Primitive Primitives} and (optionally) merge similar vertices. When merged
95
+ * and indexed, data is shared more efficiently between vertices. File size can
96
+ * be reduced, and the GPU can sometimes use the vertex cache more efficiently.
97
+ *
98
+ * When welding, the 'tolerance' threshold determines which vertices qualify for
99
+ * welding based on distance between the vertices as a fraction of the primitive's
100
+ * bounding box (AABB). For example, tolerance=0.01 welds vertices within +/-1%
101
+ * of the AABB's longest dimension. Other vertex attributes are also compared
102
+ * during welding, with attribute-specific thresholds. For `tolerance=0`, geometry
103
+ * is indexed in place, without merging.
104
+ *
105
+ * To preserve visual appearance consistently, use low `toleranceNormal` thresholds
106
+ * around 0.1 (±3º). To pre-processing a scene before simplification or LOD creation,
107
+ * use higher thresholds around 0.5 (±30º).
108
+ *
109
+ * Example:
110
+ *
111
+ * ```javascript
112
+ * import { weld } from '@gltf-transform/functions';
113
+ *
114
+ * await document.transform(
115
+ * weld({ tolerance: 0.001, toleranceNormal: 0.5 })
116
+ * );
117
+ * ```
118
+ *
119
+ * @category Transforms
120
+ */
121
+ export function weld(_options = WELD_DEFAULTS) {
122
+ const options = expandWeldOptions(_options);
123
+ return createTransform(NAME, async (doc) => {
124
+ const logger = doc.getLogger();
125
+ for (const mesh of doc.getRoot().listMeshes()) {
126
+ for (const prim of mesh.listPrimitives()) {
127
+ weldPrimitive(doc, prim, options);
128
+ if (isPrimEmpty(prim))
129
+ prim.dispose();
130
+ }
131
+ if (mesh.listPrimitives().length === 0)
132
+ mesh.dispose();
133
+ }
134
+ if (options.tolerance > 0) {
135
+ // If tolerance is greater than 0, welding may remove a mesh, so we prune
136
+ await doc.transform(prune({
137
+ propertyTypes: [PropertyType.ACCESSOR, PropertyType.NODE],
138
+ keepAttributes: true,
139
+ keepIndices: true,
140
+ keepLeaves: false,
141
+ }));
142
+ }
143
+ await doc.transform(dedup({ propertyTypes: [PropertyType.ACCESSOR] }));
144
+ logger.debug(`${NAME}: Complete.`);
145
+ });
146
+ }
147
+ /**
148
+ * Index a {@link Primitive} and (optionally) weld similar vertices. When merged
149
+ * and indexed, data is shared more efficiently between vertices. File size can
150
+ * be reduced, and the GPU can sometimes use the vertex cache more efficiently.
151
+ *
152
+ * When welding, the 'tolerance' threshold determines which vertices qualify for
153
+ * welding based on distance between the vertices as a fraction of the primitive's
154
+ * bounding box (AABB). For example, tolerance=0.01 welds vertices within +/-1%
155
+ * of the AABB's longest dimension. Other vertex attributes are also compared
156
+ * during welding, with attribute-specific thresholds. For tolerance=0, geometry
157
+ * is indexed in place, without merging.
158
+ *
159
+ * Example:
160
+ *
161
+ * ```javascript
162
+ * import { weldPrimitive } from '@gltf-transform/functions';
163
+ *
164
+ * const mesh = document.getRoot().listMeshes()
165
+ * .find((mesh) => mesh.getName() === 'Gizmo');
166
+ *
167
+ * for (const prim of mesh.listPrimitives()) {
168
+ * weldPrimitive(prim, {tolerance: 0.0001});
169
+ * }
170
+ * ```
171
+ *
172
+ * @privateRemarks TODO(v4): Remove the "Document" parameter.
173
+ */
174
+ export function weldPrimitive(a, b = WELD_DEFAULTS, c = WELD_DEFAULTS) {
175
+ let _document;
176
+ let _prim;
177
+ let _options;
178
+ if (a instanceof Primitive) {
179
+ const graph = a.getGraph();
180
+ _document = Document.fromGraph(graph);
181
+ _prim = a;
182
+ _options = expandWeldOptions(b);
183
+ }
184
+ else {
185
+ _document = a;
186
+ _prim = b;
187
+ _options = expandWeldOptions(c);
188
+ }
189
+ if (_prim.getIndices() && !_options.overwrite)
190
+ return;
191
+ if (_prim.getMode() === Primitive.Mode.POINTS)
192
+ return;
193
+ if (_options.tolerance === 0) {
194
+ _indexPrimitive(_document, _prim);
195
+ }
196
+ else {
197
+ _weldPrimitive(_document, _prim, _options);
198
+ }
199
+ }
200
+ /** @internal Adds indices, if missing. Does not merge vertices. */
201
+ function _indexPrimitive(doc, prim) {
202
+ // No need to overwrite here, even if options.overwrite=true.
203
+ if (prim.getIndices())
204
+ return;
205
+ const attr = prim.listAttributes()[0];
206
+ const numVertices = attr.getCount();
207
+ const buffer = attr.getBuffer();
208
+ const indices = doc
209
+ .createAccessor()
210
+ .setBuffer(buffer)
211
+ .setType(Accessor.Type.SCALAR)
212
+ .setArray(createIndices(numVertices));
213
+ prim.setIndices(indices);
214
+ }
215
+ /** @internal Weld and merge, combining vertices that are similar on all vertex attributes. */
216
+ function _weldPrimitive(doc, prim, options) {
217
+ const logger = doc.getLogger();
218
+ const srcPosition = prim.getAttribute('POSITION');
219
+ const srcIndices = prim.getIndices() || doc.createAccessor().setArray(createIndices(srcPosition.getCount()));
220
+ const uniqueIndices = new Uint32Array(new Set(srcIndices.getArray())).sort();
221
+ // (1) Compute per-attribute tolerance and spatial grid for vertices.
222
+ const attributeTolerance = {};
223
+ for (const semantic of prim.listSemantics()) {
224
+ const attribute = prim.getAttribute(semantic);
225
+ attributeTolerance[semantic] = getAttributeTolerance(semantic, attribute, options);
226
+ }
227
+ logger.debug(`${NAME}: Tolerance thresholds: ${formatKV(attributeTolerance)}`);
228
+ // (2) Compare and identify vertices to weld.
229
+ const posA = [0, 0, 0];
230
+ const posB = [0, 0, 0];
231
+ const grid = {};
232
+ const cellSize = attributeTolerance.POSITION;
233
+ for (let i = 0; i < uniqueIndices.length; i++) {
234
+ srcPosition.getElement(uniqueIndices[i], posA);
235
+ const key = getGridKey(posA, cellSize);
236
+ grid[key] = grid[key] || [];
237
+ grid[key].push(uniqueIndices[i]);
238
+ }
239
+ // (2) Compare and identify vertices to weld.
240
+ const srcMaxIndex = uniqueIndices[uniqueIndices.length - 1];
241
+ const weldMap = createIndices(srcMaxIndex + 1); // oldIndex → oldCommonIndex
242
+ const writeMap = new Array(uniqueIndices.length).fill(-1); // oldIndex → newIndex
243
+ const srcVertexCount = srcPosition.getCount();
244
+ let dstVertexCount = 0;
245
+ for (let i = 0; i < uniqueIndices.length; i++) {
246
+ const a = uniqueIndices[i];
247
+ srcPosition.getElement(a, posA);
248
+ const cellKeys = options.exhaustive ? getGridNeighborhoodKeys(posA, cellSize) : [getGridKey(posA, cellSize)];
249
+ cells: for (const cellKey of cellKeys) {
250
+ if (!grid[cellKey])
251
+ continue cells; // May occur in exhaustive search.
252
+ neighbors: for (const j of grid[cellKey]) {
253
+ const b = weldMap[j];
254
+ // Only weld to lower indices, preventing two-way match.
255
+ if (a <= b)
256
+ continue neighbors;
257
+ srcPosition.getElement(b, posB);
258
+ // Weld if base attributes and morph target attributes match.
259
+ const isBaseMatch = prim.listSemantics().every((semantic) => {
260
+ const attribute = prim.getAttribute(semantic);
261
+ const tolerance = attributeTolerance[semantic];
262
+ return compareAttributes(attribute, a, b, tolerance, semantic);
263
+ });
264
+ const isTargetMatch = prim.listTargets().every((target) => {
265
+ return target.listSemantics().every((semantic) => {
266
+ const attribute = target.getAttribute(semantic);
267
+ const tolerance = attributeTolerance[semantic];
268
+ return compareAttributes(attribute, a, b, tolerance, semantic);
269
+ });
270
+ });
271
+ if (isBaseMatch && isTargetMatch) {
272
+ weldMap[a] = b;
273
+ break cells;
274
+ }
275
+ }
276
+ }
277
+ // Output the vertex if we didn't find a match, else record the index of the match. Because
278
+ // we iterate vertices in ascending order, and only match to lower indices, we're
279
+ // guaranteed the source vertex for a weld has already been marked for output.
280
+ if (weldMap[a] === a) {
281
+ writeMap[a] = dstVertexCount++;
282
+ }
283
+ else {
284
+ writeMap[a] = writeMap[weldMap[a]];
285
+ }
286
+ }
287
+ logger.debug(`${NAME}: ${formatDeltaOp(srcVertexCount, dstVertexCount)} vertices.`);
288
+ // (3) Update indices.
289
+ const dstIndicesCount = srcIndices.getCount(); // # primitives does not change.
290
+ const dstIndicesArray = createIndices(dstIndicesCount, uniqueIndices.length);
291
+ for (let i = 0; i < dstIndicesCount; i++) {
292
+ dstIndicesArray[i] = writeMap[srcIndices.getScalar(i)];
293
+ }
294
+ prim.setIndices(srcIndices.clone().setArray(dstIndicesArray));
295
+ if (srcIndices.listParents().length === 1)
296
+ srcIndices.dispose();
297
+ // (4) Update vertex attributes.
298
+ for (const srcAttr of prim.listAttributes()) {
299
+ swapAttributes(prim, srcAttr, writeMap, dstVertexCount);
300
+ }
301
+ for (const target of prim.listTargets()) {
302
+ for (const srcAttr of target.listAttributes()) {
303
+ swapAttributes(target, srcAttr, writeMap, dstVertexCount);
304
+ }
305
+ }
306
+ // (5) Clean up degenerate triangles.
307
+ cleanPrimitive(prim);
308
+ }
309
+ /** Creates a new TypedArray of the same type as an original, with a new length. */
310
+ function createArrayOfType(array, length) {
311
+ const ArrayCtor = array.constructor;
312
+ return new ArrayCtor(length);
313
+ }
314
+ /** Replaces an {@link Attribute}, creating a new one with the given elements. */
315
+ function swapAttributes(parent, srcAttr, reorder, dstCount) {
316
+ const dstAttrArray = createArrayOfType(srcAttr.getArray(), dstCount * srcAttr.getElementSize());
317
+ const dstAttr = srcAttr.clone().setArray(dstAttrArray);
318
+ const done = new Uint8Array(dstCount);
319
+ for (let i = 0, el = []; i < reorder.length; i++) {
320
+ if (!done[reorder[i]]) {
321
+ dstAttr.setElement(reorder[i], srcAttr.getElement(i, el));
322
+ done[reorder[i]] = 1;
323
+ }
324
+ }
325
+ parent.swap(srcAttr, dstAttr);
326
+ // Clean up.
327
+ if (srcAttr.listParents().length === 1)
328
+ srcAttr.dispose();
329
+ }
330
+ const _a = [];
331
+ const _b = [];
332
+ /** Computes a per-attribute tolerance, based on domain and usage of the attribute. */
333
+ function getAttributeTolerance(semantic, attribute, options) {
334
+ // Attributes like NORMAL and COLOR_# do not vary in range like POSITION,
335
+ // so do not apply the given tolerance factor to these attributes.
336
+ if (semantic === 'NORMAL' || semantic === 'TANGENT')
337
+ return options.toleranceNormal;
338
+ if (semantic.startsWith('COLOR_'))
339
+ return Tolerance.COLOR;
340
+ if (semantic.startsWith('TEXCOORD_'))
341
+ return Tolerance.TEXCOORD;
342
+ if (semantic.startsWith('JOINTS_'))
343
+ return Tolerance.JOINTS;
344
+ if (semantic.startsWith('WEIGHTS_'))
345
+ return Tolerance.WEIGHTS;
346
+ _a.length = _b.length = 0;
347
+ attribute.getMinNormalized(_a);
348
+ attribute.getMaxNormalized(_b);
349
+ const diff = _b.map((bi, i) => bi - _a[i]);
350
+ const range = Math.max(...diff);
351
+ return options.tolerance * range;
352
+ }
353
+ /** Compares two vertex attributes against a tolerance threshold. */
354
+ function compareAttributes(attribute, a, b, tolerance, _semantic) {
355
+ attribute.getElement(a, _a);
356
+ attribute.getElement(b, _b);
357
+ for (let i = 0, il = attribute.getElementSize(); i < il; i++) {
358
+ if (Math.abs(_a[i] - _b[i]) > tolerance) {
359
+ return false;
360
+ }
361
+ }
362
+ return true;
363
+ }
364
+ function formatKV(kv) {
365
+ return Object.entries(kv)
366
+ .map(([k, v]) => `${k}=${v}`)
367
+ .join(', ');
368
+ }
369
+ // Order to search nearer cells first.
370
+ const CELL_OFFSETS = [0, -1, 1];
371
+ function getGridNeighborhoodKeys(p, cellSize) {
372
+ const keys = [];
373
+ const _p = [0, 0, 0];
374
+ for (const i of CELL_OFFSETS) {
375
+ for (const j of CELL_OFFSETS) {
376
+ for (const k of CELL_OFFSETS) {
377
+ _p[0] = p[0] + i * cellSize;
378
+ _p[1] = p[1] + j * cellSize;
379
+ _p[2] = p[2] + k * cellSize;
380
+ keys.push(getGridKey(_p, cellSize));
381
+ }
382
+ }
383
+ }
384
+ return keys;
385
+ }
386
+ function getGridKey(p, cellSize) {
387
+ const cellX = Math.round(p[0] / cellSize);
388
+ const cellY = Math.round(p[1] / cellSize);
389
+ const cellZ = Math.round(p[2] / cellSize);
390
+ return cellX + ':' + cellY + ':' + cellZ;
391
+ }
392
+ function expandWeldOptions(_options) {
393
+ const options = { ...WELD_DEFAULTS, ..._options };
394
+ if (options.tolerance < 0 || options.tolerance > 0.1) {
395
+ throw new Error(`${NAME}: Requires 0 <= tolerance <= 0.1`);
396
+ }
397
+ if (options.toleranceNormal < 0 || options.toleranceNormal > Math.PI / 2) {
398
+ throw new Error(`${NAME}: Requires 0 <= toleranceNormal <= ${(Math.PI / 2).toFixed(2)}`);
399
+ }
400
+ if (options.tolerance > 0) {
401
+ options.tolerance = Math.max(options.tolerance, Number.EPSILON);
402
+ options.toleranceNormal = Math.max(options.toleranceNormal, Number.EPSILON);
403
+ }
404
+ return options;
405
+ }
406
+ /**
407
+ * For purposes of welding, we consider a primitive to be 'empty' or degenerate
408
+ * if (1) it has an index, and (2) that index is empty. In some cases
409
+ * (mode=POINTS) the index may be missing — this is outside the scope of welding.
410
+ */
411
+ function isPrimEmpty(prim) {
412
+ const indices = prim.getIndices();
413
+ return !!indices && indices.getCount() === 0;
414
+ }