@needle-tools/gltf-build-pipeline 1.6.0-alpha.1 → 1.6.0-alpha.3
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 +4 -1
- package/README.md +17 -0
- package/dist/cache/cache.d.ts +1 -0
- package/dist/cache/cache.js +3 -2
- package/dist/cli/index.js +96 -37
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +26 -13
- package/dist/scripts/pack-gltf.d.ts +4 -1
- package/dist/scripts/pack-gltf.js +19 -13
- package/dist/transforms/needle_progressive.d.ts +1 -1
- package/dist/transforms/needle_progressive.js +18 -7
- package/dist/utils/fileutils.d.ts +1 -0
- package/dist/utils/fileutils.js +19 -14
- package/dist/utils/stats.d.ts +16 -1
- package/dist/utils/stats.js +35 -2
- package/dist/utils/version.js +6 -1
- package/package.json +2 -1
- package/scripts/pack-gltf.mjs +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,10 @@ 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.6.0-alpha] - 2023-05-
|
|
7
|
+
## [1.6.0-alpha.3] - 2023-05-28
|
|
8
|
+
- fix: determine build pipeline version
|
|
9
|
+
|
|
10
|
+
## [1.6.0-alpha.2] - 2023-05-27
|
|
8
11
|
- add: `gltf-build-pipeline` commandline support with `transform` and `compress` (pack-gltf) options.
|
|
9
12
|
|
|
10
13
|
## [1.5.0-alpha.12] - 2023-05-20
|
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# glTF Build Pipeline
|
|
2
|
+
|
|
3
|
+
Creating optimized glTF, GLB and VRM files for the web.
|
|
4
|
+
Includes support for generating LOD quality level for textures and meshes that can be used with [@needle-tools/gltf-progressive](https://www.npmjs.com/package/@needle-tools/gltf-progressive)
|
|
5
|
+
|
|
6
|
+
## Examples
|
|
7
|
+
|
|
8
|
+
- [Example on Github](https://github.com/needle-engine/progressive-meshes-sample)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Contact ✒️
|
|
13
|
+
<b>[🌵 needle — tools for creators](https://needle.tools)</b> •
|
|
14
|
+
[Twitter](https://twitter.com/NeedleTools) •
|
|
15
|
+
[Discord](https://discord.needle.tools) •
|
|
16
|
+
[Forum](https://forum.needle.tools)
|
|
17
|
+
|
package/dist/cache/cache.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Logger } from "@donmccurdy/caporal";
|
|
|
5
5
|
* This will remove all files in the cache directory.
|
|
6
6
|
*/
|
|
7
7
|
export declare function clearCache(logger?: Logger): void;
|
|
8
|
+
export declare const cacheSizeLimit = 1024;
|
|
8
9
|
/**
|
|
9
10
|
* Ensure that the cache size does not exceed the given maximum size in MB.
|
|
10
11
|
* @param maxSizeMB The maximum size in MB.
|
package/dist/cache/cache.js
CHANGED
|
@@ -31,11 +31,12 @@ export function clearCache(logger) {
|
|
|
31
31
|
else
|
|
32
32
|
logger.info(`Cleared cache at ${dir}`);
|
|
33
33
|
}
|
|
34
|
+
export const cacheSizeLimit = 1024;
|
|
34
35
|
/**
|
|
35
36
|
* Ensure that the cache size does not exceed the given maximum size in MB.
|
|
36
37
|
* @param maxSizeMB The maximum size in MB.
|
|
37
38
|
*/
|
|
38
|
-
export function limitCacheSize(maxSizeMB =
|
|
39
|
+
export function limitCacheSize(maxSizeMB = cacheSizeLimit) {
|
|
39
40
|
const dir = getCacheDirectory(true);
|
|
40
41
|
console.log(`Limit cache size to ${maxSizeMB.toFixed(2)} MB at ${dir}...`);
|
|
41
42
|
let totalSize = 0;
|
|
@@ -213,7 +214,7 @@ export function getHash(property, level = 0) {
|
|
|
213
214
|
}
|
|
214
215
|
return hash;
|
|
215
216
|
}
|
|
216
|
-
const cleanBuildPipelineVersion =
|
|
217
|
+
const cleanBuildPipelineVersion = getVersion(false).replace(/[^a-zA-Z0-9]/g, "_");
|
|
217
218
|
function getCacheDirectory(root) {
|
|
218
219
|
const tmpDir = path.join(tmp.tmpdir, "Needle", "build-pipeline-cache", root ? "" : cleanBuildPipelineVersion);
|
|
219
220
|
if (!fs.existsSync(tmpDir)) {
|
package/dist/cli/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
2
|
import { program } from '@donmccurdy/caporal';
|
|
3
3
|
import { packGLTF } from "../scripts/index.js";
|
|
4
|
-
import { printStats } from '../utils/stats.js';
|
|
4
|
+
import { printStats, writeFileStatsToFile } from '../utils/stats.js';
|
|
5
5
|
import { getVersion } from '../utils/version.js';
|
|
6
6
|
import { UsecaseOptions, createConfig, getConfig } from '../config/index.js';
|
|
7
7
|
import { make_progressive } from '../transforms/needle_progressive.js';
|
|
8
|
-
import { clearCache } from '../cache/cache.js';
|
|
8
|
+
import { cacheSizeLimit, clearCache } from '../cache/cache.js';
|
|
9
|
+
import { existsSync, statSync } from 'fs';
|
|
10
|
+
import { foreachGLTF } from '../utils/fileutils.js';
|
|
11
|
+
import { dirname } from 'path';
|
|
9
12
|
// For testing / dev you can run `npm link` in the package directory
|
|
10
13
|
// For removing the link run `npm rm --global @needle-tools/gltf-build-pipeline`
|
|
11
14
|
program
|
|
@@ -14,21 +17,22 @@ program
|
|
|
14
17
|
logger.info(getVersion());
|
|
15
18
|
})
|
|
16
19
|
.command("clear-caches", "Clear all caches")
|
|
17
|
-
.help(`
|
|
18
|
-
|
|
20
|
+
.help(`Clears all cached files
|
|
21
|
+
Caches are limited to ${cacheSizeLimit} MB disc space by default.
|
|
19
22
|
`)
|
|
20
23
|
.action(async ({ logger }) => {
|
|
21
24
|
logger.info("Clearing caches");
|
|
22
25
|
clearCache(logger);
|
|
23
26
|
})
|
|
24
|
-
.command("compress", "Compress
|
|
27
|
+
.command("compress", "Compress glTF, GLB or VRM files")
|
|
25
28
|
.help(`
|
|
26
29
|
Compress a single glTF file. The input file is compressed and written to the output file.
|
|
27
30
|
`)
|
|
28
|
-
.argument('<input>', "The input glTF, GLB or VRM file that should be compressed")
|
|
29
|
-
.option("--usecase <usecase>", `The usecase for the compression. This will set the compression settings.
|
|
31
|
+
.argument('<input>', "The input glTF, GLB or VRM file that should be compressed OR a directory that contains files to be compressed")
|
|
32
|
+
.option("--usecase <usecase>", `The usecase for the compression. This will set the compression settings. Possible options: [${UsecaseOptions.join(", ")}] e.g. \`--usecase ${UsecaseOptions[0]}\"`, { validator: program.STRING })
|
|
30
33
|
.option("--config <config>", "The path to a JSON file containing the compression settings. This can also be a needle.config.json file with a \"compression\" object or usecase string.", { validator: program.STRING })
|
|
31
34
|
.option("--output <output>", "The output file. If not provided, the input file will be overwritten.", { validator: program.STRING })
|
|
35
|
+
.option("--output-stats", "The path to a JSON file where the stats will be written to.", { validator: program.STRING })
|
|
32
36
|
.action(async ({ logger, args, options }) => {
|
|
33
37
|
logger.info("Transforming glTF file: " + args.input);
|
|
34
38
|
const stats = {
|
|
@@ -41,28 +45,64 @@ Compress a single glTF file. The input file is compressed and written to the out
|
|
|
41
45
|
let config = getConfig(options.config?.toString());
|
|
42
46
|
if (options.usecase) {
|
|
43
47
|
logger.info("Using usecase: " + options.usecase);
|
|
44
|
-
config = { ...
|
|
48
|
+
config = { ...createConfig(options.usecase.toString()), ...config, };
|
|
49
|
+
if (config.usecase != "unknown" && config.usecase != options.usecase.toString()) {
|
|
50
|
+
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.`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
config.usecase = options.usecase.toString();
|
|
54
|
+
}
|
|
45
55
|
}
|
|
46
56
|
const file = args.input.toString();
|
|
57
|
+
const outDir = statSync(file).isDirectory() ? file : dirname(file);
|
|
58
|
+
let outputIsDirectory = false;
|
|
59
|
+
if (statSync(file).isDirectory() && options.output) {
|
|
60
|
+
if (statSync(options.output.toString()).isDirectory()) {
|
|
61
|
+
outputIsDirectory = true;
|
|
62
|
+
if (!existsSync(options.output.toString())) {
|
|
63
|
+
logger.error("Output directory does not exist: " + options.output.toString());
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
logger.error("--output must be a directory if input is a directory");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const file_stats = [];
|
|
47
73
|
const set = new Set();
|
|
48
|
-
await
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
74
|
+
await foreachGLTF(file, async (file) => {
|
|
75
|
+
let outputFile = options.output?.toString() || file;
|
|
76
|
+
if (outputIsDirectory) {
|
|
77
|
+
const filename = file.replaceAll("\\", "/").split("/").pop();
|
|
78
|
+
outputFile = options.output?.toString() + "/" + filename;
|
|
79
|
+
}
|
|
80
|
+
await packGLTF(file, outputFile, {
|
|
81
|
+
config,
|
|
82
|
+
packedFiles: set,
|
|
83
|
+
stats,
|
|
84
|
+
debug: false,
|
|
85
|
+
useCache: true,
|
|
86
|
+
logger: logger,
|
|
87
|
+
file_stats: file_stats,
|
|
88
|
+
});
|
|
54
89
|
});
|
|
55
90
|
printStats(stats);
|
|
91
|
+
console.log("Stats: ", options);
|
|
92
|
+
if (options.outputStats) {
|
|
93
|
+
writeFileStatsToFile(file_stats, options.outputStats.toString());
|
|
94
|
+
}
|
|
56
95
|
})
|
|
57
|
-
.command("transform", "Transform
|
|
96
|
+
.command("transform", "Transform (progressive loading) and compress glTF, GLB or VRM files")
|
|
58
97
|
.help(`
|
|
59
98
|
This will produce multiple versions of the input file, each with a different level of optimization.
|
|
60
99
|
Each version will be compressed and written to the output directory.
|
|
61
100
|
`)
|
|
62
|
-
.argument('<input>', "The input glTF, GLB or VRM file that should be compressed")
|
|
63
|
-
.option("--usecase <usecase>", `The usecase for the compression. This will set the compression settings.
|
|
101
|
+
.argument('<input>', "The input glTF, GLB or VRM file that should be compressed OR a directory that contains files to be compressed")
|
|
102
|
+
.option("--usecase <usecase>", `The usecase for the compression. This will set the compression settings. Possible options: [${UsecaseOptions.join(", ")}]`, { validator: program.STRING })
|
|
64
103
|
.option("--config <config>", "The path to a JSON file containing the compression settings. This can also be a needle.config.json file with a \"compression\" object or usecase string.", { validator: program.STRING })
|
|
65
|
-
// .option("--output <output>", "The output
|
|
104
|
+
// .option("--output <output>", "The output directory. If not provided, the input files will be overwritten.", { validator: program.STRING })
|
|
105
|
+
.option("--output-stats", "The path to a JSON file where the stats will be written to.", { validator: program.STRING })
|
|
66
106
|
.action(async ({ logger, args, options }) => {
|
|
67
107
|
logger.info("Transforming glTF file: " + args.input);
|
|
68
108
|
const stats = {
|
|
@@ -75,28 +115,47 @@ Each version will be compressed and written to the output directory.
|
|
|
75
115
|
let config = getConfig(options.config?.toString());
|
|
76
116
|
if (options.usecase) {
|
|
77
117
|
logger.info("Using usecase: " + options.usecase);
|
|
78
|
-
config = { ...
|
|
118
|
+
config = { ...createConfig(options.usecase.toString()), ...config, };
|
|
119
|
+
if (config.usecase != "unknown" && config.usecase != options.usecase.toString()) {
|
|
120
|
+
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.`);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
config.usecase = options.usecase.toString();
|
|
124
|
+
}
|
|
79
125
|
}
|
|
80
126
|
const file = args.input.toString();
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
127
|
+
const outDir = statSync(file).isDirectory() ? file : dirname(file);
|
|
128
|
+
const file_stats = [];
|
|
129
|
+
const set = new Set();
|
|
130
|
+
await foreachGLTF(file, async (file) => {
|
|
131
|
+
logger.info("> Transform " + file);
|
|
132
|
+
const progressive_results = new Array();
|
|
133
|
+
await make_progressive({
|
|
134
|
+
path: file,
|
|
135
|
+
results: progressive_results,
|
|
136
|
+
config,
|
|
137
|
+
debug: false,
|
|
138
|
+
useCache: true,
|
|
139
|
+
});
|
|
140
|
+
const opts = {
|
|
141
|
+
config,
|
|
142
|
+
stats,
|
|
143
|
+
packedFiles: set,
|
|
144
|
+
debug: false,
|
|
145
|
+
useCache: true,
|
|
146
|
+
logger: logger,
|
|
147
|
+
file_stats,
|
|
148
|
+
};
|
|
149
|
+
for (const res of progressive_results) {
|
|
150
|
+
logger.info("> Compress " + res);
|
|
151
|
+
await packGLTF(res, res, opts);
|
|
152
|
+
}
|
|
153
|
+
logger.info("> Compress " + file);
|
|
154
|
+
await packGLTF(file, file, opts);
|
|
88
155
|
});
|
|
89
|
-
console.log("Progressive results: ", progressive_results);
|
|
90
|
-
const opts = {
|
|
91
|
-
config,
|
|
92
|
-
stats,
|
|
93
|
-
debug: false,
|
|
94
|
-
useCache: true,
|
|
95
|
-
};
|
|
96
|
-
for (const prog of progressive_results) {
|
|
97
|
-
await packGLTF(prog, prog, opts);
|
|
98
|
-
}
|
|
99
|
-
await packGLTF(file, file, opts);
|
|
100
156
|
printStats(stats);
|
|
157
|
+
if (options.outputStats) {
|
|
158
|
+
writeFileStatsToFile(file_stats, options.outputStats.toString());
|
|
159
|
+
}
|
|
101
160
|
});
|
|
102
161
|
program.run();
|
package/dist/config/index.d.ts
CHANGED
package/dist/config/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "fs";
|
|
2
|
-
export const UsecaseOptions = ["product"
|
|
2
|
+
export const UsecaseOptions = ["product"];
|
|
3
3
|
const defaultConfig = {
|
|
4
4
|
usecase: "unknown",
|
|
5
5
|
};
|
|
@@ -101,25 +101,38 @@ function internalResolveConfig() {
|
|
|
101
101
|
function resolveConfig(filename, content) {
|
|
102
102
|
// Handle config being part of `needle.config.json`
|
|
103
103
|
if (filename.endsWith("needle.config.json")) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
104
|
+
try {
|
|
105
|
+
const config = JSON.parse(content);
|
|
106
|
+
if (config.optimization) {
|
|
107
|
+
console.log("INFO: Found optimization config at \"" + filename + "\".");
|
|
108
|
+
// If the optimization config is a string, it's the usecase
|
|
109
|
+
if (typeof config.optimization === "string") {
|
|
110
|
+
return createConfig(config.optimization);
|
|
111
|
+
}
|
|
112
|
+
return config.optimization;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
console.log("INFO: No optimization config found in needle.config.json. Using default config.");
|
|
116
|
+
return null;
|
|
110
117
|
}
|
|
111
|
-
return config.optimization;
|
|
112
118
|
}
|
|
113
|
-
|
|
114
|
-
console.
|
|
115
|
-
return null;
|
|
119
|
+
catch (err) {
|
|
120
|
+
console.error(`ERR: Failed parsing config \"${err.message}\" at ${filename}`);
|
|
116
121
|
}
|
|
117
122
|
}
|
|
118
123
|
// Handle config being the content of the file
|
|
119
124
|
else if (content.trimStart().startsWith("{")) {
|
|
120
|
-
|
|
125
|
+
try {
|
|
126
|
+
return JSON.parse(content);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
if (err instanceof Error)
|
|
130
|
+
console.error(`ERR: Failed parsing config \"${err.message}\" at ${filename}`);
|
|
131
|
+
else
|
|
132
|
+
console.error(`ERR: Failed parsing config \"${err}\" at ${filename}`);
|
|
133
|
+
}
|
|
121
134
|
}
|
|
122
135
|
else {
|
|
123
|
-
|
|
136
|
+
console.error("ERR: Invalid config file format. Expected JSON object at " + filename);
|
|
124
137
|
}
|
|
125
138
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ILogger } from '@gltf-transform/core';
|
|
2
|
+
import { FileStats, Stats } from "../utils/index.js";
|
|
2
3
|
import { Config } from '../config/index.js';
|
|
3
4
|
export type PackGLTFOptions = {
|
|
4
5
|
debug: boolean;
|
|
5
6
|
useCache: boolean;
|
|
6
7
|
packedFiles?: Set<string>;
|
|
7
8
|
stats?: Stats;
|
|
9
|
+
file_stats: FileStats[] | null;
|
|
8
10
|
config?: Config;
|
|
11
|
+
logger?: ILogger;
|
|
9
12
|
};
|
|
10
13
|
/** Compress a glTF file
|
|
11
14
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import { NodeIO } from '@gltf-transform/core';
|
|
2
|
+
import { Logger, NodeIO, Verbosity } from '@gltf-transform/core';
|
|
3
3
|
import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
|
|
4
4
|
import draco3d from 'draco3dgltf';
|
|
5
5
|
import { existsSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
@@ -7,17 +7,20 @@ import { MeshoptDecoder, MeshoptEncoder } from 'meshoptimizer';
|
|
|
7
7
|
import { resample } from '@gltf-transform/functions';
|
|
8
8
|
import { ALL_EXTENSIONS as NEEDLE_EXTENSIONS, NEEDLE_compression_texture, NEEDLE_mesh_compression, } from '../extensions/index.js';
|
|
9
9
|
import { isMeshLOD, needle_animation_transform, needle_asset, needle_mesh_transform, needle_texture_transform, } from '../transforms/index.js';
|
|
10
|
-
import { writeNodeIO } from "../utils/index.js";
|
|
10
|
+
import { createNewFilestats, writeNodeIO } from "../utils/index.js";
|
|
11
11
|
import { addToCache, getHash, tryGetFromCache, } from "../cache/index.js";
|
|
12
12
|
import { getConfig } from '../config/index.js';
|
|
13
13
|
/** Compress a glTF file
|
|
14
14
|
*/
|
|
15
15
|
export async function packGLTF(inputFile, outputFile, options) {
|
|
16
|
+
if (!options.logger) {
|
|
17
|
+
options.logger = new Logger(Verbosity.INFO);
|
|
18
|
+
}
|
|
19
|
+
const { debug, stats, packedFiles, useCache, logger } = options;
|
|
16
20
|
if (!existsSync(inputFile)) {
|
|
17
|
-
|
|
21
|
+
logger.error("WARN: can not pack, file does not exist: " + inputFile);
|
|
18
22
|
return;
|
|
19
23
|
}
|
|
20
|
-
const { debug, stats, packedFiles, useCache } = options;
|
|
21
24
|
if (debug && inputFile.includes(".debug.")) {
|
|
22
25
|
return;
|
|
23
26
|
}
|
|
@@ -31,7 +34,7 @@ export async function packGLTF(inputFile, outputFile, options) {
|
|
|
31
34
|
cacheKey = "packed-" + hash;
|
|
32
35
|
const cached = tryGetFromCache(cacheKey);
|
|
33
36
|
if (cached) {
|
|
34
|
-
|
|
37
|
+
logger.info(`→ Loaded compressed file from cache: \"${path.basename(inputFile)}\"`);
|
|
35
38
|
writeFileSync(inputFile, cached);
|
|
36
39
|
return;
|
|
37
40
|
}
|
|
@@ -44,7 +47,7 @@ export async function packGLTF(inputFile, outputFile, options) {
|
|
|
44
47
|
try {
|
|
45
48
|
const config = options.config || getConfig();
|
|
46
49
|
const prevSizeInMB = statSync(inputFile).size / 1024 / 1024;
|
|
47
|
-
|
|
50
|
+
logger.info(`→ Compressing ${prevSizeInMB.toFixed(1)} MB: ${inputFile}`);
|
|
48
51
|
// const isProgressiveAsset = isLOD(file);
|
|
49
52
|
const startTime = Date.now();
|
|
50
53
|
const io = new NodeIO()
|
|
@@ -62,8 +65,8 @@ export async function packGLTF(inputFile, outputFile, options) {
|
|
|
62
65
|
});
|
|
63
66
|
const document = await io.read(inputFile);
|
|
64
67
|
/** @ts-ignore */
|
|
65
|
-
document.getLogger().verbosity =
|
|
66
|
-
let
|
|
68
|
+
document.getLogger().verbosity = Verbosity.ERROR;
|
|
69
|
+
let totalGPUTextureMemory = 0;
|
|
67
70
|
/** @type {Array<import('@gltf-transform/core').Transform>} */
|
|
68
71
|
const transforms = [
|
|
69
72
|
needle_asset(),
|
|
@@ -73,8 +76,7 @@ export async function packGLTF(inputFile, outputFile, options) {
|
|
|
73
76
|
noToktx: false,
|
|
74
77
|
debug: debug,
|
|
75
78
|
reportGPUMemory: val => {
|
|
76
|
-
|
|
77
|
-
stats.totalGPUMemoryInMB += val;
|
|
79
|
+
totalGPUTextureMemory += val;
|
|
78
80
|
},
|
|
79
81
|
config,
|
|
80
82
|
}),
|
|
@@ -99,18 +101,22 @@ export async function packGLTF(inputFile, outputFile, options) {
|
|
|
99
101
|
const duration = Date.now() - startTime;
|
|
100
102
|
const sizeInMB = statSync(outputFile).size / 1024 / 1024;
|
|
101
103
|
const filename = path.basename(outputFile);
|
|
102
|
-
const gpuMemoryInMB =
|
|
103
|
-
|
|
104
|
+
const gpuMemoryInMB = totalGPUTextureMemory / 1024 / 1024;
|
|
105
|
+
logger.info(`← Compressing done in ${(duration / 1000).toFixed(1)} sec, ${prevSizeInMB.toFixed(1)} MB → ${sizeInMB.toFixed(1)} MB (GPU: ${gpuMemoryInMB.toFixed(1)} MB textures): ${filename} at ${outputFile}`);
|
|
104
106
|
if (stats) {
|
|
105
107
|
stats.totalFilesProcessed++;
|
|
106
108
|
stats.totalFileSizeInMB += sizeInMB;
|
|
107
109
|
stats.totalFileSizeInMBBefore += prevSizeInMB;
|
|
108
110
|
stats.totalGPUMemoryInMB += gpuMemoryInMB;
|
|
109
111
|
}
|
|
112
|
+
const filestats = createNewFilestats(outputFile, document, {
|
|
113
|
+
gpu_memory: totalGPUTextureMemory,
|
|
114
|
+
});
|
|
115
|
+
options.file_stats?.push(filestats);
|
|
110
116
|
return true;
|
|
111
117
|
}
|
|
112
118
|
catch (err) {
|
|
113
|
-
|
|
119
|
+
logger.error("ERR: packing " + inputFile + " failed: " + err.message);
|
|
114
120
|
if (debug)
|
|
115
121
|
throw err;
|
|
116
122
|
return false;
|
|
@@ -9,7 +9,7 @@ declare interface ProgressiveOptions {
|
|
|
9
9
|
}
|
|
10
10
|
export declare function make_progressive(opts: ProgressiveOptions): Promise<void>;
|
|
11
11
|
export declare function createTransform(name: string, fn: Transform): Transform;
|
|
12
|
-
export declare function make_progressive_textures(filePath: string, _options
|
|
12
|
+
export declare function make_progressive_textures(filePath: string, _options: TextureResizeOptions, opts: ProgressiveOptions): Transform;
|
|
13
13
|
/** Options for the {@link textureResize} function. */
|
|
14
14
|
export interface TextureResizeOptions {
|
|
15
15
|
/**
|
|
@@ -69,11 +69,15 @@ async function onProcess(file, opts) {
|
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
+
let smallesTextureSize = 128;
|
|
73
|
+
if (opts.config.usecase === "product") {
|
|
74
|
+
smallesTextureSize = 512;
|
|
75
|
+
}
|
|
72
76
|
//@ts-ignore
|
|
73
77
|
document.getLogger().verbosity = 0;
|
|
74
78
|
const transforms = [
|
|
75
79
|
needle_asset(),
|
|
76
|
-
make_progressive_textures(file, { size: [
|
|
80
|
+
make_progressive_textures(file, { size: [smallesTextureSize, smallesTextureSize] }, opts),
|
|
77
81
|
sparse(),
|
|
78
82
|
];
|
|
79
83
|
// TODO: progressive meshes aren't supported yet for VRMs
|
|
@@ -143,14 +147,14 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
|
|
|
143
147
|
let settings = texture.getExtension(NEEDLE_progressive_texture_settings.EXTENSION_NAME);
|
|
144
148
|
if (!settings) {
|
|
145
149
|
const size = texture.getSize();
|
|
146
|
-
if (size?.length >= 2 && size[0] <=
|
|
147
|
-
logger.warn(`[${NAME}] Skipping texture[${textureIndex}], no settings found and the texture is already
|
|
150
|
+
if (size?.length >= 2 && size[0] <= options.size[0] && size[1] <= options.size[1]) {
|
|
151
|
+
logger.warn(`[${NAME}] Skipping texture[${textureIndex}], no settings found and the texture is already <= ${size[0]}x${size[1]}`);
|
|
148
152
|
continue;
|
|
149
153
|
}
|
|
150
154
|
else {
|
|
151
155
|
settings = {
|
|
152
156
|
guid: generateGuid(filename + "_texture_" + index.toString()),
|
|
153
|
-
maxSize:
|
|
157
|
+
maxSize: _options.size[0],
|
|
154
158
|
};
|
|
155
159
|
logger.debug(`[${NAME}] No settings found for texture[${index}] - will use default settings (maxSize: ${settings.maxSize}, guid: ${settings.guid})`);
|
|
156
160
|
}
|
|
@@ -360,7 +364,10 @@ export function make_progressive_meshes(filePath, _options, opts) {
|
|
|
360
364
|
console.log(`[${NAME}]: Begin generating progressive meshes for \"${filename}\"`);
|
|
361
365
|
let index = 0;
|
|
362
366
|
// the lowest LOD (last entry) will be saved in the original file
|
|
363
|
-
|
|
367
|
+
let MAX_LODS = 6;
|
|
368
|
+
if (opts.config.usecase === "product") {
|
|
369
|
+
MAX_LODS = 3;
|
|
370
|
+
}
|
|
364
371
|
const LODS = [
|
|
365
372
|
{
|
|
366
373
|
ratio: 1,
|
|
@@ -369,10 +376,13 @@ export function make_progressive_meshes(filePath, _options, opts) {
|
|
|
369
376
|
},
|
|
370
377
|
];
|
|
371
378
|
for (let l = 0; l < MAX_LODS; l++) {
|
|
372
|
-
const t = l / MAX_LODS;
|
|
379
|
+
// const t = l / MAX_LODS;
|
|
373
380
|
const latest = LODS[LODS.length - 1];
|
|
374
381
|
const ratio = latest.ratio * .5;
|
|
375
|
-
|
|
382
|
+
let error = Math.min(.013 * (l + 1), 0.05);
|
|
383
|
+
if (opts.config.usecase === "product") {
|
|
384
|
+
error *= .5;
|
|
385
|
+
}
|
|
376
386
|
LODS.push({
|
|
377
387
|
ratio: ratio,
|
|
378
388
|
error,
|
|
@@ -626,6 +636,7 @@ async function createMeshLod(doc, basePath, level, mesh, meshInfo, guid, lodLeve
|
|
|
626
636
|
mesh["needle:mesh-hash"] = hash;
|
|
627
637
|
}
|
|
628
638
|
hash += getHash(lodLevel);
|
|
639
|
+
hash += getHash(opts.config);
|
|
629
640
|
if (opts.useCache !== false) {
|
|
630
641
|
cacheKey = `mesh_lod_${level}_${hash}`;
|
|
631
642
|
const cachedValue = tryGetFromCache(cacheKey);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare function getFileName(filepath: string): string;
|
|
1
2
|
export declare type GLTFFileCallback = (file: string) => void | Promise<void>;
|
|
2
3
|
export declare function foreachGLTF(args: string | string[], callback: GLTFFileCallback): Promise<void>;
|
|
3
4
|
export declare function replaceExtension(filepath: string, newExtension: string): string;
|
package/dist/utils/fileutils.js
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
import { existsSync, statSync, readdirSync, readFileSync } from 'fs';
|
|
2
2
|
import { isAbsolute } from 'path';
|
|
3
|
+
export function getFileName(filepath) {
|
|
4
|
+
const parts = filepath.replaceAll("\\", "/").split("/");
|
|
5
|
+
return parts[parts.length - 1];
|
|
6
|
+
}
|
|
3
7
|
export async function foreachGLTF(args, callback) {
|
|
4
8
|
// get original directory this run was invoked from
|
|
5
9
|
const initDir = process.env.INIT_CWD;
|
|
6
10
|
const context = {
|
|
7
11
|
baseDirectory: initDir,
|
|
8
|
-
|
|
12
|
+
files: [],
|
|
9
13
|
};
|
|
10
14
|
// traverse recursively through all files and directories
|
|
11
15
|
if (Array.isArray(args)) {
|
|
12
16
|
for (const arg of args) {
|
|
13
|
-
await testEntry(arg,
|
|
17
|
+
await testEntry(arg, context);
|
|
14
18
|
}
|
|
15
19
|
}
|
|
16
20
|
else if (typeof args === "string") {
|
|
17
|
-
await testEntry(args,
|
|
21
|
+
await testEntry(args, context);
|
|
18
22
|
}
|
|
19
23
|
// try to run with needle config
|
|
20
|
-
if (context.
|
|
24
|
+
if (context.files?.length <= 0 && context.baseDirectory) {
|
|
21
25
|
const needleConfigPath = context.baseDirectory + "/needle.config.json";
|
|
22
26
|
if (existsSync(needleConfigPath)) {
|
|
23
27
|
const config = JSON.parse(readFileSync(needleConfigPath, "utf8"));
|
|
@@ -26,13 +30,13 @@ export async function foreachGLTF(args, callback) {
|
|
|
26
30
|
// If it's an absolute path
|
|
27
31
|
if (existsSync(buildDirectory)) {
|
|
28
32
|
console.log("INFO: Using build directory from needle.config.json: " + buildDirectory);
|
|
29
|
-
await testEntry(buildDirectory,
|
|
33
|
+
await testEntry(buildDirectory, context);
|
|
30
34
|
}
|
|
31
35
|
else { // if its a relative path
|
|
32
36
|
const newPath = context.baseDirectory + "/" + buildDirectory;
|
|
33
37
|
if (existsSync(newPath)) {
|
|
34
38
|
console.log("INFO: Using build directory from needle.config.json: " + newPath);
|
|
35
|
-
await testEntry(newPath,
|
|
39
|
+
await testEntry(newPath, context);
|
|
36
40
|
}
|
|
37
41
|
}
|
|
38
42
|
}
|
|
@@ -43,11 +47,16 @@ export async function foreachGLTF(args, callback) {
|
|
|
43
47
|
// await testEntry(context.baseDirectory + "/dist", callback, context);
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
|
-
if (context.
|
|
50
|
+
if (context.files.length <= 0) {
|
|
47
51
|
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);
|
|
48
52
|
}
|
|
53
|
+
else {
|
|
54
|
+
for (const file of context.files) {
|
|
55
|
+
await callback(file);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
49
58
|
}
|
|
50
|
-
async function testEntry(entry,
|
|
59
|
+
async function testEntry(entry, context) {
|
|
51
60
|
// ignore argument names (e.g. --debug)
|
|
52
61
|
if (entry.startsWith("--")) {
|
|
53
62
|
return false;
|
|
@@ -90,11 +99,7 @@ async function testEntry(entry, callback, context) {
|
|
|
90
99
|
}
|
|
91
100
|
}
|
|
92
101
|
if (entry.endsWith(".glb") || entry.endsWith(".gltf") || entry.endsWith(".vrm")) {
|
|
93
|
-
context.
|
|
94
|
-
const res = callback(entry);
|
|
95
|
-
if (res instanceof Promise) {
|
|
96
|
-
await res;
|
|
97
|
-
}
|
|
102
|
+
context.files.push(entry);
|
|
98
103
|
}
|
|
99
104
|
else {
|
|
100
105
|
const stat = statSync(entry);
|
|
@@ -105,7 +110,7 @@ async function testEntry(entry, callback, context) {
|
|
|
105
110
|
}
|
|
106
111
|
const files = readdirSync(entry);
|
|
107
112
|
for (const file of files) {
|
|
108
|
-
await testEntry(entry + "/" + file,
|
|
113
|
+
await testEntry(entry + "/" + file, context);
|
|
109
114
|
}
|
|
110
115
|
}
|
|
111
116
|
else {
|
package/dist/utils/stats.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Logger } from "@donmccurdy/caporal";
|
|
2
|
+
import { Document } from "@gltf-transform/core";
|
|
1
3
|
export type Stats = {
|
|
2
4
|
startTime: number;
|
|
3
5
|
totalGPUMemoryInMB: number;
|
|
@@ -5,4 +7,17 @@ export type Stats = {
|
|
|
5
7
|
totalFileSizeInMB: number;
|
|
6
8
|
totalFileSizeInMBBefore: number;
|
|
7
9
|
};
|
|
8
|
-
export declare function printStats(stats:
|
|
10
|
+
export declare function printStats(stats: Stats, logger?: Logger): void;
|
|
11
|
+
export type FileStats = {
|
|
12
|
+
filename: string;
|
|
13
|
+
/** Size in bytes */
|
|
14
|
+
size: number;
|
|
15
|
+
gpu_memory: number;
|
|
16
|
+
vertex_count: number;
|
|
17
|
+
nodes: number;
|
|
18
|
+
materials: number;
|
|
19
|
+
};
|
|
20
|
+
export declare function createNewFilestats(file: string, document: Document, stats: {
|
|
21
|
+
gpu_memory: number;
|
|
22
|
+
}): FileStats;
|
|
23
|
+
export declare function writeFileStatsToFile(filestats: FileStats[], output_path: string): void;
|
package/dist/utils/stats.js
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
import { getVersion } from "./version.js";
|
|
2
|
-
|
|
2
|
+
import { getFileName } from "./fileutils.js";
|
|
3
|
+
import { statSync, writeFileSync } from "fs";
|
|
4
|
+
export function printStats(stats, logger) {
|
|
3
5
|
const duration = Date.now() - stats.startTime;
|
|
4
6
|
const durationInMinutes = duration / 1000 / 60;
|
|
5
|
-
|
|
7
|
+
const msg = `[Needle Build Pipeline] v${getVersion()} — Finished packing ${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
|
+
if (logger)
|
|
9
|
+
logger.info(msg);
|
|
10
|
+
else
|
|
11
|
+
console.log(msg);
|
|
12
|
+
}
|
|
13
|
+
export function createNewFilestats(file, document, stats) {
|
|
14
|
+
const filename = getFileName(file);
|
|
15
|
+
const root = document.getRoot();
|
|
16
|
+
const filestats = {
|
|
17
|
+
filename,
|
|
18
|
+
size: statSync(file).size,
|
|
19
|
+
gpu_memory: stats.gpu_memory,
|
|
20
|
+
nodes: root.listNodes().length,
|
|
21
|
+
materials: root.listMaterials().length,
|
|
22
|
+
vertex_count: 0,
|
|
23
|
+
};
|
|
24
|
+
// Count vertices
|
|
25
|
+
for (const mesh of root.listMeshes()) {
|
|
26
|
+
filestats.vertex_count += mesh.listPrimitives().reduce((acc, p) => acc + p.getAttribute('POSITION').getCount(), 0);
|
|
27
|
+
}
|
|
28
|
+
filestats.vertex_count = Math.round(filestats.vertex_count / 3);
|
|
29
|
+
return filestats;
|
|
30
|
+
}
|
|
31
|
+
export function writeFileStatsToFile(filestats, output_path) {
|
|
32
|
+
if (output_path == undefined) {
|
|
33
|
+
console.error("No stats output path provided");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
console.log("Writing file stats to: " + output_path);
|
|
37
|
+
const json = JSON.stringify(filestats, null, 2);
|
|
38
|
+
writeFileSync(output_path, json);
|
|
6
39
|
}
|
package/dist/utils/version.js
CHANGED
|
@@ -3,12 +3,14 @@ let version = undefined;
|
|
|
3
3
|
let isLocal = false;
|
|
4
4
|
export function getVersion(marklocal = true) {
|
|
5
5
|
if (version === undefined) {
|
|
6
|
+
console.log(process.argv);
|
|
6
7
|
const scriptLocation = process.argv[1].replaceAll("\\", "/");
|
|
8
|
+
console.log("Determining version from " + scriptLocation);
|
|
7
9
|
const path = scriptLocation.split('/');
|
|
8
10
|
let packageJsonPath = null;
|
|
9
11
|
while (path.length) {
|
|
10
12
|
const dirname = path.join('/');
|
|
11
|
-
if (statSync(dirname
|
|
13
|
+
if (statSync(dirname).isDirectory()) {
|
|
12
14
|
const filename = dirname + '/package.json';
|
|
13
15
|
if (existsSync(filename)) {
|
|
14
16
|
packageJsonPath = filename;
|
|
@@ -27,6 +29,9 @@ export function getVersion(marklocal = true) {
|
|
|
27
29
|
else
|
|
28
30
|
console.log("Could not find package.json at " + packageJsonPath);
|
|
29
31
|
}
|
|
32
|
+
if (!version) {
|
|
33
|
+
throw new Error("Could not determine version");
|
|
34
|
+
}
|
|
30
35
|
}
|
|
31
36
|
let res = version;
|
|
32
37
|
if (marklocal && isLocal)
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@needle-tools/gltf-build-pipeline",
|
|
3
|
-
"version": "1.6.0-alpha.
|
|
3
|
+
"version": "1.6.0-alpha.3",
|
|
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",
|
|
7
|
+
"readme": "README.md",
|
|
7
8
|
"bin": {
|
|
8
9
|
"gltf-build-pipeline": "./dist/cli/index.js"
|
|
9
10
|
},
|