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

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,35 @@ 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.10] - 2023-03-19
8
+ - change: NEEDLE_progressive calculates density per mesh level
9
+ - change: NEEDLE_progressive `uri` field is removed and added `lod` array of objects
10
+
11
+ ## [1.4.1-beta.8] - 2023-03-18
12
+ - change: if an asset doesnt have a guid we use a uuid package now
13
+ - change: ignore mesh LODs with vertex count < 4
14
+ - change: allow generating mesh LODs without extension (if the mesh has > 32 vertices)
15
+ - change: omit generated LODs if the change is too small (e.g. the vertex count didnt change enough)
16
+
17
+ ## [1.4.1-beta.6] - 2023-03-15
18
+ - add: caching support for mesh LODs
19
+
20
+ ## [1.4.1-beta.5] - 2023-03-15
21
+ - add: auto LODs now calculate mesh density
22
+ - change: generate 6 LOD level
23
+ - change: use ratio 0.5 of the previous LOD level
24
+ - change: bump gltf-transform to 3.10.1 to fix simplifier
25
+ - change: add sparse transform to progressive transform
26
+
27
+ ## [1.4.1-beta.3] - 2023-03-14
28
+ - fix: wait for mesh simplifier to be ready
29
+
30
+ ## [1.4.1-beta.2] - 2023-03-13
8
31
  - change: update gltf-transform to `^3.10.0`
