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

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,29 @@ 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] - 2023-04-02
8
+ - release
9
+
10
+ ## [1.4.1-beta.15] - 2023-04-02
11
+ - fix: issue where discarding all LODs might result in broken mesh due to simplify having modified the original mesh
12
+ - fix: don't produce LODs if GLB/glTF was produced with old generator
13
+
14
+ ## [1.4.1-beta.13] - 2023-03-26
15
+ - change: passthrough `NEEDLE_editor` & `NEEDLE_gltf_dependencies` extensions
16
+
17
+ ## [1.4.1-beta.12] - 2023-03-23
18
+ - add: NEEDLE_progressive LODs can now contain more info per texture or mesh. Meshes contain vertex- and index counts
19
+ - change: the cache can now store and retrieve meta information
20
+
21
+ ## [1.4.1-beta.11] - 2023-03-20
22
+ - add: NEEDLE_progressive LOD meshes morph targets data
23
+ - fix: NEEDLE_progressive should check if simplifcation did result in improved meshes (> 32 verts removed && > 5 % improvement) and otherwise discard LODs (and the whole extension if no improvement was made)
24
+ - change: Unity NEEDLE_progressive logs
25
+
26
+ ## [1.4.1-beta.10] - 2023-03-19
27
+ - change: NEEDLE_progressive calculates density per mesh level
28
+ - change: NEEDLE_progressive `uri` field is removed and added `lod` array of objects
29
+
7
30
  ## [1.4.1-beta.8] - 2023-03-18
8
31
  - change: if an asset doesnt have a guid we use a uuid package now
9
32
  - change: ignore mesh LODs with vertex count < 4
@@ -1,10 +1,47 @@
1
1
  import { ExtensibleProperty } from "@gltf-transform/core";
2
2
  /**
3
- * Get the available space on the cache directory in bytes.
3
+ * Clear the cache.
4
+ * This will remove all files in the cache directory.
4
5
  */
5
6
  export declare function clearCache(): void;
7
+ /**
8
+ * Ensure that the cache size does not exceed the given maximum size in MB.
9
+ * @param maxSizeMB The maximum size in MB.
10
+ */
6
11
  export declare function ensureCacheSize(maxSizeMB: number): void;
12
+ /**
13
+ * Add data to the cache.
14
+ * @param key The key to store the data under.
15
+ * @param bytes The data to store as a Uint8Array.
16
+ */
7
17
  export declare function addToCache(key: string, bytes: Uint8Array): void;
18
+ /**
19
+ * Try to get the data from the cache.
20
+ * @param key The key to look up.
21
+ * @returns The data as a Uint8Array or null if not found.
22
+ * @throws If the cache file is corrupted.
23
+ */
8
24
  export declare function tryGetFromCache(key: string): Uint8Array | null;
25
+ /** Add meta data to the cache.
26
+ * @param key The key to store the meta data for.
27
+ * @param meta The meta data object to store. It will be serialized to JSON and stored in a file with the key as the name with the extension ".meta".
28
+ */
29
+ export declare function addMetaToCache(key: string, meta: object): void;
30
+ /** Try to get the meta data from the cache.
31
+ * @param key The key to look up.
32
+ * @param ignoreVersion If true, the version check is skipped. If false, the version must match the current build pipeline version.
33
+ * @returns The meta data object or null if not found.
34
+ */
35
+ export declare function tryGetMetaFromCache(key: string, ignoreVersion?: boolean): object | null;
36
+ /**
37
+ * Get the cache key for a property.
38
+ * @param property The property to get the key for.
39
+ * @returns The key as a string.
40
+ */
9
41
  export declare function getKey(property: ExtensibleProperty): string;
42
+ /**
43
+ * Get the hash for a property or an object.
44
+ * @param property The property to get the hash for.
45
+ * @returns The hash as a number.
46
+ */
10
47
  export declare function getHash(property: ExtensibleProperty | object): number;
@@ -2,6 +2,7 @@ import { ExtensibleProperty, Texture, Primitive, Accessor, Mesh } from "@gltf-tr
2
2
  import tmp from 'tmp';
3
3
  import fs from 'fs';
4
4
  import path from 'path';
5
+ import { getVersion } from "../utils/version.js";
5
6
  // import * as CHECKDISCSPACE from 'check-disk-space'
6
7
  /**
7
8
  * Get the available space on the cache directory in bytes.
@@ -17,11 +18,20 @@ import path from 'path';
17
18
  // }
18
19
  // return 0;
19
20
  // }
21
+ const buildPipelineVersion = getVersion(false);
22
+ /**
23
+ * Clear the cache.
24
+ * This will remove all files in the cache directory.
25
+ */
20
26
  export function clearCache() {
21
27
  const dir = getCacheDirectory();
22
28
  fs.rmSync(dir, { recursive: true });
23
29
  console.log(`Cleared cache at ${dir}`);
24
30
  }
31
+ /**
32
+ * Ensure that the cache size does not exceed the given maximum size in MB.
33
+ * @param maxSizeMB The maximum size in MB.
34
+ */
25
35
  export function ensureCacheSize(maxSizeMB) {
26
36
  const dir = getCacheDirectory();
27
37
  let totalSize = 0;
@@ -50,12 +60,23 @@ export function ensureCacheSize(maxSizeMB) {
50
60
  }
51
61
  console.log(`Cache size reduced to ${(totalSize / (1024 * 1024)).toFixed(1)} MB`);
52
62
  }
