@needle-tools/gltf-build-pipeline 1.4.1-beta.1 → 1.4.1-beta.11
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 +31 -1
- package/dist/cache/cache.d.ts +5 -0
- package/dist/cache/cache.js +90 -26
- package/dist/extensions/NEEDLE_mesh_compression/NEEDLE_mesh_compression.js +3 -1
- package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.d.ts +12 -6
- package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.js +1 -5
- package/dist/extensions/NEEDLE_progressive_mesh_settings/index.d.ts +2 -2
- package/dist/extensions/NEEDLE_progressive_mesh_settings/index.js +1 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/transforms/needle_mesh_transform.d.ts +1 -1
- package/dist/transforms/needle_mesh_transform.js +22 -5
- package/dist/transforms/needle_progressive.d.ts +3 -0
- package/dist/transforms/needle_progressive.js +274 -69
- package/dist/utils/guid.d.ts +1 -0
- package/dist/utils/guid.js +5 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/mesh.d.ts +7 -0
- package/dist/utils/mesh.js +86 -0
- package/index.mjs +12 -4
- package/pack-gltf.mjs +36 -6
- package/package.json +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,10 +4,40 @@ 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-
|
|
7
|
+
## [1.4.1-beta.11] - 2023-03-20
|
|
8
|
+
- add: NEEDLE_progressive LOD meshes morph targets data
|
|
9
|
+
- 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)
|
|
10
|
+
- change: Unity NEEDLE_progressive logs
|
|
11
|
+
|
|
12
|
+
## [1.4.1-beta.10] - 2023-03-19
|
|
13
|
+
- change: NEEDLE_progressive calculates density per mesh level
|
|
14
|
+
- change: NEEDLE_progressive `uri` field is removed and added `lod` array of objects
|
|
15
|
+
|
|
16
|
+
## [1.4.1-beta.8] - 2023-03-18
|
|
17
|
+
- change: if an asset doesnt have a guid we use a uuid package now
|
|
18
|
+
- change: ignore mesh LODs with vertex count < 4
|
|
19
|
+
- change: allow generating mesh LODs without extension (if the mesh has > 32 vertices)
|
|
20
|
+
- change: omit generated LODs if the change is too small (e.g. the vertex count didnt change enough)
|
|
21
|
+
|
|
22
|
+
## [1.4.1-beta.6] - 2023-03-15
|
|
23
|
+
- add: caching support for mesh LODs
|
|
24
|
+
|
|
25
|
+
## [1.4.1-beta.5] - 2023-03-15
|
|
26
|
+
- add: auto LODs now calculate mesh density
|
|
27
|
+
- change: generate 6 LOD level
|
|
28
|
+
- change: use ratio 0.5 of the previous LOD level
|
|
29
|
+
- change: bump gltf-transform to 3.10.1 to fix simplifier
|
|
30
|
+
- change: add sparse transform to progressive transform
|
|
31
|
+
|
|
32
|
+
## [1.4.1-beta.3] - 2023-03-14
|
|
33
|
+
- fix: wait for mesh simplifier to be ready
|
|
34
|
+
|
|
35
|
+
## [1.4.1-beta.2] - 2023-03-13
|
|
8
36
|
- change: update gltf-transform to `^3.10.0`
|
|
9
37
|
- change: progressive mesh transform does now set ratio for LODs (see [simplifier issue 661 on github](https://github.com/zeux/meshoptimizer/issues/661))
|
|
10
38
|
- fix: issue where progressive meshes were not generated if a mesh was multiple times in the glTF
|
|
39
|
+
- change: simplify to use ratio, fix issue where not all meshes were processed
|
|
40
|
+
- change: auto LOD max error set to 0.1
|
|
11
41
|
|
|
12
42
|
## [1.4.0-beta] - 2023-03-12
|
|
13
43
|
- add: progressive mesh transform to automatically generate mesh LOD levels (currently we only generate one low poly version)
|
package/dist/cache/cache.d.ts
CHANGED
|
@@ -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;
|
package/dist/cache/cache.js
CHANGED
|
@@ -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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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.
|
|
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
|
|
3
|
+
export declare interface IProgressiveLoadingInfo {
|
|
4
|
+
/** guid of the asset the LODs belong to */
|
|
4
5
|
guid: string;
|
|
5
|
-
|
|
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:
|
|
12
|
-
create(data:
|
|
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:
|
|
28
|
+
model: IProgressiveLoadingInfo;
|
|
23
29
|
}
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import { Extension, ExtensionProperty, PropertyType
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
return this.maxSize !== undefined && this.maxSize > 0;
|
|
41
|
-
}
|
|
42
|
-
maxSize;
|
|
39
|
+
generateLods;
|
|
43
40
|
guid;
|
|
44
41
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -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>):
|
|
4
|
+
export declare function compressPrimitive(document: Document, mesh: Mesh, prim: Primitive, index: number, weldOptions: Required<WeldOptions>, meshOptions: Required<SimplifyOptions>): Promise<Primitive>;
|
|
@@ -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,17 +86,31 @@ 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(`[${
|
|
96
|
+
logger.debug(`weld: primitive[${index}] of mesh "${mesh.getName()}" → tolerance: ${weldOptions.tolerance}`);
|
|
94
97
|
weldPrimitive(document, prim, weldOptions);
|
|
95
|
-
|
|
96
|
-
|
|
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(`simplify: primitive[${index}] of mesh "${mesh.getName()}" → ratio: ${meshOptions.ratio}, error: ${meshOptions.error}, lockBorder: ${meshOptions.lockBorder}`);
|
|
113
|
+
return simplifyPrimitive(document, prim, meshOptions);
|
|
97
114
|
}
|
|
98
115
|
// function updatePrimitive(prim: Primitive, simplifier: any, positionArray, dstIndicesArray) {
|
|
99
116
|
// const position = prim.getAttribute('POSITION')!;
|
|
@@ -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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
127
|
+
logger.warn(`[${NAME}] Skipping, no settings found ${textureIndex}`);
|
|
127
128
|
continue;
|
|
128
129
|
}
|
|
129
130
|
if (!settings.isValid) {
|
|
130
|
-
logger.warn(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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,203 @@ 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(
|
|
309
|
+
console.log(`[${NAME}]: Begin generating progressive 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
|
-
{
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
316
|
-
const
|
|
336
|
+
/** The size and vertex count of the original mesh */
|
|
337
|
+
const originalMeshInformation = getMeshInformation(mesh);
|
|
338
|
+
const { totalVertexCount: originalTotalVertexCount, maxVertexCount: originalMaxVertexCount } = originalMeshInformation;
|
|
339
|
+
let settings = mesh.getExtension(NEEDLE_progressive_mesh_settings.EXTENSION_NAME);
|
|
317
340
|
if (!settings) {
|
|
318
|
-
|
|
341
|
+
if (originalMaxVertexCount < 32) {
|
|
342
|
+
logger.debug(`[${NAME}] Skipping, no settings found and no primitive with more than 32 vertices. mesh[${index}] has ${originalTotalVertexCount} vertices (max: ${originalMaxVertexCount})`);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
settings = {
|
|
347
|
+
guid: generateGuid(path + "_mesh_" + index.toString()),
|
|
348
|
+
generateLods: true,
|
|
349
|
+
};
|
|
350
|
+
logger.debug(`[${NAME}] No settings found for mesh[${index}] with ${originalTotalVertexCount} vertices (max: ${originalMaxVertexCount}) - will use default settings (guid: ${settings.guid})`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
else if (settings.generateLods === false) {
|
|
354
|
+
logger.debug(`[${NAME}] progressive mesh disabled for mesh at index ${index++}`);
|
|
319
355
|
continue;
|
|
320
356
|
}
|
|
321
357
|
const progressiveExt = doc.createExtension(NEEDLE_progressive);
|
|
322
358
|
if (!progressiveExt || !progressiveExt.create) {
|
|
323
|
-
logger.debug(
|
|
359
|
+
logger.debug(`[${NAME}] Skipping because progressive extension is not available. ${index++}`);
|
|
324
360
|
continue;
|
|
325
361
|
}
|
|
326
362
|
// check if we have already produced LODs for this mesh
|
|
327
363
|
let needsToGenerateLODs = true;
|
|
328
|
-
const
|
|
364
|
+
const generatedLODInfos = new Array();
|
|
329
365
|
if (visited.has(settings.guid)) {
|
|
330
366
|
// we can not just set the extension here...
|
|
331
|
-
logger.debug(
|
|
367
|
+
logger.debug(`[${NAME}] Mesh has already been processed ${index} - will use previously generated LODs`);
|
|
332
368
|
needsToGenerateLODs = false;
|
|
333
|
-
const existingPaths = visited.get(settings.guid)
|
|
334
|
-
|
|
369
|
+
const existingPaths = visited.get(settings.guid);
|
|
370
|
+
generatedLODInfos.push(...existingPaths.infos);
|
|
335
371
|
}
|
|
372
|
+
/** async simplification tasks... */
|
|
373
|
+
const tasks = new Array();
|
|
336
374
|
if (needsToGenerateLODs) {
|
|
375
|
+
console.log(`[${NAME}] Begin generating mesh LODs for \"${mesh.getName()}\"`);
|
|
337
376
|
// produce LODS, skip the last level because it will be saved in the original file
|
|
338
377
|
for (let level = 0; level < LODS.length - 1; level++) {
|
|
339
378
|
const lod = LODS[level];
|
|
340
|
-
console.log(`${NAME}: Generate LOD ${level}`);
|
|
341
379
|
const guid = settings.guid;
|
|
342
|
-
|
|
343
|
-
|
|
380
|
+
// TODO: check if the LOD level did actually change the mesh - otherwise we can just skip generating lower resolutions
|
|
381
|
+
const lodTask = createMeshLod(doc, outputDirectory, level, mesh, originalMeshInformation, guid, lod, opts);
|
|
382
|
+
lodTask.then(res => {
|
|
383
|
+
if (res?.success) {
|
|
384
|
+
generatedLODInfos[level] = res;
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
tasks.push(lodTask);
|
|
388
|
+
}
|
|
389
|
+
// wait for all LODs to be generated
|
|
390
|
+
await Promise.all(tasks);
|
|
391
|
+
tasks.length = 0;
|
|
392
|
+
}
|
|
393
|
+
// compare LOD results and ignore if the mesh did not change
|
|
394
|
+
// TODO: this should actually check mesh semantics (perhaps hash the mesh using the caching system?)
|
|
395
|
+
let prevSize = -Infinity;
|
|
396
|
+
for (let i = 0; i < generatedLODInfos.length; i++) {
|
|
397
|
+
const lodInfo = generatedLODInfos[i];
|
|
398
|
+
if (!lodInfo) {
|
|
399
|
+
console.log(`[${NAME}] LOD ${i} doesnt exist`);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
const fullPath = outputDirectory + lodInfo.path;
|
|
403
|
+
if (existsSync(fullPath)) {
|
|
404
|
+
const lastSize = prevSize;
|
|
405
|
+
const sizeOnDisc = statSync(fullPath).size;
|
|
406
|
+
const diff = lastSize - sizeOnDisc;
|
|
407
|
+
const level = lodInfo.level;
|
|
408
|
+
let shouldOmitLevel = false;
|
|
409
|
+
// if the mesh size on disc is identical to the last level, we can omit this level
|
|
410
|
+
if (sizeOnDisc <= 0) {
|
|
411
|
+
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}\"`);
|
|
412
|
+
shouldOmitLevel = true;
|
|
413
|
+
}
|
|
414
|
+
else if (diff == 0) {
|
|
415
|
+
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}\"`);
|
|
416
|
+
shouldOmitLevel = true;
|
|
417
|
+
}
|
|
418
|
+
else if (!shouldOmitLevel) {
|
|
419
|
+
// 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
|
|
420
|
+
const meshInfo = lodInfo.lodMeshInfo;
|
|
421
|
+
const lastMeshInfo = generatedLODInfos[i - 1]?.lodMeshInfo;
|
|
422
|
+
if (meshInfo && lastMeshInfo) {
|
|
423
|
+
const vertexDifference = lastMeshInfo.totalVertexCount - meshInfo.totalVertexCount;
|
|
424
|
+
const diff01 = vertexDifference / lastMeshInfo.totalVertexCount;
|
|
425
|
+
if (vertexDifference < 0) {
|
|
426
|
+
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)}`);
|
|
427
|
+
shouldOmitLevel = true;
|
|
428
|
+
}
|
|
429
|
+
else if (vertexDifference < 32 || diff01 < 0.05) {
|
|
430
|
+
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}\"`);
|
|
431
|
+
shouldOmitLevel = true;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (shouldOmitLevel && opts.useCache != false && lodInfo.cacheKey != undefined) {
|
|
435
|
+
// we invalidate the cached entry because the mesh change was too small
|
|
436
|
+
addToCache(lodInfo.cacheKey, new Uint8Array(0));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (shouldOmitLevel) {
|
|
440
|
+
generatedLODInfos.splice(i, 1);
|
|
441
|
+
rmSync(fullPath);
|
|
442
|
+
i--;
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
prevSize = sizeOnDisc;
|
|
446
|
+
}
|
|
344
447
|
}
|
|
345
448
|
}
|
|
449
|
+
let maxDensity = 0;
|
|
450
|
+
let primitiveIndex = 0;
|
|
451
|
+
const lodLevelInFile = generatedLODInfos.length;
|
|
346
452
|
for (const prim of mesh.listPrimitives()) {
|
|
347
|
-
const lastLod = LODS[
|
|
348
|
-
|
|
349
|
-
|
|
453
|
+
const lastLod = LODS[lodLevelInFile];
|
|
454
|
+
// TODO: this should be run after simplification but then we need to change the runtime to use the LOD 0 density
|
|
455
|
+
const density = calculateMeshDensity(prim);
|
|
456
|
+
maxDensity = Math.max(maxDensity, density);
|
|
457
|
+
const task = simplifyMesh(doc, mesh, prim, primitiveIndex++, lastLod);
|
|
458
|
+
tasks.push(task);
|
|
350
459
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
460
|
+
await Promise.all(tasks);
|
|
461
|
+
tasks.length = 0;
|
|
462
|
+
const { totalVertexCount: totalVertexCountAfter } = getMeshInformation(mesh);
|
|
463
|
+
const difference = originalTotalVertexCount - totalVertexCountAfter;
|
|
464
|
+
const diff01 = difference / originalTotalVertexCount;
|
|
465
|
+
if (totalVertexCountAfter <= 0) {
|
|
466
|
+
logger.error(`ERR: [${NAME}]: Mesh[${index}] has no vertices after simplification: ${mesh.getName()}`);
|
|
467
|
+
}
|
|
468
|
+
else if (difference < 32 || diff01 <= 0.05) {
|
|
469
|
+
// 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
|
|
470
|
+
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()}\"`);
|
|
471
|
+
try {
|
|
472
|
+
for (const lodInfo of generatedLODInfos) {
|
|
473
|
+
rmSync(outputDirectory + "/" + lodInfo.path);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
catch (err) {
|
|
477
|
+
logger.error(`ERR: [${NAME}] Failed to remove LOD files for \"${mesh.getName()}\"`);
|
|
478
|
+
}
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (generatedLODInfos.length > 0) {
|
|
482
|
+
const model = progressiveExt.create({
|
|
483
|
+
guid: settings.guid,
|
|
484
|
+
lods: generatedLODInfos.map(l => {
|
|
485
|
+
return {
|
|
486
|
+
path: l.path,
|
|
487
|
+
density: l.density,
|
|
488
|
+
};
|
|
489
|
+
}),
|
|
490
|
+
density: maxDensity,
|
|
361
491
|
});
|
|
492
|
+
mesh.setExtension(NEEDLE_PROGRESSIVE_EXTENSION_NAME, model);
|
|
493
|
+
index++;
|
|
494
|
+
if (!visited.has(settings.guid)) {
|
|
495
|
+
visited.set(settings.guid, {
|
|
496
|
+
model: model,
|
|
497
|
+
infos: generatedLODInfos,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
362
500
|
}
|
|
363
501
|
}
|
|
364
|
-
console.log(
|
|
502
|
+
console.log(`[${NAME}]: Finished generating progressive meshes for \"${filename}\"`);
|
|
365
503
|
});
|
|
366
504
|
}
|
|
367
505
|
/**
|
|
@@ -371,19 +509,47 @@ export function make_progressive_meshes(filePath, _options, opts) {
|
|
|
371
509
|
* @param guid a unique identifier for the mesh
|
|
372
510
|
* @param error the error to use for simplification. Higher means more simplification
|
|
373
511
|
*/
|
|
374
|
-
function createMeshLod(doc, basePath, level, mesh, guid, lodLevel) {
|
|
512
|
+
async function createMeshLod(doc, basePath, level, mesh, meshInfo, guid, lodLevel, opts) {
|
|
513
|
+
const fileExtension = ".glb";
|
|
514
|
+
const fileName_withoutExtension = "mesh_lod_" + level + "_" + guid;
|
|
515
|
+
const fileName = fileName_withoutExtension + fileExtension;
|
|
516
|
+
const outPath = basePath + fileName;
|
|
517
|
+
const primitives = mesh.listPrimitives();
|
|
518
|
+
let cacheKey = undefined;
|
|
519
|
+
if (opts.useCache !== false) {
|
|
520
|
+
// generate hash for mesh
|
|
521
|
+
let hash = mesh["needle:mesh-hash"];
|
|
522
|
+
if (hash === undefined) {
|
|
523
|
+
hash = getHash(mesh);
|
|
524
|
+
mesh["needle:mesh-hash"] = hash;
|
|
525
|
+
}
|
|
526
|
+
hash += getHash(lodLevel);
|
|
527
|
+
cacheKey = `mesh_lod_${level}_${hash}`;
|
|
528
|
+
const cachedValue = tryGetFromCache(cacheKey);
|
|
529
|
+
if (cachedValue) {
|
|
530
|
+
console.log(`[${NAME}] LOD ${level} loaded ${(cachedValue.length / 1024).toFixed()} KB from cache for \"${mesh.getName()}\"`);
|
|
531
|
+
writeFileSync(outPath, cachedValue);
|
|
532
|
+
return { level, path: fileName, success: true, cacheKey: cacheKey };
|
|
533
|
+
}
|
|
534
|
+
}
|
|
375
535
|
const newDoc = new Document();
|
|
376
536
|
const newScene = newDoc.createScene();
|
|
377
537
|
newDoc.createBuffer();
|
|
378
538
|
const newNode = newDoc.createNode();
|
|
379
539
|
newScene.addChild(newNode);
|
|
380
|
-
newNode.setName("
|
|
540
|
+
newNode.setName("LOD " + level + "_" + guid);
|
|
381
541
|
newNode.setTranslation([0, 0, 0]);
|
|
382
542
|
const newMesh = newDoc.createMesh();
|
|
543
|
+
newMesh.setName(mesh.getName() + " LOD " + level);
|
|
383
544
|
newNode.setMesh(newMesh);
|
|
384
|
-
const
|
|
545
|
+
const newMat = newDoc.createMaterial();
|
|
546
|
+
const weights = mesh.getWeights();
|
|
547
|
+
if (weights) {
|
|
548
|
+
newMesh.setWeights(weights);
|
|
549
|
+
}
|
|
550
|
+
const tasks = new Array();
|
|
551
|
+
// let didRunSimplification = false;
|
|
385
552
|
for (const prim of primitives) {
|
|
386
|
-
const newMat = newDoc.createMaterial();
|
|
387
553
|
const newPrim = newDoc.createPrimitive();
|
|
388
554
|
newMesh.addPrimitive(newPrim);
|
|
389
555
|
newPrim.setName(prim.getName());
|
|
@@ -398,6 +564,9 @@ function createMeshLod(doc, basePath, level, mesh, guid, lodLevel) {
|
|
|
398
564
|
newAttr.setType(attr.getType());
|
|
399
565
|
newPrim.setAttribute(semantics[i], newAttr);
|
|
400
566
|
}
|
|
567
|
+
else {
|
|
568
|
+
console.warn(`[${NAME}] Missing attribute ${semantics[i]} in mesh LOD ${level}`);
|
|
569
|
+
}
|
|
401
570
|
}
|
|
402
571
|
const indices = prim.getIndices();
|
|
403
572
|
if (indices) {
|
|
@@ -406,21 +575,52 @@ function createMeshLod(doc, basePath, level, mesh, guid, lodLevel) {
|
|
|
406
575
|
newIndices.setType(indices.getType());
|
|
407
576
|
newPrim.setIndices(newIndices);
|
|
408
577
|
}
|
|
578
|
+
const targets = prim.listTargets();
|
|
579
|
+
for (const target of targets) {
|
|
580
|
+
const newTarget = newDoc.createPrimitiveTarget(target.getName());
|
|
581
|
+
newPrim.addTarget(newTarget);
|
|
582
|
+
for (const attr of target.listSemantics()) {
|
|
583
|
+
const accessor = target.getAttribute(attr);
|
|
584
|
+
if (accessor) {
|
|
585
|
+
const newAccessor = newDoc.createAccessor();
|
|
586
|
+
newAccessor.setArray(accessor.getArray());
|
|
587
|
+
newAccessor.setType(accessor.getType());
|
|
588
|
+
newTarget.setAttribute(attr, newAccessor);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
;
|
|
409
593
|
const extensions = prim.listExtensions();
|
|
410
594
|
for (const ext of extensions) {
|
|
411
595
|
const PropertyClass = ext.constructor;
|
|
412
596
|
const newProp = new PropertyClass(newDoc.getGraph());
|
|
413
597
|
const copy = newProp.copy(ext);
|
|
414
598
|
const name = ext.extensionName;
|
|
415
|
-
console.log(`${NAME}: Copy primitive ${name} extension`);
|
|
416
599
|
newPrim.setExtension(name, copy);
|
|
417
600
|
}
|
|
418
601
|
}
|
|
602
|
+
let primitiveIndex = 0;
|
|
419
603
|
if (lodLevel.error != undefined || lodLevel.ratio != undefined) {
|
|
420
|
-
|
|
421
|
-
|
|
604
|
+
if (lodLevel.error > 0 || lodLevel.ratio < 1) {
|
|
605
|
+
for (const newPrim of newMesh.listPrimitives()) {
|
|
606
|
+
// didRunSimplification = true;
|
|
607
|
+
const task = simplifyMesh(newDoc, newMesh, newPrim, primitiveIndex++, lodLevel);
|
|
608
|
+
tasks.push(task);
|
|
609
|
+
}
|
|
422
610
|
}
|
|
423
611
|
}
|
|
612
|
+
await Promise.all(tasks);
|
|
613
|
+
tasks.length = 0;
|
|
614
|
+
const newMeshInformation = getMeshInformation(newMesh);
|
|
615
|
+
if (newMeshInformation.totalVertexCount < 4) {
|
|
616
|
+
console.error(`[${NAME}] mesh LOD ${level} is has less than 4 vertices → skipping`);
|
|
617
|
+
return { level, path: fileName, success: false, cacheKey: cacheKey };
|
|
618
|
+
}
|
|
619
|
+
let density = 0;
|
|
620
|
+
for (const prim of newMesh.listPrimitives()) {
|
|
621
|
+
const d = calculateMeshDensity(prim);
|
|
622
|
+
density = Math.max(density, d);
|
|
623
|
+
}
|
|
424
624
|
const progressiveExt = newDoc.createExtension(NEEDLE_progressive);
|
|
425
625
|
if (!progressiveExt || !progressiveExt.create) {
|
|
426
626
|
console.error("Failed to create progressive extension");
|
|
@@ -428,6 +628,7 @@ function createMeshLod(doc, basePath, level, mesh, guid, lodLevel) {
|
|
|
428
628
|
}
|
|
429
629
|
newMesh.setExtension(NEEDLE_PROGRESSIVE_EXTENSION_NAME, progressiveExt.create({
|
|
430
630
|
guid: guid,
|
|
631
|
+
density: density,
|
|
431
632
|
}));
|
|
432
633
|
// we need to compress the mesh with the same compression method as the original file
|
|
433
634
|
const root = doc.getRoot();
|
|
@@ -435,36 +636,40 @@ function createMeshLod(doc, basePath, level, mesh, guid, lodLevel) {
|
|
|
435
636
|
if (compressionExt) {
|
|
436
637
|
const newExt = newDoc.createExtension(NEEDLE_mesh_compression);
|
|
437
638
|
const newProp = newExt.create(compressionExt.model);
|
|
438
|
-
console.log(
|
|
639
|
+
console.log(`[${NAME}] Pass compression format to mesh LOD ${level} → ${JSON.stringify(compressionExt.model)}`);
|
|
439
640
|
newDoc.getRoot().setExtension(NEEDLE_mesh_compression.EXTENSION_NAME, newProp);
|
|
440
641
|
}
|
|
441
642
|
const newIo = new NodeIO();
|
|
442
643
|
registerExtensions(newIo);
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
644
|
+
console.log(`[${NAME}] Write mesh LOD ${level} with to \"${fileName}\"`);
|
|
645
|
+
await newIo.write(outPath, newDoc);
|
|
646
|
+
// const hasChanged = meshInfo.maxVertexCount == newMeshInformation.maxVertexCount && meshInfo.totalVertexCount == newMeshInformation.totalVertexCount;
|
|
647
|
+
// add to cache
|
|
648
|
+
if (cacheKey != undefined) {
|
|
649
|
+
const bytes = readFileSync(outPath);
|
|
650
|
+
addToCache(cacheKey, bytes);
|
|
651
|
+
}
|
|
652
|
+
return { level, path: fileName, success: true, lodMeshInfo: newMeshInformation, cacheKey: cacheKey, density: density };
|
|
450
653
|
}
|
|
451
|
-
function simplifyMesh(doc, mesh, prim, lodinfo) {
|
|
654
|
+
async function simplifyMesh(doc, mesh, prim, index, lodinfo) {
|
|
452
655
|
const weldOptions = {
|
|
453
656
|
...WELD_DEFAULTS,
|
|
454
657
|
overwrite: true,
|
|
658
|
+
...lodinfo,
|
|
455
659
|
};
|
|
456
660
|
const simplifyOptions = {
|
|
457
|
-
...SIMPLIFY_DEFAULTS,
|
|
661
|
+
...SIMPLIFY_DEFAULTS,
|
|
662
|
+
...lodinfo,
|
|
663
|
+
simplifier: MeshoptSimplifier,
|
|
458
664
|
};
|
|
459
|
-
|
|
460
|
-
simplifyOptions.error = lodinfo.error;
|
|
461
|
-
if (lodinfo.ratio != undefined)
|
|
462
|
-
simplifyOptions.ratio = lodinfo.ratio;
|
|
463
|
-
compressPrimitive(doc, mesh, prim, weldOptions, simplifyOptions);
|
|
665
|
+
return compressPrimitive(doc, mesh, prim, index, weldOptions, simplifyOptions);
|
|
464
666
|
}
|
|
465
|
-
function isMeshLOD(filepath) {
|
|
667
|
+
export function isMeshLOD(filepath) {
|
|
466
668
|
return filepath.includes("mesh_lod_");
|
|
467
669
|
}
|
|
468
|
-
function isImageLOD(filepath) {
|
|
670
|
+
export function isImageLOD(filepath) {
|
|
469
671
|
return filepath.includes("image_");
|
|
470
672
|
}
|
|
673
|
+
export function isLOD(filepath) {
|
|
674
|
+
return isMeshLOD(filepath) || isImageLOD(filepath);
|
|
675
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function generateGuid(key: string): any;
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
|
@@ -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
|
-
|
|
33
|
-
runWithArgs("node
|
|
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.11",
|
|
4
4
|
"description": "Pipeline and tools for optimizing gltf files using gltf-transform and compression settings within glTF extensions",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
"compile": "tsc || exit 1"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@gltf-transform/core": "^3.10.
|
|
20
|
-
"@gltf-transform/extensions": "^3.10.
|
|
21
|
-
"@gltf-transform/functions": "^3.10.
|
|
19
|
+
"@gltf-transform/core": "^3.10.1",
|
|
20
|
+
"@gltf-transform/extensions": "^3.10.1",
|
|
21
|
+
"@gltf-transform/functions": "^3.10.1",
|
|
22
22
|
"command-exists": "^1.2.9",
|
|
23
23
|
"command-line-args": "^5.2.1",
|
|
24
24
|
"draco3dgltf": "^1.5.3",
|
|
@@ -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": {
|