@needle-tools/gltf-build-pipeline 2.2.1-alpha.6 → 2.2.1-alpha.8
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 -1
- package/dist/cli/index.js +33 -15
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.js +25 -7
- package/dist/scripts/pack-gltf.js +6 -2
- package/dist/transforms/needle_mesh_transform.js +6 -3
- package/dist/transforms/needle_progressive.d.ts +2 -0
- package/dist/transforms/needle_progressive.js +25 -7
- package/dist/transforms/needle_texture_transform.js +7 -3
- package/dist/transforms/needle_toktx.js +7 -3
- package/dist/transforms/needle_webp.d.ts +1 -0
- package/dist/transforms/needle_webp.js +8 -0
- package/dist/utils/fileutils.d.ts +13 -0
- package/dist/utils/fileutils.js +35 -2
- package/dist/utils/texture.d.ts +3 -0
- package/dist/utils/texture.js +6 -0
- package/dist/utils/validate.js +3 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,15 @@ 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
|
+
## [2.2.1-alpha.8] - 2023-06-22
|
|
8
|
+
- add: CLI <output> support
|
|
9
|
+
- fix: register webp extension for valid glTF files. [issue 5122](https://linear.app/needle/issue/NE-5122)
|
|
10
|
+
- fix: issue where VRM material would be pruned
|
|
11
|
+
- change: VRM use meshopt compression where any blendshapes are found
|
|
12
|
+
|
|
13
|
+
## [2.2.1-alpha.7] - 2023-06-19
|
|
8
14
|
- fix: WARN if image validation can not run
|
|
15
|
+
- change: compress lightmap textures with webp
|
|
9
16
|
|
|
10
17
|
## [2.2.1-alpha.5] - 2023-06-18
|
|
11
18
|
- add: `stats` now includes json and animation size info
|
package/dist/cli/index.js
CHANGED
|
@@ -6,9 +6,8 @@ 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
8
|
import { cacheSizeLimit, clearCache, limitCacheSize } from '../cache/cache.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import path, { dirname } from 'path';
|
|
9
|
+
import { ensureIsDirectory, foreachGLTF, isDirectory } from '../utils/fileutils.js';
|
|
10
|
+
import path, { resolve } from 'path';
|
|
12
11
|
import { ERROR_CODES } from '../constants.js';
|
|
13
12
|
// For testing / dev you can run `npm link` in the package directory
|
|
14
13
|
// For removing the link run `npm rm --global @needle-tools/gltf-build-pipeline`
|
|
@@ -57,7 +56,7 @@ This will produce multiple versions of the input file, each with a different lev
|
|
|
57
56
|
Each version will be compressed and written to the output directory.
|
|
58
57
|
`)
|
|
59
58
|
.argument('<input>', "The input glTF, GLB or VRM file that should be compressed OR a directory that contains files to be compressed")
|
|
60
|
-
|
|
59
|
+
.argument('<output>', "The output file or directory. It can be an absolute path or a relative path to the input file. If not provided, the input file will be overwritten.", { default: null })
|
|
61
60
|
.option("--compress", `When enabled files will be compressed`, { validator: program.BOOLEAN, default: true })
|
|
62
61
|
.option("--progressive", `When enabled files will processed to be progressively loaded`, { validator: program.BOOLEAN, default: true })
|
|
63
62
|
.option("--usecase <usecase>", `The usecase for the compression. This will set the compression settings. Possible options: [${UsecaseOptions.join(", ")}]`, { validator: program.STRING })
|
|
@@ -78,8 +77,8 @@ Each version will be compressed and written to the output directory.
|
|
|
78
77
|
totalFileSizeInMB: 0,
|
|
79
78
|
totalFileSizeInMBBefore: 0,
|
|
80
79
|
};
|
|
81
|
-
const
|
|
82
|
-
let config = getConfig(options.config?.toString(),
|
|
80
|
+
const input = args.input.toString();
|
|
81
|
+
let config = getConfig(options.config?.toString(), input, {
|
|
83
82
|
logger
|
|
84
83
|
});
|
|
85
84
|
if (options.usecase) {
|
|
@@ -92,18 +91,29 @@ Each version will be compressed and written to the output directory.
|
|
|
92
91
|
config.usecase = options.usecase.toString();
|
|
93
92
|
}
|
|
94
93
|
}
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
const
|
|
94
|
+
let output = args.output?.toString() || input;
|
|
95
|
+
const inputIsDirectory = isDirectory(input);
|
|
96
|
+
const outputIsDirectory = isDirectory(output);
|
|
97
|
+
if (inputIsDirectory && !outputIsDirectory) {
|
|
98
|
+
logger.error("When <input> is a directory then <output> must also be a directory");
|
|
99
|
+
process.exit(ERROR_CODES.INVALID_ARGS);
|
|
100
|
+
}
|
|
101
|
+
// Handle the output bath being relative to the input path
|
|
102
|
+
if (path.isAbsolute(output) === false) {
|
|
103
|
+
const base = ensureIsDirectory(input);
|
|
104
|
+
output = resolve(base, output);
|
|
105
|
+
}
|
|
98
106
|
const set = new Set();
|
|
99
107
|
const useCache = options.cache !== false;
|
|
100
108
|
const debug = options.debug === true;
|
|
101
|
-
|
|
109
|
+
const verbose = options.verbose === true;
|
|
110
|
+
await foreachGLTF(input, async (file) => {
|
|
102
111
|
logger.info("→ Transform " + file);
|
|
103
112
|
const progressive_results = new Array();
|
|
104
113
|
if (options.progressive === true) {
|
|
105
114
|
if (await make_progressive({
|
|
106
115
|
path: file,
|
|
116
|
+
outpath: output,
|
|
107
117
|
results: progressive_results,
|
|
108
118
|
config,
|
|
109
119
|
debug,
|
|
@@ -121,16 +131,24 @@ Each version will be compressed and written to the output directory.
|
|
|
121
131
|
stats,
|
|
122
132
|
packedFiles: set,
|
|
123
133
|
debug,
|
|
134
|
+
verbose,
|
|
124
135
|
useCache,
|
|
125
136
|
logger: logger,
|
|
126
137
|
};
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
138
|
+
// If we have produced progressive assets then the array already contains the input
|
|
139
|
+
if (progressive_results.length > 0) {
|
|
140
|
+
for (const res of progressive_results) {
|
|
141
|
+
if (await packGLTF(res, res, opts) === false) {
|
|
142
|
+
process.exit(ERROR_CODES.PACKING_FAILED);
|
|
143
|
+
}
|
|
130
144
|
}
|
|
131
145
|
}
|
|
132
|
-
|
|
133
|
-
|
|
146
|
+
// If we have not produced progressive assets then we just need to compress the input
|
|
147
|
+
// We can not just check if the file is in the list of results because the output path might have changed the name
|
|
148
|
+
else {
|
|
149
|
+
if (await packGLTF(file, output, opts) === false) {
|
|
150
|
+
process.exit(ERROR_CODES.PACKING_FAILED);
|
|
151
|
+
}
|
|
134
152
|
}
|
|
135
153
|
}
|
|
136
154
|
});
|
package/dist/constants.d.ts
CHANGED
package/dist/constants.js
CHANGED
|
@@ -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
|
}
|
|
@@ -524,6 +524,7 @@ class JsonPointerHandler {
|
|
|
524
524
|
map = new Map();
|
|
525
525
|
debug = false;
|
|
526
526
|
breakPoint = false;
|
|
527
|
+
VRM_0_MaterialIndexRegex = new RegExp(/materialProperties\/(?<material_index>\d+)\//g);
|
|
527
528
|
constructor(document, id) {
|
|
528
529
|
this.document = document;
|
|
529
530
|
this.id = id;
|
|
@@ -671,7 +672,7 @@ class JsonPointerHandler {
|
|
|
671
672
|
// key texture is matching for example for VRM meta/texture (AvatarSample_A)
|
|
672
673
|
key === "texture"
|
|
673
674
|
// VRM 0.0 material properties
|
|
674
|
-
|| fullPath.match(/materialProperties
|
|
675
|
+
|| fullPath.match(/materialProperties\/(?<material_index>\d+)\/textureProperties/g)
|
|
675
676
|
// VRM 1.0 meta
|
|
676
677
|
|| fullPath.match(/meta\/thumbnailImage/g)) {
|
|
677
678
|
const index = value;
|
|
@@ -705,6 +706,23 @@ class JsonPointerHandler {
|
|
|
705
706
|
this.map.set(dictKey, pointer);
|
|
706
707
|
prop.setReference(dictKey, object, { channels: TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A });
|
|
707
708
|
}
|
|
709
|
+
const materialMatch = this.VRM_0_MaterialIndexRegex.exec(fullPath);
|
|
710
|
+
if (materialMatch) {
|
|
711
|
+
const materialIndex = parseInt(materialMatch.groups.material_index);
|
|
712
|
+
const material = context.materials[materialIndex];
|
|
713
|
+
prop.setReference(`/materials/${materialIndex}`, material);
|
|
714
|
+
// const res = new MaterialIndexResolver(this.document, material, context.jsonDoc.json.materials![materialIndex]);
|
|
715
|
+
// res.debug = this.debug;
|
|
716
|
+
// res.init(context);
|
|
717
|
+
// const dictKey = `/materials/${materialIndex}`
|
|
718
|
+
// let pointer = this.map.get(dictKey);
|
|
719
|
+
// if (!pointer) {
|
|
720
|
+
// pointer = new JsonPointer(null, res, value);
|
|
721
|
+
// }
|
|
722
|
+
// pointer.debug = this.debug;
|
|
723
|
+
// pointer.register(obj, key, fullPath);
|
|
724
|
+
// this.map.set(dictKey, pointer);
|
|
725
|
+
}
|
|
708
726
|
}
|
|
709
727
|
break;
|
|
710
728
|
default:
|
|
@@ -7,7 +7,7 @@ import { MeshoptDecoder, MeshoptEncoder } from 'meshoptimizer';
|
|
|
7
7
|
import { dedup, prune, resample } from '@gltf-transform/functions';
|
|
8
8
|
import { ALL_EXTENSIONS as NEEDLE_EXTENSIONS, NEEDLE_compression_texture, NEEDLE_mesh_compression, } from '../extensions/index.js';
|
|
9
9
|
import { isLOD, isMeshLOD, needle_animation_transform, needle_asset, needle_mesh_transform, needle_texture_transform, } from '../transforms/index.js';
|
|
10
|
-
import { writeNodeIO } from "../utils/index.js";
|
|
10
|
+
import { getOutputPath, writeNodeIO } from "../utils/index.js";
|
|
11
11
|
import { addToCache, getHash, tryGetFromCache, } from "../cache/index.js";
|
|
12
12
|
/** Compress a glTF file
|
|
13
13
|
*/
|
|
@@ -60,7 +60,7 @@ export async function packGLTF(inputFile, outputFile, options) {
|
|
|
60
60
|
});
|
|
61
61
|
const document = await io.read(inputFile);
|
|
62
62
|
/** @ts-ignore */
|
|
63
|
-
document.getLogger().verbosity = options.
|
|
63
|
+
document.getLogger().verbosity = options.verbose ? Verbosity.DEBUG : options.debug ? Verbosity.INFO : Verbosity.WARN;
|
|
64
64
|
let totalGPUTextureMemory = 0;
|
|
65
65
|
/** @type {Array<import('@gltf-transform/core').Transform>} */
|
|
66
66
|
const transforms = [
|
|
@@ -93,6 +93,10 @@ export async function packGLTF(inputFile, outputFile, options) {
|
|
|
93
93
|
await document.transform(...transforms);
|
|
94
94
|
if (!outputFile)
|
|
95
95
|
outputFile = inputFile;
|
|
96
|
+
else {
|
|
97
|
+
outputFile = getOutputPath(inputFile, outputFile);
|
|
98
|
+
logger.info(`⇄ Writing to ${outputFile}`);
|
|
99
|
+
}
|
|
96
100
|
await writeNodeIO(io, outputFile, document);
|
|
97
101
|
if (useCache && cacheKey != undefined && existsSync(outputFile))
|
|
98
102
|
addToCache(cacheKey, readFileSync(outputFile));
|
|
@@ -33,9 +33,12 @@ async function applyMeshCompression(document, transform_context, context) {
|
|
|
33
33
|
}
|
|
34
34
|
// Meshop is better at re-using vertex attributes
|
|
35
35
|
if (context.file.endsWith(".vrm")) {
|
|
36
|
-
document.
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
const hasBlendshapes = document.getRoot().listMeshes().some((mesh) => mesh.listPrimitives().some((prim) => prim.listTargets().length > 0));
|
|
37
|
+
if (hasBlendshapes) {
|
|
38
|
+
document.getLogger().info(`[${NAME}]: VRM → will apply meshopt mesh compression`);
|
|
39
|
+
const transform = meshopt({ encoder: MeshoptEncoder });
|
|
40
|
+
return transform(document, transform_context);
|
|
41
|
+
}
|
|
39
42
|
}
|
|
40
43
|
// by default we still want to run draco if the root doesnt have a compression model configured
|
|
41
44
|
document.getLogger().info(`[${NAME}]: → will apply draco mesh compression (default)`);
|
|
@@ -2,6 +2,8 @@ import { ILogger, Texture, Transform, vec2 } from '@gltf-transform/core';
|
|
|
2
2
|
import { Config } from '../config/index.js';
|
|
3
3
|
declare interface ProgressiveOptions {
|
|
4
4
|
path: string | Array<string>;
|
|
5
|
+
/** Directory to output progressive assets to */
|
|
6
|
+
outpath?: string;
|
|
5
7
|
results: Array<string>;
|
|
6
8
|
config: Config;
|
|
7
9
|
debug?: boolean;
|
|
@@ -4,10 +4,10 @@ import draco3d from 'draco3dgltf';
|
|
|
4
4
|
import { NEEDLE_PROGRESSIVE_EXTENSION_NAME, NEEDLE_progressive } from "../extensions/NEEDLE_progressive/index.js";
|
|
5
5
|
import { createOpaqueExtension, currentGenerator, NEEDLE_compression_texture, NEEDLE_mesh_compression, NEEDLE_progressive_mesh_settings, NEEDLE_progressive_texture_settings, registerExtensions as registerDefaultExtensions } from '../extensions/index.js';
|
|
6
6
|
import path, { basename, resolve } from 'path';
|
|
7
|
-
import { existsSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
|
|
7
|
+
import { existsSync, mkdirSync, 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, ensureIsDirectory, foreachGLTF, generateGuid, getOutputPath, 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';
|
|
@@ -86,7 +86,8 @@ async function onProcess(file, opts) {
|
|
|
86
86
|
transforms.push(sparse());
|
|
87
87
|
logger.info("→ Transforming: " + file);
|
|
88
88
|
await document.transform(...transforms);
|
|
89
|
-
const filename = file;
|
|
89
|
+
const filename = getOutputPath(file, opts.outpath);
|
|
90
|
+
;
|
|
90
91
|
await writeNodeIO(io, filename, document);
|
|
91
92
|
opts.results.push(filename);
|
|
92
93
|
logger.info("← Transforming done: " + filename);
|
|
@@ -111,7 +112,10 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
|
|
|
111
112
|
const startTime = Date.now();
|
|
112
113
|
const filename = basename(filePath);
|
|
113
114
|
const options = { ...TEXTURE_RESIZE_DEFAULTS, ..._options };
|
|
114
|
-
|
|
115
|
+
let directory = path.dirname(filePath);
|
|
116
|
+
if (opts.outpath) {
|
|
117
|
+
directory = ensureIsDirectory(opts.outpath);
|
|
118
|
+
}
|
|
115
119
|
// const fileNameWithoutExtension = path.basename(filePath, path.extname(filePath));
|
|
116
120
|
const textureLodBasePath = directory + "/";
|
|
117
121
|
return createTransform(NAME, async (doc) => {
|
|
@@ -193,7 +197,9 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
|
|
|
193
197
|
// generateLods ||= opts.config?.textures?.lods === true;
|
|
194
198
|
// // Ensure that texture lods are not disabled by { textures: { lods: false } }
|
|
195
199
|
// generateLods &&= opts.config?.textures?.lods !== false;
|
|
196
|
-
|
|
200
|
+
const _isLightmap = slots.includes("lightmapTexture") || isLightmap(texture);
|
|
201
|
+
// Lightmaps don't have multiple levels right now
|
|
202
|
+
if (generateLods && !_isLightmap) {
|
|
197
203
|
const LEVEL = 6;
|
|
198
204
|
let lastSize = currentSize;
|
|
199
205
|
for (let i = 1; i < LEVEL; i++) {
|
|
@@ -273,6 +279,7 @@ async function createTextureLod(args, opts) {
|
|
|
273
279
|
newTexture.setImage(img);
|
|
274
280
|
newTexture.setURI(texture.getURI());
|
|
275
281
|
newTexture.setMimeType(texture.getMimeType());
|
|
282
|
+
newTexture.setName(texture.getName() || ("LOD_" + textureIndex));
|
|
276
283
|
if (args.maxSize && args.maxSize >= 16) {
|
|
277
284
|
const currentSize = newTexture.getSize();
|
|
278
285
|
if (currentSize[0] > args.maxSize && currentSize[1] > args.maxSize) {
|
|
@@ -308,6 +315,10 @@ async function createTextureLod(args, opts) {
|
|
|
308
315
|
case "emissiveTexture":
|
|
309
316
|
newMaterial.setEmissiveTexture(newTexture);
|
|
310
317
|
break;
|
|
318
|
+
case "lightmapTexture":
|
|
319
|
+
newMaterial.setBaseColorTexture(newTexture);
|
|
320
|
+
setLightmapName(newTexture);
|
|
321
|
+
break;
|
|
311
322
|
default:
|
|
312
323
|
if (slot.startsWith("/textures/")) {
|
|
313
324
|
// Ensure the texture is using the alpha channel since we don't know what slots the original texture was used for
|
|
@@ -322,6 +333,7 @@ async function createTextureLod(args, opts) {
|
|
|
322
333
|
// Make sure the material is used
|
|
323
334
|
createMeshForMaterial(newDoc, newMaterial, slot_index++);
|
|
324
335
|
}
|
|
336
|
+
// Pass compression settings to the new texture
|
|
325
337
|
const compressionSettingsExt = texture.getExtension(NEEDLE_compression_texture.EXTENSION_NAME);
|
|
326
338
|
if (compressionSettingsExt !== undefined && compressionSettingsExt !== null && compressionSettingsExt.getExtensionDefinition) {
|
|
327
339
|
logger.info("> Pass compression extension to new texture");
|
|
@@ -359,7 +371,10 @@ async function createTextureLod(args, opts) {
|
|
|
359
371
|
const ext = ".glb";
|
|
360
372
|
const imageName = "image_" + level + "_" + guid;
|
|
361
373
|
const filename = imageName + ext;
|
|
362
|
-
const outName = basePath
|
|
374
|
+
const outName = path.join(basePath, filename);
|
|
375
|
+
if (!existsSync(basePath)) {
|
|
376
|
+
mkdirSync(basePath, { recursive: true });
|
|
377
|
+
}
|
|
363
378
|
logger.info(`Write texture LOD ${level} to \"${filename}\"`);
|
|
364
379
|
newIo.write(outName, newDoc);
|
|
365
380
|
return {
|
|
@@ -472,7 +487,10 @@ async function resizeTexture(options, texture, logger, uri, slots) {
|
|
|
472
487
|
};
|
|
473
488
|
}
|
|
474
489
|
export function make_progressive_meshes(filePath, _options, opts) {
|
|
475
|
-
|
|
490
|
+
let directory = path.dirname(filePath);
|
|
491
|
+
if (opts.outpath) {
|
|
492
|
+
directory = ensureIsDirectory(opts.outpath);
|
|
493
|
+
}
|
|
476
494
|
// const fileNameWithoutExtension = path.basename(filePath, path.extname(filePath));
|
|
477
495
|
const outputDirectory = directory + "/";
|
|
478
496
|
return createTransform("PROGRESSIVE_MESHES", async (doc, _context) => {
|
|
@@ -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,
|
|
5
|
+
import { existsSync, mkdirSync, rmSync } 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;
|
|
@@ -175,6 +175,10 @@ export class NeedleToktxContext {
|
|
|
175
175
|
quality = 255;
|
|
176
176
|
compression = 4;
|
|
177
177
|
}
|
|
178
|
+
else if (slots.includes("lightmapTexture") || isLightmap(texture)) {
|
|
179
|
+
quality = 255;
|
|
180
|
+
compression = 4;
|
|
181
|
+
}
|
|
178
182
|
}
|
|
179
183
|
const defaults = mode === Mode.ETC1S ? ETC1S_DEFAULTS : UASTC_DEFAULTS;
|
|
180
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
|
|
@@ -229,7 +233,7 @@ export class NeedleToktxContext {
|
|
|
229
233
|
this.basisuExtension?.dispose();
|
|
230
234
|
}
|
|
231
235
|
if (this.cacheDirectory && existsSync(this.cacheDirectory)) {
|
|
232
|
-
|
|
236
|
+
rmSync(this.cacheDirectory, { recursive: true });
|
|
233
237
|
}
|
|
234
238
|
}
|
|
235
239
|
}
|
|
@@ -6,6 +6,7 @@ export interface NeedleWebPOptions {
|
|
|
6
6
|
export declare class NeedleWebPContext implements INeedleTextureTransformContext {
|
|
7
7
|
options: NeedleWebPOptions;
|
|
8
8
|
logger: ILogger;
|
|
9
|
+
private compressed;
|
|
9
10
|
constructor(options: NeedleWebPOptions);
|
|
10
11
|
prepare(document: Document): Promise<void>;
|
|
11
12
|
process(index: number, texture: Texture, settings: Pick<NEEDLE_compression_texture_schema, "quality">, context: NeedleTransformContext): Promise<boolean>;
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import sharp from 'sharp';
|
|
2
2
|
import { BufferUtils, HTTPUtils } from '@gltf-transform/core';
|
|
3
|
+
import { EXTTextureWebP } from '@gltf-transform/extensions';
|
|
3
4
|
export class NeedleWebPContext {
|
|
4
5
|
options;
|
|
5
6
|
logger;
|
|
7
|
+
compressed = 0;
|
|
6
8
|
constructor(options) {
|
|
7
9
|
this.options = options;
|
|
8
10
|
}
|
|
9
11
|
async prepare(document) {
|
|
12
|
+
this.compressed = 0;
|
|
10
13
|
this.logger = document.getLogger();
|
|
11
14
|
}
|
|
12
15
|
async process(index, texture, settings, context) {
|
|
@@ -30,8 +33,13 @@ export class NeedleWebPContext {
|
|
|
30
33
|
.setMimeType('image/webp')
|
|
31
34
|
.setImage(BufferUtils.toView(buffer))
|
|
32
35
|
.setURI(HTTPUtils.basename(texture.getURI()) + '.webp');
|
|
36
|
+
this.compressed += 1;
|
|
33
37
|
return true;
|
|
34
38
|
}
|
|
35
39
|
async finalize(document) {
|
|
40
|
+
if (this.compressed > 0) {
|
|
41
|
+
const webpExtension = document.createExtension(EXTTextureWebP);
|
|
42
|
+
webpExtension.setRequired(true);
|
|
43
|
+
}
|
|
36
44
|
}
|
|
37
45
|
}
|
|
@@ -1,6 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the output path for a file based on the output directory or file.
|
|
3
|
+
* If outDirOrFile is a filepath, it will be returned as is.
|
|
4
|
+
* If outDirOrFile is a directory, the file will be placed in that directory.
|
|
5
|
+
*/
|
|
6
|
+
export declare function getOutputPath(filepath: string, outDirOrFile: string | undefined | null, opts?: {
|
|
7
|
+
create?: boolean;
|
|
8
|
+
}): string;
|
|
1
9
|
export declare function getFileName(filepath: string): string;
|
|
2
10
|
export declare function getDirName(filepath: string): string;
|
|
3
11
|
export declare type GLTFFileCallback = (file: string) => void | Promise<void>;
|
|
4
12
|
export declare function foreachGLTF(args: string | string[], callback: GLTFFileCallback): Promise<void>;
|
|
5
13
|
export declare function replaceExtension(filepath: string, newExtension: string): string;
|
|
6
14
|
export declare function isDirectory(path: string): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Ensure that a path is a directory.
|
|
17
|
+
* If the directory does not exist, it will be created.
|
|
18
|
+
*/
|
|
19
|
+
export declare function ensureIsDirectory(path: string): string;
|
package/dist/utils/fileutils.js
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
|
-
import { existsSync, statSync, readdirSync, readFileSync } from 'fs';
|
|
2
|
-
import { isAbsolute } from 'path';
|
|
1
|
+
import { existsSync, statSync, readdirSync, readFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import { dirname, isAbsolute, join } from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Get the output path for a file based on the output directory or file.
|
|
5
|
+
* If outDirOrFile is a filepath, it will be returned as is.
|
|
6
|
+
* If outDirOrFile is a directory, the file will be placed in that directory.
|
|
7
|
+
*/
|
|
8
|
+
export function getOutputPath(filepath, outDirOrFile, opts) {
|
|
9
|
+
if (!outDirOrFile) {
|
|
10
|
+
return filepath;
|
|
11
|
+
}
|
|
12
|
+
if (!isDirectory(outDirOrFile)) {
|
|
13
|
+
return outDirOrFile;
|
|
14
|
+
}
|
|
15
|
+
const filename = getFileName(filepath);
|
|
16
|
+
// create the directory if it doesn't exist (default behavior)
|
|
17
|
+
if (opts?.create != false && !existsSync(outDirOrFile)) {
|
|
18
|
+
mkdirSync(outDirOrFile, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
const outpath = join(outDirOrFile, filename);
|
|
21
|
+
return outpath;
|
|
22
|
+
}
|
|
3
23
|
export function getFileName(filepath) {
|
|
4
24
|
const parts = filepath.replaceAll("\\", "/").split("/");
|
|
5
25
|
return parts[parts.length - 1];
|
|
@@ -139,3 +159,16 @@ export function isDirectory(path) {
|
|
|
139
159
|
const hasExt = path.replaceAll("\\", "/").split("/").pop()?.includes(".");
|
|
140
160
|
return !hasExt;
|
|
141
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Ensure that a path is a directory.
|
|
164
|
+
* If the directory does not exist, it will be created.
|
|
165
|
+
*/
|
|
166
|
+
export function ensureIsDirectory(path) {
|
|
167
|
+
if (!isDirectory(path)) {
|
|
168
|
+
path = dirname(path);
|
|
169
|
+
}
|
|
170
|
+
if (!existsSync(path)) {
|
|
171
|
+
mkdirSync(path, { recursive: true });
|
|
172
|
+
}
|
|
173
|
+
return path;
|
|
174
|
+
}
|
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,10 +2,11 @@ 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
|
}
|
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.8",
|
|
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",
|