@needle-tools/gltf-build-pipeline 2.2.0-alpha.1 → 2.2.0-alpha.11

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,12 +4,25 @@ 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] - 2023-06-06
8
- - Add: CLI `--progressive` option to `transform` command (default is enabled)
9
- - Add: config `meshes: { lods: boolean }` option
10
- - Remove: CLI "compress" command
11
- - Change: enable texture LODs by default
12
- - Fix: `--config` path can now point to a directory or a `needle.config.json` file as well
7
+ ## [2.2.0-alpha.11] - 2023-06-14
8
+ - fix: issue where progressive transform with un-indexed meshes would fail
9
+
10
+ ## [2.2.0-alpha.10] - 2023-06-13
11
+ - fix: progressive transform of converted GLB files where meshes don't have indicies
12
+ - fix: skip progressive texture generation for textures for textures that already match the lowest LOD size
13
+ - fix: if compression is set to None the LOD textures should respect that setting and also not be compressed
14
+ - fix: fallback mesh density calculation for meshes that aren't indexed
15
+ - fix: NEEDLE_mesh_compression extension falsely overriding primitive extensions
16
+
17
+ ## [2.2.0-alpha.5] - 2023-06-06
18
+ - add: CLI `--progressive` option to `transform` command (default is true)
19
+ - add: CLI `--compress` option to `transform` command (default is true)
20
+ - add: config `meshes: { lods: boolean }` option
21
+ - remove: CLI "compress" command
22
+ - change: enable texture LODs by default
23
+ - fix: `--config` path can now point to a directory or a `needle.config.json` file as well
24
+ - fix: mesh LOD regression caused by change in gltf-transform `weld` behaviour
25
+ - fix: KTX SOFTWARE >= 4.3.0 is installed
13
26
 
14
27
  ## [2.1.0-alpha.6] - 2023-06-04
15
28
  - fix: errors when invoking npm scripts caused by the cli program
@@ -19,6 +19,7 @@ import { getVersion } from "../utils/version.js";
19
19
  // return 0;
20
20
  // }
21
21
  const buildPipelineVersion = getVersion(false);
