@needle-tools/gltf-build-pipeline 0.1.0
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 +140 -0
- package/clear-caches.mjs +4 -0
- package/defaults.mjs +12 -0
- package/dist/cache/cache.d.ts +5 -0
- package/dist/cache/cache.js +82 -0
- package/dist/cache/index.d.ts +1 -0
- package/dist/cache/index.js +1 -0
- package/dist/extensions/NEEDLE_compression_texture/NEEDLE_compression_texture.d.ts +24 -0
- package/dist/extensions/NEEDLE_compression_texture/NEEDLE_compression_texture.js +54 -0
- package/dist/extensions/NEEDLE_compression_texture/index.d.ts +1 -0
- package/dist/extensions/NEEDLE_compression_texture/index.js +1 -0
- package/dist/extensions/NEEDLE_mesh_compression/NEEDLE_mesh_compression.d.ts +23 -0
- package/dist/extensions/NEEDLE_mesh_compression/NEEDLE_mesh_compression.js +43 -0
- package/dist/extensions/NEEDLE_mesh_compression/index.d.ts +1 -0
- package/dist/extensions/NEEDLE_mesh_compression/index.js +1 -0
- package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.d.ts +20 -0
- package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.js +699 -0
- package/dist/extensions/NEEDLE_opaque/index.d.ts +3 -0
- package/dist/extensions/NEEDLE_opaque/index.js +18 -0
- package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.d.ts +23 -0
- package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.js +53 -0
- package/dist/extensions/NEEDLE_progressive/index.d.ts +1 -0
- package/dist/extensions/NEEDLE_progressive/index.js +1 -0
- package/dist/extensions/NEEDLE_progressive_texture_settings/index.d.ts +21 -0
- package/dist/extensions/NEEDLE_progressive_texture_settings/index.js +39 -0
- package/dist/extensions/index.d.ts +6 -0
- package/dist/extensions/index.js +16 -0
- package/dist/extensions/utils.d.ts +6 -0
- package/dist/extensions/utils.js +72 -0
- package/dist/functions/list-texture-slots.d.ts +1 -0
- package/dist/functions/list-texture-slots.js +35 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/transforms/index.d.ts +9 -0
- package/dist/transforms/index.js +9 -0
- package/dist/transforms/needle_animation_transform.d.ts +2 -0
- package/dist/transforms/needle_animation_transform.js +5 -0
- package/dist/transforms/needle_asset.d.ts +2 -0
- package/dist/transforms/needle_asset.js +28 -0
- package/dist/transforms/needle_common.d.ts +11 -0
- package/dist/transforms/needle_common.js +1 -0
- package/dist/transforms/needle_mesh_transform.d.ts +3 -0
- package/dist/transforms/needle_mesh_transform.js +83 -0
- package/dist/transforms/needle_progressive.d.ts +34 -0
- package/dist/transforms/needle_progressive.js +264 -0
- package/dist/transforms/needle_simplify.d.ts +3 -0
- package/dist/transforms/needle_simplify.js +76 -0
- package/dist/transforms/needle_texture_transform.d.ts +5 -0
- package/dist/transforms/needle_texture_transform.js +123 -0
- package/dist/transforms/needle_toktx.d.ts +24 -0
- package/dist/transforms/needle_toktx.js +144 -0
- package/dist/transforms/needle_webp.d.ts +13 -0
- package/dist/transforms/needle_webp.js +53 -0
- package/dist/transforms/toktx.d.ts +77 -0
- package/dist/transforms/toktx.js +230 -0
- package/dist/transforms/types/squoosh.d.ts +31 -0
- package/dist/transforms/types/squoosh.js +33 -0
- package/dist/transforms/util.d.ts +21 -0
- package/dist/transforms/util.js +90 -0
- package/dist/utils/fileutils.d.ts +2 -0
- package/dist/utils/fileutils.js +95 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/fileutils.mjs +6 -0
- package/index.mjs +33 -0
- package/make-progressive.mjs +11 -0
- package/pack-gltf.mjs +119 -0
- package/package.json +60 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { BufferUtils, Document, NodeIO, PropertyType } from '@gltf-transform/core';
|
|
2
|
+
import { listTextureSlots } from '@gltf-transform/functions';
|
|
3
|
+
import { NEEDLE_PROGRESSIVE_TEXTURE_EXTENSION_NAME, NEEDLE_progressive, generateGuid } from "../extensions/NEEDLE_progressive/index.js";
|
|
4
|
+
import { createOpaqueExtension, NEEDLE_compression_texture, NEEDLE_mesh_compression, NEEDLE_progressive_texture_settings, registerExtensions as registerDefaultExtensions } from '../extensions/index.js';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { existsSync } from 'fs';
|
|
7
|
+
import { addToCache, getKey, tryGetFromCache } from '../cache/index.js';
|
|
8
|
+
import sharp from "sharp";
|
|
9
|
+
import { foreachGLTF } from '../utils/index.js';
|
|
10
|
+
import { needle_asset } from './needle_asset.js';
|
|
11
|
+
function registerExtensions(io) {
|
|
12
|
+
registerDefaultExtensions(io);
|
|
13
|
+
io.registerExtensions([
|
|
14
|
+
NEEDLE_progressive,
|
|
15
|
+
NEEDLE_progressive_texture_settings,
|
|
16
|
+
//@ts-ignore
|
|
17
|
+
createOpaqueExtension(NEEDLE_compression_texture.EXTENSION_NAME, PropertyType.TEXTURE),
|
|
18
|
+
//@ts-ignore
|
|
19
|
+
createOpaqueExtension(NEEDLE_mesh_compression.EXTENSION_NAME, [PropertyType.MESH, PropertyType.PRIMITIVE])
|
|
20
|
+
]);
|
|
21
|
+
}
|
|
22
|
+
export async function make_progressive(opts) {
|
|
23
|
+
console.log("INFO: Run progressive transform: " + opts.path);
|
|
24
|
+
const file = opts.path;
|
|
25
|
+
const tasks = [];
|
|
26
|
+
foreachGLTF(file, f => {
|
|
27
|
+
const prog = process(f, opts);
|
|
28
|
+
tasks.push(prog);
|
|
29
|
+
});
|
|
30
|
+
await Promise.all(tasks);
|
|
31
|
+
}
|
|
32
|
+
async function process(file, opts) {
|
|
33
|
+
if (!file.endsWith(".glb") && !file.endsWith(".gltf")) {
|
|
34
|
+
console.log("ERR: file is not gltf/glb");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (!existsSync(file)) {
|
|
38
|
+
console.log("ERR: file does not exist: " + file);
|
|
39
|
+
}
|
|
40
|
+
const io = new NodeIO();
|
|
41
|
+
registerExtensions(io);
|
|
42
|
+
const document = await io.read(file).catch((err) => {
|
|
43
|
+
console.log("ERR: failed to read file \"" + file + "\", Reason: " + err.message);
|
|
44
|
+
return;
|
|
45
|
+
});
|
|
46
|
+
if (document) {
|
|
47
|
+
if (opts.debug === true) {
|
|
48
|
+
if (file.includes(".debug.")) {
|
|
49
|
+
console.log("Skip debug file: " + file);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//@ts-ignore
|
|
54
|
+
document.getLogger().verbosity = 0;
|
|
55
|
+
await document.transform(needle_asset(), make_progressive_textures(file, { size: [256, 256] }, opts));
|
|
56
|
+
let filename = file;
|
|
57
|
+
if (opts.debug === true) {
|
|
58
|
+
// get extension
|
|
59
|
+
const extension = filename.split('.').pop();
|
|
60
|
+
// remove extension
|
|
61
|
+
filename = filename.split('.').slice(0, -1).join('.');
|
|
62
|
+
// add new extension
|
|
63
|
+
filename = filename + ".debug." + extension;
|
|
64
|
+
}
|
|
65
|
+
io.write(filename, document);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// basic idea:
|
|
69
|
+
// 1. iterate all images that have an extension marking them to be progressively loaded
|
|
70
|
+
// 2. for each image, create a new image that is a downsampled version of the original and create a new gltf that contains the high res texture
|
|
71
|
+
// 3. add extension to low res image containing info about where to find the high res texture
|
|
72
|
+
export function createTransform(name, fn) {
|
|
73
|
+
Object.defineProperty(fn, 'name', { value: name });
|
|
74
|
+
return fn;
|
|
75
|
+
}
|
|
76
|
+
const NAME = 'textureResize';
|
|
77
|
+
export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DEFAULTS, opts) {
|
|
78
|
+
console.log("Make progressive: " + filePath);
|
|
79
|
+
const startTime = Date.now();
|
|
80
|
+
const options = { ...TEXTURE_RESIZE_DEFAULTS, ..._options };
|
|
81
|
+
const directory = path.dirname(filePath);
|
|
82
|
+
// const fileNameWithoutExtension = path.basename(filePath, path.extname(filePath));
|
|
83
|
+
const textureLodBasePath = directory + "/";
|
|
84
|
+
return createTransform(NAME, async (doc) => {
|
|
85
|
+
const logger = doc.getLogger();
|
|
86
|
+
let index = 0;
|
|
87
|
+
for (const texture of doc.getRoot().listTextures()) {
|
|
88
|
+
const textureIndex = index++;
|
|
89
|
+
const name = texture.getName();
|
|
90
|
+
const uri = texture.getURI();
|
|
91
|
+
const match = !options.pattern || options.pattern.test(name) || options.pattern.test(uri);
|
|
92
|
+
if (!match) {
|
|
93
|
+
logger.debug(`${NAME}: Skipping, excluded by "pattern" parameter.`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (texture.getMimeType() !== 'image/png' && texture.getMimeType() !== 'image/jpeg') {
|
|
97
|
+
logger.warn(`${NAME}: Skipping, unsupported texture type "${texture.getMimeType()}".`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const slots = listTextureSlots(texture);
|
|
101
|
+
if (options.slots && !slots.some((slot) => options.slots?.test(slot))) {
|
|
102
|
+
logger.debug(`${NAME}: Skipping, [${slots.join(', ')}] excluded by "slots" parameter.`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const settings = texture.getExtension(NEEDLE_progressive_texture_settings.EXTENSION_NAME);
|
|
106
|
+
if (!settings) {
|
|
107
|
+
logger.warn(`${NAME}: Skipping, no settings found`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!settings.isValid) {
|
|
111
|
+
logger.warn(`${NAME}: Skipping, settings are invalid`);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
options.size[0] = settings.maxSize;
|
|
115
|
+
options.size[1] = settings.maxSize;
|
|
116
|
+
const existingExtension = texture.getExtension(NEEDLE_PROGRESSIVE_TEXTURE_EXTENSION_NAME);
|
|
117
|
+
if (existingExtension !== undefined && existingExtension !== null) {
|
|
118
|
+
logger.debug(`${NAME}: Skipping, texture is already progressive`);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const progressiveExt = doc.createExtension(NEEDLE_progressive);
|
|
122
|
+
if (!progressiveExt || !progressiveExt.create) {
|
|
123
|
+
logger.debug(`${NAME}: Skipping because progressive extension is not available.`);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const textureGuid = settings.guid?.length ? settings.guid : generateGuid();
|
|
127
|
+
const outPath = createTextureLod(textureLodBasePath, 0, texture, slots, textureGuid, textureIndex);
|
|
128
|
+
if (!outPath) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const relName = path.relative(directory, outPath);
|
|
132
|
+
// TODO: this path should be added to the NEEDLE dependencies extension
|
|
133
|
+
texture.setExtension(NEEDLE_PROGRESSIVE_TEXTURE_EXTENSION_NAME, progressiveExt.create({
|
|
134
|
+
uri: relName,
|
|
135
|
+
guid: textureGuid,
|
|
136
|
+
level: 0,
|
|
137
|
+
}));
|
|
138
|
+
const key = getKey(texture);
|
|
139
|
+
if (opts.useCache) {
|
|
140
|
+
const cached = tryGetFromCache(key);
|
|
141
|
+
if (cached) {
|
|
142
|
+
logger.debug(`Progressive image ${index}: using cached preview version (size: ${options.size}))`);
|
|
143
|
+
texture.setImage(cached);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
await resizeTexture(options, texture, logger, uri || name, slots);
|
|
148
|
+
addToCache(key, texture.getImage());
|
|
149
|
+
}
|
|
150
|
+
logger.debug(`Progressive transform finished in ${((Date.now() - startTime) / 1000).toFixed(1)} sec.`);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function createTextureLod(basePath, level, texture, slots, guid, textureIndex) {
|
|
154
|
+
// const imageBytes = texture.getImage();
|
|
155
|
+
// if (imageBytes) {
|
|
156
|
+
// // write bytes
|
|
157
|
+
// const outPath = basePath + guid + "_" + level + ".png";
|
|
158
|
+
// writeFileSync(outPath, imageBytes);
|
|
159
|
+
// return outPath;
|
|
160
|
+
// }
|
|
161
|
+
const newDoc = new Document();
|
|
162
|
+
newDoc.createScene();
|
|
163
|
+
newDoc.createBuffer();
|
|
164
|
+
// add texture
|
|
165
|
+
const newTexture = newDoc.createTexture();
|
|
166
|
+
const img = texture.getImage();
|
|
167
|
+
newTexture.setImage(img);
|
|
168
|
+
newTexture.setURI(texture.getURI());
|
|
169
|
+
newTexture.setMimeType(texture.getMimeType());
|
|
170
|
+
// create material adding the texture to the same slot
|
|
171
|
+
for (const slot of slots) {
|
|
172
|
+
const newMaterial = newDoc.createMaterial();
|
|
173
|
+
switch (slot) {
|
|
174
|
+
case "baseColorTexture":
|
|
175
|
+
newMaterial.setBaseColorTexture(newTexture);
|
|
176
|
+
break;
|
|
177
|
+
case "metallicRoughnessTexture":
|
|
178
|
+
newMaterial.setMetallicRoughnessTexture(newTexture);
|
|
179
|
+
break;
|
|
180
|
+
case "normalTexture":
|
|
181
|
+
newMaterial.setNormalTexture(newTexture);
|
|
182
|
+
break;
|
|
183
|
+
case "occlusionTexture":
|
|
184
|
+
newMaterial.setOcclusionTexture(newTexture);
|
|
185
|
+
break;
|
|
186
|
+
case "emissiveTexture":
|
|
187
|
+
newMaterial.setEmissiveTexture(newTexture);
|
|
188
|
+
break;
|
|
189
|
+
default:
|
|
190
|
+
console.warn("WARN: Unknown texture slot: " + slot + " for texture " + textureIndex + " → can not load progressively");
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const compressionSettings = texture.getExtension(NEEDLE_compression_texture.EXTENSION_NAME);
|
|
195
|
+
if (compressionSettings !== undefined && compressionSettings !== null && compressionSettings.getExtensionDefinition) {
|
|
196
|
+
console.log("> Pass compression extension to new texture");
|
|
197
|
+
const newExt = newDoc.createExtension(NEEDLE_compression_texture);
|
|
198
|
+
const def = compressionSettings.getExtensionDefinition();
|
|
199
|
+
const newProp = newExt.create(def);
|
|
200
|
+
newTexture.setExtension(NEEDLE_compression_texture.EXTENSION_NAME, newProp);
|
|
201
|
+
}
|
|
202
|
+
// add progressive texture extension
|
|
203
|
+
const progressiveExt = newDoc.createExtension(NEEDLE_progressive);
|
|
204
|
+
newTexture.setExtension(NEEDLE_PROGRESSIVE_TEXTURE_EXTENSION_NAME, progressiveExt.create({
|
|
205
|
+
guid: guid,
|
|
206
|
+
}));
|
|
207
|
+
const newIo = new NodeIO();
|
|
208
|
+
registerExtensions(newIo);
|
|
209
|
+
const ext = ".glb";
|
|
210
|
+
const imageName = "image_" + level + "_" + guid;
|
|
211
|
+
const outName = basePath + imageName + ext;
|
|
212
|
+
newIo.write(outName, newDoc);
|
|
213
|
+
return outName;
|
|
214
|
+
}
|
|
215
|
+
/** Resampling filter methods. LANCZOS3 is sharper, LANCZOS2 is smoother. */
|
|
216
|
+
export var TextureResizeFilter;
|
|
217
|
+
(function (TextureResizeFilter) {
|
|
218
|
+
/** Lanczos3 (sharp) */
|
|
219
|
+
TextureResizeFilter["LANCZOS3"] = "lanczos3";
|
|
220
|
+
/** Lanczos2 (smooth) */
|
|
221
|
+
TextureResizeFilter["LANCZOS2"] = "lanczos2";
|
|
222
|
+
})(TextureResizeFilter || (TextureResizeFilter = {}));
|
|
223
|
+
export const TEXTURE_RESIZE_DEFAULTS = {
|
|
224
|
+
size: [2048, 2048],
|
|
225
|
+
filter: TextureResizeFilter.LANCZOS3,
|
|
226
|
+
pattern: null,
|
|
227
|
+
slots: null,
|
|
228
|
+
};
|
|
229
|
+
export async function resizeTextureToMax(texture, maxSize, logger) {
|
|
230
|
+
const options = { ...TEXTURE_RESIZE_DEFAULTS };
|
|
231
|
+
options.size = [maxSize, maxSize];
|
|
232
|
+
await resizeTexture(options, texture, logger);
|
|
233
|
+
}
|
|
234
|
+
async function resizeTexture(options, texture, logger, uri, slots) {
|
|
235
|
+
const [maxWidth, maxHeight] = options.size;
|
|
236
|
+
const [srcWidth, srcHeight] = texture.getSize();
|
|
237
|
+
if (srcWidth <= maxWidth && srcHeight <= maxHeight) {
|
|
238
|
+
logger.debug(`${NAME}: Skipping, not within size range.`);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (maxWidth <= 0 || maxHeight <= 0) {
|
|
242
|
+
logger.debug(`${uri}: Skipping, invalid size range: ${maxWidth}x${maxHeight}`);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
let dstWidth = srcWidth;
|
|
246
|
+
let dstHeight = srcHeight;
|
|
247
|
+
if (dstWidth > maxWidth) {
|
|
248
|
+
dstHeight = Math.floor(dstHeight * (maxWidth / dstWidth));
|
|
249
|
+
dstWidth = maxWidth;
|
|
250
|
+
}
|
|
251
|
+
if (dstHeight > maxHeight) {
|
|
252
|
+
dstWidth = Math.floor(dstWidth * (maxHeight / dstHeight));
|
|
253
|
+
dstHeight = maxHeight;
|
|
254
|
+
}
|
|
255
|
+
// https://sharp.pixelplumbing.com/api-resize
|
|
256
|
+
const buffer = await sharp(texture.getImage())
|
|
257
|
+
.resize(dstWidth, dstHeight, {
|
|
258
|
+
fit: 'inside',
|
|
259
|
+
kernel: "lanczos3",
|
|
260
|
+
withoutEnlargement: true,
|
|
261
|
+
})
|
|
262
|
+
.toBuffer();
|
|
263
|
+
texture.setImage(BufferUtils.toView(buffer));
|
|
264
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Primitive } from "@gltf-transform/core";
|
|
2
|
+
import { simplifyPrimitive, weldPrimitive } from "@gltf-transform/functions";
|
|
3
|
+
import { MeshoptSimplifier } from "meshoptimizer";
|
|
4
|
+
import { NEEDLE_mesh_compression } from "../extensions/index.js";
|
|
5
|
+
const NAME = 'needle_simplify';
|
|
6
|
+
export const needle_mesh_transform = function (options) {
|
|
7
|
+
const simplifier = MeshoptSimplifier;
|
|
8
|
+
if (!simplifier) {
|
|
9
|
+
throw new Error(`${NAME}: simplifier dependency required — install "meshoptimizer".`);
|
|
10
|
+
}
|
|
11
|
+
return (document, _context) => {
|
|
12
|
+
const logger = document.getLogger();
|
|
13
|
+
options = { ...options, simplifier: simplifier };
|
|
14
|
+
const weldOptions = {
|
|
15
|
+
tolerance: 0.00000000001,
|
|
16
|
+
overwrite: true
|
|
17
|
+
};
|
|
18
|
+
for (const mesh of document.getRoot().listMeshes()) {
|
|
19
|
+
for (const prim of mesh.listPrimitives()) {
|
|
20
|
+
// const key = getKey(prim);
|
|
21
|
+
// const cached = tryGetFromCache(key);
|
|
22
|
+
// if (cached) {
|
|
23
|
+
// logger.debug(`${NAME}: Skipping primitive of mesh "${mesh.getName()}": Already cached.`);
|
|
24
|
+
// continue;
|
|
25
|
+
// }
|
|
26
|
+
// updatePrimitive(prim, simplifier, null, prim.getIndices());
|
|
27
|
+
// continue;
|
|
28
|
+
const ext = prim.getExtension(NEEDLE_mesh_compression.EXTENSION_NAME);
|
|
29
|
+
if (!ext) {
|
|
30
|
+
// logger.warn("Skipping primitive of mesh " + mesh.getName() + ": No NEEDLE_compression_mesh extension.");
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
let meshOptions = { ...options };
|
|
34
|
+
if (ext.model) {
|
|
35
|
+
const model = ext.model;
|
|
36
|
+
if (!model.useSimplifier) {
|
|
37
|
+
logger.debug(`${NAME}: Skipping primitive of mesh "${mesh.getName()}": useSimplifier is false.`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (model.error >= 0)
|
|
41
|
+
meshOptions.error = model.error;
|
|
42
|
+
if (model.ratio >= 0)
|
|
43
|
+
meshOptions.ratio = model.ratio;
|
|
44
|
+
meshOptions.lockBorder = model.lockBorder;
|
|
45
|
+
}
|
|
46
|
+
if (prim.getMode() !== Primitive.Mode.TRIANGLES) {
|
|
47
|
+
logger.warn(`${NAME}: Skipping primitive of mesh "${mesh.getName()}": Requires TRIANGLES draw mode.`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
logger.debug(`${NAME}: Welding primitive of mesh "${mesh.getName()}" → tolerance: ${weldOptions.tolerance}`);
|
|
51
|
+
weldPrimitive(document, prim, weldOptions);
|
|
52
|
+
logger.debug(`${NAME}: Simplifying primitive of mesh "${mesh.getName()}" → ratio: ${meshOptions.ratio}, error: ${meshOptions.error}, lockBorder: ${meshOptions.lockBorder}`);
|
|
53
|
+
simplifyPrimitive(document, prim, meshOptions);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
// function updatePrimitive(prim: Primitive, simplifier: any, positionArray, dstIndicesArray) {
|
|
59
|
+
// const position = prim.getAttribute('POSITION')!;
|
|
60
|
+
// const srcIndices = prim.getIndices()!;
|
|
61
|
+
// const srcVertexCount = position.getCount();
|
|
62
|
+
// const [remap, unique] = simplifier.compactMesh(dstIndicesArray);
|
|
63
|
+
// // logger.debug(`${NAME}: ${formatDeltaOp(position.getCount(), unique)} vertices, error: ${error.toFixed(4)}.`);
|
|
64
|
+
// // (3) Write vertex attributes.
|
|
65
|
+
// for (const srcAttribute of deepListAttributes(prim)) {
|
|
66
|
+
// const dstAttribute = srcAttribute.clone();
|
|
67
|
+
// remapAttribute(dstAttribute, remap, unique);
|
|
68
|
+
// deepSwapAttribute(prim, srcAttribute, dstAttribute);
|
|
69
|
+
// if (srcAttribute.listParents().length === 1) srcAttribute.dispose();
|
|
70
|
+
// }
|
|
71
|
+
// // (4) Write indices.
|
|
72
|
+
// const dstIndices = srcIndices.clone();
|
|
73
|
+
// dstIndices.setArray(srcVertexCount <= 65534 ? new Uint16Array(dstIndicesArray) : dstIndicesArray);
|
|
74
|
+
// prim.setIndices(dstIndices);
|
|
75
|
+
// if (srcIndices.listParents().length === 1) srcIndices.dispose();
|
|
76
|
+
// }
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Texture } from "@gltf-transform/core";
|
|
2
|
+
import { Transform } from '@gltf-transform/core';
|
|
3
|
+
import { NeedleTransformContext } from "./needle_common.js";
|
|
4
|
+
export declare function resizeTextureIfNecessary(texture: Texture): Promise<void>;
|
|
5
|
+
export declare const needle_texture_transform: (options: NeedleTransformContext) => Transform;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { resizeTextureToMax } from "./needle_progressive.js";
|
|
2
|
+
import { NEEDLE_compression_texture } from '../extensions/index.js';
|
|
3
|
+
import { addToCache, getKey, tryGetFromCache } from "../cache/index.js";
|
|
4
|
+
import { NeedleWebPContext } from "./needle_webp.js";
|
|
5
|
+
import { NeedleToktxContext } from "./needle_toktx.js";
|
|
6
|
+
import { createTransform } from "@gltf-transform/functions";
|
|
7
|
+
export async function resizeTextureIfNecessary(texture) {
|
|
8
|
+
const ext = texture.getExtension(NEEDLE_compression_texture.EXTENSION_NAME);
|
|
9
|
+
if (ext?.model) {
|
|
10
|
+
const mimeType = texture.getMimeType();
|
|
11
|
+
const isSupported = mimeType === "image/jpeg" || mimeType === "image/png";
|
|
12
|
+
if (isSupported) {
|
|
13
|
+
const settings = ext.model;
|
|
14
|
+
const size = texture.getSize();
|
|
15
|
+
if (size) {
|
|
16
|
+
if (settings.maxSize > 4 && (size[0] > settings.maxSize || size[1] > settings.maxSize)) {
|
|
17
|
+
console.log(`> Will resize texture ${texture.getName()} from ${size[0]}x${size[1]} to ${settings.maxSize}x${settings.maxSize}`);
|
|
18
|
+
await resizeTextureToMax(texture, settings.maxSize, console);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
export const needle_texture_transform = function (options) {
|
|
26
|
+
const webPContext = new NeedleWebPContext({});
|
|
27
|
+
const toktxContext = new NeedleToktxContext({
|
|
28
|
+
jobs: undefined, // default to number of cores
|
|
29
|
+
});
|
|
30
|
+
const context = {
|
|
31
|
+
...options,
|
|
32
|
+
webPContext,
|
|
33
|
+
toktxContext
|
|
34
|
+
};
|
|
35
|
+
return createTransform("needle_texture_transform", async (document) => {
|
|
36
|
+
await webPContext.prepare(document);
|
|
37
|
+
if (context.noToktx !== true)
|
|
38
|
+
await toktxContext.prepare(document);
|
|
39
|
+
try {
|
|
40
|
+
const logger = document.getLogger();
|
|
41
|
+
const textures = document.getRoot().listTextures();
|
|
42
|
+
await Promise.all(textures.map((texture, textureIndex) => {
|
|
43
|
+
return processTexture(texture, textureIndex, document, logger, context);
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
await webPContext.finalize(document);
|
|
48
|
+
if (context.noToktx !== true)
|
|
49
|
+
await toktxContext.finalize(document);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
async function processTexture(texture, index, document, logger, context) {
|
|
54
|
+
const mimeType = texture.getMimeType();
|
|
55
|
+
const isSupported = mimeType === "image/jpeg" || mimeType === "image/png" || mimeType === "image/webp";
|
|
56
|
+
// dont try to transform EXR or already processed textures
|
|
57
|
+
if (!isSupported)
|
|
58
|
+
return;
|
|
59
|
+
const ext = texture.getExtension(NEEDLE_compression_texture.EXTENSION_NAME);
|
|
60
|
+
const compressionSettings = ext?.model;
|
|
61
|
+
const key = context.useCache ? getKey(texture) : undefined;
|
|
62
|
+
let cached = context.useCache && key ? tryGetFromCache(key) : undefined;
|
|
63
|
+
if (cached) {
|
|
64
|
+
logger.debug(`> Using cached texture ${texture.getName()} [${index}] – ${(cached.length / 1024).toFixed(0)} KB`);
|
|
65
|
+
if (cached.length <= 0) {
|
|
66
|
+
logger.warn(`WARN: Cached texture ${texture.getName()} [${index}] is empty. Will reprocess.`);
|
|
67
|
+
cached = undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
let successFullyProcessed = false;
|
|
71
|
+
const canRunToktx = context.noToktx !== true;
|
|
72
|
+
if (compressionSettings && compressionSettings.mode !== "auto") {
|
|
73
|
+
switch (compressionSettings.mode) {
|
|
74
|
+
case "webp":
|
|
75
|
+
if (cached) {
|
|
76
|
+
texture.setImage(cached).setMimeType("image/webp");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
await resizeTextureIfNecessary(texture);
|
|
80
|
+
successFullyProcessed = await context.webPContext.process(index, texture, compressionSettings);
|
|
81
|
+
break;
|
|
82
|
+
case "ETC1S":
|
|
83
|
+
if (cached) {
|
|
84
|
+
texture.setImage(cached).setMimeType("image/ktx2");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
await resizeTextureIfNecessary(texture);
|
|
88
|
+
successFullyProcessed = await context.toktxContext.process(index, texture, compressionSettings);
|
|
89
|
+
break;
|
|
90
|
+
case "UASTC":
|
|
91
|
+
if (cached) {
|
|
92
|
+
texture.setImage(cached).setMimeType("image/ktx2");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
await resizeTextureIfNecessary(texture);
|
|
96
|
+
successFullyProcessed = await context.toktxContext.process(index, texture, compressionSettings);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
// If we have a texture cached we can use it
|
|
102
|
+
// the default compression is toktx so we can safely use that for now
|
|
103
|
+
if (cached) {
|
|
104
|
+
texture.setImage(cached).setMimeType("image/ktx2");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
await resizeTextureIfNecessary(texture);
|
|
108
|
+
if (canRunToktx) {
|
|
109
|
+
// if no specific compression settings are provided we just use the toktx default settings
|
|
110
|
+
// if we ever change what is used for default compression we need to teach the cache to save the mime type
|
|
111
|
+
await context.toktxContext.process(index, texture, null);
|
|
112
|
+
successFullyProcessed = true;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
console.log("toktx is disallowed. Skipping texture compression.");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (context.useCache && successFullyProcessed && key) {
|
|
119
|
+
const data = texture.getImage();
|
|
120
|
+
if (data)
|
|
121
|
+
addToCache(key, data);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
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 } from "./needle_common.js";
|
|
5
|
+
export interface NeedleToktxOptions {
|
|
6
|
+
jobs?: number;
|
|
7
|
+
toktxPath?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class NeedleToktxContext implements INeedleTextureTransformContext {
|
|
10
|
+
limit: any;
|
|
11
|
+
basisuExtension: KHRTextureBasisu;
|
|
12
|
+
version: string;
|
|
13
|
+
tmpPathBase: string;
|
|
14
|
+
document: Document;
|
|
15
|
+
env: undefined | {
|
|
16
|
+
PATH: string;
|
|
17
|
+
};
|
|
18
|
+
numTextures: number;
|
|
19
|
+
private _prepareFailed;
|
|
20
|
+
constructor(options: NeedleToktxOptions);
|
|
21
|
+
prepare(document: Document): Promise<void>;
|
|
22
|
+
process(index: number, texture: Texture, settings: NEEDLE_compression_texture_schema | null): Promise<boolean>;
|
|
23
|
+
finalize(document: Document): Promise<void>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { FileUtils, ImageUtils, uuid } from "@gltf-transform/core";
|
|
2
|
+
import { KHRTextureBasisu } from "@gltf-transform/extensions";
|
|
3
|
+
import { checkKTXSoftware, createParams, ETC1S_DEFAULTS, Mode, UASTC_DEFAULTS } from "./toktx.js";
|
|
4
|
+
import { existsSync, mkdirSync } from "fs";
|
|
5
|
+
import { listTextureSlots, getTextureChannelMask } from "@gltf-transform/functions";
|
|
6
|
+
import { spawn, waitExit } from './util.js';
|
|
7
|
+
import fs from 'fs/promises';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
import tmp from 'tmp';
|
|
10
|
+
import pLimit from 'p-limit';
|
|
11
|
+
const NUM_CPUS = os.cpus().length || 1; // microsoft/vscode#112122
|
|
12
|
+
export class NeedleToktxContext {
|
|
13
|
+
limit;
|
|
14
|
+
basisuExtension;
|
|
15
|
+
version;
|
|
16
|
+
tmpPathBase;
|
|
17
|
+
document;
|
|
18
|
+
env;
|
|
19
|
+
numTextures;
|
|
20
|
+
_prepareFailed = false;
|
|
21
|
+
constructor(options) {
|
|
22
|
+
this.limit = pLimit(options.jobs || NUM_CPUS);
|
|
23
|
+
// allow overriding toktx installation location / adding additional search paths
|
|
24
|
+
const isUsingWindows = process.platform === 'win32';
|
|
25
|
+
this.env = {
|
|
26
|
+
PATH: process.env.PATH ?? ""
|
|
27
|
+
};
|
|
28
|
+
if (options?.toktxPath?.length && this.env) {
|
|
29
|
+
this.env.PATH = `${options.toktxPath};${this.env.PATH}`;
|
|
30
|
+
}
|
|
31
|
+
// Use global env variable
|
|
32
|
+
if (process.env.NEEDLE_TOKTX?.length) {
|
|
33
|
+
if (!existsSync(process.env.NEEDLE_TOKTX)) {
|
|
34
|
+
console.log("WARN: NEEDLE_TOKTX is set to a non-existing path: \"" + process.env.NEEDLE_TOKTX + "\"");
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
console.log("INFO: Adding NEEDLE_TOKTX to PATH: \"" + process.env.NEEDLE_TOKTX + "\"");
|
|
38
|
+
this.env.PATH = `${process.env.NEEDLE_TOKTX};${this.env.PATH}`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
console.log("INFO: Environment variable 'NEEDLE_TOKTX' not set. Use 'NEEDLE_TOKTX' to specify the directory of the toktx installation (path/to/bin)");
|
|
43
|
+
}
|
|
44
|
+
// fallback default installation directory:
|
|
45
|
+
if (isUsingWindows) {
|
|
46
|
+
const defaultPath = "C:\\Program Files\\KTX-Software\\bin";
|
|
47
|
+
if (existsSync(defaultPath))
|
|
48
|
+
this.env.PATH = `${this.env.PATH};${defaultPath}`;
|
|
49
|
+
}
|
|
50
|
+
// append default system paths
|
|
51
|
+
if (isUsingWindows && process.env.Path?.length)
|
|
52
|
+
this.env.PATH = `${this.env.PATH};${process.env.Path}`;
|
|
53
|
+
if (process.argv?.includes("debug")) {
|
|
54
|
+
const ktxPaths = this.env.PATH.split(";").filter(p => p.toLocaleLowerCase().includes("ktx"));
|
|
55
|
+
console.log("INFO: Found these paths including ktx:\n\t" + ktxPaths.join("\n\t") + "");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async prepare(document) {
|
|
59
|
+
try {
|
|
60
|
+
this.document = document;
|
|
61
|
+
const logger = document.getLogger();
|
|
62
|
+
this.version = await checkKTXSoftware(logger, this.env);
|
|
63
|
+
this.basisuExtension = document.createExtension(KHRTextureBasisu).setRequired(true);
|
|
64
|
+
const tmpDir = `${tmp.tmpdir}/gltf-transform`;
|
|
65
|
+
if (!(existsSync(tmpDir)))
|
|
66
|
+
mkdirSync(tmpDir);
|
|
67
|
+
this.tmpPathBase = tmpDir + "/" + uuid();
|
|
68
|
+
this.numTextures = document.getRoot().listTextures().length;
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
console.error(e);
|
|
72
|
+
this._prepareFailed = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async process(index, texture, settings) {
|
|
76
|
+
const task = async () => {
|
|
77
|
+
if (this._prepareFailed)
|
|
78
|
+
return false;
|
|
79
|
+
const doc = this.document;
|
|
80
|
+
const logger = doc.getLogger();
|
|
81
|
+
const slots = listTextureSlots(texture);
|
|
82
|
+
const channels = getTextureChannelMask(texture);
|
|
83
|
+
const textureLabel = texture.getURI() ||
|
|
84
|
+
texture.getName() ||
|
|
85
|
+
`${index + 1}/${doc.getRoot().listTextures().length}`;
|
|
86
|
+
const prefix = `toktx:texture(${textureLabel})`;
|
|
87
|
+
if (slots.length > 0)
|
|
88
|
+
logger.debug(`${prefix}: Slots → [${slots.join(', ')}]`);
|
|
89
|
+
const image = texture.getImage();
|
|
90
|
+
const size = texture.getSize();
|
|
91
|
+
if (!image || !size) {
|
|
92
|
+
logger.warn(`${prefix}: Skipping, unreadable texture.`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const extension = texture.getURI()
|
|
96
|
+
? FileUtils.extension(texture.getURI())
|
|
97
|
+
: ImageUtils.mimeTypeToExtension(texture.getMimeType());
|
|
98
|
+
const tmpName = this.tmpPathBase + "_" + index;
|
|
99
|
+
const inPath = tmpName + '.' + extension;
|
|
100
|
+
const outPath = tmpName + '.ktx2';
|
|
101
|
+
// const inBytes = image.byteLength;
|
|
102
|
+
await fs.writeFile(inPath, Buffer.from(image));
|
|
103
|
+
//@ts-ignore
|
|
104
|
+
const defaults = (!settings || settings.mode === "ETC1S") ? ETC1S_DEFAULTS : UASTC_DEFAULTS;
|
|
105
|
+
defaults.mode = (!settings || settings.mode === "ETC1S") ? Mode.ETC1S : Mode.UASTC;
|
|
106
|
+
//@ts-ignore
|
|
107
|
+
const toktxParams = createParams(slots, channels, size, logger, this.numTextures, { ...defaults }, this.version);
|
|
108
|
+
const params = [
|
|
109
|
+
...toktxParams,
|
|
110
|
+
outPath,
|
|
111
|
+
inPath,
|
|
112
|
+
];
|
|
113
|
+
logger.debug(`${prefix}: Spawning → toktx ${params.join(' ')}`);
|
|
114
|
+
try {
|
|
115
|
+
// COMPRESS: Run `toktx` CLI tool.
|
|
116
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
117
|
+
//@ts-ignore
|
|
118
|
+
const [status, stdout, stderr] = await waitExit(spawn('toktx', params, { env: this.env }));
|
|
119
|
+
if (status !== 0) {
|
|
120
|
+
logger.error(`${prefix}: Failed → \n\n${stderr.toString()}`);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const result = await fs.readFile(outPath);
|
|
124
|
+
texture.setImage(result).setMimeType('image/ktx2');
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
console.error("Error running toktx", err);
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
};
|
|
133
|
+
return this.limit(task);
|
|
134
|
+
}
|
|
135
|
+
async finalize(document) {
|
|
136
|
+
const usesKTX2 = document
|
|
137
|
+
.getRoot()
|
|
138
|
+
.listTextures()
|
|
139
|
+
.some((t) => t.getMimeType() === 'image/ktx2');
|
|
140
|
+
if (!usesKTX2) {
|
|
141
|
+
this.basisuExtension?.dispose();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Document, ILogger, Texture } from '@gltf-transform/core';
|
|
2
|
+
import { NEEDLE_compression_texture_schema } from '../extensions/index.js';
|
|
3
|
+
import { INeedleTextureTransformContext } from './needle_common.js';
|
|
4
|
+
export interface NeedleWebPOptions {
|
|
5
|
+
}
|
|
6
|
+
export declare class NeedleWebPContext implements INeedleTextureTransformContext {
|
|
7
|
+
options: NeedleWebPOptions;
|
|
8
|
+
logger: ILogger;
|
|
9
|
+
constructor(options: NeedleWebPOptions);
|
|
10
|
+
prepare(document: Document): Promise<void>;
|
|
11
|
+
process(index: number, texture: Texture, settings: NEEDLE_compression_texture_schema): Promise<boolean>;
|
|
12
|
+
finalize(document: Document): Promise<void>;
|
|
13
|
+
}
|