@needle-tools/gltf-build-pipeline 2.13.0 → 2.13.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.
Files changed (103) hide show
  1. package/CHANGELOG.md +520 -517
  2. package/README.md +38 -38
  3. package/dist/cache/cache.d.ts +55 -55
  4. package/dist/cache/cache.js +326 -326
  5. package/dist/cache/index.d.ts +1 -1
  6. package/dist/cache/index.js +1 -1
  7. package/dist/cli/index.d.ts +2 -2
  8. package/dist/cli/index.js +198 -198
  9. package/dist/config/index.d.ts +53 -53
  10. package/dist/config/index.js +152 -152
  11. package/dist/constants.d.ts +7 -7
  12. package/dist/constants.js +7 -7
  13. package/dist/extensions/NEEDLE_compression_texture/NEEDLE_compression_texture.d.ts +24 -24
  14. package/dist/extensions/NEEDLE_compression_texture/NEEDLE_compression_texture.js +54 -54
  15. package/dist/extensions/NEEDLE_compression_texture/index.d.ts +1 -1
  16. package/dist/extensions/NEEDLE_compression_texture/index.js +1 -1
  17. package/dist/extensions/NEEDLE_mesh_compression/NEEDLE_mesh_compression.d.ts +27 -27
  18. package/dist/extensions/NEEDLE_mesh_compression/NEEDLE_mesh_compression.js +88 -88
  19. package/dist/extensions/NEEDLE_mesh_compression/index.d.ts +1 -1
  20. package/dist/extensions/NEEDLE_mesh_compression/index.js +1 -1
  21. package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.d.ts +27 -27
  22. package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.js +980 -980
  23. package/dist/extensions/NEEDLE_opaque/index.d.ts +3 -3
  24. package/dist/extensions/NEEDLE_opaque/index.js +30 -30
  25. package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.d.ts +48 -48
  26. package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.js +69 -69
  27. package/dist/extensions/NEEDLE_progressive/index.d.ts +1 -1
  28. package/dist/extensions/NEEDLE_progressive/index.js +1 -1
  29. package/dist/extensions/NEEDLE_progressive_mesh_settings/index.d.ts +23 -23
  30. package/dist/extensions/NEEDLE_progressive_mesh_settings/index.js +41 -41
  31. package/dist/extensions/NEEDLE_progressive_texture_settings/index.d.ts +25 -25
  32. package/dist/extensions/NEEDLE_progressive_texture_settings/index.js +38 -38
  33. package/dist/extensions/index.d.ts +7 -7
  34. package/dist/extensions/index.js +17 -17
  35. package/dist/extensions/utils.d.ts +6 -6
  36. package/dist/extensions/utils.js +72 -72
  37. package/dist/functions/weld.3.d.ts +76 -76
  38. package/dist/functions/weld.3.js +414 -414
  39. package/dist/index.d.ts +8 -8
  40. package/dist/index.js +8 -8
  41. package/dist/scripts/index.d.ts +2 -2
  42. package/dist/scripts/index.js +2 -2
  43. package/dist/scripts/pack-gltf.d.ts +15 -15
  44. package/dist/scripts/pack-gltf.js +135 -133
  45. package/dist/scripts/stats.d.ts +2 -2
  46. package/dist/scripts/stats.js +19 -19
  47. package/dist/transforms/index.d.ts +9 -9
  48. package/dist/transforms/index.js +9 -9
  49. package/dist/transforms/needle_animation_transform.d.ts +2 -2
  50. package/dist/transforms/needle_animation_transform.js +5 -5
  51. package/dist/transforms/needle_asset.d.ts +3 -3
  52. package/dist/transforms/needle_asset.js +50 -50
  53. package/dist/transforms/needle_common.d.ts +17 -17
  54. package/dist/transforms/needle_common.js +1 -1
  55. package/dist/transforms/needle_mesh_transform.d.ts +15 -15
  56. package/dist/transforms/needle_mesh_transform.js +223 -223
  57. package/dist/transforms/needle_progressive.d.ts +45 -45
  58. package/dist/transforms/needle_progressive.js +1043 -1043
  59. package/dist/transforms/needle_texture_transform.d.ts +13 -13
  60. package/dist/transforms/needle_texture_transform.js +257 -257
  61. package/dist/transforms/needle_toktx.d.ts +26 -26
  62. package/dist/transforms/needle_toktx.js +243 -243
  63. package/dist/transforms/needle_webp.d.ts +15 -15
  64. package/dist/transforms/needle_webp.js +53 -53
  65. package/dist/transforms/toktx.d.ts +52 -52
  66. package/dist/transforms/toktx.js +221 -221
  67. package/dist/transforms/toktx_types.d.ts +78 -78
  68. package/dist/transforms/toktx_types.js +62 -62
  69. package/dist/transforms/util.d.ts +21 -21
  70. package/dist/transforms/util.js +90 -90
  71. package/dist/utils/compression_utils.d.ts +2 -2
  72. package/dist/utils/compression_utils.js +6 -6
  73. package/dist/utils/fileutils.d.ts +19 -19
  74. package/dist/utils/fileutils.js +177 -177
  75. package/dist/utils/guid.d.ts +1 -1
  76. package/dist/utils/guid.js +5 -5
  77. package/dist/utils/index.d.ts +10 -10
  78. package/dist/utils/index.js +10 -10
  79. package/dist/utils/merge.d.ts +12 -12
  80. package/dist/utils/merge.js +31 -31
  81. package/dist/utils/mesh.d.ts +13 -13
  82. package/dist/utils/mesh.js +124 -124
  83. package/dist/utils/nodeio.d.ts +6 -6
  84. package/dist/utils/nodeio.js +86 -86
  85. package/dist/utils/stats.d.ts +70 -70
  86. package/dist/utils/stats.js +258 -258
  87. package/dist/utils/texture.d.ts +5 -5
  88. package/dist/utils/texture.js +14 -14
  89. package/dist/utils/validate.d.ts +5 -5
  90. package/dist/utils/validate.js +17 -17
  91. package/dist/utils/version.d.ts +1 -1
  92. package/dist/utils/version.gen.d.ts +1 -1
  93. package/dist/utils/version.gen.js +1 -1
  94. package/dist/utils/version.js +4 -4
  95. package/package.json +79 -79
  96. package/scripts/clear-caches.mjs +3 -3
  97. package/scripts/defaults.mjs +11 -11
  98. package/scripts/fileutils.mjs +5 -5
  99. package/scripts/index.mjs +41 -41
  100. package/scripts/limit-caches.mjs +3 -3
  101. package/scripts/make-progressive.mjs +16 -16
  102. package/scripts/pack-gltf.mjs +106 -106
  103. package/tsconfig.json +19 -19
