@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.
- package/CHANGELOG.md +520 -517
- package/README.md +38 -38
- package/dist/cache/cache.d.ts +55 -55
- package/dist/cache/cache.js +326 -326
- package/dist/cache/index.d.ts +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.js +198 -198
- package/dist/config/index.d.ts +53 -53
- package/dist/config/index.js +152 -152
- package/dist/constants.d.ts +7 -7
- package/dist/constants.js +7 -7
- package/dist/extensions/NEEDLE_compression_texture/NEEDLE_compression_texture.d.ts +24 -24
- package/dist/extensions/NEEDLE_compression_texture/NEEDLE_compression_texture.js +54 -54
- package/dist/extensions/NEEDLE_compression_texture/index.d.ts +1 -1
- package/dist/extensions/NEEDLE_compression_texture/index.js +1 -1
- package/dist/extensions/NEEDLE_mesh_compression/NEEDLE_mesh_compression.d.ts +27 -27
- package/dist/extensions/NEEDLE_mesh_compression/NEEDLE_mesh_compression.js +88 -88
- package/dist/extensions/NEEDLE_mesh_compression/index.d.ts +1 -1
- package/dist/extensions/NEEDLE_mesh_compression/index.js +1 -1
- package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.d.ts +27 -27
- package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.js +980 -980
- package/dist/extensions/NEEDLE_opaque/index.d.ts +3 -3
- package/dist/extensions/NEEDLE_opaque/index.js +30 -30
- package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.d.ts +48 -48
- package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.js +69 -69
- package/dist/extensions/NEEDLE_progressive/index.d.ts +1 -1
- package/dist/extensions/NEEDLE_progressive/index.js +1 -1
- package/dist/extensions/NEEDLE_progressive_mesh_settings/index.d.ts +23 -23
- package/dist/extensions/NEEDLE_progressive_mesh_settings/index.js +41 -41
- package/dist/extensions/NEEDLE_progressive_texture_settings/index.d.ts +25 -25
- package/dist/extensions/NEEDLE_progressive_texture_settings/index.js +38 -38
- package/dist/extensions/index.d.ts +7 -7
- package/dist/extensions/index.js +17 -17
- package/dist/extensions/utils.d.ts +6 -6
- package/dist/extensions/utils.js +72 -72
- package/dist/functions/weld.3.d.ts +76 -76
- package/dist/functions/weld.3.js +414 -414
- package/dist/index.d.ts +8 -8
- package/dist/index.js +8 -8
- package/dist/scripts/index.d.ts +2 -2
- package/dist/scripts/index.js +2 -2
- package/dist/scripts/pack-gltf.d.ts +15 -15
- package/dist/scripts/pack-gltf.js +135 -133
- package/dist/scripts/stats.d.ts +2 -2
- package/dist/scripts/stats.js +19 -19
- package/dist/transforms/index.d.ts +9 -9
- package/dist/transforms/index.js +9 -9
- package/dist/transforms/needle_animation_transform.d.ts +2 -2
- package/dist/transforms/needle_animation_transform.js +5 -5
- package/dist/transforms/needle_asset.d.ts +3 -3
- package/dist/transforms/needle_asset.js +50 -50
- package/dist/transforms/needle_common.d.ts +17 -17
- package/dist/transforms/needle_common.js +1 -1
- package/dist/transforms/needle_mesh_transform.d.ts +15 -15
- package/dist/transforms/needle_mesh_transform.js +223 -223
- package/dist/transforms/needle_progressive.d.ts +45 -45
- package/dist/transforms/needle_progressive.js +1043 -1043
- package/dist/transforms/needle_texture_transform.d.ts +13 -13
- package/dist/transforms/needle_texture_transform.js +257 -257
- package/dist/transforms/needle_toktx.d.ts +26 -26
- package/dist/transforms/needle_toktx.js +243 -243
- package/dist/transforms/needle_webp.d.ts +15 -15
- package/dist/transforms/needle_webp.js +53 -53
- package/dist/transforms/toktx.d.ts +52 -52
- package/dist/transforms/toktx.js +221 -221
- package/dist/transforms/toktx_types.d.ts +78 -78
- package/dist/transforms/toktx_types.js +62 -62
- package/dist/transforms/util.d.ts +21 -21
- package/dist/transforms/util.js +90 -90
- package/dist/utils/compression_utils.d.ts +2 -2
- package/dist/utils/compression_utils.js +6 -6
- package/dist/utils/fileutils.d.ts +19 -19
- package/dist/utils/fileutils.js +177 -177
- package/dist/utils/guid.d.ts +1 -1
- package/dist/utils/guid.js +5 -5
- package/dist/utils/index.d.ts +10 -10
- package/dist/utils/index.js +10 -10
- package/dist/utils/merge.d.ts +12 -12
- package/dist/utils/merge.js +31 -31
- package/dist/utils/mesh.d.ts +13 -13
- package/dist/utils/mesh.js +124 -124
- package/dist/utils/nodeio.d.ts +6 -6
- package/dist/utils/nodeio.js +86 -86
- package/dist/utils/stats.d.ts +70 -70
- package/dist/utils/stats.js +258 -258
- package/dist/utils/texture.d.ts +5 -5
- package/dist/utils/texture.js +14 -14
- package/dist/utils/validate.d.ts +5 -5
- package/dist/utils/validate.js +17 -17
- package/dist/utils/version.d.ts +1 -1
- package/dist/utils/version.gen.d.ts +1 -1
- package/dist/utils/version.gen.js +1 -1
- package/dist/utils/version.js +4 -4
- package/package.json +79 -79
- package/scripts/clear-caches.mjs +3 -3
- package/scripts/defaults.mjs +11 -11
- package/scripts/fileutils.mjs +5 -5
- package/scripts/index.mjs +41 -41
- package/scripts/limit-caches.mjs +3 -3
- package/scripts/make-progressive.mjs +16 -16
- package/scripts/pack-gltf.mjs +106 -106
- package/tsconfig.json +19 -19
package/dist/functions/weld.3.js
CHANGED
|
@@ -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
|
+
}
|