@needle-tools/gltf-build-pipeline 2.1.0-alpha.6 → 2.2.0-alpha.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 +7 -0
- package/dist/cli/index.js +118 -0
- package/dist/{src/config → config}/index.d.ts +8 -2
- package/dist/{src/config → config}/index.js +31 -15
- package/dist/{src/transforms → transforms}/needle_mesh_transform.js +1 -1
- package/dist/{src/transforms → transforms}/needle_progressive.js +20 -18
- package/dist/{src/transforms → transforms}/needle_toktx.js +1 -1
- package/dist/{src/transforms → transforms}/toktx.d.ts +1 -1
- package/dist/{src/transforms → transforms}/toktx.js +2 -6
- package/dist/transforms/toktx_types.d.ts +78 -0
- package/dist/transforms/toktx_types.js +62 -0
- package/dist/{src/utils → utils}/version.js +1 -1
- package/package.json +10 -9
- package/scripts/clear-caches.mjs +1 -1
- package/scripts/defaults.mjs +2 -2
- package/scripts/fileutils.mjs +1 -1
- package/scripts/index.mjs +1 -1
- package/scripts/limit-caches.mjs +1 -1
- package/scripts/make-progressive.mjs +2 -2
- package/scripts/pack-gltf.mjs +2 -2
- package/tsconfig.json +7 -3
- package/dist/___config.json +0 -3
- package/dist/package.json +0 -75
- package/dist/src/cli/index.js +0 -192
- /package/dist/{src/cache → cache}/cache.d.ts +0 -0
- /package/dist/{src/cache → cache}/cache.js +0 -0
- /package/dist/{src/cache → cache}/index.d.ts +0 -0
- /package/dist/{src/cache → cache}/index.js +0 -0
- /package/dist/{src/cli → cli}/index.d.ts +0 -0
- /package/dist/{src/constants.d.ts → constants.d.ts} +0 -0
- /package/dist/{src/constants.js → constants.js} +0 -0
- /package/dist/{src/extensions → extensions}/NEEDLE_compression_texture/NEEDLE_compression_texture.d.ts +0 -0
- /package/dist/{src/extensions → extensions}/NEEDLE_compression_texture/NEEDLE_compression_texture.js +0 -0
- /package/dist/{src/extensions → extensions}/NEEDLE_compression_texture/index.d.ts +0 -0
- /package/dist/{src/extensions → extensions}/NEEDLE_compression_texture/index.js +0 -0
- /package/dist/{src/extensions → extensions}/NEEDLE_mesh_compression/NEEDLE_mesh_compression.d.ts +0 -0
- /package/dist/{src/extensions → extensions}/NEEDLE_mesh_compression/NEEDLE_mesh_compression.js +0 -0
- /package/dist/{src/extensions → extensions}/NEEDLE_mesh_compression/index.d.ts +0 -0
- /package/dist/{src/extensions → extensions}/NEEDLE_mesh_compression/index.js +0 -0
- /package/dist/{src/extensions → extensions}/NEEDLE_opaque/NEEDLE_opaque.d.ts +0 -0
- /package/dist/{src/extensions → extensions}/NEEDLE_opaque/NEEDLE_opaque.js +0 -0
- /package/dist/{src/extensions → extensions}/NEEDLE_opaque/index.d.ts +0 -0
- /package/dist/{src/extensions → extensions}/NEEDLE_opaque/index.js +0 -0
- /package/dist/{src/extensions → extensions}/NEEDLE_progressive/NEEDLE_progressive.d.ts +0 -0
- /package/dist/{src/extensions → extensions}/NEEDLE_progressive/NEEDLE_progressive.js +0 -0
- /package/dist/{src/extensions → extensions}/NEEDLE_progressive/index.d.ts +0 -0
- /package/dist/{src/extensions → extensions}/NEEDLE_progressive/index.js +0 -0
- /package/dist/{src/extensions → extensions}/NEEDLE_progressive_mesh_settings/index.d.ts +0 -0
- /package/dist/{src/extensions → extensions}/NEEDLE_progressive_mesh_settings/index.js +0 -0
- /package/dist/{src/extensions → extensions}/NEEDLE_progressive_texture_settings/index.d.ts +0 -0
- /package/dist/{src/extensions → extensions}/NEEDLE_progressive_texture_settings/index.js +0 -0
- /package/dist/{src/extensions → extensions}/index.d.ts +0 -0
- /package/dist/{src/extensions → extensions}/index.js +0 -0
- /package/dist/{src/extensions → extensions}/utils.d.ts +0 -0
- /package/dist/{src/extensions → extensions}/utils.js +0 -0
- /package/dist/{src/index.d.ts → index.d.ts} +0 -0
- /package/dist/{src/index.js → index.js} +0 -0
- /package/dist/{src/scripts → scripts}/index.d.ts +0 -0
- /package/dist/{src/scripts → scripts}/index.js +0 -0
- /package/dist/{src/scripts → scripts}/pack-gltf.d.ts +0 -0
- /package/dist/{src/scripts → scripts}/pack-gltf.js +0 -0
- /package/dist/{src/transforms → transforms}/index.d.ts +0 -0
- /package/dist/{src/transforms → transforms}/index.js +0 -0
- /package/dist/{src/transforms → transforms}/needle_animation_transform.d.ts +0 -0
- /package/dist/{src/transforms → transforms}/needle_animation_transform.js +0 -0
- /package/dist/{src/transforms → transforms}/needle_asset.d.ts +0 -0
- /package/dist/{src/transforms → transforms}/needle_asset.js +0 -0
- /package/dist/{src/transforms → transforms}/needle_common.d.ts +0 -0
- /package/dist/{src/transforms → transforms}/needle_common.js +0 -0
- /package/dist/{src/transforms → transforms}/needle_mesh_transform.d.ts +0 -0
- /package/dist/{src/transforms → transforms}/needle_progressive.d.ts +0 -0
- /package/dist/{src/transforms → transforms}/needle_texture_transform.d.ts +0 -0
- /package/dist/{src/transforms → transforms}/needle_texture_transform.js +0 -0
- /package/dist/{src/transforms → transforms}/needle_toktx.d.ts +0 -0
- /package/dist/{src/transforms → transforms}/needle_webp.d.ts +0 -0
- /package/dist/{src/transforms → transforms}/needle_webp.js +0 -0
- /package/dist/{src/transforms → transforms}/util.d.ts +0 -0
- /package/dist/{src/transforms → transforms}/util.js +0 -0
- /package/dist/{src/utils → utils}/compression_utils.d.ts +0 -0
- /package/dist/{src/utils → utils}/compression_utils.js +0 -0
- /package/dist/{src/utils → utils}/fileutils.d.ts +0 -0
- /package/dist/{src/utils → utils}/fileutils.js +0 -0
- /package/dist/{src/utils → utils}/guid.d.ts +0 -0
- /package/dist/{src/utils → utils}/guid.js +0 -0
- /package/dist/{src/utils → utils}/index.d.ts +0 -0
- /package/dist/{src/utils → utils}/index.js +0 -0
- /package/dist/{src/utils → utils}/mesh.d.ts +0 -0
- /package/dist/{src/utils → utils}/mesh.js +0 -0
- /package/dist/{src/utils → utils}/nodeio.d.ts +0 -0
- /package/dist/{src/utils → utils}/nodeio.js +0 -0
- /package/dist/{src/utils → utils}/stats.d.ts +0 -0
- /package/dist/{src/utils → utils}/stats.js +0 -0
- /package/dist/{src/utils → utils}/texture.d.ts +0 -0
- /package/dist/{src/utils → utils}/texture.js +0 -0
- /package/dist/{src/utils → utils}/version.d.ts +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@ 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.2.0-alpha.2] - 2023-06-06
|
|
8
|
+
- Add: CLI `--progressive` option to `transform` command (default is enabled)
|
|
9
|
+
- Add: config `meshes: { lods: boolean }` option
|
|
10
|
+
- Remove: CLI "compress" command
|
|
11
|
+
- Change: enable texture LODs by default
|
|
12
|
+
- Fix: `--config` path can now point to a directory or a `needle.config.json` file as well
|
|
13
|
+
|
|
7
14
|
## [2.1.0-alpha.6] - 2023-06-04
|
|
8
15
|
- fix: errors when invoking npm scripts caused by the cli program
|
|
9
16
|
- fix: remove cache directory error
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#! /usr/bin/env node
|
|
2
|
+
import { program } from '@donmccurdy/caporal';
|
|
3
|
+
import { packGLTF } from "../scripts/index.js";
|
|
4
|
+
import { printStats, writeFileStatsToFile } from '../utils/stats.js';
|
|
5
|
+
import { getVersion } from '../utils/version.js';
|
|
6
|
+
import { UsecaseOptions, createConfig, getConfig } from '../config/index.js';
|
|
7
|
+
import { make_progressive } from '../transforms/needle_progressive.js';
|
|
8
|
+
import { cacheSizeLimit, clearCache } from '../cache/cache.js';
|
|
9
|
+
import { statSync } from 'fs';
|
|
10
|
+
import { foreachGLTF, getDirName } from '../utils/fileutils.js';
|
|
11
|
+
import path, { dirname } from 'path';
|
|
12
|
+
import { ERROR_CODES } from '../constants.js';
|
|
13
|
+
// For testing / dev you can run `npm link` in the package directory
|
|
14
|
+
// For removing the link run `npm rm --global @needle-tools/gltf-build-pipeline`
|
|
15
|
+
program
|
|
16
|
+
.command("version", "Print current verison")
|
|
17
|
+
.action(async ({ logger }) => {
|
|
18
|
+
logger.info(getVersion());
|
|
19
|
+
})
|
|
20
|
+
.command("clear-caches", "Clear all caches")
|
|
21
|
+
.help(`Clears all cached files
|
|
22
|
+
Caches are limited to ${cacheSizeLimit} MB disc space by default.
|
|
23
|
+
`)
|
|
24
|
+
.action(async ({ logger }) => {
|
|
25
|
+
logger.info("Clearing caches");
|
|
26
|
+
clearCache(logger);
|
|
27
|
+
})
|
|
28
|
+
.command("transform", "Transform (progressive loading) and compress glTF, GLB or VRM files")
|
|
29
|
+
.help(`
|
|
30
|
+
This will produce multiple versions of the input file, each with a different level of optimization.
|
|
31
|
+
Each version will be compressed and written to the output directory.
|
|
32
|
+
`)
|
|
33
|
+
.argument('<input>', "The input glTF, GLB or VRM file that should be compressed OR a directory that contains files to be compressed")
|
|
34
|
+
// .argument('<output>', "The output file. If not provided, the input file will be overwritten.", { default: "" })
|
|
35
|
+
.option("--progressive", `When enabled files will processed to be progressively loaded`, { validator: program.BOOLEAN, default: true })
|
|
36
|
+
.option("--usecase <usecase>", `The usecase for the compression. This will set the compression settings. Possible options: [${UsecaseOptions.join(", ")}]`, { validator: program.STRING })
|
|
37
|
+
.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 })
|
|
38
|
+
.option("--stats", "Output a stats file next to input file (or inside the input directory). It will be named as the input file/directory \"<input>.needle.stats.json\"", { validator: program.BOOLEAN, default: false })
|
|
39
|
+
.option("--cache", "Use \"--cache False\" to disable caching.\nWhen caching is enabled then previously processed files will be detected and loaded from disc cache instead of being processed again if settings or the file contents have not changed.", { validator: program.BOOLEAN, default: true })
|
|
40
|
+
.option("--debug", "Enable for more verbose outout with \"--debug True\"", { validator: program.BOOLEAN, default: false })
|
|
41
|
+
.action(async ({ logger, args, options }) => {
|
|
42
|
+
logger.info("Transforming glTF file: " + args.input);
|
|
43
|
+
const stats = {
|
|
44
|
+
startTime: Date.now(),
|
|
45
|
+
totalGPUMemoryInMB: 0,
|
|
46
|
+
totalFilesProcessed: 0,
|
|
47
|
+
totalFileSizeInMB: 0,
|
|
48
|
+
totalFileSizeInMBBefore: 0,
|
|
49
|
+
};
|
|
50
|
+
const file = args.input.toString();
|
|
51
|
+
let config = getConfig(options.config?.toString(), file, {
|
|
52
|
+
logger
|
|
53
|
+
});
|
|
54
|
+
if (options.usecase) {
|
|
55
|
+
logger.info("Using usecase: " + options.usecase);
|
|
56
|
+
config = { ...createConfig(options.usecase.toString()), ...config, };
|
|
57
|
+
if (config.usecase != "default" && config.usecase != options.usecase.toString()) {
|
|
58
|
+
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.`);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
config.usecase = options.usecase.toString();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const output = options.output?.toString() || file;
|
|
65
|
+
const outputIsDirectory = statSync(output).isDirectory();
|
|
66
|
+
const outDir = outputIsDirectory ? output : dirname(output);
|
|
67
|
+
const file_stats = [];
|
|
68
|
+
const set = new Set();
|
|
69
|
+
const useCache = options.cache !== false;
|
|
70
|
+
const debug = options.debug === true;
|
|
71
|
+
await foreachGLTF(file, async (file) => {
|
|
72
|
+
logger.info("→ Transform " + file);
|
|
73
|
+
const progressive_results = new Array();
|
|
74
|
+
if (options.progressive === true) {
|
|
75
|
+
await make_progressive({
|
|
76
|
+
path: file,
|
|
77
|
+
results: progressive_results,
|
|
78
|
+
config,
|
|
79
|
+
debug,
|
|
80
|
+
useCache,
|
|
81
|
+
}).catch(err => {
|
|
82
|
+
logger.error(err);
|
|
83
|
+
process.exit(ERROR_CODES.PROGRESSIVE_FAILED);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const opts = {
|
|
87
|
+
config,
|
|
88
|
+
stats,
|
|
89
|
+
packedFiles: set,
|
|
90
|
+
debug,
|
|
91
|
+
useCache,
|
|
92
|
+
logger: logger,
|
|
93
|
+
file_stats,
|
|
94
|
+
};
|
|
95
|
+
for (const res of progressive_results) {
|
|
96
|
+
if (await packGLTF(res, res, opts) === false) {
|
|
97
|
+
process.exit(ERROR_CODES.PACKING_FAILED);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (await packGLTF(file, file, opts) === false) {
|
|
101
|
+
process.exit(ERROR_CODES.PACKING_FAILED);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
printStats(stats, logger, config);
|
|
105
|
+
if (options.stats) {
|
|
106
|
+
const statsFile = (outputIsDirectory ? (path.join(file, getDirName(file))) : file) + ".needle.stats.json";
|
|
107
|
+
writeFileStatsToFile(config, file_stats, statsFile);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
program.run().catch(err => {
|
|
111
|
+
// Since we still invoke this package with paths when running npm scripts we need to ignore this error
|
|
112
|
+
// We might want to move this code here into an extra package.
|
|
113
|
+
if (err.message.includes("Unknown command") && !err.message.includes("Did you mean")) {
|
|
114
|
+
// ignore
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
throw err;
|
|
118
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ILogger } from "@gltf-transform/core";
|
|
1
2
|
declare type Usecase = "default" | "product" | "world";
|
|
2
3
|
export declare const UsecaseOptions: string[];
|
|
3
4
|
/**
|
|
@@ -9,6 +10,9 @@ export type Config = {
|
|
|
9
10
|
textures?: {
|
|
10
11
|
lods: boolean;
|
|
11
12
|
};
|
|
13
|
+
meshes?: {
|
|
14
|
+
lods: boolean;
|
|
15
|
+
};
|
|
12
16
|
};
|
|
13
17
|
/**
|
|
14
18
|
* Create a config object with the given usecase.
|
|
@@ -42,6 +46,8 @@ export declare function createConfig(usecase: string): {
|
|
|
42
46
|
*
|
|
43
47
|
*
|
|
44
48
|
*/
|
|
45
|
-
export declare function getConfig(configPath?: string, fallbackSearchDirectory?: string
|
|
46
|
-
|
|
49
|
+
export declare function getConfig(configPath?: string, fallbackSearchDirectory?: string, opts?: {
|
|
50
|
+
logger: ILogger;
|
|
51
|
+
}): Config;
|
|
52
|
+
export declare function searchConfig(directory: string, logger: ILogger): Config | null;
|
|
47
53
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { Logger, Verbosity } from "@gltf-transform/core";
|
|
1
2
|
import { existsSync, readFileSync, statSync } from "fs";
|
|
2
|
-
import path, { dirname } from "path";
|
|
3
|
+
import path, { dirname, resolve } from "path";
|
|
3
4
|
export const UsecaseOptions = ["product", "world"];
|
|
4
5
|
const defaultConfig = {
|
|
5
6
|
usecase: "default",
|
|
@@ -41,12 +42,27 @@ export function createConfig(usecase) {
|
|
|
41
42
|
*
|
|
42
43
|
*
|
|
43
44
|
*/
|
|
44
|
-
export function getConfig(configPath, fallbackSearchDirectory) {
|
|
45
|
+
export function getConfig(configPath, fallbackSearchDirectory, opts) {
|
|
46
|
+
let logger = opts?.logger;
|
|
47
|
+
if (!logger) {
|
|
48
|
+
logger = new Logger(Verbosity.WARN);
|
|
49
|
+
}
|
|
45
50
|
if (configPath != null) {
|
|
51
|
+
configPath = resolve(process.cwd(), configPath);
|
|
46
52
|
if (!existsSync(configPath)) {
|
|
47
53
|
throw new Error(`No config found at \"${configPath}\"`);
|
|
48
54
|
}
|
|
49
|
-
const
|
|
55
|
+
const isDirectory = statSync(configPath).isDirectory();
|
|
56
|
+
if (isDirectory) {
|
|
57
|
+
const res = searchConfig(configPath, logger);
|
|
58
|
+
if (res) {
|
|
59
|
+
return res;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
throw new Error(`No config found in directory \"${configPath}\"`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const res = resolveConfig(configPath, readFileSync(configPath, "utf8"), logger);
|
|
50
66
|
if (res) {
|
|
51
67
|
return res;
|
|
52
68
|
}
|
|
@@ -60,21 +76,21 @@ export function getConfig(configPath, fallbackSearchDirectory) {
|
|
|
60
76
|
// Resolve the config once and cache it
|
|
61
77
|
// Check if a needle.config.json exists in the directory from which the script is run (or any parent directory)
|
|
62
78
|
if (fallbackSearchDirectory) {
|
|
63
|
-
const result = searchConfig(fallbackSearchDirectory);
|
|
79
|
+
const result = searchConfig(fallbackSearchDirectory, logger);
|
|
64
80
|
if (result) {
|
|
65
81
|
_cachedConfig = { ...defaultConfig, ...result };
|
|
66
|
-
|
|
82
|
+
logger.info("Using config with usecase: " + _cachedConfig.usecase);
|
|
67
83
|
}
|
|
68
84
|
}
|
|
69
85
|
// If no config was found, use the default config
|
|
70
86
|
if (!_cachedConfig) {
|
|
71
|
-
|
|
87
|
+
logger.info("No config found. Using default config.");
|
|
72
88
|
_cachedConfig = defaultConfig;
|
|
73
89
|
}
|
|
74
90
|
return _cachedConfig;
|
|
75
91
|
}
|
|
76
92
|
let _cachedConfig = undefined;
|
|
77
|
-
export function searchConfig(directory) {
|
|
93
|
+
export function searchConfig(directory, logger) {
|
|
78
94
|
if (statSync(directory).isFile()) {
|
|
79
95
|
directory = dirname(directory);
|
|
80
96
|
}
|
|
@@ -83,7 +99,7 @@ export function searchConfig(directory) {
|
|
|
83
99
|
const configPath = path.join(directory, "needle.config.json");
|
|
84
100
|
if (existsSync(configPath)) {
|
|
85
101
|
const content = readFileSync(configPath, "utf8");
|
|
86
|
-
const config = resolveConfig(configPath, content);
|
|
102
|
+
const config = resolveConfig(configPath, content, logger);
|
|
87
103
|
if (config) {
|
|
88
104
|
return config;
|
|
89
105
|
}
|
|
@@ -96,13 +112,13 @@ export function searchConfig(directory) {
|
|
|
96
112
|
}
|
|
97
113
|
return null;
|
|
98
114
|
}
|
|
99
|
-
function resolveConfig(filename, content) {
|
|
115
|
+
function resolveConfig(filename, content, logger) {
|
|
100
116
|
// Handle config being part of `needle.config.json`
|
|
101
117
|
if (filename.endsWith("needle.config.json")) {
|
|
102
118
|
try {
|
|
103
119
|
const config = JSON.parse(content);
|
|
104
120
|
if (config.gltf) {
|
|
105
|
-
|
|
121
|
+
logger.info("Found \"gltf\" config at \"" + filename + "\".");
|
|
106
122
|
// If the optimization config is a string, it's the usecase
|
|
107
123
|
if (typeof config.gltf === "string") {
|
|
108
124
|
return createConfig(config.gltf);
|
|
@@ -110,12 +126,12 @@ function resolveConfig(filename, content) {
|
|
|
110
126
|
return config.gltf;
|
|
111
127
|
}
|
|
112
128
|
else {
|
|
113
|
-
|
|
129
|
+
logger.info("No \"gltf\" config found in needle.config.json. Using default config.");
|
|
114
130
|
return null;
|
|
115
131
|
}
|
|
116
132
|
}
|
|
117
133
|
catch (err) {
|
|
118
|
-
|
|
134
|
+
logger.error(`Failed parsing config \"${err.message}\" at ${filename}`);
|
|
119
135
|
}
|
|
120
136
|
}
|
|
121
137
|
// Handle config being the content of the file
|
|
@@ -125,12 +141,12 @@ function resolveConfig(filename, content) {
|
|
|
125
141
|
}
|
|
126
142
|
catch (err) {
|
|
127
143
|
if (err instanceof Error)
|
|
128
|
-
|
|
144
|
+
logger.error(`Failed parsing config \"${err.message}\" at ${filename}`);
|
|
129
145
|
else
|
|
130
|
-
|
|
146
|
+
logger.error(`Failed parsing config \"${err}\" at ${filename}`);
|
|
131
147
|
}
|
|
132
148
|
}
|
|
133
149
|
else {
|
|
134
|
-
|
|
150
|
+
logger.error("Invalid config file format. Expected JSON object at " + filename);
|
|
135
151
|
}
|
|
136
152
|
}
|
|
@@ -37,7 +37,7 @@ async function applyMeshCompression(document, transform_context, context) {
|
|
|
37
37
|
// return transform(document, transform_context);
|
|
38
38
|
// }
|
|
39
39
|
// by default we still want to run draco if the root doesnt have a compression model configured
|
|
40
|
-
|
|
40
|
+
document.getLogger().info(`[${NAME}]: → will apply draco mesh compression (default)`);
|
|
41
41
|
return draco()(document, transform_context);
|
|
42
42
|
}
|
|
43
43
|
async function simplifierTransform(document, _context, _options) {
|
|
@@ -13,7 +13,6 @@ import { MeshoptDecoder, MeshoptSimplifier } from 'meshoptimizer';
|
|
|
13
13
|
import { compressPrimitive } from './needle_mesh_transform.js';
|
|
14
14
|
import { calculateMeshDensity, getMeshInformation } from "../utils/mesh.js";
|
|
15
15
|
export async function make_progressive(opts) {
|
|
16
|
-
console.log(`INFO: Run progressive transform: ${opts.path}`);
|
|
17
16
|
const file = opts.path;
|
|
18
17
|
return foreachGLTF(file, async (f) => {
|
|
19
18
|
// Don't re-process files that are already LODs
|
|
@@ -64,15 +63,17 @@ async function onProcess(file, opts) {
|
|
|
64
63
|
}
|
|
65
64
|
const transforms = [
|
|
66
65
|
needle_asset(),
|
|
67
|
-
make_progressive_textures(file, { size: [smallesTextureSize, smallesTextureSize] }, opts),
|
|
68
|
-
sparse(),
|
|
69
66
|
];
|
|
67
|
+
if (opts.config?.textures?.lods !== false) {
|
|
68
|
+
transforms.push(make_progressive_textures(file, { size: [smallesTextureSize, smallesTextureSize] }, opts));
|
|
69
|
+
}
|
|
70
70
|
// TODO: progressive meshes aren't supported yet for VRMs
|
|
71
|
-
if (!file.endsWith(".vrm")) {
|
|
71
|
+
if (!file.endsWith(".vrm") && opts.config?.meshes?.lods !== false) {
|
|
72
72
|
transforms.push(make_progressive_meshes(file, {}, opts));
|
|
73
73
|
}
|
|
74
74
|
else
|
|
75
75
|
logger.warn("WARN: Progressive meshes are not supported for VRM files yet");
|
|
76
|
+
transforms.push(sparse());
|
|
76
77
|
logger.info("→ Transforming: " + file);
|
|
77
78
|
await document.transform(...transforms);
|
|
78
79
|
let filename = file;
|
|
@@ -174,11 +175,11 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
|
|
|
174
175
|
const currentSize = Math.min(...texture.getSize());
|
|
175
176
|
const LOD_LEVEL = new Array();
|
|
176
177
|
LOD_LEVEL.push({ size: currentSize });
|
|
177
|
-
let generateLods = opts.config?.usecase === "world";
|
|
178
|
+
let generateLods = true; //opts.config?.usecase === "world";
|
|
178
179
|
// 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;
|
|
180
|
+
// generateLods ||= opts.config?.textures?.lods === true;
|
|
181
|
+
// // Ensure that texture lods are not disabled by { textures: { lods: false } }
|
|
182
|
+
// generateLods &&= opts.config?.textures?.lods !== false;
|
|
182
183
|
if (generateLods) {
|
|
183
184
|
const LEVEL = 6;
|
|
184
185
|
let lastSize = currentSize;
|
|
@@ -619,7 +620,7 @@ export function make_progressive_meshes(filePath, _options, opts) {
|
|
|
619
620
|
const lodLevelInFile = generatedLODInfos.length;
|
|
620
621
|
const clonedData = { primitives: [], weights: [...mesh.getWeights()] };
|
|
621
622
|
for (const prim of mesh.listPrimitives()) {
|
|
622
|
-
const data = clonePrimitiveData(prim);
|
|
623
|
+
const data = clonePrimitiveData(prim, logger);
|
|
623
624
|
if (data != null) {
|
|
624
625
|
clonedData.primitives.push(data);
|
|
625
626
|
}
|
|
@@ -689,7 +690,7 @@ export function make_progressive_meshes(filePath, _options, opts) {
|
|
|
689
690
|
}
|
|
690
691
|
});
|
|
691
692
|
}
|
|
692
|
-
function clonePrimitiveData(prim) {
|
|
693
|
+
function clonePrimitiveData(prim, logger) {
|
|
693
694
|
const semantics = prim.listSemantics();
|
|
694
695
|
if (semantics.some(s => s.includes("JOINTS"))) {
|
|
695
696
|
return null;
|
|
@@ -697,7 +698,7 @@ function clonePrimitiveData(prim) {
|
|
|
697
698
|
const indices = new Uint32Array(prim.getIndices().getArray());
|
|
698
699
|
const attributes = {};
|
|
699
700
|
for (const attr of semantics) {
|
|
700
|
-
|
|
701
|
+
logger.info(`[${NAME}] Clone attribute ${attr}`);
|
|
701
702
|
const accessor = prim.getAttribute(attr);
|
|
702
703
|
if (accessor) {
|
|
703
704
|
attributes[attr] = new Float32Array(accessor.getArray());
|
|
@@ -725,6 +726,7 @@ function setPrimitiveData(prim, data) {
|
|
|
725
726
|
* @param error the error to use for simplification. Higher means more simplification
|
|
726
727
|
*/
|
|
727
728
|
async function createMeshLod(doc, basePath, level, mesh, meshInfo, guid, lodLevel, opts) {
|
|
729
|
+
const logger = doc.getLogger();
|
|
728
730
|
const fileExtension = ".glb";
|
|
729
731
|
const fileName_withoutExtension = "mesh_lod_" + level + "_" + guid;
|
|
730
732
|
const fileName = fileName_withoutExtension + fileExtension;
|
|
@@ -744,7 +746,7 @@ async function createMeshLod(doc, basePath, level, mesh, meshInfo, guid, lodLeve
|
|
|
744
746
|
const cachedValue = tryGetFromCache(cacheKey);
|
|
745
747
|
const meta = tryGetMetaFromCache(cacheKey);
|
|
746
748
|
if (cachedValue && meta) {
|
|
747
|
-
|
|
749
|
+
logger.info(`[${NAME}] LOD ${level} loaded ${(cachedValue.length / 1024).toFixed()} KB from cache for \"${mesh.getName()}\"`);
|
|
748
750
|
writeFileSync(outPath, cachedValue);
|
|
749
751
|
return { level, path: fileName, success: true, cacheKey: cacheKey, content_hash: hash, lodMeshInfo: meta.meshInfo, density: meta.density };
|
|
750
752
|
}
|
|
@@ -783,7 +785,7 @@ async function createMeshLod(doc, basePath, level, mesh, meshInfo, guid, lodLeve
|
|
|
783
785
|
newPrim.setAttribute(semantics[i], newAttr);
|
|
784
786
|
}
|
|
785
787
|
else {
|
|
786
|
-
|
|
788
|
+
logger.warn(`[${NAME}] Missing attribute ${semantics[i]} in mesh LOD ${level}`);
|
|
787
789
|
}
|
|
788
790
|
}
|
|
789
791
|
const indices = prim.getIndices();
|
|
@@ -818,7 +820,7 @@ async function createMeshLod(doc, basePath, level, mesh, meshInfo, guid, lodLeve
|
|
|
818
820
|
newPrim.setExtension(name, copy);
|
|
819
821
|
}
|
|
820
822
|
catch (err) {
|
|
821
|
-
|
|
823
|
+
logger.error(`[${NAME}] Failed to copy extension ${ext.extensionName} for mesh LOD ${level} → ${err}`);
|
|
822
824
|
}
|
|
823
825
|
}
|
|
824
826
|
}
|
|
@@ -836,7 +838,7 @@ async function createMeshLod(doc, basePath, level, mesh, meshInfo, guid, lodLeve
|
|
|
836
838
|
tasks.length = 0;
|
|
837
839
|
const newMeshInformation = getMeshInformation(newMesh);
|
|
838
840
|
if (newMeshInformation.totalVertexCount < 4) {
|
|
839
|
-
|
|
841
|
+
logger.error(`[${NAME}] mesh LOD ${level} is has less than 4 vertices → skipping`);
|
|
840
842
|
return { level, path: fileName, success: false, cacheKey: cacheKey, content_hash: hash, lodMeshInfo: null, density: null, };
|
|
841
843
|
}
|
|
842
844
|
let density = 0;
|
|
@@ -846,7 +848,7 @@ async function createMeshLod(doc, basePath, level, mesh, meshInfo, guid, lodLeve
|
|
|
846
848
|
}
|
|
847
849
|
const progressiveExt = newDoc.createExtension(NEEDLE_progressive);
|
|
848
850
|
if (!progressiveExt || !progressiveExt.createMesh) {
|
|
849
|
-
|
|
851
|
+
logger.error("Failed to create progressive extension");
|
|
850
852
|
return;
|
|
851
853
|
}
|
|
852
854
|
newMesh.setExtension(NEEDLE_PROGRESSIVE_EXTENSION_NAME, progressiveExt.createMesh({
|
|
@@ -859,13 +861,13 @@ async function createMeshLod(doc, basePath, level, mesh, meshInfo, guid, lodLeve
|
|
|
859
861
|
if (compressionExt) {
|
|
860
862
|
const newExt = newDoc.createExtension(NEEDLE_mesh_compression);
|
|
861
863
|
const newProp = newExt.create(compressionExt.model);
|
|
862
|
-
|
|
864
|
+
logger.info(`[${NAME}] Pass compression format to mesh LOD ${level} → ${JSON.stringify(compressionExt.model)}`);
|
|
863
865
|
newDoc.getRoot().setExtension(NEEDLE_mesh_compression.EXTENSION_NAME, newProp);
|
|
864
866
|
}
|
|
865
867
|
await newDoc.transform(sparse());
|
|
866
868
|
const newIo = new NodeIO();
|
|
867
869
|
registerExtensions(newIo);
|
|
868
|
-
|
|
870
|
+
logger.info(`[${NAME}] Write mesh LOD ${level} with to \"${fileName}\"`);
|
|
869
871
|
await newIo.write(outPath, newDoc);
|
|
870
872
|
// const hasChanged = meshInfo.maxVertexCount == newMeshInformation.maxVertexCount && meshInfo.totalVertexCount == newMeshInformation.totalVertexCount;
|
|
871
873
|
// add to cache
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { BufferUtils, FileUtils, ImageUtils, uuid } from "@gltf-transform/core";
|
|
2
2
|
import { listTextureSlots, getTextureChannelMask, fitPowerOfTwo, TextureResizeFilter } from "@gltf-transform/functions";
|
|
3
|
-
import { ETC1S_DEFAULTS, UASTC_DEFAULTS } from "@gltf-transform/cli";
|
|
4
3
|
import { KHRTextureBasisu } from "@gltf-transform/extensions";
|
|
5
4
|
import { checkKTXSoftware, createParams, Mode } from "./toktx.js";
|
|
6
5
|
import { existsSync, mkdirSync } from "fs";
|
|
7
6
|
import { formatBytes, spawn, waitExit } from './util.js';
|
|
7
|
+
import { ETC1S_DEFAULTS, UASTC_DEFAULTS } from "./toktx_types.js";
|
|
8
8
|
import fs from 'fs/promises';
|
|
9
9
|
import os from 'os';
|
|
10
10
|
import tmp from 'tmp';
|
|
@@ -1,17 +1,13 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
1
2
|
import micromatch from 'micromatch';
|
|
2
3
|
import os from 'os';
|
|
3
4
|
import semver from 'semver';
|
|
4
5
|
import tmp from 'tmp';
|
|
5
|
-
import { ETC1S_DEFAULTS, UASTC_DEFAULTS } from "@gltf-transform/cli";
|
|
6
6
|
import { TextureChannel } from '@gltf-transform/core';
|
|
7
|
-
// import { TextureBasisu } from '@gltf-transform/extensions';
|
|
8
|
-
// import { getTextureChannelMask, listTextureSlots } from '@gltf-transform/functions';
|
|
9
7
|
import { execSync } from 'child_process';
|
|
10
8
|
import { spawn, commandExists, waitExit, MICROMATCH_OPTIONS } from './util.js';
|
|
11
9
|
import { TextureResizeFilter, getTextureColorSpace } from '@gltf-transform/functions';
|
|
12
|
-
|
|
13
|
-
// import { tryGetFromCache, getKey, addToCache } from '../cache';
|
|
14
|
-
// import { resizeTextureIfNecessary } from "./needle_texture_transform"
|
|
10
|
+
import { ETC1S_DEFAULTS, UASTC_DEFAULTS } from './toktx_types.js';
|
|
15
11
|
tmp.setGracefulCleanup();
|
|
16
12
|
const NUM_CPUS = os.cpus().length || 1; // microsoft/vscode#112122
|
|
17
13
|
const KTX_SOFTWARE_VERSION_MIN = '4.0.0-rc1';
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { vec2 } from "@gltf-transform/core";
|
|
2
|
+
import { TextureResizeFilter } from "@gltf-transform/functions";
|
|
3
|
+
/**********************************************************************************************
|
|
4
|
+
* Interfaces.
|
|
5
|
+
*/
|
|
6
|
+
export declare const Mode: {
|
|
7
|
+
ETC1S: string;
|
|
8
|
+
UASTC: string;
|
|
9
|
+
};
|
|
10
|
+
export declare const Filter: {
|
|
11
|
+
BOX: string;
|
|
12
|
+
TENT: string;
|
|
13
|
+
BELL: string;
|
|
14
|
+
BSPLINE: string;
|
|
15
|
+
MITCHELL: string;
|
|
16
|
+
LANCZOS3: string;
|
|
17
|
+
LANCZOS4: string;
|
|
18
|
+
LANCZOS6: string;
|
|
19
|
+
LANCZOS12: string;
|
|
20
|
+
BLACKMAN: string;
|
|
21
|
+
KAISER: string;
|
|
22
|
+
GAUSSIAN: string;
|
|
23
|
+
CATMULLROM: string;
|
|
24
|
+
QUADRATIC_INTERP: string;
|
|
25
|
+
QUADRATIC_APPROX: string;
|
|
26
|
+
QUADRATIC_MIX: string;
|
|
27
|
+
};
|
|
28
|
+
interface GlobalOptions {
|
|
29
|
+
/** Instance of the Sharp encoder, required if resizing textures. */
|
|
30
|
+
encoder: unknown;
|
|
31
|
+
mode: string;
|
|
32
|
+
/** Pattern identifying textures to compress, matched to name or URI. */
|
|
33
|
+
pattern?: RegExp | null;
|
|
34
|
+
/**
|
|
35
|
+
* Pattern matching the material texture slot(s) to be compressed or converted.
|
|
36
|
+
* Passing a string (glob) is deprecated; use a RegExp instead.
|
|
37
|
+
*/
|
|
38
|
+
slots?: RegExp | null;
|
|
39
|
+
/** Interpolation used for generating mipmaps. Default: 'lanczos4'. */
|
|
40
|
+
filter?: string;
|
|
41
|
+
filterScale?: number;
|
|
42
|
+
resize?: vec2 | 'nearest-pot' | 'ceil-pot' | 'floor-pot';
|
|
43
|
+
/** Interpolation used if resizing. Default: TextureResizeFilter.LANCZOS3. */
|
|
44
|
+
resizeFilter?: TextureResizeFilter;
|
|
45
|
+
jobs?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Whether to clean up temporary files created during texture compression. See
|
|
48
|
+
* verbose log output for temporary file paths. Default: true.
|
|
49
|
+
*/
|
|
50
|
+
cleanup?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Attempts to avoid processing images that could exceed memory or other other
|
|
53
|
+
* limits, throwing an error instead. Default: true.
|
|
54
|
+
* @experimental
|
|
55
|
+
*/
|
|
56
|
+
limitInputPixels?: boolean;
|
|
57
|
+
}
|
|
58
|
+
export interface ETC1SOptions extends GlobalOptions {
|
|
59
|
+
quality?: number;
|
|
60
|
+
compression?: number;
|
|
61
|
+
maxEndpoints?: number;
|
|
62
|
+
maxSelectors?: number;
|
|
63
|
+
rdo?: boolean;
|
|
64
|
+
rdoThreshold?: number;
|
|
65
|
+
}
|
|
66
|
+
export interface UASTCOptions extends GlobalOptions {
|
|
67
|
+
level?: number;
|
|
68
|
+
rdo?: boolean;
|
|
69
|
+
rdoLambda?: number;
|
|
70
|
+
rdoDictionarySize?: number;
|
|
71
|
+
rdoBlockScale?: number;
|
|
72
|
+
rdoStdDev?: number;
|
|
73
|
+
rdoMultithreading?: boolean;
|
|
74
|
+
zstd?: number;
|
|
75
|
+
}
|
|
76
|
+
export declare const ETC1S_DEFAULTS: Omit<ETC1SOptions, 'encoder' | 'mode'>;
|
|
77
|
+
export declare const UASTC_DEFAULTS: Omit<UASTCOptions, 'encoder' | 'mode'>;
|
|
78
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import { TextureChannel } from "@gltf-transform/core";
|
|
3
|
+
import { TextureResizeFilter } from "@gltf-transform/functions";
|
|
4
|
+
const NUM_CPUS = os.cpus().length || 1; // microsoft/vscode#112122
|
|
5
|
+
const KTX_SOFTWARE_VERSION_MIN = '4.3.0';
|
|
6
|
+
const { R, G, A } = TextureChannel;
|
|
7
|
+
/**********************************************************************************************
|
|
8
|
+
* Interfaces.
|
|
9
|
+
*/
|
|
10
|
+
export const Mode = {
|
|
11
|
+
ETC1S: 'etc1s',
|
|
12
|
+
UASTC: 'uastc',
|
|
13
|
+
};
|
|
14
|
+
export const Filter = {
|
|
15
|
+
BOX: 'box',
|
|
16
|
+
TENT: 'tent',
|
|
17
|
+
BELL: 'bell',
|
|
18
|
+
BSPLINE: 'b-spline',
|
|
19
|
+
MITCHELL: 'mitchell',
|
|
20
|
+
LANCZOS3: 'lanczos3',
|
|
21
|
+
LANCZOS4: 'lanczos4',
|
|
22
|
+
LANCZOS6: 'lanczos6',
|
|
23
|
+
LANCZOS12: 'lanczos12',
|
|
24
|
+
BLACKMAN: 'blackman',
|
|
25
|
+
KAISER: 'kaiser',
|
|
26
|
+
GAUSSIAN: 'gaussian',
|
|
27
|
+
CATMULLROM: 'catmullrom',
|
|
28
|
+
QUADRATIC_INTERP: 'quadratic_interp',
|
|
29
|
+
QUADRATIC_APPROX: 'quadratic_approx',
|
|
30
|
+
QUADRATIC_MIX: 'quadratic_mix',
|
|
31
|
+
};
|
|
32
|
+
const GLOBAL_DEFAULTS = {
|
|
33
|
+
resizeFilter: TextureResizeFilter.LANCZOS3,
|
|
34
|
+
filter: Filter.LANCZOS4,
|
|
35
|
+
filterScale: 1,
|
|
36
|
+
pattern: null,
|
|
37
|
+
slots: null,
|
|
38
|
+
// See: https://github.com/donmccurdy/glTF-Transform/pull/389#issuecomment-1089842185
|
|
39
|
+
jobs: 2 * NUM_CPUS,
|
|
40
|
+
cleanup: true,
|
|
41
|
+
limitInputPixels: true,
|
|
42
|
+
};
|
|
43
|
+
export const ETC1S_DEFAULTS = {
|
|
44
|
+
quality: 128,
|
|
45
|
+
compression: 1,
|
|
46
|
+
rdo: true,
|
|
47
|
+
rdoThreshold: 1.25,
|
|
48
|
+
maxSelectors: 0,
|
|
49
|
+
maxEndpoints: 0,
|
|
50
|
+
...GLOBAL_DEFAULTS,
|
|
51
|
+
};
|
|
52
|
+
export const UASTC_DEFAULTS = {
|
|
53
|
+
level: 2,
|
|
54
|
+
rdo: false,
|
|
55
|
+
rdoLambda: 1.0,
|
|
56
|
+
rdoDictionarySize: 32768,
|
|
57
|
+
rdoBlockScale: 10.0,
|
|
58
|
+
rdoStdDev: 18.0,
|
|
59
|
+
rdoMultithreading: true,
|
|
60
|
+
zstd: 18,
|
|
61
|
+
...GLOBAL_DEFAULTS,
|
|
62
|
+
};
|