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