@needle-tools/gltf-build-pipeline 2.2.1-alpha.5 → 2.2.1-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 +4 -0
- package/dist/cache/cache.js +9 -7
- package/dist/cli/index.js +2 -1
- package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.js +6 -6
- package/dist/transforms/needle_progressive.js +10 -2
- package/dist/transforms/needle_texture_transform.js +7 -3
- package/dist/transforms/needle_toktx.d.ts +1 -0
- package/dist/transforms/needle_toktx.js +11 -2
- package/dist/utils/texture.d.ts +3 -0
- package/dist/utils/texture.js +6 -0
- package/dist/utils/validate.js +4 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,10 @@ 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
|
+
## [2.2.1-alpha.7] - 2023-06-19
|
|
8
|
+
- fix: WARN if image validation can not run
|
|
9
|
+
- change: compress lightmap textures with webp
|
|
10
|
+
|
|
7
11
|
## [2.2.1-alpha.5] - 2023-06-18
|
|
8
12
|
- add: `stats` now includes json and animation size info
|
|
9
13
|
- change: VRM's are compressed with meshopt again
|
package/dist/cache/cache.js
CHANGED
|
@@ -41,8 +41,9 @@ export function limitCacheSize(maxSizeMB = cacheSizeLimit) {
|
|
|
41
41
|
const dir = getCacheDirectory(true);
|
|
42
42
|
console.log(`Limit cache size to ${maxSizeMB.toFixed(2)} MB at ${dir}...`);
|
|
43
43
|
let totalSize = 0;
|
|
44
|
+
const files = new Array();
|
|
44
45
|
function recurseDirectories(dir) {
|
|
45
|
-
|
|
46
|
+
fs.readdirSync(dir).forEach(file => {
|
|
46
47
|
const path = `${dir}/${file}`;
|
|
47
48
|
const stats = statSync(path);
|
|
48
49
|
if (stats.isDirectory()) {
|
|
@@ -50,12 +51,11 @@ export function limitCacheSize(maxSizeMB = cacheSizeLimit) {
|
|
|
50
51
|
}
|
|
51
52
|
else {
|
|
52
53
|
totalSize += stats.size;
|
|
54
|
+
files.push({ path, size: stats.size });
|
|
53
55
|
}
|
|
54
|
-
return { path, size: stats.size };
|
|
55
56
|
});
|
|
56
|
-
return files;
|
|
57
57
|
}
|
|
58
|
-
|
|
58
|
+
recurseDirectories(dir);
|
|
59
59
|
const maxSizeInBytes = maxSizeMB * 1024 * 1024;
|
|
60
60
|
console.log(`Current cache size is ${(totalSize / (1024 * 1024)).toFixed(1)} MB`);
|
|
61
61
|
if (totalSize < maxSizeInBytes)
|
|
@@ -67,17 +67,19 @@ export function limitCacheSize(maxSizeMB = cacheSizeLimit) {
|
|
|
67
67
|
});
|
|
68
68
|
const targetSize = maxSizeInBytes * 0.9;
|
|
69
69
|
// remove oldest files first
|
|
70
|
+
let filesRemoved = 0;
|
|
70
71
|
while (totalSize > targetSize) {
|
|
71
72
|
const file = files.shift();
|
|
72
73
|
if (!file)
|
|
73
74
|
break;
|
|
74
|
-
if (fs.statSync(file.path).isDirectory())
|
|
75
|
+
if (fs.statSync(file.path).isDirectory()) {
|
|
75
76
|
continue;
|
|
76
|
-
|
|
77
|
+
}
|
|
78
|
+
filesRemoved++;
|
|
77
79
|
fs.rmSync(file.path);
|
|
78
80
|
totalSize -= file.size;
|
|
79
81
|
}
|
|
80
|
-
console.log(`Cache size reduced to ${(totalSize / (1024 * 1024)).toFixed(1)} MB`);
|
|
82
|
+
console.log(`Cache size reduced to ${(totalSize / (1024 * 1024)).toFixed(1)} MB (${filesRemoved} files removed)`);
|
|
81
83
|
}
|
|
82
84
|
/**
|
|
83
85
|
* Add data to the cache.
|
package/dist/cli/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { printStats, writeFileStatsToFile } from '../utils/stats.js';
|
|
|
5
5
|
import { getVersion } from '../utils/version.js';
|
|
6
6
|
import { UsecaseOptions, createConfig, getConfig } from '../config/index.js';
|
|
7
7
|
import { make_progressive } from '../transforms/needle_progressive.js';
|
|
8
|
-
import { cacheSizeLimit, clearCache } from '../cache/cache.js';
|
|
8
|
+
import { cacheSizeLimit, clearCache, limitCacheSize } from '../cache/cache.js';
|
|
9
9
|
import { statSync } from 'fs';
|
|
10
10
|
import { foreachGLTF, isDirectory } from '../utils/fileutils.js';
|
|
11
11
|
import path, { dirname } from 'path';
|
|
@@ -135,6 +135,7 @@ Each version will be compressed and written to the output directory.
|
|
|
135
135
|
}
|
|
136
136
|
});
|
|
137
137
|
printStats(stats, logger, config);
|
|
138
|
+
limitCacheSize();
|
|
138
139
|
});
|
|
139
140
|
program.run().catch(err => {
|
|
140
141
|
// Since we still invoke this package with paths when running npm scripts we need to ignore this error
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Extension, ExtensionProperty, PropertyType, TextureChannel } from '@gltf-transform/core';
|
|
2
2
|
import { getTexture, tryGetInArray, writeExtension } from '../utils.js';
|
|
3
|
+
import { isLightmap } from '../../utils/texture.js';
|
|
3
4
|
const ALL_PROPERTY_TYPES = [];
|
|
4
5
|
for (const key in PropertyType) {
|
|
5
6
|
ALL_PROPERTY_TYPES.push(PropertyType[key]);
|
|
@@ -163,11 +164,11 @@ export function createOpaqueExtension(name, types, opts) {
|
|
|
163
164
|
continue;
|
|
164
165
|
const ext = sourceArray[EXTENSION_NAME];
|
|
165
166
|
if (ext) {
|
|
166
|
-
this.lightmapWorkaround(context);
|
|
167
167
|
const root = this.document.getRoot();
|
|
168
168
|
const prop = this.createExtension();
|
|
169
169
|
this.jsonPointerHandler.read(context, root, ext, prop);
|
|
170
170
|
root.setExtension(EXTENSION_NAME, prop);
|
|
171
|
+
this.lightmapWorkaround(context, prop, this.document.getLogger());
|
|
171
172
|
prop.add(ctx => {
|
|
172
173
|
ctx.jsonDoc.json.extensions = ctx.jsonDoc.json.extensions || {};
|
|
173
174
|
ctx.jsonDoc.json.extensions[EXTENSION_NAME] = ext;
|
|
@@ -180,7 +181,7 @@ export function createOpaqueExtension(name, types, opts) {
|
|
|
180
181
|
}
|
|
181
182
|
// this is temporarely here to prevent compression on textures named Lightmap
|
|
182
183
|
// until we have better control over the compression process
|
|
183
|
-
lightmapWorkaround(context) {
|
|
184
|
+
lightmapWorkaround(context, prop, logger) {
|
|
184
185
|
const textureDefinitions = context.jsonDoc.json.textures;
|
|
185
186
|
if (!textureDefinitions)
|
|
186
187
|
return;
|
|
@@ -189,10 +190,9 @@ export function createOpaqueExtension(name, types, opts) {
|
|
|
189
190
|
const tex = textures[i];
|
|
190
191
|
const texDef = textureDefinitions[i];
|
|
191
192
|
const name = texDef?.name;
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
tex.setMimeType("image/lightmap");
|
|
193
|
+
if (isLightmap(tex) && tex.getMimeType() !== "image/exr") {
|
|
194
|
+
prop.setReference("lightmapTexture", tex, { channels: TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A });
|
|
195
|
+
logger.info(`Found lightmap texture ${name}: ${tex.getMimeType()} in ${EXTENSION_NAME}`);
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
}
|
|
@@ -7,7 +7,7 @@ import path, { basename, resolve } from 'path';
|
|
|
7
7
|
import { existsSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
|
|
8
8
|
import { addMetaToCache, addToCache, getHash, getKey, tryGetFromCache, tryGetMetaFromCache } from '../cache/index.js';
|
|
9
9
|
import sharp from "sharp";
|
|
10
|
-
import { detectTextureCompressionMode, foreachGLTF, generateGuid, validateImageMimeType, writeNodeIO } from '../utils/index.js';
|
|
10
|
+
import { detectTextureCompressionMode, foreachGLTF, generateGuid, isLightmap, setLightmapName, validateImageMimeType, 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';
|
|
@@ -193,7 +193,9 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
|
|
|
193
193
|
// generateLods ||= opts.config?.textures?.lods === true;
|
|
194
194
|
// // Ensure that texture lods are not disabled by { textures: { lods: false } }
|
|
195
195
|
// generateLods &&= opts.config?.textures?.lods !== false;
|
|
196
|
-
|
|
196
|
+
const _isLightmap = slots.includes("lightmapTexture") || isLightmap(texture);
|
|
197
|
+
// Lightmaps don't have multiple levels right now
|
|
198
|
+
if (generateLods && !_isLightmap) {
|
|
197
199
|
const LEVEL = 6;
|
|
198
200
|
let lastSize = currentSize;
|
|
199
201
|
for (let i = 1; i < LEVEL; i++) {
|
|
@@ -273,6 +275,7 @@ async function createTextureLod(args, opts) {
|
|
|
273
275
|
newTexture.setImage(img);
|
|
274
276
|
newTexture.setURI(texture.getURI());
|
|
275
277
|
newTexture.setMimeType(texture.getMimeType());
|
|
278
|
+
newTexture.setName(texture.getName() || ("LOD_" + textureIndex));
|
|
276
279
|
if (args.maxSize && args.maxSize >= 16) {
|
|
277
280
|
const currentSize = newTexture.getSize();
|
|
278
281
|
if (currentSize[0] > args.maxSize && currentSize[1] > args.maxSize) {
|
|
@@ -308,6 +311,10 @@ async function createTextureLod(args, opts) {
|
|
|
308
311
|
case "emissiveTexture":
|
|
309
312
|
newMaterial.setEmissiveTexture(newTexture);
|
|
310
313
|
break;
|
|
314
|
+
case "lightmapTexture":
|
|
315
|
+
newMaterial.setBaseColorTexture(newTexture);
|
|
316
|
+
setLightmapName(newTexture);
|
|
317
|
+
break;
|
|
311
318
|
default:
|
|
312
319
|
if (slot.startsWith("/textures/")) {
|
|
313
320
|
// Ensure the texture is using the alpha channel since we don't know what slots the original texture was used for
|
|
@@ -322,6 +329,7 @@ async function createTextureLod(args, opts) {
|
|
|
322
329
|
// Make sure the material is used
|
|
323
330
|
createMeshForMaterial(newDoc, newMaterial, slot_index++);
|
|
324
331
|
}
|
|
332
|
+
// Pass compression settings to the new texture
|
|
325
333
|
const compressionSettingsExt = texture.getExtension(NEEDLE_compression_texture.EXTENSION_NAME);
|
|
326
334
|
if (compressionSettingsExt !== undefined && compressionSettingsExt !== null && compressionSettingsExt.getExtensionDefinition) {
|
|
327
335
|
logger.info("> Pass compression extension to new texture");
|
|
@@ -4,10 +4,11 @@ import { NEEDLE_compression_texture } from '../extensions/index.js';
|
|
|
4
4
|
import { addToCache, getHash, getKey, tryGetFromCache } from "../cache/index.js";
|
|
5
5
|
import { NeedleWebPContext } from "./needle_webp.js";
|
|
6
6
|
import { NeedleToktxContext } from "./needle_toktx.js";
|
|
7
|
-
import { createTransform } from "@gltf-transform/functions";
|
|
7
|
+
import { createTransform, listTextureSlots } from "@gltf-transform/functions";
|
|
8
8
|
import { dirname } from "path";
|
|
9
9
|
import { existsSync, rmSync } from "fs";
|
|
10
10
|
import { validateImageMimeType } from "../utils/validate.js";
|
|
11
|
+
import { isLightmap } from "../utils/texture.js";
|
|
11
12
|
export async function resizeTextureIfNecessary(texture) {
|
|
12
13
|
const ext = texture.getExtension(NEEDLE_compression_texture.EXTENSION_NAME);
|
|
13
14
|
if (ext?.model) {
|
|
@@ -84,7 +85,7 @@ export const needle_texture_transform = function (options) {
|
|
|
84
85
|
async function processTexture(texture, index, document, logger, context) {
|
|
85
86
|
const validationResult = validateImageMimeType(texture.getMimeType(), texture.getImage());
|
|
86
87
|
if (validationResult?.error) {
|
|
87
|
-
logger.warn(`Texture ${texture.getName()} [${index}] has an incorrect mime type
|
|
88
|
+
logger.warn(`WARN: Texture ${texture.getName()} [${index}] has an incorrect mime type \"${texture.getMimeType()}\" → auto-correcting to ${validationResult.mimeType}`);
|
|
88
89
|
texture.setMimeType(validationResult.mimeType);
|
|
89
90
|
}
|
|
90
91
|
const originalMimeType = texture.getMimeType();
|
|
@@ -142,13 +143,16 @@ async function processTexture(texture, index, document, logger, context) {
|
|
|
142
143
|
}
|
|
143
144
|
else {
|
|
144
145
|
let mimeType = "image/ktx2";
|
|
145
|
-
|
|
146
|
+
const slots = listTextureSlots(texture);
|
|
146
147
|
// if (slots.includes("metallicRoughnessTexture"))
|
|
147
148
|
// mimeType = "image/webp";
|
|
148
149
|
const isVRM = context.file?.toLowerCase().endsWith(".vrm");
|
|
149
150
|
if (context.config?.usecase === "product") {
|
|
150
151
|
mimeType = "image/webp";
|
|
151
152
|
}
|
|
153
|
+
else if (slots.includes("lightmapTexture") || isLightmap(texture)) {
|
|
154
|
+
mimeType = "image/webp";
|
|
155
|
+
}
|
|
152
156
|
// TODO: need to make this into a function
|
|
153
157
|
// const webpThreshold = 256; // this is the default threshold in progressive textures (needle_progressive.ts)
|
|
154
158
|
// console.log(context.file, isVRM);
|
|
@@ -2,7 +2,7 @@ import { BufferUtils, FileUtils, ImageUtils, uuid } from "@gltf-transform/core";
|
|
|
2
2
|
import { listTextureSlots, getTextureChannelMask, fitPowerOfTwo, TextureResizeFilter } from "@gltf-transform/functions";
|
|
3
3
|
import { KHRTextureBasisu } from "@gltf-transform/extensions";
|
|
4
4
|
import { checkKTXSoftware, createParams, KTX_SOFTWARE_VERSION_MIN, Mode } from "./toktx.js";
|
|
5
|
-
import { existsSync, mkdirSync } from "fs";
|
|
5
|
+
import { existsSync, mkdirSync, rmdirSync } from "fs";
|
|
6
6
|
import { formatBytes, spawn, waitExit } from './util.js';
|
|
7
7
|
import { ETC1S_DEFAULTS, UASTC_DEFAULTS } from "./toktx_types.js";
|
|
8
8
|
import fs from 'fs/promises';
|
|
@@ -10,7 +10,7 @@ import os from 'os';
|
|
|
10
10
|
import tmp from 'tmp';
|
|
11
11
|
import pLimit from 'p-limit';
|
|
12
12
|
import { replaceExtension } from "../utils/fileutils.js";
|
|
13
|
-
import { ceilMultipleOfFour, isMultipleOfFour } from "../utils/index.js";
|
|
13
|
+
import { ceilMultipleOfFour, isLightmap, isMultipleOfFour } from "../utils/index.js";
|
|
14
14
|
import sharp from "sharp";
|
|
15
15
|
const NUM_CPUS = os.cpus().length || 1; // microsoft/vscode#112122
|
|
16
16
|
let didLogEnvironmentInfo = false;
|
|
@@ -18,6 +18,7 @@ export class NeedleToktxContext {
|
|
|
18
18
|
limit;
|
|
19
19
|
basisuExtension;
|
|
20
20
|
version;
|
|
21
|
+
cacheDirectory;
|
|
21
22
|
tmpPathBase;
|
|
22
23
|
document;
|
|
23
24
|
env;
|
|
@@ -76,6 +77,7 @@ export class NeedleToktxContext {
|
|
|
76
77
|
}
|
|
77
78
|
this.basisuExtension = document.createExtension(KHRTextureBasisu).setRequired(true);
|
|
78
79
|
const tmpDir = `${tmp.tmpdir}/gltf-transform`;
|
|
80
|
+
this.cacheDirectory = tmpDir;
|
|
79
81
|
if (!(existsSync(tmpDir)))
|
|
80
82
|
mkdirSync(tmpDir);
|
|
81
83
|
this.tmpPathBase = tmpDir + "/" + uuid();
|
|
@@ -173,6 +175,10 @@ export class NeedleToktxContext {
|
|
|
173
175
|
quality = 255;
|
|
174
176
|
compression = 4;
|
|
175
177
|
}
|
|
178
|
+
else if (slots.includes("lightmapTexture") || isLightmap(texture)) {
|
|
179
|
+
quality = 255;
|
|
180
|
+
compression = 4;
|
|
181
|
+
}
|
|
176
182
|
}
|
|
177
183
|
const defaults = mode === Mode.ETC1S ? ETC1S_DEFAULTS : UASTC_DEFAULTS;
|
|
178
184
|
// TODO: should we ensure that the texture is using all channels? Unused channels are discarded anyways. The issue we have here is that textures that are solely used from pointers have unknown channels. We register them with RGBA when generating progressive assets as well as assign texture json pointers /textures/ to
|
|
@@ -226,5 +232,8 @@ export class NeedleToktxContext {
|
|
|
226
232
|
if (!usesKTX2) {
|
|
227
233
|
this.basisuExtension?.dispose();
|
|
228
234
|
}
|
|
235
|
+
if (this.cacheDirectory && existsSync(this.cacheDirectory)) {
|
|
236
|
+
rmdirSync(this.cacheDirectory, { recursive: true });
|
|
237
|
+
}
|
|
229
238
|
}
|
|
230
239
|
}
|
package/dist/utils/texture.d.ts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
+
import { Texture } from "@gltf-transform/core";
|
|
1
2
|
export declare function isMultipleOfFour(value: number): boolean;
|
|
2
3
|
export declare function ceilMultipleOfFour(value: number): number;
|
|
4
|
+
export declare function isLightmap(texture: Texture): boolean;
|
|
5
|
+
export declare function setLightmapName(texture: Texture): void;
|
package/dist/utils/texture.js
CHANGED
|
@@ -6,3 +6,9 @@ export function ceilMultipleOfFour(value) {
|
|
|
6
6
|
return 4;
|
|
7
7
|
return value % 4 ? value + 4 - (value % 4) : value;
|
|
8
8
|
}
|
|
9
|
+
export function isLightmap(texture) {
|
|
10
|
+
return texture.getName()?.startsWith("Lightmap");
|
|
11
|
+
}
|
|
12
|
+
export function setLightmapName(texture) {
|
|
13
|
+
texture.setName("Lightmap");
|
|
14
|
+
}
|
package/dist/utils/validate.js
CHANGED
|
@@ -2,15 +2,16 @@ import { ImageUtils } from "@gltf-transform/core";
|
|
|
2
2
|
export function validateImageMimeType(mimetype, image) {
|
|
3
3
|
const actualMimeType = ImageUtils.getMimeType(image);
|
|
4
4
|
if (actualMimeType != null && actualMimeType != mimetype) {
|
|
5
|
-
|
|
5
|
+
const msg = `Image reported wrong mime type \"${mimetype}\" but is actually \"${actualMimeType}\"`;
|
|
6
|
+
console.error(`WARN: ${msg}`);
|
|
6
7
|
return {
|
|
7
8
|
error: true,
|
|
8
|
-
message:
|
|
9
|
+
message: msg,
|
|
9
10
|
mimeType: actualMimeType
|
|
10
11
|
};
|
|
11
12
|
}
|
|
12
13
|
else if (actualMimeType === null) {
|
|
13
|
-
console.warn(
|
|
14
|
+
console.warn(`WARN: Could not validate image type for \"${mimetype}\"`);
|
|
14
15
|
}
|
|
15
16
|
return null;
|
|
16
17
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@needle-tools/gltf-build-pipeline",
|
|
3
|
-
"version": "2.2.1-alpha.
|
|
3
|
+
"version": "2.2.1-alpha.7",
|
|
4
4
|
"description": "Pipeline and tools for optimizing gltf files using gltf-transform and compression settings within glTF extensions",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|