63
+ /**
64
+ * Add data to the cache.
65
+ * @param key The key to store the data under.
66
+ * @param bytes The data to store as a Uint8Array.
67
+ */
53
68
  export function addToCache(key, bytes) {
54
69
  const dir = getCacheDirectory();
55
70
  const path = `${dir}/${key}.bin`;
56
71
  console.log(`Added ${(bytes.byteLength / 1024).toFixed(1)} KB to cache at \"${path}\"`);
57
72
  fs.writeFileSync(path, bytes);
58
73
  }
74
+ /**
75
+ * Try to get the data from the cache.
76
+ * @param key The key to look up.
77
+ * @returns The data as a Uint8Array or null if not found.
78
+ * @throws If the cache file is corrupted.
79
+ */
59
80
  export function tryGetFromCache(key) {
60
81
  const dir = getCacheDirectory();
61
82
  const path = `${dir}/${key}.bin`;
@@ -65,6 +86,50 @@ export function tryGetFromCache(key) {
65
86
  }
66
87
  return null;
67
88
  }
89
+ /** Add meta data to the cache.
90
+ * @param key The key to store the meta data for.
91
+ * @param meta The meta data object to store. It will be serialized to JSON and stored in a file with the key as the name with the extension ".meta".
92
+ */
93
+ export function addMetaToCache(key, meta) {
94
+ const dir = getCacheDirectory();
95
+ const metaPath = `${dir}/${key}.meta`;
96
+ meta['needle-build-pipeline:version'] = buildPipelineVersion;
97
+ fs.writeFileSync(metaPath, JSON.stringify(meta));
98
+ }
99
+ /** Try to get the meta data from the cache.
100
+ * @param key The key to look up.
101
+ * @param ignoreVersion If true, the version check is skipped. If false, the version must match the current build pipeline version.
102
+ * @returns The meta data object or null if not found.
103
+ */
104
+ export function tryGetMetaFromCache(key, ignoreVersion = false) {
105
+ try {
106
+ const dir = getCacheDirectory();
107
+ const path = `${dir}/${key}.meta`;
108
+ if (fs.existsSync(path)) {
109
+ const bytes = fs.readFileSync(path);
110
+ if (bytes?.length > 0) {
111
+ const text = bytes.toString('utf-8');
112
+ if (text) {
113
+ const obj = JSON.parse(text);
114
+ if (ignoreVersion)
115
+ return obj;
116
+ if (obj['needle-build-pipeline:version'] === buildPipelineVersion)
117
+ return obj;
118
+ }
119
+ }
120
+ }
121
+ return null;
122
+ }
123
+ catch (e) {
124
+ console.error(`Error reading cache meta: ${e.message}`);
125
+ return null;
126
+ }
127
+ }
128
+ /**
129
+ * Get the cache key for a property.
130
+ * @param property The property to get the key for.
131
+ * @returns The key as a string.
132
+ */
68
133
  export function getKey(property) {
69
134
  let prefix = "prop";
70
135
  if (property instanceof Texture) {
@@ -74,6 +139,11 @@ export function getKey(property) {
74
139
  const key = `${prefix}-${hash}`;
75
140
  return key;
76
141
  }
142
+ /**
143
+ * Get the hash for a property or an object.
144
+ * @param property The property to get the hash for.
145
+ * @returns The hash as a number.
146
+ */
77
147
  export function getHash(property) {
78
148
  let hash = 0;
79
149
  if (property instanceof ExtensibleProperty) {
@@ -20,5 +20,6 @@ export interface IExtensionWriter {
20
20
  assignTo?(obj: object): any;
21
21
  write(context: WriterContext, prop: IExtensibleProperty): void;
22
22
  }
23
+ export declare let currentGenerator: string;
23
24
  export declare function createOpaqueExtension(name: string, types: PropertyType | PropertyType[], opts?: Options): IOpaqueExtension;
24
25
  export {};
@@ -4,6 +4,7 @@ const ALL_PROPERTY_TYPES = [];
4
4
  for (const key in PropertyType) {
5
5
  ALL_PROPERTY_TYPES.push(PropertyType[key]);
6
6
  }
7
+ export let currentGenerator = "";
7
8
  // an opaque extension can be used to pass data through the gltf transform process
8
9
  // it should also be able to re-write/fix json pointers after the transformation which might have shuffled them around
9
10
  export function createOpaqueExtension(name, types, opts) {
@@ -18,6 +19,7 @@ export function createOpaqueExtension(name, types, opts) {
18
19
  // private roots: Array<OpaqueProperty> = [];
19
20
  jsonPointerHandler;
20
21
  read(context) {
22
+ currentGenerator = context.jsonDoc.json.asset?.generator;
21
23
  if (debugLog)
22
24
  console.log("READ " + EXTENSION_NAME);
23
25
  this.jsonPointerHandler = new JsonPointerHandler(this.document, EXTENSION_NAME);
@@ -15,4 +15,6 @@ export const ALL_EXTENSIONS = [
15
15
  createOpaqueExtension("NEEDLE_render_objects", PropertyType.ROOT, { ...opaqueOptions }),
16
16
  createOpaqueExtension("NEEDLE_progressive", [PropertyType.TEXTURE, PropertyType.MATERIAL, PropertyType.MESH], { ...opaqueOptions }),
17
17
  createOpaqueExtension("EXT_texture_exr", [PropertyType.TEXTURE], { ...opaqueOptions }),
18
+ createOpaqueExtension("NEEDLE_editor", [PropertyType.NODE, PropertyType.MATERIAL], { ...opaqueOptions }),
19
+ createOpaqueExtension("NEEDLE_gltf_dependencies", [PropertyType.ROOT], { ...opaqueOptions }),
18
20
  ];
@@ -1,25 +1,39 @@
1
- import { Extension, ExtensionProperty, Graph, IProperty, Property, PropertyType, ReaderContext, WriterContext } from "@gltf-transform/core";
1
+ import { Extension, ExtensionProperty, PropertyType, ReaderContext, WriterContext } from "@gltf-transform/core";
2
2
  export declare const NEEDLE_PROGRESSIVE_EXTENSION_NAME = "NEEDLE_progressive";
3
- export declare interface IProgressiveLoadingInfo {
3
+ export type ProgressiveLoadingInfo = {
4
4
  /** guid of the asset the LODs belong to */
5
5
  guid: string;
6
- /** paths to the LOD level files */
7
- uri?: string | Array<string>;
8
- density?: number;
9
- }
6
+ /** lod infos and paths */
7
+ lods?: Array<LodInfo>;
8
+ };
9
+ type LodInfo = {
10
+ path: string;
11
+ };
12
+ export type ProgressiveTextureModel = ProgressiveLoadingInfo & {
13
+ lods?: Array<LodInfo & {}>;
14
+ };
15
+ export type ProgressiveMeshModel = ProgressiveLoadingInfo & {
16
+ density: number;
17
+ lods?: Array<LodInfo & {
18
+ density: number;
19
+ vertexCount: number;
20
+ indexCount: number;
21
+ }>;
22
+ };
10
23
  export declare class NEEDLE_progressive extends Extension {
11
24
  static EXTENSION_NAME: string;
12
25
  extensionName: string;
13
- static create(graph: Graph<Property<IProperty>>, data: IProgressiveLoadingInfo): NEEDLE_progressive_texture_property;
14
- create(data: IProgressiveLoadingInfo): NEEDLE_progressive_texture_property;
26
+ createTexture(data: ProgressiveTextureModel): NEEDLE_progressive_property;
27
+ createMesh(data: ProgressiveMeshModel): NEEDLE_progressive_property;
15
28
  read(_context: ReaderContext): this;
16
29
  write(context: WriterContext): this;
17
30
  }
18
- export declare class NEEDLE_progressive_texture_property extends ExtensionProperty {
31
+ export declare class NEEDLE_progressive_property extends ExtensionProperty {
19
32
  static EXTENSION_NAME: string;
20
33
  readonly extensionName: string;
21
34
  parentTypes: PropertyType[];
22
35
  propertyType: string;
23
36
  init(): void;
24
- model: IProgressiveLoadingInfo;
37
+ model: ProgressiveLoadingInfo;
25
38
  }
39
+ export {};
@@ -4,13 +4,15 @@ export const NEEDLE_PROGRESSIVE_EXTENSION_NAME = "NEEDLE_progressive";
4
4
  export class NEEDLE_progressive extends Extension {
5
5
  static EXTENSION_NAME = NEEDLE_PROGRESSIVE_EXTENSION_NAME;
6
6
  extensionName = NEEDLE_PROGRESSIVE_EXTENSION_NAME;
7
- static create(graph, data) {
8
- const ext = new NEEDLE_progressive_texture_property(graph);
7
+ createTexture(data) {
8
+ const ext = new NEEDLE_progressive_property(this.document.getGraph());
9
9
  ext.model = data;
10
10
  return ext;
11
11
  }
12
- create(data) {
13
- return NEEDLE_progressive.create(this.document.getGraph(), data);
12
+ createMesh(data) {
13
+ const ext = new NEEDLE_progressive_property(this.document.getGraph());
14
+ ext.model = data;
15
+ return ext;
14
16
  }
15
17
  read(_context) {
16
18
  // console.log("READ " + NEEDLE_PROGRESSIVE_TEXTURE_EXTENSION_NAME);
@@ -57,7 +59,7 @@ export class NEEDLE_progressive extends Extension {
57
59
  return this;
58
60
  }
59
61
  }
60
- export class NEEDLE_progressive_texture_property extends ExtensionProperty {
62
+ export class NEEDLE_progressive_property extends ExtensionProperty {
61
63
  static EXTENSION_NAME = NEEDLE_PROGRESSIVE_EXTENSION_NAME;
62
64
  extensionName = NEEDLE_PROGRESSIVE_EXTENSION_NAME;
63
65
  parentTypes = [PropertyType.TEXTURE, PropertyType.MESH];
@@ -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>): Promise<void>;
4
+ export declare function compressPrimitive(document: Document, mesh: Mesh, prim: Primitive, index: number, weldOptions: Required<WeldOptions>, meshOptions: Required<SimplifyOptions>): Promise<Primitive>;
@@ -50,7 +50,9 @@ async function simplifierTransform(document, _context, _options) {
50
50
  overwrite: true,
51
51
  };
52
52
  for (const mesh of document.getRoot().listMeshes()) {
53
+ let i = 0;
53
54
  for (const prim of mesh.listPrimitives()) {
55
+ const index = i++;
54
56
  // TODO: mesh compression caching
55
57
  // const key = getKey(prim);
56
58
  // const cached = tryGetFromCache(key);
@@ -84,14 +86,14 @@ async function simplifierTransform(document, _context, _options) {
84
86
  logger.warn(`[${NAME}]: Skipping primitive of mesh "${mesh.getName()}": Requires TRIANGLES draw mode.`);
85
87
  continue;
86
88
  }
87
- compressPrimitive(document, mesh, prim, weldOptions, simplifyOpts);
89
+ compressPrimitive(document, mesh, prim, index, weldOptions, simplifyOpts);
88
90
  }
89
91
  }
90
92
  return true;
91
93
  }
92
- export async function compressPrimitive(document, mesh, prim, weldOptions, meshOptions) {
94
+ export async function compressPrimitive(document, mesh, prim, index, weldOptions, meshOptions) {
93
95
  const logger = document.getLogger();
94
- logger.debug(`[${NAME}]: Welding primitive of mesh "${mesh.getName()}" → tolerance: ${weldOptions.tolerance}`);
96
+ logger.debug(`weld: primitive[${index}] of mesh "${mesh.getName()}" → tolerance: ${weldOptions.tolerance}`);
95
97
  weldPrimitive(document, prim, weldOptions);
96
98
  // make sure we wait for the mesh simplifier to be ready
97
99
  if (meshOptions.simplifier && meshOptions.simplifier?.ready instanceof Promise) {
@@ -107,8 +109,8 @@ export async function compressPrimitive(document, mesh, prim, weldOptions, meshO
107
109
  if (!res)
108
110
  return;
109
111
  }
110
- logger.debug(`[${NAME}]: Simplifying primitive of mesh "${mesh.getName()}" → ratio: ${meshOptions.ratio}, error: ${meshOptions.error}, lockBorder: ${meshOptions.lockBorder}`);
111
- simplifyPrimitive(document, prim, meshOptions);
112
+ logger.debug(`simplify: primitive[${index}] of mesh "${mesh.getName()}" → ratio: ${meshOptions.ratio}, error: ${meshOptions.error}, lockBorder: ${meshOptions.lockBorder}`);
113
+ return simplifyPrimitive(document, prim, meshOptions);
112
114
  }
113
115
  // function updatePrimitive(prim: Primitive, simplifier: any, positionArray, dstIndicesArray) {
114
116
  // const position = prim.getAttribute('POSITION')!;
@@ -1,10 +1,10 @@
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
3
  import { NEEDLE_PROGRESSIVE_EXTENSION_NAME, NEEDLE_progressive } from "../extensions/NEEDLE_progressive/index.js";
4
- import { createOpaqueExtension, NEEDLE_compression_texture, NEEDLE_mesh_compression, NEEDLE_progressive_mesh_settings, NEEDLE_progressive_texture_settings, registerExtensions as registerDefaultExtensions } from '../extensions/index.js';
4
+ import { createOpaqueExtension, currentGenerator, 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
- import { addToCache, getHash, getKey, tryGetFromCache } from '../cache/index.js';
7
+ import { addMetaToCache, addToCache, getHash, getKey, tryGetFromCache, tryGetMetaFromCache } from '../cache/index.js';
8
8
  import sharp from "sharp";
9
9
  import { foreachGLTF, generateGuid } from '../utils/index.js';
10
10
  import { needle_asset } from './needle_asset.js';
@@ -139,7 +139,7 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
139
139
  continue;
140
140
  }
141
141
  const progressiveExt = doc.createExtension(NEEDLE_progressive);
142
- if (!progressiveExt || !progressiveExt.create) {
142
+ if (!progressiveExt || !progressiveExt.createTexture) {
143
143
  logger.debug(`[${NAME}] Skipping because progressive extension is not available. ${textureIndex}`);
144
144
  continue;
145
145
  }
@@ -150,9 +150,13 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
150
150
  }
151
151
  const relName = path.relative(directory, outPath);
152
152
  // TODO: this path should be added to the NEEDLE dependencies extension
153
- texture.setExtension(NEEDLE_PROGRESSIVE_EXTENSION_NAME, progressiveExt.create({
153
+ texture.setExtension(NEEDLE_PROGRESSIVE_EXTENSION_NAME, progressiveExt.createTexture({
154
154
  guid: textureGuid,
155
- uri: relName,
155
+ lods: [
156
+ {
157
+ path: relName,
158
+ }
159
+ ],
156
160
  }));
157
161
  const key = getKey(texture);
158
162
  if (opts.useCache) {
@@ -227,7 +231,7 @@ function createTextureLod(basePath, level, texture, slots, guid, textureIndex) {
227
231
  }
228
232
  // add progressive texture extension
229
233
  const progressiveExt = newDoc.createExtension(NEEDLE_progressive);
230
- newTexture.setExtension(NEEDLE_PROGRESSIVE_EXTENSION_NAME, progressiveExt.create({
234
+ newTexture.setExtension(NEEDLE_PROGRESSIVE_EXTENSION_NAME, progressiveExt.createTexture({
231
235
  guid: guid,
232
236
  }));
233
237
  const newIo = new NodeIO();
@@ -295,6 +299,12 @@ export function make_progressive_meshes(filePath, _options, opts) {
295
299
  // const fileNameWithoutExtension = path.basename(filePath, path.extname(filePath));
296
300
  const outputDirectory = directory + "/";
297
301
  return createTransform("PROGRESSIVE_MESHES", async (doc, _context) => {
302
+ // this is to ensure we're not creating progressive meshes for engine versions that don't support loading them yet
303
+ // the generator should include the version or the app that produced the GLB too (needle engine repo: 2c00d0e304e205cf9d4ac0499faabb92a706a5c4)
304
+ if (currentGenerator == "Needle Engine") {
305
+ console.log("WARN: Skip creating mesh LODs - Unsupported generator found... Please update your editor integration");
306
+ return;
307
+ }
298
308
  if (isMeshLOD(filePath)) {
299
309
  console.log("Progressive Mesh Transform: skip lod at " + filePath);
300
310
  return Promise.resolve();
@@ -302,7 +312,7 @@ export function make_progressive_meshes(filePath, _options, opts) {
302
312
  await MeshoptSimplifier.ready;
303
313
  const logger = doc.getLogger();
304
314
  const filename = path.basename(filePath);
305
- console.log("> Progressive Mesh Transform: " + filename);
315
+ console.log(`[${NAME}]: Begin generating progressive meshes for \"${filename}\"`);
306
316
  let index = 0;
307
317
  // the lowest LOD (last entry) will be saved in the original file
308
318
  const MAX_LODS = 6;
@@ -327,32 +337,38 @@ export function make_progressive_meshes(filePath, _options, opts) {
327
337
  }
328
338
  /** Map of visited GUID -> extension */
329
339
  const visited = new Map();
330
- const meshes = doc.getRoot().listMeshes();
340
+ const root = doc.getRoot();
341
+ const meshes = root.listMeshes();
331
342
  for (const mesh of meshes) {
332
- const meshInformation = getMeshInformation(mesh);
333
- const { totalVertexCount, maxVertexCount } = meshInformation;
343
+ await processMesh(mesh);
344
+ }
345
+ console.log(`[${NAME}]: Finished generating progressive meshes for \"${filename}\"`);
346
+ async function processMesh(mesh) {
347
+ /** The size and vertex count of the original mesh */
348
+ const originalMeshInformation = getMeshInformation(mesh);
349
+ const { totalVertexCount: originalTotalVertexCount, maxVertexCount: originalMaxVertexCount } = originalMeshInformation;
334
350
  let settings = mesh.getExtension(NEEDLE_progressive_mesh_settings.EXTENSION_NAME);
335
351
  if (!settings) {
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;
352
+ if (originalMaxVertexCount < 32) {
353
+ logger.debug(`[${NAME}] Skipping, no settings found and no primitive with more than 32 vertices. mesh[${index}] has ${originalTotalVertexCount} vertices (max: ${originalMaxVertexCount})`);
354
+ return;
339
355
  }
340
356
  else {
341
357
  settings = {
342
358
  guid: generateGuid(path + "_mesh_" + index.toString()),
343
359
  generateLods: true,
344
360
  };
345
- logger.debug(`[${NAME}] No settings found for mesh[${index}] with ${totalVertexCount} vertices (max: ${maxVertexCount}) - will use default settings (guid: ${settings.guid})`);
361
+ logger.debug(`[${NAME}] No settings found for mesh[${index}] with ${originalTotalVertexCount} vertices (max: ${originalMaxVertexCount}) - will use default settings (guid: ${settings.guid})`);
346
362
  }
347
363
  }
348
364
  else if (settings.generateLods === false) {
349
365
  logger.debug(`[${NAME}] progressive mesh disabled for mesh at index ${index++}`);
350
- continue;
366
+ return;
351
367
  }
352
368
  const progressiveExt = doc.createExtension(NEEDLE_progressive);
353
- if (!progressiveExt || !progressiveExt.create) {
369
+ if (!progressiveExt || !progressiveExt.createMesh) {
354
370
  logger.debug(`[${NAME}] Skipping because progressive extension is not available. ${index++}`);
355
- continue;
371
+ return;
356
372
  }
357
373
  // check if we have already produced LODs for this mesh
358
374
  let needsToGenerateLODs = true;
@@ -367,12 +383,13 @@ export function make_progressive_meshes(filePath, _options, opts) {
367
383
  /** async simplification tasks... */
368
384
  const tasks = new Array();
369
385
  if (needsToGenerateLODs) {
386
+ console.log(`[${NAME}] Begin generating mesh LODs for \"${mesh.getName()}\"`);
370
387
  // produce LODS, skip the last level because it will be saved in the original file
371
388
  for (let level = 0; level < LODS.length - 1; level++) {
372
389
  const lod = LODS[level];
373
390
  const guid = settings.guid;
374
391
  // TODO: check if the LOD level did actually change the mesh - otherwise we can just skip generating lower resolutions
375
- const lodTask = createMeshLod(doc, outputDirectory, level, mesh, meshInformation, guid, lod, opts);
392
+ const lodTask = createMeshLod(doc, outputDirectory, level, mesh, originalMeshInformation, guid, lod, opts);
376
393
  lodTask.then(res => {
377
394
  if (res?.success) {
378
395
  generatedLODInfos[level] = res;
@@ -402,11 +419,11 @@ export function make_progressive_meshes(filePath, _options, opts) {
402
419
  let shouldOmitLevel = false;
403
420
  // if the mesh size on disc is identical to the last level, we can omit this level
404
421
  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}\"`);
422
+ console.log(`[${NAME}] Flag 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). \"${mesh.getName()}\" at \"${lodInfo.path}\"`);
406
423
  shouldOmitLevel = true;
407
424
  }
408
425
  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}\"`);
426
+ console.log(`[${NAME}] Flag mesh LOD ${level}: size on disc did not change compared to previous level → level will be omitted. \"${mesh.getName()}\" at \"${lodInfo.path}\"`);
410
427
  shouldOmitLevel = true;
411
428
  }
412
429
  else if (!shouldOmitLevel) {
@@ -415,17 +432,17 @@ export function make_progressive_meshes(filePath, _options, opts) {
415
432
  const lastMeshInfo = generatedLODInfos[i - 1]?.lodMeshInfo;
416
433
  if (meshInfo && lastMeshInfo) {
417
434
  const vertexDifference = lastMeshInfo.totalVertexCount - meshInfo.totalVertexCount;
418
- const diffPercent = vertexDifference / lastMeshInfo.totalVertexCount;
435
+ const diff01 = vertexDifference / lastMeshInfo.totalVertexCount;
419
436
  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)}`);
437
+ console.log(`WARN: ❌ [${NAME}] Flag mesh LOD ${level}: vertex count did increase (${-vertexDifference.toFixed(0)} vertices, ${(-diff01 * 100).toFixed(4)}%) → LOD ${level} will be omitted. \"${mesh.getName()}\" at \"${lodInfo.path}\" ${JSON.stringify(lastMeshInfo)} → ${JSON.stringify(meshInfo)}`);
421
438
  shouldOmitLevel = true;
422
439
  }
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)}`);
440
+ else if (vertexDifference < 32 || diff01 < 0.05) {
441
+ 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}\"`);
425
442
  shouldOmitLevel = true;
426
443
  }
427
444
  }
428
- if (shouldOmitLevel) {
445
+ if (shouldOmitLevel && opts.useCache != false && lodInfo.cacheKey != undefined) {
429
446
  // we invalidate the cached entry because the mesh change was too small
430
447
  addToCache(lodInfo.cacheKey, new Uint8Array(0));
431
448
  }
@@ -441,25 +458,59 @@ export function make_progressive_meshes(filePath, _options, opts) {
441
458
  }
442
459
  }
443
460
  let maxDensity = 0;
461
+ let primitiveIndex = 0;
462
+ const lodLevelInFile = generatedLODInfos.length;
463
+ const clonedData = new Array();
444
464
  for (const prim of mesh.listPrimitives()) {
445
- const lodsCount = generatedLODInfos.length;
446
- const lastLod = LODS[lodsCount];
447
- console.log(`[${NAME}] ${mesh.getName()} LOD ${lodsCount}`);
465
+ const data = clonePrimitiveData(prim);
466
+ clonedData.push(data);
467
+ console.log(`[${NAME}] Simplify mesh[${index}] primitive[${primitiveIndex}] with ${data.indices.length / 3} triangles`);
468
+ const lastLod = LODS[lodLevelInFile];
469
+ // TODO: this should be run after simplification but then we need to change the runtime to use the LOD 0 density
448
470
  const density = calculateMeshDensity(prim);
449
471
  maxDensity = Math.max(maxDensity, density);
450
- const task = simplifyMesh(doc, mesh, prim, lastLod);
472
+ const task = simplifyMesh(doc, mesh, prim, primitiveIndex++, lastLod);
451
473
  tasks.push(task);
452
474
  }
453
475
  await Promise.all(tasks);
454
476
  tasks.length = 0;
455
477
  const { totalVertexCount: totalVertexCountAfter } = getMeshInformation(mesh);
478
+ const difference = originalTotalVertexCount - totalVertexCountAfter;
479
+ const diff01 = difference / originalTotalVertexCount;
456
480
  if (totalVertexCountAfter <= 0) {
457
- logger.error(`ERR: [${NAME}] Mesh[${index}] has no vertices after simplification: ${mesh.getName()}`);
481
+ logger.error(`ERR: [${NAME}]: Mesh[${index}] has no vertices after simplification: ${mesh.getName()}`);
482
+ }
483
+ else if (difference < 32 || diff01 <= 0.05) {
484
+ // 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
485
+ console.log(`[${NAME}]: Flag mesh LOD ${generatedLODInfos.length}: vertex count difference below threshold (removed ${difference} vertices, ${(-diff01 * 100).toFixed(2)} %) → will remove LODs. \"${mesh.getName()}\"`);
486
+ try {
487
+ for (const lodInfo of generatedLODInfos) {
488
+ rmSync(outputDirectory + "/" + lodInfo.path);
489
+ }
490
+ }
491
+ catch (err) {
492
+ logger.error(`ERR: [${NAME}] Failed to remove LOD files for \"${mesh.getName()}\"`);
493
+ }
494
+ // reset data (simplify might have changed the mesh data that we cloned before)
495
+ const primitives = mesh.listPrimitives();
496
+ if (clonedData.length == primitives.length) {
497
+ for (let i = 0; i < primitives.length; i++) {
498
+ setPrimitiveData(primitives[i], clonedData[i]);
499
+ }
500
+ }
501
+ return;
458
502
  }
459
503
  if (generatedLODInfos.length > 0) {
460
- const model = progressiveExt.create({
504
+ const model = progressiveExt.createMesh({
461
505
  guid: settings.guid,
462
- uri: generatedLODInfos.map(l => l.path),
506
+ lods: generatedLODInfos.map(l => {
507
+ return {
508
+ path: l.path,
509
+ density: l.density,
510
+ vertexCount: l.lodMeshInfo?.totalVertexCount,
511
+ indexCount: l.lodMeshInfo?.totalIndexCount,
512
+ };
513
+ }),
463
514
  density: maxDensity,
464
515
  });
465
516
  mesh.setExtension(NEEDLE_PROGRESSIVE_EXTENSION_NAME, model);
@@ -472,9 +523,31 @@ export function make_progressive_meshes(filePath, _options, opts) {
472
523
  }
473
524
  }
474
525
  }
475
- console.log("< Progressive Mesh Transform: finished");
476
526
  });
477
527
  }
528
+ function clonePrimitiveData(prim) {
529
+ const indices = new Uint32Array(prim.getIndices().getArray());
530
+ const attributes = {};
531
+ for (const attr of prim.listSemantics()) {
532
+ const accessor = prim.getAttribute(attr);
533
+ if (accessor) {
534
+ attributes[attr] = new Float32Array(accessor.getArray());
535
+ }
536
+ }
537
+ return { indices, attributes };
538
+ }
539
+ function setPrimitiveData(prim, data) {
540
+ const indices = prim.getIndices();
541
+ if (indices) {
542
+ indices.setArray(data.indices);
543
+ }
544
+ for (const attr of prim.listSemantics()) {
545
+ const accessor = prim.getAttribute(attr);
546
+ if (accessor) {
547
+ accessor.setArray(data.attributes[attr]);
548
+ }
549
+ }
550
+ }
478
551
  /**
479
552
  * @param basePath the base path to save the lod files
480
553
  * @param level: 0 = original, 1 = first lod, 2 = second lod, ...
@@ -499,10 +572,11 @@ async function createMeshLod(doc, basePath, level, mesh, meshInfo, guid, lodLeve
499
572
  hash += getHash(lodLevel);
500
573
  cacheKey = `mesh_lod_${level}_${hash}`;
501
574
  const cachedValue = tryGetFromCache(cacheKey);
502
- if (cachedValue) {
575
+ const meta = tryGetMetaFromCache(cacheKey);
576
+ if (cachedValue && meta) {
503
577
  console.log(`[${NAME}] LOD ${level} loaded ${(cachedValue.length / 1024).toFixed()} KB from cache for \"${mesh.getName()}\"`);
504
578
  writeFileSync(outPath, cachedValue);
505
- return { level, path: fileName, success: true, cacheKey: cacheKey };
579
+ return { level, path: fileName, success: true, cacheKey: cacheKey, lodMeshInfo: meta.meshInfo, density: meta.density };
506
580
  }
507
581
  }
508
582
  const newDoc = new Document();
@@ -516,6 +590,10 @@ async function createMeshLod(doc, basePath, level, mesh, meshInfo, guid, lodLeve
516
590
  newMesh.setName(mesh.getName() + " LOD " + level);
517
591
  newNode.setMesh(newMesh);
518
592
  const newMat = newDoc.createMaterial();
593
+ const weights = mesh.getWeights();
594
+ if (weights) {
595
+ newMesh.setWeights(weights);
596
+ }
519
597
  const tasks = new Array();
520
598
  // let didRunSimplification = false;
521
599
  for (const prim of primitives) {
@@ -533,6 +611,9 @@ async function createMeshLod(doc, basePath, level, mesh, meshInfo, guid, lodLeve
533
611
  newAttr.setType(attr.getType());
534
612
  newPrim.setAttribute(semantics[i], newAttr);
535
613
  }
614
+ else {
615
+ console.warn(`[${NAME}] Missing attribute ${semantics[i]} in mesh LOD ${level}`);
616
+ }
536
617
  }
537
618
  const indices = prim.getIndices();
538
619
  if (indices) {
@@ -541,6 +622,21 @@ async function createMeshLod(doc, basePath, level, mesh, meshInfo, guid, lodLeve
541
622
  newIndices.setType(indices.getType());
542
623
  newPrim.setIndices(newIndices);
543
624
  }
625
+ const targets = prim.listTargets();
626
+ for (const target of targets) {
627
+ const newTarget = newDoc.createPrimitiveTarget(target.getName());
628
+ newPrim.addTarget(newTarget);
629
+ for (const attr of target.listSemantics()) {
630
+ const accessor = target.getAttribute(attr);
631
+ if (accessor) {
632
+ const newAccessor = newDoc.createAccessor();
633
+ newAccessor.setArray(accessor.getArray());
634
+ newAccessor.setType(accessor.getType());
635
+ newTarget.setAttribute(attr, newAccessor);
636
+ }
637
+ }
638
+ }
639
+ ;
544
640
  const extensions = prim.listExtensions();
545
641
  for (const ext of extensions) {
546
642
  const PropertyClass = ext.constructor;
@@ -550,11 +646,12 @@ async function createMeshLod(doc, basePath, level, mesh, meshInfo, guid, lodLeve
550
646
  newPrim.setExtension(name, copy);
551
647
  }
552
648
  }
553
- for (const newPrim of newMesh.listPrimitives()) {
554
- if (lodLevel.error != undefined || lodLevel.ratio != undefined) {
555
- if (lodLevel.error > 0 || lodLevel.ratio < 1) {
649
+ let primitiveIndex = 0;
650
+ if (lodLevel.error != undefined || lodLevel.ratio != undefined) {
651
+ if (lodLevel.error > 0 || lodLevel.ratio < 1) {
652
+ for (const newPrim of newMesh.listPrimitives()) {
556
653
  // didRunSimplification = true;
557
- const task = simplifyMesh(newDoc, newMesh, newPrim, lodLevel);
654
+ const task = simplifyMesh(newDoc, newMesh, newPrim, primitiveIndex++, lodLevel);
558
655
  tasks.push(task);
559
656
  }
560
657
  }
@@ -564,15 +661,21 @@ async function createMeshLod(doc, basePath, level, mesh, meshInfo, guid, lodLeve
564
661
  const newMeshInformation = getMeshInformation(newMesh);
565
662
  if (newMeshInformation.totalVertexCount < 4) {
566
663
  console.error(`[${NAME}] mesh LOD ${level} is has less than 4 vertices → skipping`);
567
- return { level, path: fileName, success: false, cacheKey: cacheKey };
664
+ return { level, path: fileName, success: false, cacheKey: cacheKey, lodMeshInfo: null, density: null, };
665
+ }
666
+ let density = 0;
667
+ for (const prim of newMesh.listPrimitives()) {
668
+ const d = calculateMeshDensity(prim);
669
+ density = Math.max(density, d);
568
670
  }
569
671
  const progressiveExt = newDoc.createExtension(NEEDLE_progressive);
570
- if (!progressiveExt || !progressiveExt.create) {
672
+ if (!progressiveExt || !progressiveExt.createMesh) {
571
673
  console.error("Failed to create progressive extension");
572
674
  return;
573
675
  }
574
- newMesh.setExtension(NEEDLE_PROGRESSIVE_EXTENSION_NAME, progressiveExt.create({
676
+ newMesh.setExtension(NEEDLE_PROGRESSIVE_EXTENSION_NAME, progressiveExt.createMesh({
575
677
  guid: guid,
678
+ density: density,
576
679
  }));
577
680
  // we need to compress the mesh with the same compression method as the original file
578
681
  const root = doc.getRoot();
@@ -585,17 +688,18 @@ async function createMeshLod(doc, basePath, level, mesh, meshInfo, guid, lodLeve
585
688
  }
586
689
  const newIo = new NodeIO();
587
690
  registerExtensions(newIo);
588
- console.log(`[${NAME}] Write mesh LOD ${level} with: ${JSON.stringify(lodLevel)} to \"${fileName}\"`);
691
+ console.log(`[${NAME}] Write mesh LOD ${level} with to \"${fileName}\"`);
589
692
  await newIo.write(outPath, newDoc);
590
693
  // const hasChanged = meshInfo.maxVertexCount == newMeshInformation.maxVertexCount && meshInfo.totalVertexCount == newMeshInformation.totalVertexCount;
591
694
  // add to cache
592
695
  if (cacheKey != undefined) {
593
696
  const bytes = readFileSync(outPath);
594
697
  addToCache(cacheKey, bytes);
698
+ addMetaToCache(cacheKey, { meshInfo: newMeshInformation, density: density });
595
699
  }
596
- return { level, path: fileName, success: true, lodMeshInfo: newMeshInformation, cacheKey: cacheKey };
700
+ return { level, path: fileName, success: true, lodMeshInfo: newMeshInformation, cacheKey: cacheKey, density: density };
597
701
  }
598
- async function simplifyMesh(doc, mesh, prim, lodinfo) {
702
+ async function simplifyMesh(doc, mesh, prim, index, lodinfo) {
599
703
  const weldOptions = {
600
704
  ...WELD_DEFAULTS,
601
705
  overwrite: true,
@@ -606,13 +710,7 @@ async function simplifyMesh(doc, mesh, prim, lodinfo) {
606
710
  ...lodinfo,
607
711
  simplifier: MeshoptSimplifier,
608
712
  };
609
- // if (lodinfo.error != undefined)
610
- // simplifyOptions.error = lodinfo.error;
611
- // if (lodinfo.ratio != undefined)
612
- // simplifyOptions.ratio = lodinfo.ratio;
613
- // if (lodinfo.lockBorder != undefined)
614
- // simplifyOptions.lockBorder = lodinfo.lockBorder;
615
- return compressPrimitive(doc, mesh, prim, weldOptions, simplifyOptions);
713
+ return compressPrimitive(doc, mesh, prim, index, weldOptions, simplifyOptions);
616
714
  }
617
715
  export function isMeshLOD(filepath) {
618
716
  return filepath.includes("mesh_lod_");
@@ -2,6 +2,7 @@ import { Mesh, Primitive } from "@gltf-transform/core";
2
2
  export declare type MeshInformation = {
3
3
  readonly maxVertexCount: number;
4
4
  readonly totalVertexCount: number;
5
+ readonly totalIndexCount: number;
5
6
  };
6
7
  export declare function getMeshInformation(mesh: Mesh): MeshInformation;
7
8
  export declare function calculateMeshDensity(prim: Primitive): number;
@@ -1,6 +1,9 @@
1
1
  export function getMeshInformation(mesh) {
2
2
  let maxVertexCount = 0;
3
+ let totalIndexCount = 0;
3
4
  const totalVertexCount = mesh.listPrimitives().reduce((acc, prim) => {
5
+ const indices = prim.getIndices()?.getCount() || 0;
6
+ totalIndexCount += indices;
4
7
  const vertices = prim.getAttribute('POSITION')?.getCount() || 0;
5
8
  maxVertexCount = Math.max(maxVertexCount, vertices);
6
9
  return acc + vertices;
@@ -8,6 +11,7 @@ export function getMeshInformation(mesh) {
8
11
  return {
9
12
  maxVertexCount,
10
13
  totalVertexCount,
14
+ totalIndexCount,
11
15
  };
12
16
  }
13
17
  export function calculateMeshDensity(prim) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/gltf-build-pipeline",
3
- "version": "1.4.1-beta.9",
3
+ "version": "1.4.1",
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",