@needle-tools/gltf-build-pipeline 2.2.0-alpha.3 → 2.2.0-alpha.4
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
CHANGED
|
@@ -4,13 +4,14 @@ All notable changes to this package will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
5
5
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
-
## [2.2.0-alpha.
|
|
7
|
+
## [2.2.0-alpha.4] - 2023-06-06
|
|
8
8
|
- add: CLI `--progressive` option to `transform` command (default is true)
|
|
9
9
|
- add: CLI `--compress` option to `transform` command (default is true)
|
|
10
10
|
- add: config `meshes: { lods: boolean }` option
|
|
11
11
|
- remove: CLI "compress" command
|
|
12
12
|
- change: enable texture LODs by default
|
|
13
13
|
- fix: `--config` path can now point to a directory or a `needle.config.json` file as well
|
|
14
|
+
- fix: mesh LOD regression caused by change in gltf-transform `weld` behaviour
|
|
14
15
|
|
|
15
16
|
## [2.1.0-alpha.6] - 2023-06-04
|
|
16
17
|
- fix: errors when invoking npm scripts caused by the cli program
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Document, Primitive, Transform } from '@gltf-transform/core';
|
|
2
|
+
/** @hidden */
|
|
3
|
+
export declare function createIndices(count: number, maxIndex?: number): Uint16Array | Uint32Array;
|
|
4
|
+
export declare function formatLong(x: number): string;
|
|
5
|
+
export declare function formatDelta(a: number, b: number, decimals?: number): string;
|
|
6
|
+
export declare function formatDeltaOp(a: number, b: number): string;
|
|
7
|
+
export declare function cleanPrimitive(prim: Primitive): void;
|
|
8
|
+
/** Options for the {@link weld} function. */
|
|
9
|
+
export interface WeldOptions {
|
|
10
|
+
/** Tolerance for vertex positions, as a fraction of primitive AABB. */
|
|
11
|
+
tolerance?: number;
|
|
12
|
+
/** Tolerance for vertex normals, in radians. */
|
|
13
|
+
toleranceNormal?: number;
|
|
14
|
+
/** Whether to overwrite existing indices. */
|
|
15
|
+
overwrite?: boolean;
|
|
16
|
+
/** Enables a more thorough, but slower, search for vertices to weld. */
|
|
17
|
+
exhaustive?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare const WELD_DEFAULTS: Required<WeldOptions>;
|
|
20
|
+
/**
|
|
21
|
+
* Index {@link Primitive Primitives} and (optionally) merge similar vertices. When merged
|
|
22
|
+
* and indexed, data is shared more efficiently between vertices. File size can
|
|
23
|
+
* be reduced, and the GPU can sometimes use the vertex cache more efficiently.
|
|
24
|
+
*
|
|
25
|
+
* When welding, the 'tolerance' threshold determines which vertices qualify for
|
|
26
|
+
* welding based on distance between the vertices as a fraction of the primitive's
|
|
27
|
+
* bounding box (AABB). For example, tolerance=0.01 welds vertices within +/-1%
|
|
28
|
+
* of the AABB's longest dimension. Other vertex attributes are also compared
|
|
29
|
+
* during welding, with attribute-specific thresholds. For `tolerance=0`, geometry
|
|
30
|
+
* is indexed in place, without merging.
|
|
31
|
+
*
|
|
32
|
+
* To preserve visual appearance consistently, use low `toleranceNormal` thresholds
|
|
33
|
+
* around 0.1 (±3º). To pre-processing a scene before simplification or LOD creation,
|
|
34
|
+
* use higher thresholds around 0.5 (±30º).
|
|
35
|
+
*
|
|
36
|
+
* Example:
|
|
37
|
+
*
|
|
38
|
+
* ```javascript
|
|
39
|
+
* import { weld } from '@gltf-transform/functions';
|
|
40
|
+
*
|
|
41
|
+
* await document.transform(
|
|
42
|
+
* weld({ tolerance: 0.001, toleranceNormal: 0.5 })
|
|
43
|
+
* );
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @category Transforms
|
|
47
|
+
*/
|
|
48
|
+
export declare function weld(_options?: WeldOptions): Transform;
|
|
49
|
+
/**
|
|
50
|
+
* Index a {@link Primitive} and (optionally) weld similar vertices. When merged
|
|
51
|
+
* and indexed, data is shared more efficiently between vertices. File size can
|
|
52
|
+
* be reduced, and the GPU can sometimes use the vertex cache more efficiently.
|
|
53
|
+
*
|
|
54
|
+
* When welding, the 'tolerance' threshold determines which vertices qualify for
|
|
55
|
+
* welding based on distance between the vertices as a fraction of the primitive's
|
|
56
|
+
* bounding box (AABB). For example, tolerance=0.01 welds vertices within +/-1%
|
|
57
|
+
* of the AABB's longest dimension. Other vertex attributes are also compared
|
|
58
|
+
* during welding, with attribute-specific thresholds. For tolerance=0, geometry
|
|
59
|
+
* is indexed in place, without merging.
|
|
60
|
+
*
|
|
61
|
+
* Example:
|
|
62
|
+
*
|
|
63
|
+
* ```javascript
|
|
64
|
+
* import { weldPrimitive } from '@gltf-transform/functions';
|
|
65
|
+
*
|
|
66
|
+
* const mesh = document.getRoot().listMeshes()
|
|
67
|
+
* .find((mesh) => mesh.getName() === 'Gizmo');
|
|
68
|
+
*
|
|
69
|
+
* for (const prim of mesh.listPrimitives()) {
|
|
70
|
+
* weldPrimitive(prim, {tolerance: 0.0001});
|
|
71
|
+
* }
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @privateRemarks TODO(v4): Remove the "Document" parameter.
|
|
75
|
+
*/
|
|
76
|
+
export declare function weldPrimitive(a: Document | Primitive, b?: Primitive | WeldOptions, c?: Required<WeldOptions>): void;
|
|
@@ -0,0 +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,7 +1,8 @@
|
|
|
1
1
|
import { Primitive } from "@gltf-transform/core";
|
|
2
|
-
import { meshopt, simplifyPrimitive, SIMPLIFY_DEFAULTS,
|
|
2
|
+
import { meshopt, simplifyPrimitive, SIMPLIFY_DEFAULTS, WELD_DEFAULTS, draco } from "@gltf-transform/functions";
|
|
3
3
|
import { MeshoptEncoder, MeshoptSimplifier } from "meshoptimizer";
|
|
4
4
|
import { NEEDLE_mesh_compression } from "../extensions/index.js";
|
|
5
|
+
import { weldPrimitive } from "../functions/weld.3.js";
|
|
5
6
|
const NAME = 'needle_mesh_transform';
|
|
6
7
|
export const needle_mesh_transform = function (context, simplify) {
|
|
7
8
|
return async (document, _context) => {
|
|
@@ -52,6 +53,7 @@ async function simplifierTransform(document, _context, _options) {
|
|
|
52
53
|
options.simplifier = simplifier;
|
|
53
54
|
const weldOptions = {
|
|
54
55
|
...WELD_DEFAULTS,
|
|
56
|
+
tolerance: 0.00000000001,
|
|
55
57
|
overwrite: true,
|
|
56
58
|
};
|
|
57
59
|
for (const mesh of document.getRoot().listMeshes()) {
|
|
@@ -68,23 +68,16 @@ async function onProcess(file, opts) {
|
|
|
68
68
|
transforms.push(make_progressive_textures(file, { size: [smallesTextureSize, smallesTextureSize] }, opts));
|
|
69
69
|
}
|
|
70
70
|
// TODO: progressive meshes aren't supported yet for VRMs
|
|
71
|
-
|
|
71
|
+
const isVRM = file.endsWith(".vrm");
|
|
72
|
+
if (!isVRM && opts.config?.meshes?.lods !== false) {
|
|
72
73
|
transforms.push(make_progressive_meshes(file, {}, opts));
|
|
73
74
|
}
|
|
74
|
-
else
|
|
75
|
+
else if (isVRM)
|
|
75
76
|
logger.warn("WARN: Progressive meshes are not supported for VRM files yet");
|
|
76
77
|
transforms.push(sparse());
|
|
77
78
|
logger.info("→ Transforming: " + file);
|
|
78
79
|
await document.transform(...transforms);
|
|
79
|
-
|
|
80
|
-
if (opts.debug === true) {
|
|
81
|
-
// get extension
|
|
82
|
-
const extension = filename.split('.').pop();
|
|
83
|
-
// remove extension
|
|
84
|
-
filename = filename.split('.').slice(0, -1).join('.');
|
|
85
|
-
// add new extension
|
|
86
|
-
filename = filename + ".debug." + extension;
|
|
87
|
-
}
|
|
80
|
+
const filename = file;
|
|
88
81
|
await writeNodeIO(io, filename, document);
|
|
89
82
|
opts.results.push(filename);
|
|
90
83
|
logger.info("← Transforming done: " + filename);
|
|
@@ -596,7 +589,7 @@ export function make_progressive_meshes(filePath, _options, opts) {
|
|
|
596
589
|
shouldOmitLevel = true;
|
|
597
590
|
}
|
|
598
591
|
else if (vertexDifference < 32 || diff01 < 0.05) {
|
|
599
|
-
|
|
592
|
+
logger.warn(`[${NAME}] Flag mesh LOD ${level}: vertex count difference below threshold (removed ${vertexDifference.toFixed(0)} vertices, ${(-diff01 * 100).toFixed(4)}%) → LOD ${level} will be omitted. \"${mesh.getName()}\" at \"${lodInfo.path}\"`);
|
|
600
593
|
shouldOmitLevel = true;
|
|
601
594
|
}
|
|
602
595
|
}
|
|
@@ -642,7 +635,7 @@ export function make_progressive_meshes(filePath, _options, opts) {
|
|
|
642
635
|
}
|
|
643
636
|
else if (difference < 32 || diff01 <= 0.05) {
|
|
644
637
|
// the lowest LOD has the same vertex count as the original mesh → we don't need to write the extension and can also delete previously generated LODs
|
|
645
|
-
logger.info(`[${NAME}]: Flag mesh LOD ${generatedLODInfos.length} (embedded): vertex count difference below threshold (removed ${difference} vertices, ${(-diff01 * 100).toFixed(2)} %) → will remove
|
|
638
|
+
logger.info(`[${NAME}]: Flag mesh LOD ${generatedLODInfos.length} (embedded): vertex count difference below threshold (removed ${difference} vertices, ${(-diff01 * 100).toFixed(2)} %) → will remove all LODs. \"${mesh.getName()}\"`);
|
|
646
639
|
try {
|
|
647
640
|
for (const lodInfo of generatedLODInfos) {
|
|
648
641
|
rmSync(outputDirectory + "/" + lodInfo.path);
|
|
@@ -882,6 +875,8 @@ async function simplifyMesh(doc, mesh, prim, index, lodinfo) {
|
|
|
882
875
|
const weldOptions = {
|
|
883
876
|
...WELD_DEFAULTS,
|
|
884
877
|
overwrite: true,
|
|
878
|
+
cleanup: false,
|
|
879
|
+
tolerance: 0.00000000001,
|
|
885
880
|
...lodinfo,
|
|
886
881
|
};
|
|
887
882
|
const simplifyOptions = {
|
package/dist/utils/stats.js
CHANGED
|
@@ -4,7 +4,7 @@ import { statSync, writeFileSync } from "fs";
|
|
|
4
4
|
export function printStats(stats, logger, config) {
|
|
5
5
|
const duration = Date.now() - stats.startTime;
|
|
6
6
|
const durationInMinutes = duration / 1000 / 60;
|
|
7
|
-
const msg = `[Needle Build Pipeline] v${getVersion()} — Finished ${config?.usecase}
|
|
7
|
+
const msg = `[Needle Build Pipeline] v${getVersion()} — Finished compression \"${config?.usecase}\" of ${stats.totalFilesProcessed} files in ${durationInMinutes.toFixed(1)} minutes; total size ${stats.totalFileSizeInMBBefore.toFixed(1)} MB → ${stats.totalFileSizeInMB.toFixed(1)} MB, total GPU memory: ${stats.totalGPUMemoryInMB.toFixed(1)} MB`;
|
|
8
8
|
if (logger)
|
|
9
9
|
logger.info(msg);
|
|
10
10
|
else
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@needle-tools/gltf-build-pipeline",
|
|
3
|
-
"version": "2.2.0-alpha.
|
|
3
|
+
"version": "2.2.0-alpha.4",
|
|
4
4
|
"description": "Pipeline and tools for optimizing gltf files using gltf-transform and compression settings within glTF extensions",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|