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