@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.
- package/CHANGELOG.md +520 -517
- package/README.md +38 -38
- package/dist/cache/cache.d.ts +55 -55
- package/dist/cache/cache.js +326 -326
- package/dist/cache/index.d.ts +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.js +198 -198
- package/dist/config/index.d.ts +53 -53
- package/dist/config/index.js +152 -152
- package/dist/constants.d.ts +7 -7
- package/dist/constants.js +7 -7
- package/dist/extensions/NEEDLE_compression_texture/NEEDLE_compression_texture.d.ts +24 -24
- package/dist/extensions/NEEDLE_compression_texture/NEEDLE_compression_texture.js +54 -54
- package/dist/extensions/NEEDLE_compression_texture/index.d.ts +1 -1
- package/dist/extensions/NEEDLE_compression_texture/index.js +1 -1
- package/dist/extensions/NEEDLE_mesh_compression/NEEDLE_mesh_compression.d.ts +27 -27
- package/dist/extensions/NEEDLE_mesh_compression/NEEDLE_mesh_compression.js +88 -88
- package/dist/extensions/NEEDLE_mesh_compression/index.d.ts +1 -1
- package/dist/extensions/NEEDLE_mesh_compression/index.js +1 -1
- package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.d.ts +27 -27
- package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.js +980 -980
- package/dist/extensions/NEEDLE_opaque/index.d.ts +3 -3
- package/dist/extensions/NEEDLE_opaque/index.js +30 -30
- package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.d.ts +48 -48
- package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.js +69 -69
- package/dist/extensions/NEEDLE_progressive/index.d.ts +1 -1
- package/dist/extensions/NEEDLE_progressive/index.js +1 -1
- package/dist/extensions/NEEDLE_progressive_mesh_settings/index.d.ts +23 -23
- package/dist/extensions/NEEDLE_progressive_mesh_settings/index.js +41 -41
- package/dist/extensions/NEEDLE_progressive_texture_settings/index.d.ts +25 -25
- package/dist/extensions/NEEDLE_progressive_texture_settings/index.js +38 -38
- package/dist/extensions/index.d.ts +7 -7
- package/dist/extensions/index.js +17 -17
- package/dist/extensions/utils.d.ts +6 -6
- package/dist/extensions/utils.js +72 -72
- package/dist/functions/weld.3.d.ts +76 -76
- package/dist/functions/weld.3.js +414 -414
- package/dist/index.d.ts +8 -8
- package/dist/index.js +8 -8
- package/dist/scripts/index.d.ts +2 -2
- package/dist/scripts/index.js +2 -2
- package/dist/scripts/pack-gltf.d.ts +15 -15
- package/dist/scripts/pack-gltf.js +135 -133
- package/dist/scripts/stats.d.ts +2 -2
- package/dist/scripts/stats.js +19 -19
- package/dist/transforms/index.d.ts +9 -9
- package/dist/transforms/index.js +9 -9
- package/dist/transforms/needle_animation_transform.d.ts +2 -2
- package/dist/transforms/needle_animation_transform.js +5 -5
- package/dist/transforms/needle_asset.d.ts +3 -3
- package/dist/transforms/needle_asset.js +50 -50
- package/dist/transforms/needle_common.d.ts +17 -17
- package/dist/transforms/needle_common.js +1 -1
- package/dist/transforms/needle_mesh_transform.d.ts +15 -15
- package/dist/transforms/needle_mesh_transform.js +223 -223
- package/dist/transforms/needle_progressive.d.ts +45 -45
- package/dist/transforms/needle_progressive.js +1043 -1043
- package/dist/transforms/needle_texture_transform.d.ts +13 -13
- package/dist/transforms/needle_texture_transform.js +257 -257
- package/dist/transforms/needle_toktx.d.ts +26 -26
- package/dist/transforms/needle_toktx.js +243 -243
- package/dist/transforms/needle_webp.d.ts +15 -15
- package/dist/transforms/needle_webp.js +53 -53
- package/dist/transforms/toktx.d.ts +52 -52
- package/dist/transforms/toktx.js +221 -221
- package/dist/transforms/toktx_types.d.ts +78 -78
- package/dist/transforms/toktx_types.js +62 -62
- package/dist/transforms/util.d.ts +21 -21
- package/dist/transforms/util.js +90 -90
- package/dist/utils/compression_utils.d.ts +2 -2
- package/dist/utils/compression_utils.js +6 -6
- package/dist/utils/fileutils.d.ts +19 -19
- package/dist/utils/fileutils.js +177 -177
- package/dist/utils/guid.d.ts +1 -1
- package/dist/utils/guid.js +5 -5
- package/dist/utils/index.d.ts +10 -10
- package/dist/utils/index.js +10 -10
- package/dist/utils/merge.d.ts +12 -12
- package/dist/utils/merge.js +31 -31
- package/dist/utils/mesh.d.ts +13 -13
- package/dist/utils/mesh.js +124 -124
- package/dist/utils/nodeio.d.ts +6 -6
- package/dist/utils/nodeio.js +86 -86
- package/dist/utils/stats.d.ts +70 -70
- package/dist/utils/stats.js +258 -258
- package/dist/utils/texture.d.ts +5 -5
- package/dist/utils/texture.js +14 -14
- package/dist/utils/validate.d.ts +5 -5
- package/dist/utils/validate.js +17 -17
- package/dist/utils/version.d.ts +1 -1
- package/dist/utils/version.gen.d.ts +1 -1
- package/dist/utils/version.gen.js +1 -1
- package/dist/utils/version.js +4 -4
- package/package.json +79 -79
- package/scripts/clear-caches.mjs +3 -3
- package/scripts/defaults.mjs +11 -11
- package/scripts/fileutils.mjs +5 -5
- package/scripts/index.mjs +41 -41
- package/scripts/limit-caches.mjs +3 -3
- package/scripts/make-progressive.mjs +16 -16
- package/scripts/pack-gltf.mjs +106 -106
- 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
|
+
}
|