@needle-tools/gltf-build-pipeline 2.6.0-alpha.1 โ†’ 2.7.0-alpha.1

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,16 @@ 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.7.0-alpha.1] - 2024-01-13
8
+ - fix: handle LOD omitting and remove deleted files from progressive transform results list
9
+
10
+ ## [2.7.0-alpha] - 2024-01-13
11
+ - add: VRM mesh simplification support
12
+ - fix: issue where the output directory was not created if it didn't exist yet
13
+ - fix: try resolve deduplicated meshes
14
+ - change: update gltf-transform to 4.1.1
15
+ - change: modify mesh LOD settings for more vertices
16
+
7
17
  ## [2.6.0-alpha] - 2024-12-16
8
18
  - add: create placeholder textures for missing jpg, png, webp, gif, tif on disc
9
19
  - fix: compressing a gltf file to a different output directory does not falsely delete old external textures on disc in the input directory
@@ -1,4 +1,4 @@
1
- import { Extension, ExtensionProperty, PropertyType, TextureChannel } from '@gltf-transform/core';
1
+ import { Extension, ExtensionProperty, PropertyType, Node, TextureChannel } from '@gltf-transform/core';
2
2
  import { getTexture, tryGetInArray, writeExtension } from '../utils.js';
3
3
  import { isLightmap } from '../../utils/texture.js';
4
4
  const ALL_PROPERTY_TYPES = [];
@@ -369,7 +369,7 @@ export function createOpaqueExtension(name, types, opts) {
369
369
  writeExtension(result, EXTENSION_NAME, this.ext);
370
370
  if (debugLog) {
371
371
  // console.log("WRITE", EXTENSION_NAME, "to", this.gltfField, "at index", index);
372
- console.log(result);
372
+ // console.log("WRITE RESULT", result);
373
373
  }
374
374
  }
375
375
  }
@@ -646,6 +646,26 @@ class JsonPointerHandler {
646
646
  pointer?.register(obj, key, fullPath);
647
647
  }
648
648
  }
649
+ if (value.startsWith("/meshes/")) {
650
+ if (this.debug)
651
+ console.log("Read pointer", value);
652
+ const index = parseInt(value.substring("/meshes/".length));
653
+ const object = context.meshes[index];
654
+ if (object) {
655
+ let pointer = this.map.get(value);
656
+ if (!pointer) {
657
+ const definition = context.jsonDoc.json.meshes[index];
658
+ const res = new MeshIndexResolver(this.document, object, definition, prop);
659
+ res.debug = this.debug;
660
+ res.init(context);
661
+ pointer = new JsonPointer("/meshes/", res, value);
662
+ pointer.debug = this.debug;
663
+ this.map.set(value, pointer);
664
+ prop.setReference(value, object);
665
+ }
666
+ pointer?.register(obj, key, fullPath);
667
+ }
668
+ }
649
669
  }
650
670
  break;
651
671
  case 'number':
@@ -770,14 +790,15 @@ class JsonPointer {
770
790
  else {
771
791
  this.newPointer = index;
772
792
  }
773
- if (this.debug)
774
- console.log(`RESOLVED pointer ${this.oldPointer} to โ†’ ${this.newPointer}`);
775
793
  return true;
776
794
  }
777
795
  else {
778
796
  // If the pointer could not be resolved during write we need to log an error
779
- if (step === "write")
780
- console.log("ERR: ๐Ÿงจ FAILED to resolve pointer", this.pointerBase, ` used by ${this._debugInfo.join(", ")}`);
797
+ if (step === "write") {
798
+ console.log(`ERR: ๐Ÿงจ FAILED to resolve pointer \"${this.oldPointer}\" used by ${this._debugInfo.join(", ")}`);
799
+ if (this.debug)
800
+ debugger;
801
+ }
781
802
  }
782
803
  return false;
783
804
  }
@@ -790,7 +811,7 @@ class JsonPointer {
790
811
  const obj = this._objects[i];
791
812
  const prop = this._props[i];
792
813
  if (this.debug) {
793
- console.log(`< Resolved pointer ${this._debugInfo[i]}: ${this.newPointer}`);
814
+ console.log(`< Resolved pointer ${this._debugInfo[i]}: ${this.oldPointer} โ†’ ${this.newPointer}`);
794
815
  }
795
816
  obj[prop] = this.newPointer;
796
817
  }
