@needle-tools/gltf-build-pipeline 2.2.0-alpha.9 → 2.2.1-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 +12 -1
- package/dist/cli/index.js +31 -4
- package/dist/extensions/NEEDLE_mesh_compression/NEEDLE_mesh_compression.js +1 -1
- package/dist/scripts/index.d.ts +1 -0
- package/dist/scripts/index.js +1 -0
- package/dist/scripts/pack-gltf.d.ts +4 -3
- package/dist/scripts/pack-gltf.js +2 -4
- package/dist/scripts/stats.d.ts +2 -0
- package/dist/scripts/stats.js +18 -0
- package/dist/transforms/needle_progressive.d.ts +1 -0
- package/dist/transforms/needle_progressive.js +8 -5
- package/dist/transforms/needle_texture_transform.js +3 -1
- package/dist/transforms/needle_toktx.js +6 -6
- package/dist/transforms/needle_webp.js +1 -1
- package/dist/utils/fileutils.d.ts +1 -0
- package/dist/utils/fileutils.js +7 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/merge.d.ts +12 -0
- package/dist/utils/merge.js +31 -0
- package/dist/utils/stats.d.ts +37 -13
- package/dist/utils/stats.js +144 -23
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,11 +4,22 @@ 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.
|
|
7
|
+
## [2.2.1-alpha.1] - 2023-06-17
|
|
8
|
+
- add: `--verbose` option to transform command
|
|
9
|
+
- add: `stats` command to create stats.needle.json
|
|
10
|
+
|
|
11
|
+
## [2.2.1-alpha] - 2023-06-15
|
|
12
|
+
- fix: issue with progressive mesh cache key calculation when running multiple transform processes with different configurations
|
|
13
|
+
|
|
14
|
+
## [2.2.0-alpha.11] - 2023-06-14
|
|
15
|
+
- fix: issue where progressive transform with un-indexed meshes would fail
|
|
16
|
+
|
|
17
|
+
## [2.2.0-alpha.10] - 2023-06-13
|
|
8
18
|
- fix: progressive transform of converted GLB files where meshes don't have indicies
|
|
9
19
|
- fix: skip progressive texture generation for textures for textures that already match the lowest LOD size
|
|
10
20
|
- fix: if compression is set to None the LOD textures should respect that setting and also not be compressed
|
|
11
21
|
- fix: fallback mesh density calculation for meshes that aren't indexed
|
|
22
|
+
- fix: NEEDLE_mesh_compression extension falsely overriding primitive extensions
|
|
12
23
|
|
|
13
24
|
## [2.2.0-alpha.5] - 2023-06-06
|
|
14
25
|
- add: CLI `--progressive` option to `transform` command (default is true)
|
package/dist/cli/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
2
|
import { program } from '@donmccurdy/caporal';
|
|
3
|
-
import { packGLTF } from "../scripts/index.js";
|
|
3
|
+
import { calculateStats, packGLTF } from "../scripts/index.js";
|
|
4
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
8
|
import { cacheSizeLimit, clearCache } from '../cache/cache.js';
|
|
9
9
|
import { statSync } from 'fs';
|
|
10
|
-
import { foreachGLTF, getDirName } from '../utils/fileutils.js';
|
|
10
|
+
import { foreachGLTF, getDirName, isDirectory } from '../utils/fileutils.js';
|
|
11
11
|
import path, { dirname } from 'path';
|
|
12
12
|
import { ERROR_CODES } from '../constants.js';
|
|
13
13
|
// For testing / dev you can run `npm link` in the package directory
|
|
@@ -24,6 +24,32 @@ Caches are limited to ${cacheSizeLimit} MB disc space by default.
|
|
|
24
24
|
.action(async ({ logger }) => {
|
|
25
25
|
logger.info("Clearing caches");
|
|
26
26
|
clearCache(logger);
|
|
27
|
+
})
|
|
28
|
+
.command("stats", "Calculate glTF stats")
|
|
29
|
+
.help(`
|
|
30
|
+
`)
|
|
31
|
+
.argument('<input>', "Path to a glTF, GLB or VRM file OR a directory")
|
|
32
|
+
.argument('<output>', "Output path for the stats", { default: null, validator: program.STRING })
|
|
33
|
+
.action(async ({ logger, args, options }) => {
|
|
34
|
+
logger.level = "info";
|
|
35
|
+
logger.info("Calculating stats for \"" + args.input + "\"");
|
|
36
|
+
const input = args.input.toString();
|
|
37
|
+
const filestats = new Array();
|
|
38
|
+
await foreachGLTF(input, async (file) => {
|
|
39
|
+
const stats = await calculateStats(file);
|
|
40
|
+
filestats.push(stats);
|
|
41
|
+
});
|
|
42
|
+
let output = args.ourput?.toString();
|
|
43
|
+
if (!output) {
|
|
44
|
+
if (isDirectory(input)) {
|
|
45
|
+
output = path.join(input, "stats.needle.json");
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
output = input + ".needle.stats.json";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
logger.info("Writing stats to: " + output);
|
|
52
|
+
writeFileStatsToFile(filestats, output);
|
|
27
53
|
})
|
|
28
54
|
.command("transform", "Transform (progressive loading) and compress glTF, GLB or VRM files")
|
|
29
55
|
.help(`
|
|
@@ -39,6 +65,7 @@ Each version will be compressed and written to the output directory.
|
|
|
39
65
|
.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 })
|
|
40
66
|
.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 })
|
|
41
67
|
.option("--debug", "Enable for more verbose outout with \"--debug True\"", { validator: program.BOOLEAN, default: false })
|
|
68
|
+
.option("--verbose", "Enable for more verbose outout with \"--verbose True\"", { validator: program.BOOLEAN, default: false })
|
|
42
69
|
.action(async ({ logger, args, options }) => {
|
|
43
70
|
if (options.compress === false && options.progressive === false) {
|
|
44
71
|
logger.error("You need to enable at least one of the options: --compress or --progressive");
|
|
@@ -69,7 +96,7 @@ Each version will be compressed and written to the output directory.
|
|
|
69
96
|
const output = options.output?.toString() || file;
|
|
70
97
|
const outputIsDirectory = statSync(output).isDirectory();
|
|
71
98
|
const outDir = outputIsDirectory ? output : dirname(output);
|
|
72
|
-
const file_stats =
|
|
99
|
+
const file_stats = options.stats === true ? new Array : null;
|
|
73
100
|
const set = new Set();
|
|
74
101
|
const useCache = options.cache !== false;
|
|
75
102
|
const debug = options.debug === true;
|
|
@@ -113,7 +140,7 @@ Each version will be compressed and written to the output directory.
|
|
|
113
140
|
printStats(stats, logger, config);
|
|
114
141
|
if (options.stats) {
|
|
115
142
|
const statsFile = (outputIsDirectory ? (path.join(file, getDirName(file))) : file) + ".needle.stats.json";
|
|
116
|
-
writeFileStatsToFile(
|
|
143
|
+
writeFileStatsToFile(file_stats, statsFile);
|
|
117
144
|
}
|
|
118
145
|
});
|
|
119
146
|
program.run().catch(err => {
|
|
@@ -62,7 +62,7 @@ export class NEEDLE_mesh_compression extends Extension {
|
|
|
62
62
|
if (ext) {
|
|
63
63
|
const prim = meshDef.primitives[i];
|
|
64
64
|
if (prim) {
|
|
65
|
-
prim.extensions =
|
|
65
|
+
prim.extensions = prim.extensions || {};
|
|
66
66
|
prim.extensions[EXTENSION_NAME] = ext.model;
|
|
67
67
|
}
|
|
68
68
|
}
|
package/dist/scripts/index.d.ts
CHANGED
package/dist/scripts/index.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { ILogger } from '@gltf-transform/core';
|
|
2
|
-
import { FileStats,
|
|
2
|
+
import { FileStats, ProcessStats } from "../utils/index.js";
|
|
3
3
|
import { Config } from '../config/index.js';
|
|
4
4
|
export type PackGLTFOptions = {
|
|
5
|
-
debug: boolean;
|
|
6
5
|
useCache: boolean;
|
|
7
6
|
packedFiles?: Set<string>;
|
|
8
|
-
stats?:
|
|
7
|
+
stats?: ProcessStats;
|
|
9
8
|
file_stats: FileStats[] | null;
|
|
10
9
|
config: Config;
|
|
11
10
|
logger?: ILogger;
|
|
11
|
+
debug: boolean;
|
|
12
|
+
verbose?: boolean;
|
|
12
13
|
};
|
|
13
14
|
/** Compress a glTF file
|
|
14
15
|
*/
|
|
@@ -60,7 +60,7 @@ export async function packGLTF(inputFile, outputFile, options) {
|
|
|
60
60
|
});
|
|
61
61
|
const document = await io.read(inputFile);
|
|
62
62
|
/** @ts-ignore */
|
|
63
|
-
document.getLogger().verbosity = options.debug ? Verbosity.DEBUG : Verbosity.WARN;
|
|
63
|
+
document.getLogger().verbosity = options.debug || options.verbose ? Verbosity.DEBUG : Verbosity.WARN;
|
|
64
64
|
let totalGPUTextureMemory = 0;
|
|
65
65
|
/** @type {Array<import('@gltf-transform/core').Transform>} */
|
|
66
66
|
const transforms = [
|
|
@@ -107,9 +107,7 @@ export async function packGLTF(inputFile, outputFile, options) {
|
|
|
107
107
|
stats.totalFileSizeInMBBefore += prevSizeInMB;
|
|
108
108
|
stats.totalGPUMemoryInMB += gpuMemoryInMB;
|
|
109
109
|
}
|
|
110
|
-
const filestats = createNewFilestats(outputFile, document
|
|
111
|
-
gpu_memory: totalGPUTextureMemory,
|
|
112
|
-
});
|
|
110
|
+
const filestats = createNewFilestats(outputFile, document);
|
|
113
111
|
options.file_stats?.push(filestats);
|
|
114
112
|
return true;
|
|
115
113
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { NodeIO } from "@gltf-transform/core";
|
|
2
|
+
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
|
3
|
+
import draco3d from 'draco3dgltf';
|
|
4
|
+
import { MeshoptDecoder, MeshoptEncoder } from "meshoptimizer";
|
|
5
|
+
import { createNewFilestats } from "../utils/stats.js";
|
|
6
|
+
export async function calculateStats(inputfile) {
|
|
7
|
+
const io = new NodeIO()
|
|
8
|
+
.registerExtensions(ALL_EXTENSIONS)
|
|
9
|
+
.registerDependencies({
|
|
10
|
+
'draco3d.decoder': await draco3d.createDecoderModule(),
|
|
11
|
+
'draco3d.encoder': await draco3d.createEncoderModule(),
|
|
12
|
+
'meshopt.decoder': MeshoptDecoder,
|
|
13
|
+
'meshopt.encoder': MeshoptEncoder,
|
|
14
|
+
});
|
|
15
|
+
const document = await io.read(inputfile);
|
|
16
|
+
const stats = createNewFilestats(inputfile, document);
|
|
17
|
+
return stats;
|
|
18
|
+
}
|
|
@@ -65,7 +65,7 @@ async function onProcess(file, opts) {
|
|
|
65
65
|
if (document) {
|
|
66
66
|
const logger = document.getLogger();
|
|
67
67
|
//@ts-ignore
|
|
68
|
-
logger.verbosity = opts.debug ? Verbosity.DEBUG : Verbosity.WARN;
|
|
68
|
+
logger.verbosity = opts.debug || opts.verbose ? Verbosity.DEBUG : Verbosity.WARN;
|
|
69
69
|
let smallesTextureSize = 128;
|
|
70
70
|
if (opts.config.usecase === "product") {
|
|
71
71
|
smallesTextureSize = 512;
|
|
@@ -709,7 +709,7 @@ function clonePrimitiveData(prim, logger) {
|
|
|
709
709
|
return null;
|
|
710
710
|
}
|
|
711
711
|
const prevIndices = prim.getIndices();
|
|
712
|
-
const indices = !prevIndices ?
|
|
712
|
+
const indices = !prevIndices ? new Uint32Array() : new Uint32Array(prevIndices.getArray());
|
|
713
713
|
const attributes = {};
|
|
714
714
|
for (const attr of semantics) {
|
|
715
715
|
logger.info(`[${NAME}] Clone attribute ${attr}`);
|
|
@@ -754,15 +754,18 @@ async function createMeshLod(doc, basePath, level, mesh, meshInfo, guid, lodLeve
|
|
|
754
754
|
mesh["needle:mesh-hash"] = hash;
|
|
755
755
|
}
|
|
756
756
|
hash += getHash(lodLevel);
|
|
757
|
-
hash += getHash(
|
|
757
|
+
hash += getHash({
|
|
758
|
+
guid,
|
|
759
|
+
config: opts.config,
|
|
760
|
+
});
|
|
758
761
|
if (opts.useCache !== false) {
|
|
759
762
|
cacheKey = `mesh_lod_${level}_${hash}`;
|
|
760
763
|
const cachedValue = tryGetFromCache(cacheKey);
|
|
761
764
|
const meta = tryGetMetaFromCache(cacheKey);
|
|
762
765
|
if (cachedValue && meta) {
|
|
763
|
-
logger.info(`[${NAME}] LOD ${level} loaded
|
|
766
|
+
logger.info(`[${NAME}] LOD ${level} loaded from cache for \"${mesh.getName()}\"`);
|
|
764
767
|
writeFileSync(outPath, cachedValue);
|
|
765
|
-
return { level, path: fileName, success: true, cacheKey
|
|
768
|
+
return { level, path: fileName, success: true, cacheKey, content_hash: hash, lodMeshInfo: meta.meshInfo, density: meta.density };
|
|
766
769
|
}
|
|
767
770
|
}
|
|
768
771
|
const newDoc = new Document();
|
|
@@ -95,11 +95,13 @@ async function processTexture(texture, index, document, logger, context) {
|
|
|
95
95
|
const key = context.useCache ? (getKey(texture) + getHash(context.config)) : undefined;
|
|
96
96
|
let cached = context.useCache && key ? tryGetFromCache(key) : undefined;
|
|
97
97
|
if (cached) {
|
|
98
|
-
logger.debug(`> Using cached texture ${texture.getName()} [${index}] – ${(cached.length / 1024).toFixed(0)} KB`);
|
|
99
98
|
if (cached.length <= 0) {
|
|
100
99
|
logger.warn(`WARN: Cached texture ${texture.getName()} [${index}] is empty. Will reprocess.`);
|
|
101
100
|
cached = undefined;
|
|
102
101
|
}
|
|
102
|
+
else {
|
|
103
|
+
logger.info(`→ Using cached texture ${texture.getName()} [${index}] – ${(cached.length / 1024).toFixed(0)} KB`);
|
|
104
|
+
}
|
|
103
105
|
}
|
|
104
106
|
let successFullyProcessed = false;
|
|
105
107
|
const canRunToktx = context.noToktx !== true;
|
|
@@ -45,11 +45,6 @@ export class NeedleToktxContext {
|
|
|
45
45
|
this.env.PATH = `${process.env.NEEDLE_TOKTX};${this.env.PATH}`;
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
else {
|
|
49
|
-
if (didLogEnvironmentInfo === false)
|
|
50
|
-
console.log("INFO: Environment variable 'NEEDLE_TOKTX' not set. Use 'NEEDLE_TOKTX' to specify the directory of the toktx installation (path/to/bin)");
|
|
51
|
-
didLogEnvironmentInfo = true;
|
|
52
|
-
}
|
|
53
48
|
// fallback default installation directory:
|
|
54
49
|
if (isUsingWindows) {
|
|
55
50
|
const defaultPath = "C:\\Program Files\\KTX-Software\\bin";
|
|
@@ -59,10 +54,15 @@ export class NeedleToktxContext {
|
|
|
59
54
|
// append default system paths
|
|
60
55
|
if (isUsingWindows && process.env.Path?.length)
|
|
61
56
|
this.env.PATH = `${this.env.PATH};${process.env.Path}`;
|
|
57
|
+
const ktxPaths = this.env.PATH.split(";").filter(p => p.toLocaleLowerCase().includes("ktx"));
|
|
62
58
|
if (options.debug) {
|
|
63
|
-
const ktxPaths = this.env.PATH.split(";").filter(p => p.toLocaleLowerCase().includes("ktx"));
|
|
64
59
|
console.log("INFO: Found these paths including ktx:\n\t" + ktxPaths.join("\n\t") + "");
|
|
65
60
|
}
|
|
61
|
+
else if (ktxPaths?.length <= 0) {
|
|
62
|
+
if (didLogEnvironmentInfo === false)
|
|
63
|
+
console.log("INFO: Environment variable 'NEEDLE_TOKTX' not set. Use 'NEEDLE_TOKTX' to specify the directory of the toktx installation (path/to/bin)");
|
|
64
|
+
didLogEnvironmentInfo = true;
|
|
65
|
+
}
|
|
66
66
|
}
|
|
67
67
|
async prepare(document) {
|
|
68
68
|
try {
|
|
@@ -24,7 +24,7 @@ export class NeedleWebPContext {
|
|
|
24
24
|
options.quality = 1;
|
|
25
25
|
}
|
|
26
26
|
// console.log(options);
|
|
27
|
-
|
|
27
|
+
this.logger.info(`→ Will compress texture ${texture.getName()} to webp with quality ${options.quality}`);
|
|
28
28
|
const buffer = await sharp(texture.getImage()).toFormat('webp', options).toBuffer();
|
|
29
29
|
texture
|
|
30
30
|
.setMimeType('image/webp')
|
|
@@ -3,3 +3,4 @@ export declare function getDirName(filepath: string): string;
|
|
|
3
3
|
export declare type GLTFFileCallback = (file: string) => void | Promise<void>;
|
|
4
4
|
export declare function foreachGLTF(args: string | string[], callback: GLTFFileCallback): Promise<void>;
|
|
5
5
|
export declare function replaceExtension(filepath: string, newExtension: string): string;
|
|
6
|
+
export declare function isDirectory(path: string): boolean;
|
package/dist/utils/fileutils.js
CHANGED
|
@@ -132,3 +132,10 @@ export function replaceExtension(filepath, newExtension) {
|
|
|
132
132
|
parts.pop();
|
|
133
133
|
return parts.join(".") + "." + newExtension;
|
|
134
134
|
}
|
|
135
|
+
export function isDirectory(path) {
|
|
136
|
+
if (existsSync(path)) {
|
|
137
|
+
return statSync(path).isDirectory();
|
|
138
|
+
}
|
|
139
|
+
const hasExt = path.replaceAll("\\", "/").split("/").pop()?.includes(".");
|
|
140
|
+
return !hasExt;
|
|
141
|
+
}
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple object check.
|
|
3
|
+
* @param item
|
|
4
|
+
* @returns {boolean}
|
|
5
|
+
*/
|
|
6
|
+
export declare function isObject(item: object): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Deep merge two objects.
|
|
9
|
+
* @param target
|
|
10
|
+
* @param ...sources
|
|
11
|
+
*/
|
|
12
|
+
export declare function mergeDeep(target: any, ...sources: any[]): any;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple object check.
|
|
3
|
+
* @param item
|
|
4
|
+
* @returns {boolean}
|
|
5
|
+
*/
|
|
6
|
+
export function isObject(item) {
|
|
7
|
+
return (item && typeof item === 'object' && !Array.isArray(item));
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Deep merge two objects.
|
|
11
|
+
* @param target
|
|
12
|
+
* @param ...sources
|
|
13
|
+
*/
|
|
14
|
+
export function mergeDeep(target, ...sources) {
|
|
15
|
+
if (!sources.length)
|
|
16
|
+
return target;
|
|
17
|
+
const source = sources.shift();
|
|
18
|
+
if (isObject(target) && isObject(source)) {
|
|
19
|
+
for (const key in source) {
|
|
20
|
+
if (isObject(source[key])) {
|
|
21
|
+
if (!target[key])
|
|
22
|
+
Object.assign(target, { [key]: {} });
|
|
23
|
+
mergeDeep(target[key], source[key]);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
Object.assign(target, { [key]: source[key] });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return mergeDeep(target, ...sources);
|
|
31
|
+
}
|
package/dist/utils/stats.d.ts
CHANGED
|
@@ -1,29 +1,53 @@
|
|
|
1
1
|
import { Logger } from "@donmccurdy/caporal";
|
|
2
2
|
import { Document, Logger as _Logger } from "@gltf-transform/core";
|
|
3
3
|
import { Config } from "../config/index.js";
|
|
4
|
-
export type
|
|
4
|
+
export type ProcessStats = {
|
|
5
5
|
startTime: number;
|
|
6
6
|
totalGPUMemoryInMB: number;
|
|
7
7
|
totalFilesProcessed: number;
|
|
8
8
|
totalFileSizeInMB: number;
|
|
9
9
|
totalFileSizeInMBBefore: number;
|
|
10
10
|
};
|
|
11
|
-
export declare function printStats(stats:
|
|
12
|
-
|
|
13
|
-
filename: string;
|
|
11
|
+
export declare function printStats(stats: ProcessStats, logger: Logger | _Logger, config: Config): void;
|
|
12
|
+
declare class Stats {
|
|
14
13
|
/** Size in bytes */
|
|
15
14
|
size: number;
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
asset: {
|
|
16
|
+
version: string;
|
|
17
|
+
generator?: string;
|
|
18
|
+
copyright?: string;
|
|
19
|
+
extras: any;
|
|
20
|
+
};
|
|
18
21
|
nodes: number;
|
|
19
22
|
materials: number;
|
|
20
|
-
meshes:
|
|
21
|
-
textures:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
23
|
+
meshes: MeshInfo;
|
|
24
|
+
textures: TextureInfo;
|
|
25
|
+
constructor(document: Document, size: number);
|
|
26
|
+
static combine(others: Stats[]): Stats;
|
|
27
|
+
}
|
|
28
|
+
export declare class FileStats extends Stats {
|
|
29
|
+
readonly filename: string;
|
|
30
|
+
constructor(file: string, document: Document);
|
|
31
|
+
}
|
|
32
|
+
export declare class MeshInfo {
|
|
33
|
+
count: number;
|
|
34
|
+
primitives: number;
|
|
35
|
+
vertices: number;
|
|
36
|
+
triangles: number;
|
|
37
|
+
memory: number;
|
|
38
|
+
formats: string[];
|
|
39
|
+
addTo(other: MeshInfo): void;
|
|
40
|
+
}
|
|
41
|
+
export declare class TextureInfo {
|
|
42
|
+
count: number;
|
|
43
|
+
memory: number;
|
|
44
|
+
gpu_memory: number;
|
|
45
|
+
formats: string[];
|
|
46
|
+
addTo(other: TextureInfo): void;
|
|
47
|
+
}
|
|
25
48
|
/**
|
|
26
49
|
* Create a new filestats object from a file and a document
|
|
27
50
|
*/
|
|
28
|
-
export declare function createNewFilestats(file: string, document: Document
|
|
29
|
-
export declare function writeFileStatsToFile(
|
|
51
|
+
export declare function createNewFilestats(file: string, document: Document): FileStats;
|
|
52
|
+
export declare function writeFileStatsToFile(filestats: FileStats[], output_path: string): string | null;
|
|
53
|
+
export {};
|
package/dist/utils/stats.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getVersion } from "./version.js";
|
|
2
|
+
import { Document, ImageUtils } from "@gltf-transform/core";
|
|
2
3
|
import { getFileName } from "./fileutils.js";
|
|
3
4
|
import { statSync, writeFileSync } from "fs";
|
|
4
5
|
export function printStats(stats, logger, config) {
|
|
@@ -10,36 +11,156 @@ export function printStats(stats, logger, config) {
|
|
|
10
11
|
else
|
|
11
12
|
console.log(msg);
|
|
12
13
|
}
|
|
14
|
+
class Stats {
|
|
15
|
+
/** Size in bytes */
|
|
16
|
+
size;
|
|
17
|
+
asset;
|
|
18
|
+
nodes;
|
|
19
|
+
materials;
|
|
20
|
+
meshes;
|
|
21
|
+
textures;
|
|
22
|
+
constructor(document, size) {
|
|
23
|
+
const root = document.getRoot();
|
|
24
|
+
const asset = root.getAsset();
|
|
25
|
+
this.asset = {
|
|
26
|
+
generator: asset.generator,
|
|
27
|
+
version: asset.version,
|
|
28
|
+
copyright: asset.copyright,
|
|
29
|
+
extras: asset.extras,
|
|
30
|
+
};
|
|
31
|
+
this.size = size;
|
|
32
|
+
this.nodes = root.listNodes().length;
|
|
33
|
+
this.materials = root.listMaterials().length;
|
|
34
|
+
this.meshes = calculateMeshInfo(document);
|
|
35
|
+
this.textures = calculateTextureInfo(document);
|
|
36
|
+
}
|
|
37
|
+
static combine(others) {
|
|
38
|
+
const res = new Stats(new Document(), 0);
|
|
39
|
+
for (const other of others) {
|
|
40
|
+
res.size += other.size;
|
|
41
|
+
res.nodes += other.nodes;
|
|
42
|
+
res.materials += other.materials;
|
|
43
|
+
res.meshes.addTo(other.meshes);
|
|
44
|
+
res.textures.addTo(other.textures);
|
|
45
|
+
}
|
|
46
|
+
return res;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export class FileStats extends Stats {
|
|
50
|
+
filename;
|
|
51
|
+
constructor(file, document) {
|
|
52
|
+
super(document, statSync(file).size);
|
|
53
|
+
this.filename = getFileName(file);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export class MeshInfo {
|
|
57
|
+
count = 0;
|
|
58
|
+
primitives = 0;
|
|
59
|
+
vertices = 0;
|
|
60
|
+
triangles = 0;
|
|
61
|
+
memory = 0;
|
|
62
|
+
formats = [];
|
|
63
|
+
addTo(other) {
|
|
64
|
+
this.count += other.count;
|
|
65
|
+
this.primitives += other.primitives;
|
|
66
|
+
this.vertices += other.vertices;
|
|
67
|
+
this.triangles += other.triangles;
|
|
68
|
+
this.memory += other.memory;
|
|
69
|
+
this.formats = Array.from(new Set(this.formats.concat(other.formats)));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export class TextureInfo {
|
|
73
|
+
count = 0;
|
|
74
|
+
memory = 0;
|
|
75
|
+
gpu_memory = 0;
|
|
76
|
+
formats = [];
|
|
77
|
+
addTo(other) {
|
|
78
|
+
this.count += other.count;
|
|
79
|
+
this.memory += other.memory;
|
|
80
|
+
this.gpu_memory += other.gpu_memory;
|
|
81
|
+
this.formats = Array.from(new Set(this.formats.concat(other.formats)));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
13
84
|
/**
|
|
14
85
|
* Create a new filestats object from a file and a document
|
|
15
86
|
*/
|
|
16
|
-
export function createNewFilestats(file, document
|
|
17
|
-
const
|
|
18
|
-
const root = document.getRoot();
|
|
19
|
-
const filestats = {
|
|
20
|
-
filename,
|
|
21
|
-
size: statSync(file).size,
|
|
22
|
-
gpu_memory: Math.round(stats.gpu_memory),
|
|
23
|
-
nodes: root.listNodes().length,
|
|
24
|
-
materials: root.listMaterials().length,
|
|
25
|
-
meshes: root.listMeshes().length,
|
|
26
|
-
textures: root.listTextures().length,
|
|
27
|
-
vertex_count: 0,
|
|
28
|
-
triangle_count: 0,
|
|
29
|
-
};
|
|
30
|
-
// Count vertices
|
|
31
|
-
for (const mesh of root.listMeshes()) {
|
|
32
|
-
filestats.vertex_count += mesh.listPrimitives().reduce((acc, p) => acc + p.getAttribute('POSITION').getCount(), 0);
|
|
33
|
-
}
|
|
34
|
-
filestats.triangle_count = filestats.vertex_count / 3;
|
|
87
|
+
export function createNewFilestats(file, document) {
|
|
88
|
+
const filestats = new FileStats(file, document);
|
|
35
89
|
return filestats;
|
|
36
90
|
}
|
|
37
|
-
export function writeFileStatsToFile(
|
|
91
|
+
export function writeFileStatsToFile(filestats, output_path) {
|
|
38
92
|
if (output_path == undefined) {
|
|
39
93
|
console.error("No stats output path provided");
|
|
40
|
-
return;
|
|
94
|
+
return null;
|
|
41
95
|
}
|
|
42
|
-
|
|
43
|
-
|
|
96
|
+
const res = {
|
|
97
|
+
sum: Stats.combine(filestats),
|
|
98
|
+
files: filestats,
|
|
99
|
+
};
|
|
100
|
+
const json = JSON.stringify(res);
|
|
44
101
|
writeFileSync(output_path, json);
|
|
102
|
+
return json;
|
|
103
|
+
}
|
|
104
|
+
function calculateMeshInfo(document) {
|
|
105
|
+
const root = document.getRoot();
|
|
106
|
+
const meshes = root.listMeshes();
|
|
107
|
+
const res = new MeshInfo();
|
|
108
|
+
res.count = meshes.length;
|
|
109
|
+
for (const mesh of root.listMeshes()) {
|
|
110
|
+
const primitives = mesh.listPrimitives();
|
|
111
|
+
res.primitives += primitives.length;
|
|
112
|
+
for (const prim of primitives) {
|
|
113
|
+
const positions = prim.getAttribute('POSITION');
|
|
114
|
+
const indices = prim.getIndices();
|
|
115
|
+
if (positions) {
|
|
116
|
+
res.vertices += positions.getCount();
|
|
117
|
+
}
|
|
118
|
+
if (indices) {
|
|
119
|
+
res.triangles += indices.getCount() / 3;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
res.triangles += positions.getCount() / 3;
|
|
123
|
+
}
|
|
124
|
+
const semantics = prim.listSemantics();
|
|
125
|
+
for (const semantic of semantics) {
|
|
126
|
+
const accessor = prim.getAttribute(semantic);
|
|
127
|
+
if (accessor) {
|
|
128
|
+
const buffer = accessor.getByteLength();
|
|
129
|
+
res.memory += buffer;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const extensions = root.listExtensionsUsed();
|
|
135
|
+
for (const ext of extensions) {
|
|
136
|
+
if (ext.extensionName.startsWith("EXT_meshopt_compression")) {
|
|
137
|
+
res.formats.push("meshopt");
|
|
138
|
+
}
|
|
139
|
+
else if (ext.extensionName.startsWith("KHR_draco_mesh_compression")) {
|
|
140
|
+
res.formats.push("draco");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// filter formats so we only have unique values
|
|
144
|
+
res.formats = Array.from(new Set(res.formats));
|
|
145
|
+
return res;
|
|
146
|
+
}
|
|
147
|
+
function calculateTextureInfo(document) {
|
|
148
|
+
const root = document.getRoot();
|
|
149
|
+
const textures = root.listTextures();
|
|
150
|
+
const res = new TextureInfo();
|
|
151
|
+
res.count = textures.length;
|
|
152
|
+
res.formats = textures.map(t => t.getMimeType());
|
|
153
|
+
res.gpu_memory = 0;
|
|
154
|
+
res.memory = 0;
|
|
155
|
+
for (const texture of textures) {
|
|
156
|
+
const image = texture.getImage();
|
|
157
|
+
const mimetype = texture.getMimeType();
|
|
158
|
+
if (image && mimetype) {
|
|
159
|
+
res.gpu_memory += ImageUtils.getVRAMByteLength(image, mimetype);
|
|
160
|
+
res.memory += image.byteLength;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// filter formats so we only have unique values
|
|
164
|
+
res.formats = Array.from(new Set(res.formats));
|
|
165
|
+
return res;
|
|
45
166
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@needle-tools/gltf-build-pipeline",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.1-alpha.1",
|
|
4
4
|
"description": "Pipeline and tools for optimizing gltf files using gltf-transform and compression settings within glTF extensions",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|