@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 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
@@ -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
- const files = fs.readdirSync(dir).map(file => {
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
- const files = recurseDirectories(dir);
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
- console.log(`Removing cache file ${file.path}...`);
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 (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
  }
@@ -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
- if (generateLods) {
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: ${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);
@@ -11,6 +11,7 @@ export declare class NeedleToktxContext implements INeedleTextureTransformContex
11
11
  limit: any;
12
12
  basisuExtension: KHRTextureBasisu;
13
13
  version: string;
14
+ cacheDirectory: string;
14
15
  tmpPathBase: string;
15
16
  document: Document;
16
17
  env: undefined | {
@@ -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
  }
@@ -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,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
- 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
  }
12
13
  else if (actualMimeType === null) {
13
- console.warn("Could not determine image type");
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.5",
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",