@needle-tools/gltf-build-pipeline 1.5.0-alpha.1 → 1.5.0-alpha.11

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/cache/cache.d.ts +2 -2
  3. package/dist/cache/cache.js +36 -20
  4. package/dist/extensions/NEEDLE_compression_texture/NEEDLE_compression_texture.d.ts +1 -1
  5. package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.d.ts +2 -1
  6. package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.js +74 -17
  7. package/dist/extensions/NEEDLE_opaque/index.d.ts +2 -2
  8. package/dist/extensions/NEEDLE_opaque/index.js +7 -0
  9. package/dist/extensions/NEEDLE_progressive_mesh_settings/index.d.ts +4 -1
  10. package/dist/extensions/NEEDLE_progressive_texture_settings/index.d.ts +4 -0
  11. package/dist/transforms/needle_common.d.ts +2 -0
  12. package/dist/transforms/needle_mesh_transform.d.ts +5 -1
  13. package/dist/transforms/needle_mesh_transform.js +13 -7
  14. package/dist/transforms/needle_progressive.js +32 -8
  15. package/dist/transforms/needle_texture_transform.d.ts +9 -1
  16. package/dist/transforms/needle_texture_transform.js +46 -7
  17. package/dist/transforms/needle_toktx.d.ts +1 -1
  18. package/dist/transforms/needle_toktx.js +35 -5
  19. package/dist/transforms/needle_webp.d.ts +1 -1
  20. package/dist/utils/compression_utils.d.ts +2 -0
  21. package/dist/utils/compression_utils.js +6 -0
  22. package/dist/utils/fileutils.d.ts +1 -0
  23. package/dist/utils/fileutils.js +5 -0
  24. package/dist/utils/index.d.ts +2 -0
  25. package/dist/utils/index.js +2 -0
  26. package/dist/utils/nodeio.d.ts +5 -0
  27. package/dist/utils/nodeio.js +14 -0
  28. package/package.json +9 -8
  29. package/scripts/clear-caches.mjs +4 -0
  30. package/{defaults.mjs → scripts/defaults.mjs} +1 -1
  31. package/{fileutils.mjs → scripts/fileutils.mjs} +1 -1
  32. package/{index.mjs → scripts/index.mjs} +3 -3
  33. package/scripts/limit-caches.mjs +4 -0
  34. package/{make-progressive.mjs → scripts/make-progressive.mjs} +2 -2
  35. package/{pack-gltf.mjs → scripts/pack-gltf.mjs} +40 -23
  36. package/clear-caches.mjs +0 -4
  37. package/dist/functions/gzip.d.ts +0 -1
  38. package/dist/functions/gzip.js +0 -16
  39. package/dist/functions/index.d.ts +0 -1
  40. package/dist/functions/index.js +0 -1
  41. package/dist/functions/list-texture-slots.d.ts +0 -1
  42. package/dist/functions/list-texture-slots.js +0 -35
  43. package/dist/transforms/needle_simplify.d.ts +0 -3
  44. package/dist/transforms/needle_simplify.js +0 -76
  45. package/dist/transforms/types/squoosh.d.ts +0 -31
  46. package/dist/transforms/types/squoosh.js +0 -33
package/CHANGELOG.md CHANGED
@@ -4,6 +4,41 @@ All notable changes to this package will be documented in this file.
4
4
  The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5
5
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.5.0-alpha.11] - 2023-05-20
8
+ - add: pack-gltf now also runs `resample` for animations
9
+ - fix: nullreference error in accessing opaque debug options
10
+
11
+ ## [1.5.0-alpha.10] - 2023-05-17
12
+ - add: `generateLods` option to progressive texture settings
13
+
14
+ ## [1.5.0-alpha.9] - 2023-05-16
15
+ - add: start of VRM 1.0 support. Not implemented is the `VRMC_materials_mtoon` extension
16
+
17
+ ## [1.5.0-alpha.8] - 2023-05-16
18
+ - fix: issue where updating ktx compressed texture extension also flattened the path
19
+
20
+ ## [1.5.0-alpha.7] - 2023-05-16
21
+ - fix: external resources that are KTX2 compressed are now saved with a `.ktx2` extension
22
+ - change: toktx now uses compression "auto" mode to select uastc or etc1s
23
+ - change: toktx now checks the slots and increases quality for `metallicRoughnessTexture` and `normalTexture` but keeps using ETC1S for smaller file size
24
+
25
+ ## [1.5.0-alpha.6] - 2023-05-15
26
+ - fix: If not further specified then automatically use UASTC for textures that are in either `normalTexture` or `metallicRoughnessTexture` slots
27
+
28
+ ## [1.5.0-alpha.5] - 2023-05-04
29
+ - fix: NEEDLE_opaque should handle if VRM texture index is negative
30
+ - change: exit with code 1 if any file packing fails
31
+
32
+ ## [1.5.0-alpha.4] - 2023-05-03
33
+ - fix: issue in NEEDLE_opaque resolving of VRM textures, also resolve `meta.texture`
34
+ - fix: VRMs now compress meshes with mesh-opt if nothing is specified
35
+ - change: include version in cache directory
36
+
37
+ ## [1.5.0-alpha.3] - 2023-05-03
38
+ - add: VRM progressive textures support
39
+ - fix: VRM format should be saved as GLB. See https://github.com/donmccurdy/glTF-Transform/issues/1390
40
+ - fix: VRM texture index handling
41
+
7
42
  ## [1.5.0-alpha] - 2023-05-01
