@needle-tools/gltf-build-pipeline 1.0.0 → 1.0.2
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 +9 -0
- package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.js +2 -1
- package/dist/transforms/needle_common.d.ts +1 -0
- package/dist/transforms/needle_progressive.js +38 -24
- package/dist/transforms/needle_texture_transform.js +25 -2
- package/dist/transforms/needle_toktx.d.ts +1 -0
- package/dist/transforms/needle_toktx.js +9 -4
- package/dist/transforms/toktx.js +4 -1
- package/dist/utils/fileutils.js +9 -1
- package/pack-gltf.mjs +46 -29
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ 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.0.2] - 2023-04-19
|
|
8
|
+
- change: progressive should not log error out when running for already compressed gltf
|
|
9
|
+
- change: ignore EXT_texture_exr extension re-assignment
|
|
10
|
+
|
|
11
|
+
## [1.0.1] - 2023-04-17
|
|
12
|
+
- fix: register meshopt decoder
|
|
13
|
+
- fix: catch exception in document read
|
|
14
|
+
- change: log gpu memory estimation for textures
|
|
15
|
+
|
|
7
16
|
## [1.0.0] - 2023-04-04
|
|
8
17
|
- change: file utils use needle.config.json to find files if no directory was specified
|
|
9
18
|
- change: add generator info
|
|
@@ -400,7 +400,8 @@ class TextureHandler {
|
|
|
400
400
|
}
|
|
401
401
|
});
|
|
402
402
|
if (foundTexture === false) {
|
|
403
|
-
|
|
403
|
+
if (this.key !== "EXT_texture_exr" || this.debug)
|
|
404
|
+
console.warn("WARN: failed to re-assign extension:", this.key, this.index, this.tryGetSourceIndex(this.textureDefinition), textureDefs.length);
|
|
404
405
|
}
|
|
405
406
|
// this.document
|
|
406
407
|
// .getRoot()
|
|
@@ -8,6 +8,7 @@ import { addToCache, getKey, tryGetFromCache } from '../cache/index.js';
|
|
|
8
8
|
import sharp from "sharp";
|
|
9
9
|
import { foreachGLTF } from '../utils/index.js';
|
|
10
10
|
import { needle_asset } from './needle_asset.js';
|
|
11
|
+
import { MeshoptDecoder } from 'meshoptimizer';
|
|
11
12
|
function registerExtensions(io) {
|
|
12
13
|
registerDefaultExtensions(io);
|
|
13
14
|
io.registerExtensions([
|
|
@@ -37,32 +38,45 @@ async function process(file, opts) {
|
|
|
37
38
|
if (!existsSync(file)) {
|
|
38
39
|
console.log("ERR: file does not exist: " + file);
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
console.log("
|
|
50
|
-
return;
|
|
41
|
+
try {
|
|
42
|
+
const io = new NodeIO();
|
|
43
|
+
registerExtensions(io);
|
|
44
|
+
io.registerDependencies({
|
|
45
|
+
'meshopt.decoder': MeshoptDecoder,
|
|
46
|
+
});
|
|
47
|
+
const document = await io.read(file).catch((err) => {
|
|
48
|
+
// Log this message as a warning, it happens when trying to run on a already compressed gltf file (e.g. pre-processed. When logging an error we fail the build which is not desired)
|
|
49
|
+
if (err.message === "Cannot read properties of undefined (reading 'DT_FLOAT32')") {
|
|
50
|
+
console.log("WARN: \"" + err.message + "\" at \"" + file + "\"");
|
|
51
51
|
}
|
|
52
|
+
else
|
|
53
|
+
console.log("ERR: \"" + err.message + "\" at \"" + file + "\"");
|
|
54
|
+
return;
|
|
55
|
+
});
|
|
56
|
+
if (document) {
|
|
57
|
+
if (opts.debug === true) {
|
|
58
|
+
if (file.includes(".debug.")) {
|
|
59
|
+
console.log("Skip debug file: " + file);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
//@ts-ignore
|
|
64
|
+
document.getLogger().verbosity = 0;
|
|
65
|
+
await document.transform(needle_asset(), make_progressive_textures(file, { size: [256, 256] }, opts));
|
|
66
|
+
let filename = file;
|
|
67
|
+
if (opts.debug === true) {
|
|
68
|
+
// get extension
|
|
69
|
+
const extension = filename.split('.').pop();
|
|
70
|
+
// remove extension
|
|
71
|
+
filename = filename.split('.').slice(0, -1).join('.');
|
|
72
|
+
// add new extension
|
|
73
|
+
filename = filename + ".debug." + extension;
|
|
74
|
+
}
|
|
75
|
+
io.write(filename, document);
|
|
52
76
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
let filename = file;
|
|
57
|
-
if (opts.debug === true) {
|
|
58
|
-
// get extension
|
|
59
|
-
const extension = filename.split('.').pop();
|
|
60
|
-
// remove extension
|
|
61
|
-
filename = filename.split('.').slice(0, -1).join('.');
|
|
62
|
-
// add new extension
|
|
63
|
-
filename = filename + ".debug." + extension;
|
|
64
|
-
}
|
|
65
|
-
io.write(filename, document);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
console.log("ERR: failed to process file \"" + file + "\", Reason: " + err.message);
|
|
66
80
|
}
|
|
67
81
|
}
|
|
68
82
|
// basic idea:
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ImageUtils } from "@gltf-transform/core";
|
|
1
2
|
import { resizeTextureToMax } from "./needle_progressive.js";
|
|
2
3
|
import { NEEDLE_compression_texture } from '../extensions/index.js';
|
|
3
4
|
import { addToCache, getKey, tryGetFromCache } from "../cache/index.js";
|
|
@@ -25,7 +26,8 @@ export async function resizeTextureIfNecessary(texture) {
|
|
|
25
26
|
export const needle_texture_transform = function (options) {
|
|
26
27
|
const webPContext = new NeedleWebPContext({});
|
|
27
28
|
const toktxContext = new NeedleToktxContext({
|
|
28
|
-
jobs: undefined,
|
|
29
|
+
jobs: undefined,
|
|
30
|
+
debug: options.debug,
|
|
29
31
|
});
|
|
30
32
|
const context = {
|
|
31
33
|
...options,
|
|
@@ -39,9 +41,30 @@ export const needle_texture_transform = function (options) {
|
|
|
39
41
|
try {
|
|
40
42
|
const logger = document.getLogger();
|
|
41
43
|
const textures = document.getRoot().listTextures();
|
|
44
|
+
let totalGpuSize = 0;
|
|
42
45
|
await Promise.all(textures.map((texture, textureIndex) => {
|
|
43
|
-
return processTexture(texture, textureIndex, document, logger, context)
|
|
46
|
+
return processTexture(texture, textureIndex, document, logger, context).then(() => {
|
|
47
|
+
try {
|
|
48
|
+
const size = ImageUtils.getVRAMByteLength(texture.getImage(), texture.getMimeType());
|
|
49
|
+
totalGpuSize += size;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Ignore
|
|
53
|
+
}
|
|
54
|
+
});
|
|
44
55
|
}));
|
|
56
|
+
if (totalGpuSize > 0) {
|
|
57
|
+
// TODO: report this to the parent
|
|
58
|
+
const gpuMemoryEstimate = totalGpuSize / 1024 / 1024;
|
|
59
|
+
if (context.reportGPUMemory)
|
|
60
|
+
context.reportGPUMemory(totalGpuSize);
|
|
61
|
+
else {
|
|
62
|
+
console.log(`> Total GPU size: ${(gpuMemoryEstimate).toFixed(2)} MB of ${textures.length} textures`);
|
|
63
|
+
}
|
|
64
|
+
if (gpuMemoryEstimate > 1000) {
|
|
65
|
+
console.log(`WARN: Total estimated GPU size is very large - this may cause issues on some devices. Consider changing the compression settings or reducing texture size.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
45
68
|
}
|
|
46
69
|
finally {
|
|
47
70
|
await webPContext.finalize(document);
|
|
@@ -3,6 +3,7 @@ import { KHRTextureBasisu } from "@gltf-transform/extensions";
|
|
|
3
3
|
import { NEEDLE_compression_texture_schema } from "../extensions/index.js";
|
|
4
4
|
import { INeedleTextureTransformContext } from "./needle_common.js";
|
|
5
5
|
export interface NeedleToktxOptions {
|
|
6
|
+
debug?: boolean;
|
|
6
7
|
jobs?: number;
|
|
7
8
|
toktxPath?: string;
|
|
8
9
|
}
|
|
@@ -9,6 +9,7 @@ import os from 'os';
|
|
|
9
9
|
import tmp from 'tmp';
|
|
10
10
|
import pLimit from 'p-limit';
|
|
11
11
|
const NUM_CPUS = os.cpus().length || 1; // microsoft/vscode#112122
|
|
12
|
+
let didLogEnvironmentInfo = false;
|
|
12
13
|
export class NeedleToktxContext {
|
|
13
14
|
limit;
|
|
14
15
|
basisuExtension;
|
|
@@ -31,15 +32,19 @@ export class NeedleToktxContext {
|
|
|
31
32
|
// Use global env variable
|
|
32
33
|
if (process.env.NEEDLE_TOKTX?.length) {
|
|
33
34
|
if (!existsSync(process.env.NEEDLE_TOKTX)) {
|
|
34
|
-
|
|
35
|
+
if (options.debug)
|
|
36
|
+
console.log("WARN: NEEDLE_TOKTX is set to a non-existing path: \"" + process.env.NEEDLE_TOKTX + "\"");
|
|
35
37
|
}
|
|
36
38
|
else {
|
|
37
|
-
|
|
39
|
+
if (options.debug)
|
|
40
|
+
console.log("INFO: Adding NEEDLE_TOKTX to PATH: \"" + process.env.NEEDLE_TOKTX + "\"");
|
|
38
41
|
this.env.PATH = `${process.env.NEEDLE_TOKTX};${this.env.PATH}`;
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
44
|
else {
|
|
42
|
-
|
|
45
|
+
if (didLogEnvironmentInfo === false)
|
|
46
|
+
console.log("INFO: Environment variable 'NEEDLE_TOKTX' not set. Use 'NEEDLE_TOKTX' to specify the directory of the toktx installation (path/to/bin)");
|
|
47
|
+
didLogEnvironmentInfo = true;
|
|
43
48
|
}
|
|
44
49
|
// fallback default installation directory:
|
|
45
50
|
if (isUsingWindows) {
|
|
@@ -50,7 +55,7 @@ export class NeedleToktxContext {
|
|
|
50
55
|
// append default system paths
|
|
51
56
|
if (isUsingWindows && process.env.Path?.length)
|
|
52
57
|
this.env.PATH = `${this.env.PATH};${process.env.Path}`;
|
|
53
|
-
if (
|
|
58
|
+
if (options.debug) {
|
|
54
59
|
const ktxPaths = this.env.PATH.split(";").filter(p => p.toLocaleLowerCase().includes("ktx"));
|
|
55
60
|
console.log("INFO: Found these paths including ktx:\n\t" + ktxPaths.join("\n\t") + "");
|
|
56
61
|
}
|
package/dist/transforms/toktx.js
CHANGED
|
@@ -167,6 +167,7 @@ export function createParams(slots, channels, size, logger, numTextures, options
|
|
|
167
167
|
}
|
|
168
168
|
return params;
|
|
169
169
|
}
|
|
170
|
+
let didLogToktxVersion = false;
|
|
170
171
|
export async function checkKTXSoftware(logger, env) {
|
|
171
172
|
const [status, stdout, stderr] = await waitExit(spawn('toktx', ['--version'], { env: env }));
|
|
172
173
|
const version = (stdout || stderr)
|
|
@@ -180,7 +181,9 @@ export async function checkKTXSoftware(logger, env) {
|
|
|
180
181
|
logger.warn(`toktx: Expected KTX-Software >= v${KTX_SOFTWARE_VERSION_MIN}, found ${version}.`);
|
|
181
182
|
}
|
|
182
183
|
else {
|
|
183
|
-
|
|
184
|
+
if (!didLogToktxVersion)
|
|
185
|
+
logger.debug(`toktx: Found KTX-Software ${version}.`);
|
|
186
|
+
didLogToktxVersion = true;
|
|
184
187
|
}
|
|
185
188
|
return semver.clean(version);
|
|
186
189
|
}
|
package/dist/utils/fileutils.js
CHANGED
|
@@ -51,12 +51,13 @@ function testEntry(entry, callback, context) {
|
|
|
51
51
|
if (existsSync(entry) === false) {
|
|
52
52
|
// If no base directory is defined we cannot try to resolve the path
|
|
53
53
|
if (context.baseDirectory === undefined) {
|
|
54
|
+
console.log("WARN: Path does not exist: " + entry);
|
|
54
55
|
return;
|
|
55
56
|
}
|
|
56
57
|
// Try resolve potential relative path
|
|
57
58
|
const newPath = context.baseDirectory + "/" + entry;
|
|
58
|
-
console.log(newPath);
|
|
59
59
|
if (existsSync(newPath) === false) {
|
|
60
|
+
console.log(`INFO: Resolved path failed: ${entry} -> ${newPath}`);
|
|
60
61
|
return;
|
|
61
62
|
}
|
|
62
63
|
else {
|
|
@@ -86,10 +87,17 @@ function testEntry(entry, callback, context) {
|
|
|
86
87
|
else {
|
|
87
88
|
const stat = statSync(entry);
|
|
88
89
|
if (stat.isDirectory()) {
|
|
90
|
+
// Don't traverse node_modules
|
|
91
|
+
if (entry.endsWith("node_modules")) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
89
94
|
const files = readdirSync(entry);
|
|
90
95
|
for (const file of files) {
|
|
91
96
|
testEntry(entry + "/" + file, callback, context);
|
|
92
97
|
}
|
|
93
98
|
}
|
|
99
|
+
else {
|
|
100
|
+
// console.log("WARN: Ignoring file: " + entry);
|
|
101
|
+
}
|
|
94
102
|
}
|
|
95
103
|
}
|
package/pack-gltf.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { needle_texture_transform } from "./dist/transforms/needle_texture_trans
|
|
|
13
13
|
import { needle_animation_transform } from "./dist/transforms/needle_animation_transform.js";
|
|
14
14
|
import { foreachGLTF } from './fileutils.mjs';
|
|
15
15
|
import { needle_asset } from './dist/transforms/needle_asset.js';
|
|
16
|
+
import { MeshoptDecoder, MeshoptEncoder } from 'meshoptimizer';
|
|
16
17
|
|
|
17
18
|
const args = process.argv;
|
|
18
19
|
const showHelp = args.includes("help");
|
|
@@ -82,35 +83,51 @@ async function packFile(file) {
|
|
|
82
83
|
packedFiles.add(file);
|
|
83
84
|
console.log("→ Begin packing: " + file);
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
86
|
+
try {
|
|
87
|
+
const prevSize = statSync(file).size;
|
|
88
|
+
const startTime = Date.now();
|
|
89
|
+
const io = new NodeIO()
|
|
90
|
+
.registerExtensions(ALL_EXTENSIONS)
|
|
91
|
+
.registerExtensions(NEEDLE_EXTENSIONS)
|
|
92
|
+
.registerExtensions([
|
|
93
|
+
NEEDLE_compression_texture,
|
|
94
|
+
NEEDLE_mesh_compression,
|
|
95
|
+
])
|
|
96
|
+
.registerDependencies({
|
|
97
|
+
'draco3d.decoder': await draco3d.createDecoderModule(),
|
|
98
|
+
'draco3d.encoder': await draco3d.createEncoderModule(),
|
|
99
|
+
'meshopt.decoder': MeshoptDecoder,
|
|
100
|
+
'meshopt.encoder': MeshoptEncoder,
|
|
101
|
+
});
|
|
102
|
+
const document = await io.read(file);
|
|
103
|
+
document.getLogger().verbosity = 0;
|
|
104
|
+
let totalGPUTextureMemmory = 0;
|
|
105
|
+
await document.transform(
|
|
106
|
+
needle_asset(),
|
|
107
|
+
needle_texture_transform({
|
|
108
|
+
useCache: allowCaching,
|
|
109
|
+
noToktx: noToktx,
|
|
110
|
+
debug: debug,
|
|
111
|
+
reportGPUMemory: val => totalGPUTextureMemmory += val
|
|
112
|
+
}),
|
|
113
|
+
needle_mesh_transform({ ratio: 0.75, error: 0.0005 }),
|
|
114
|
+
needle_animation_transform(),
|
|
115
|
+
draco(),
|
|
116
|
+
);
|
|
117
|
+
if (debug) file += ".debug.glb";
|
|
118
|
+
await io.write(file, document);
|
|
119
|
+
const duration = Date.now() - startTime;
|
|
120
|
+
const sizeInBytes = statSync(file).size;
|
|
121
|
+
const filename = path.basename(file);
|
|
122
|
+
const gpuMemoryInMB = totalGPUTextureMemmory / 1024 / 1024;
|
|
123
|
+
console.log(`← Compressing done in ${(duration / 1000).toFixed(1)} sec, ${(prevSize / 1024 / 1024).toFixed(1)} MB → ${(sizeInBytes / 1024 / 1024).toFixed(1)} MB (GPU: ${gpuMemoryInMB.toFixed(1)} MB textures): ${filename} at ${file}`);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
console.error("ERR: packing " + file + " failed: " + err.message);
|
|
128
|
+
if (debug) throw err;
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
114
131
|
}
|
|
115
132
|
|
|
116
133
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@needle-tools/gltf-build-pipeline",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
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",
|