@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,13 +1,13 @@
1
- import { Texture } from "@gltf-transform/core";
2
- import { Transform } from '@gltf-transform/core';
3
- import { NeedleWebPContext } from "./needle_webp.js";
4
- import { NeedleTransformContext } from "./needle_common.js";
5
- import { NeedleToktxContext } from "./needle_toktx.js";
6
- export declare function resizeTextureIfNecessary(texture: Texture): Promise<void>;
7
- interface NeedleTextureTransformContext extends NeedleTransformContext {
8
- webPContext: NeedleWebPContext;
9
- toktxContext: NeedleToktxContext;
10
- reportGPUMemory?: (bytes: number) => void;
11
- }
12
- export declare const needle_texture_transform: (options: NeedleTransformContext & Pick<NeedleTextureTransformContext, "reportGPUMemory">) => Transform;
13
- export {};
1
+ import { Texture } from "@gltf-transform/core";
2
+ import { Transform } from '@gltf-transform/core';
3
+ import { NeedleWebPContext } from "./needle_webp.js";
4
+ import { NeedleTransformContext } from "./needle_common.js";
5
+ import { NeedleToktxContext } from "./needle_toktx.js";
6
+ export declare function resizeTextureIfNecessary(texture: Texture): Promise<void>;
7
+ interface NeedleTextureTransformContext extends NeedleTransformContext {
8
+ webPContext: NeedleWebPContext;
9
+ toktxContext: NeedleToktxContext;
10
+ reportGPUMemory?: (bytes: number) => void;
11
+ }
12
+ export declare const needle_texture_transform: (options: NeedleTransformContext & Pick<NeedleTextureTransformContext, "reportGPUMemory">) => Transform;
13
+ export {};
@@ -1,257 +1,257 @@
1
- import { ImageUtils } from "@gltf-transform/core";
2
- import { resizeTextureToMax } from "./needle_progressive.js";
3
- import { NEEDLE_compression_texture } from '../extensions/index.js';
4
- import { addToCache, getHash, getKey, tryGetFromCache } from "../cache/index.js";
5
- import { NeedleWebPContext } from "./needle_webp.js";
6
- import { NeedleToktxContext } from "./needle_toktx.js";
7
- import { createTransform, listTextureSlots } from "@gltf-transform/functions";
8
- import { dirname } from "path";
9
- import { existsSync, rmSync } from "fs";
10
- import { validateImageMimeType } from "../utils/validate.js";
11
- import { isLightmap } from "../utils/texture.js";
12
- export async function resizeTextureIfNecessary(texture) {
13
- const ext = texture.getExtension(NEEDLE_compression_texture.EXTENSION_NAME);
14
- if (ext?.model) {
15
- const mimeType = texture.getMimeType();
16
- const isSupported = mimeType === "image/jpeg" || mimeType === "image/png";
17
- if (isSupported) {
18
- const settings = ext.model;
19
- const size = texture.getSize();
20
- if (size) {
21
- if (settings.maxSize > 4 && (size[0] > settings.maxSize || size[1] > settings.maxSize)) {
22
- console.log(`> Will resize texture ${texture.getName()} from ${size[0]}x${size[1]} to ${settings.maxSize}x${settings.maxSize}`);
23
- await resizeTextureToMax(texture, settings.maxSize, console);
24
- }
25
- }
26
- }
27
- }
28
- return undefined;
29
- }
30
- export const needle_texture_transform = function (options) {
31
- const webPContext = new NeedleWebPContext({});
32
- const toktxContext = new NeedleToktxContext({
33
- jobs: undefined,
34
- debug: options.debug,
35
- });
36
- const context = {
37
- config: options.config,
38
- ...options,
39
- webPContext,
40
- toktxContext
41
- };
42
- return createTransform("needle_texture_transform", async (document) => {
43
- await webPContext.prepare(document);
44
- if (context.noToktx !== true) {
45
- const validToktx = await toktxContext.prepare(document);
46
- if (!validToktx) {
47
- context.noToktx = true;
48
- }
49
- }
50
- try {
51
- const logger = document.getLogger();
52
- const textures = document.getRoot().listTextures();
53
- let totalGpuSize = 0;
54
- await Promise.all(textures.map((texture, textureIndex) => {
55
- return processTexture(texture, textureIndex, document, logger, context).then(() => {
56
- try {
57
- const size = ImageUtils.getVRAMByteLength(texture.getImage(), texture.getMimeType());
58
- totalGpuSize += size;
59
- }
60
- catch {
61
- // Ignore
62
- }
63
- });
64
- }));
65
- if (totalGpuSize > 0) {
66
- // TODO: report this to the parent
67
- const gpuMemoryEstimate = totalGpuSize / 1024 / 1024;
68
- if (context.reportGPUMemory)
69
- context.reportGPUMemory(totalGpuSize);
70
- else {
71
- console.log(`> Total GPU size: ${(gpuMemoryEstimate).toFixed(2)} MB of ${textures.length} textures`);
72
- }
73
- if (gpuMemoryEstimate > 1000) {
74
- 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.`);
75
- }
76
- }
77
- }
78
- finally {
79
- await webPContext.finalize(document);
80
- if (context.noToktx !== true)
81
- await toktxContext.finalize(document);
82
- }
83
- });
84
- };
85
- async function processTexture(texture, index, document, logger, context) {
86
- const validationResult = validateImageMimeType(texture.getMimeType(), texture.getImage());
87
- if (validationResult?.error) {
88
- logger.warn(`WARN: Texture ${texture.getName()} [${index}] has an incorrect mime type \"${texture.getMimeType()}\" → auto-correcting to ${validationResult.mimeType}`);
89
- texture.setMimeType(validationResult.mimeType);
90
- }
91
- const originalMimeType = texture.getMimeType();
92
- const isSupported = originalMimeType === "image/jpeg" || originalMimeType === "image/png" || originalMimeType === "image/webp";
93
- // dont try to transform EXR or already processed textures
94
- if (!isSupported)
95
- return;
96
- const ext = texture.getExtension(NEEDLE_compression_texture.EXTENSION_NAME);
97
- const compressionSettings = ext?.model;
98
- // is a user has configured a texture explictly to NOT be compressed
99
- // we should respect that
100
- if (compressionSettings?.mode === "none") {
101
- logger.info(`→ Skipping texture compression for ${texture.getName()} [${index}] as configured in the texture extension`);
102
- return;
103
- }
104
- // generate the cache key from the file data and settings
105
- const key = context.useCache ? (getKey(texture) + getHash(context.config)) : undefined;
106
- let cached = context.useCache && key ? tryGetFromCache(key) : undefined;
107
- if (cached) {
108
- if (cached.length <= 0) {
109
- logger.warn(`WARN: Cached texture ${texture.getName()} [${index}] is empty. Will reprocess.`);
110
- cached = undefined;
111
- }
112
- else {
113
- logger.info(`→ Using cached texture ${texture.getName()} [${index}] – ${(cached.length / 1024).toFixed(0)} KB`);
114
- }
115
- }
116
- let successfullyProcessed = false;
117
- let useDefaultCompression = true;
118
- const canRunToktx = context.noToktx !== true;
119
- const originalUri = texture.getURI();
120
- if (compressionSettings && compressionSettings.mode !== "auto") {
121
- // Handle highlevel modes
122
- switch (compressionSettings.mode) {
123
- case "world":
124
- compressionSettings.mode = "ETC1S";
125
- break;
126
- case "product":
127
- switch (compressionSettings.slot) {
128
- default:
129
- compressionSettings.mode = "ETC1S";
130
- break;
131
- case "normalTexture":
132
- compressionSettings.mode = "UASTC";
133
- break;
134
- case "baseColorTexture":
135
- case "metallicRoughnessTexture":
136
- case "lightmapTexture":
137
- compressionSettings.mode = "webp";
138
- compressionSettings.quality = 0.8;
139
- break;
140
- }
141
- break;
142
- }
143
- switch (compressionSettings.mode) {
144
- case "webp":
145
- if (cached) {
146
- texture.setImage(cached).setMimeType("image/webp");
147
- return;
148
- }
149
- await resizeTextureIfNecessary(texture);
150
- useDefaultCompression = false;
151
- successfullyProcessed = await context.webPContext.process(index, texture, compressionSettings, context);
152
- break;
153
- case "ETC1S":
154
- if (cached) {
155
- texture.setImage(cached).setMimeType("image/ktx2");
156
- return;
157
- }
158
- await resizeTextureIfNecessary(texture);
159
- useDefaultCompression = false;
160
- successfullyProcessed = await context.toktxContext.process(index, texture, compressionSettings, context);
161
- break;
162
- case "UASTC":
163
- if (cached) {
164
- texture.setImage(cached).setMimeType("image/ktx2");
165
- return;
166
- }
167
- await resizeTextureIfNecessary(texture);
168
- useDefaultCompression = false;
169
- successfullyProcessed = await context.toktxContext.process(index, texture, compressionSettings, context);
170
- break;
171
- }
172
- }
173
- if (useDefaultCompression) {
174
- let mimeType = "image/ktx2";
175
- let ktx2Format = undefined;
176
- const slots = listTextureSlots(texture);
177
- const size = texture.getSize();
178
- const isVRM = context.file?.toLowerCase().endsWith(".vrm");
179
- // TODO: need to make this into a function
180
- // const webpThreshold = 256; // this is the default threshold in progressive textures (needle_progressive.ts)
181
- // console.log(context.file, isVRM);
182
- // // if we're dealing with a VRM and the texture size is < 128x128 we can use webp (this is e.g. the case for progressive textures)
183
- // if (isVRM) {
184
- // const size = texture.getSize();
185
- // console.log(isVRM, size)
186
- // if (size && size[0] <= webpThreshold && size[1] <= webpThreshold) {
187
- // mimetype = "image/webp";
188
- // }
189
- // }
190
- const webpOptions = {};
191
- if (slots.includes("lightmapTexture") || isLightmap(texture)) {
192
- mimeType = "image/webp";
193
- }
194
- // for https://linear.app/needle/issue/NE-6464
195
- else if (size[0] <= 32 || size[1] <= 32) {
196
- // can not use UASTC because it's ALSO shitty, see issue above
197
- mimeType = "image/webp";
198
- }
199
- // For product compression we automatically select webP if the texture is used in a PBR material
200
- // https://linear.app/needle/issue/NE-6458
201
- // TODO: this should probably moved to the top and we should create a compression settings object
202
- else if (context.config.usecase === "product") {
203
- const shouldUseWebp = slots.includes("normalTexture") || slots.includes("metallicRoughnessTexture");
204
- if (shouldUseWebp) {
205
- mimeType = "image/webp";
206
- }
207
- }
208
- // If we have a texture cached we can use it
209
- // the default compression is toktx so we can safely use that for now
210
- if (cached) {
211
- texture.setImage(cached).setMimeType(mimeType);
212
- return;
213
- }
214
- await resizeTextureIfNecessary(texture);
215
- if (mimeType === "image/webp") {
216
- successfullyProcessed = await context.webPContext.process(index, texture, {
217
- ...webpOptions,
218
- quality: .95,
219
- slot: slots.join(","),
220
- }, context);
221
- }
222
- else if (canRunToktx) {
223
- // if no specific compression settings are provided we just use the toktx default settings
224
- // if we ever change what is used for default compression we need to teach the cache to save the mime type
225
- // For VRMs we use ETC1S always
226
- let options = null;
227
- if (ktx2Format || isVRM) {
228
- options = { mode: ktx2Format || "ETC1S" };
229
- }
230
- successfullyProcessed = await context.toktxContext.process(index, texture, options, context);
231
- }
232
- else {
233
- logger.warn("Toktx not found: Skipping texture compression: " + originalMimeType);
234
- }
235
- }
236
- if (context.useCache && successfullyProcessed && key) {
237
- const data = texture.getImage();
238
- if (data)
239
- addToCache(key, data);
240
- }
241
- if (successfullyProcessed) {
242
- const newUri = texture.getURI();
243
- if (originalUri && originalUri != newUri) {
244
- const inDirectory = dirname(context.file);
245
- const outDirectory = dirname(context.outfile);
246
- const originalPath = inDirectory + "/" + originalUri;
247
- // if the file output directory is the same as the input directory
248
- // (meaning compression runs in the same directory)
249
- // we remove the original file (since it effectively gets replaced)
250
- // otherwise we dont touch the original file
251
- if (inDirectory == outDirectory && existsSync(originalPath)) {
252
- logger.info(`Removing old texture \"${originalUri}\" → \"${newUri}\"`);
253
- rmSync(originalPath);
254
- }
255
- }
256
- }
257
- }
1
+ import { ImageUtils } from "@gltf-transform/core";
2
+ import { resizeTextureToMax } from "./needle_progressive.js";
3
+ import { NEEDLE_compression_texture } from '../extensions/index.js';
4
+ import { addToCache, getHash, getKey, tryGetFromCache } from "../cache/index.js";
5
+ import { NeedleWebPContext } from "./needle_webp.js";
6
+ import { NeedleToktxContext } from "./needle_toktx.js";
7
+ import { createTransform, listTextureSlots } from "@gltf-transform/functions";
8
+ import { dirname } from "path";
9
+ import { existsSync, rmSync } from "fs";
10
+ import { validateImageMimeType } from "../utils/validate.js";
11
+ import { isLightmap } from "../utils/texture.js";
12
+ export async function resizeTextureIfNecessary(texture) {
13
+ const ext = texture.getExtension(NEEDLE_compression_texture.EXTENSION_NAME);
14
+ if (ext?.model) {
15
+ const mimeType = texture.getMimeType();
16
+ const isSupported = mimeType === "image/jpeg" || mimeType === "image/png";
17
+ if (isSupported) {
18
+ const settings = ext.model;
19
+ const size = texture.getSize();
20
+ if (size) {
21
+ if (settings.maxSize > 4 && (size[0] > settings.maxSize || size[1] > settings.maxSize)) {
22
+ console.log(`> Will resize texture ${texture.getName()} from ${size[0]}x${size[1]} to ${settings.maxSize}x${settings.maxSize}`);
23
+ await resizeTextureToMax(texture, settings.maxSize, console);
24
+ }
25
+ }
26
+ }
27
+ }
28
+ return undefined;
29
+ }
30
+ export const needle_texture_transform = function (options) {
31
+ const webPContext = new NeedleWebPContext({});
32
+ const toktxContext = new NeedleToktxContext({
33
+ jobs: undefined,
34
+ debug: options.debug,
35
+ });
36
+ const context = {
37
+ config: options.config,
38
+ ...options,
39
+ webPContext,
40
+ toktxContext
41
+ };
42
+ return createTransform("needle_texture_transform", async (document) => {
43
+ await webPContext.prepare(document);
44
+ if (context.noToktx !== true) {
45
+ const validToktx = await toktxContext.prepare(document);
46
+ if (!validToktx) {
47
+ context.noToktx = true;
48
+ }
49
+ }
50
+ try {
51
+ const logger = document.getLogger();
52
+ const textures = document.getRoot().listTextures();
53
+ let totalGpuSize = 0;
54
+ await Promise.all(textures.map((texture, textureIndex) => {
55
+ return processTexture(texture, textureIndex, document, logger, context).then(() => {
56
+ try {
57
+ const size = ImageUtils.getVRAMByteLength(texture.getImage(), texture.getMimeType());
58
+ totalGpuSize += size;
59
+ }
60
+ catch {
61
+ // Ignore
62
+ }
63
+ });
64
+ }));
65
+ if (totalGpuSize > 0) {
66
+ // TODO: report this to the parent
67
+ const gpuMemoryEstimate = totalGpuSize / 1024 / 1024;
68
+ if (context.reportGPUMemory)
69
+ context.reportGPUMemory(totalGpuSize);
70
+ else {
71
+ console.log(`> Total GPU size: ${(gpuMemoryEstimate).toFixed(2)} MB of ${textures.length} textures`);
72
+ }
73
+ if (gpuMemoryEstimate > 1000) {
74
+ 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.`);
75
+ }
76
+ }
77
+ }
78
+ finally {
79
+ await webPContext.finalize(document);
80
+ if (context.noToktx !== true)
81
+ await toktxContext.finalize(document);
82
+ }
83
+ });
84
+ };
85
+ async function processTexture(texture, index, document, logger, context) {
86
+ const validationResult = validateImageMimeType(texture.getMimeType(), texture.getImage());
87
+ if (validationResult?.error) {
88
+ logger.warn(`WARN: Texture ${texture.getName()} [${index}] has an incorrect mime type \"${texture.getMimeType()}\" → auto-correcting to ${validationResult.mimeType}`);
89
+ texture.setMimeType(validationResult.mimeType);
90
+ }
91
+ const originalMimeType = texture.getMimeType();
92
+ const isSupported = originalMimeType === "image/jpeg" || originalMimeType === "image/png" || originalMimeType === "image/webp";
93
+ // dont try to transform EXR or already processed textures
94
+ if (!isSupported)
95
+ return;
96
+ const ext = texture.getExtension(NEEDLE_compression_texture.EXTENSION_NAME);
97
+ const compressionSettings = ext?.model;
98
+ // is a user has configured a texture explictly to NOT be compressed
99
+ // we should respect that
100
+ if (compressionSettings?.mode === "none") {
101
+ logger.info(`→ Skipping texture compression for ${texture.getName()} [${index}] as configured in the texture extension`);
102
+ return;
103
+ }
104
+ // generate the cache key from the file data and settings
105
+ const key = context.useCache ? (getKey(texture) + getHash(context.config)) : undefined;
106
+ let cached = context.useCache && key ? tryGetFromCache(key) : undefined;
107
+ if (cached) {
108
+ if (cached.length <= 0) {
109
+ logger.warn(`WARN: Cached texture ${texture.getName()} [${index}] is empty. Will reprocess.`);
110
+ cached = undefined;
111
+ }
112
+ else {
113
+ logger.info(`→ Using cached texture ${texture.getName()} [${index}] – ${(cached.length / 1024).toFixed(0)} KB`);
114
+ }
115
+ }
116
+ let successfullyProcessed = false;
117
+ let useDefaultCompression = true;
118
+ const canRunToktx = context.noToktx !== true;
119
+ const originalUri = texture.getURI();
120
+ if (compressionSettings && compressionSettings.mode !== "auto") {
121
+ // Handle highlevel modes
122
+ switch (compressionSettings.mode) {
123
+ case "world":
124
+ compressionSettings.mode = "ETC1S";
125
+ break;
126
+ case "product":
127
+ switch (compressionSettings.slot) {
128
+ default:
129
+ compressionSettings.mode = "ETC1S";
130
+ break;
131
+ case "normalTexture":
132
+ compressionSettings.mode = "UASTC";
133
+ break;
134
+ case "baseColorTexture":
135
+ case "metallicRoughnessTexture":
136
+ case "lightmapTexture":
137
+ compressionSettings.mode = "webp";
138
+ compressionSettings.quality = 0.8;
139
+ break;
140
+ }
141
+ break;
142
+ }
143
+ switch (compressionSettings.mode) {
144
+ case "webp":
145
+ if (cached) {
146
+ texture.setImage(cached).setMimeType("image/webp");
147
+ return;
148
+ }
149
+ await resizeTextureIfNecessary(texture);
150
+ useDefaultCompression = false;
151
+ successfullyProcessed = await context.webPContext.process(index, texture, compressionSettings, context);
152
+ break;
153
+ case "ETC1S":
154
+ if (cached) {
155
+ texture.setImage(cached).setMimeType("image/ktx2");
156
+ return;
157
+ }
158
+ await resizeTextureIfNecessary(texture);
159
+ useDefaultCompression = false;
160
+ successfullyProcessed = await context.toktxContext.process(index, texture, compressionSettings, context);
161
+ break;
162
+ case "UASTC":
163
+ if (cached) {
164
+ texture.setImage(cached).setMimeType("image/ktx2");
165
+ return;
166
+ }
167
+ await resizeTextureIfNecessary(texture);
168
+ useDefaultCompression = false;
169
+ successfullyProcessed = await context.toktxContext.process(index, texture, compressionSettings, context);
170
+ break;
171
+ }
172
+ }
173
+ if (useDefaultCompression) {
174
+ let mimeType = "image/ktx2";
175
+ let ktx2Format = undefined;
176
+ const slots = listTextureSlots(texture);
177
+ const size = texture.getSize();
178
+ const isVRM = context.file?.toLowerCase().endsWith(".vrm");
179
+ // TODO: need to make this into a function
180
+ // const webpThreshold = 256; // this is the default threshold in progressive textures (needle_progressive.ts)
181
+ // console.log(context.file, isVRM);
182
+ // // if we're dealing with a VRM and the texture size is < 128x128 we can use webp (this is e.g. the case for progressive textures)
183
+ // if (isVRM) {
184
+ // const size = texture.getSize();
185
+ // console.log(isVRM, size)
186
+ // if (size && size[0] <= webpThreshold && size[1] <= webpThreshold) {
187
+ // mimetype = "image/webp";
188
+ // }
189
+ // }
190
+ const webpOptions = {};
191
+ if (slots.includes("lightmapTexture") || isLightmap(texture)) {
192
+ mimeType = "image/webp";
193
+ }
194
+ // for https://linear.app/needle/issue/NE-6464
195
+ else if (size[0] <= 32 || size[1] <= 32) {
196
+ // can not use UASTC because it's ALSO shitty, see issue above
197
+ mimeType = "image/webp";
198
+ }
199
+ // For product compression we automatically select webP if the texture is used in a PBR material
200
+ // https://linear.app/needle/issue/NE-6458
201
+ // TODO: this should probably moved to the top and we should create a compression settings object
202
+ else if (context.config.usecase === "product") {
203
+ const shouldUseWebp = slots.includes("normalTexture") || slots.includes("metallicRoughnessTexture");
204
+ if (shouldUseWebp) {
205
+ mimeType = "image/webp";
206
+ }
207
+ }
208
+ // If we have a texture cached we can use it
209
+ // the default compression is toktx so we can safely use that for now
210
+ if (cached) {
211
+ texture.setImage(cached).setMimeType(mimeType);
212
+ return;
213
+ }
214
+ await resizeTextureIfNecessary(texture);
215
+ if (mimeType === "image/webp") {
216
+ successfullyProcessed = await context.webPContext.process(index, texture, {
217
+ ...webpOptions,
218
+ quality: .95,
219
+ slot: slots.join(","),
220
+ }, context);
221
+ }
222
+ else if (canRunToktx) {
223
+ // if no specific compression settings are provided we just use the toktx default settings
224
+ // if we ever change what is used for default compression we need to teach the cache to save the mime type
225
+ // For VRMs we use ETC1S always
226
+ let options = null;
227
+ if (ktx2Format || isVRM) {
228
+ options = { mode: ktx2Format || "ETC1S" };
229
+ }
230
+ successfullyProcessed = await context.toktxContext.process(index, texture, options, context);
231
+ }
232
+ else {
233
+ logger.warn("Toktx not found: Skipping texture compression: " + originalMimeType);
234
+ }
235
+ }
236
+ if (context.useCache && successfullyProcessed && key) {
237
+ const data = texture.getImage();
238
+ if (data)
239
+ addToCache(key, data);
240
+ }
241
+ if (successfullyProcessed) {
242
+ const newUri = texture.getURI();
243
+ if (originalUri && originalUri != newUri) {
244
+ const inDirectory = dirname(context.file);
245
+ const outDirectory = dirname(context.outfile);
246
+ const originalPath = inDirectory + "/" + originalUri;
247
+ // if the file output directory is the same as the input directory
248
+ // (meaning compression runs in the same directory)
249
+ // we remove the original file (since it effectively gets replaced)
250
+ // otherwise we dont touch the original file
251
+ if (inDirectory == outDirectory && existsSync(originalPath)) {
252
+ logger.info(`Removing old texture \"${originalUri}\" → \"${newUri}\"`);
253
+ rmSync(originalPath);
254
+ }
255
+ }
256
+ }
257
+ }
@@ -1,26 +1,26 @@
1
- import { Document, Texture } from "@gltf-transform/core";
2
- import { KHRTextureBasisu } from "@gltf-transform/extensions";
3
- import { NEEDLE_compression_texture_schema } from "../extensions/index.js";
4
- import { INeedleTextureTransformContext, NeedleTransformContext } from "./needle_common.js";
5
- export interface NeedleToktxOptions {
6
- debug?: boolean;
7
- jobs?: number;
8
- toktxPath?: string;
9
- }
10
- export declare class NeedleToktxContext implements INeedleTextureTransformContext {
11
- limit: any;
12
- basisuExtension: KHRTextureBasisu;
13
- version: string;
14
- cacheDirectory: string;
15
- tmpPathBase: string;
16
- document: Document;
17
- env: undefined | {
18
- PATH: string;
19
- };
20
- numTextures: number;
21
- private _prepareFailed;
22
- constructor(options: NeedleToktxOptions);
23
- prepare(document: Document): Promise<boolean>;
24
- process(index: number, texture: Texture, settings: Pick<NEEDLE_compression_texture_schema, "mode"> | null, context: NeedleTransformContext): Promise<boolean>;
25
- finalize(document: Document): Promise<void>;
26
- }
1
+ import { Document, Texture } from "@gltf-transform/core";
2
+ import { KHRTextureBasisu } from "@gltf-transform/extensions";
3
+ import { NEEDLE_compression_texture_schema } from "../extensions/index.js";
4
+ import { INeedleTextureTransformContext, NeedleTransformContext } from "./needle_common.js";
5
+ export interface NeedleToktxOptions {
6
+ debug?: boolean;
7
+ jobs?: number;
8
+ toktxPath?: string;
9
+ }
10
+ export declare class NeedleToktxContext implements INeedleTextureTransformContext {
11
+ limit: any;
12
+ basisuExtension: KHRTextureBasisu;
13
+ version: string;
14
+ cacheDirectory: string;
15
+ tmpPathBase: string;
16
+ document: Document;
17
+ env: undefined | {
18
+ PATH: string;
19
+ };
20
+ numTextures: number;
21
+ private _prepareFailed;
22
+ constructor(options: NeedleToktxOptions);
23
+ prepare(document: Document): Promise<boolean>;
24
+ process(index: number, texture: Texture, settings: Pick<NEEDLE_compression_texture_schema, "mode"> | null, context: NeedleTransformContext): Promise<boolean>;
25
+ finalize(document: Document): Promise<void>;
26
+ }