@@ -1,243 +1,243 @@
1
- import { BufferUtils, FileUtils, ImageUtils, uuid } from "@gltf-transform/core";
2
- import { listTextureSlots, getTextureChannelMask, fitPowerOfTwo, TextureResizeFilter } from "@gltf-transform/functions";
3
- import { KHRTextureBasisu } from "@gltf-transform/extensions";
4
- import { checkKTXSoftware, createParams, KTX_SOFTWARE_VERSION_MIN, Mode } from "./toktx.js";
5
- import { existsSync, mkdirSync, rmSync } from "fs";
6
- import { formatBytes, spawn, waitExit } from './util.js';
7
- import { ETC1S_DEFAULTS, UASTC_DEFAULTS } from "./toktx_types.js";
8
- import fs from 'fs/promises';
9
- import os from 'os';
10
- import tmp from 'tmp';
11
- import pLimit from 'p-limit';
12
- import { replaceExtension } from "../utils/fileutils.js";
13
- import { ceilMultipleOfFour, isLightmap, isMultipleOfFour } from "../utils/index.js";
14
- import sharp from "sharp";
15
- const NUM_CPUS = os.cpus().length || 1; // microsoft/vscode#112122
16
- let didLogEnvironmentInfo = false;
17
- export class NeedleToktxContext {
18
- limit;
19
- basisuExtension;
20
- version;
21
- cacheDirectory;
22
- tmpPathBase;
23
- document;
24
- env;
25
- numTextures;
26
- _prepareFailed = false;
27
- constructor(options) {
28
- this.limit = pLimit(options.jobs || NUM_CPUS);
29
- // allow overriding toktx installation location / adding additional search paths
30
- const isUsingWindows = process.platform === 'win32';
31
- this.env = {
32
- PATH: process.env.PATH ?? ""
33
- };
34
- if (options?.toktxPath?.length && this.env) {
35
- this.env.PATH = `${options.toktxPath};${this.env.PATH}`;
36
- }
37
- // Use global env variable
38
- if (process.env.NEEDLE_TOKTX?.length) {
39
- if (!existsSync(process.env.NEEDLE_TOKTX)) {
40
- if (options.debug)
41
- console.log("WARN: NEEDLE_TOKTX is set to a non-existing path: \"" + process.env.NEEDLE_TOKTX + "\"");
42
- }
43
- else {
44
- if (options.debug)
45
- console.log("INFO: Adding NEEDLE_TOKTX to PATH: \"" + process.env.NEEDLE_TOKTX + "\"");
46
- this.env.PATH = `${process.env.NEEDLE_TOKTX};${this.env.PATH}`;
47
- }
48
- }
49
- // fallback default installation directory:
50
- if (isUsingWindows) {
51
- const defaultPath = "C:\\Program Files\\KTX-Software\\bin";
52
- if (existsSync(defaultPath))
53
- this.env.PATH = `${this.env.PATH};${defaultPath}`;
54
- }
55
- // append default system paths
56
- if (isUsingWindows && process.env.Path?.length)
57
- this.env.PATH = `${this.env.PATH};${process.env.Path}`;
58
- const ktxPaths = this.env.PATH.split(";").filter(p => p.toLocaleLowerCase().includes("ktx"));
59
- if (options.debug) {
60
- console.log("INFO: Found these paths including ktx:\n\t" + ktxPaths.join("\n\t") + "");
61
- }
62
- else if (ktxPaths?.length <= 0) {
63
- if (didLogEnvironmentInfo === false)
64
- console.log("INFO: Environment variable 'NEEDLE_TOKTX' not set. Use 'NEEDLE_TOKTX' to specify the directory of the toktx installation (path/to/bin)");
65
- didLogEnvironmentInfo = true;
66
- }
67
- }
68
- async prepare(document) {
69
- try {
70
- this.document = document;
71
- const logger = document.getLogger();
72
- this.version = await checkKTXSoftware(logger, this.env);
73
- if (this.version === null) {
74
- logger.warn(`WARN: toktx not found, skipping texture compression. Please install KTX-Software >= ${KTX_SOFTWARE_VERSION_MIN}`);
75
- this._prepareFailed = true;
76
- return false;
77
- }
78
- this.basisuExtension = document.createExtension(KHRTextureBasisu).setRequired(true);
79
- const tmpDir = `${tmp.tmpdir}/gltf-transform`;
80
- this.cacheDirectory = tmpDir;
81
- if (!(existsSync(tmpDir)))
82
- mkdirSync(tmpDir);
83
- this.tmpPathBase = tmpDir + "/" + uuid();
84
- this.numTextures = document.getRoot().listTextures().length;
85
- }
86
- catch (e) {
87
- console.error(e);
88
- console.log("Error preparing toktx transform");
89
- this._prepareFailed = true;
90
- return false;
91
- }
92
- return true;
93
- }
94
- async process(index, texture, settings, context) {
95
- const task = async () => {
96
- if (this._prepareFailed)
97
- return false;
98
- const doc = this.document;
99
- const logger = doc.getLogger();
100
- const slots = listTextureSlots(texture);
101
- let channels = getTextureChannelMask(texture);
102
- // const textureLabel =
103
- // texture.getURI() ||
104
- // texture.getName() ||
105
- // `${index + 1}/${doc.getRoot().listTextures().length}`;
106
- const prefix = `toktx:texture[${index}] ${texture.getName()}`;
107
- let srcMimeType = texture.getMimeType();
108
- if (srcMimeType === 'image/ktx2') {
109
- logger.debug(`${prefix}: Skipping, already KTX.`);
110
- return;
111
- }
112
- else if (srcMimeType !== 'image/png' && srcMimeType !== 'image/jpeg') {
113
- logger.warn(`${prefix}: Skipping, unsupported texture type "${texture.getMimeType()}".`);
114
- return;
115
- }
116
- if (slots.length > 0)
117
- logger.debug(`${prefix}: Slots → [${slots.join(', ')}]`);
118
- else
119
- logger.debug(`${prefix}: No slots, assuming default texture.`);
120
- let srcImage = texture.getImage();
121
- let srcExtension = texture.getURI()
122
- ? FileUtils.extension(texture.getURI())
123
- : ImageUtils.mimeTypeToExtension(texture.getMimeType());
124
- const srcSize = texture.getSize();
125
- const srcBytes = srcImage ? srcImage.byteLength : null;
126
- if (!srcImage || !srcSize || !srcBytes) {
127
- logger.warn(`${prefix}: Skipping, unreadable texture.`);
128
- return;
129
- }
130
- if (!isMultipleOfFour(srcSize[0]) || !isMultipleOfFour(srcSize[1])) {
131
- const limitInputPixels = true;
132
- const instance = sharp(srcImage, { limitInputPixels }).toFormat('png');
133
- const srcSize = ImageUtils.getSize(srcImage, srcMimeType);
134
- const dstSize = fitPowerOfTwo(srcSize, 'nearest-pot');
135
- dstSize[0] = ceilMultipleOfFour(dstSize[0]);
136
- dstSize[1] = ceilMultipleOfFour(dstSize[1]);
137
- logger.warn(`${prefix}: Resizing ${srcSize.join('x')} → ${dstSize.join('x')}px`);
138
- instance.resize(dstSize[0], dstSize[1], { fit: 'fill', kernel: TextureResizeFilter.LANCZOS3 });
139
- const buffer = await instance.toBuffer();
140
- srcImage = BufferUtils.toView(new Uint8Array(buffer));
141
- srcExtension = 'png';
142
- srcMimeType = 'image/png';
143
- }
144
- const tmpName = this.tmpPathBase + "_" + index;
145
- const inPath = tmpName + '.' + srcExtension;
146
- const outPath = tmpName + '.ktx2';
147
- // const inBytes = image.byteLength;
148
- const buffer = Buffer.from(srcImage);
149
- await fs.writeFile(inPath, buffer);
150
- let mode = Mode.ETC1S;
151
- let quality = 128;
152
- let compression = 1;
153
- if (settings) {
154
- if (settings.mode === "auto") {
155
- const useUASTC = slots.includes("normalTexture") || slots.includes("metallicRoughnessTexture");
156
- mode = useUASTC ? Mode.UASTC : Mode.ETC1S;
157
- // in auto-mode we use higher quality for normal and metallicRoughness textures
158
- quality = useUASTC ? 255 : 128;
159
- compression = useUASTC ? 4 : 1;
160
- }
161
- else {
162
- mode = settings.mode === "ETC1S" ? Mode.ETC1S : Mode.UASTC;
163
- }
164
- }
165
- else if (slots) {
166
- const isNormal = slots.includes("normalTexture");
167
- const isMetallicRoughness = slots.includes("metallicRoughnessTexture");
168
- const betterQuality = isNormal || isMetallicRoughness;
169
- mode = Mode.ETC1S;
170
- // https://github.khronos.org/KTX-Software/ktxtools/toktx.html
171
- quality = betterQuality ? 255 : 128;
172
- // ETC1S / BasisLZ compression level, an encoding speed vs. quality tradeoff. Range is [0,5], default is 1. Higher values are slower but give higher quality.
173
- compression = betterQuality ? 4 : 1;
174
- // For the product usecase we use UASTC for meallicroughness and normal textures
175
- if (context.config.usecase === "product") {
176
- if (betterQuality) {
177
- mode = Mode.UASTC;
178
- }
179
- quality = 255;
180
- compression = 4;
181
- }
182
- else if (slots.includes("lightmapTexture") || isLightmap(texture)) {
183
- quality = 255;
184
- compression = 4;
185
- }
186
- }
187
- const defaults = mode === Mode.ETC1S ? ETC1S_DEFAULTS : UASTC_DEFAULTS;
188
- // TODO: should we ensure that the texture is using all channels? Unused channels are discarded anyways. The issue we have here is that textures that are solely used from pointers have unknown channels. We register them with RGBA when generating progressive assets as well as assign texture json pointers /textures/ to
189
- // Alpha blended materials to make sure RGBA channels are used when progressive GLBs are compressed (in needle_progressive 377)
190
- // https://linear.app/needle/issue/NE-4985
191
- // channels = TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A;
192
- //@ts-ignore
193
- const toktxParams = createParams(texture, slots, channels, srcSize, logger, this.numTextures, { ...defaults, mode, quality, compression });
194
- await fs.writeFile(inPath, Buffer.from(srcImage));
195
- const params = [
196
- 'create',
197
- ...toktxParams,
198
- inPath,
199
- outPath,
200
- ];
201
- logger.debug(`${prefix}: Spawning → toktx ${mode.toUpperCase()} ${params.join(' ')}`);
202
- try {
203
- // COMPRESS: Run `ktx create` CLI tool.
204
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
205
- const [status, stdout, stderr] = await waitExit(spawn('ktx', params, { env: this.env }));
206
- if (status !== 0) {
207
- logger.error(`${prefix}: Failed → \n\n${stderr.toString()}`);
208
- }
209
- else {
210
- const result = await fs.readFile(outPath);
211
- texture.setImage(result).setMimeType('image/ktx2');
212
- // TODO: we also need to remove the old file when the extension has changed - this should probably be done outside of this method to also support webp
213
- // If this is a texture that was loaded from a file, we need to update the URI
214
- if (texture.getURI()) {
215
- const oldUri = texture.getURI();
216
- const newUri = replaceExtension(oldUri, 'ktx2');
217
- texture.setURI(newUri);
218
- }
219
- const dstBytes = texture.getImage().byteLength;
220
- logger.debug(`${prefix}: ${formatBytes(srcBytes)} → ${formatBytes(dstBytes)}`);
221
- return true;
222
- }
223
- }
224
- catch (err) {
225
- logger.error(`${prefix}: Toktx failed → ${err}`);
226
- }
227
- return false;
228
- };
229
- return this.limit(task);
230
- }
231
- async finalize(document) {
232
- const usesKTX2 = document
233
- .getRoot()
234
- .listTextures()
235
- .some((t) => t.getMimeType() === 'image/ktx2');
236
- if (!usesKTX2) {
237
- this.basisuExtension?.dispose();
238
- }
239
- if (this.cacheDirectory && existsSync(this.cacheDirectory)) {
240
- rmSync(this.cacheDirectory, { recursive: true });
241
- }
242
- }
243
- }
1
+ import { BufferUtils, FileUtils, ImageUtils, uuid } from "@gltf-transform/core";
2
+ import { listTextureSlots, getTextureChannelMask, fitPowerOfTwo, TextureResizeFilter } from "@gltf-transform/functions";
3
+ import { KHRTextureBasisu } from "@gltf-transform/extensions";
4
+ import { checkKTXSoftware, createParams, KTX_SOFTWARE_VERSION_MIN, Mode } from "./toktx.js";
5
+ import { existsSync, mkdirSync, rmSync } from "fs";
6
+ import { formatBytes, spawn, waitExit } from './util.js';
7
+ import { ETC1S_DEFAULTS, UASTC_DEFAULTS } from "./toktx_types.js";
8
+ import fs from 'fs/promises';
9
+ import os from 'os';
10
+ import tmp from 'tmp';
11
+ import pLimit from 'p-limit';
12
+ import { replaceExtension } from "../utils/fileutils.js";
13
+ import { ceilMultipleOfFour, isLightmap, isMultipleOfFour } from "../utils/index.js";
14
+ import sharp from "sharp";
15
+ const NUM_CPUS = os.cpus().length || 1; // microsoft/vscode#112122
16
+ let didLogEnvironmentInfo = false;
17
+ export class NeedleToktxContext {
18
+ limit;
19
+ basisuExtension;
20
+ version;
21
+ cacheDirectory;
22
+ tmpPathBase;
23
+ document;
24
+ env;
25
+ numTextures;
26
+ _prepareFailed = false;
27
+ constructor(options) {
28
+ this.limit = pLimit(options.jobs || NUM_CPUS);
29
+ // allow overriding toktx installation location / adding additional search paths
30
+ const isUsingWindows = process.platform === 'win32';
31
+ this.env = {
32
+ PATH: process.env.PATH ?? ""
33
+ };
34
+ if (options?.toktxPath?.length && this.env) {
35
+ this.env.PATH = `${options.toktxPath};${this.env.PATH}`;
36
+ }
37
+ // Use global env variable
38
+ if (process.env.NEEDLE_TOKTX?.length) {
39
+ if (!existsSync(process.env.NEEDLE_TOKTX)) {
40
+ if (options.debug)
41
+ console.log("WARN: NEEDLE_TOKTX is set to a non-existing path: \"" + process.env.NEEDLE_TOKTX + "\"");
42
+ }
43
+ else {
44
+ if (options.debug)
45
+ console.log("INFO: Adding NEEDLE_TOKTX to PATH: \"" + process.env.NEEDLE_TOKTX + "\"");
46
+ this.env.PATH = `${process.env.NEEDLE_TOKTX};${this.env.PATH}`;
47
+ }
48
+ }
49
+ // fallback default installation directory:
50
+ if (isUsingWindows) {
51
+ const defaultPath = "C:\\Program Files\\KTX-Software\\bin";
52
+ if (existsSync(defaultPath))
53
+ this.env.PATH = `${this.env.PATH};${defaultPath}`;
54
+ }
55
+ // append default system paths
56
+ if (isUsingWindows && process.env.Path?.length)
57
+ this.env.PATH = `${this.env.PATH};${process.env.Path}`;
58
+ const ktxPaths = this.env.PATH.split(";").filter(p => p.toLocaleLowerCase().includes("ktx"));
59
+ if (options.debug) {
60
+ console.log("INFO: Found these paths including ktx:\n\t" + ktxPaths.join("\n\t") + "");
61
+ }
62
+ else if (ktxPaths?.length <= 0) {
63
+ if (didLogEnvironmentInfo === false)
64
+ console.log("INFO: Environment variable 'NEEDLE_TOKTX' not set. Use 'NEEDLE_TOKTX' to specify the directory of the toktx installation (path/to/bin)");
65
+ didLogEnvironmentInfo = true;
66
+ }
67
+ }
68
+ async prepare(document) {
69
+ try {
70
+ this.document = document;
71
+ const logger = document.getLogger();
72
+ this.version = await checkKTXSoftware(logger, this.env);
73
+ if (this.version === null) {
74
+ logger.warn(`WARN: toktx not found, skipping texture compression. Please install KTX-Software >= ${KTX_SOFTWARE_VERSION_MIN}`);
75
+ this._prepareFailed = true;
76
+ return false;
77
+ }
78
+ this.basisuExtension = document.createExtension(KHRTextureBasisu).setRequired(true);
79
+ const tmpDir = `${tmp.tmpdir}/gltf-transform`;
80
+ this.cacheDirectory = tmpDir;
81
+ if (!(existsSync(tmpDir)))
82
+ mkdirSync(tmpDir);
83
+ this.tmpPathBase = tmpDir + "/" + uuid();
84
+ this.numTextures = document.getRoot().listTextures().length;
85
+ }
86
+ catch (e) {
87
+ console.error(e);
88
+ console.log("Error preparing toktx transform");
89
+ this._prepareFailed = true;
90
+ return false;
91
+ }
92
+ return true;
93
+ }
94
+ async process(index, texture, settings, context) {
95
+ const task = async () => {
96
+ if (this._prepareFailed)
97
+ return false;
98
+ const doc = this.document;
99
+ const logger = doc.getLogger();
100
+ const slots = listTextureSlots(texture);
101
+ let channels = getTextureChannelMask(texture);
102
+ // const textureLabel =
103
+ // texture.getURI() ||
104
+ // texture.getName() ||
105
+ // `${index + 1}/${doc.getRoot().listTextures().length}`;
106
+ const prefix = `toktx:texture[${index}] ${texture.getName()}`;
107
+ let srcMimeType = texture.getMimeType();
108
+ if (srcMimeType === 'image/ktx2') {
109
+ logger.debug(`${prefix}: Skipping, already KTX.`);
110
+ return;
111
+ }
112
+ else if (srcMimeType !== 'image/png' && srcMimeType !== 'image/jpeg') {
113
+ logger.warn(`${prefix}: Skipping, unsupported texture type "${texture.getMimeType()}".`);
114
+ return;
115
+ }
116
+ if (slots.length > 0)
117
+ logger.debug(`${prefix}: Slots → [${slots.join(', ')}]`);
118
+ else
119
+ logger.debug(`${prefix}: No slots, assuming default texture.`);
120
+ let srcImage = texture.getImage();
121
+ let srcExtension = texture.getURI()
122
+ ? FileUtils.extension(texture.getURI())
123
+ : ImageUtils.mimeTypeToExtension(texture.getMimeType());
124
+ const srcSize = texture.getSize();
125
+ const srcBytes = srcImage ? srcImage.byteLength : null;
126
+ if (!srcImage || !srcSize || !srcBytes) {
127
+ logger.warn(`${prefix}: Skipping, unreadable texture.`);
128
+ return;
129
+ }
130
+ if (!isMultipleOfFour(srcSize[0]) || !isMultipleOfFour(srcSize[1])) {
131
+ const limitInputPixels = true;
132
+ const instance = sharp(srcImage, { limitInputPixels }).toFormat('png');
133
+ const srcSize = ImageUtils.getSize(srcImage, srcMimeType);
134
+ const dstSize = fitPowerOfTwo(srcSize, 'nearest-pot');
135
+ dstSize[0] = ceilMultipleOfFour(dstSize[0]);
136
+ dstSize[1] = ceilMultipleOfFour(dstSize[1]);
137
+ logger.warn(`${prefix}: Resizing ${srcSize.join('x')} → ${dstSize.join('x')}px`);
138
+ instance.resize(dstSize[0], dstSize[1], { fit: 'fill', kernel: TextureResizeFilter.LANCZOS3 });
139
+ const buffer = await instance.toBuffer();
140
+ srcImage = BufferUtils.toView(new Uint8Array(buffer));
141
+ srcExtension = 'png';
142
+ srcMimeType = 'image/png';
143
+ }
144
+ const tmpName = this.tmpPathBase + "_" + index;
145
+ const inPath = tmpName + '.' + srcExtension;
146
+ const outPath = tmpName + '.ktx2';
147
+ // const inBytes = image.byteLength;
148
+ const buffer = Buffer.from(srcImage);
149
+ await fs.writeFile(inPath, buffer);
150
+ let mode = Mode.ETC1S;
151
+ let quality = 128;
152
+ let compression = 1;
153
+ if (settings) {
154
+ if (settings.mode === "auto") {
155
+ const useUASTC = slots.includes("normalTexture") || slots.includes("metallicRoughnessTexture");
156
+ mode = useUASTC ? Mode.UASTC : Mode.ETC1S;
157
+ // in auto-mode we use higher quality for normal and metallicRoughness textures
158
+ quality = useUASTC ? 255 : 128;
159
+ compression = useUASTC ? 4 : 1;
160
+ }
161
+ else {
162
+ mode = settings.mode === "ETC1S" ? Mode.ETC1S : Mode.UASTC;
163
+ }
164
+ }
165
+ else if (slots) {
166
+ const isNormal = slots.includes("normalTexture");
167
+ const isMetallicRoughness = slots.includes("metallicRoughnessTexture");
168
+ const betterQuality = isNormal || isMetallicRoughness;
169
+ mode = Mode.ETC1S;
170
+ // https://github.khronos.org/KTX-Software/ktxtools/toktx.html
171
+ quality = betterQuality ? 255 : 128;
172
+ // ETC1S / BasisLZ compression level, an encoding speed vs. quality tradeoff. Range is [0,5], default is 1. Higher values are slower but give higher quality.
173
+ compression = betterQuality ? 4 : 1;
174
+ // For the product usecase we use UASTC for meallicroughness and normal textures
175
+ if (context.config.usecase === "product") {
176
+ if (betterQuality) {
177
+ mode = Mode.UASTC;
178
+ }
179
+ quality = 255;
180
+ compression = 4;
181
+ }
182
+ else if (slots.includes("lightmapTexture") || isLightmap(texture)) {
183
+ quality = 255;
184
+ compression = 4;
185
+ }
186
+ }
187
+ const defaults = mode === Mode.ETC1S ? ETC1S_DEFAULTS : UASTC_DEFAULTS;
188
+ // TODO: should we ensure that the texture is using all channels? Unused channels are discarded anyways. The issue we have here is that textures that are solely used from pointers have unknown channels. We register them with RGBA when generating progressive assets as well as assign texture json pointers /textures/ to
189
+ // Alpha blended materials to make sure RGBA channels are used when progressive GLBs are compressed (in needle_progressive 377)
190
+ // https://linear.app/needle/issue/NE-4985
191
+ // channels = TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A;
192
+ //@ts-ignore
193
+ const toktxParams = createParams(texture, slots, channels, srcSize, logger, this.numTextures, { ...defaults, mode, quality, compression });
194
+ await fs.writeFile(inPath, Buffer.from(srcImage));
195
+ const params = [
196
+ 'create',
197
+ ...toktxParams,
198
+ inPath,
199
+ outPath,
200
+ ];
201
+ logger.debug(`${prefix}: Spawning → toktx ${mode.toUpperCase()} ${params.join(' ')}`);
202
+ try {
203
+ // COMPRESS: Run `ktx create` CLI tool.
204
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
205
+ const [status, stdout, stderr] = await waitExit(spawn('ktx', params, { env: this.env }));
206
+ if (status !== 0) {
207
+ logger.error(`${prefix}: Failed → \n\n${stderr.toString()}`);
208
+ }
209
+ else {
210
+ const result = await fs.readFile(outPath);
211
+ texture.setImage(result).setMimeType('image/ktx2');
212
+ // TODO: we also need to remove the old file when the extension has changed - this should probably be done outside of this method to also support webp
213
+ // If this is a texture that was loaded from a file, we need to update the URI
214
+ if (texture.getURI()) {
215
+ const oldUri = texture.getURI();
216
+ const newUri = replaceExtension(oldUri, 'ktx2');
217
+ texture.setURI(newUri);
218
+ }
219
+ const dstBytes = texture.getImage().byteLength;
220
+ logger.debug(`${prefix}: ${formatBytes(srcBytes)} → ${formatBytes(dstBytes)}`);
221
+ return true;
222
+ }
223
+ }
224
+ catch (err) {
225
+ logger.error(`${prefix}: Toktx failed → ${err}`);
226
+ }
227
+ return false;
228
+ };
229
+ return this.limit(task);
230
+ }
231
+ async finalize(document) {
232
+ const usesKTX2 = document
233
+ .getRoot()
234
+ .listTextures()
235
+ .some((t) => t.getMimeType() === 'image/ktx2');
236
+ if (!usesKTX2) {
237
+ this.basisuExtension?.dispose();
238
+ }
239
+ if (this.cacheDirectory && existsSync(this.cacheDirectory)) {
240
+ rmSync(this.cacheDirectory, { recursive: true });
241
+ }
242
+ }
243
+ }
@@ -1,15 +1,15 @@
1
- import { Document, ILogger, Texture } from '@gltf-transform/core';
2
- import { NEEDLE_compression_texture_schema } from '../extensions/index.js';
3
- import { INeedleTextureTransformContext, NeedleTransformContext } from './needle_common.js';
4
- import sharp from 'sharp';
5
- export interface NeedleWebPOptions {
6
- }
7
- export declare class NeedleWebPContext implements INeedleTextureTransformContext {
8
- options: NeedleWebPOptions;
9
- logger: ILogger;
10
- private compressed;
11
- constructor(options: NeedleWebPOptions);
12
- prepare(document: Document): Promise<void>;
13
- process(index: number, texture: Texture, settings: Pick<NEEDLE_compression_texture_schema, "quality" | "slot"> & Partial<sharp.WebpOptions>, context: NeedleTransformContext): Promise<boolean>;
14
- finalize(document: Document): Promise<void>;
15
- }
1
+ import { Document, ILogger, Texture } from '@gltf-transform/core';
2
+ import { NEEDLE_compression_texture_schema } from '../extensions/index.js';
3
+ import { INeedleTextureTransformContext, NeedleTransformContext } from './needle_common.js';
4
+ import sharp from 'sharp';
5
+ export interface NeedleWebPOptions {
6
+ }
7
+ export declare class NeedleWebPContext implements INeedleTextureTransformContext {
8
+ options: NeedleWebPOptions;
9
+ logger: ILogger;
10
+ private compressed;
11
+ constructor(options: NeedleWebPOptions);
12
+ prepare(document: Document): Promise<void>;
13
+ process(index: number, texture: Texture, settings: Pick<NEEDLE_compression_texture_schema, "quality" | "slot"> & Partial<sharp.WebpOptions>, context: NeedleTransformContext): Promise<boolean>;
14
+ finalize(document: Document): Promise<void>;
15
+ }
@@ -1,53 +1,53 @@
1
- import sharp from 'sharp';
2
- import { BufferUtils, HTTPUtils } from '@gltf-transform/core';
3
- import { EXTTextureWebP } from '@gltf-transform/extensions';
4
- export class NeedleWebPContext {
5
- options;
6
- logger;
7
- compressed = 0;
8
- constructor(options) {
9
- this.options = options;
10
- }
11
- async prepare(document) {
12
- this.compressed = 0;
13
- this.logger = document.getLogger();
14
- }
15
- async process(index, texture, settings, context) {
16
- const size = texture.getSize();
17
- // if the texture is too small we will use lossless compression
18
- let lossless = false;
19
- if (size[0] < 64 || size[1] < 64) {
20
- lossless = true;
21
- }
22
- const options = {
23
- lossless,
24
- ...settings,
25
- quality: settings.quality,
26
- // effort: remap(options.effort, 100, 6),
27
- // lossless: options.lossless,
28
- // nearLossless: options.nearLossless,
29
- };
30
- if (typeof options !== "string" && options.quality >= 0) {
31
- // the quality range is 0-1 so we remap to 0-100
32
- options.quality = Math.round(settings.quality * 100);
33
- }
34
- if (options.quality < 0) {
35
- options.quality = undefined; // If quality is 0 we use the default value
36
- }
37
- // console.log(options);
38
- this.logger.info(`webp:texture[${index}] ${texture.getName()}: Slots → [${settings.slot}] with quality '${options.quality === undefined ? 'default' : options.quality}'`);
39
- const buffer = await sharp(texture.getImage()).toFormat('webp', options).toBuffer();
40
- texture
41
- .setMimeType('image/webp')
42
- .setImage(BufferUtils.toView(buffer))
43
- .setURI(HTTPUtils.basename(texture.getURI()) + '.webp');
44
- this.compressed += 1;
45
- return true;
46
- }
47
- async finalize(document) {
48
- if (this.compressed > 0) {
49
- const webpExtension = document.createExtension(EXTTextureWebP);
50
- webpExtension.setRequired(true);
51
- }
52
- }
53
- }
1
+ import sharp from 'sharp';
2
+ import { BufferUtils, HTTPUtils } from '@gltf-transform/core';
3
+ import { EXTTextureWebP } from '@gltf-transform/extensions';
4
+ export class NeedleWebPContext {
5
+ options;
6
+ logger;
7
+ compressed = 0;
8
+ constructor(options) {
9
+ this.options = options;
10
+ }
11
+ async prepare(document) {
12
+ this.compressed = 0;
13
+ this.logger = document.getLogger();
14
+ }
15
+ async process(index, texture, settings, context) {
16
+ const size = texture.getSize();
17
+ // if the texture is too small we will use lossless compression
18
+ let lossless = false;
19
+ if (size[0] < 64 || size[1] < 64) {
20
+ lossless = true;
21
+ }
22
+ const options = {
23
+ lossless,
24
+ ...settings,
25
+ quality: settings.quality,
26
+ // effort: remap(options.effort, 100, 6),
27
+ // lossless: options.lossless,
28
+ // nearLossless: options.nearLossless,
29
+ };
30
+ if (typeof options !== "string" && options.quality >= 0) {
31
+ // the quality range is 0-1 so we remap to 0-100
32
+ options.quality = Math.round(settings.quality * 100);
33
+ }
34
+ if (options.quality < 0) {
35
+ options.quality = undefined; // If quality is 0 we use the default value
36
+ }
37
+ // console.log(options);
38
+ this.logger.info(`webp:texture[${index}] ${texture.getName()}: Slots → [${settings.slot}] with quality '${options.quality === undefined ? 'default' : options.quality}'`);
39
+ const buffer = await sharp(texture.getImage()).toFormat('webp', options).toBuffer();
40
+ texture
41
+ .setMimeType('image/webp')
42
+ .setImage(BufferUtils.toView(buffer))
43
+ .setURI(HTTPUtils.basename(texture.getURI()) + '.webp');
44
+ this.compressed += 1;
45
+ return true;
46
+ }
47
+ async finalize(document) {
48
+ if (this.compressed > 0) {
49
+ const webpExtension = document.createExtension(EXTTextureWebP);
50
+ webpExtension.setRequired(true);
51
+ }
52
+ }
53
+ }