@@ -802,10 +823,13 @@ class PointerResolver {
802
823
  inputObject;
803
824
  /* the original json definition */
804
825
  inputDefinition;
805
- constructor(doc, obj, def) {
826
+ /** The graph property this pointer belongs to */
827
+ property;
828
+ constructor(doc, obj, def, prop) {
806
829
  this.document = doc;
807
830
  this.inputObject = obj;
808
831
  this.inputDefinition = def;
832
+ this.property = prop;
809
833
  }
810
834
  debug = false;
811
835
  }
@@ -913,3 +937,35 @@ class NodeIndexResolver extends PointerResolver {
913
937
  return context.nodeIndexMap.get(this.inputObject);
914
938
  }
915
939
  }
940
+ class MeshIndexResolver extends PointerResolver {
941
+ init(context) {
942
+ }
943
+ resolve(context) {
944
+ let index = context.meshIndexMap.get(this.inputObject);
945
+ if (index >= 0) {
946
+ return index;
947
+ }
948
+ let obj = null;
949
+ if (this.inputObject.isDisposed()) {
950
+ // If the mesh was deduplicated we try to get the mesh from a parent node
951
+ // This is a HACK, we need to get the reference to the mesh this one was deduplicated with since the parent node potentially has many children
952
+ const parents = this.property?.listParents();
953
+ for (const parent of parents) {
954
+ if (parent instanceof Node) {
955
+ obj = parent.getMesh();
956
+ if (obj) {
957
+ console.log(`INFO: resolved mesh \"${this.inputObject.getName()}\" from parent node \"${parent.getName()}\"`);
958
+ break;
959
+ }
960
+ }
961
+ }
962
+ if (obj.isDisposed()) {
963
+ console.warn("WARN: mesh is disposed", this.inputObject.getName());
964
+ }
965
+ }
966
+ if (!obj) {
967
+ return undefined;
968
+ }
969
+ return context.meshIndexMap.get(obj);
970
+ }
971
+ }
@@ -81,10 +81,11 @@ export async function packGLTF(inputFile, outputFile, options) {
81
81
  },
82
82
  config,
83
83
  }),
84
- needle_mesh_transform({ file: inputFile, config }, { ratio: 0.75, error: 0.0005 }),
84
+ needle_mesh_transform({ file: inputFile, config }),
85
85
  needle_animation_transform({}),
86
86
  resample(),
87
87
  // Can not dedup "materials" because progressive textures will then assigned to the wrong material instance
88
+ // HACK: keepUniqueNames is enabled to fix https://linear.app/needle/issue/NE-6288
88
89
  dedup({ propertyTypes: [PropertyType.MESH, PropertyType.MATERIAL] }),
89
90
  // prune(),
90
91
  ];
@@ -102,7 +103,7 @@ export async function packGLTF(inputFile, outputFile, options) {
102
103
  transforms.push(join({ keepNamed: false }));
103
104
  }
104
105
  await document.transform(...transforms);
105
- logger.info(`โ‡„ Writing to ${outputFile}`);
106
+ logger.info(`โ† Writing to ${outputFile}`);
106
107
  await writeNodeIO(io, outputFile, document);
107
108
  if (useCache && cacheKey != undefined && existsSync(outputFile))
108
109
  addToCache(cacheKey, readFileSync(outputFile));
