@needle-tools/gltf-build-pipeline 1.4.1-beta.8 → 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 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.ROOT];
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, uuid } from "@gltf-transform/core";
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
- get isValid(): boolean;
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
- get isValid() {
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, generateGuid } from "../extensions/NEEDLE_progressive/index.js";
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;
@@ -142,7 +143,7 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
142
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;
@@ -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,12 +328,25 @@ 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
- // TODO: skip meshes that are already below a certain threshold (E.g. just 4 vertices)
332
- const settings = mesh.getExtension(NEEDLE_progressive_mesh_settings.EXTENSION_NAME);
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
- logger.debug(`[${NAME}] no ${NEEDLE_progressive_mesh_settings.EXTENSION_NAME} found for mesh at index ${index++}`);
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);
@@ -341,13 +356,13 @@ export function make_progressive_meshes(filePath, _options, opts) {
341
356
  }
342
357
  // check if we have already produced LODs for this mesh
343
358
  let needsToGenerateLODs = true;
344
- const generatedLODs = new Array();
359
+ const generatedLODInfos = new Array();
345
360
  if (visited.has(settings.guid)) {
346
361
  // we can not just set the extension here...
347
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).paths;
350
- generatedLODs.push(...existingPaths);
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
- generatedLODs[level] = res.path;
378
+ generatedLODInfos[level] = res;
364
379
  }
365
380
  });
366
381
  tasks.push(lodTask);
