@needle-tools/gltf-build-pipeline 1.4.1-beta.1 → 1.4.1-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,10 +4,20 @@ 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] - 2023-03-13
7
+ ## [1.4.1-beta.4] - 2023-03-15
8
+ - add: auto LODs now calculate mesh density
9
+ - change: generate 6 LOD level with roughly 0.6 ratio of the previous level
10
+ - change: bump gltf-transform to 3.10.1 to fix simplifier
11
+
12
+ ## [1.4.1-beta.3] - 2023-03-14
13
+ - fix: wait for mesh simplifier to be ready
14
+
15
+ ## [1.4.1-beta.2] - 2023-03-13
8
16
  - change: update gltf-transform to `^3.10.0`
9
17
  - change: progressive mesh transform does now set ratio for LODs (see [simplifier issue 661 on github](https://github.com/zeux/meshoptimizer/issues/661))
10
18
  - fix: issue where progressive meshes were not generated if a mesh was multiple times in the glTF
19
+ - change: simplify to use ratio, fix issue where not all meshes were processed
20
+ - change: auto LOD max error set to 0.1
11
21
 
12
22
  ## [1.4.0-beta] - 2023-03-12
13
23
  - add: progressive mesh transform to automatically generate mesh LOD levels (currently we only generate one low poly version)
@@ -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 IProgressiveTextureInfo {
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: IProgressiveTextureInfo): NEEDLE_progressive_texture_property;
12
- create(data: IProgressiveTextureInfo): NEEDLE_progressive_texture_property;
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: IProgressiveTextureInfo;
25
+ model: IProgressiveLoadingInfo;
23
26
  }
@@ -1,4 +1,4 @@
1
1
  import { Transform, Document, Primitive, Mesh } from "@gltf-transform/core";
2
2
  import { SimplifyOptions, WeldOptions } from "@gltf-transform/functions";
3
3
  export declare const needle_mesh_transform: (_options: SimplifyOptions) => Transform;
4
- export declare function compressPrimitive(document: Document, mesh: Mesh, prim: Primitive, weldOptions: Required<WeldOptions>, meshOptions: Required<SimplifyOptions>): void;
4
+ export declare function compressPrimitive(document: Document, mesh: Mesh, prim: Primitive, weldOptions: Required<WeldOptions>, meshOptions: Required<SimplifyOptions>): Promise<void>;
@@ -88,10 +88,24 @@ async function simplifierTransform(document, _context, _options) {
88
88
  }
89
89
  return true;
90
90
  }
91
- export function compressPrimitive(document, mesh, prim, weldOptions, meshOptions) {
91
+ export async function compressPrimitive(document, mesh, prim, weldOptions, meshOptions) {
92
92
  const logger = document.getLogger();
93
93
  logger.debug(`[${NAME}]: Welding primitive of mesh "${mesh.getName()}" → tolerance: ${weldOptions.tolerance}`);
94
94
  weldPrimitive(document, prim, weldOptions);
95
+ // make sure we wait for the mesh simplifier to be ready
96
+ if (meshOptions.simplifier && meshOptions.simplifier?.ready instanceof Promise) {
97
+ const res = await meshOptions.simplifier?.ready
98
+ .then(() => {
99
+ // ready!
100
+ return true;
101
+ })
102
+ .catch((err) => {
103
+ console.error(`[${NAME}]: Simplifier failed to load...\n${err}`);
104
+ return false;
105
+ });
106
+ if (!res)
107
+ return;
108
+ }
95
109
  logger.debug(`[${NAME}]: Simplifying primitive of mesh "${mesh.getName()}" → ratio: ${meshOptions.ratio}, error: ${meshOptions.error}, lockBorder: ${meshOptions.lockBorder}`);
96
110
  simplifyPrimitive(document, prim, meshOptions);
97
111
  }
@@ -3,7 +3,7 @@ import { SIMPLIFY_DEFAULTS, WELD_DEFAULTS, listTextureSlots } from '@gltf-transf
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';
@@ -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
- { error: 0 },
308
- { ratio: 0.5, error: 1 },
309
- { ratio: 0.25, error: 1 },
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 * .6;
318
+ const error = .1 + t * 0.4;
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
- const lodPath = createMeshLod(doc, outputDirectory, level, mesh, guid, lod);
343
- generatedLODs.push(lodPath);
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 lastLod = LODS[LODS.length - 1];
348
- console.log(`${NAME}: ${filename} LOD ${LODS.length - 1}`);
349
- simplifyMesh(doc, mesh, prim, lastLod);
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("node_" + level + "_" + guid);
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
- if (lodLevel.error != undefined || lodLevel.ratio != undefined) {
420
- for (const prim of newMesh.listPrimitives()) {
421
- simplifyMesh(newDoc, newMesh, prim, lodLevel);
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,22 +516,101 @@ 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
- function simplifyMesh(doc, mesh, prim, lodinfo) {
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, simplifier: MeshoptSimplifier,
603
+ ...SIMPLIFY_DEFAULTS,
604
+ ...lodinfo,
605
+ simplifier: MeshoptSimplifier,
458
606
  };
459
- if (lodinfo.error != undefined)
460
- simplifyOptions.error = lodinfo.error;
461
- if (lodinfo.ratio != undefined)
462
- simplifyOptions.ratio = lodinfo.ratio;
463
- compressPrimitive(doc, mesh, prim, weldOptions, simplifyOptions);
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;
613
+ return compressPrimitive(doc, mesh, prim, weldOptions, simplifyOptions);
464
614
  }
465
615
  function isMeshLOD(filepath) {
466
616
  return filepath.includes("mesh_lod_");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/gltf-build-pipeline",
3
- "version": "1.4.1-beta.1",
3
+ "version": "1.4.1-beta.4",
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.0",
20
- "@gltf-transform/extensions": "^3.10.0",
21
- "@gltf-transform/functions": "^3.10.0",
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",