9
32
  - change: progressive mesh transform does now set ratio for LODs (see [simplifier issue 661 on github](https://github.com/zeux/meshoptimizer/issues/661))
10
33
  - fix: issue where progressive meshes were not generated if a mesh was multiple times in the glTF
34
+ - change: simplify to use ratio, fix issue where not all meshes were processed
35
+ - change: auto LOD max error set to 0.1
11
36
 
12
37
  ## [1.4.0-beta] - 2023-03-12
13
38
  - add: progressive mesh transform to automatically generate mesh LOD levels (currently we only generate one low poly version)
@@ -1,5 +1,10 @@
1
1
  import { ExtensibleProperty } from "@gltf-transform/core";
2
+ /**
3
+ * Get the available space on the cache directory in bytes.
4
+ */
2
5
  export declare function clearCache(): void;
6
+ export declare function ensureCacheSize(maxSizeMB: number): void;
3
7
  export declare function addToCache(key: string, bytes: Uint8Array): void;
4
8
  export declare function tryGetFromCache(key: string): Uint8Array | null;
5
9
  export declare function getKey(property: ExtensibleProperty): string;
10
+ export declare function getHash(property: ExtensibleProperty | object): number;
@@ -1,16 +1,59 @@
1
- import { Texture, Primitive } from "@gltf-transform/core";
1
+ import { ExtensibleProperty, Texture, Primitive, Accessor, Mesh } from "@gltf-transform/core";
2
2
  import tmp from 'tmp';
3
3
  import fs from 'fs';
4
4
  import path from 'path';
5
+ // import * as CHECKDISCSPACE from 'check-disk-space'
6
+ /**
7
+ * Get the available space on the cache directory in bytes.
8
+ */
9
+ // export async function getCacheAvailableSpaceOnDisc(): Promise<number> {
10
+ // try {
11
+ // const dir = getCacheDirectory();
12
+ // const res = await CHECKDISCSPACE.default(dir).catch(() => { return { free: 0 } });
13
+ // return res.free;
14
+ // }
15
+ // catch (e) {
16
+ // console.error(`Error checking disk space: ${e}`);
17
+ // }
18
+ // return 0;
19
+ // }
5
20
  export function clearCache() {
6
21
  const dir = getCacheDirectory();
7
22
  fs.rmSync(dir, { recursive: true });
8
23
  console.log(`Cleared cache at ${dir}`);
9
24
  }
25
+ export function ensureCacheSize(maxSizeMB) {
26
+ const dir = getCacheDirectory();
27
+ let totalSize = 0;
28
+ const files = fs.readdirSync(dir).map(file => {
29
+ const path = `${dir}/${file}`;
30
+ const stats = fs.statSync(path);
31
+ totalSize += stats.size;
32
+ return { path, size: stats.size };
33
+ });
34
+ const maxSizeInBytes = maxSizeMB * 1024 * 1024;
35
+ if (totalSize < maxSizeInBytes)
36
+ return;
37
+ files.sort((a, b) => {
38
+ const mtimeA = fs.statSync(a.path).mtimeMs;
39
+ const mtimeB = fs.statSync(b.path).mtimeMs;
40
+ return mtimeA - mtimeB;
41
+ });
42
+ const targetSize = maxSizeInBytes * 0.9;
43
+ // remove oldest files first
44
+ while (totalSize > targetSize) {
45
+ const file = files.shift();
46
+ if (!file)
47
+ break;
48
+ fs.rmSync(file.path);
49
+ totalSize -= file.size;
50
+ }
51
+ console.log(`Cache size reduced to ${(totalSize / (1024 * 1024)).toFixed(1)} MB`);
52
+ }
10
53
  export function addToCache(key, bytes) {
11
54
  const dir = getCacheDirectory();
12
55
  const path = `${dir}/${key}.bin`;
13
- console.log(`Added ${(bytes.byteLength / 1024).toFixed(1)} KB to cache at ${path}`);
56
+ console.log(`Added ${(bytes.byteLength / 1024).toFixed(1)} KB to cache at \"${path}\"`);
14
57
  fs.writeFileSync(path, bytes);
15
58
  }
16
59
  export function tryGetFromCache(key) {
@@ -23,37 +66,54 @@ export function tryGetFromCache(key) {
23
66
  return null;
24
67
  }
25
68
  export function getKey(property) {
26
- let hash = 0;
27
69
  let prefix = "prop";
28
- const extensions = property.listExtensions();
29
- const extras = property.getExtras();
30
- // const slots = listTextureSlots(doc, texture);
31
- // const channels = getTextureChannelMask(doc, texture);
32
- hash += hashObject({
33
- extensions,
34
- extras,
35
- });
36
70
  if (property instanceof Texture) {
37
71
  prefix = "image";
38
- const image = property.getImage();
39
- const bytes = image?.buffer;
40
- if (bytes)
41
- hash += hashBuffer(bytes, image.byteOffset, image.byteLength);
42
72
  }
43
- else if (property instanceof Primitive) {
44
- prefix = "prim";
45
- const indices = property.getIndices();
46
- const attributes = property.listAttributes();
47
- const targets = property.listTargets();
73
+ let hash = getHash(property);
74
+ const key = `${prefix}-${hash}`;
75
+ return key;
76
+ }
77
+ export function getHash(property) {
78
+ let hash = 0;
79
+ if (property instanceof ExtensibleProperty) {
80
+ const extensions = property.listExtensions();
81
+ const extras = property.getExtras();
82
+ // const slots = listTextureSlots(doc, texture);
83
+ // const channels = getTextureChannelMask(doc, texture);
48
84
  hash += hashObject({
49
- indices,
50
- attributes,
51
- targets
85
+ extensions,
86
+ extras,
52
87
  });
88
+ if (property instanceof Texture) {
89
+ const image = property.getImage();
90
+ const bytes = image?.buffer;
91
+ if (bytes)
92
+ hash += hashBuffer(bytes, image.byteOffset, image.byteLength);
93
+ }
94
+ else if (property instanceof Mesh) {
95
+ const weights = property.getWeights();
96
+ hash += hashObject(weights);
97
+ const primitives = property.listPrimitives();
98
+ hash += hashObject(primitives);
99
+ }
100
+ else if (property instanceof Primitive) {
101
+ const indices = property.getIndices();
102
+ const attributes = property.listAttributes();
103
+ const targets = property.listTargets();
104
+ const semantics = property.listSemantics();
105
+ hash += hashObject({
106
+ indices,
107
+ attributes,
108
+ targets,
109
+ semantics
110
+ });
111
+ }
53
112
  }
54
- let key = hash.toString();
55
- key = `${prefix}-${key}`;
56
- return key;
113
+ else {
114
+ hash += hashObject(property);
115
+ }
116
+ return hash;
57
117
  }
58
118
  function getCacheDirectory() {
59
119
  const tmpDir = path.join(tmp.tmpdir, "Needle", "build-pipeline-cache");
@@ -63,6 +123,10 @@ function getCacheDirectory() {
63
123
  return tmpDir;
64
124
  }
65
125
  function hashObject(obj) {
126
+ if (obj instanceof Accessor) {
127
+ const buffer = obj.getArray();
128
+ return hashBuffer(buffer);
129
+ }
66
130
  const str = JSON.stringify(obj);
67
131
  let hash = 0;
68
132
  for (let i = 0; i < str.length; i++) {
@@ -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;
@@ -1,15 +1,21 @@
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;
5
- uri?: string | Array<string>;
6
+ /** paths to the LOD level files */
7
+ lods?: Array<{
8
+ path: string;
9
+ /** the density of the lod level */
10
+ density?: number;
11
+ }>;
12
+ density?: number;
6
13
  }
7
- export declare function generateGuid(): string;
8
14
  export declare class NEEDLE_progressive extends Extension {
9
15
  static EXTENSION_NAME: string;
10
16
  extensionName: string;
11
- static create(graph: Graph<Property<IProperty>>, data: IProgressiveTextureInfo): NEEDLE_progressive_texture_property;
12
- create(data: IProgressiveTextureInfo): NEEDLE_progressive_texture_property;
17
+ static create(graph: Graph<Property<IProperty>>, data: IProgressiveLoadingInfo): NEEDLE_progressive_texture_property;
18
+ create(data: IProgressiveLoadingInfo): NEEDLE_progressive_texture_property;
13
19
  read(_context: ReaderContext): this;
14
20
  write(context: WriterContext): this;
15
21
  }
@@ -19,5 +25,5 @@ export declare class NEEDLE_progressive_texture_property extends ExtensionProper
19
25
  parentTypes: PropertyType[];
20
26
  propertyType: string;
21
27
  init(): void;
22
- model: IProgressiveTextureInfo;
28
+ model: IProgressiveLoadingInfo;
23
29
  }
@@ -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
  }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./utils/index.js";
2
2
  export * from "./transforms/index.js";
3
3
  export * from "./extensions/index.js";
4
+ export * from "./cache/index.js";
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./utils/index.js";
2
2
  export * from "./transforms/index.js";
3
3
  export * from "./extensions/index.js";
4
+ export * from "./cache/index.js";
@@ -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, index: number, weldOptions: Required<WeldOptions>, meshOptions: Required<SimplifyOptions>): Promise<void>;
@@ -5,6 +5,7 @@ import { NEEDLE_mesh_compression } from "../extensions/index.js";
5
5
  const NAME = 'needle_mesh_transform';
6
6
  export const needle_mesh_transform = function (_options) {
7
7
  return async (document, _context) => {
8
+ // TODO: needs to be moved into NEEDLE_progressive
8
9
  await simplifierTransform(document, _context, _options);
9
10
  await applyMeshCompression(document, _context);
10
11
  };
@@ -49,7 +50,9 @@ async function simplifierTransform(document, _context, _options) {
49
50
  overwrite: true,
50
51
  };
51
52
  for (const mesh of document.getRoot().listMeshes()) {
53
+ let i = 0;
52
54
  for (const prim of mesh.listPrimitives()) {
55
+ const index = i++;
53
56
  // TODO: mesh compression caching
54
57
  // const key = getKey(prim);
55
58
  // const cached = tryGetFromCache(key);
@@ -83,16 +86,30 @@ async function simplifierTransform(document, _context, _options) {
83
86
  logger.warn(`[${NAME}]: Skipping primitive of mesh "${mesh.getName()}": Requires TRIANGLES draw mode.`);
84
87
  continue;
85
88
  }
86
- compressPrimitive(document, mesh, prim, weldOptions, simplifyOpts);
89
+ compressPrimitive(document, mesh, prim, index, weldOptions, simplifyOpts);
87
90
  }
88
91
  }
89
92
  return true;
90
93
  }
91
- export function compressPrimitive(document, mesh, prim, weldOptions, meshOptions) {
94
+ export async function compressPrimitive(document, mesh, prim, index, weldOptions, meshOptions) {
92
95
  const logger = document.getLogger();
93
- logger.debug(`[${NAME}]: Welding primitive of mesh "${mesh.getName()}" → tolerance: ${weldOptions.tolerance}`);
96
+ logger.debug(`[${NAME}]: Welding primitive[${index}] of mesh "${mesh.getName()}" → tolerance: ${weldOptions.tolerance}`);
94
97
  weldPrimitive(document, prim, weldOptions);
95
- logger.debug(`[${NAME}]: Simplifying primitive of mesh "${mesh.getName()}" → ratio: ${meshOptions.ratio}, error: ${meshOptions.error}, lockBorder: ${meshOptions.lockBorder}`);
98
+ // make sure we wait for the mesh simplifier to be ready
99
+ if (meshOptions.simplifier && meshOptions.simplifier?.ready instanceof Promise) {
100
+ const res = await meshOptions.simplifier?.ready
101
+ .then(() => {
102
+ // ready!
103
+ return true;
104
+ })
105
+ .catch((err) => {
106
+ console.error(`[${NAME}]: Simplifier failed to load...\n${err}`);
107
+ return false;
108
+ });
109
+ if (!res)
110
+ return;
111
+ }
112
+ logger.debug(`[${NAME}]: Simplifying primitive[${index}] of mesh "${mesh.getName()}" → ratio: ${meshOptions.ratio}, error: ${meshOptions.error}, lockBorder: ${meshOptions.lockBorder}`);
96
113
  simplifyPrimitive(document, prim, meshOptions);
97
114
  }
98
115
  // function updatePrimitive(prim: Primitive, simplifier: any, positionArray, dstIndicesArray) {
@@ -32,4 +32,7 @@ export declare enum TextureResizeFilter {
32
32
  export declare const TEXTURE_RESIZE_DEFAULTS: TextureResizeOptions;
33
33
  export declare function resizeTextureToMax(texture: Texture, maxSize: number, logger: ILogger): Promise<void>;
34
34
  export declare function make_progressive_meshes(filePath: string, _options: any, opts?: ProgressiveOptions): Transform;
35
+ export declare function isMeshLOD(filepath: string): boolean;
36
+ export declare function isImageLOD(filepath: string): boolean;
37
+ export declare function isLOD(filepath: string): boolean;
35
38
  export {};
@@ -1,15 +1,16 @@
1
1
  import { BufferUtils, Document, NodeIO, PropertyType } from '@gltf-transform/core';
2
- import { SIMPLIFY_DEFAULTS, WELD_DEFAULTS, listTextureSlots } from '@gltf-transform/functions';
3
- import { NEEDLE_PROGRESSIVE_EXTENSION_NAME, NEEDLE_progressive, generateGuid } from "../extensions/NEEDLE_progressive/index.js";
2
+ import { SIMPLIFY_DEFAULTS, WELD_DEFAULTS, listTextureSlots, sparse } from '@gltf-transform/functions';
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
- import { existsSync } from 'fs';
7
- import { addToCache, getKey, tryGetFromCache } from '../cache/index.js';
6
+ import { existsSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
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;
@@ -67,7 +68,7 @@ async function process(file, opts) {
67
68
  }
68
69
  //@ts-ignore
69
70
  document.getLogger().verbosity = 0;
70
- await document.transform(needle_asset(), make_progressive_textures(file, { size: [256, 256] }, opts), make_progressive_meshes(file, {}, opts));
71
+ await document.transform(needle_asset(), make_progressive_textures(file, { size: [256, 256] }, opts), make_progressive_meshes(file, {}, opts), sparse());
71
72
  let filename = file;
72
73
  if (opts.debug === true) {
73
74
  // get extension
@@ -109,40 +110,40 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
109
110
  const uri = texture.getURI();
110
111
  const match = !options.pattern || options.pattern.test(name) || options.pattern.test(uri);
111
112
  if (!match) {
112
- logger.debug(`${NAME}: Skipping, excluded by "pattern" parameter. ${textureIndex}`);
113
+ logger.debug(`[${NAME}] Skipping, excluded by "pattern" parameter. ${textureIndex}`);
113
114
  continue;
114
115
  }
115
116
  if (texture.getMimeType() !== 'image/png' && texture.getMimeType() !== 'image/jpeg') {
116
- logger.warn(`${NAME}: Skipping, unsupported texture type "${texture.getMimeType()} at ${textureIndex}".`);
117
+ logger.warn(`[${NAME}] Skipping, unsupported texture type "${texture.getMimeType()} at ${textureIndex}".`);
117
118
  continue;
118
119
  }
119
120
  const slots = listTextureSlots(texture);
120
121
  if (options.slots && !slots.some((slot) => options.slots?.test(slot))) {
121
- logger.debug(`${NAME}: Skipping, [${slots.join(', ')}] excluded by "slots" parameter. ${textureIndex}`);
122
+ logger.debug(`[${NAME}] Skipping, [${slots.join(', ')}] excluded by "slots" parameter. ${textureIndex}`);
122
123
  continue;
123
124
  }
124
125
  const settings = texture.getExtension(NEEDLE_progressive_texture_settings.EXTENSION_NAME);
125
126
  if (!settings) {
126
- logger.warn(`${NAME}: Skipping, no settings found ${textureIndex}`);
127
+ logger.warn(`[${NAME}] Skipping, no settings found ${textureIndex}`);
127
128
  continue;
128
129
  }
129
130
  if (!settings.isValid) {
130
- logger.warn(`${NAME}: Skipping, settings are invalid ${textureIndex}`);
131
+ logger.warn(`[${NAME}] Skipping, settings are invalid ${textureIndex}`);
131
132
  continue;
132
133
  }
133
134
  options.size[0] = settings.maxSize;
134
135
  options.size[1] = settings.maxSize;
135
136
  const existingExtension = texture.getExtension(NEEDLE_PROGRESSIVE_EXTENSION_NAME);
136
137
  if (existingExtension !== undefined && existingExtension !== null) {
137
- logger.debug(`${NAME}: Skipping, texture is already progressive ${textureIndex}`);
138
+ logger.debug(`[${NAME}] Skipping, texture is already progressive ${textureIndex}`);
138
139
  continue;
139
140
  }
140
141
  const progressiveExt = doc.createExtension(NEEDLE_progressive);
141
142
  if (!progressiveExt || !progressiveExt.create) {
142
- logger.debug(`${NAME}: Skipping because progressive extension is not available. ${textureIndex}`);
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;
@@ -151,7 +152,11 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
151
152
  // TODO: this path should be added to the NEEDLE dependencies extension
152
153
  texture.setExtension(NEEDLE_PROGRESSIVE_EXTENSION_NAME, progressiveExt.create({
153
154
  guid: textureGuid,
154
- uri: relName,
155
+ lods: [
156
+ {
157
+ path: relName,
158
+ }
159
+ ],
155
160
  }));
156
161
  const key = getKey(texture);
157
162
  if (opts.useCache) {
@@ -262,7 +267,7 @@ async function resizeTexture(options, texture, logger, uri, slots) {
262
267
  const [maxWidth, maxHeight] = options.size;
263
268
  const [srcWidth, srcHeight] = texture.getSize();
264
269
  if (srcWidth <= maxWidth && srcHeight <= maxHeight) {
265
- logger.debug(`${NAME}: Skipping, the texture is already smaller than the requested size ${maxWidth}x${maxHeight}}`);
270
+ logger.debug(`[${NAME}] Skipping, the texture is already smaller than the requested size ${maxWidth}x${maxHeight}}`);
266
271
  return;
267
272
  }
268
273
  if (maxWidth <= 0 || maxHeight <= 0) {
@@ -298,70 +303,187 @@ export function make_progressive_meshes(filePath, _options, opts) {
298
303
  console.log("Progressive Mesh Transform: skip lod at " + filePath);
299
304
  return Promise.resolve();
300
305
  }
306
+ await MeshoptSimplifier.ready;
301
307
  const logger = doc.getLogger();
302
308
  const filename = path.basename(filePath);
303
- console.log("> Progressive Mesh Transform: " + filename);
309
+ console.log(`> [${NAME}] start generating LOD meshes for \"${filename}\"`);
304
310
  let index = 0;
305
311
  // the lowest LOD (last entry) will be saved in the original file
312
+ const MAX_LODS = 6;
306
313
  const LODS = [
307
- { error: 0 },
308
- { ratio: 0.5, error: 1 },
309
- { ratio: 0.25, error: 1 },
314
+ {
315
+ ratio: 1,
316
+ error: 0,
317
+ lockBorder: true
318
+ },
310
319
  ];
320
+ for (let l = 0; l < MAX_LODS; l++) {
321
+ const t = l / MAX_LODS;
322
+ const latest = LODS[LODS.length - 1];
323
+ const ratio = latest.ratio * .5;
324
+ const error = .05 + t * .5;
325
+ LODS.push({
326
+ ratio: ratio,
327
+ error,
328
+ lockBorder: true,
329
+ // toleranceNormal: 0.1 + t * 0.1,
330
+ });
331
+ }
311
332
  /** Map of visited GUID -> extension */
312
333
  const visited = new Map();
313
334
  const meshes = doc.getRoot().listMeshes();
314
335
  for (const mesh of meshes) {
315
- // TODO: skip meshes that are already below a certain threshold (E.g. just 4 vertices)
316
- const settings = mesh.getExtension(NEEDLE_progressive_mesh_settings.EXTENSION_NAME);
336
+ const meshInformation = getMeshInformation(mesh);
337
+ const { totalVertexCount, maxVertexCount } = meshInformation;
338
+ let settings = mesh.getExtension(NEEDLE_progressive_mesh_settings.EXTENSION_NAME);
317
339
  if (!settings) {
318
- logger.debug(`${NAME}: no ${NEEDLE_progressive_mesh_settings.EXTENSION_NAME} found for mesh at index ${index++}`);
340
+ if (maxVertexCount < 32) {
341
+ logger.debug(`[${NAME}] Skipping, no settings found and no primitive with more than 32 vertices. mesh[${index}] has ${totalVertexCount} vertices (max: ${maxVertexCount})`);
342
+ continue;
343
+ }
344
+ else {
345
+ settings = {
346
+ guid: generateGuid(path + "_mesh_" + index.toString()),
347
+ generateLods: true,
348
+ };
349
+ logger.debug(`[${NAME}] No settings found for mesh[${index}] with ${totalVertexCount} vertices (max: ${maxVertexCount}) - will use default settings (guid: ${settings.guid})`);
350
+ }
351
+ }
352
+ else if (settings.generateLods === false) {
353
+ logger.debug(`[${NAME}] progressive mesh disabled for mesh at index ${index++}`);
319
354
  continue;
320
355
  }
321
356
  const progressiveExt = doc.createExtension(NEEDLE_progressive);
322
357
  if (!progressiveExt || !progressiveExt.create) {
323
- logger.debug(`${NAME}: Skipping because progressive extension is not available. ${index++}`);
358
+ logger.debug(`[${NAME}] Skipping because progressive extension is not available. ${index++}`);
324
359
  continue;
325
360
  }
326
361
  // check if we have already produced LODs for this mesh
327
362
  let needsToGenerateLODs = true;
328
- const generatedLODs = new Array();
363
+ const generatedLODInfos = new Array();
329
364
  if (visited.has(settings.guid)) {
330
365
  // we can not just set the extension here...
331
- logger.debug(`${NAME}: Mesh has already been processed ${index} - will use previously generated LODs`);
366
+ logger.debug(`[${NAME}] Mesh has already been processed ${index} - will use previously generated LODs`);
332
367
  needsToGenerateLODs = false;
333
- const existingPaths = visited.get(settings.guid).paths;
334
- generatedLODs.push(...existingPaths);
368
+ const existingPaths = visited.get(settings.guid);
369
+ generatedLODInfos.push(...existingPaths.infos);
335
370
  }
371
+ /** async simplification tasks... */
372
+ const tasks = new Array();
336
373
  if (needsToGenerateLODs) {
337
374
  // produce LODS, skip the last level because it will be saved in the original file
338
375
  for (let level = 0; level < LODS.length - 1; level++) {
339
376
  const lod = LODS[level];
340
- console.log(`${NAME}: Generate LOD ${level}`);
341
377
  const guid = settings.guid;
342
- const lodPath = createMeshLod(doc, outputDirectory, level, mesh, guid, lod);
343
- generatedLODs.push(lodPath);
378
+ // TODO: check if the LOD level did actually change the mesh - otherwise we can just skip generating lower resolutions
379
+ const lodTask = createMeshLod(doc, outputDirectory, level, mesh, meshInformation, guid, lod, opts);
380
+ lodTask.then(res => {
381
+ if (res?.success) {
382
+ generatedLODInfos[level] = res;
383
+ }
384
+ });
385
+ tasks.push(lodTask);
344
386
  }
387
+ // wait for all LODs to be generated
388
+ await Promise.all(tasks);
389
+ tasks.length = 0;
345
390
  }
391
+ // compare LOD results and ignore if the mesh did not change
392
+ // TODO: this should actually check mesh semantics (perhaps hash the mesh using the caching system?)
393
+ let prevSize = -Infinity;
394
+ for (let i = 0; i < generatedLODInfos.length; i++) {
395
+ const lodInfo = generatedLODInfos[i];
396
+ if (!lodInfo) {
397
+ console.log(`[${NAME}] LOD ${i} doesnt exist`);
398
+ continue;
399
+ }
400
+ const fullPath = outputDirectory + lodInfo.path;
401
+ if (existsSync(fullPath)) {
402
+ const lastSize = prevSize;
403
+ const sizeOnDisc = statSync(fullPath).size;
404
+ const diff = lastSize - sizeOnDisc;
405
+ const level = lodInfo.level;
406
+ let shouldOmitLevel = false;
407
+ // if the mesh size on disc is identical to the last level, we can omit this level
408
+ if (sizeOnDisc <= 0) {
409
+ 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}\"`);
410
+ shouldOmitLevel = true;
411
+ }
412
+ else if (diff == 0) {
413
+ console.log(`[${NAME}] mesh LOD ${level} size on disc did not change compared to previous level - level will be omitted \"${lodInfo.path}\"`);
414
+ shouldOmitLevel = true;
415
+ }
416
+ else if (!shouldOmitLevel) {
417
+ // 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
418
+ const meshInfo = lodInfo.lodMeshInfo;
419
+ const lastMeshInfo = generatedLODInfos[i - 1]?.lodMeshInfo;
420
+ if (meshInfo && lastMeshInfo) {
421
+ const vertexDifference = lastMeshInfo.totalVertexCount - meshInfo.totalVertexCount;
422
+ const diffPercent = vertexDifference / lastMeshInfo.totalVertexCount;
423
+ if (vertexDifference < 0) {
424
+ 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)}`);
425
+ shouldOmitLevel = true;
426
+ }
427
+ else if (vertexDifference < 32 || diffPercent < 0.05) {
428
+ 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)}`);
429
+ shouldOmitLevel = true;
430
+ }
431
+ }
432
+ if (shouldOmitLevel && opts.useCache != false && lodInfo.cacheKey != undefined) {
433
+ // we invalidate the cached entry because the mesh change was too small
434
+ addToCache(lodInfo.cacheKey, new Uint8Array(0));
435
+ }
436
+ }
437
+ if (shouldOmitLevel) {
438
+ generatedLODInfos.splice(i, 1);
439
+ rmSync(fullPath);
440
+ i--;
441
+ }
442
+ else {
443
+ prevSize = sizeOnDisc;
444
+ }
445
+ }
446
+ }
447
+ let maxDensity = 0;
448
+ let primitiveIndex = 0;
346
449
  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);
450
+ const lodsCount = generatedLODInfos.length;
451
+ const lastLod = LODS[lodsCount];
452
+ console.log(`[${NAME}] ${mesh.getName()} LOD ${lodsCount}`);
453
+ // TODO: this should be run after simplification but then we need to change the runtime to use the LOD 0 density
454
+ const density = calculateMeshDensity(prim);
455
+ maxDensity = Math.max(maxDensity, density);
456
+ const task = simplifyMesh(doc, mesh, prim, primitiveIndex++, lastLod);
457
+ tasks.push(task);
350
458
  }
351
- const model = progressiveExt.create({
352
- guid: settings.guid,
353
- uri: generatedLODs.map(l => l),
354
- });
355
- mesh.setExtension(NEEDLE_PROGRESSIVE_EXTENSION_NAME, model);
356
- index++;
357
- if (!visited.has(settings.guid)) {
358
- visited.set(settings.guid, {
359
- model: model,
360
- paths: generatedLODs,
459
+ await Promise.all(tasks);
460
+ tasks.length = 0;
461
+ const { totalVertexCount: totalVertexCountAfter } = getMeshInformation(mesh);
462
+ if (totalVertexCountAfter <= 0) {
463
+ logger.error(`ERR: [${NAME}] Mesh[${index}] has no vertices after simplification: ${mesh.getName()}`);
464
+ }
465
+ if (generatedLODInfos.length > 0) {
466
+ const model = progressiveExt.create({
467
+ guid: settings.guid,
468
+ lods: generatedLODInfos.map(l => {
469
+ return {
470
+ path: l.path,
471
+ density: l.density,
472
+ };
473
+ }),
474
+ density: maxDensity,
361
475
  });
476
+ mesh.setExtension(NEEDLE_PROGRESSIVE_EXTENSION_NAME, model);
477
+ index++;
478
+ if (!visited.has(settings.guid)) {
479
+ visited.set(settings.guid, {
480
+ model: model,
481
+ infos: generatedLODInfos,
482
+ });
483
+ }
362
484
  }
363
485
  }
364
- console.log("< Progressive Mesh Transform: finished");
486
+ console.log(`< [${NAME}] finished generating LOD meshes for \"${filename}\"`);
365
487
  });
366
488
  }
367
489
  /**
@@ -371,19 +493,43 @@ export function make_progressive_meshes(filePath, _options, opts) {
371
493
  * @param guid a unique identifier for the mesh
372
494
  * @param error the error to use for simplification. Higher means more simplification
373
495
  */
374
- function createMeshLod(doc, basePath, level, mesh, guid, lodLevel) {
496
+ async function createMeshLod(doc, basePath, level, mesh, meshInfo, guid, lodLevel, opts) {
497
+ const fileExtension = ".glb";
498
+ const fileName_withoutExtension = "mesh_lod_" + level + "_" + guid;
499
+ const fileName = fileName_withoutExtension + fileExtension;
500
+ const outPath = basePath + fileName;
501
+ const primitives = mesh.listPrimitives();
502
+ let cacheKey = undefined;
503
+ if (opts.useCache !== false) {
504
+ // generate hash for mesh
505
+ let hash = mesh["needle:mesh-hash"];
506
+ if (hash === undefined) {
507
+ hash = getHash(mesh);
508
+ mesh["needle:mesh-hash"] = hash;
509
+ }
510
+ hash += getHash(lodLevel);
511
+ cacheKey = `mesh_lod_${level}_${hash}`;
512
+ const cachedValue = tryGetFromCache(cacheKey);
513
+ if (cachedValue) {
514
+ console.log(`[${NAME}] LOD ${level} loaded ${(cachedValue.length / 1024).toFixed()} KB from cache for \"${mesh.getName()}\"`);
515
+ writeFileSync(outPath, cachedValue);
516
+ return { level, path: fileName, success: true, cacheKey: cacheKey };
517
+ }
518
+ }
375
519
  const newDoc = new Document();
376
520
  const newScene = newDoc.createScene();
377
521
  newDoc.createBuffer();
378
522
  const newNode = newDoc.createNode();
379
523
  newScene.addChild(newNode);
380
- newNode.setName("node_" + level + "_" + guid);
524
+ newNode.setName("LOD " + level + "_" + guid);
381
525
  newNode.setTranslation([0, 0, 0]);
382
526
  const newMesh = newDoc.createMesh();
527
+ newMesh.setName(mesh.getName() + " LOD " + level);
383
528
  newNode.setMesh(newMesh);
384
- const primitives = mesh.listPrimitives();
529
+ const newMat = newDoc.createMaterial();
530
+ const tasks = new Array();
531
+ // let didRunSimplification = false;
385
532
  for (const prim of primitives) {
386
- const newMat = newDoc.createMaterial();
387
533
  const newPrim = newDoc.createPrimitive();
388
534
  newMesh.addPrimitive(newPrim);
389
535
  newPrim.setName(prim.getName());
@@ -412,15 +558,31 @@ function createMeshLod(doc, basePath, level, mesh, guid, lodLevel) {
412
558
  const newProp = new PropertyClass(newDoc.getGraph());
413
559
  const copy = newProp.copy(ext);
414
560
  const name = ext.extensionName;
415
- console.log(`${NAME}: Copy primitive ${name} extension`);
416
561
  newPrim.setExtension(name, copy);
417
562
  }
418
563
  }
564
+ let primitiveIndex = 0;
419
565
  if (lodLevel.error != undefined || lodLevel.ratio != undefined) {
420
- for (const prim of newMesh.listPrimitives()) {
421
- simplifyMesh(newDoc, newMesh, prim, lodLevel);
566
+ if (lodLevel.error > 0 || lodLevel.ratio < 1) {
567
+ for (const newPrim of newMesh.listPrimitives()) {
568
+ // didRunSimplification = true;
569
+ const task = simplifyMesh(newDoc, newMesh, newPrim, primitiveIndex++, lodLevel);
570
+ tasks.push(task);
571
+ }
422
572
  }
423
573
  }
574
+ await Promise.all(tasks);
575
+ tasks.length = 0;
576
+ const newMeshInformation = getMeshInformation(newMesh);
577
+ if (newMeshInformation.totalVertexCount < 4) {
578
+ console.error(`[${NAME}] mesh LOD ${level} is has less than 4 vertices → skipping`);
579
+ return { level, path: fileName, success: false, cacheKey: cacheKey };
580
+ }
581
+ let density = 0;
582
+ for (const prim of newMesh.listPrimitives()) {
583
+ const d = calculateMeshDensity(prim);
584
+ density = Math.max(density, d);
585
+ }
424
586
  const progressiveExt = newDoc.createExtension(NEEDLE_progressive);
425
587
  if (!progressiveExt || !progressiveExt.create) {
426
588
  console.error("Failed to create progressive extension");
@@ -428,6 +590,7 @@ function createMeshLod(doc, basePath, level, mesh, guid, lodLevel) {
428
590
  }
429
591
  newMesh.setExtension(NEEDLE_PROGRESSIVE_EXTENSION_NAME, progressiveExt.create({
430
592
  guid: guid,
593
+ density: density,
431
594
  }));
432
595
  // we need to compress the mesh with the same compression method as the original file
433
596
  const root = doc.getRoot();
@@ -435,36 +598,40 @@ function createMeshLod(doc, basePath, level, mesh, guid, lodLevel) {
435
598
  if (compressionExt) {
436
599
  const newExt = newDoc.createExtension(NEEDLE_mesh_compression);
437
600
  const newProp = newExt.create(compressionExt.model);
438
- console.log(`${NAME}: Pass compression format to mesh LOD ${level} → ${JSON.stringify(compressionExt.model)}`);
601
+ console.log(`[${NAME}] Pass compression format to mesh LOD ${level} → ${JSON.stringify(compressionExt.model)}`);
439
602
  newDoc.getRoot().setExtension(NEEDLE_mesh_compression.EXTENSION_NAME, newProp);
440
603
  }
441
604
  const newIo = new NodeIO();
442
605
  registerExtensions(newIo);
443
- const ext = ".glb";
444
- const name = "mesh_lod_" + level + "_" + guid;
445
- const filename = name + ext;
446
- const outName = basePath + filename;
447
- console.log(`${NAME}: Write mesh LOD ${level} to \"${filename}\" with: ${JSON.stringify(lodLevel)}`);
448
- newIo.write(outName, newDoc);
449
- return filename;
606
+ console.log(`[${NAME}] Write mesh LOD ${level} with: ${JSON.stringify(lodLevel)} to \"${fileName}\"`);
607
+ await newIo.write(outPath, newDoc);
608
+ // const hasChanged = meshInfo.maxVertexCount == newMeshInformation.maxVertexCount && meshInfo.totalVertexCount == newMeshInformation.totalVertexCount;
609
+ // add to cache
610
+ if (cacheKey != undefined) {
611
+ const bytes = readFileSync(outPath);
612
+ addToCache(cacheKey, bytes);
613
+ }
614
+ return { level, path: fileName, success: true, lodMeshInfo: newMeshInformation, cacheKey: cacheKey, density: density };
450
615
  }
451
- function simplifyMesh(doc, mesh, prim, lodinfo) {
616
+ async function simplifyMesh(doc, mesh, prim, index, lodinfo) {
452
617
  const weldOptions = {
453
618
  ...WELD_DEFAULTS,
454
619
  overwrite: true,
620
+ ...lodinfo,
455
621
  };
456
622
  const simplifyOptions = {
457
- ...SIMPLIFY_DEFAULTS, simplifier: MeshoptSimplifier,
623
+ ...SIMPLIFY_DEFAULTS,
624
+ ...lodinfo,
625
+ simplifier: MeshoptSimplifier,
458
626
  };
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);
627
+ return compressPrimitive(doc, mesh, prim, index, weldOptions, simplifyOptions);
464
628
  }
465
- function isMeshLOD(filepath) {
629
+ export function isMeshLOD(filepath) {
466
630
  return filepath.includes("mesh_lod_");
467
631
  }
468
- function isImageLOD(filepath) {
632
+ export function isImageLOD(filepath) {
469
633
  return filepath.includes("image_");
470
634
  }
635
+ export function isLOD(filepath) {
636
+ return isMeshLOD(filepath) || isImageLOD(filepath);
637
+ }
@@ -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/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
 
2
-
3
2
  import { spawnSync } from "child_process";
3
+ import { getVersion } from "./dist/index.js";
4
+
4
5
  const workingDir = process.cwd();
5
6
 
6
7
  // Original invoked dir set by npm
@@ -28,6 +29,13 @@ function runWithArgs(cmd) {
28
29
  spawnSync(cmd + " " + commandline_args, defaultArgs);
29
30
  }
30
31
 
31
-
32
- runWithArgs("node make-progressive.mjs");
33
- runWithArgs("node pack-gltf.mjs");
32
+ try {
33
+ const startTime = Date.now();
34
+ runWithArgs("node make-progressive.mjs");
35
+ runWithArgs("node pack-gltf.mjs");
36
+ const durationInMinutes = ((Date.now() - startTime) / 1000 / 60).toFixed(1);
37
+ console.log(`[Needle Build Pipeline] v${getVersion()} — Finished progressive transform and optimization in ${durationInMinutes} minutes.`);
38
+ }
39
+ catch (err) {
40
+ console.error("Failed running commandline args:", err.message);
41
+ }
package/pack-gltf.mjs CHANGED
@@ -1,20 +1,18 @@
1
- import glob from 'glob';
2
1
  import path from 'path';
3
2
  import { NodeIO } from '@gltf-transform/core';
4
3
  import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
5
- import { draco } from '@gltf-transform/functions';
6
4
  import draco3d from 'draco3dgltf';
7
5
  import { ALL_EXTENSIONS as NEEDLE_EXTENSIONS } from "./dist/extensions/NEEDLE_opaque/index.js";
8
6
  import commandLineArgs from "command-line-args";
9
7
  import { NEEDLE_mesh_compression, NEEDLE_compression_texture } from './dist/extensions/index.js';
10
- import { existsSync, statSync } from 'fs';
8
+ import { existsSync, readFileSync, statSync, writeFileSync } from 'fs';
11
9
  import { needle_mesh_transform } from "./dist/transforms/needle_mesh_transform.js";
12
10
  import { needle_texture_transform } from "./dist/transforms/needle_texture_transform.js";
13
11
  import { needle_animation_transform } from "./dist/transforms/needle_animation_transform.js";
14
12
  import { foreachGLTF } from './fileutils.mjs';
15
13
  import { needle_asset } from './dist/transforms/needle_asset.js';
16
14
  import { MeshoptDecoder, MeshoptEncoder } from 'meshoptimizer';
17
- import { getVersion } from './dist/index.js';
15
+ import { getVersion, getHash, addToCache, tryGetFromCache, isLOD, ensureCacheSize as ensureMaxCacheSize, isMeshLOD } from './dist/index.js';
18
16
 
19
17
  console.log(`[Needle Build Pipeline] pack glTF — version ${getVersion()}`)
20
18
 
@@ -77,6 +75,9 @@ let totalFilesProcessed = 0;
77
75
  let totalFileSizeInMB = 0;
78
76
  let totalFileSizeInMBBefore = 0;
79
77
 
78
+ /** Compress a glTF file
79
+ * @param {string} file
80
+ */
80
81
  async function packFile(file) {
81
82
  if (!existsSync(file)) {
82
83
  console.error("WARN: can not pack, file does not exist: " + file);
@@ -86,6 +87,22 @@ async function packFile(file) {
86
87
  return;
87
88
  }
88
89
 
90
+ // check if it's in the cache.
91
+ // We only need to check mesh LOD files because image LODs are cache via our toktx transform
92
+ // We can not cache the main glTF files because the hash always changes for them
93
+ let cacheKey = undefined;
94
+ if (allowCaching && isMeshLOD(file)) {
95
+ const existing = readFileSync(file);
96
+ const hash = getHash(existing);
97
+ cacheKey = "packed-" + hash;
98
+ const cached = tryGetFromCache(cacheKey);
99
+ if (cached) {
100
+ console.log(`→ Loaded compressed file from cache: \"${path.basename(file)}\"`);
101
+ writeFileSync(file, cached);
102
+ return;
103
+ }
104
+ }
105
+
89
106
  if (packedFiles.has(file)) return;
90
107
  packedFiles.add(file);
91
108
  console.log("→ Begin packing: " + file);
@@ -123,6 +140,10 @@ async function packFile(file) {
123
140
  );
124
141
  if (debug) file += ".debug.glb";
125
142
  await io.write(file, document);
143
+
144
+ if (allowCaching && cacheKey != undefined && existsSync(file))
145
+ addToCache(cacheKey, readFileSync(file));
146
+
126
147
  const duration = Date.now() - startTime;
127
148
  const sizeInMB = statSync(file).size / 1024 / 1024;
128
149
  const filename = path.basename(file);
@@ -148,8 +169,17 @@ let startTime = Date.now();
148
169
  const inputArgs = args.slice(2);
149
170
  foreachGLTF(inputArgs, async (file) => {
150
171
  await packFile(file);
151
- }).then(() => {
172
+ }).then(async () => {
152
173
  const duration = Date.now() - startTime;
153
174
  const durationInMinutes = duration / 1000 / 60;
175
+ if (allowCaching) {
176
+ // if the user's cache directory has more than 25GB free space, we allow a cache of 2 GB - otherwise 1 GB
177
+ // const freeSpaceInMB = (await getCacheAvailableSpaceOnDisc().catch(() => 0)) / 1024 / 1024;
178
+ // const maxCacheSize = freeSpaceInMB > 25000 ? 2000 : 1000;
179
+ ensureMaxCacheSize(1000);
180
+ }
154
181
  console.log(`[Needle Build Pipeline] v${getVersion()} — Finished packing ${totalFilesProcessed} files in ${durationInMinutes.toFixed(1)} minutes; total size ${totalFileSizeInMBBefore.toFixed(1)} MB → ${totalFileSizeInMB.toFixed(1)} MB, total GPU memory: ${totalGPUMemoryInMB.toFixed(1)} MB`);
155
- })
182
+ }).catch(err => {
183
+ console.error("ERR: packing failed: " + err.message);
184
+ if (debug) throw err;
185
+ });
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.10",
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",
@@ -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": {