@@ -372,34 +387,64 @@ export function make_progressive_meshes(filePath, _options, opts) {
372
387
  // compare LOD results and ignore if the mesh did not change
373
388
  // TODO: this should actually check mesh semantics (perhaps hash the mesh using the caching system?)
374
389
  let prevSize = -Infinity;
375
- let omittedLevelsIndex = -1;
376
- for (let i = 0; i < generatedLODs.length; i++) {
377
- const filename = generatedLODs[i];
378
- if (!filename) {
390
+ for (let i = 0; i < generatedLODInfos.length; i++) {
391
+ const lodInfo = generatedLODInfos[i];
392
+ if (!lodInfo) {
379
393
  console.log(`[${NAME}] LOD ${i} doesnt exist`);
380
394
  continue;
381
395
  }
382
- const fullPath = outputDirectory + filename;
396
+ const fullPath = outputDirectory + lodInfo.path;
383
397
  if (existsSync(fullPath)) {
384
398
  const lastSize = prevSize;
385
399
  const sizeOnDisc = statSync(fullPath).size;
386
400
  const diff = lastSize - sizeOnDisc;
387
- prevSize = sizeOnDisc;
388
- if (diff == 0) {
389
- if (omittedLevelsIndex == -1)
390
- omittedLevelsIndex = i;
391
- console.log(`[${NAME}] LOD ${omittedLevelsIndex++} did not change mesh (${diff.toFixed(0)}) - level will be omitted \"${filename}\"`);
392
- generatedLODs.splice(i, 1);
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);
393
435
  rmSync(fullPath);
394
436
  i--;
395
437
  }
438
+ else {
439
+ prevSize = sizeOnDisc;
440
+ }
396
441
  }
397
442
  }
398
443
  let maxDensity = 0;
399
444
  for (const prim of mesh.listPrimitives()) {
400
- const lodsCount = generatedLODs.length;
445
+ const lodsCount = generatedLODInfos.length;
401
446
  const lastLod = LODS[lodsCount];
402
- console.log(`[${NAME}] ${mesh.getName()} LOD ${LODS.length - 1}`);
447
+ console.log(`[${NAME}] ${mesh.getName()} LOD ${lodsCount}`);
403
448
  const density = calculateMeshDensity(prim);
404
449
  maxDensity = Math.max(maxDensity, density);
405
450
  const task = simplifyMesh(doc, mesh, prim, lastLod);
@@ -407,18 +452,24 @@ export function make_progressive_meshes(filePath, _options, opts) {
407
452
  }
408
453
  await Promise.all(tasks);
409
454
  tasks.length = 0;
410
- const model = progressiveExt.create({
411
- guid: settings.guid,
412
- uri: generatedLODs.map(l => l),
413
- density: maxDensity,
414
- });
415
- mesh.setExtension(NEEDLE_PROGRESSIVE_EXTENSION_NAME, model);
416
- index++;
417
- if (!visited.has(settings.guid)) {
418
- visited.set(settings.guid, {
419
- model: model,
420
- 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,
421
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
+ }
422
473
  }
423
474
  }
424
475
  console.log("< Progressive Mesh Transform: finished");
@@ -431,7 +482,7 @@ export function make_progressive_meshes(filePath, _options, opts) {
431
482
  * @param guid a unique identifier for the mesh
432
483
  * @param error the error to use for simplification. Higher means more simplification
433
484
  */
434
- async function createMeshLod(doc, basePath, level, mesh, guid, lodLevel, opts) {
485
+ async function createMeshLod(doc, basePath, level, mesh, meshInfo, guid, lodLevel, opts) {
435
486
  const fileExtension = ".glb";
436
487
  const fileName_withoutExtension = "mesh_lod_" + level + "_" + guid;
437
488
  const fileName = fileName_withoutExtension + fileExtension;
@@ -451,7 +502,7 @@ async function createMeshLod(doc, basePath, level, mesh, guid, lodLevel, opts) {
451
502
  if (cachedValue) {
452
503
  console.log(`[${NAME}] LOD ${level} loaded ${(cachedValue.length / 1024).toFixed()} KB from cache for \"${mesh.getName()}\"`);
453
504
  writeFileSync(outPath, cachedValue);
454
- return { path: fileName, success: true };
505
+ return { level, path: fileName, success: true, cacheKey: cacheKey };
455
506
  }
456
507
  }
457
508
  const newDoc = new Document();
@@ -510,14 +561,11 @@ async function createMeshLod(doc, basePath, level, mesh, guid, lodLevel, opts) {
510
561
  }
511
562
  await Promise.all(tasks);
512
563
  tasks.length = 0;
513
- // if we run simplify but the mesh didnt change
514
- // if (didRunSimplification) {
515
- // let anyPrimitiveDidChange = false;
516
- // for (const res of results) {
517
- // if (res) anyPrimitiveDidChange = true;
518
- // }
519
- // if (!anyPrimitiveDidChange) return false;
520
- // }
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
+ }
521
569
  const progressiveExt = newDoc.createExtension(NEEDLE_progressive);
522
570
  if (!progressiveExt || !progressiveExt.create) {
523
571
  console.error("Failed to create progressive extension");
@@ -539,86 +587,13 @@ async function createMeshLod(doc, basePath, level, mesh, guid, lodLevel, opts) {
539
587
  registerExtensions(newIo);
540
588
  console.log(`[${NAME}] Write mesh LOD ${level} with: ${JSON.stringify(lodLevel)} to \"${fileName}\"`);
541
589
  await newIo.write(outPath, newDoc);
590
+ // const hasChanged = meshInfo.maxVertexCount == newMeshInformation.maxVertexCount && meshInfo.totalVertexCount == newMeshInformation.totalVertexCount;
542
591
  // add to cache
543
592
  if (cacheKey != undefined) {
544
593
  const bytes = readFileSync(outPath);
545
594
  addToCache(cacheKey, bytes);
546
595
  }
547
- return { path: fileName, success: true };
548
- }
549
- function calculateMeshDensity(prim) {
550
- const bounds = {
551
- min: [Infinity, Infinity, Infinity],
552
- max: [-Infinity, -Infinity, -Infinity],
553
- };
554
- const position = prim.getAttribute('POSITION');
555
- if (!position)
556
- return -1;
557
- let tempVec3 = [0, 0, 0];
558
- for (let i = 0; i < position.getCount(); i++) {
559
- tempVec3 = position.getElement(i, tempVec3);
560
- expandBounds(tempVec3, bounds);
561
- }
562
- const indices = prim.getIndices();
563
- let area = 0;
564
- if (indices) {
565
- // calculate surface area
566
- const numIndices = indices.getCount();
567
- const indexArray = indices.getArray();
568
- const v0 = [0, 0, 0];
569
- const v1 = [0, 0, 0];
570
- const v2 = [0, 0, 0];
571
- const e1 = [0, 0, 0];
572
- const e2 = [0, 0, 0];
573
- const boundSizeX = bounds.max[0] - bounds.min[0];
574
- const boundSizeY = bounds.max[1] - bounds.min[1];
575
- const boundSizeZ = bounds.max[2] - bounds.min[2];
576
- const maxBounds = Math.max(boundSizeX, boundSizeY, boundSizeZ);
577
- for (let i = 0; i < numIndices; i += 3) {
578
- const i0 = indexArray[i + 0];
579
- const i1 = indexArray[i + 1];
580
- const i2 = indexArray[i + 2];
581
- position.getElement(i0, v0);
582
- position.getElement(i1, v1);
583
- position.getElement(i2, v2);
584
- e1[0] = v1[0] - v0[0];
585
- e1[1] = v1[1] - v0[1];
586
- e1[2] = v1[2] - v0[2];
587
- e2[0] = v2[0] - v0[0];
588
- e2[1] = v2[1] - v0[1];
589
- e2[2] = v2[2] - v0[2];
590
- // Normalize vertex distance in mesh bounds
591
- e1[0] /= maxBounds;
592
- e1[1] /= maxBounds;
593
- e1[2] /= maxBounds;
594
- e2[0] /= maxBounds;
595
- e2[1] /= maxBounds;
596
- e2[2] /= maxBounds;
597
- // Cross product to get the area of the triangle
598
- const cross = [
599
- e1[1] * e2[2] - e1[2] * e2[1],
600
- e1[2] * e2[0] - e1[0] * e2[2],
601
- e1[0] * e2[1] - e1[1] * e2[0]
602
- ];
603
- // Add the area (half the magnitude of the cross product) to the total
604
- area += Math.sqrt(cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]) / 2;
605
- }
606
- }
607
- const count = indices.getCount() / 3;
608
- const density = count / area;
609
- // const size = [0, 0, 0] as vec3;
610
- // size[0] = bounds.max[0] - bounds.min[0];
611
- // size[1] = bounds.max[1] - bounds.min[1];
612
- // size[2] = bounds.max[2] - bounds.min[2];
613
- // const volume = size[0] * size[1] * size[2];
614
- // const density = count / volume;
615
- return density;
616
- function expandBounds(point, target) {
617
- for (let i = 0; i < 3; i++) {
618
- target.min[i] = Math.min(point[i], target.min[i]);
619
- target.max[i] = Math.max(point[i], target.max[i]);
620
- }
621
- }
596
+ return { level, path: fileName, success: true, lodMeshInfo: newMeshInformation, cacheKey: cacheKey };
622
597
  }
623
598
  async function simplifyMesh(doc, mesh, prim, lodinfo) {
624
599
  const weldOptions = {
@@ -0,0 +1 @@
1
+ export declare function generateGuid(key: string): any;
@@ -0,0 +1,5 @@
1
+ import { v5 as uuidv5 } from 'uuid';
2
+ const NEEDLE_BUILD_PIPELINE_NAMESPACE = '208dcaa6-1398-4d42-a625-8f5fc1cc1ccf';
3
+ export function generateGuid(key) {
4
+ return uuidv5(key, NEEDLE_BUILD_PIPELINE_NAMESPACE);
5
+ }
@@ -1,2 +1,4 @@
1
1
  export * from "./fileutils.js";
2
2
  export * from "./version.js";
3
+ export * from "./mesh.js";
4
+ export * from "./guid.js";
@@ -1,2 +1,4 @@
1
1
  export * from "./fileutils.js";
2
2
  export * from "./version.js";
3
+ export * from "./mesh.js";
4
+ export * from "./guid.js";
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/gltf-build-pipeline",
3
- "version": "1.4.1-beta.8",
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": {