8
43
  - add: NEEDLE_opaque `node: <index>` retargeting
9
44
  - add: passthrough VRM extension
@@ -8,7 +8,7 @@ export declare function clearCache(): void;
8
8
  * Ensure that the cache size does not exceed the given maximum size in MB.
9
9
  * @param maxSizeMB The maximum size in MB.
10
10
  */
11
- export declare function ensureCacheSize(maxSizeMB: number): void;
11
+ export declare function limitCacheSize(maxSizeMB?: number): void;
12
12
  /**
13
13
  * Add data to the cache.
14
14
  * @param key The key to store the data under.
@@ -48,4 +48,4 @@ export declare function getKeyWithHash(property: ExtensibleProperty): {
48
48
  * @param property The property to get the hash for.
49
49
  * @returns The hash as a number.
50
50
  */
51
- export declare function getHash(property: ExtensibleProperty | object): number;
51
+ export declare function getHash(property: ExtensibleProperty | object, level?: number): number;
@@ -1,6 +1,6 @@
1
1
  import { ExtensibleProperty, Texture, Primitive, Accessor, Mesh } from "@gltf-transform/core";
2
2
  import tmp from 'tmp';
3
- import fs from 'fs';
3
+ import fs, { statSync } from 'fs';
4
4
  import path from 'path';
5
5
  import { getVersion } from "../utils/version.js";
6
6
  // import * as CHECKDISCSPACE from 'check-disk-space'
@@ -24,7 +24,7 @@ const buildPipelineVersion = getVersion(false);
24
24
  * This will remove all files in the cache directory.
25
25
  */
26
26
  export function clearCache() {
27
- const dir = getCacheDirectory();
27
+ const dir = getCacheDirectory(true);
28
28
  fs.rmSync(dir, { recursive: true });
29
29
  console.log(`Cleared cache at ${dir}`);
30
30
  }
