@needle-tools/gltf-build-pipeline 1.4.1-beta.3 → 1.4.1-beta.5
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,6 +4,13 @@ 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.5] - 2023-03-15
|
|
8
|
+
- add: auto LODs now calculate mesh density
|
|
9
|
+
- change: generate 6 LOD level
|
|
10
|
+
- change: use ratio 0.5 of the previous LOD level
|
|
11
|
+
- change: bump gltf-transform to 3.10.1 to fix simplifier
|
|
12
|
+
- change: add sparse transform to progressive transform
|
|
13
|
+
|
|
7
14
|
## [1.4.1-beta.3] - 2023-03-14
|
|
8
15
|
- fix: wait for mesh simplifier to be ready
|
|
9
16
|
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { Extension, ExtensionProperty, Graph, IProperty, Property, PropertyType, ReaderContext, WriterContext } from "@gltf-transform/core";
|
|
2
2
|
export declare const NEEDLE_PROGRESSIVE_EXTENSION_NAME = "NEEDLE_progressive";
|
|
3
|
-
export declare interface
|
|
3
|
+
export declare interface IProgressiveLoadingInfo {
|
|
4
|
+
/** guid of the asset the LODs belong to */
|
|
4
5
|
guid: string;
|
|
6
|
+
/** paths to the LOD level files */
|
|
5
7
|
uri?: string | Array<string>;
|
|
8
|
+
density?: number;
|
|
6
9
|
}
|
|
7
10
|
export declare function generateGuid(): string;
|
|
8
11
|
export declare class NEEDLE_progressive extends Extension {
|
|
9
12
|
static EXTENSION_NAME: string;
|
|
10
13
|
extensionName: string;
|
|
11
|
-
static create(graph: Graph<Property<IProperty>>, data:
|
|
12
|
-
create(data:
|
|
14
|
+
static create(graph: Graph<Property<IProperty>>, data: IProgressiveLoadingInfo): NEEDLE_progressive_texture_property;
|
|
15
|
+
create(data: IProgressiveLoadingInfo): NEEDLE_progressive_texture_property;
|
|
13
16
|
read(_context: ReaderContext): this;
|
|
14
17
|
write(context: WriterContext): this;
|
|
15
18
|
}
|
|
@@ -19,5 +22,5 @@ export declare class NEEDLE_progressive_texture_property extends ExtensionProper
|
|
|
19
22
|
parentTypes: PropertyType[];
|
|
20
23
|
propertyType: string;
|
|
21
24
|
init(): void;
|
|
22
|
-
model:
|
|
25
|
+
model: IProgressiveLoadingInfo;
|
|
23
26
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { BufferUtils, Document, NodeIO, PropertyType } from '@gltf-transform/core';
|
|
2
|
-
import { SIMPLIFY_DEFAULTS, WELD_DEFAULTS, listTextureSlots } from '@gltf-transform/functions';
|
|
2
|
+
import { SIMPLIFY_DEFAULTS, WELD_DEFAULTS, listTextureSlots, sparse } from '@gltf-transform/functions';
|
|
3
3
|
import { NEEDLE_PROGRESSIVE_EXTENSION_NAME, NEEDLE_progressive, generateGuid } 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
|
-
import { existsSync } from 'fs';
|
|
6
|
+
import { existsSync, rmSync, statSync } from 'fs';
|
|
7
7
|
import { addToCache, getKey, tryGetFromCache } from '../cache/index.js';
|
|
8
8
|
import sharp from "sharp";
|
|
9
9
|
import { foreachGLTF } from '../utils/index.js';
|
|
@@ -67,7 +67,7 @@ async function process(file, opts) {
|
|
|
67
67
|
}
|
|
68
68
|
//@ts-ignore
|
|
69
69
|
document.getLogger().verbosity = 0;
|
|
70
|
-
await document.transform(needle_asset(), make_progressive_textures(file, { size: [256, 256] }, opts), make_progressive_meshes(file, {}, opts));
|
|
70
|
+
await document.transform(needle_asset(), make_progressive_textures(file, { size: [256, 256] }, opts), make_progressive_meshes(file, {}, opts), sparse());
|
|
71
71
|
let filename = file;
|
|
72
72
|
if (opts.debug === true) {
|
|
73
73
|
// get extension
|
|
@@ -303,14 +303,30 @@ export function make_progressive_meshes(filePath, _options, opts) {
|
|
|
303
303
|
console.log("> Progressive Mesh Transform: " + filename);
|
|
304
304
|
let index = 0;
|
|
305
305
|
// the lowest LOD (last entry) will be saved in the original file
|
|
306
|
+
const MAX_LODS = 6;
|
|
306
307
|
const LODS = [
|
|
307
|
-
{
|
|
308
|
-
|
|
309
|
-
|
|
308
|
+
{
|
|
309
|
+
ratio: 1,
|
|
310
|
+
error: 0,
|
|
311
|
+
lockBorder: true
|
|
312
|
+
},
|
|
310
313
|
];
|
|
314
|
+
for (let l = 0; l < MAX_LODS; l++) {
|
|
315
|
+
const t = l / MAX_LODS;
|
|
316
|
+
const latest = LODS[LODS.length - 1];
|
|
317
|
+
const ratio = latest.ratio * .5;
|
|
318
|
+
const error = .05 + t * .5;
|
|
319
|
+
LODS.push({
|
|
320
|
+
ratio: ratio,
|
|
321
|
+
error,
|
|
322
|
+
lockBorder: true,
|
|
323
|
+
// toleranceNormal: 0.1 + t * 0.1,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
311
326
|
/** Map of visited GUID -> extension */
|
|
312
327
|
const visited = new Map();
|
|
313
328
|
const meshes = doc.getRoot().listMeshes();
|
|
329
|
+
await MeshoptSimplifier.ready;
|
|
314
330
|
for (const mesh of meshes) {
|
|
315
331
|
// TODO: skip meshes that are already below a certain threshold (E.g. just 4 vertices)
|
|
316
332
|
const settings = mesh.getExtension(NEEDLE_progressive_mesh_settings.EXTENSION_NAME);
|
|
@@ -333,24 +349,65 @@ export function make_progressive_meshes(filePath, _options, opts) {
|
|
|
333
349
|
const existingPaths = visited.get(settings.guid).paths;
|
|
334
350
|
generatedLODs.push(...existingPaths);
|
|
335
351
|
}
|
|
352
|
+
/** async simplification tasks... */
|
|
353
|
+
const tasks = new Array();
|
|
336
354
|
if (needsToGenerateLODs) {
|
|
337
355
|
// produce LODS, skip the last level because it will be saved in the original file
|
|
338
356
|
for (let level = 0; level < LODS.length - 1; level++) {
|
|
339
357
|
const lod = LODS[level];
|
|
340
|
-
console.log(`${NAME}: Generate LOD ${level}`);
|
|
341
358
|
const guid = settings.guid;
|
|
342
|
-
|
|
343
|
-
|
|
359
|
+
// 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);
|
|
361
|
+
lodTask.then(res => {
|
|
362
|
+
console.log(`${NAME}: ${mesh.getName()} Register LOD ${level}`);
|
|
363
|
+
if (res.success) {
|
|
364
|
+
generatedLODs[level] = res.path;
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
tasks.push(lodTask);
|
|
344
368
|
}
|
|
369
|
+
// wait for all LODs to be generated
|
|
370
|
+
await Promise.all(tasks);
|
|
371
|
+
tasks.length = 0;
|
|
345
372
|
}
|
|
373
|
+
// compare LOD results and ignore if the mesh did not change
|
|
374
|
+
let prevSize = -1;
|
|
375
|
+
for (let i = 1; i < generatedLODs.length; i++) {
|
|
376
|
+
const path = generatedLODs[i];
|
|
377
|
+
if (!path) {
|
|
378
|
+
console.log(`${NAME}: LOD ${i} was not generated for ${mesh.getName()}`);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const fullPath = outputDirectory + path;
|
|
382
|
+
if (existsSync(fullPath)) {
|
|
383
|
+
const lastSize = prevSize;
|
|
384
|
+
const sizeOnDisc = statSync(fullPath).size;
|
|
385
|
+
prevSize = sizeOnDisc;
|
|
386
|
+
const diff = lastSize - sizeOnDisc;
|
|
387
|
+
if (diff < 100) {
|
|
388
|
+
console.log(`${NAME}: LOD ${i} did not change for ${mesh.getName()} - ignore it`);
|
|
389
|
+
generatedLODs.splice(i, 1);
|
|
390
|
+
rmSync(fullPath);
|
|
391
|
+
i--;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
let maxDensity = 0;
|
|
346
396
|
for (const prim of mesh.listPrimitives()) {
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
397
|
+
const lodsCount = generatedLODs.length;
|
|
398
|
+
const lastLod = LODS[lodsCount];
|
|
399
|
+
console.log(`${NAME}: ${mesh.getName()} LOD ${LODS.length - 1}`);
|
|
400
|
+
const density = calculateMeshDensity(prim);
|
|
401
|
+
maxDensity = Math.max(maxDensity, density);
|
|
402
|
+
const task = simplifyMesh(doc, mesh, prim, lastLod);
|
|
403
|
+
tasks.push(task);
|
|
350
404
|
}
|
|
405
|
+
await Promise.all(tasks);
|
|
406
|
+
tasks.length = 0;
|
|
351
407
|
const model = progressiveExt.create({
|
|
352
408
|
guid: settings.guid,
|
|
353
409
|
uri: generatedLODs.map(l => l),
|
|
410
|
+
density: maxDensity,
|
|
354
411
|
});
|
|
355
412
|
mesh.setExtension(NEEDLE_PROGRESSIVE_EXTENSION_NAME, model);
|
|
356
413
|
index++;
|
|
@@ -371,17 +428,20 @@ export function make_progressive_meshes(filePath, _options, opts) {
|
|
|
371
428
|
* @param guid a unique identifier for the mesh
|
|
372
429
|
* @param error the error to use for simplification. Higher means more simplification
|
|
373
430
|
*/
|
|
374
|
-
function createMeshLod(doc, basePath, level, mesh, guid, lodLevel) {
|
|
431
|
+
async function createMeshLod(doc, basePath, level, mesh, guid, lodLevel) {
|
|
375
432
|
const newDoc = new Document();
|
|
376
433
|
const newScene = newDoc.createScene();
|
|
377
434
|
newDoc.createBuffer();
|
|
378
435
|
const newNode = newDoc.createNode();
|
|
379
436
|
newScene.addChild(newNode);
|
|
380
|
-
newNode.setName("
|
|
437
|
+
newNode.setName("LOD " + level + "_" + guid);
|
|
381
438
|
newNode.setTranslation([0, 0, 0]);
|
|
382
439
|
const newMesh = newDoc.createMesh();
|
|
440
|
+
newMesh.setName(mesh.getName() + " LOD " + level);
|
|
383
441
|
newNode.setMesh(newMesh);
|
|
384
442
|
const primitives = mesh.listPrimitives();
|
|
443
|
+
const tasks = new Array();
|
|
444
|
+
// let didRunSimplification = false;
|
|
385
445
|
for (const prim of primitives) {
|
|
386
446
|
const newMat = newDoc.createMaterial();
|
|
387
447
|
const newPrim = newDoc.createPrimitive();
|
|
@@ -415,12 +475,23 @@ function createMeshLod(doc, basePath, level, mesh, guid, lodLevel) {
|
|
|
415
475
|
console.log(`${NAME}: Copy primitive ${name} extension`);
|
|
416
476
|
newPrim.setExtension(name, copy);
|
|
417
477
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
478
|
+
if (lodLevel.error != undefined || lodLevel.ratio != undefined) {
|
|
479
|
+
if (lodLevel.error > 0 || lodLevel.ratio < 1) {
|
|
480
|
+
// didRunSimplification = true;
|
|
481
|
+
const task = simplifyMesh(newDoc, newMesh, newPrim, lodLevel);
|
|
482
|
+
tasks.push(task);
|
|
483
|
+
}
|
|
422
484
|
}
|
|
423
485
|
}
|
|
486
|
+
await Promise.all(tasks);
|
|
487
|
+
// if we run simplify but the mesh didnt change
|
|
488
|
+
// if (didRunSimplification) {
|
|
489
|
+
// let anyPrimitiveDidChange = false;
|
|
490
|
+
// for (const res of results) {
|
|
491
|
+
// if (res) anyPrimitiveDidChange = true;
|
|
492
|
+
// }
|
|
493
|
+
// if (!anyPrimitiveDidChange) return false;
|
|
494
|
+
// }
|
|
424
495
|
const progressiveExt = newDoc.createExtension(NEEDLE_progressive);
|
|
425
496
|
if (!progressiveExt || !progressiveExt.create) {
|
|
426
497
|
console.error("Failed to create progressive extension");
|
|
@@ -445,21 +516,100 @@ function createMeshLod(doc, basePath, level, mesh, guid, lodLevel) {
|
|
|
445
516
|
const filename = name + ext;
|
|
446
517
|
const outName = basePath + filename;
|
|
447
518
|
console.log(`${NAME}: Write mesh LOD ${level} to \"${filename}\" with: ${JSON.stringify(lodLevel)}`);
|
|
448
|
-
newIo.write(outName, newDoc);
|
|
449
|
-
return filename;
|
|
519
|
+
await newIo.write(outName, newDoc);
|
|
520
|
+
return { path: filename, success: true };
|
|
521
|
+
}
|
|
522
|
+
function calculateMeshDensity(prim) {
|
|
523
|
+
const bounds = {
|
|
524
|
+
min: [Infinity, Infinity, Infinity],
|
|
525
|
+
max: [-Infinity, -Infinity, -Infinity],
|
|
526
|
+
};
|
|
527
|
+
const position = prim.getAttribute('POSITION');
|
|
528
|
+
if (!position)
|
|
529
|
+
return -1;
|
|
530
|
+
let tempVec3 = [0, 0, 0];
|
|
531
|
+
for (let i = 0; i < position.getCount(); i++) {
|
|
532
|
+
tempVec3 = position.getElement(i, tempVec3);
|
|
533
|
+
expandBounds(tempVec3, bounds);
|
|
534
|
+
}
|
|
535
|
+
const indices = prim.getIndices();
|
|
536
|
+
let area = 0;
|
|
537
|
+
if (indices) {
|
|
538
|
+
// calculate surface area
|
|
539
|
+
const numIndices = indices.getCount();
|
|
540
|
+
const indexArray = indices.getArray();
|
|
541
|
+
const v0 = [0, 0, 0];
|
|
542
|
+
const v1 = [0, 0, 0];
|
|
543
|
+
const v2 = [0, 0, 0];
|
|
544
|
+
const e1 = [0, 0, 0];
|
|
545
|
+
const e2 = [0, 0, 0];
|
|
546
|
+
const boundSizeX = bounds.max[0] - bounds.min[0];
|
|
547
|
+
const boundSizeY = bounds.max[1] - bounds.min[1];
|
|
548
|
+
const boundSizeZ = bounds.max[2] - bounds.min[2];
|
|
549
|
+
const maxBounds = Math.max(boundSizeX, boundSizeY, boundSizeZ);
|
|
550
|
+
for (let i = 0; i < numIndices; i += 3) {
|
|
551
|
+
const i0 = indexArray[i + 0];
|
|
552
|
+
const i1 = indexArray[i + 1];
|
|
553
|
+
const i2 = indexArray[i + 2];
|
|
554
|
+
position.getElement(i0, v0);
|
|
555
|
+
position.getElement(i1, v1);
|
|
556
|
+
position.getElement(i2, v2);
|
|
557
|
+
e1[0] = v1[0] - v0[0];
|
|
558
|
+
e1[1] = v1[1] - v0[1];
|
|
559
|
+
e1[2] = v1[2] - v0[2];
|
|
560
|
+
e2[0] = v2[0] - v0[0];
|
|
561
|
+
e2[1] = v2[1] - v0[1];
|
|
562
|
+
e2[2] = v2[2] - v0[2];
|
|
563
|
+
// Normalize vertex distance in mesh bounds
|
|
564
|
+
e1[0] /= maxBounds;
|
|
565
|
+
e1[1] /= maxBounds;
|
|
566
|
+
e1[2] /= maxBounds;
|
|
567
|
+
e2[0] /= maxBounds;
|
|
568
|
+
e2[1] /= maxBounds;
|
|
569
|
+
e2[2] /= maxBounds;
|
|
570
|
+
// Cross product to get the area of the triangle
|
|
571
|
+
const cross = [
|
|
572
|
+
e1[1] * e2[2] - e1[2] * e2[1],
|
|
573
|
+
e1[2] * e2[0] - e1[0] * e2[2],
|
|
574
|
+
e1[0] * e2[1] - e1[1] * e2[0]
|
|
575
|
+
];
|
|
576
|
+
// Add the area (half the magnitude of the cross product) to the total
|
|
577
|
+
area += Math.sqrt(cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]) / 2;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const count = indices.getCount() / 3;
|
|
581
|
+
const density = count / area;
|
|
582
|
+
// const size = [0, 0, 0] as vec3;
|
|
583
|
+
// size[0] = bounds.max[0] - bounds.min[0];
|
|
584
|
+
// size[1] = bounds.max[1] - bounds.min[1];
|
|
585
|
+
// size[2] = bounds.max[2] - bounds.min[2];
|
|
586
|
+
// const volume = size[0] * size[1] * size[2];
|
|
587
|
+
// const density = count / volume;
|
|
588
|
+
return density;
|
|
589
|
+
function expandBounds(point, target) {
|
|
590
|
+
for (let i = 0; i < 3; i++) {
|
|
591
|
+
target.min[i] = Math.min(point[i], target.min[i]);
|
|
592
|
+
target.max[i] = Math.max(point[i], target.max[i]);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
450
595
|
}
|
|
451
596
|
async function simplifyMesh(doc, mesh, prim, lodinfo) {
|
|
452
597
|
const weldOptions = {
|
|
453
598
|
...WELD_DEFAULTS,
|
|
454
599
|
overwrite: true,
|
|
600
|
+
...lodinfo,
|
|
455
601
|
};
|
|
456
602
|
const simplifyOptions = {
|
|
457
|
-
...SIMPLIFY_DEFAULTS,
|
|
603
|
+
...SIMPLIFY_DEFAULTS,
|
|
604
|
+
...lodinfo,
|
|
605
|
+
simplifier: MeshoptSimplifier,
|
|
458
606
|
};
|
|
459
|
-
if (lodinfo.error != undefined)
|
|
460
|
-
|
|
461
|
-
if (lodinfo.ratio != undefined)
|
|
462
|
-
|
|
607
|
+
// if (lodinfo.error != undefined)
|
|
608
|
+
// simplifyOptions.error = lodinfo.error;
|
|
609
|
+
// if (lodinfo.ratio != undefined)
|
|
610
|
+
// simplifyOptions.ratio = lodinfo.ratio;
|
|
611
|
+
// if (lodinfo.lockBorder != undefined)
|
|
612
|
+
// simplifyOptions.lockBorder = lodinfo.lockBorder;
|
|
463
613
|
return compressPrimitive(doc, mesh, prim, weldOptions, simplifyOptions);
|
|
464
614
|
}
|
|
465
615
|
function isMeshLOD(filepath) {
|
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.5",
|
|
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",
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
"compile": "tsc || exit 1"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@gltf-transform/core": "^3.10.
|
|
20
|
-
"@gltf-transform/extensions": "^3.10.
|
|
21
|
-
"@gltf-transform/functions": "^3.10.
|
|
19
|
+
"@gltf-transform/core": "^3.10.1",
|
|
20
|
+
"@gltf-transform/extensions": "^3.10.1",
|
|
21
|
+
"@gltf-transform/functions": "^3.10.1",
|
|
22
22
|
"command-exists": "^1.2.9",
|
|
23
23
|
"command-line-args": "^5.2.1",
|
|
24
24
|
"draco3dgltf": "^1.5.3",
|