@needle-tools/gltf-build-pipeline 1.4.1-beta.7 → 1.4.1-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/extensions/NEEDLE_mesh_compression/NEEDLE_mesh_compression.js +3 -1
- package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.d.ts +0 -1
- package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.js +1 -5
- package/dist/extensions/NEEDLE_progressive_mesh_settings/index.d.ts +2 -2
- package/dist/extensions/NEEDLE_progressive_mesh_settings/index.js +1 -4
- package/dist/transforms/needle_progressive.js +112 -134
- package/dist/utils/guid.d.ts +1 -0
- package/dist/utils/guid.js +5 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/mesh.d.ts +7 -0
- package/dist/utils/mesh.js +86 -0
- package/index.mjs +0 -2
- package/pack-gltf.mjs +1 -1
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ 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
|
+
## [1.4.1-beta.8] - 2023-03-18
|
|
8
|
+
- change: if an asset doesnt have a guid we use a uuid package now
|
|
9
|
+
- change: ignore mesh LODs with vertex count < 4
|
|
10
|
+
- change: allow generating mesh LODs without extension (if the mesh has > 32 vertices)
|
|
11
|
+
- change: omit generated LODs if the change is too small (e.g. the vertex count didnt change enough)
|
|
12
|
+
|
|
7
13
|
## [1.4.1-beta.6] - 2023-03-15
|
|
8
14
|
- add: caching support for mesh LODs
|
|
9
15
|
|
|
@@ -44,6 +44,8 @@ export class NEEDLE_mesh_compression extends Extension {
|
|
|
44
44
|
return this;
|
|
45
45
|
}
|
|
46
46
|
write(ctx) {
|
|
47
|
+
if (debugLog)
|
|
48
|
+
console.log("WRITE " + EXTENSION_NAME);
|
|
47
49
|
const root = this.document.getRoot();
|
|
48
50
|
const rootExtension = root.getExtension(EXTENSION_NAME);
|
|
49
51
|
if (rootExtension) {
|
|
@@ -72,7 +74,7 @@ export class NEEDLE_mesh_compression extends Extension {
|
|
|
72
74
|
export class NEEDLE_mesh_compression_property extends ExtensionProperty {
|
|
73
75
|
static EXTENSION_NAME = EXTENSION_NAME;
|
|
74
76
|
extensionName = EXTENSION_NAME;
|
|
75
|
-
parentTypes = [PropertyType.PRIMITIVE, PropertyType.
|
|
77
|
+
parentTypes = [PropertyType.ROOT, PropertyType.PRIMITIVE, PropertyType.MESH];
|
|
76
78
|
propertyType = EXTENSION_NAME;
|
|
77
79
|
init() { }
|
|
78
80
|
model;
|
|
@@ -7,7 +7,6 @@ export declare interface IProgressiveLoadingInfo {
|
|
|
7
7
|
uri?: string | Array<string>;
|
|
8
8
|
density?: number;
|
|
9
9
|
}
|
|
10
|
-
export declare function generateGuid(): string;
|
|
11
10
|
export declare class NEEDLE_progressive extends Extension {
|
|
12
11
|
static EXTENSION_NAME: string;
|
|
13
12
|
extensionName: string;
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import { Extension, ExtensionProperty, PropertyType
|
|
1
|
+
import { Extension, ExtensionProperty, PropertyType } from "@gltf-transform/core";
|
|
2
2
|
// TODO: rename to NEEDLE_progressive_texture
|
|
3
3
|
export const NEEDLE_PROGRESSIVE_EXTENSION_NAME = "NEEDLE_progressive";
|
|
4
|
-
export function generateGuid() {
|
|
5
|
-
// TODO: replace with same uuid package as in needle engine
|
|
6
|
-
return uuid();
|
|
7
|
-
}
|
|
8
4
|
export class NEEDLE_progressive extends Extension {
|
|
9
5
|
static EXTENSION_NAME = NEEDLE_PROGRESSIVE_EXTENSION_NAME;
|
|
10
6
|
extensionName = NEEDLE_PROGRESSIVE_EXTENSION_NAME;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Extension, ExtensionProperty, PropertyType, ReaderContext, WriterContext } from "@gltf-transform/core";
|
|
2
2
|
export declare interface IProgressiveMeshSettings {
|
|
3
3
|
guid: string;
|
|
4
|
+
generateLods: boolean;
|
|
4
5
|
}
|
|
5
6
|
export declare class NEEDLE_progressive_mesh_settings extends Extension {
|
|
6
7
|
static EXTENSION_NAME: string;
|
|
@@ -14,7 +15,6 @@ export declare class NEEDLE_progressive_mesh_settings_property extends Extension
|
|
|
14
15
|
parentTypes: PropertyType[];
|
|
15
16
|
propertyType: string;
|
|
16
17
|
init(): void;
|
|
17
|
-
|
|
18
|
-
maxSize: number;
|
|
18
|
+
generateLods: boolean;
|
|
19
19
|
guid: string;
|
|
20
20
|
}
|
|
@@ -36,9 +36,6 @@ export class NEEDLE_progressive_mesh_settings_property extends ExtensionProperty
|
|
|
36
36
|
parentTypes = [PropertyType.MESH];
|
|
37
37
|
propertyType = EXTENSION_NAME;
|
|
38
38
|
init() { }
|
|
39
|
-
|
|
40
|
-
return this.maxSize !== undefined && this.maxSize > 0;
|
|
41
|
-
}
|
|
42
|
-
maxSize;
|
|
39
|
+
generateLods;
|
|
43
40
|
guid;
|
|
44
41
|
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { BufferUtils, Document, NodeIO, PropertyType } from '@gltf-transform/core';
|
|
2
2
|
import { SIMPLIFY_DEFAULTS, WELD_DEFAULTS, listTextureSlots, sparse } from '@gltf-transform/functions';
|
|
3
|
-
import { NEEDLE_PROGRESSIVE_EXTENSION_NAME, NEEDLE_progressive
|
|
3
|
+
import { NEEDLE_PROGRESSIVE_EXTENSION_NAME, NEEDLE_progressive } from "../extensions/NEEDLE_progressive/index.js";
|
|
4
4
|
import { createOpaqueExtension, NEEDLE_compression_texture, NEEDLE_mesh_compression, NEEDLE_progressive_mesh_settings, NEEDLE_progressive_texture_settings, registerExtensions as registerDefaultExtensions } from '../extensions/index.js';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import { existsSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
|
|
7
7
|
import { addToCache, getHash, getKey, tryGetFromCache } from '../cache/index.js';
|
|
8
8
|
import sharp from "sharp";
|
|
9
|
-
import { foreachGLTF } from '../utils/index.js';
|
|
9
|
+
import { foreachGLTF, generateGuid } from '../utils/index.js';
|
|
10
10
|
import { needle_asset } from './needle_asset.js';
|
|
11
11
|
import { MeshoptDecoder, MeshoptSimplifier } from 'meshoptimizer';
|
|
12
12
|
import { compressPrimitive } from './needle_mesh_transform.js';
|
|
13
|
+
import { calculateMeshDensity, getMeshInformation } from "../utils/mesh.js";
|
|
13
14
|
export async function make_progressive(opts) {
|
|
14
15
|
console.log("INFO: Run progressive transform: " + opts.path);
|
|
15
16
|
const file = opts.path;
|
|
@@ -109,40 +110,40 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
|
|
|
109
110
|
const uri = texture.getURI();
|
|
110
111
|
const match = !options.pattern || options.pattern.test(name) || options.pattern.test(uri);
|
|
111
112
|
if (!match) {
|
|
112
|
-
logger.debug(
|
|
113
|
+
logger.debug(`[${NAME}] Skipping, excluded by "pattern" parameter. ${textureIndex}`);
|
|
113
114
|
continue;
|
|
114
115
|
}
|
|
115
116
|
if (texture.getMimeType() !== 'image/png' && texture.getMimeType() !== 'image/jpeg') {
|
|
116
|
-
logger.warn(
|
|
117
|
+
logger.warn(`[${NAME}] Skipping, unsupported texture type "${texture.getMimeType()} at ${textureIndex}".`);
|
|
117
118
|
continue;
|
|
118
119
|
}
|
|
119
120
|
const slots = listTextureSlots(texture);
|
|
120
121
|
if (options.slots && !slots.some((slot) => options.slots?.test(slot))) {
|
|
121
|
-
logger.debug(
|
|
122
|
+
logger.debug(`[${NAME}] Skipping, [${slots.join(', ')}] excluded by "slots" parameter. ${textureIndex}`);
|
|
122
123
|
continue;
|
|
123
124
|
}
|
|
124
125
|
const settings = texture.getExtension(NEEDLE_progressive_texture_settings.EXTENSION_NAME);
|
|
125
126
|
if (!settings) {
|
|
126
|
-
logger.warn(
|
|
127
|
+
logger.warn(`[${NAME}] Skipping, no settings found ${textureIndex}`);
|
|
127
128
|
continue;
|
|
128
129
|
}
|
|
129
130
|
if (!settings.isValid) {
|
|
130
|
-
logger.warn(
|
|
131
|
+
logger.warn(`[${NAME}] Skipping, settings are invalid ${textureIndex}`);
|
|
131
132
|
continue;
|
|
132
133
|
}
|
|
133
134
|
options.size[0] = settings.maxSize;
|
|
134
135
|
options.size[1] = settings.maxSize;
|
|
135
136
|
const existingExtension = texture.getExtension(NEEDLE_PROGRESSIVE_EXTENSION_NAME);
|
|
136
137
|
if (existingExtension !== undefined && existingExtension !== null) {
|
|
137
|
-
logger.debug(
|
|
138
|
+
logger.debug(`[${NAME}] Skipping, texture is already progressive ${textureIndex}`);
|
|
138
139
|
continue;
|
|
139
140
|
}
|
|
140
141
|
const progressiveExt = doc.createExtension(NEEDLE_progressive);
|
|
141
142
|
if (!progressiveExt || !progressiveExt.create) {
|
|
142
|
-
logger.debug(
|
|
143
|
+
logger.debug(`[${NAME}] Skipping because progressive extension is not available. ${textureIndex}`);
|
|
143
144
|
continue;
|
|
144
145
|
}
|
|
145
|
-
const textureGuid = settings.guid?.length ? settings.guid : generateGuid();
|
|
146
|
+
const textureGuid = settings.guid?.length ? settings.guid : generateGuid(path + "_texture_" + index.toString());
|
|
146
147
|
const outPath = createTextureLod(textureLodBasePath, 0, texture, slots, textureGuid, textureIndex);
|
|
147
148
|
if (!outPath) {
|
|
148
149
|
continue;
|
|
@@ -262,7 +263,7 @@ async function resizeTexture(options, texture, logger, uri, slots) {
|
|
|
262
263
|
const [maxWidth, maxHeight] = options.size;
|
|
263
264
|
const [srcWidth, srcHeight] = texture.getSize();
|
|
264
265
|
if (srcWidth <= maxWidth && srcHeight <= maxHeight) {
|
|
265
|
-
logger.debug(
|
|
266
|
+
logger.debug(`[${NAME}] Skipping, the texture is already smaller than the requested size ${maxWidth}x${maxHeight}}`);
|
|
266
267
|
return;
|
|
267
268
|
}
|
|
268
269
|
if (maxWidth <= 0 || maxHeight <= 0) {
|
|
@@ -298,6 +299,7 @@ export function make_progressive_meshes(filePath, _options, opts) {
|
|
|
298
299
|
console.log("Progressive Mesh Transform: skip lod at " + filePath);
|
|
299
300
|
return Promise.resolve();
|
|
300
301
|
}
|
|
302
|
+
await MeshoptSimplifier.ready;
|
|
301
303
|
const logger = doc.getLogger();
|
|
302
304
|
const filename = path.basename(filePath);
|
|
303
305
|
console.log("> Progressive Mesh Transform: " + filename);
|
|
@@ -326,28 +328,41 @@ export function make_progressive_meshes(filePath, _options, opts) {
|
|
|
326
328
|
/** Map of visited GUID -> extension */
|
|
327
329
|
const visited = new Map();
|
|
328
330
|
const meshes = doc.getRoot().listMeshes();
|
|
329
|
-
await MeshoptSimplifier.ready;
|
|
330
331
|
for (const mesh of meshes) {
|
|
331
|
-
|
|
332
|
-
const
|
|
332
|
+
const meshInformation = getMeshInformation(mesh);
|
|
333
|
+
const { totalVertexCount, maxVertexCount } = meshInformation;
|
|
334
|
+
let settings = mesh.getExtension(NEEDLE_progressive_mesh_settings.EXTENSION_NAME);
|
|
333
335
|
if (!settings) {
|
|
334
|
-
|
|
336
|
+
if (maxVertexCount < 32) {
|
|
337
|
+
logger.debug(`[${NAME}] Skipping, no settings found and no primitive with more than 32 vertices. mesh[${index}] has ${totalVertexCount} vertices (max: ${maxVertexCount})`);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
settings = {
|
|
342
|
+
guid: generateGuid(path + "_mesh_" + index.toString()),
|
|
343
|
+
generateLods: true,
|
|
344
|
+
};
|
|
345
|
+
logger.debug(`[${NAME}] No settings found for mesh[${index}] with ${totalVertexCount} vertices (max: ${maxVertexCount}) - will use default settings (guid: ${settings.guid})`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
else if (settings.generateLods === false) {
|
|
349
|
+
logger.debug(`[${NAME}] progressive mesh disabled for mesh at index ${index++}`);
|
|
335
350
|
continue;
|
|
336
351
|
}
|
|
337
352
|
const progressiveExt = doc.createExtension(NEEDLE_progressive);
|
|
338
353
|
if (!progressiveExt || !progressiveExt.create) {
|
|
339
|
-
logger.debug(
|
|
354
|
+
logger.debug(`[${NAME}] Skipping because progressive extension is not available. ${index++}`);
|
|
340
355
|
continue;
|
|
341
356
|
}
|
|
342
357
|
// check if we have already produced LODs for this mesh
|
|
343
358
|
let needsToGenerateLODs = true;
|
|
344
|
-
const
|
|
359
|
+
const generatedLODInfos = new Array();
|
|
345
360
|
if (visited.has(settings.guid)) {
|
|
346
361
|
// we can not just set the extension here...
|
|
347
|
-
logger.debug(
|
|
362
|
+
logger.debug(`[${NAME}] Mesh has already been processed ${index} - will use previously generated LODs`);
|
|
348
363
|
needsToGenerateLODs = false;
|
|
349
|
-
const existingPaths = visited.get(settings.guid)
|
|
350
|
-
|
|
364
|
+
const existingPaths = visited.get(settings.guid);
|
|
365
|
+
generatedLODInfos.push(...existingPaths.infos);
|
|
351
366
|
}
|
|
352
367
|
/** async simplification tasks... */
|
|
353
368
|
const tasks = new Array();
|
|
@@ -357,10 +372,10 @@ export function make_progressive_meshes(filePath, _options, opts) {
|
|
|
357
372
|
const lod = LODS[level];
|
|
358
373
|
const guid = settings.guid;
|
|
359
374
|
// TODO: check if the LOD level did actually change the mesh - otherwise we can just skip generating lower resolutions
|
|
360
|
-
const lodTask = createMeshLod(doc, outputDirectory, level, mesh, guid, lod, opts);
|
|
375
|
+
const lodTask = createMeshLod(doc, outputDirectory, level, mesh, meshInformation, guid, lod, opts);
|
|
361
376
|
lodTask.then(res => {
|
|
362
377
|
if (res?.success) {
|
|
363
|
-
|
|
378
|
+
generatedLODInfos[level] = res;
|
|
364
379
|
}
|
|
365
380
|
});
|
|
366
381
|
tasks.push(lodTask);
|
|
@@ -370,32 +385,66 @@ export function make_progressive_meshes(filePath, _options, opts) {
|
|
|
370
385
|
tasks.length = 0;
|
|
371
386
|
}
|
|
372
387
|
// compare LOD results and ignore if the mesh did not change
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
388
|
+
// TODO: this should actually check mesh semantics (perhaps hash the mesh using the caching system?)
|
|
389
|
+
let prevSize = -Infinity;
|
|
390
|
+
for (let i = 0; i < generatedLODInfos.length; i++) {
|
|
391
|
+
const lodInfo = generatedLODInfos[i];
|
|
392
|
+
if (!lodInfo) {
|
|
393
|
+
console.log(`[${NAME}] LOD ${i} doesnt exist`);
|
|
378
394
|
continue;
|
|
379
395
|
}
|
|
380
|
-
const fullPath = outputDirectory + path;
|
|
396
|
+
const fullPath = outputDirectory + lodInfo.path;
|
|
381
397
|
if (existsSync(fullPath)) {
|
|
382
398
|
const lastSize = prevSize;
|
|
383
399
|
const sizeOnDisc = statSync(fullPath).size;
|
|
384
|
-
prevSize = sizeOnDisc;
|
|
385
400
|
const diff = lastSize - sizeOnDisc;
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
401
|
+
const level = lodInfo.level;
|
|
402
|
+
let shouldOmitLevel = false;
|
|
403
|
+
// if the mesh size on disc is identical to the last level, we can omit this level
|
|
404
|
+
if (sizeOnDisc <= 0) {
|
|
405
|
+
console.log(`[${NAME}] mesh LOD ${level} size on disc is 0 - level will be omitted (if this mesh is is taken from cache it can mean that the LOD level was omitted before) \"${lodInfo.path}\"`);
|
|
406
|
+
shouldOmitLevel = true;
|
|
407
|
+
}
|
|
408
|
+
else if (diff == 0) {
|
|
409
|
+
console.log(`[${NAME}] mesh LOD ${level} size on disc did not change compared to previous level - level will be omitted \"${lodInfo.path}\"`);
|
|
410
|
+
shouldOmitLevel = true;
|
|
411
|
+
}
|
|
412
|
+
else if (!shouldOmitLevel) {
|
|
413
|
+
// if the mesh vertex count difference to the last valid level is less than 32 vertices or less than 5% of the last level, we can omit this level
|
|
414
|
+
const meshInfo = lodInfo.lodMeshInfo;
|
|
415
|
+
const lastMeshInfo = generatedLODInfos[i - 1]?.lodMeshInfo;
|
|
416
|
+
if (meshInfo && lastMeshInfo) {
|
|
417
|
+
const vertexDifference = lastMeshInfo.totalVertexCount - meshInfo.totalVertexCount;
|
|
418
|
+
const diffPercent = vertexDifference / lastMeshInfo.totalVertexCount;
|
|
419
|
+
if (vertexDifference < 0) {
|
|
420
|
+
console.log(`WARN: [${NAME}] mesh LOD ${level} did increase vertex count (${-vertexDifference.toFixed(0)} vertices, ${(-diffPercent * 100).toFixed(4)}%) - level will be omitted \"${lodInfo.path}\" ${JSON.stringify(lastMeshInfo)} → ${JSON.stringify(meshInfo)}`);
|
|
421
|
+
shouldOmitLevel = true;
|
|
422
|
+
}
|
|
423
|
+
else if (vertexDifference < 32 || diffPercent < 0.05) {
|
|
424
|
+
console.log(`[${NAME}] mesh LOD ${level} change was too low (${-vertexDifference.toFixed(0)} vertices, ${(-diffPercent * 100).toFixed(4)}%) - level will be omitted \"${lodInfo.path}\" ${JSON.stringify(lastMeshInfo)} → ${JSON.stringify(meshInfo)}`);
|
|
425
|
+
shouldOmitLevel = true;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (shouldOmitLevel) {
|
|
429
|
+
// we invalidate the cached entry because the mesh change was too small
|
|
430
|
+
addToCache(lodInfo.cacheKey, new Uint8Array(0));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (shouldOmitLevel) {
|
|
434
|
+
generatedLODInfos.splice(i, 1);
|
|
389
435
|
rmSync(fullPath);
|
|
390
436
|
i--;
|
|
391
437
|
}
|
|
438
|
+
else {
|
|
439
|
+
prevSize = sizeOnDisc;
|
|
440
|
+
}
|
|
392
441
|
}
|
|
393
442
|
}
|
|
394
443
|
let maxDensity = 0;
|
|
395
444
|
for (const prim of mesh.listPrimitives()) {
|
|
396
|
-
const lodsCount =
|
|
445
|
+
const lodsCount = generatedLODInfos.length;
|
|
397
446
|
const lastLod = LODS[lodsCount];
|
|
398
|
-
console.log(
|
|
447
|
+
console.log(`[${NAME}] ${mesh.getName()} LOD ${lodsCount}`);
|
|
399
448
|
const density = calculateMeshDensity(prim);
|
|
400
449
|
maxDensity = Math.max(maxDensity, density);
|
|
401
450
|
const task = simplifyMesh(doc, mesh, prim, lastLod);
|
|
@@ -403,18 +452,24 @@ export function make_progressive_meshes(filePath, _options, opts) {
|
|
|
403
452
|
}
|
|
404
453
|
await Promise.all(tasks);
|
|
405
454
|
tasks.length = 0;
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
model: model,
|
|
416
|
-
paths: generatedLODs,
|
|
455
|
+
const { totalVertexCount: totalVertexCountAfter } = getMeshInformation(mesh);
|
|
456
|
+
if (totalVertexCountAfter <= 0) {
|
|
457
|
+
logger.error(`ERR: [${NAME}] Mesh[${index}] has no vertices after simplification: ${mesh.getName()}`);
|
|
458
|
+
}
|
|
459
|
+
if (generatedLODInfos.length > 0) {
|
|
460
|
+
const model = progressiveExt.create({
|
|
461
|
+
guid: settings.guid,
|
|
462
|
+
uri: generatedLODInfos.map(l => l.path),
|
|
463
|
+
density: maxDensity,
|
|
417
464
|
});
|
|
465
|
+
mesh.setExtension(NEEDLE_PROGRESSIVE_EXTENSION_NAME, model);
|
|
466
|
+
index++;
|
|
467
|
+
if (!visited.has(settings.guid)) {
|
|
468
|
+
visited.set(settings.guid, {
|
|
469
|
+
model: model,
|
|
470
|
+
infos: generatedLODInfos,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
418
473
|
}
|
|
419
474
|
}
|
|
420
475
|
console.log("< Progressive Mesh Transform: finished");
|
|
@@ -427,7 +482,7 @@ export function make_progressive_meshes(filePath, _options, opts) {
|
|
|
427
482
|
* @param guid a unique identifier for the mesh
|
|
428
483
|
* @param error the error to use for simplification. Higher means more simplification
|
|
429
484
|
*/
|
|
430
|
-
async function createMeshLod(doc, basePath, level, mesh, guid, lodLevel, opts) {
|
|
485
|
+
async function createMeshLod(doc, basePath, level, mesh, meshInfo, guid, lodLevel, opts) {
|
|
431
486
|
const fileExtension = ".glb";
|
|
432
487
|
const fileName_withoutExtension = "mesh_lod_" + level + "_" + guid;
|
|
433
488
|
const fileName = fileName_withoutExtension + fileExtension;
|
|
@@ -445,9 +500,9 @@ async function createMeshLod(doc, basePath, level, mesh, guid, lodLevel, opts) {
|
|
|
445
500
|
cacheKey = `mesh_lod_${level}_${hash}`;
|
|
446
501
|
const cachedValue = tryGetFromCache(cacheKey);
|
|
447
502
|
if (cachedValue) {
|
|
448
|
-
console.log(
|
|
503
|
+
console.log(`[${NAME}] LOD ${level} loaded ${(cachedValue.length / 1024).toFixed()} KB from cache for \"${mesh.getName()}\"`);
|
|
449
504
|
writeFileSync(outPath, cachedValue);
|
|
450
|
-
return { path: fileName, success: true };
|
|
505
|
+
return { level, path: fileName, success: true, cacheKey: cacheKey };
|
|
451
506
|
}
|
|
452
507
|
}
|
|
453
508
|
const newDoc = new Document();
|
|
@@ -492,7 +547,6 @@ async function createMeshLod(doc, basePath, level, mesh, guid, lodLevel, opts) {
|
|
|
492
547
|
const newProp = new PropertyClass(newDoc.getGraph());
|
|
493
548
|
const copy = newProp.copy(ext);
|
|
494
549
|
const name = ext.extensionName;
|
|
495
|
-
console.log(`${NAME}: Copy primitive ${name} extension`);
|
|
496
550
|
newPrim.setExtension(name, copy);
|
|
497
551
|
}
|
|
498
552
|
}
|
|
@@ -507,14 +561,11 @@ async function createMeshLod(doc, basePath, level, mesh, guid, lodLevel, opts) {
|
|
|
507
561
|
}
|
|
508
562
|
await Promise.all(tasks);
|
|
509
563
|
tasks.length = 0;
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
// }
|
|
516
|
-
// if (!anyPrimitiveDidChange) return false;
|
|
517
|
-
// }
|
|
564
|
+
const newMeshInformation = getMeshInformation(newMesh);
|
|
565
|
+
if (newMeshInformation.totalVertexCount < 4) {
|
|
566
|
+
console.error(`[${NAME}] mesh LOD ${level} is has less than 4 vertices → skipping`);
|
|
567
|
+
return { level, path: fileName, success: false, cacheKey: cacheKey };
|
|
568
|
+
}
|
|
518
569
|
const progressiveExt = newDoc.createExtension(NEEDLE_progressive);
|
|
519
570
|
if (!progressiveExt || !progressiveExt.create) {
|
|
520
571
|
console.error("Failed to create progressive extension");
|
|
@@ -529,93 +580,20 @@ async function createMeshLod(doc, basePath, level, mesh, guid, lodLevel, opts) {
|
|
|
529
580
|
if (compressionExt) {
|
|
530
581
|
const newExt = newDoc.createExtension(NEEDLE_mesh_compression);
|
|
531
582
|
const newProp = newExt.create(compressionExt.model);
|
|
532
|
-
console.log(
|
|
583
|
+
console.log(`[${NAME}] Pass compression format to mesh LOD ${level} → ${JSON.stringify(compressionExt.model)}`);
|
|
533
584
|
newDoc.getRoot().setExtension(NEEDLE_mesh_compression.EXTENSION_NAME, newProp);
|
|
534
585
|
}
|
|
535
586
|
const newIo = new NodeIO();
|
|
536
587
|
registerExtensions(newIo);
|
|
537
|
-
console.log(
|
|
588
|
+
console.log(`[${NAME}] Write mesh LOD ${level} with: ${JSON.stringify(lodLevel)} to \"${fileName}\"`);
|
|
538
589
|
await newIo.write(outPath, newDoc);
|
|
590
|
+
// const hasChanged = meshInfo.maxVertexCount == newMeshInformation.maxVertexCount && meshInfo.totalVertexCount == newMeshInformation.totalVertexCount;
|
|
539
591
|
// add to cache
|
|
540
592
|
if (cacheKey != undefined) {
|
|
541
593
|
const bytes = readFileSync(outPath);
|
|
542
594
|
addToCache(cacheKey, bytes);
|
|
543
595
|
}
|
|
544
|
-
return { path: fileName, success: true };
|
|
545
|
-
}
|
|
546
|
-
function calculateMeshDensity(prim) {
|
|
547
|
-
const bounds = {
|
|
548
|
-
min: [Infinity, Infinity, Infinity],
|
|
549
|
-
max: [-Infinity, -Infinity, -Infinity],
|
|
550
|
-
};
|
|
551
|
-
const position = prim.getAttribute('POSITION');
|
|
552
|
-
if (!position)
|
|
553
|
-
return -1;
|
|
554
|
-
let tempVec3 = [0, 0, 0];
|
|
555
|
-
for (let i = 0; i < position.getCount(); i++) {
|
|
556
|
-
tempVec3 = position.getElement(i, tempVec3);
|
|
557
|
-
expandBounds(tempVec3, bounds);
|
|
558
|
-
}
|
|
559
|
-
const indices = prim.getIndices();
|
|
560
|
-
let area = 0;
|
|
561
|
-
if (indices) {
|
|
562
|
-
// calculate surface area
|
|
563
|
-
const numIndices = indices.getCount();
|
|
564
|
-
const indexArray = indices.getArray();
|
|
565
|
-
const v0 = [0, 0, 0];
|
|
566
|
-
const v1 = [0, 0, 0];
|
|
567
|
-
const v2 = [0, 0, 0];
|
|
568
|
-
const e1 = [0, 0, 0];
|
|
569
|
-
const e2 = [0, 0, 0];
|
|
570
|
-
const boundSizeX = bounds.max[0] - bounds.min[0];
|
|
571
|
-
const boundSizeY = bounds.max[1] - bounds.min[1];
|
|
572
|
-
const boundSizeZ = bounds.max[2] - bounds.min[2];
|
|
573
|
-
const maxBounds = Math.max(boundSizeX, boundSizeY, boundSizeZ);
|
|
574
|
-
for (let i = 0; i < numIndices; i += 3) {
|
|
575
|
-
const i0 = indexArray[i + 0];
|
|
576
|
-
const i1 = indexArray[i + 1];
|
|
577
|
-
const i2 = indexArray[i + 2];
|
|
578
|
-
position.getElement(i0, v0);
|
|
579
|
-
position.getElement(i1, v1);
|
|
580
|
-
position.getElement(i2, v2);
|
|
581
|
-
e1[0] = v1[0] - v0[0];
|
|
582
|
-
e1[1] = v1[1] - v0[1];
|
|
583
|
-
e1[2] = v1[2] - v0[2];
|
|
584
|
-
e2[0] = v2[0] - v0[0];
|
|
585
|
-
e2[1] = v2[1] - v0[1];
|
|
586
|
-
e2[2] = v2[2] - v0[2];
|
|
587
|
-
// Normalize vertex distance in mesh bounds
|
|
588
|
-
e1[0] /= maxBounds;
|
|
589
|
-
e1[1] /= maxBounds;
|
|
590
|
-
e1[2] /= maxBounds;
|
|
591
|
-
e2[0] /= maxBounds;
|
|
592
|
-
e2[1] /= maxBounds;
|
|
593
|
-
e2[2] /= maxBounds;
|
|
594
|
-
// Cross product to get the area of the triangle
|
|
595
|
-
const cross = [
|
|
596
|
-
e1[1] * e2[2] - e1[2] * e2[1],
|
|
597
|
-
e1[2] * e2[0] - e1[0] * e2[2],
|
|
598
|
-
e1[0] * e2[1] - e1[1] * e2[0]
|
|
599
|
-
];
|
|
600
|
-
// Add the area (half the magnitude of the cross product) to the total
|
|
601
|
-
area += Math.sqrt(cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]) / 2;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
const count = indices.getCount() / 3;
|
|
605
|
-
const density = count / area;
|
|
606
|
-
// const size = [0, 0, 0] as vec3;
|
|
607
|
-
// size[0] = bounds.max[0] - bounds.min[0];
|
|
608
|
-
// size[1] = bounds.max[1] - bounds.min[1];
|
|
609
|
-
// size[2] = bounds.max[2] - bounds.min[2];
|
|
610
|
-
// const volume = size[0] * size[1] * size[2];
|
|
611
|
-
// const density = count / volume;
|
|
612
|
-
return density;
|
|
613
|
-
function expandBounds(point, target) {
|
|
614
|
-
for (let i = 0; i < 3; i++) {
|
|
615
|
-
target.min[i] = Math.min(point[i], target.min[i]);
|
|
616
|
-
target.max[i] = Math.max(point[i], target.max[i]);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
596
|
+
return { level, path: fileName, success: true, lodMeshInfo: newMeshInformation, cacheKey: cacheKey };
|
|
619
597
|
}
|
|
620
598
|
async function simplifyMesh(doc, mesh, prim, lodinfo) {
|
|
621
599
|
const weldOptions = {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function generateGuid(key: string): any;
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Mesh, Primitive } from "@gltf-transform/core";
|
|
2
|
+
export declare type MeshInformation = {
|
|
3
|
+
readonly maxVertexCount: number;
|
|
4
|
+
readonly totalVertexCount: number;
|
|
5
|
+
};
|
|
6
|
+
export declare function getMeshInformation(mesh: Mesh): MeshInformation;
|
|
7
|
+
export declare function calculateMeshDensity(prim: Primitive): number;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export function getMeshInformation(mesh) {
|
|
2
|
+
let maxVertexCount = 0;
|
|
3
|
+
const totalVertexCount = mesh.listPrimitives().reduce((acc, prim) => {
|
|
4
|
+
const vertices = prim.getAttribute('POSITION')?.getCount() || 0;
|
|
5
|
+
maxVertexCount = Math.max(maxVertexCount, vertices);
|
|
6
|
+
return acc + vertices;
|
|
7
|
+
}, 0);
|
|
8
|
+
return {
|
|
9
|
+
maxVertexCount,
|
|
10
|
+
totalVertexCount,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function calculateMeshDensity(prim) {
|
|
14
|
+
const bounds = {
|
|
15
|
+
min: [Infinity, Infinity, Infinity],
|
|
16
|
+
max: [-Infinity, -Infinity, -Infinity],
|
|
17
|
+
};
|
|
18
|
+
const position = prim.getAttribute('POSITION');
|
|
19
|
+
if (!position)
|
|
20
|
+
return -1;
|
|
21
|
+
let tempVec3 = [0, 0, 0];
|
|
22
|
+
for (let i = 0; i < position.getCount(); i++) {
|
|
23
|
+
tempVec3 = position.getElement(i, tempVec3);
|
|
24
|
+
expandBounds(tempVec3, bounds);
|
|
25
|
+
}
|
|
26
|
+
const indices = prim.getIndices();
|
|
27
|
+
let area = 0;
|
|
28
|
+
if (indices) {
|
|
29
|
+
// calculate surface area
|
|
30
|
+
const numIndices = indices.getCount();
|
|
31
|
+
const indexArray = indices.getArray();
|
|
32
|
+
const v0 = [0, 0, 0];
|
|
33
|
+
const v1 = [0, 0, 0];
|
|
34
|
+
const v2 = [0, 0, 0];
|
|
35
|
+
const e1 = [0, 0, 0];
|
|
36
|
+
const e2 = [0, 0, 0];
|
|
37
|
+
const boundSizeX = bounds.max[0] - bounds.min[0];
|
|
38
|
+
const boundSizeY = bounds.max[1] - bounds.min[1];
|
|
39
|
+
const boundSizeZ = bounds.max[2] - bounds.min[2];
|
|
40
|
+
const maxBounds = Math.max(boundSizeX, boundSizeY, boundSizeZ);
|
|
41
|
+
for (let i = 0; i < numIndices; i += 3) {
|
|
42
|
+
const i0 = indexArray[i + 0];
|
|
43
|
+
const i1 = indexArray[i + 1];
|
|
44
|
+
const i2 = indexArray[i + 2];
|
|
45
|
+
position.getElement(i0, v0);
|
|
46
|
+
position.getElement(i1, v1);
|
|
47
|
+
position.getElement(i2, v2);
|
|
48
|
+
e1[0] = v1[0] - v0[0];
|
|
49
|
+
e1[1] = v1[1] - v0[1];
|
|
50
|
+
e1[2] = v1[2] - v0[2];
|
|
51
|
+
e2[0] = v2[0] - v0[0];
|
|
52
|
+
e2[1] = v2[1] - v0[1];
|
|
53
|
+
e2[2] = v2[2] - v0[2];
|
|
54
|
+
// Normalize vertex distance in mesh bounds
|
|
55
|
+
e1[0] /= maxBounds;
|
|
56
|
+
e1[1] /= maxBounds;
|
|
57
|
+
e1[2] /= maxBounds;
|
|
58
|
+
e2[0] /= maxBounds;
|
|
59
|
+
e2[1] /= maxBounds;
|
|
60
|
+
e2[2] /= maxBounds;
|
|
61
|
+
// Cross product to get the area of the triangle
|
|
62
|
+
const cross = [
|
|
63
|
+
e1[1] * e2[2] - e1[2] * e2[1],
|
|
64
|
+
e1[2] * e2[0] - e1[0] * e2[2],
|
|
65
|
+
e1[0] * e2[1] - e1[1] * e2[0]
|
|
66
|
+
];
|
|
67
|
+
// Add the area (half the magnitude of the cross product) to the total
|
|
68
|
+
area += Math.sqrt(cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]) / 2;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const count = indices.getCount() / 3;
|
|
72
|
+
const density = count / area;
|
|
73
|
+
// const size = [0, 0, 0] as vec3;
|
|
74
|
+
// size[0] = bounds.max[0] - bounds.min[0];
|
|
75
|
+
// size[1] = bounds.max[1] - bounds.min[1];
|
|
76
|
+
// size[2] = bounds.max[2] - bounds.min[2];
|
|
77
|
+
// const volume = size[0] * size[1] * size[2];
|
|
78
|
+
// const density = count / volume;
|
|
79
|
+
return density;
|
|
80
|
+
function expandBounds(point, target) {
|
|
81
|
+
for (let i = 0; i < 3; i++) {
|
|
82
|
+
target.min[i] = Math.min(point[i], target.min[i]);
|
|
83
|
+
target.max[i] = Math.max(point[i], target.max[i]);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
package/index.mjs
CHANGED
|
@@ -35,8 +35,6 @@ try {
|
|
|
35
35
|
runWithArgs("node pack-gltf.mjs");
|
|
36
36
|
const durationInMinutes = ((Date.now() - startTime) / 1000 / 60).toFixed(1);
|
|
37
37
|
console.log(`[Needle Build Pipeline] v${getVersion()} — Finished progressive transform and optimization in ${durationInMinutes} minutes.`);
|
|
38
|
-
// small delay to not close the process immediately and give external processes some time to read the output
|
|
39
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
40
38
|
}
|
|
41
39
|
catch (err) {
|
|
42
40
|
console.error("Failed running commandline args:", err.message);
|
package/pack-gltf.mjs
CHANGED
|
@@ -97,7 +97,7 @@ async function packFile(file) {
|
|
|
97
97
|
cacheKey = "packed-" + hash;
|
|
98
98
|
const cached = tryGetFromCache(cacheKey);
|
|
99
99
|
if (cached) {
|
|
100
|
-
console.log(
|
|
100
|
+
console.log(`→ Loaded compressed file from cache: \"${path.basename(file)}\"`);
|
|
101
101
|
writeFileSync(file, cached);
|
|
102
102
|
return;
|
|
103
103
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@needle-tools/gltf-build-pipeline",
|
|
3
|
-
"version": "1.4.1-beta.
|
|
3
|
+
"version": "1.4.1-beta.9",
|
|
4
4
|
"description": "Pipeline and tools for optimizing gltf files using gltf-transform and compression settings within glTF extensions",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"p-limit": "3.1.0",
|
|
32
32
|
"semver": "^7.3.7",
|
|
33
33
|
"sharp": "^0.31.3",
|
|
34
|
-
"tmp": "^0.2.1"
|
|
34
|
+
"tmp": "^0.2.1",
|
|
35
|
+
"uuid": "^9.0.1"
|
|
35
36
|
},
|
|
36
37
|
"watch": {
|
|
37
38
|
"compile": {
|