@@ -32,16 +32,27 @@ export function clearCache() {
32
32
  * Ensure that the cache size does not exceed the given maximum size in MB.
33
33
  * @param maxSizeMB The maximum size in MB.
34
34
  */
35
- export function ensureCacheSize(maxSizeMB) {
36
- const dir = getCacheDirectory();
35
+ export function limitCacheSize(maxSizeMB = 1024) {
36
+ const dir = getCacheDirectory(true);
37
+ console.log(`Limit cache size to ${maxSizeMB.toFixed(2)} MB at ${dir}...`);
37
38
  let totalSize = 0;
38
- const files = fs.readdirSync(dir).map(file => {
39
- const path = `${dir}/${file}`;
40
- const stats = fs.statSync(path);
41
- totalSize += stats.size;
42
- return { path, size: stats.size };
43
- });
39
+ function recurseDirectories(dir) {
40
+ const files = fs.readdirSync(dir).map(file => {
41
+ const path = `${dir}/${file}`;
42
+ const stats = statSync(path);
43
+ if (stats.isDirectory()) {
44
+ recurseDirectories(path);
45
+ }
46
+ else {
47
+ totalSize += stats.size;
48
+ }
49
+ return { path, size: stats.size };
50
+ });
51
+ return files;
52
+ }
53
+ const files = recurseDirectories(dir);
44
54
  const maxSizeInBytes = maxSizeMB * 1024 * 1024;
55
+ console.log(`Current cache size is ${(totalSize / (1024 * 1024)).toFixed(1)} MB`);
45
56
  if (totalSize < maxSizeInBytes)
46
57
  return;
47
58
  files.sort((a, b) => {
@@ -55,6 +66,7 @@ export function ensureCacheSize(maxSizeMB) {
55
66
  const file = files.shift();
56
67
  if (!file)
57
68
  break;
69
+ console.log(`Removing cache file ${file.path}...`);
58
70
  fs.rmSync(file.path);
59
71
  totalSize -= file.size;
60
72
  }
@@ -153,7 +165,7 @@ export function getKeyWithHash(property) {
153
165
  * @param property The property to get the hash for.
154
166
  * @returns The hash as a number.
155
167
  */
156
- export function getHash(property) {
168
+ export function getHash(property, level = 0) {
157
169
  let hash = 0;
158
170
  if (property instanceof ExtensibleProperty) {
159
171
  const extensions = property.listExtensions();
@@ -175,7 +187,7 @@ export function getHash(property) {
175
187
  hash += hashObject(weights);
176
188
  const primitives = property.listPrimitives();
177
189
  for (const prim of primitives)
178
- hash += getHash(prim);
190
+ hash += getHash(prim, level + 1);
179
191
  }
180
192
  else if (property instanceof Primitive) {
181
193
  const indices = property.getIndices();
@@ -198,8 +210,9 @@ export function getHash(property) {
198
210
  }
199
211
  return hash;
200
212
  }
201
- function getCacheDirectory() {
202
- const tmpDir = path.join(tmp.tmpdir, "Needle", "build-pipeline-cache");
213
+ const cleanBuildPipelineVersion = buildPipelineVersion.replace(/[^a-zA-Z0-9]/g, "_");
214
+ function getCacheDirectory(root) {
215
+ const tmpDir = path.join(tmp.tmpdir, "Needle", "build-pipeline-cache", root ? "" : cleanBuildPipelineVersion);
203
216
  if (!fs.existsSync(tmpDir)) {
204
217
  fs.mkdirSync(tmpDir, { recursive: true });
205
218
  }
@@ -211,12 +224,7 @@ function hashObject(obj) {
211
224
  return hashBuffer(buffer);
212
225
  }
213
226
  const str = JSON.stringify(obj);
214
- let hash = 0;
215
- for (let i = 0; i < str.length; i++) {
216
- hash = ((hash << 5) - hash) + str.charCodeAt(i);
217
- hash |= 0;
218
- }
219
- return hash;
227
+ return hashString(str);
220
228
  }
221
229
  function hashBuffer(buffer, offset, length) {
222
230
  let hash = 0;
@@ -227,3 +235,11 @@ function hashBuffer(buffer, offset, length) {
227
235
  }
228
236
  return hash;
229
237
  }
238
+ function hashString(str) {
239
+ let hash = 0;
240
+ for (let i = 0; i < str.length; i++) {
241
+ hash = ((hash << 5) - hash) + str.charCodeAt(i);
242
+ hash |= 0;
243
+ }
244
+ return hash;
245
+ }
@@ -1,7 +1,7 @@
1
1
  import { Extension, ExtensionProperty, PropertyType, ReaderContext, WriterContext } from "@gltf-transform/core";
2
2
  export declare const EXTENSION_NAME = "NEEDLE_compression_texture";
3
3
  export declare interface NEEDLE_compression_texture_schema {
4
- mode: string;
4
+ mode: "none" | "auto" | "webp" | "UASTC" | "ETC1S";
5
5
  slot: string;
6
6
  /** quality in range of 0 - 1 */
7
7
  quality: number;
@@ -1,9 +1,10 @@
1
1
  import { WriterContext, PropertyType } from '@gltf-transform/core';
2
2
  declare interface IExtensibleProperty {
3
3
  }
4
+ type DebugBreakPoint = boolean | "pointer:read";
4
5
  export type Options = {
5
6
  debug?: boolean;
6
- breakpoint?: boolean;
7
+ breakpoint?: DebugBreakPoint;
7
8
  write?: boolean;
8
9
  };
9
10
  export interface IOpaqueExtension {
@@ -19,13 +19,14 @@ export function createOpaqueExtension(name, types, opts) {
19
19
  // private roots: Array<OpaqueProperty> = [];
20
20
  jsonPointerHandler;
21
21
  read(context) {
22
- if (opts.breakpoint === true)
22
+ if (opts?.breakpoint === true)
23
23
  debugger;
24
24
  currentGenerator = context.jsonDoc.json.asset?.generator;
25
25
  if (debugLog)
26
26
  console.log("READ " + EXTENSION_NAME);
27
27
  this.jsonPointerHandler = new JsonPointerHandler(this.document, EXTENSION_NAME);
28
28
  this.jsonPointerHandler.debug = debugLog;
29
+ this.jsonPointerHandler.breakPoint = opts.breakpoint;
29
30
  for (const type of types) {
30
31
  switch (type) {
31
32
  case PropertyType.ACCESSOR:
@@ -224,7 +225,7 @@ export function createOpaqueExtension(name, types, opts) {
224
225
  }
225
226
  }
226
227
  write(context) {
227
- if (opts.breakpoint === true)
228
+ if (opts?.breakpoint === true)
228
229
  debugger;
229
230
  if (opts?.write === false) {
230
231
  return this;
@@ -514,6 +515,7 @@ class JsonPointerHandler {
514
515
  document;
515
516
  map = new Map();
516
517
  debug = false;
518
+ breakPoint = false;
517
519
  constructor(document, id) {
518
520
  this.document = document;
519
521
  this.id = id;
@@ -524,7 +526,11 @@ class JsonPointerHandler {
524
526
  if (key !== undefined) {
525
527
  fullPath ??= "";
526
528
  fullPath += "/" + key;
529
+ if (this.breakPoint === "pointer:read") {
530
+ debugger;
531
+ }
527
532
  }
533
+ // if(this.debug && key === "colliders") debugger;
528
534
  switch (typeof value) {
529
535
  case 'object':
530
536
  if (value !== null && value !== undefined) {
@@ -556,7 +562,8 @@ class JsonPointerHandler {
556
562
  console.log("Texture index", index, imageIndex, context.jsonDoc.json.textures.length, "Textures:" + context.textures.length);
557
563
  const object = tryGetInArray(context.textures, imageIndex);
558
564
  if (object) {
559
- const res = new TexturePointerResolver(this.document, object, definition);
565
+ const res = new TextureIndexResolver(this.document, object, definition);
566
+ res.debug = this.debug;
560
567
  res.init(context); // < init here otherwise the fields defined in the TexturePointer will be undefined (this caused e.g. finding the previous texture filter to fail)
561
568
  pointer = new JsonPointer(path, res, value);
562
569
  pointer.debug = this.debug;
@@ -569,7 +576,7 @@ class JsonPointerHandler {
569
576
  if (pointer) {
570
577
  if (this.debug)
571
578
  console.log("> Register pointer", this.id + "/" + fullPath + " : " + value);
572
- pointer?.register(obj, key);
579
+ pointer?.register(obj, key, fullPath);
573
580
  }
574
581
  else
575
582
  console.warn("WARN: failed registering pointer", fullPath, value);
@@ -589,33 +596,80 @@ class JsonPointerHandler {
589
596
  let pointer = this.map.get(value);
590
597
  if (!pointer) {
591
598
  const definition = context.jsonDoc.json.materials[index];
592
- const res = new MaterialPointerResolver(this.document, mat, definition);
599
+ const res = new MaterialIndexResolver(this.document, mat, definition);
600
+ res.debug = this.debug;
593
601
  res.init(context); // < init here otherwise the fields defined in the TexturePointer will be undefined (this caused e.g. finding the previous texture filter to fail)
594
602
  pointer = new JsonPointer("/materials/", res, value);
595
603
  pointer.debug = this.debug;
596
604
  this.map.set(value, pointer);
597
605
  }
598
- pointer?.register(obj, key);
606
+ pointer?.register(obj, key, fullPath);
599
607
  }
600
608
  }
601
609
  }
602
610
  break;
603
611
  case 'number':
604
- if (key === "node") {
612
+ if (key === "node" || key === "mesh"
613
+ // VRM 1.0 spring bone center https://github.com/vrm-c/vrm-specification/blob/master/specification/VRMC_springBone-1.0/schema/VRMC_springBone.spring.schema.json
614
+ || fullPath.match(/springs\/\d+\/center/g)) {
605
615
  const object = context.nodes[value];
606
616
  if (object) {
607
617
  const res = new NodeIndexResolver(this.document, object, context.jsonDoc.json.nodes[value]);
618
+ res.debug = this.debug;
608
619
  res.init(context);
609
- const dictKey = "/nodes/" + value;
620
+ const dictKey = `/${key}/${value}`;
610
621
  let pointer = this.map.get(dictKey);
611
622
  if (!pointer) {
612
623
  pointer = new JsonPointer(null, res, value);
613
624
  }
614
625
  pointer.debug = this.debug;
615
- pointer.register(obj, key);
626
+ pointer.register(obj, key, fullPath);
616
627
  this.map.set(dictKey, pointer);
617
628
  }
618
629
  }
630
+ else if (
631
+ // key texture is matching for example for VRM meta/texture (AvatarSample_A)
632
+ key === "texture"
633
+ // VRM 0.0 material properties
634
+ || fullPath.match(/materialProperties\/\d+\/textureProperties/g)
635
+ // VRM 1.0 meta
636
+ || fullPath.match(/meta\/thumbnailImage/g)) {
637
+ const index = value;
638
+ // e.g. in meta.texture for VRM avatars it seems like we can have negative values
639
+ if (index < 0) {
640
+ console.warn(`WARN: texture index is negative ${index} at ${fullPath}`);
641
+ return;
642
+ }
643
+ const definition = context.jsonDoc.json.textures[index];
644
+ if (!definition) {
645
+ console.warn(`WARN: texture definition with index ${index} not found or does not exist. Set in \"${fullPath}\"`);
646
+ return;
647
+ }
648
+ const imageIndex = findSource(definition);
649
+ if (this.debug)
650
+ console.log("Texture index", index, imageIndex, context.jsonDoc.json.textures.length, "Textures:" + context.textures.length);
651
+ const object = tryGetInArray(context.textures, imageIndex);
652
+ if (object) {
653
+ if (this.debug)
654
+ console.log("Found texture index at " + fullPath + ": " + value);
655
+ const res = new TextureIndexResolver(this.document, object, definition);
656
+ res.debug = this.debug;
657
+ res.init(context);
658
+ const dictKey = `/textures/${value}`;
659
+ let pointer = this.map.get(dictKey);
660
+ if (!pointer) {
661
+ pointer = new JsonPointer(null, res, value);
662
+ }
663
+ pointer.debug = this.debug;
664
+ pointer.register(obj, key, fullPath);
665
+ this.map.set(dictKey, pointer);
666
+ if (ext) {
667
+ ext.setReference(dictKey, object);
668
+ }
669
+ }
670
+ }
671
+ break;
672
+ default:
619
673
  break;
620
674
  }
621
675
  }
@@ -638,9 +692,11 @@ class JsonPointer {
638
692
  }
639
693
  _objects = [];
640
694
  _props = [];
641
- register(obj, prop) {
695
+ _debugInfo = [];
696
+ register(obj, prop, fullpath) {
642
697
  this._objects.push(obj);
643
698
  this._props.push(prop);
699
+ this._debugInfo.push(fullpath);
644
700
  }
645
701
  newPointer;
646
702
  resolve(context) {
@@ -655,18 +711,19 @@ class JsonPointer {
655
711
  this.newPointer = index;
656
712
  }
657
713
  if (this.debug)
658
- console.log(`RESOLVED ${this.oldPointer} to → ${this.newPointer}`);
714
+ console.log(`RESOLVED pointer ${this.oldPointer} to → ${this.newPointer}`);
659
715
  }
660
716
  else {
661
- console.log("ERR: failed to resolve pointer", this.pointerBase);
717
+ console.log("ERR: 🧨 FAILED to resolve pointer", this.pointerBase, ` used by ${this._debugInfo.join(", ")}`);
662
718
  }
663
719
  }
664
720
  write() {
665
721
  for (let i = 0; i < this._objects.length; i++) {
666
722
  const obj = this._objects[i];
667
723
  const prop = this._props[i];
668
- if (this.debug)
669
- console.log("< Resolved pointer", this.newPointer, "in", prop);
724
+ if (this.debug) {
725
+ console.log(`< Resolved pointer ${this._debugInfo[i]}: ${this.newPointer}`);
726
+ }
670
727
  obj[prop] = this.newPointer;
671
728
  }
672
729
  }
@@ -684,7 +741,7 @@ class PointerResolver {
684
741
  }
685
742
  debug = false;
686
743
  }
687
- class TexturePointerResolver extends PointerResolver {
744
+ class TextureIndexResolver extends PointerResolver {
688
745
  _debugName;
689
746
  _samplerDef;
690
747
  init(context) {
@@ -719,7 +776,7 @@ class TexturePointerResolver extends PointerResolver {
719
776
  // the texture was not found - add it
720
777
  // create a new texture def entry
721
778
  if (this.debug)
722
- console.log("Creating new texture", this._debugName);
779
+ console.log("Could not find texture, creating a new one: " + this._debugName);
723
780
  const hasExtensionWithSource = this.inputDefinition.extensions && Object.keys(this.inputDefinition.extensions).some(key => this.inputDefinition.extensions[key]["source"] !== undefined);
724
781
  const textureDef = {
725
782
  source: hasExtensionWithSource ? undefined : imgIndex,
@@ -774,7 +831,7 @@ function findSource(texDef) {
774
831
  }
775
832
  return undefined;
776
833
  }
777
- class MaterialPointerResolver extends PointerResolver {
834
+ class MaterialIndexResolver extends PointerResolver {
778
835
  init(context) {
779
836
  }
780
837
  resolve(context) {
@@ -1,3 +1,3 @@
1
- import { IOpaqueExtension } from "./NEEDLE_opaque.js";
1
+ import { Extension } from "@gltf-transform/core";
2
2
  export * from "./NEEDLE_opaque.js";
3
- export declare const ALL_EXTENSIONS: IOpaqueExtension[];
3
+ export declare const ALL_EXTENSIONS: (typeof Extension)[];
@@ -17,5 +17,12 @@ export const ALL_EXTENSIONS = [
17
17
  createOpaqueExtension("EXT_texture_exr", [PropertyType.TEXTURE], { ...opaqueOptions }),
18
18
  createOpaqueExtension("NEEDLE_editor", [PropertyType.NODE, PropertyType.MATERIAL], { ...opaqueOptions }),
19
19
  createOpaqueExtension("NEEDLE_gltf_dependencies", [PropertyType.ROOT], { ...opaqueOptions }),
20
+ // VRM 0.0
20
21
  createOpaqueExtension("VRM", [PropertyType.ROOT], { ...opaqueOptions }),
22
+ // VRM 1.0 https://vrm.dev/en/vrm1/changed/
23
+ createOpaqueExtension("VRMC_vrm", [PropertyType.ROOT], { ...opaqueOptions }),
24
+ createOpaqueExtension("VRMC_springBone", [PropertyType.ROOT], { ...opaqueOptions }),
25
+ createOpaqueExtension("VRMC_node_constraint", [PropertyType.NODE], { ...opaqueOptions }),
26
+ // TODO: mtoon materials don't work yet
27
+ // createOpaqueExtension("VRMC_materials_mtoon", [PropertyType.MATERIAL], { ...opaqueOptions, debug: true, breakpoint: "pointer:read" },),
21
28
  ];
@@ -1,7 +1,10 @@
1
1
  import { Extension, ExtensionProperty, PropertyType, ReaderContext, WriterContext } from "@gltf-transform/core";
2
2
  export declare interface IProgressiveMeshSettings {
3
- guid: string;
3
+ /**
4
+ * When false no LODs will be generated for this mesh
5
+ */
4
6
  generateLods: boolean;
7
+ guid: string;
5
8
  }
6
9
  export declare class NEEDLE_progressive_mesh_settings extends Extension {
7
10
  static EXTENSION_NAME: string;
@@ -1,5 +1,9 @@
1
1
  import { Extension, ExtensionProperty, PropertyType, ReaderContext, WriterContext } from "@gltf-transform/core";
2
2
  export declare interface IProgressiveTextureSettings {
3
+ /**
4
+ * When false the texture no progressive texture will be generated
5
+ */
6
+ generateLods?: boolean;
3
7
  maxSize: number;
4
8
  guid: string;
5
9
  }
@@ -1,6 +1,8 @@
1
1
  import { Document, Texture } from '@gltf-transform/core';
2
2
  import { NEEDLE_compression_texture_schema } from '../extensions/index.js';
3
3
  export interface NeedleTransformContext {
4
+ /** full path to the file being processed */
5
+ file: string;
4
6
  debug?: boolean;
5
7
  useCache: boolean;
6
8
  noToktx: boolean;
@@ -1,4 +1,8 @@
1
1
  import { Transform, Document, Primitive, Mesh } from "@gltf-transform/core";
2
2
  import { SimplifyOptions, WeldOptions } from "@gltf-transform/functions";
3
- export declare const needle_mesh_transform: (_options: SimplifyOptions) => Transform;
3
+ declare type MeshTransformContext = {
4
+ file: string;
5
+ };
6
+ export declare const needle_mesh_transform: (context: MeshTransformContext, simplify: Omit<SimplifyOptions, "simplifier">) => Transform;
4
7
  export declare function compressPrimitive(document: Document, mesh: Mesh, prim: Primitive, index: number, weldOptions: Required<WeldOptions>, meshOptions: Required<SimplifyOptions>): Promise<Primitive>;
8
+ export {};
@@ -3,14 +3,14 @@ import { meshopt, simplifyPrimitive, SIMPLIFY_DEFAULTS, weldPrimitive, WELD_DEFA
3
3
  import { MeshoptEncoder, MeshoptSimplifier } from "meshoptimizer";
4
4
  import { NEEDLE_mesh_compression } from "../extensions/index.js";
5
5
  const NAME = 'needle_mesh_transform';
6
- export const needle_mesh_transform = function (_options) {
6
+ export const needle_mesh_transform = function (context, simplify) {
7
7
  return async (document, _context) => {
8
8
  // TODO: needs to be moved into NEEDLE_progressive
9
- await simplifierTransform(document, _context, _options);
10
- await applyMeshCompression(document, _context);
9
+ await simplifierTransform(document, _context, simplify);
10
+ await applyMeshCompression(document, _context, context);
11
11
  };
12
12
  };
13
- async function applyMeshCompression(document, _context) {
13
+ async function applyMeshCompression(document, transform_context, context) {
14
14
  const rootExtension = document.getRoot().getExtension(NEEDLE_mesh_compression.EXTENSION_NAME);
15
15
  if (rootExtension) {
16
16
  const rootModel = rootExtension.model;
@@ -22,17 +22,23 @@ async function applyMeshCompression(document, _context) {
22
22
  console.log(`[${NAME}]: ${rootModel.compression.toUpperCase()} → will apply ${rootModel.compression} mesh compression`);
23
23
  if (rootModel.compression === "draco") {
24
24
  const transform = draco();
25
- return transform(document, _context);
25
+ return transform(document, transform_context);
26
26
  }
27
27
  else if (rootModel.compression === "meshopt") {
28
28
  const transform = meshopt({ encoder: MeshoptEncoder });
29
- return transform(document, _context);
29
+ return transform(document, transform_context);
30
30
  }
31
31
  }
32
32
  }
33
+ // If nothing is configured apply meshopt compression as long as we're running gltf-transform 3.x
34
+ if (context.file.endsWith(".vrm")) {
35
+ console.log(`[${NAME}]: VRM → will apply meshopt mesh compression`);
36
+ const transform = meshopt({ encoder: MeshoptEncoder });
37
+ return transform(document, transform_context);
38
+ }
33
39
  // by default we still want to run draco if the root doesnt have a compression model configured
34
40
  console.log(`[${NAME}]: No compression model configured → will apply draco mesh compression`);
35
- return draco()(document, _context);
41
+ return draco()(document, transform_context);
36
42
  }
37
43
  async function simplifierTransform(document, _context, _options) {
38
44
  const simplifier = MeshoptSimplifier;
@@ -7,13 +7,13 @@ import path, { basename } from 'path';
7
7
  import { existsSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
8
8
  import { addMetaToCache, addToCache, getHash, getKeyWithHash, tryGetFromCache, tryGetMetaFromCache } from '../cache/index.js';
9
9
  import sharp from "sharp";
10
- import { foreachGLTF, generateGuid } from '../utils/index.js';
10
+ import { detectTextureCompressionMode, foreachGLTF, generateGuid, writeNodeIO } from '../utils/index.js';
11
11
  import { needle_asset, writeNeedleAsset } from './needle_asset.js';
12
12
  import { MeshoptDecoder, MeshoptSimplifier } from 'meshoptimizer';
13
13
  import { compressPrimitive } from './needle_mesh_transform.js';
14
14
  import { calculateMeshDensity, getMeshInformation } from "../utils/mesh.js";
15
15
  export async function make_progressive(opts) {
16
- console.log("INFO: Run progressive transform: " + opts.path);
16
+ console.log(`INFO: Run progressive transform: ${opts.path}`);
17
17
  const file = opts.path;
18
18
  return foreachGLTF(file, async (f) => {
19
19
  // Don't re-process files that are already LODs
@@ -38,7 +38,7 @@ function registerExtensions(io) {
38
38
  ]);
39
39
  }
40
40
  async function onProcess(file, opts) {
41
- if (!file.endsWith(".glb") && !file.endsWith(".gltf")) {
41
+ if (!file.endsWith(".glb") && !file.endsWith(".gltf") && !file.endsWith(".vrm")) {
42
42
  console.log("ERR: file is not gltf/glb");
43
43
  return;
44
44
  }
@@ -71,7 +71,19 @@ async function onProcess(file, opts) {
71
71
  }
72
72
  //@ts-ignore
73
73
  document.getLogger().verbosity = 0;
74
- await document.transform(needle_asset(), make_progressive_textures(file, { size: [256, 256] }, opts), make_progressive_meshes(file, {}, opts), sparse());
74
+ const transforms = [
75
+ needle_asset(),
76
+ make_progressive_textures(file, { size: [256, 256] }, opts),
77
+ sparse(),
78
+ ];
79
+ // TODO: progressive meshes aren't supported yet for VRMs
80
+ if (!file.endsWith(".vrm")) {
81
+ transforms.push(make_progressive_meshes(file, {}, opts));
82
+ }
83
+ else
84
+ console.warn("WARN: Progressive meshes are not supported for VRM files yet");
85
+ console.log("→ Transforming: " + file);
86
+ await document.transform(...transforms);
75
87
  let filename = file;
76
88
  if (opts.debug === true) {
77
89
  // get extension
@@ -81,7 +93,8 @@ async function onProcess(file, opts) {
81
93
  // add new extension
82
94
  filename = filename + ".debug." + extension;
83
95
  }
84
- io.write(filename, document);
96
+ await writeNodeIO(io, filename, document);
97
+ console.log("← Transforming done: " + filename);
85
98
  }
86
99
  }
87
100
  catch (err) {
@@ -130,7 +143,7 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
130
143
  if (!settings) {
131
144
  const size = texture.getSize();
132
145
  if (size?.length >= 2 && size[0] <= 128 && size[1] <= 128) {
133
- logger.warn(`[${NAME}] Skipping texture[${textureIndex}], no settings found`);
146
+ logger.warn(`[${NAME}] Skipping texture[${textureIndex}], no settings found and the texture is already < 128x128`);
134
147
  continue;
135
148
  }
136
149
  else {
@@ -141,6 +154,10 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
141
154
  logger.debug(`[${NAME}] No settings found for texture[${index}] - will use default settings (maxSize: ${settings.maxSize}, guid: ${settings.guid})`);
142
155
  }
143
156
  }
157
+ else if (settings.generateLods === false) {
158
+ logger.debug(`[${NAME}] Skipping texture[${textureIndex}]: progressive texture disabled`);
159
+ continue;
160
+ }
144
161
  if (settings.maxSize == undefined || settings.maxSize <= 0) {
145
162
  logger.warn(`[${NAME}] Skipping texture[${textureIndex}], settings are invalid (maxSize: ${settings.maxSize})`);
146
163
  continue;
@@ -158,7 +175,7 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
158
175
  continue;
159
176
  }
160
177
  const textureGuid = settings.guid?.length ? settings.guid : generateGuid(path + "_texture_" + index.toString());
161
- const outPath = createTextureLod(doc, textureLodBasePath, 0, texture, slots, textureGuid, textureIndex);
178
+ const outPath = createTextureLod(doc, filePath, textureLodBasePath, 0, texture, slots, textureGuid, textureIndex);
162
179
  if (!outPath) {
163
180
  continue;
164
181
  }
@@ -194,7 +211,7 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
194
211
  logger.debug(`Progressive transform finished in ${((Date.now() - startTime) / 1000).toFixed(1)} sec.`);
195
212
  });
196
213
  }
197
- function createTextureLod(doc, basePath, level, texture, slots, guid, textureIndex) {
214
+ function createTextureLod(doc, file, basePath, level, texture, slots, guid, textureIndex) {
198
215
  // const imageBytes = texture.getImage();
199
216
  // if (imageBytes) {
200
217
  // // write bytes
@@ -245,6 +262,13 @@ function createTextureLod(doc, basePath, level, texture, slots, guid, textureInd
245
262
  console.log("> Pass compression extension to new texture");
246
263
  const newExt = newDoc.createExtension(NEEDLE_compression_texture);
247
264
  const def = compressionSettings.getExtensionDefinition();
265
+ def.mode = detectTextureCompressionMode(slots);
266
+ def.slot = slots[0];
267
+ // for VRMS we just use webp
268
+ // if (file?.toLocaleLowerCase().endsWith(".vrm")) {
269
+ // def.mode = "webp";
270
+ // def.quality = 70;
271
+ // }
248
272
  const newProp = newExt.create(def);
249
273
  newTexture.setExtension(NEEDLE_compression_texture.EXTENSION_NAME, newProp);
250
274
  }
@@ -1,5 +1,13 @@
1
1
  import { Texture } from "@gltf-transform/core";
2
2
  import { Transform } from '@gltf-transform/core';
3
+ import { NeedleWebPContext } from "./needle_webp.js";
3
4
  import { NeedleTransformContext } from "./needle_common.js";
5
+ import { NeedleToktxContext } from "./needle_toktx.js";
4
6
  export declare function resizeTextureIfNecessary(texture: Texture): Promise<void>;
5
- export declare const needle_texture_transform: (options: NeedleTransformContext) => Transform;
7
+ interface NeedleTextureTransformContext extends NeedleTransformContext {
8
+ webPContext: NeedleWebPContext;
9
+ toktxContext: NeedleToktxContext;
10
+ reportGPUMemory?: (bytes: number) => void;
11
+ }
12
+ export declare const needle_texture_transform: (options: NeedleTransformContext & Pick<NeedleTextureTransformContext, "reportGPUMemory">) => Transform;
13
+ export {};