@needle-tools/gltf-build-pipeline 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,15 @@ All notable changes to this package will be documented in this file.
4
4
  The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5
5
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.0.2] - 2023-04-19
8
+ - change: progressive should not log error out when running for already compressed gltf
9
+ - change: ignore EXT_texture_exr extension re-assignment
10
+
11
+ ## [1.0.1] - 2023-04-17
12
+ - fix: register meshopt decoder
13
+ - fix: catch exception in document read
14
+ - change: log gpu memory estimation for textures
15
+
7
16
  ## [1.0.0] - 2023-04-04
8
17
  - change: file utils use needle.config.json to find files if no directory was specified
9
18
  - change: add generator info
@@ -400,7 +400,8 @@ class TextureHandler {
400
400
  }
401
401
  });
402
402
  if (foundTexture === false) {
403
- console.warn("WARN: failed to re-assign extension:", this.key, this.index, this.tryGetSourceIndex(this.textureDefinition), textureDefs.length);
403
+ if (this.key !== "EXT_texture_exr" || this.debug)
404
+ console.warn("WARN: failed to re-assign extension:", this.key, this.index, this.tryGetSourceIndex(this.textureDefinition), textureDefs.length);
404
405
  }
405
406
  // this.document
406
407
  // .getRoot()
@@ -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,45 @@ 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
+ // Log this message as a warning, it happens when trying to run on a already compressed gltf file (e.g. pre-processed. When logging an error we fail the build which is not desired)
49
+ if (err.message === "Cannot read properties of undefined (reading 'DT_FLOAT32')") {
50
+ console.log("WARN: \"" + err.message + "\" at \"" + file + "\"");
51
51
  }
52
+ else
53
+ console.log("ERR: \"" + err.message + "\" at \"" + file + "\"");
54
+ return;
55
+ });
56
+ if (document) {
57
+ if (opts.debug === true) {
58
+ if (file.includes(".debug.")) {
59
+ console.log("Skip debug file: " + file);
60
+ return;
61
+ }
62
+ }
63
+ //@ts-ignore
64
+ document.getLogger().verbosity = 0;
65
+ await document.transform(needle_asset(), make_progressive_textures(file, { size: [256, 256] }, opts));
66
+ let filename = file;
67
+ if (opts.debug === true) {
68
+ // get extension
69
+ const extension = filename.split('.').pop();
70
+ // remove extension
71
+ filename = filename.split('.').slice(0, -1).join('.');
72
+ // add new extension
73
+ filename = filename + ".debug." + extension;
74
+ }
75
+ io.write(filename, document);
52
76
  }
53
- //@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);
77
+ }
78
+ catch (err) {
79
+ console.log("ERR: failed to process file \"" + file + "\", Reason: " + err.message);
66
80
  }
67
81
  }
68
82
  // basic idea:
@@ -1,3 +1,4 @@
1
+ import { ImageUtils } from "@gltf-transform/core";
1
2
  import { resizeTextureToMax } from "./needle_progressive.js";
2
3
  import { NEEDLE_compression_texture } from '../extensions/index.js';
3
4
  import { addToCache, getKey, tryGetFromCache } from "../cache/index.js";
@@ -25,7 +26,8 @@ export async function resizeTextureIfNecessary(texture) {
25
26
  export const needle_texture_transform = function (options) {
26
27
  const webPContext = new NeedleWebPContext({});
27
28
  const toktxContext = new NeedleToktxContext({
28
- jobs: undefined, // 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.2",
4
4
  "description": "Pipeline and tools for optimizing gltf files using gltf-transform and compression settings within glTF extensions",
5
5
  "main": "index.mjs",
6
6
  "type": "module",