@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 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.6] - 2023-06-19
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 { statSync } from 'fs';
10
- import { foreachGLTF, isDirectory } from '../utils/fileutils.js';
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
- // .argument('<output>', "The output file. If not provided, the input file will be overwritten.", { default: "" })
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 file = args.input.toString();
82
- let config = getConfig(options.config?.toString(), file, {
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
- const output = options.output?.toString() || file;
96
- const outputIsDirectory = statSync(output).isDirectory();
97
- const outDir = outputIsDirectory ? output : dirname(output);
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
- await foreachGLTF(file, async (file) => {
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
- for (const res of progressive_results) {
128
- if (await packGLTF(res, res, opts) === false) {
129
- process.exit(ERROR_CODES.PACKING_FAILED);
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
- if (await packGLTF(file, file, opts) === false) {
133
- process.exit(ERROR_CODES.PACKING_FAILED);
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
  });
@@ -2,4 +2,5 @@ export declare const ERROR_CODES: {
2
2
  UNKNOWN_ERROR: number;
3
3
  PACKING_FAILED: number;
4
4
  PROGRESSIVE_FAILED: number;
5
+ INVALID_ARGS: number;
5
6
  };
package/dist/constants.js CHANGED
@@ -2,4 +2,5 @@ export const ERROR_CODES = {
2
2
  UNKNOWN_ERROR: 1,
3
3
  PACKING_FAILED: 2,
4
4
  PROGRESSIVE_FAILED: 3,
5
+ INVALID_ARGS: 4,
5
6
  };
@@ -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 (name?.startsWith("Lightmap") && tex.getMimeType() !== "image/exr") {
193
- if (debugLog)
194
- console.log("Found lightmap texture", name, tex.getMimeType(), EXTENSION_NAME);
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\/\d+\/textureProperties/g)
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.debug || options.verbose ? Verbosity.DEBUG : Verbosity.WARN;
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.getLogger().info(`[${NAME}]: VRM → will apply meshopt mesh compression`);
37
- const transform = meshopt({ encoder: MeshoptEncoder });
38
- return transform(document, transform_context);
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
- const directory = path.dirname(filePath);
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
- if (generateLods) {
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 + filename;
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
- const directory = path.dirname(filePath);
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: ${validationResult.mimeType}: auto-correcting.`);
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
- // const slots = listTextureSlots(texture);
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, rmdirSync } from "fs";
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
- rmdirSync(this.cacheDirectory, { recursive: true });
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;
@@ -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
+ }
@@ -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;
@@ -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
+ }
@@ -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
- console.error(`Expected image to be of type ${mimetype}, but was ${actualMimeType}`);
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: `Expected image to be of type ${mimetype}, but was ${actualMimeType}`,
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.6",
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",