@needle-tools/gltf-build-pipeline 1.5.0-alpha.5 → 1.5.0-alpha.7
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 +8 -0
- package/dist/extensions/NEEDLE_compression_texture/NEEDLE_compression_texture.d.ts +1 -1
- package/dist/transforms/needle_common.d.ts +2 -0
- package/dist/transforms/needle_progressive.js +13 -4
- package/dist/transforms/needle_texture_transform.js +39 -5
- package/dist/transforms/needle_toktx.d.ts +1 -1
- package/dist/transforms/needle_toktx.js +31 -5
- package/dist/utils/compression_utils.d.ts +2 -0
- package/dist/utils/compression_utils.js +6 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/pack-gltf.mjs +4 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ All notable changes to this package will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
5
5
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [1.5.0-alpha.7] - 2023-05-16
|
|
8
|
+
- fix: external resources that are KTX2 compressed are now saved with a `.ktx2` extension
|
|
9
|
+
- change: toktx now uses compression "auto" mode to select uastc or etc1s
|
|
10
|
+
- change: toktx now checks the slots and increases quality for `metallicRoughnessTexture` and `normalTexture` but keeps using ETC1S for smaller file size
|
|
11
|
+
|
|
12
|
+
## [1.5.0-alpha.6] - 2023-05-15
|
|
13
|
+
- fix: If not further specified then automatically use UASTC for textures that are in either `normalTexture` or `metallicRoughnessTexture` slots
|
|
14
|
+
|
|
7
15
|
## [1.5.0-alpha.5] - 2023-05-04
|
|
8
16
|
- fix: NEEDLE_opaque should handle if VRM texture index is negative
|
|
9
17
|
- change: exit with code 1 if any file packing fails
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Extension, ExtensionProperty, PropertyType, ReaderContext, WriterContext } from "@gltf-transform/core";
|
|
2
2
|
export declare const EXTENSION_NAME = "NEEDLE_compression_texture";
|
|
3
3
|
export declare interface NEEDLE_compression_texture_schema {
|
|
4
|
-
mode:
|
|
4
|
+
mode: "none" | "auto" | "webp" | "UASTC" | "ETC1S";
|
|
5
5
|
slot: string;
|
|
6
6
|
/** quality in range of 0 - 1 */
|
|
7
7
|
quality: number;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Document, Texture } from '@gltf-transform/core';
|
|
2
2
|
import { NEEDLE_compression_texture_schema } from '../extensions/index.js';
|
|
3
3
|
export interface NeedleTransformContext {
|
|
4
|
+
/** full path to the file being processed */
|
|
5
|
+
file: string;
|
|
4
6
|
debug?: boolean;
|
|
5
7
|
useCache: boolean;
|
|
6
8
|
noToktx: boolean;
|
|
@@ -7,13 +7,13 @@ import path, { basename } from 'path';
|
|
|
7
7
|
import { existsSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
|
|
8
8
|
import { addMetaToCache, addToCache, getHash, getKeyWithHash, tryGetFromCache, tryGetMetaFromCache } from '../cache/index.js';
|
|
9
9
|
import sharp from "sharp";
|
|
10
|
-
import { foreachGLTF, generateGuid, writeNodeIO } from '../utils/index.js';
|
|
10
|
+
import { detectTextureCompressionMode, foreachGLTF, generateGuid, writeNodeIO } from '../utils/index.js';
|
|
11
11
|
import { needle_asset, writeNeedleAsset } from './needle_asset.js';
|
|
12
12
|
import { MeshoptDecoder, MeshoptSimplifier } from 'meshoptimizer';
|
|
13
13
|
import { compressPrimitive } from './needle_mesh_transform.js';
|
|
14
14
|
import { calculateMeshDensity, getMeshInformation } from "../utils/mesh.js";
|
|
15
15
|
export async function make_progressive(opts) {
|
|
16
|
-
console.log(
|
|
16
|
+
console.log(`INFO: Run progressive transform: ${opts.path}`);
|
|
17
17
|
const file = opts.path;
|
|
18
18
|
return foreachGLTF(file, async (f) => {
|
|
19
19
|
// Don't re-process files that are already LODs
|
|
@@ -82,6 +82,7 @@ async function onProcess(file, opts) {
|
|
|
82
82
|
}
|
|
83
83
|
else
|
|
84
84
|
console.warn("WARN: Progressive meshes are not supported for VRM files yet");
|
|
85
|
+
console.log("→ Transforming: " + file);
|
|
85
86
|
await document.transform(...transforms);
|
|
86
87
|
let filename = file;
|
|
87
88
|
if (opts.debug === true) {
|
|
@@ -93,6 +94,7 @@ async function onProcess(file, opts) {
|
|
|
93
94
|
filename = filename + ".debug." + extension;
|
|
94
95
|
}
|
|
95
96
|
await writeNodeIO(io, filename, document);
|
|
97
|
+
console.log("← Transforming done: " + filename);
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
100
|
catch (err) {
|
|
@@ -169,7 +171,7 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
|
|
|
169
171
|
continue;
|
|
170
172
|
}
|
|
171
173
|
const textureGuid = settings.guid?.length ? settings.guid : generateGuid(path + "_texture_" + index.toString());
|
|
172
|
-
const outPath = createTextureLod(doc, textureLodBasePath, 0, texture, slots, textureGuid, textureIndex);
|
|
174
|
+
const outPath = createTextureLod(doc, filePath, textureLodBasePath, 0, texture, slots, textureGuid, textureIndex);
|
|
173
175
|
if (!outPath) {
|
|
174
176
|
continue;
|
|
175
177
|
}
|
|
@@ -205,7 +207,7 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
|
|
|
205
207
|
logger.debug(`Progressive transform finished in ${((Date.now() - startTime) / 1000).toFixed(1)} sec.`);
|
|
206
208
|
});
|
|
207
209
|
}
|
|
208
|
-
function createTextureLod(doc, basePath, level, texture, slots, guid, textureIndex) {
|
|
210
|
+
function createTextureLod(doc, file, basePath, level, texture, slots, guid, textureIndex) {
|
|
209
211
|
// const imageBytes = texture.getImage();
|
|
210
212
|
// if (imageBytes) {
|
|
211
213
|
// // write bytes
|
|
@@ -256,6 +258,13 @@ function createTextureLod(doc, basePath, level, texture, slots, guid, textureInd
|
|
|
256
258
|
console.log("> Pass compression extension to new texture");
|
|
257
259
|
const newExt = newDoc.createExtension(NEEDLE_compression_texture);
|
|
258
260
|
const def = compressionSettings.getExtensionDefinition();
|
|
261
|
+
def.mode = detectTextureCompressionMode(slots);
|
|
262
|
+
def.slot = slots[0];
|
|
263
|
+
// for VRMS we just use webp
|
|
264
|
+
// if (file?.toLocaleLowerCase().endsWith(".vrm")) {
|
|
265
|
+
// def.mode = "webp";
|
|
266
|
+
// def.quality = 70;
|
|
267
|
+
// }
|
|
259
268
|
const newProp = newExt.create(def);
|
|
260
269
|
newTexture.setExtension(NEEDLE_compression_texture.EXTENSION_NAME, newProp);
|
|
261
270
|
}
|
|
@@ -5,6 +5,8 @@ import { addToCache, getKey, tryGetFromCache } from "../cache/index.js";
|
|
|
5
5
|
import { NeedleWebPContext } from "./needle_webp.js";
|
|
6
6
|
import { NeedleToktxContext } from "./needle_toktx.js";
|
|
7
7
|
import { createTransform } from "@gltf-transform/functions";
|
|
8
|
+
import { dirname } from "path";
|
|
9
|
+
import { existsSync, rmSync } from "fs";
|
|
8
10
|
export async function resizeTextureIfNecessary(texture) {
|
|
9
11
|
const ext = texture.getExtension(NEEDLE_compression_texture.EXTENSION_NAME);
|
|
10
12
|
if (ext?.model) {
|
|
@@ -96,6 +98,7 @@ async function processTexture(texture, index, document, logger, context) {
|
|
|
96
98
|
}
|
|
97
99
|
let successFullyProcessed = false;
|
|
98
100
|
const canRunToktx = context.noToktx !== true;
|
|
101
|
+
const originalUri = texture.getURI();
|
|
99
102
|
if (compressionSettings && compressionSettings.mode !== "auto") {
|
|
100
103
|
switch (compressionSettings.mode) {
|
|
101
104
|
case "webp":
|
|
@@ -125,21 +128,41 @@ async function processTexture(texture, index, document, logger, context) {
|
|
|
125
128
|
}
|
|
126
129
|
}
|
|
127
130
|
else {
|
|
131
|
+
let mimetype = "image/ktx2";
|
|
132
|
+
const isVRM = context.file?.toLowerCase().endsWith(".vrm");
|
|
133
|
+
// TODO: need to make this into a function
|
|
134
|
+
// const webpThreshold = 256; // this is the default threshold in progressive textures (needle_progressive.ts)
|
|
135
|
+
// console.log(context.file, isVRM);
|
|
136
|
+
// // 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)
|
|
137
|
+
// if (isVRM) {
|
|
138
|
+
// const size = texture.getSize();
|
|
139
|
+
// console.log(isVRM, size)
|
|
140
|
+
// if (size && size[0] <= webpThreshold && size[1] <= webpThreshold) {
|
|
141
|
+
// mimetype = "image/webp";
|
|
142
|
+
// }
|
|
143
|
+
// }
|
|
128
144
|
// If we have a texture cached we can use it
|
|
129
145
|
// the default compression is toktx so we can safely use that for now
|
|
130
146
|
if (cached) {
|
|
131
|
-
texture.setImage(cached).setMimeType(
|
|
147
|
+
texture.setImage(cached).setMimeType(mimetype);
|
|
132
148
|
return;
|
|
133
149
|
}
|
|
134
150
|
await resizeTextureIfNecessary(texture);
|
|
135
|
-
if (
|
|
151
|
+
if (mimeType === "image/webp") {
|
|
152
|
+
successFullyProcessed = await context.webPContext.process(index, texture, null);
|
|
153
|
+
}
|
|
154
|
+
else if (canRunToktx) {
|
|
136
155
|
// if no specific compression settings are provided we just use the toktx default settings
|
|
137
156
|
// if we ever change what is used for default compression we need to teach the cache to save the mime type
|
|
138
|
-
|
|
139
|
-
|
|
157
|
+
// For VRMs we use ETC1S always
|
|
158
|
+
let options = null;
|
|
159
|
+
if (isVRM) {
|
|
160
|
+
options = { mode: "ETC1S" };
|
|
161
|
+
}
|
|
162
|
+
successFullyProcessed = await context.toktxContext.process(index, texture, options);
|
|
140
163
|
}
|
|
141
164
|
else {
|
|
142
|
-
console.log("
|
|
165
|
+
console.log("Toktx not found: Skipping texture compression.");
|
|
143
166
|
}
|
|
144
167
|
}
|
|
145
168
|
if (context.useCache && successFullyProcessed && key) {
|
|
@@ -147,4 +170,15 @@ async function processTexture(texture, index, document, logger, context) {
|
|
|
147
170
|
if (data)
|
|
148
171
|
addToCache(key, data);
|
|
149
172
|
}
|
|
173
|
+
if (successFullyProcessed) {
|
|
174
|
+
const newUri = texture.getURI();
|
|
175
|
+
if (originalUri && originalUri != newUri) {
|
|
176
|
+
const fileDirectory = dirname(context.file);
|
|
177
|
+
const originalPath = fileDirectory + "/" + originalUri;
|
|
178
|
+
if (existsSync(originalPath)) {
|
|
179
|
+
console.log(`INFO: Texture moved \"${originalUri}\" → \"${newUri}\"`);
|
|
180
|
+
rmSync(originalPath);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
150
184
|
}
|
|
@@ -20,6 +20,6 @@ export declare class NeedleToktxContext implements INeedleTextureTransformContex
|
|
|
20
20
|
private _prepareFailed;
|
|
21
21
|
constructor(options: NeedleToktxOptions);
|
|
22
22
|
prepare(document: Document): Promise<boolean>;
|
|
23
|
-
process(index: number, texture: Texture, settings: NEEDLE_compression_texture_schema | null): Promise<boolean>;
|
|
23
|
+
process(index: number, texture: Texture, settings: Pick<NEEDLE_compression_texture_schema, "mode"> | null): Promise<boolean>;
|
|
24
24
|
finalize(document: Document): Promise<void>;
|
|
25
25
|
}
|
|
@@ -113,17 +113,36 @@ export class NeedleToktxContext {
|
|
|
113
113
|
const outPath = tmpName + '.ktx2';
|
|
114
114
|
// const inBytes = image.byteLength;
|
|
115
115
|
await fs.writeFile(inPath, Buffer.from(image));
|
|
116
|
+
let mode = Mode.ETC1S;
|
|
117
|
+
let quality = 128;
|
|
118
|
+
let compression = 1;
|
|
119
|
+
if (settings) {
|
|
120
|
+
if (settings.mode === "auto") {
|
|
121
|
+
const useUASTC = slots.includes("normalTexture") || slots.includes("metallicRoughnessTexture");
|
|
122
|
+
mode = useUASTC ? Mode.UASTC : Mode.ETC1S;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
mode = settings.mode === "ETC1S" ? Mode.ETC1S : Mode.UASTC;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else if (slots) {
|
|
129
|
+
const betterQuality = slots.includes("normalTexture") || slots.includes("metallicRoughnessTexture");
|
|
130
|
+
// mode = useUASTC ? Mode.UASTC : Mode.ETC1S;
|
|
131
|
+
mode = Mode.ETC1S;
|
|
132
|
+
// https://github.khronos.org/KTX-Software/ktxtools/toktx.html
|
|
133
|
+
quality = betterQuality ? 255 : 128;
|
|
134
|
+
// ETC1S / BasisLZ compression level, an encoding speed vs. quality tradeoff. Range is [0,5], default is 1. Higher values are slower but give higher quality.
|
|
135
|
+
compression = betterQuality ? 4 : 1;
|
|
136
|
+
}
|
|
137
|
+
const defaults = mode === Mode.ETC1S ? ETC1S_DEFAULTS : UASTC_DEFAULTS;
|
|
116
138
|
//@ts-ignore
|
|
117
|
-
const
|
|
118
|
-
defaults.mode = (!settings || settings.mode === "ETC1S") ? Mode.ETC1S : Mode.UASTC;
|
|
119
|
-
//@ts-ignore
|
|
120
|
-
const toktxParams = createParams(slots, channels, size, logger, this.numTextures, { ...defaults }, this.version);
|
|
139
|
+
const toktxParams = createParams(slots, channels, size, logger, this.numTextures, { ...defaults, mode, quality, compression }, this.version);
|
|
121
140
|
const params = [
|
|
122
141
|
...toktxParams,
|
|
123
142
|
outPath,
|
|
124
143
|
inPath,
|
|
125
144
|
];
|
|
126
|
-
logger.debug(`${prefix}: Spawning → toktx ${params.join(' ')}`);
|
|
145
|
+
logger.debug(`${prefix}: Spawning → toktx ${mode.toUpperCase()} ${params.join(' ')}`);
|
|
127
146
|
try {
|
|
128
147
|
// COMPRESS: Run `toktx` CLI tool.
|
|
129
148
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
@@ -135,6 +154,13 @@ export class NeedleToktxContext {
|
|
|
135
154
|
else {
|
|
136
155
|
const result = await fs.readFile(outPath);
|
|
137
156
|
texture.setImage(result).setMimeType('image/ktx2');
|
|
157
|
+
// TODO: we also need to remove the old file when the extension has changed - this should probably be done outside of this method to also support webp
|
|
158
|
+
// If this is a texture that was loaded from a file, we need to update the URI
|
|
159
|
+
if (texture.getURI()) {
|
|
160
|
+
const oldUri = texture.getURI();
|
|
161
|
+
const newUri = FileUtils.basename(oldUri) + '.ktx2';
|
|
162
|
+
texture.setURI(newUri);
|
|
163
|
+
}
|
|
138
164
|
return true;
|
|
139
165
|
}
|
|
140
166
|
}
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
package/pack-gltf.mjs
CHANGED
|
@@ -60,11 +60,9 @@ if (debug) {
|
|
|
60
60
|
|
|
61
61
|
let allowCaching = true;
|
|
62
62
|
if (args.includes("--no-caching")) {
|
|
63
|
-
console.log("Caching is disabled")
|
|
64
63
|
allowCaching = false;
|
|
65
64
|
}
|
|
66
|
-
else
|
|
67
|
-
console.log("Caching is enabled, use \"--no-caching\" to disable it");
|
|
65
|
+
else console.log("Use --no-caching to disable caching");
|
|
68
66
|
|
|
69
67
|
const noToktx = args.includes("no-toktx");
|
|
70
68
|
if (noToktx)
|
|
@@ -106,10 +104,11 @@ async function packFile(file) {
|
|
|
106
104
|
|
|
107
105
|
if (packedFiles.has(file)) return;
|
|
108
106
|
packedFiles.add(file);
|
|
109
|
-
console.log("→ Begin packing: " + file);
|
|
110
107
|
|
|
111
108
|
try {
|
|
112
109
|
const prevSizeInMB = statSync(file).size / 1024 / 1024;
|
|
110
|
+
console.log(`→ Compressing ${prevSizeInMB.toFixed(1)} MB: ${file}`);
|
|
111
|
+
|
|
113
112
|
const startTime = Date.now();
|
|
114
113
|
const io = new NodeIO()
|
|
115
114
|
.registerExtensions(ALL_EXTENSIONS)
|
|
@@ -130,6 +129,7 @@ async function packFile(file) {
|
|
|
130
129
|
await document.transform(
|
|
131
130
|
needle_asset(),
|
|
132
131
|
needle_texture_transform({
|
|
132
|
+
file,
|
|
133
133
|
useCache: allowCaching,
|
|
134
134
|
noToktx: noToktx,
|
|
135
135
|
debug: debug,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@needle-tools/gltf-build-pipeline",
|
|
3
|
-
"version": "1.5.0-alpha.
|
|
3
|
+
"version": "1.5.0-alpha.7",
|
|
4
4
|
"description": "Pipeline and tools for optimizing gltf files using gltf-transform and compression settings within glTF extensions",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"type": "module",
|