@needle-tools/gltf-build-pipeline 2.0.0-alpha → 2.1.0-alpha.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 +30 -0
- package/dist/___config.json +3 -0
- package/dist/package.json +1 -1
- package/dist/src/cache/cache.d.ts +1 -0
- package/dist/src/cache/cache.js +1 -0
- package/dist/src/cli/index.js +10 -10
- package/dist/src/config/index.d.ts +6 -2
- package/dist/src/config/index.js +36 -38
- package/dist/src/extensions/NEEDLE_opaque/NEEDLE_opaque.js +22 -1
- package/dist/src/extensions/NEEDLE_progressive/NEEDLE_progressive.d.ts +5 -1
- package/dist/src/scripts/pack-gltf.js +5 -3
- package/dist/src/transforms/needle_mesh_transform.js +8 -8
- package/dist/src/transforms/needle_progressive.js +128 -62
- package/dist/src/transforms/needle_toktx.js +5 -1
- package/dist/src/utils/fileutils.js +1 -1
- package/dist/src/utils/stats.d.ts +3 -2
- package/dist/src/utils/stats.js +3 -3
- package/package.json +1 -1
- package/scripts/pack-gltf.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,36 @@ 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
|
+
## [2.1.0-alpha] - 2023-06-03
|
|
8
|
+
- add: compression config support for high-level usecases that can be passed via commandline arguments or via config. Available options are currently `default`, `world` and `product`.
|
|
9
|
+
- add: texture LODs can now be generated by passing in the path to a compression config. `--config <path/to/config.json>`
|
|
10
|
+
- change: VRMs now use draco mesh compression by default
|
|
11
|
+
|
|
12
|
+
### Config Examples
|
|
13
|
+
- `default` - use default compression. When not explictly configured textures will be compressed with ETC1S
|
|
14
|
+
- `product` - when not explictly configured textures will use UASTC for `metallicRoughnessTexture` and `normalTexture`
|
|
15
|
+
- `world` - generate texture LODs when enabled (and not explictly disabled by `textures: { lods: false }`)
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"usecase": "default" // optional
|
|
19
|
+
"textures" : {
|
|
20
|
+
"lods": true
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
The config can also be part of a `needle.config.json`:
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
... regular needle.config
|
|
28
|
+
"gltf": {
|
|
29
|
+
"usecase": "default" // optional
|
|
30
|
+
"textures": {
|
|
31
|
+
"lods": true
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
7
37
|
## [2.0.0-alpha] - 2023-06-01
|
|
8
38
|
- update gltf-transform dependency to 4.x
|
|
9
39
|
- change: ensure ETC1S and UASTC textures are power of 2
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@needle-tools/gltf-build-pipeline",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0-alpha.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",
|
|
@@ -48,6 +48,7 @@ export declare function getKeyWithHash(property: ExtensibleProperty): {
|
|
|
48
48
|
/**
|
|
49
49
|
* Get the hash for a property or an object.
|
|
50
50
|
* @param property The property to get the hash for.
|
|
51
|
+
* @param level The current level of recursion.
|
|
51
52
|
* @returns The hash as a number.
|
|
52
53
|
*/
|
|
53
54
|
export declare function getHash(property: ExtensibleProperty | object, level?: number): number;
|
package/dist/src/cache/cache.js
CHANGED
|
@@ -167,6 +167,7 @@ export function getKeyWithHash(property) {
|
|
|
167
167
|
/**
|
|
168
168
|
* Get the hash for a property or an object.
|
|
169
169
|
* @param property The property to get the hash for.
|
|
170
|
+
* @param level The current level of recursion.
|
|
170
171
|
* @returns The hash as a number.
|
|
171
172
|
*/
|
|
172
173
|
export function getHash(property, level = 0) {
|
package/dist/src/cli/index.js
CHANGED
|
@@ -45,11 +45,12 @@ Compress a single glTF file. The input file is compressed and written to the out
|
|
|
45
45
|
totalFileSizeInMB: 0,
|
|
46
46
|
totalFileSizeInMBBefore: 0,
|
|
47
47
|
};
|
|
48
|
-
|
|
48
|
+
const file = args.input.toString();
|
|
49
|
+
let config = getConfig(options.config?.toString(), file);
|
|
49
50
|
if (options.usecase) {
|
|
50
51
|
logger.info("Using usecase: " + options.usecase);
|
|
51
52
|
config = { ...createConfig(options.usecase.toString()), ...config, };
|
|
52
|
-
if (config.usecase != "
|
|
53
|
+
if (config.usecase != "default" && config.usecase != options.usecase.toString()) {
|
|
53
54
|
logger.warn(`Conflicting usecase. Your specific config at \"${options.config}\" has a different usecase than the one provided by the CLI: \"${options.usecase}\". Using the usecase from the config file.`);
|
|
54
55
|
}
|
|
55
56
|
else {
|
|
@@ -57,7 +58,6 @@ Compress a single glTF file. The input file is compressed and written to the out
|
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
const debug = options.debug === true;
|
|
60
|
-
const file = args.input.toString();
|
|
61
61
|
const isDirectory = statSync(file).isDirectory();
|
|
62
62
|
const outDir = isDirectory ? file : dirname(file);
|
|
63
63
|
let outputIsDirectory = false;
|
|
@@ -98,11 +98,11 @@ Compress a single glTF file. The input file is compressed and written to the out
|
|
|
98
98
|
process.exit(ERROR_CODES.PACKING_FAILED);
|
|
99
99
|
}
|
|
100
100
|
});
|
|
101
|
-
printStats(stats);
|
|
101
|
+
printStats(stats, logger, config);
|
|
102
102
|
console.log("Stats: ", options);
|
|
103
103
|
if (options.stats) {
|
|
104
104
|
const statsFile = (isDirectory ? (path.join(file, getDirName(file))) : file) + ".needle.stats.json";
|
|
105
|
-
writeFileStatsToFile(file_stats, statsFile);
|
|
105
|
+
writeFileStatsToFile(config, file_stats, statsFile);
|
|
106
106
|
}
|
|
107
107
|
})
|
|
108
108
|
.command("transform", "Transform (progressive loading) and compress glTF, GLB or VRM files")
|
|
@@ -126,18 +126,18 @@ Each version will be compressed and written to the output directory.
|
|
|
126
126
|
totalFileSizeInMB: 0,
|
|
127
127
|
totalFileSizeInMBBefore: 0,
|
|
128
128
|
};
|
|
129
|
-
|
|
129
|
+
const file = args.input.toString();
|
|
130
|
+
let config = getConfig(options.config?.toString(), file);
|
|
130
131
|
if (options.usecase) {
|
|
131
132
|
logger.info("Using usecase: " + options.usecase);
|
|
132
133
|
config = { ...createConfig(options.usecase.toString()), ...config, };
|
|
133
|
-
if (config.usecase != "
|
|
134
|
+
if (config.usecase != "default" && config.usecase != options.usecase.toString()) {
|
|
134
135
|
logger.warn(`Conflicting usecase. Your specific config at \"${options.config}\" has a different usecase than the one provided by the CLI: \"${options.usecase}\". Using the usecase from the config file.`);
|
|
135
136
|
}
|
|
136
137
|
else {
|
|
137
138
|
config.usecase = options.usecase.toString();
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
|
-
let file = args.input.toString();
|
|
141
141
|
const isDirectory = statSync(file).isDirectory();
|
|
142
142
|
const outDir = isDirectory ? file : dirname(file);
|
|
143
143
|
const file_stats = [];
|
|
@@ -175,10 +175,10 @@ Each version will be compressed and written to the output directory.
|
|
|
175
175
|
process.exit(ERROR_CODES.PACKING_FAILED);
|
|
176
176
|
}
|
|
177
177
|
});
|
|
178
|
-
printStats(stats);
|
|
178
|
+
printStats(stats, logger, config);
|
|
179
179
|
if (options.stats) {
|
|
180
180
|
const statsFile = (isDirectory ? (path.join(file, getDirName(file))) : file) + ".needle.stats.json";
|
|
181
|
-
writeFileStatsToFile(file_stats, statsFile);
|
|
181
|
+
writeFileStatsToFile(config, file_stats, statsFile);
|
|
182
182
|
}
|
|
183
183
|
});
|
|
184
184
|
program.run();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
declare type Usecase = "
|
|
1
|
+
declare type Usecase = "default" | "product" | "world";
|
|
2
2
|
export declare const UsecaseOptions: string[];
|
|
3
3
|
/**
|
|
4
4
|
* The config object contains information about how a file should be processed.
|
|
@@ -6,6 +6,9 @@ export declare const UsecaseOptions: string[];
|
|
|
6
6
|
export type Config = {
|
|
7
7
|
/** High-level configuration */
|
|
8
8
|
usecase: Usecase;
|
|
9
|
+
textures?: {
|
|
10
|
+
lods: boolean;
|
|
11
|
+
};
|
|
9
12
|
};
|
|
10
13
|
/**
|
|
11
14
|
* Create a config object with the given usecase.
|
|
@@ -39,5 +42,6 @@ export declare function createConfig(usecase: string): {
|
|
|
39
42
|
*
|
|
40
43
|
*
|
|
41
44
|
*/
|
|
42
|
-
export declare function getConfig(
|
|
45
|
+
export declare function getConfig(configPath?: string, fallbackSearchDirectory?: string): Config;
|
|
46
|
+
export declare function searchConfig(directory: string): Config | null;
|
|
43
47
|
export {};
|
package/dist/src/config/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "fs";
|
|
2
|
-
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
2
|
+
import path, { dirname } from "path";
|
|
3
|
+
export const UsecaseOptions = ["product", "world"];
|
|
3
4
|
const defaultConfig = {
|
|
4
|
-
usecase: "
|
|
5
|
+
usecase: "default",
|
|
5
6
|
};
|
|
6
7
|
/**
|
|
7
8
|
* Create a config object with the given usecase.
|
|
@@ -40,49 +41,46 @@ export function createConfig(usecase) {
|
|
|
40
41
|
*
|
|
41
42
|
*
|
|
42
43
|
*/
|
|
43
|
-
export function getConfig(
|
|
44
|
-
if (
|
|
45
|
-
if (!existsSync(
|
|
46
|
-
throw new Error(`No config found at \"${
|
|
44
|
+
export function getConfig(configPath, fallbackSearchDirectory) {
|
|
45
|
+
if (configPath != null) {
|
|
46
|
+
if (!existsSync(configPath)) {
|
|
47
|
+
throw new Error(`No config found at \"${configPath}\"`);
|
|
48
|
+
}
|
|
49
|
+
const res = resolveConfig(configPath, readFileSync(configPath, "utf8"));
|
|
50
|
+
if (res) {
|
|
51
|
+
return res;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
throw new Error(`Failed parsing config at \"${configPath}\"`);
|
|
47
55
|
}
|
|
48
|
-
return resolveConfig(path, readFileSync(path, "utf8"));
|
|
49
56
|
}
|
|
50
57
|
if (_cachedConfig !== undefined) {
|
|
51
58
|
return _cachedConfig;
|
|
52
59
|
}
|
|
53
60
|
// Resolve the config once and cache it
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
// Check if a needle.config.json exists in the directory from which the script is run (or any parent directory)
|
|
62
|
+
if (fallbackSearchDirectory) {
|
|
63
|
+
const result = searchConfig(fallbackSearchDirectory);
|
|
64
|
+
if (result) {
|
|
65
|
+
_cachedConfig = { ...defaultConfig, ...result };
|
|
66
|
+
console.log("INFO: Using config with usecase: " + _cachedConfig.usecase);
|
|
67
|
+
}
|
|
58
68
|
}
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
// If no config was found, use the default config
|
|
70
|
+
if (!_cachedConfig) {
|
|
71
|
+
console.log("INFO: No config found. Using default config.");
|
|
61
72
|
_cachedConfig = defaultConfig;
|
|
62
73
|
}
|
|
63
74
|
return _cachedConfig;
|
|
64
75
|
}
|
|
65
76
|
let _cachedConfig = undefined;
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
for (let i = 0; i < args.length; i++) {
|
|
70
|
-
if (args[i] === "--config") {
|
|
71
|
-
if (i + 1 < args.length) {
|
|
72
|
-
const configPath = args[i + 1];
|
|
73
|
-
if (existsSync(configPath)) {
|
|
74
|
-
const content = readFileSync(configPath, "utf8");
|
|
75
|
-
return resolveConfig(configPath, content);
|
|
76
|
-
}
|
|
77
|
-
throw new Error(`Config file not found at path configured by CLI --config argument: \"${configPath}\"`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
77
|
+
export function searchConfig(directory) {
|
|
78
|
+
if (statSync(directory).isFile()) {
|
|
79
|
+
directory = dirname(directory);
|
|
80
80
|
}
|
|
81
|
-
|
|
82
|
-
const workingDir = process.cwd();
|
|
83
|
-
let directory = workingDir.replaceAll("\\", "/");
|
|
81
|
+
directory = directory.replaceAll("\\", "/");
|
|
84
82
|
while (directory.length > 0) {
|
|
85
|
-
const configPath =
|
|
83
|
+
const configPath = path.join(directory, "needle.config.json");
|
|
86
84
|
if (existsSync(configPath)) {
|
|
87
85
|
const content = readFileSync(configPath, "utf8");
|
|
88
86
|
const config = resolveConfig(configPath, content);
|
|
@@ -103,16 +101,16 @@ function resolveConfig(filename, content) {
|
|
|
103
101
|
if (filename.endsWith("needle.config.json")) {
|
|
104
102
|
try {
|
|
105
103
|
const config = JSON.parse(content);
|
|
106
|
-
if (config.
|
|
107
|
-
console.log("INFO: Found
|
|
104
|
+
if (config.gltf) {
|
|
105
|
+
console.log("INFO: Found \"gltf\" config at \"" + filename + "\".");
|
|
108
106
|
// If the optimization config is a string, it's the usecase
|
|
109
|
-
if (typeof config.
|
|
110
|
-
return createConfig(config.
|
|
107
|
+
if (typeof config.gltf === "string") {
|
|
108
|
+
return createConfig(config.gltf);
|
|
111
109
|
}
|
|
112
|
-
return config.
|
|
110
|
+
return config.gltf;
|
|
113
111
|
}
|
|
114
112
|
else {
|
|
115
|
-
console.log("INFO: No
|
|
113
|
+
console.log("INFO: No \"gltf\" config found in needle.config.json. Using default config.");
|
|
116
114
|
return null;
|
|
117
115
|
}
|
|
118
116
|
}
|
|
@@ -624,6 +624,27 @@ class JsonPointerHandler {
|
|
|
624
624
|
pointer?.register(obj, key, fullPath);
|
|
625
625
|
}
|
|
626
626
|
}
|
|
627
|
+
if (value.startsWith("/animations/")) {
|
|
628
|
+
if (this.debug)
|
|
629
|
+
console.log("Read pointer", value);
|
|
630
|
+
const index = parseInt(value.substring("/animations/".length));
|
|
631
|
+
const object = context.animations[index];
|
|
632
|
+
if (object) {
|
|
633
|
+
let pointer = this.map.get(value);
|
|
634
|
+
if (!pointer) {
|
|
635
|
+
// TODO: resolve animations + we need to set the reference properly so we can use prune. See https://linear.app/needle/issue/NE-4982
|
|
636
|
+
// const definition = context.jsonDoc.json.animations![index];
|
|
637
|
+
// const res = new AnimationIndexResolver(this.document, object, definition);
|
|
638
|
+
// res.debug = this.debug;
|
|
639
|
+
// res.init(context);
|
|
640
|
+
// pointer = new JsonPointer("/animations/", res, value);
|
|
641
|
+
// pointer.debug = this.debug;
|
|
642
|
+
// this.map.set(value, pointer);
|
|
643
|
+
prop.setReference(value, object);
|
|
644
|
+
}
|
|
645
|
+
pointer?.register(obj, key, fullPath);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
627
648
|
}
|
|
628
649
|
break;
|
|
629
650
|
case 'number':
|
|
@@ -682,7 +703,7 @@ class JsonPointerHandler {
|
|
|
682
703
|
pointer.debug = this.debug;
|
|
683
704
|
pointer.register(obj, key, fullPath);
|
|
684
705
|
this.map.set(dictKey, pointer);
|
|
685
|
-
prop.setReference(dictKey, object);
|
|
706
|
+
prop.setReference(dictKey, object, { channels: TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A });
|
|
686
707
|
}
|
|
687
708
|
}
|
|
688
709
|
break;
|
|
@@ -12,8 +12,12 @@ type LodInfo = {
|
|
|
12
12
|
/** Hash of the LOD file. It has been generated using the file content and settings */
|
|
13
13
|
hash: string;
|
|
14
14
|
};
|
|
15
|
+
export type ProgressiveTextureLodEntry = LodInfo & {
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
};
|
|
15
19
|
export type ProgressiveTextureModel = ProgressiveLoadingInfo & {
|
|
16
|
-
lods?: Array<
|
|
20
|
+
lods?: Array<ProgressiveTextureLodEntry>;
|
|
17
21
|
};
|
|
18
22
|
export type ProgressiveMeshModel = ProgressiveLoadingInfo & {
|
|
19
23
|
/** LOD0 vertex density. TODO: remove this since we now have a LODs array. This is just for supporting older engine versions */
|
|
@@ -9,7 +9,6 @@ import { ALL_EXTENSIONS as NEEDLE_EXTENSIONS, NEEDLE_compression_texture, NEEDLE
|
|
|
9
9
|
import { isLOD, isMeshLOD, needle_animation_transform, needle_asset, needle_mesh_transform, needle_texture_transform, } from '../transforms/index.js';
|
|
10
10
|
import { createNewFilestats, writeNodeIO } from "../utils/index.js";
|
|
11
11
|
import { addToCache, getHash, tryGetFromCache, } from "../cache/index.js";
|
|
12
|
-
import { getConfig } from '../config/index.js';
|
|
13
12
|
/** Compress a glTF file
|
|
14
13
|
*/
|
|
15
14
|
export async function packGLTF(inputFile, outputFile, options) {
|
|
@@ -42,7 +41,7 @@ export async function packGLTF(inputFile, outputFile, options) {
|
|
|
42
41
|
packedFiles.add(inputFile);
|
|
43
42
|
}
|
|
44
43
|
try {
|
|
45
|
-
const config = options.config
|
|
44
|
+
const config = options.config;
|
|
46
45
|
const prevSizeInMB = statSync(inputFile).size / 1024 / 1024;
|
|
47
46
|
logger.info(`→ Compressing ${prevSizeInMB.toFixed(1)} MB: ${inputFile}`);
|
|
48
47
|
const startTime = Date.now();
|
|
@@ -85,7 +84,10 @@ export async function packGLTF(inputFile, outputFile, options) {
|
|
|
85
84
|
const isProgressiveAsset = isLOD(inputFile);
|
|
86
85
|
// TODO: progressive mesh LODs should not be pruned right now because it will remove e.g. UVs from meshes (and probably blend shapes too) (so we just not do it for any progressive asset)
|
|
87
86
|
if (!isProgressiveAsset) {
|
|
88
|
-
transforms.push(prune(
|
|
87
|
+
transforms.push(prune({
|
|
88
|
+
// Pruning animations is not supported yet (e.g. if the animation is only referenced by a component)
|
|
89
|
+
propertyTypes: ['MESH', 'MATERIAL', 'TEXTURE']
|
|
90
|
+
}));
|
|
89
91
|
}
|
|
90
92
|
await document.transform(...transforms);
|
|
91
93
|
if (!outputFile)
|
|
@@ -16,10 +16,10 @@ async function applyMeshCompression(document, transform_context, context) {
|
|
|
16
16
|
const rootModel = rootExtension.model;
|
|
17
17
|
if (rootModel.compression) {
|
|
18
18
|
if (rootModel.compression === "none") {
|
|
19
|
-
|
|
19
|
+
document.getLogger().info(`[${NAME}]: NONE → will not apply mesh compression`);
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
document.getLogger().info(`[${NAME}]: ${rootModel.compression.toUpperCase()} → will apply ${rootModel.compression} mesh compression`);
|
|
23
23
|
if (rootModel.compression === "draco") {
|
|
24
24
|
const transform = draco();
|
|
25
25
|
return transform(document, transform_context);
|
|
@@ -31,13 +31,13 @@ async function applyMeshCompression(document, transform_context, context) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
// If nothing is configured apply meshopt compression as long as we're running gltf-transform 3.x
|
|
34
|
-
if (context.file.endsWith(".vrm")) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
34
|
+
// if (context.file.endsWith(".vrm")) {
|
|
35
|
+
// document.getLogger().info(`[${NAME}]: VRM → will apply meshopt mesh compression`);
|
|
36
|
+
// const transform = meshopt({ encoder: MeshoptEncoder });
|
|
37
|
+
// return transform(document, transform_context);
|
|
38
|
+
// }
|
|
39
39
|
// by default we still want to run draco if the root doesnt have a compression model configured
|
|
40
|
-
console.log(`[${NAME}]:
|
|
40
|
+
console.log(`[${NAME}]: → will apply draco mesh compression (default)`);
|
|
41
41
|
return draco()(document, transform_context);
|
|
42
42
|
}
|
|
43
43
|
async function simplifierTransform(document, _context, _options) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { BufferUtils, Document, NodeIO, PropertyType } from '@gltf-transform/core';
|
|
2
|
-
import { SIMPLIFY_DEFAULTS, WELD_DEFAULTS, listTextureSlots, sparse } from '@gltf-transform/functions';
|
|
1
|
+
import { BufferUtils, Document, NodeIO, PropertyType, TextureChannel, Verbosity } from '@gltf-transform/core';
|
|
2
|
+
import { SIMPLIFY_DEFAULTS, WELD_DEFAULTS, listTextureChannels, listTextureSlots, sparse } from '@gltf-transform/functions';
|
|
3
3
|
import draco3d from 'draco3dgltf';
|
|
4
4
|
import { NEEDLE_PROGRESSIVE_EXTENSION_NAME, NEEDLE_progressive } from "../extensions/NEEDLE_progressive/index.js";
|
|
5
5
|
import { createOpaqueExtension, currentGenerator, NEEDLE_compression_texture, NEEDLE_mesh_compression, NEEDLE_progressive_mesh_settings, NEEDLE_progressive_texture_settings, registerExtensions as registerDefaultExtensions } from '../extensions/index.js';
|
|
6
6
|
import path, { basename, resolve } from 'path';
|
|
7
7
|
import { existsSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
|
|
8
|
-
import { addMetaToCache, addToCache, getHash,
|
|
8
|
+
import { addMetaToCache, addToCache, getHash, getKey, tryGetFromCache, tryGetMetaFromCache } from '../cache/index.js';
|
|
9
9
|
import sharp from "sharp";
|
|
10
10
|
import { detectTextureCompressionMode, foreachGLTF, generateGuid, writeNodeIO } from '../utils/index.js';
|
|
11
11
|
import { needle_asset, writeNeedleAsset } from './needle_asset.js';
|
|
@@ -55,18 +55,13 @@ async function onProcess(file, opts) {
|
|
|
55
55
|
});
|
|
56
56
|
let document = await io.read(file);
|
|
57
57
|
if (document) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
58
|
+
const logger = document.getLogger();
|
|
59
|
+
//@ts-ignore
|
|
60
|
+
logger.verbosity = opts.debug ? Verbosity.DEBUG : Verbosity.WARN;
|
|
64
61
|
let smallesTextureSize = 128;
|
|
65
62
|
if (opts.config.usecase === "product") {
|
|
66
63
|
smallesTextureSize = 512;
|
|
67
64
|
}
|
|
68
|
-
//@ts-ignore
|
|
69
|
-
document.getLogger().verbosity = 0;
|
|
70
65
|
const transforms = [
|
|
71
66
|
needle_asset(),
|
|
72
67
|
make_progressive_textures(file, { size: [smallesTextureSize, smallesTextureSize] }, opts),
|
|
@@ -77,8 +72,8 @@ async function onProcess(file, opts) {
|
|
|
77
72
|
transforms.push(make_progressive_meshes(file, {}, opts));
|
|
78
73
|
}
|
|
79
74
|
else
|
|
80
|
-
|
|
81
|
-
|
|
75
|
+
logger.warn("WARN: Progressive meshes are not supported for VRM files yet");
|
|
76
|
+
logger.info("→ Transforming: " + file);
|
|
82
77
|
await document.transform(...transforms);
|
|
83
78
|
let filename = file;
|
|
84
79
|
if (opts.debug === true) {
|
|
@@ -91,7 +86,7 @@ async function onProcess(file, opts) {
|
|
|
91
86
|
}
|
|
92
87
|
await writeNodeIO(io, filename, document);
|
|
93
88
|
opts.results.push(filename);
|
|
94
|
-
|
|
89
|
+
logger.info("← Transforming done: " + filename);
|
|
95
90
|
}
|
|
96
91
|
}
|
|
97
92
|
catch (err) {
|
|
@@ -108,7 +103,6 @@ export function createTransform(name, fn) {
|
|
|
108
103
|
}
|
|
109
104
|
const NAME = 'NEEDLE_progressive';
|
|
110
105
|
export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DEFAULTS, opts) {
|
|
111
|
-
console.log("Make progressive: " + filePath);
|
|
112
106
|
const startTime = Date.now();
|
|
113
107
|
const filename = basename(filePath);
|
|
114
108
|
const options = { ...TEXTURE_RESIZE_DEFAULTS, ..._options };
|
|
@@ -117,6 +111,10 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
|
|
|
117
111
|
const textureLodBasePath = directory + "/";
|
|
118
112
|
return createTransform(NAME, async (doc) => {
|
|
119
113
|
const logger = doc.getLogger();
|
|
114
|
+
if (isLOD(filePath)) {
|
|
115
|
+
logger.info("Progressive Image Transform: skip lod at " + filePath);
|
|
116
|
+
return Promise.resolve();
|
|
117
|
+
}
|
|
120
118
|
let index = 0;
|
|
121
119
|
for (const texture of doc.getRoot().listTextures()) {
|
|
122
120
|
const textureIndex = index++;
|
|
@@ -159,8 +157,6 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
|
|
|
159
157
|
logger.warn(`[${NAME}] Skipping texture[${textureIndex}], settings are invalid (maxSize: ${settings.maxSize})`);
|
|
160
158
|
continue;
|
|
161
159
|
}
|
|
162
|
-
options.size[0] = settings.maxSize;
|
|
163
|
-
options.size[1] = settings.maxSize;
|
|
164
160
|
const existingExtension = texture.getExtension(NEEDLE_PROGRESSIVE_EXTENSION_NAME);
|
|
165
161
|
if (existingExtension !== undefined && existingExtension !== null) {
|
|
166
162
|
logger.debug(`[${NAME}] Skipping, texture is already progressive ${textureIndex}`);
|
|
@@ -172,25 +168,62 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
|
|
|
172
168
|
continue;
|
|
173
169
|
}
|
|
174
170
|
const textureGuid = settings.guid?.length ? settings.guid : generateGuid(filename + "_texture_" + index.toString());
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
171
|
+
/** This is the size of the texture that is embedded in the glTF. No LOD should be smaller than this */
|
|
172
|
+
const maxSize = settings.maxSize;
|
|
173
|
+
/** This is the current texture's size */
|
|
174
|
+
const currentSize = Math.min(...texture.getSize());
|
|
175
|
+
const LOD_LEVEL = new Array();
|
|
176
|
+
LOD_LEVEL.push({ size: currentSize });
|
|
177
|
+
let generateLods = opts.config?.usecase === "world";
|
|
178
|
+
// If lods are enabled in the config
|
|
179
|
+
generateLods ||= opts.config?.textures?.lods === true;
|
|
180
|
+
// Ensure that texture lods are not disabled by { textures: { lods: false } }
|
|
181
|
+
generateLods &&= opts.config?.textures?.lods !== false;
|
|
182
|
+
if (generateLods) {
|
|
183
|
+
const LEVEL = 6;
|
|
184
|
+
let lastSize = currentSize;
|
|
185
|
+
for (let i = 1; i < LEVEL; i++) {
|
|
186
|
+
const size = lastSize * .5;
|
|
187
|
+
if (size <= maxSize)
|
|
188
|
+
break;
|
|
189
|
+
lastSize = size;
|
|
190
|
+
LOD_LEVEL.push({ size: size });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const results = new Array();
|
|
194
|
+
for (let i = 0; i < LOD_LEVEL.length; i++) {
|
|
195
|
+
const level = LOD_LEVEL[i];
|
|
196
|
+
const lodResult = await createTextureLod({
|
|
197
|
+
doc,
|
|
198
|
+
file: filePath,
|
|
199
|
+
basePath: textureLodBasePath,
|
|
200
|
+
level: i,
|
|
201
|
+
texture,
|
|
202
|
+
slots,
|
|
203
|
+
guid: textureGuid,
|
|
204
|
+
textureIndex,
|
|
205
|
+
maxSize: level.size
|
|
206
|
+
}, opts);
|
|
207
|
+
if (!lodResult) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const { 0: width, 1: height } = lodResult.texture.getSize();
|
|
211
|
+
const outpath = lodResult?.outpath;
|
|
212
|
+
const relName = path.relative(directory, outpath);
|
|
213
|
+
results.push({
|
|
214
|
+
path: relName,
|
|
215
|
+
hash: lodResult.hash,
|
|
216
|
+
width,
|
|
217
|
+
height,
|
|
218
|
+
});
|
|
219
|
+
opts.results.push(outpath);
|
|
178
220
|
}
|
|
179
|
-
|
|
180
|
-
const { key, hash } = getKeyWithHash(texture);
|
|
181
|
-
const relName = path.relative(directory, outPath);
|
|
221
|
+
const key = getKey(texture);
|
|
182
222
|
// TODO: this path should be added to the NEEDLE dependencies extension
|
|
183
223
|
const model = {
|
|
184
224
|
guid: textureGuid,
|
|
185
|
-
lods:
|
|
186
|
-
{
|
|
187
|
-
hash: hash.toString(),
|
|
188
|
-
path: relName,
|
|
189
|
-
}
|
|
190
|
-
],
|
|
225
|
+
lods: results,
|
|
191
226
|
};
|
|
192
|
-
// for backwards compatibility - this should be removed in the future
|
|
193
|
-
model["uri"] = relName;
|
|
194
227
|
texture.setExtension(NEEDLE_PROGRESSIVE_EXTENSION_NAME, progressiveExt.createTexture(model));
|
|
195
228
|
if (opts.useCache) {
|
|
196
229
|
const cached = tryGetFromCache(key);
|
|
@@ -200,6 +233,9 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
|
|
|
200
233
|
continue;
|
|
201
234
|
}
|
|
202
235
|
}
|
|
236
|
+
options.size[0] = settings.maxSize;
|
|
237
|
+
options.size[1] = settings.maxSize;
|
|
238
|
+
logger.debug(`[${NAME}] Resizing embedded texture[${index}] to ${options.size[0]}x${options.size[1]}`);
|
|
203
239
|
await resizeTexture(options, texture, logger, uri || name, slots);
|
|
204
240
|
if (opts?.useCache === false) {
|
|
205
241
|
}
|
|
@@ -209,14 +245,10 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
|
|
|
209
245
|
logger.debug(`Progressive transform finished in ${((Date.now() - startTime) / 1000).toFixed(1)} sec.`);
|
|
210
246
|
});
|
|
211
247
|
}
|
|
212
|
-
function createTextureLod(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
// const outPath = basePath + guid + "_" + level + ".png";
|
|
217
|
-
// writeFileSync(outPath, imageBytes);
|
|
218
|
-
// return outPath;
|
|
219
|
-
// }
|
|
248
|
+
async function createTextureLod(args, opts) {
|
|
249
|
+
const { doc, file, basePath, level, texture, guid, textureIndex } = args;
|
|
250
|
+
let slots = args.slots;
|
|
251
|
+
const logger = doc.getLogger();
|
|
220
252
|
const newDoc = new Document();
|
|
221
253
|
writeNeedleAsset(newDoc);
|
|
222
254
|
const scene = newDoc.createScene();
|
|
@@ -227,14 +259,25 @@ function createTextureLod(doc, file, basePath, level, texture, slots, guid, text
|
|
|
227
259
|
newTexture.setImage(img);
|
|
228
260
|
newTexture.setURI(texture.getURI());
|
|
229
261
|
newTexture.setMimeType(texture.getMimeType());
|
|
262
|
+
if (args.maxSize && args.maxSize >= 16) {
|
|
263
|
+
const currentSize = newTexture.getSize();
|
|
264
|
+
if (currentSize[0] > args.maxSize && currentSize[1] > args.maxSize) {
|
|
265
|
+
await resizeTexture({ size: [args.maxSize, args.maxSize] }, newTexture, logger, texture.getURI(), slots);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
230
268
|
if (!slots?.length) {
|
|
231
|
-
|
|
269
|
+
logger.warn(`WARN: No material slots found for texture at index ${textureIndex} (name: \"${texture.getName()}\") → will assume slot \"baseColorTexture\"`);
|
|
232
270
|
slots = ["baseColorTexture"];
|
|
233
271
|
}
|
|
234
272
|
let slot_index = 0;
|
|
235
273
|
// create material adding the texture to the same slot
|
|
236
274
|
for (const slot of slots) {
|
|
237
|
-
const newMaterial = newDoc.createMaterial();
|
|
275
|
+
const newMaterial = newDoc.createMaterial(newTexture.getName());
|
|
276
|
+
const channels = listTextureChannels(texture);
|
|
277
|
+
// Ensure that we use the alpha channel if it is present
|
|
278
|
+
if (channels.includes(TextureChannel.A)) {
|
|
279
|
+
newMaterial.setAlphaMode("BLEND");
|
|
280
|
+
}
|
|
238
281
|
switch (slot) {
|
|
239
282
|
case "baseColorTexture":
|
|
240
283
|
newMaterial.setBaseColorTexture(newTexture);
|
|
@@ -252,7 +295,14 @@ function createTextureLod(doc, file, basePath, level, texture, slots, guid, text
|
|
|
252
295
|
newMaterial.setEmissiveTexture(newTexture);
|
|
253
296
|
break;
|
|
254
297
|
default:
|
|
255
|
-
|
|
298
|
+
if (slot.startsWith("/textures/")) {
|
|
299
|
+
// Ensure the texture is using the alpha channel since we don't know what slots the original texture was used for
|
|
300
|
+
// see https://linear.app/needle/issue/NE-4985
|
|
301
|
+
newMaterial.setAlphaMode("BLEND");
|
|
302
|
+
newMaterial.setBaseColorTexture(newTexture);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
logger.warn(`WARN: Unknown texture slot: ${slot} for texture ${textureIndex} → can not load progressively`);
|
|
256
306
|
return null;
|
|
257
307
|
}
|
|
258
308
|
// Make sure the material is used
|
|
@@ -260,16 +310,23 @@ function createTextureLod(doc, file, basePath, level, texture, slots, guid, text
|
|
|
260
310
|
}
|
|
261
311
|
const compressionSettings = texture.getExtension(NEEDLE_compression_texture.EXTENSION_NAME);
|
|
262
312
|
if (compressionSettings !== undefined && compressionSettings !== null && compressionSettings.getExtensionDefinition) {
|
|
263
|
-
|
|
313
|
+
logger.info("> Pass compression extension to new texture");
|
|
264
314
|
const newExt = newDoc.createExtension(NEEDLE_compression_texture);
|
|
265
315
|
const def = compressionSettings.getExtensionDefinition();
|
|
266
|
-
|
|
316
|
+
switch (opts.config.usecase) {
|
|
317
|
+
// For world scenarios we use ETC1S
|
|
318
|
+
case "world":
|
|
319
|
+
def.mode = "ETC1S";
|
|
320
|
+
break;
|
|
321
|
+
case "product":
|
|
322
|
+
def.mode = detectTextureCompressionMode(slots);
|
|
323
|
+
def.quality = 100;
|
|
324
|
+
break;
|
|
325
|
+
default:
|
|
326
|
+
def.mode = detectTextureCompressionMode(slots);
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
267
329
|
def.slot = slots[0];
|
|
268
|
-
// for VRMS we just use webp
|
|
269
|
-
// if (file?.toLocaleLowerCase().endsWith(".vrm")) {
|
|
270
|
-
// def.mode = "webp";
|
|
271
|
-
// def.quality = 70;
|
|
272
|
-
// }
|
|
273
330
|
const newProp = newExt.create(def);
|
|
274
331
|
newTexture.setExtension(NEEDLE_compression_texture.EXTENSION_NAME, newProp);
|
|
275
332
|
}
|
|
@@ -284,9 +341,13 @@ function createTextureLod(doc, file, basePath, level, texture, slots, guid, text
|
|
|
284
341
|
const imageName = "image_" + level + "_" + guid;
|
|
285
342
|
const filename = imageName + ext;
|
|
286
343
|
const outName = basePath + filename;
|
|
287
|
-
|
|
344
|
+
logger.info(`Write texture LOD ${level} to \"${filename}\"`);
|
|
288
345
|
newIo.write(outName, newDoc);
|
|
289
|
-
return
|
|
346
|
+
return {
|
|
347
|
+
outpath: outName,
|
|
348
|
+
texture: newTexture,
|
|
349
|
+
hash: getHash(newTexture).toString(),
|
|
350
|
+
};
|
|
290
351
|
}
|
|
291
352
|
function createMeshForMaterial(doc, mat, i) {
|
|
292
353
|
const buffer = doc.getRoot().listBuffers()[0];
|
|
@@ -369,6 +430,7 @@ async function resizeTexture(options, texture, logger, uri, slots) {
|
|
|
369
430
|
dstWidth = Math.floor(dstWidth * (maxHeight / dstHeight));
|
|
370
431
|
dstHeight = maxHeight;
|
|
371
432
|
}
|
|
433
|
+
logger.info("> Resize texture to " + dstWidth + "x" + dstHeight + " (from " + srcWidth + "x" + srcHeight + ")");
|
|
372
434
|
// https://sharp.pixelplumbing.com/api-resize
|
|
373
435
|
const buffer = await sharp(texture.getImage())
|
|
374
436
|
.resize(dstWidth, dstHeight, {
|
|
@@ -378,26 +440,30 @@ async function resizeTexture(options, texture, logger, uri, slots) {
|
|
|
378
440
|
})
|
|
379
441
|
.toBuffer();
|
|
380
442
|
texture.setImage(BufferUtils.toView(buffer));
|
|
443
|
+
return {
|
|
444
|
+
width: dstWidth,
|
|
445
|
+
height: dstHeight,
|
|
446
|
+
};
|
|
381
447
|
}
|
|
382
448
|
export function make_progressive_meshes(filePath, _options, opts) {
|
|
383
449
|
const directory = path.dirname(filePath);
|
|
384
450
|
// const fileNameWithoutExtension = path.basename(filePath, path.extname(filePath));
|
|
385
451
|
const outputDirectory = directory + "/";
|
|
386
452
|
return createTransform("PROGRESSIVE_MESHES", async (doc, _context) => {
|
|
453
|
+
const logger = doc.getLogger();
|
|
387
454
|
// this is to ensure we're not creating progressive meshes for engine versions that don't support loading them yet
|
|
388
455
|
// the generator should include the version or the app that produced the GLB too (needle engine repo: 2c00d0e304e205cf9d4ac0499faabb92a706a5c4)
|
|
389
456
|
if (currentGenerator == "Needle Engine") {
|
|
390
|
-
|
|
457
|
+
logger.warn("WARN: Skip creating mesh LODs - Unsupported generator found... Please update your editor integration");
|
|
391
458
|
return;
|
|
392
459
|
}
|
|
393
|
-
if (
|
|
394
|
-
|
|
460
|
+
if (isLOD(filePath)) {
|
|
461
|
+
logger.info("Progressive Mesh Transform: skip lod at " + filePath);
|
|
395
462
|
return Promise.resolve();
|
|
396
463
|
}
|
|
397
464
|
await MeshoptSimplifier.ready;
|
|
398
|
-
const logger = doc.getLogger();
|
|
399
465
|
const filename = path.basename(filePath);
|
|
400
|
-
|
|
466
|
+
logger.info(`[${NAME}]: Begin generating progressive meshes for \"${filename}\"`);
|
|
401
467
|
let index = 0;
|
|
402
468
|
// the lowest LOD (last entry) will be saved in the original file
|
|
403
469
|
let MAX_LODS = 6;
|
|
@@ -433,7 +499,7 @@ export function make_progressive_meshes(filePath, _options, opts) {
|
|
|
433
499
|
for (const mesh of meshes) {
|
|
434
500
|
await processMesh(mesh, { filename });
|
|
435
501
|
}
|
|
436
|
-
|
|
502
|
+
logger.info(`[${NAME}]: Finished generating progressive meshes for \"${filename}\"`);
|
|
437
503
|
async function processMesh(mesh, args) {
|
|
438
504
|
/** The size and vertex count of the original mesh */
|
|
439
505
|
const originalMeshInformation = getMeshInformation(mesh);
|
|
@@ -474,7 +540,7 @@ export function make_progressive_meshes(filePath, _options, opts) {
|
|
|
474
540
|
/** async simplification tasks... */
|
|
475
541
|
const tasks = new Array();
|
|
476
542
|
if (needsToGenerateLODs) {
|
|
477
|
-
|
|
543
|
+
logger.info(`[${NAME}] Begin generating mesh LODs for \"${mesh.getName()}\"`);
|
|
478
544
|
// produce LODS, skip the last level because it will be saved in the original file
|
|
479
545
|
for (let level = 0; level < LODS.length - 1; level++) {
|
|
480
546
|
const lod = LODS[level];
|
|
@@ -510,11 +576,11 @@ export function make_progressive_meshes(filePath, _options, opts) {
|
|
|
510
576
|
let shouldOmitLevel = false;
|
|
511
577
|
// if the mesh size on disc is identical to the last level, we can omit this level
|
|
512
578
|
if (sizeOnDisc <= 0) {
|
|
513
|
-
|
|
579
|
+
logger.info(`[${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}\"`);
|
|
514
580
|
shouldOmitLevel = true;
|
|
515
581
|
}
|
|
516
582
|
else if (diff == 0) {
|
|
517
|
-
|
|
583
|
+
logger.info(`[${NAME}] Flag mesh LOD ${level}: size on disc did not change compared to previous level → level will be omitted. \"${mesh.getName()}\" at \"${lodInfo.path}\"`);
|
|
518
584
|
shouldOmitLevel = true;
|
|
519
585
|
}
|
|
520
586
|
else if (!shouldOmitLevel) {
|
|
@@ -557,7 +623,7 @@ export function make_progressive_meshes(filePath, _options, opts) {
|
|
|
557
623
|
if (data != null) {
|
|
558
624
|
clonedData.primitives.push(data);
|
|
559
625
|
}
|
|
560
|
-
|
|
626
|
+
logger.info(`[${NAME}] Simplify mesh[${index}] primitive[${primitiveIndex}] (embedded)`);
|
|
561
627
|
const lastLod = LODS[lodLevelInFile];
|
|
562
628
|
// TODO: this should be run after simplification but then we need to change the runtime to use the LOD 0 density
|
|
563
629
|
const density = calculateMeshDensity(prim);
|
|
@@ -575,7 +641,7 @@ export function make_progressive_meshes(filePath, _options, opts) {
|
|
|
575
641
|
}
|
|
576
642
|
else if (difference < 32 || diff01 <= 0.05) {
|
|
577
643
|
// 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
|
|
578
|
-
|
|
644
|
+
logger.info(`[${NAME}]: Flag mesh LOD ${generatedLODInfos.length} (embedded): vertex count difference below threshold (removed ${difference} vertices, ${(-diff01 * 100).toFixed(2)} %) → will remove LOD. \"${mesh.getName()}\"`);
|
|
579
645
|
try {
|
|
580
646
|
for (const lodInfo of generatedLODInfos) {
|
|
581
647
|
rmSync(outputDirectory + "/" + lodInfo.path);
|
|
@@ -96,7 +96,7 @@ export class NeedleToktxContext {
|
|
|
96
96
|
const doc = this.document;
|
|
97
97
|
const logger = doc.getLogger();
|
|
98
98
|
const slots = listTextureSlots(texture);
|
|
99
|
-
|
|
99
|
+
let channels = getTextureChannelMask(texture);
|
|
100
100
|
// const textureLabel =
|
|
101
101
|
// texture.getURI() ||
|
|
102
102
|
// texture.getName() ||
|
|
@@ -175,6 +175,10 @@ export class NeedleToktxContext {
|
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
177
|
const defaults = mode === Mode.ETC1S ? ETC1S_DEFAULTS : UASTC_DEFAULTS;
|
|
178
|
+
// TODO: should we ensure that the texture is using all channels? Unused channels are discarded anyways. The issue we have here is that textures that are solely used from pointers have unknown channels. We register them with RGBA when generating progressive assets as well as assign texture json pointers /textures/ to
|
|
179
|
+
// Alpha blended materials to make sure RGBA channels are used when progressive GLBs are compressed (in needle_progressive 377)
|
|
180
|
+
// https://linear.app/needle/issue/NE-4985
|
|
181
|
+
// channels = TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A;
|
|
178
182
|
//@ts-ignore
|
|
179
183
|
const toktxParams = createParams(texture, slots, channels, srcSize, logger, this.numTextures, { ...defaults, mode, quality, compression });
|
|
180
184
|
await fs.writeFile(inPath, Buffer.from(srcImage));
|
|
@@ -57,7 +57,7 @@ export async function foreachGLTF(args, callback) {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
if (context.files.length <= 0) {
|
|
60
|
-
console.log("WARN: No glTF files found. Make sure to invoke with a valid path to a glTF file or directory. This path may be relative to " + initDir);
|
|
60
|
+
console.log("WARN: No glTF files found. Make sure to invoke with a valid path to a glTF file or directory. This path may be relative to \"" + initDir + "\"");
|
|
61
61
|
}
|
|
62
62
|
else {
|
|
63
63
|
for (const file of context.files) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Logger } from "@donmccurdy/caporal";
|
|
2
2
|
import { Document } from "@gltf-transform/core";
|
|
3
|
+
import { Config } from "../config/index.js";
|
|
3
4
|
export type Stats = {
|
|
4
5
|
startTime: number;
|
|
5
6
|
totalGPUMemoryInMB: number;
|
|
@@ -7,7 +8,7 @@ export type Stats = {
|
|
|
7
8
|
totalFileSizeInMB: number;
|
|
8
9
|
totalFileSizeInMBBefore: number;
|
|
9
10
|
};
|
|
10
|
-
export declare function printStats(stats: Stats, logger
|
|
11
|
+
export declare function printStats(stats: Stats, logger: Logger, config: Config): void;
|
|
11
12
|
export type FileStats = {
|
|
12
13
|
filename: string;
|
|
13
14
|
/** Size in bytes */
|
|
@@ -25,4 +26,4 @@ export type FileStats = {
|
|
|
25
26
|
* Create a new filestats object from a file and a document
|
|
26
27
|
*/
|
|
27
28
|
export declare function createNewFilestats(file: string, document: Document, stats: Pick<FileStats, "gpu_memory">): FileStats;
|
|
28
|
-
export declare function writeFileStatsToFile(filestats: FileStats[], output_path: string): void;
|
|
29
|
+
export declare function writeFileStatsToFile(config: Config, filestats: FileStats[], output_path: string): void;
|
package/dist/src/utils/stats.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { getVersion } from "./version.js";
|
|
2
2
|
import { getFileName } from "./fileutils.js";
|
|
3
3
|
import { statSync, writeFileSync } from "fs";
|
|
4
|
-
export function printStats(stats, logger) {
|
|
4
|
+
export function printStats(stats, logger, config) {
|
|
5
5
|
const duration = Date.now() - stats.startTime;
|
|
6
6
|
const durationInMinutes = duration / 1000 / 60;
|
|
7
|
-
const msg = `[Needle Build Pipeline] v${getVersion()} — Finished
|
|
7
|
+
const msg = `[Needle Build Pipeline] v${getVersion()} — Finished ${config?.usecase} compression ${stats.totalFilesProcessed} files in ${durationInMinutes.toFixed(1)} minutes; total size ${stats.totalFileSizeInMBBefore.toFixed(1)} MB → ${stats.totalFileSizeInMB.toFixed(1)} MB, total GPU memory: ${stats.totalGPUMemoryInMB.toFixed(1)} MB`;
|
|
8
8
|
if (logger)
|
|
9
9
|
logger.info(msg);
|
|
10
10
|
else
|
|
@@ -34,7 +34,7 @@ export function createNewFilestats(file, document, stats) {
|
|
|
34
34
|
filestats.triangle_count = filestats.vertex_count / 3;
|
|
35
35
|
return filestats;
|
|
36
36
|
}
|
|
37
|
-
export function writeFileStatsToFile(filestats, output_path) {
|
|
37
|
+
export function writeFileStatsToFile(config, filestats, output_path) {
|
|
38
38
|
if (output_path == undefined) {
|
|
39
39
|
console.error("No stats output path provided");
|
|
40
40
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@needle-tools/gltf-build-pipeline",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0-alpha.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",
|