22
+ const debugLogs = process.argv.includes("--debug");
22
23
  /**
23
24
  * Clear the cache.
24
25
  * This will remove all files in the cache directory.
@@ -86,7 +87,8 @@ export function limitCacheSize(maxSizeMB = cacheSizeLimit) {
86
87
  export function addToCache(key, bytes) {
87
88
  const dir = getCacheDirectory();
88
89
  const path = `${dir}/${key}.bin`;
89
- console.log(`Added ${(bytes.byteLength / 1024).toFixed(1)} KB to cache at \"${path}\"`);
90
+ if (debugLogs)
91
+ console.log(`Added ${(bytes.byteLength / 1024).toFixed(1)} KB to cache at \"${path}\"`);
90
92
  fs.writeFileSync(path, bytes);
91
93
  }
92
94
  /**
package/dist/cli/index.js CHANGED
@@ -32,6 +32,7 @@ Each version will be compressed and written to the output directory.
32
32
  `)
33
33
  .argument('<input>', "The input glTF, GLB or VRM file that should be compressed OR a directory that contains files to be compressed")
34
34
  // .argument('<output>', "The output file. If not provided, the input file will be overwritten.", { default: "" })
35
+ .option("--compress", `When enabled files will be compressed`, { validator: program.BOOLEAN, default: true })
35
36
  .option("--progressive", `When enabled files will processed to be progressively loaded`, { validator: program.BOOLEAN, default: true })
36
37
  .option("--usecase <usecase>", `The usecase for the compression. This will set the compression settings. Possible options: [${UsecaseOptions.join(", ")}]`, { validator: program.STRING })
37
38
  .option("--config <config>", "The path to a JSON file containing the compression settings. This can also be a needle.config.json file with a \"compression\" object or usecase string.", { validator: program.STRING })
@@ -39,6 +40,10 @@ Each version will be compressed and written to the output directory.
39
40
  .option("--cache", "Use \"--cache False\" to disable caching.\nWhen caching is enabled then previously processed files will be detected and loaded from disc cache instead of being processed again if settings or the file contents have not changed.", { validator: program.BOOLEAN, default: true })
40
41
  .option("--debug", "Enable for more verbose outout with \"--debug True\"", { validator: program.BOOLEAN, default: false })
41
42
  .action(async ({ logger, args, options }) => {
43
+ if (options.compress === false && options.progressive === false) {
44
+ logger.error("You need to enable at least one of the options: --compress or --progressive");
45
+ return;
46
+ }
42
47
  logger.info("Transforming glTF file: " + args.input);
43
48
  const stats = {
44
49
  startTime: Date.now(),
@@ -72,7 +77,7 @@ Each version will be compressed and written to the output directory.
72
77
  logger.info("→ Transform " + file);
73
78
  const progressive_results = new Array();
74
79
  if (options.progressive === true) {
75
- await make_progressive({
80
+ if (await make_progressive({
76
81
  path: file,
77
82
  results: progressive_results,
78
83
  config,
@@ -81,25 +86,29 @@ Each version will be compressed and written to the output directory.
81
86
  }).catch(err => {
82
87
  logger.error(err);
83
88
  process.exit(ERROR_CODES.PROGRESSIVE_FAILED);
84
- });
89
+ }) === false) {
90
+ process.exit(ERROR_CODES.PROGRESSIVE_FAILED);
91
+ }
85
92
  }
86
- const opts = {
87
- config,
88
- stats,
89
- packedFiles: set,
90
- debug,
91
- useCache,
92
- logger: logger,
93
- file_stats,
94
- };
95
- for (const res of progressive_results) {
96
- if (await packGLTF(res, res, opts) === false) {
93
+ if (options.compress === true) {
94
+ const opts = {
95
+ config,
96
+ stats,
97
+ packedFiles: set,
98
+ debug,
99
+ useCache,
100
+ logger: logger,
101
+ file_stats,
102
+ };
103
+ for (const res of progressive_results) {
104
+ if (await packGLTF(res, res, opts) === false) {
105
+ process.exit(ERROR_CODES.PACKING_FAILED);
106
+ }
107
+ }
108
+ if (await packGLTF(file, file, opts) === false) {
97
109
  process.exit(ERROR_CODES.PACKING_FAILED);
98
110
  }
99
111
  }
100
- if (await packGLTF(file, file, opts) === false) {
101
- process.exit(ERROR_CODES.PACKING_FAILED);
102
- }
103
112
  });
104
113
  printStats(stats, logger, config);
105
114
  if (options.stats) {
@@ -62,7 +62,7 @@ export class NEEDLE_mesh_compression extends Extension {
62
62
  if (ext) {
63
63
  const prim = meshDef.primitives[i];
64
64
  if (prim) {
65
- prim.extensions = meshDef.extensions || {};
65
+ prim.extensions = prim.extensions || {};
66
66
  prim.extensions[EXTENSION_NAME] = ext.model;
67
67
  }
68
68
  }
@@ -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, weldPrimitive, WELD_DEFAULTS, draco } from "@gltf-transform/functions";
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()) {
@@ -7,7 +7,7 @@ declare interface ProgressiveOptions {
7
7
  debug?: boolean;
8
8
  useCache?: boolean;
9
9
  }
10
- export declare function make_progressive(opts: ProgressiveOptions): Promise<void>;
10
+ export declare function make_progressive(opts: ProgressiveOptions): Promise<boolean>;
11
11
  export declare function createTransform(name: string, fn: Transform): Transform;
12
12
  export declare function make_progressive_textures(filePath: string, _options: TextureResizeOptions, opts: ProgressiveOptions): Transform;
13
13
  /** Options for the {@link textureResize} function. */