@@ -10,6 +10,6 @@ declare type MeshTransformContext = {
10
10
  * Which compression type for a model should be used. This should be respected when creating LOD meshes and the appropriate extension should be added to the LOD mesh so it receives the same mesh compression as the parent mesh
11
11
  */
12
12
  export declare function determineMeshCompression(document: Document, filePath: string): NEEDLE_mesh_compression_model_root;
13
- export declare const needle_mesh_transform: (context: MeshTransformContext, simplify: Omit<SimplifyOptions, "simplifier">) => Transform;
13
+ export declare const needle_mesh_transform: (context: MeshTransformContext) => Transform;
14
14
  export declare function compressPrimitive(document: Document, mesh: Mesh, prim: Primitive, index: number, weldOptions: Required<WeldOptions>, meshOptions: Required<SimplifyOptions>): Promise<Primitive>;
15
15
  export {};
@@ -3,6 +3,7 @@ import { meshopt, simplifyPrimitive, SIMPLIFY_DEFAULTS, WELD_DEFAULTS, draco } f
3
3
  import { MeshoptEncoder, MeshoptSimplifier } from "meshoptimizer";
4
4
  import { NEEDLE_mesh_compression, NEEDLE_mesh_compression_model_root } from "../extensions/index.js";
5
5
  import { weldPrimitive } from "../functions/weld.3.js";
6
+ import { calculateMeshDensity } from "../utils/mesh.js";
6
7
  const NAME = 'needle_mesh_transform';
7
8
  /**
8
9
  * Which compression type for a model should be used. This should be respected when creating LOD meshes and the appropriate extension should be added to the LOD mesh so it receives the same mesh compression as the parent mesh
@@ -18,7 +19,7 @@ export function determineMeshCompression(document, filePath) {
18
19
  return rootModel;
19
20
  }
20
21
  }
21
- if (filePath.endsWith(".vrm")) {
22
+ if (filePath?.toLowerCase().endsWith(".vrm")) {
22
23
  const hasBlendshapes = document.getRoot().listMeshes().some((mesh) => mesh.listPrimitives().some((prim) => prim.listTargets().length > 0));
23
24
  if (hasBlendshapes) {
24
25
  return new NEEDLE_mesh_compression_model_root("meshopt");
@@ -57,10 +58,10 @@ export function determineMeshCompression(document, filePath) {
57
58
  // }
58
59
  return new NEEDLE_mesh_compression_model_root("draco");
59
60
  }
60
- export const needle_mesh_transform = function (context, simplify) {
61
+ export const needle_mesh_transform = function (context) {
61
62
  return async (document, _context) => {
62
63
  // TODO: needs to be moved into NEEDLE_progressive
63
- await simplifierTransform(document, _context, simplify);
64
+ await simplifierTransform(document, _context, context);
64
65
  await applyMeshCompression(document, _context, context);
65
66
  };
66
67
  };
@@ -88,21 +89,28 @@ async function applyMeshCompression(document, transform_context, context) {
88
89
  document.getLogger().info(`[${NAME}]: โ†’ will apply draco mesh compression (default)`);
89
90
  return draco()(document, transform_context);
90
91
  }
91
- async function simplifierTransform(document, _context, _options) {
92
+ async function simplifierTransform(document, _context, meshContext) {
92
93
  const simplifier = MeshoptSimplifier;
93
94
  if (!simplifier) {
94
95
  console.log(`WARN: [${NAME}] simplifier dependency required โ€” install "meshoptimizer".`);
95
96
  return false;
96
97
  }
97
98
  const logger = document.getLogger();
98
- const options = { ...SIMPLIFY_DEFAULTS, ..._options };
99
- if (!options.simplifier)
100
- options.simplifier = simplifier;
99
+ const isVRM = meshContext.file?.toLowerCase().endsWith(".vrm");
100
+ const options = {
101
+ ...SIMPLIFY_DEFAULTS,
102
+ simplifier: simplifier,
103
+ ratio: 0.75,
104
+ error: 0.0005,
105
+ };
101
106
  const weldOptions = {
102
107
  ...WELD_DEFAULTS,
103
- tolerance: 0.00000000001,
104
108
  overwrite: true,
105
109
  };
110
+ // let meshInformation: MeshInformation | null = null;
111
+ // if (isVRM) {
112
+ // meshInformation = getMeshInformation(document);
113
+ // }
106
114
  for (const mesh of document.getRoot().listMeshes()) {
107
115
  let i = 0;
108
116
  for (const prim of mesh.listPrimitives()) {
@@ -119,9 +127,25 @@ async function simplifierTransform(document, _context, _options) {
119
127
  const ext = prim.getExtension(NEEDLE_mesh_compression.EXTENSION_NAME);
120
128
  const mesh_compression = ext?.model;
121
129
  const simplifyOpts = { ...options };
122
- if (!ext) {
130
+ if (isVRM) {
131
+ const density = calculateMeshDensity(prim);
132
+ if (density > 0) {
133
+ const targetDensity = 2_000;
134
+ const t = (1 / density) * targetDensity;
135
+ simplifyOpts.ratio = Math.min(Math.max(.1, t), .95);
136
+ simplifyOpts.error = 15 / density;
137
+ simplifyOpts.lockBorder = true;
138
+ }
139
+ else {
140
+ simplifyOpts.ratio = 0.8;
141
+ simplifyOpts.error = 0.001;
142
+ simplifyOpts.lockBorder = true;
143
+ }
144
+ logger.info(`${mesh.getName()}: Density: ${density.toFixed(2)}, Ratio: ${simplifyOpts.ratio.toFixed(2)}, Error: ${simplifyOpts.error}`);
145
+ }
146
+ if (!ext && !isVRM) {
123
147
  // If no extension exists on the primitive dont apply any simplification
124
- // console.log(`[${NAME}]: Skipping primitive simplification of mesh "${mesh.getName()}": No mesh compression extension found.`);
148
+ // AND if the file is not a VRM file
125
149
  continue;
126
150
  }
127
151
  if (mesh_compression) {
@@ -9,7 +9,7 @@ import { addMetaToCache, addToCache, getHash, getKey, tryGetFromCache, tryGetMet
9
9
  import sharp from "sharp";
10
10
  import { detectTextureCompressionMode, ensureIsDirectory, foreachGLTF, generateGuid, getOutputPath, isLightmap, setLightmapName, ioTryReadWithMissingResources, validateImageMimeType, writeNodeIO } from '../utils/index.js';
11
11
  import { needle_asset, writeNeedleAsset } from './needle_asset.js';
12
- import { MeshoptDecoder, MeshoptSimplifier } from 'meshoptimizer';
12
+ import { MeshoptDecoder, MeshoptEncoder, MeshoptSimplifier } from 'meshoptimizer';
13
13
  import { compressPrimitive, determineMeshCompression } from './needle_mesh_transform.js';
14
14
  import { calculateMeshDensity, getMeshInformation } from "../utils/mesh.js";
15
15
  export async function make_progressive(opts) {
@@ -29,6 +29,19 @@ export async function make_progressive(opts) {
29
29
  if (!res)
30
30
  success = false;
31
31
  });
32
+ if (success) {
33
+ // Make sure all results exist
34
+ // It's possible that files were deleted while omitting LODs - we want to remove them from the results list
35
+ const resultsCount = opts.results.length;
36
+ for (let i = opts.results.length - 1; i >= 0; i--) {
37
+ if (!existsSync(opts.results[i])) {
38
+ opts.results.splice(i, 1);
39
+ }
40
+ }
41
+ const resultsChanged = resultsCount !== opts.results.length;
42
+ if (opts.debug)
43
+ console.log(`Progressive Transform: ${opts.results.length} files processed. ${resultsChanged ? "Some results were removed" : ""}`);
44
+ }
32
45
  return success;
33
46
  }
34
47
  function registerExtensions(io) {
@@ -58,6 +71,7 @@ async function onProcess(file, opts) {
58
71
  registerExtensions(io);
59
72
  io.registerDependencies({
60
73
  'meshopt.decoder': MeshoptDecoder,
74
+ 'meshopt.encoder': MeshoptEncoder,
61
75
  'draco3d.decoder': await draco3d.createDecoderModule(),
62
76
  'draco3d.encoder': await draco3d.createEncoderModule(),
63
77
  });
@@ -533,7 +547,7 @@ export function make_progressive_meshes(filePath, _options, opts) {
533
547
  // const t = l / MAX_LODS;
534
548
  const latest = LODS[LODS.length - 1];
535
549
  const ratio = latest.ratio * .5;
536
- let error = Math.min(.013 * (l + 1), 0.05);
550
+ let error = Math.min(.013 * (l + 1), 0.003);
537
551
  if (opts.config.usecase === "product") {
538
552
  error *= .5;
539
553
  }
@@ -544,6 +558,7 @@ export function make_progressive_meshes(filePath, _options, opts) {
544
558
  // toleranceNormal: 0.1 + t * 0.1,
545
559
  });
546
560
  }
561
+ logger.debug(`[${NAME}]: Generating mesh LODs:\n${JSON.stringify(LODS, undefined, 2)}`);
547
562
  /** Map of visited GUID -> extension */
548
563
  const visited = new Map();
549
564
  const root = doc.getRoot();
@@ -659,7 +674,7 @@ export function make_progressive_meshes(filePath, _options, opts) {
659
674
  if (shouldOmitLevel) {
660
675
  logger.debug(`[${NAME}] Omitting LOD ${level} for \"${mesh.getName()}\" at \"${lodInfo.path}\"`);
661
676
  generatedLODInfos.splice(i, 1);
662
- rmSync(fullPath);
677
+ deleteFile(fullPath, opts);
663
678
  i--;
664
679
  }
665
680
  else {
@@ -697,7 +712,7 @@ export function make_progressive_meshes(filePath, _options, opts) {
697
712
  logger.info(`[${NAME}]: Flag mesh LOD ${generatedLODInfos.length} (embedded): vertex count difference below threshold (removed ${difference} vertices, ${(-diff01 * 100).toFixed(2)} %) โ†’ will remove all LODs. \"${mesh.getName()}\"`);
698
713
  try {
699
714
  for (const lodInfo of generatedLODInfos) {
700
- rmSync(outputDirectory + "/" + lodInfo.path);
715
+ deleteFile(outputDirectory + "/" + lodInfo.path, opts);
701
716
  }
702
717
  }
703
718
  catch (err) {
@@ -742,6 +757,9 @@ export function make_progressive_meshes(filePath, _options, opts) {
742
757
  }
743
758
  });
744
759
  }
760
+ function deleteFile(filePath, _context) {
761
+ rmSync(filePath);
762
+ }
745
763
  function clonePrimitiveData(prim, logger) {
746
764
  const semantics = prim.listSemantics();
747
765
  if (semantics.some(s => s.includes("JOINTS"))) {
@@ -842,6 +860,8 @@ async function createMeshLod(inputFileName, doc, basePath, level, mesh, meshInfo
842
860
  const newAttr = newDoc.createAccessor();
843
861
  newAttr.setArray(attr.getArray());
844
862
  newAttr.setType(attr.getType());
863
+ newAttr.setSparse(attr.getSparse());
864
+ newAttr.setNormalized(attr.getNormalized());
845
865
  newPrim.setAttribute(semantics[i], newAttr);
846
866
  }
847
867
  else {
@@ -853,20 +873,29 @@ async function createMeshLod(inputFileName, doc, basePath, level, mesh, meshInfo
853
873
  const newIndices = newDoc.createAccessor();
854
874
  newIndices.setArray(indices.getArray());
855
875
  newIndices.setType(indices.getType());
876
+ newIndices.setSparse(indices.getSparse());
877
+ newIndices.setNormalized(indices.getNormalized());
856
878
  newPrim.setIndices(newIndices);
857
879
  }
858
880
  const targets = prim.listTargets();
859
881
  for (const target of targets) {
860
882
  const newTarget = newDoc.createPrimitiveTarget(target.getName());
861
883
  newPrim.addTarget(newTarget);
862
- for (const attr of target.listSemantics()) {
884
+ // const attributes = target.listAttributes();
885
+ const semantics = target.listSemantics();
886
+ for (const attr of semantics) {
863
887
  const accessor = target.getAttribute(attr);
864
888
  if (accessor) {
865
889
  const newAccessor = newDoc.createAccessor();
866
890
  newAccessor.setArray(accessor.getArray());
867
891
  newAccessor.setType(accessor.getType());
892
+ newAccessor.setSparse(accessor.getSparse());
893
+ newAccessor.setNormalized(accessor.getNormalized());
868
894
  newTarget.setAttribute(attr, newAccessor);
869
895
  }
896
+ else {
897
+ console.error(`[${NAME}] Missing target attribute ${attr} in mesh LOD ${level}`);
898
+ }
870
899
  }
871
900
  }
872
901
  ;
@@ -1,10 +1,10 @@
1
- import { Mesh, Primitive } from "@gltf-transform/core";
1
+ import { Document, Mesh, Primitive } from "@gltf-transform/core";
2
2
  export declare type MeshInformation = {
3
3
  readonly maxVertexCount: number;
4
4
  readonly totalVertexCount: number;
5
5
  readonly totalIndexCount: number;
6
6
  };
7
- export declare function getMeshInformation(mesh: Mesh): MeshInformation;
7
+ export declare function getMeshInformation(mesh: Mesh | Document): MeshInformation;
8
8
  /**
9
9
  * Calculate the density of a mesh.
10
10
  * @param prim - The primitive to calculate the density for.
@@ -1,18 +1,35 @@
1
+ import { Mesh } from "@gltf-transform/core";
1
2
  export function getMeshInformation(mesh) {
2
3
  let maxVertexCount = 0;
3
4
  let totalIndexCount = 0;
4
- const totalVertexCount = mesh.listPrimitives().reduce((acc, prim) => {
5
- const indices = prim.getIndices()?.getCount() || 0;
6
- totalIndexCount += indices;
7
- const vertices = prim.getAttribute('POSITION')?.getCount() || 0;
8
- maxVertexCount = Math.max(maxVertexCount, vertices);
9
- return acc + vertices;
10
- }, 0);
11
- return {
12
- maxVertexCount,
13
- totalVertexCount,
14
- totalIndexCount,
15
- };
5
+ let totalVertexCount = 0;
6
+ if (mesh instanceof Mesh) {
7
+ totalVertexCount = mesh.listPrimitives().reduce((acc, prim) => {
8
+ const indices = prim.getIndices()?.getCount() || 0;
9
+ totalIndexCount += indices;
10
+ const vertices = prim.getAttribute('POSITION')?.getCount() || 0;
11
+ maxVertexCount = Math.max(maxVertexCount, vertices);
12
+ return acc + vertices;
13
+ }, 0);
14
+ return {
15
+ maxVertexCount,
16
+ totalVertexCount,
17
+ totalIndexCount,
18
+ };
19
+ }
20
+ else {
21
+ mesh.getRoot().listMeshes().forEach(mesh => {
22
+ const info = getMeshInformation(mesh);
23
+ maxVertexCount = Math.max(maxVertexCount, info.maxVertexCount);
24
+ totalVertexCount += info.totalVertexCount;
25
+ totalIndexCount += info.totalIndexCount;
26
+ });
27
+ return {
28
+ maxVertexCount,
29
+ totalVertexCount,
30
+ totalIndexCount,
31
+ };
32
+ }
16
33
  }
17
34
  /**
18
35
  * Calculate the density of a mesh.
@@ -1,9 +1,15 @@
1
+ import { existsSync, mkdirSync } from "fs";
1
2
  import { writeFile } from "fs/promises";
3
+ import path from "path";
2
4
  import sharp from "sharp";
3
5
  /**
4
6
  * Handle writing glTf, GLB and VRM files
5
7
  */
6
8
  export async function writeNodeIO(nodeio, filename, document) {
9
+ const directoryPath = path.dirname(filename);
10
+ if (!existsSync(directoryPath)) {
11
+ mkdirSync(directoryPath);
12
+ }
7
13
  // see https://github.com/donmccurdy/glTF-Transform/issues/1390
8
14
  if (filename.endsWith(".vrm")) {
9
15
  const bin = await nodeio.writeBinary(document);
@@ -0,0 +1 @@
1
+ export declare const version = "2.7.0-alpha.1";
@@ -0,0 +1 @@
1
+ export const version = "2.7.0-alpha.1";
@@ -1,5 +1,4 @@
1
- import packageJson from "#package.json" assert { type: "json" };
1
+ import { version } from './version.gen.js';
2
2
  export function getVersion(_marklocal = true) {
3
- let res = packageJson.version;
4
- return res;
3
+ return version;
5
4
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/gltf-build-pipeline",
3
- "version": "2.6.0-alpha.1",
3
+ "version": "2.7.0-alpha.1",
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",
@@ -47,9 +47,9 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@donmccurdy/caporal": "~0.0.10",
50
- "@gltf-transform/core": "^4.0.8",
51
- "@gltf-transform/extensions": "^4.0.8",
52
- "@gltf-transform/functions": "^4.0.8",
50
+ "@gltf-transform/core": "^4.1.1",
51
+ "@gltf-transform/extensions": "^4.1.1",
52
+ "@gltf-transform/functions": "^4.1.1",
53
53
  "command-exists": "^1.2.9",
54
54
  "command-line-args": "^5.2.1",
55
55
  "draco3dgltf": "^1.5.3",