@@ -14,14 +14,22 @@ import { compressPrimitive } from './needle_mesh_transform.js';
14
14
  import { calculateMeshDensity, getMeshInformation } from "../utils/mesh.js";
15
15
  export async function make_progressive(opts) {
16
16
  const file = opts.path;
17
- return foreachGLTF(file, async (f) => {
17
+ let success = true;
18
+ await foreachGLTF(file, async (f) => {
19
+ // Should we stop if one file fails?
20
+ if (!success) {
21
+ return;
22
+ }
18
23
  // Don't re-process files that are already LODs
19
24
  if (isMeshLOD(f) || isImageLOD(f)) {
20
25
  console.log(`Progressive Transform: Ignore existing LOD file at ${f}`);
21
26
  return;
22
27
  }
23
- await onProcess(f, opts);
28
+ const res = await onProcess(f, opts);
29
+ if (!res)
30
+ success = false;
24
31
  });
32
+ return success;
25
33
  }
26
34
  function registerExtensions(io) {
27
35
  registerDefaultExtensions(io);
@@ -39,10 +47,11 @@ function registerExtensions(io) {
39
47
  async function onProcess(file, opts) {
40
48
  if (!file.endsWith(".glb") && !file.endsWith(".gltf") && !file.endsWith(".vrm")) {
41
49
  console.log("ERR: file is not gltf/glb");
42
- return;
50
+ return false;
43
51
  }
44
52
  if (!existsSync(file)) {
45
53
  console.log("ERR: file does not exist: " + file);
54
+ return false;
46
55
  }
47
56
  try {
48
57
  const io = new NodeIO();
@@ -68,23 +77,16 @@ async function onProcess(file, opts) {
68
77
  transforms.push(make_progressive_textures(file, { size: [smallesTextureSize, smallesTextureSize] }, opts));
69
78
  }
70
79
  // TODO: progressive meshes aren't supported yet for VRMs
71
- if (!file.endsWith(".vrm") && opts.config?.meshes?.lods !== false) {
80
+ const isVRM = file.endsWith(".vrm");
81
+ if (!isVRM && opts.config?.meshes?.lods !== false) {
72
82
  transforms.push(make_progressive_meshes(file, {}, opts));
73
83
  }
74
- else
84
+ else if (isVRM)
75
85
  logger.warn("WARN: Progressive meshes are not supported for VRM files yet");
76
86
  transforms.push(sparse());
77
87
  logger.info("→ Transforming: " + file);
78
88
  await document.transform(...transforms);
79
- let filename = file;
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
- }
89
+ const filename = file;
88
90
  await writeNodeIO(io, filename, document);
89
91
  opts.results.push(filename);
90
92
  logger.info("← Transforming done: " + filename);
@@ -92,7 +94,9 @@ async function onProcess(file, opts) {
92
94
  }
93
95
  catch (err) {
94
96
  console.log("ERR: failed to process file \"" + file + "\", Reason: " + err.message, "\nStack:\n" + err.stack);
97
+ return false;
95
98
  }
99
+ return true;
96
100
  }
97
101
  // basic idea:
98
102
  // 1. iterate all images that have an extension marking them to be progressively loaded
@@ -168,11 +172,15 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
168
172
  logger.debug(`[${NAME}] Skipping because progressive extension is not available. ${textureIndex}`);
169
173
  continue;
170
174
  }
175
+ /** This is the current texture's size */
176
+ const currentSize = Math.min(...texture.getSize());
177
+ if (currentSize <= settings.maxSize) {
178
+ logger.debug(`[${NAME}] Skipping, texture[${textureIndex}] is already smaller than the requested size: ${currentSize}px <= ${settings.maxSize}px`);
179
+ continue;
180
+ }
171
181
  const textureGuid = settings.guid?.length ? settings.guid : generateGuid(filename + "_texture_" + index.toString());
172
182
  /** This is the size of the texture that is embedded in the glTF. No LOD should be smaller than this */
173
183
  const maxSize = settings.maxSize;
174
- /** This is the current texture's size */
175
- const currentSize = Math.min(...texture.getSize());
176
184
  const LOD_LEVEL = new Array();
177
185
  LOD_LEVEL.push({ size: currentSize });
178
186
  let generateLods = true; //opts.config?.usecase === "world";
@@ -309,27 +317,32 @@ async function createTextureLod(args, opts) {
309
317
  // Make sure the material is used
310
318
  createMeshForMaterial(newDoc, newMaterial, slot_index++);
311
319
  }
312
- const compressionSettings = texture.getExtension(NEEDLE_compression_texture.EXTENSION_NAME);
313
- if (compressionSettings !== undefined && compressionSettings !== null && compressionSettings.getExtensionDefinition) {
320
+ const compressionSettingsExt = texture.getExtension(NEEDLE_compression_texture.EXTENSION_NAME);
321
+ if (compressionSettingsExt !== undefined && compressionSettingsExt !== null && compressionSettingsExt.getExtensionDefinition) {
314
322
  logger.info("> Pass compression extension to new texture");
315
323
  const newExt = newDoc.createExtension(NEEDLE_compression_texture);
316
- const def = compressionSettings.getExtensionDefinition();
317
- switch (opts.config.usecase) {
318
- // For world scenarios we use ETC1S
319
- case "world":
320
- def.mode = "ETC1S";
321
- break;
322
- case "product":
323
- def.mode = detectTextureCompressionMode(slots);
324
- def.quality = 100;
325
- break;
326
- default:
327
- def.mode = detectTextureCompressionMode(slots);
328
- break;
324
+ const compressionSettings = compressionSettingsExt.getExtensionDefinition();
325
+ if (compressionSettings?.mode === "none") {
326
+ // if the mode is set to None then we keep that setting for the new texture
327
+ }
328
+ else {
329
+ switch (opts.config.usecase) {
330
+ // For world scenarios we use ETC1S
331
+ case "world":
332
+ compressionSettings.mode = "ETC1S";
333
+ break;
334
+ case "product":
335
+ compressionSettings.mode = detectTextureCompressionMode(slots);
336
+ compressionSettings.quality = 100;
337
+ break;
338
+ default:
339
+ compressionSettings.mode = detectTextureCompressionMode(slots);
340
+ break;
341
+ }
329
342
  }
330
- def.slot = slots[0];
331
- const newProp = newExt.create(def);
332
- newTexture.setExtension(NEEDLE_compression_texture.EXTENSION_NAME, newProp);
343
+ compressionSettings.slot = slots[0];
344
+ const newCompressionSettings = newExt.create(compressionSettings);
345
+ newTexture.setExtension(NEEDLE_compression_texture.EXTENSION_NAME, newCompressionSettings);
333
346
  }
334
347
  // add progressive texture extension
335
348
  const progressiveExt = newDoc.createExtension(NEEDLE_progressive);
@@ -596,7 +609,7 @@ export function make_progressive_meshes(filePath, _options, opts) {
596
609
  shouldOmitLevel = true;
597
610
  }
598
611
  else if (vertexDifference < 32 || diff01 < 0.05) {
599
- console.log(`[${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}\"`);
612
+ 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
613
  shouldOmitLevel = true;
601
614
  }
602
615
  }
@@ -642,7 +655,7 @@ export function make_progressive_meshes(filePath, _options, opts) {
642
655
  }
643
656
  else if (difference < 32 || diff01 <= 0.05) {
644
657
  // 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 LOD. \"${mesh.getName()}\"`);
658
+ 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
659
  try {
647
660
  for (const lodInfo of generatedLODInfos) {
648
661
  rmSync(outputDirectory + "/" + lodInfo.path);
@@ -695,7 +708,8 @@ function clonePrimitiveData(prim, logger) {
695
708
  if (semantics.some(s => s.includes("JOINTS"))) {
696
709
  return null;
697
710
  }
698
- const indices = new Uint32Array(prim.getIndices().getArray());
711
+ const prevIndices = prim.getIndices();
712
+ const indices = !prevIndices ? new Uint32Array() : new Uint32Array(prevIndices.getArray());
699
713
  const attributes = {};
700
714
  for (const attr of semantics) {
701
715
  logger.info(`[${NAME}] Clone attribute ${attr}`);
@@ -760,7 +774,7 @@ async function createMeshLod(doc, basePath, level, mesh, meshInfo, guid, lodLeve
760
774
  newNode.setName("LOD " + level + "_" + guid);
761
775
  newNode.setTranslation([0, 0, 0]);
762
776
  const newMesh = newDoc.createMesh();
763
- newMesh.setName(mesh.getName() + " LOD " + level);
777
+ newMesh.setName((mesh.getName() || "Unnamed") + " LOD " + level);
764
778
  newNode.setMesh(newMesh);
765
779
  const newMat = newDoc.createMaterial();
766
780
  const weights = mesh.getWeights();
@@ -882,6 +896,8 @@ async function simplifyMesh(doc, mesh, prim, index, lodinfo) {
882
896
  const weldOptions = {
883
897
  ...WELD_DEFAULTS,
884
898
  overwrite: true,
899
+ cleanup: false,
900
+ tolerance: 0.00000000001,
885
901
  ...lodinfo,
886
902
  };
887
903
  const simplifyOptions = {
@@ -39,8 +39,12 @@ export const needle_texture_transform = function (options) {
39
39
  };
40
40
  return createTransform("needle_texture_transform", async (document) => {
41
41
  await webPContext.prepare(document);
42
- if (context.noToktx !== true)
43
- await toktxContext.prepare(document);
42
+ if (context.noToktx !== true) {
43
+ const validToktx = await toktxContext.prepare(document);
44
+ if (!validToktx) {
45
+ context.noToktx = true;
46
+ }
47
+ }
44
48
  try {
45
49
  const logger = document.getLogger();
46
50
  const textures = document.getRoot().listTextures();
@@ -168,7 +172,7 @@ async function processTexture(texture, index, document, logger, context) {
168
172
  successFullyProcessed = await context.toktxContext.process(index, texture, options, context);
169
173
  }
170
174
  else {
171
- console.log("Toktx not found: Skipping texture compression.");
175
+ console.log("Toktx not found: Skipping texture compression: " + originalMimeType);
172
176
  }
173
177
  }
174
178
  if (context.useCache && successFullyProcessed && key) {
@@ -1,7 +1,7 @@
1
1
  import { BufferUtils, FileUtils, ImageUtils, uuid } from "@gltf-transform/core";
2
2
  import { listTextureSlots, getTextureChannelMask, fitPowerOfTwo, TextureResizeFilter } from "@gltf-transform/functions";
3
3
  import { KHRTextureBasisu } from "@gltf-transform/extensions";
4
- import { checkKTXSoftware, createParams, Mode } from "./toktx.js";
4
+ import { checkKTXSoftware, createParams, KTX_SOFTWARE_VERSION_MIN, Mode } from "./toktx.js";
5
5
  import { existsSync, mkdirSync } from "fs";
6
6
  import { formatBytes, spawn, waitExit } from './util.js';
7
7
  import { ETC1S_DEFAULTS, UASTC_DEFAULTS } from "./toktx_types.js";
@@ -70,7 +70,7 @@ export class NeedleToktxContext {
70
70
  const logger = document.getLogger();
71
71
  this.version = await checkKTXSoftware(logger, this.env);
72
72
  if (this.version === null) {
73
- console.log("Will not run toktx transform");
73
+ logger.warn(`WARN: toktx not found, skipping texture compression. Please install KTX-Software >= ${KTX_SOFTWARE_VERSION_MIN}`);
74
74
  this._prepareFailed = true;
75
75
  return false;
76
76
  }
@@ -1,5 +1,6 @@
1
1
  import { Document, ILogger, vec2, Texture } from '@gltf-transform/core';
2
2
  import { ETC1SOptions, UASTCOptions } from './toktx_types.js';
3
+ export declare const KTX_SOFTWARE_VERSION_MIN = "4.3.0";
3
4
  export declare const Filter: {
4
5
  BOX: string;
5
6
  TENT: string;
@@ -10,8 +10,7 @@ import { TextureResizeFilter, getTextureColorSpace } from '@gltf-transform/funct
10
10
  import { ETC1S_DEFAULTS, UASTC_DEFAULTS } from './toktx_types.js';
11
11
  tmp.setGracefulCleanup();
12
12
  const NUM_CPUS = os.cpus().length || 1; // microsoft/vscode#112122
13
- const KTX_SOFTWARE_VERSION_MIN = '4.0.0-rc1';
14
- const KTX_SOFTWARE_VERSION_ACTIVE = '4.1.0-rc1';
13
+ export const KTX_SOFTWARE_VERSION_MIN = '4.3.0';
15
14
  export const Filter = {
16
15
  BOX: 'box',
17
16
  TENT: 'tent',
@@ -167,6 +166,7 @@ export async function checkKTXSoftware(logger, env) {
167
166
  }
168
167
  else if (semver.lt(semver.clean(version), KTX_SOFTWARE_VERSION_MIN)) {
169
168
  logger.warn(`toktx: Expected KTX-Software >= v${KTX_SOFTWARE_VERSION_MIN}, found ${version}.`);
169
+ return null;
170
170
  }
171
171
  else {
172
172
  if (!didLogToktxVersion)
@@ -5,4 +5,9 @@ export declare type MeshInformation = {
5
5
  readonly totalIndexCount: number;
6
6
  };
7
7
  export declare function getMeshInformation(mesh: Mesh): MeshInformation;
8
- export declare function calculateMeshDensity(prim: Primitive): number;
8
+ /**
9
+ * Calculate the density of a mesh.
10
+ * @param prim - The primitive to calculate the density for.
11
+ * @returns The density of the mesh or -1 if the mesh is not indexed.
12
+ */
13
+ export declare function calculateMeshDensity(prim: Primitive): number | -1;
@@ -14,6 +14,11 @@ export function getMeshInformation(mesh) {
14
14
  totalIndexCount,
15
15
  };
16
16
  }
17
+ /**
18
+ * Calculate the density of a mesh.
19
+ * @param prim - The primitive to calculate the density for.
20
+ * @returns The density of the mesh or -1 if the mesh is not indexed.
21
+ */
17
22
  export function calculateMeshDensity(prim) {
18
23
  const bounds = {
19
24
  min: [Infinity, Infinity, Infinity],
@@ -71,16 +76,28 @@ export function calculateMeshDensity(prim) {
71
76
  // Add the area (half the magnitude of the cross product) to the total
72
77
  area += Math.sqrt(cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]) / 2;
73
78
  }
79
+ const count = indices.getCount() / 3;
80
+ const density = count / area;
81
+ // const size = [0, 0, 0] as vec3;
82
+ // size[0] = bounds.max[0] - bounds.min[0];
83
+ // size[1] = bounds.max[1] - bounds.min[1];
84
+ // size[2] = bounds.max[2] - bounds.min[2];
85
+ // const volume = size[0] * size[1] * size[2];
86
+ // const density = count / volume;
87
+ return density;
74
88
  }
75
- const count = indices.getCount() / 3;
76
- const density = count / area;
77
- // const size = [0, 0, 0] as vec3;
78
- // size[0] = bounds.max[0] - bounds.min[0];
79
- // size[1] = bounds.max[1] - bounds.min[1];
80
- // size[2] = bounds.max[2] - bounds.min[2];
81
- // const volume = size[0] * size[1] * size[2];
82
- // const density = count / volume;
83
- return density;
89
+ else {
90
+ // calculate density if the mesh is not indexed
91
+ const count = position.getCount();
92
+ const size = [0, 0, 0];
93
+ size[0] = bounds.max[0] - bounds.min[0];
94
+ size[1] = bounds.max[1] - bounds.min[1];
95
+ size[2] = bounds.max[2] - bounds.min[2];
96
+ const volume = size[0] * size[1] * size[2];
97
+ const density = count / volume;
98
+ return density;
99
+ }
100
+ return -1;
84
101
  function expandBounds(point, target) {
85
102
  for (let i = 0; i < 3; i++) {
86
103
  target.min[i] = Math.min(point[i], target.min[i]);
@@ -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} compression ${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`;
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,8 +1,8 @@
1
1
  {
2
2
  "name": "@needle-tools/gltf-build-pipeline",
3
- "version": "2.2.0-alpha.1",
3
+ "version": "2.2.0-alpha.11",
4
4
  "description": "Pipeline and tools for optimizing gltf files using gltf-transform and compression settings within glTF extensions",
5
- "main": "src/index.js",
5
+ "main": "dist/index.js",
6
6
  "type": "module",
7
7
  "readme": "README.md",
8
8
  "author": {
@@ -1,4 +1,4 @@
1
1
 
2
- import { clearCache } from '../dist/src/cache/index.js';
2
+ import { clearCache } from '../dist/cache/index.js';
3
3
 
4
4
  clearCache();
@@ -1,6 +1,6 @@
1
1
  import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
2
- import { ALL_EXTENSIONS as NEEDLE_EXTENSIONS } from "../dist/src/extensions/NEEDLE_opaque/index.js";
3
- import { NEEDLE_progressive } from '../dist/src/index.js';
2
+ import { ALL_EXTENSIONS as NEEDLE_EXTENSIONS } from "../dist/extensions/NEEDLE_opaque/index.js";
3
+ import { NEEDLE_progressive } from '../dist/index.js';
4
4
 
5
5
 
6
6
  export function registerExtensions(node) {
@@ -1,4 +1,4 @@
1
- import { foreachGLTF as foreach } from '../dist/src/index.js';
1
+ import { foreachGLTF as foreach } from '../dist/index.js';
2
2
 
3
3
 
4
4
  export function foreachGLTF(args, callback) {
package/scripts/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
2
  import { spawnSync } from "child_process";
3
- import { getVersion } from "../dist/src/index.js";
3
+ import { getVersion } from "../dist/index.js";
4
4
 
5
5
  const workingDir = process.cwd();
6
6
 
@@ -1,4 +1,4 @@
1
1
 
2
- import { limitCacheSize } from '../dist/src/cache/index.js';
2
+ import { limitCacheSize } from '../dist/cache/index.js';
3
3
 
4
4
  limitCacheSize(512);
@@ -1,5 +1,5 @@
1
- import { getConfig, getVersion } from '../dist/src/index.js';
2
- import { make_progressive } from '../dist/src/transforms/needle_progressive.js';
1
+ import { getConfig, getVersion } from '../dist/index.js';
2
+ import { make_progressive } from '../dist/transforms/needle_progressive.js';
3
3
 
4
4
  const args = process.argv;
5
5
 
@@ -1,6 +1,6 @@
1
1
  import commandLineArgs from "command-line-args";
2
2
  import { foreachGLTF } from './fileutils.mjs';
3
- import { getVersion, limitCacheSize, getConfig, packGLTF, printStats } from '../dist/src/index.js';
3
+ import { getVersion, limitCacheSize, getConfig, packGLTF, printStats } from '../dist/index.js';
4
4
  import { Logger, Verbosity } from "@gltf-transform/core";
5
5
 
6
6
  console.log(`[Needle Build Pipeline] pack glTF — version ${getVersion()}`)
@@ -60,7 +60,7 @@ if (noToktx)
60
60
  const packedFiles = new Set();
61
61
 
62
62
  /**
63
- * @type {import("../dist/src/index.js").Stats}
63
+ * @type {import("../dist/index.js").Stats}
64
64
  */
65
65
  const stats = {
66
66
  startTime: Date.now(),