@forgeax/engine-gltf 0.1.21 → 0.1.24

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 (68) hide show
  1. package/README.md +115 -3
  2. package/dist/__tests__/lod-import.integration.test.d.ts +2 -0
  3. package/dist/__tests__/lod-import.integration.test.d.ts.map +1 -0
  4. package/dist/__tests__/lod-meta-publication.integration.test.d.ts +2 -0
  5. package/dist/__tests__/lod-meta-publication.integration.test.d.ts.map +1 -0
  6. package/dist/__tests__/physical-clearcoat.unit.test.d.ts +2 -0
  7. package/dist/__tests__/physical-clearcoat.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/physical-material-import.integration.test.d.ts +2 -0
  9. package/dist/__tests__/physical-material-import.integration.test.d.ts.map +1 -0
  10. package/dist/bridge.d.ts +1 -0
  11. package/dist/bridge.d.ts.map +1 -1
  12. package/dist/check-extensions.d.ts +1 -1
  13. package/dist/check-extensions.d.ts.map +1 -1
  14. package/dist/cli-gltf.mjs +362 -14
  15. package/dist/cli-gltf.mjs.map +1 -1
  16. package/dist/errors.d.ts +13 -0
  17. package/dist/errors.d.ts.map +1 -1
  18. package/dist/gltf-importer.d.ts.map +1 -1
  19. package/dist/image-color-space.d.ts +12 -0
  20. package/dist/image-color-space.d.ts.map +1 -1
  21. package/dist/importer-entry.mjs +516 -25
  22. package/dist/importer-entry.mjs.map +1 -1
  23. package/dist/index.d.ts +4 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.mjs +609 -28
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/lod/parse-lod.d.ts +15 -0
  28. package/dist/lod/parse-lod.d.ts.map +1 -0
  29. package/dist/lod/project-meta.d.ts +23 -0
  30. package/dist/lod/project-meta.d.ts.map +1 -0
  31. package/dist/material/parse-material.d.ts +66 -0
  32. package/dist/material/parse-material.d.ts.map +1 -1
  33. package/dist/node-file-entry.mjs +265 -8
  34. package/dist/node-file-entry.mjs.map +1 -1
  35. package/dist/parse-gltf.d.ts +3 -1
  36. package/dist/parse-gltf.d.ts.map +1 -1
  37. package/dist/reimport-reuse-meta.d.ts +1 -1
  38. package/dist/reimport-reuse-meta.d.ts.map +1 -1
  39. package/package.json +12 -12
  40. package/src/__tests__/bridge-material-values.unit.test.ts +84 -1
  41. package/src/__tests__/bridge-skin-stride.unit.test.ts +22 -0
  42. package/src/__tests__/fixtures/lod/core-only.gltf +1 -0
  43. package/src/__tests__/fixtures/lod/msft-lod-malformed.gltf +1 -0
  44. package/src/__tests__/fixtures/lod/msft-lod-multi-root.gltf +1 -0
  45. package/src/__tests__/fixtures/lod/msft-lod-node-extras.gltf +1 -0
  46. package/src/__tests__/fixtures/lod/msft-lod.gltf +1 -0
  47. package/src/__tests__/gltf-error-derived-views.test-d.ts +8 -0
  48. package/src/__tests__/gltf-error-migration.test.ts +3 -3
  49. package/src/__tests__/gltf.unit.test.ts +60 -5
  50. package/src/__tests__/lod-import.integration.test.ts +106 -0
  51. package/src/__tests__/lod-meta-publication.integration.test.ts +24 -0
  52. package/src/__tests__/material-pack-refs.integration.test.ts +53 -0
  53. package/src/__tests__/parse-gltf.unit.test.ts +50 -0
  54. package/src/__tests__/physical-clearcoat.unit.test.ts +110 -0
  55. package/src/__tests__/physical-material-import.integration.test.ts +239 -0
  56. package/src/__tests__/source-key-producer.integration.test.ts +6 -2
  57. package/src/bridge.ts +259 -14
  58. package/src/check-extensions.ts +16 -8
  59. package/src/errors.ts +32 -2
  60. package/src/gltf-importer.ts +122 -1
  61. package/src/image-color-space.ts +29 -4
  62. package/src/index.ts +10 -1
  63. package/src/lod/parse-lod.ts +129 -0
  64. package/src/lod/project-meta.ts +48 -0
  65. package/src/material/parse-material.ts +307 -0
  66. package/src/parse-gltf.ts +97 -1
  67. package/src/reimport-reuse-meta.ts +3 -1
  68. package/dist/.tsbuildinfo +0 -1
@@ -1,8 +1,9 @@
1
1
  import { MeshoptDecoder } from 'meshoptimizer';
2
+ import { deriveDefaultLodScreenCoverages } from '@forgeax/engine-import';
2
3
  import { packMeshBinV4 } from '@forgeax/engine-import/mesh-bin';
3
4
  import { AssetGuid } from '@forgeax/engine-pack/guid';
4
- import { ImportError, toShared, err, ok, reconcileMeshMaterialSlotTopology, IMPORT_ERROR_HINTS, resolveMeshMaterialSlotDefaultGuid, createMaterialError } from '@forgeax/engine-types';
5
- import { packInterleavedVertexAttributes } from '@forgeax/engine-geometry';
5
+ import { ImportError, toShared, err, ok, reconcileMeshMaterialSlotTopology, IMPORT_ERROR_HINTS, resolveMeshMaterialSlotDefaultGuid, createMaterialError, standardMaterialParameters, STANDARD_MATERIAL_PARAM_SCHEMA, STANDARD_PHYSICAL_PARAMETER_NAMES, STANDARD_TRANSMISSION_PARAMETER_NAMES, STANDARD_LAYER_PARAMETER_GROUPS } from '@forgeax/engine-types';
6
+ import { computeTangentVec4, packInterleavedVertexAttributes } from '@forgeax/engine-geometry';
6
7
  import { box3, mat4, vec3, quat } from '@forgeax/engine-math';
7
8
  import { deriveAnimationTargetId } from '@forgeax/engine-animation/target-id';
8
9
 
@@ -23,13 +24,21 @@ var gltfErrorPolicy = {
23
24
  hint: "rebuild .gltf with valid bufferViews; check accessor index; ensure accessor.byteOffset + EFFECTIVE_STRIDE * (count - 1) + element_size <= bufferView.byteLength"
24
25
  },
25
26
  "gltf-extension-unsupported": {
26
- expected: "extension listed in v1 allowlist (see EXTENSION_ALLOWLIST in @forgeax/engine-gltf)",
27
- hint: "see feat-future-gltf-extensions-allowlist; remove this extension or wait for the allowlist to expand"
27
+ expected: "extension listed in the supported allowlist (see EXTENSION_ALLOWLIST in @forgeax/engine-gltf)",
28
+ hint: "remove the unsupported required extension or use a supported glTF extension; extensionsUsed-only entries remain diagnostic"
29
+ },
30
+ "gltf-lod-invalid": {
31
+ expected: "MSFT_lod ids to reference unique existing node indices with valid coverage",
32
+ hint: "repair the MSFT_lod node relation or remove it from extensionsRequired before re-importing"
28
33
  },
29
34
  "gltf-material-transmission-invalid": {
30
35
  expected: "KHR transmission, IOR, and volume values are finite and within their glTF ranges",
31
36
  hint: "repair the named glTF material extension value and re-import the source"
32
37
  },
38
+ "gltf-material-physical-invalid": {
39
+ expected: "KHR clearcoat, anisotropy, sheen, iridescence, and specular values are finite and within their glTF ranges",
40
+ hint: "repair the named physical material extension value and re-import the source"
41
+ },
33
42
  "gltf-accessor-type-mismatch": {
34
43
  expected: "dense fixed-stride accessor with supported componentType",
35
44
  hint: "sparse: see feat-future-gltf-sparse-accessor; morph: see feat-future-gltf-morph; interleaved: see feat-future-gltf-mesh-multi-section"
@@ -213,6 +222,16 @@ function meshIrToMeshAsset(prims, materials = {}) {
213
222
  const materialSlot = slotFor(mesh.materialIndex);
214
223
  const primVertexCount = mesh.positions.length / 3;
215
224
  const primIndexCount = mesh.indices === void 0 ? 0 : mesh.indices.length;
225
+ let generatedTangents;
226
+ if (mesh.tangents === void 0 && mesh.normals !== void 0 && mesh.texcoord0 !== void 0) {
227
+ const generated = computeTangentVec4(
228
+ mesh.positions,
229
+ mesh.normals,
230
+ mesh.texcoord0,
231
+ mesh.indices
232
+ );
233
+ if (generated.ok) generatedTangents = generated.value;
234
+ }
216
235
  if (mesh.colors0 !== void 0 && mesh.colors0.length !== primVertexCount * 4) {
217
236
  return err(
218
237
  gltfErr("gltf-mesh-bridge-invalid", {
@@ -244,12 +263,13 @@ function meshIrToMeshAsset(prims, materials = {}) {
244
263
  uvsCat[(vertexCursor + i) * 2 + 0] = mesh.texcoord0[t + 0];
245
264
  uvsCat[(vertexCursor + i) * 2 + 1] = mesh.texcoord0[t + 1];
246
265
  }
247
- if (mesh.tangents !== void 0) {
266
+ const sourceTangents = mesh.tangents ?? generatedTangents;
267
+ if (sourceTangents !== void 0) {
248
268
  const g = i * 4;
249
- tangentsCat[(vertexCursor + i) * 4 + 0] = mesh.tangents[g + 0];
250
- tangentsCat[(vertexCursor + i) * 4 + 1] = mesh.tangents[g + 1];
251
- tangentsCat[(vertexCursor + i) * 4 + 2] = mesh.tangents[g + 2];
252
- tangentsCat[(vertexCursor + i) * 4 + 3] = mesh.tangents[g + 3];
269
+ tangentsCat[(vertexCursor + i) * 4 + 0] = sourceTangents[g + 0];
270
+ tangentsCat[(vertexCursor + i) * 4 + 1] = sourceTangents[g + 1];
271
+ tangentsCat[(vertexCursor + i) * 4 + 2] = sourceTangents[g + 2];
272
+ tangentsCat[(vertexCursor + i) * 4 + 3] = sourceTangents[g + 3];
253
273
  } else {
254
274
  tangentsCat[(vertexCursor + i) * 4 + 0] = 1;
255
275
  tangentsCat[(vertexCursor + i) * 4 + 3] = 1;
@@ -584,6 +604,12 @@ function textureValue(info, slot, ctx) {
584
604
  return { ...value, normalScale: normal.scale };
585
605
  }
586
606
  }
607
+ if (slot === "clearcoatNormalTexture") {
608
+ const normal = info;
609
+ if (typeof normal === "object" && normal?.scale !== void 0) {
610
+ return { ...value, normalScale: normal.scale };
611
+ }
612
+ }
587
613
  if (slot === "occlusionTexture") {
588
614
  const occlusion = info;
589
615
  if (typeof occlusion === "object" && occlusion?.strength !== void 0) {
@@ -592,6 +618,47 @@ function textureValue(info, slot, ctx) {
592
618
  }
593
619
  return value;
594
620
  }
621
+ function standardRootParameterNames(mat) {
622
+ const names = new Set(
623
+ STANDARD_MATERIAL_PARAM_SCHEMA.filter(
624
+ (entry) => !STANDARD_PHYSICAL_PARAMETER_NAMES.has(entry.name) && !STANDARD_TRANSMISSION_PARAMETER_NAMES.has(entry.name)
625
+ ).map((entry) => entry.name)
626
+ );
627
+ names.add("ior");
628
+ const addLayer = (layer) => {
629
+ for (const name of STANDARD_LAYER_PARAMETER_GROUPS[layer]) names.add(name);
630
+ };
631
+ const addTexture = (name, info) => {
632
+ if (info !== void 0) names.add(name);
633
+ };
634
+ const clearcoat = mat.clearcoatFactor !== void 0 || mat.clearcoatRoughnessFactor !== void 0 || mat.clearcoatNormalTexture !== void 0 || mat.clearcoatTexture !== void 0 || mat.clearcoatRoughnessTexture !== void 0;
635
+ const anisotropy = mat.anisotropyStrength !== void 0 || mat.anisotropyRotation !== void 0 || mat.anisotropyTexture !== void 0;
636
+ const sheen = mat.sheenColorFactor !== void 0 || mat.sheenRoughnessFactor !== void 0 || mat.sheenColorTexture !== void 0 || mat.sheenRoughnessTexture !== void 0;
637
+ const iridescence = mat.iridescenceFactor !== void 0 || mat.iridescenceIor !== void 0 || mat.iridescenceThicknessMinimum !== void 0 || mat.iridescenceThicknessMaximum !== void 0 || mat.iridescenceTexture !== void 0 || mat.iridescenceThicknessTexture !== void 0;
638
+ if (clearcoat) addLayer("clearcoat");
639
+ if (anisotropy) addLayer("anisotropy");
640
+ if (sheen) addLayer("sheen");
641
+ if (iridescence) addLayer("iridescence");
642
+ if (clearcoat && mat.clearcoatNormalTexture !== void 0) names.add("clearcoatNormalScale");
643
+ addTexture("clearcoatTexture", mat.clearcoatTexture);
644
+ addTexture("clearcoatRoughnessTexture", mat.clearcoatRoughnessTexture);
645
+ addTexture("clearcoatNormalTexture", mat.clearcoatNormalTexture);
646
+ addTexture("anisotropyTexture", mat.anisotropyTexture);
647
+ addTexture("sheenColorTexture", mat.sheenColorTexture);
648
+ addTexture("sheenRoughnessTexture", mat.sheenRoughnessTexture);
649
+ addTexture("iridescenceTexture", mat.iridescenceTexture);
650
+ addTexture("iridescenceThicknessTexture", mat.iridescenceThicknessTexture);
651
+ addTexture("specularTexture", mat.specularTexture);
652
+ addTexture("specularColorTexture", mat.specularColorTexture);
653
+ const transmission = mat.transmissionFactor !== void 0 || mat.transmissionTexture !== void 0 || mat.ior !== void 0 || mat.thicknessFactor !== void 0 || mat.thicknessTexture !== void 0 || mat.attenuationColor !== void 0 || mat.attenuationDistance !== void 0;
654
+ if (transmission) {
655
+ for (const name of STANDARD_TRANSMISSION_PARAMETER_NAMES) names.add(name);
656
+ }
657
+ return {
658
+ names,
659
+ extended: clearcoat || anisotropy || sheen || iridescence || transmission || mat.specularTexture !== void 0 || mat.specularColorTexture !== void 0
660
+ };
661
+ }
595
662
  function validateMaterialUvSets(mat, primitive, availableSets) {
596
663
  const available = new Set(availableSets);
597
664
  const slots = [
@@ -601,7 +668,17 @@ function validateMaterialUvSets(mat, primitive, availableSets) {
601
668
  ["occlusionTexture", mat.occlusionTexture],
602
669
  ["emissiveTexture", mat.emissiveTexture],
603
670
  ["transmissionTexture", mat.transmissionTexture],
604
- ["thicknessTexture", mat.thicknessTexture]
671
+ ["thicknessTexture", mat.thicknessTexture],
672
+ ["clearcoatTexture", mat.clearcoatTexture],
673
+ ["clearcoatRoughnessTexture", mat.clearcoatRoughnessTexture],
674
+ ["clearcoatNormalTexture", mat.clearcoatNormalTexture],
675
+ ["anisotropyTexture", mat.anisotropyTexture],
676
+ ["sheenColorTexture", mat.sheenColorTexture],
677
+ ["sheenRoughnessTexture", mat.sheenRoughnessTexture],
678
+ ["iridescenceTexture", mat.iridescenceTexture],
679
+ ["iridescenceThicknessTexture", mat.iridescenceThicknessTexture],
680
+ ["specularTexture", mat.specularTexture],
681
+ ["specularColorTexture", mat.specularColorTexture]
605
682
  ];
606
683
  for (const [slot, rawBinding] of slots) {
607
684
  const binding = textureInfo(rawBinding);
@@ -621,7 +698,44 @@ function validateMaterialUvSets(mat, primitive, availableSets) {
621
698
  }
622
699
  return ok(void 0);
623
700
  }
701
+ function validateMaterialTangentInputs(mat, mesh, layer = "clearcoat") {
702
+ const anisotropyDeclared = mat.anisotropyStrength !== void 0 || mat.anisotropyRotation !== void 0 || mat.anisotropyTexture !== void 0;
703
+ const tangentSlot = anisotropyDeclared ? { layer: "anisotropy", info: mat.anisotropyTexture } : mat.clearcoatNormalTexture !== void 0 ? { layer: "clearcoat", info: mat.clearcoatNormalTexture } : void 0;
704
+ if (tangentSlot === void 0) return ok(void 0);
705
+ const selected = textureInfo(tangentSlot.info);
706
+ const uvSet = selected?.texCoord ?? 0;
707
+ const uv = mesh[`texcoord${uvSet === 0 ? "0" : uvSet}`];
708
+ const attributes = ["NORMAL", `TEXCOORD_${uvSet}`, "TANGENT"];
709
+ const fail = (reason) => err(
710
+ createMaterialError("material-tangent-required", {
711
+ code: "material-tangent-required",
712
+ material: mat.name ?? "<unnamed>",
713
+ mesh: mesh.name ?? "<unnamed>",
714
+ layer: tangentSlot.layer ?? layer,
715
+ uv: `TEXCOORD_${uvSet}`,
716
+ attributes,
717
+ reason
718
+ })
719
+ );
720
+ if (mesh.tangents !== void 0) {
721
+ if (mesh.tangents.length !== mesh.positions.length / 3 * 4 || mesh.tangents.some((value) => !Number.isFinite(value))) {
722
+ return fail("imported TANGENT must be finite vec4 per vertex");
723
+ }
724
+ return ok(void 0);
725
+ }
726
+ if (mesh.normals === void 0) return fail("NORMAL is required to generate tangent");
727
+ if (!(uv instanceof Float32Array))
728
+ return fail("the selected UV set is required to generate tangent");
729
+ const generated = computeTangentVec4(mesh.positions, mesh.normals, uv, mesh.indices);
730
+ if (!generated.ok) {
731
+ const detail = generated.error.detail;
732
+ const reason = detail !== void 0 && "reason" in detail ? String(detail.reason) : "tangent producer rejected topology";
733
+ return fail(reason);
734
+ }
735
+ return ok(void 0);
736
+ }
624
737
  function toMaterialAsset(mat, ctx) {
738
+ const rootContract = standardRootParameterNames(mat);
625
739
  const values = {
626
740
  baseColor: mat.baseColorFactor,
627
741
  metallic: mat.metallicFactor,
@@ -638,7 +752,17 @@ function toMaterialAsset(mat, ctx) {
638
752
  ["occlusionTexture", mat.occlusionTexture],
639
753
  ["emissiveTexture", mat.emissiveTexture],
640
754
  ["transmissionTexture", mat.transmissionTexture],
641
- ["thicknessTexture", mat.thicknessTexture]
755
+ ["thicknessTexture", mat.thicknessTexture],
756
+ ["clearcoatTexture", mat.clearcoatTexture],
757
+ ["clearcoatRoughnessTexture", mat.clearcoatRoughnessTexture],
758
+ ["clearcoatNormalTexture", mat.clearcoatNormalTexture],
759
+ ["anisotropyTexture", mat.anisotropyTexture],
760
+ ["sheenColorTexture", mat.sheenColorTexture],
761
+ ["sheenRoughnessTexture", mat.sheenRoughnessTexture],
762
+ ["iridescenceTexture", mat.iridescenceTexture],
763
+ ["iridescenceThicknessTexture", mat.iridescenceThicknessTexture],
764
+ ["specularTexture", mat.specularTexture],
765
+ ["specularColorTexture", mat.specularColorTexture]
642
766
  ];
643
767
  for (const [slot, info] of textureSlots) {
644
768
  const value = textureValue(info, slot, ctx);
@@ -647,11 +771,35 @@ function toMaterialAsset(mat, ctx) {
647
771
  if (mat.occlusionTexture !== void 0 && values.occlusionStrength === void 0) {
648
772
  values.occlusionStrength = 1;
649
773
  }
650
- if (mat.transmissionFactor !== void 0) values.transmission = mat.transmissionFactor;
651
- if (mat.ior !== void 0) values.ior = mat.ior;
652
- if (mat.thicknessFactor !== void 0) values.thickness = mat.thicknessFactor;
653
- if (mat.attenuationColor !== void 0) values.attenuationColor = mat.attenuationColor;
654
- if (mat.attenuationDistance !== void 0) values.attenuationDistance = mat.attenuationDistance;
774
+ if (rootContract.extended && rootContract.names.has("transmission")) {
775
+ values.transmission = mat.transmissionFactor ?? 0;
776
+ values.ior = mat.ior ?? 1.5;
777
+ values.thickness = mat.thicknessFactor ?? 0;
778
+ values.attenuationColor = mat.attenuationColor ?? [1, 1, 1];
779
+ if (mat.attenuationDistance !== void 0) values.attenuationDistance = mat.attenuationDistance;
780
+ }
781
+ if (rootContract.names.has("clearcoat")) {
782
+ values.clearcoat = mat.clearcoatFactor ?? 0;
783
+ values.clearcoatRoughness = mat.clearcoatRoughnessFactor ?? 0;
784
+ }
785
+ const clearcoatNormal = textureInfo(mat.clearcoatNormalTexture);
786
+ if (clearcoatNormal?.scale !== void 0) values.clearcoatNormalScale = clearcoatNormal.scale;
787
+ if (rootContract.names.has("anisotropyStrength")) {
788
+ values.anisotropyStrength = mat.anisotropyStrength ?? 0;
789
+ values.anisotropyRotation = mat.anisotropyRotation ?? 0;
790
+ }
791
+ if (rootContract.names.has("sheenColor")) {
792
+ values.sheenColor = mat.sheenColorFactor ?? [0, 0, 0];
793
+ values.sheenRoughness = mat.sheenRoughnessFactor ?? 0;
794
+ }
795
+ if (rootContract.names.has("iridescence")) {
796
+ values.iridescence = mat.iridescenceFactor ?? 0;
797
+ values.iridescenceIor = mat.iridescenceIor ?? 1.3;
798
+ values.iridescenceThicknessMinimum = mat.iridescenceThicknessMinimum ?? 100;
799
+ values.iridescenceThicknessMaximum = mat.iridescenceThicknessMaximum ?? 400;
800
+ }
801
+ if (mat.specularFactor !== void 0) values.specular = mat.specularFactor;
802
+ if (mat.specularColorFactor !== void 0) values.specularColor = mat.specularColorFactor;
655
803
  const module = ctx?.skinned === true ? "forgeax::pbr-skin" : "forgeax::default-standard-pbr";
656
804
  const isMask = mat.alphaMode === "MASK";
657
805
  const alphaCutoff = isMask ? mat.alphaCutoff ?? 0.5 : void 0;
@@ -684,8 +832,9 @@ function toMaterialAsset(mat, ctx) {
684
832
  return {
685
833
  kind: "material",
686
834
  colorSpace: "linear",
687
- ...ctx?.standardRootGuid === void 0 ? {} : { parent: ctx.standardRootGuid },
835
+ ...!rootContract.extended && ctx?.standardRootGuid !== void 0 ? { parent: ctx.standardRootGuid } : {},
688
836
  passes: [pass],
837
+ ...ctx?.standardRootGuid !== void 0 && !rootContract.extended ? {} : { parameters: standardMaterialParameters(rootContract.names) },
689
838
  values
690
839
  };
691
840
  }
@@ -1104,9 +1253,21 @@ function deriveTextureColorSpace(input) {
1104
1253
  for (const mat of input.materials) {
1105
1254
  record(imageOfTexture(mat.baseColorTexture), "srgb");
1106
1255
  record(imageOfTexture(mat.emissiveTexture), "srgb");
1256
+ record(imageOfTexture(mat.sheenColorTexture), "srgb");
1257
+ record(imageOfTexture(mat.specularColorTexture), "srgb");
1107
1258
  record(imageOfTexture(mat.metallicRoughnessTexture), "linear");
1108
1259
  record(imageOfTexture(mat.normalTexture), "linear");
1109
1260
  record(imageOfTexture(mat.occlusionTexture), "linear");
1261
+ record(imageOfTexture(mat.transmissionTexture), "linear");
1262
+ record(imageOfTexture(mat.thicknessTexture), "linear");
1263
+ record(imageOfTexture(mat.clearcoatTexture), "linear");
1264
+ record(imageOfTexture(mat.clearcoatRoughnessTexture), "linear");
1265
+ record(imageOfTexture(mat.clearcoatNormalTexture), "linear");
1266
+ record(imageOfTexture(mat.anisotropyTexture), "linear");
1267
+ record(imageOfTexture(mat.sheenRoughnessTexture), "linear");
1268
+ record(imageOfTexture(mat.iridescenceTexture), "linear");
1269
+ record(imageOfTexture(mat.iridescenceThicknessTexture), "linear");
1270
+ record(imageOfTexture(mat.specularTexture), "linear");
1110
1271
  }
1111
1272
  for (let i = 0; i < input.imageCount; i++) {
1112
1273
  if (!result.has(i)) {
@@ -1394,15 +1555,22 @@ function decodeColorAccessor(input, flags = {}) {
1394
1555
  // src/check-extensions.ts
1395
1556
  var EXTENSION_ALLOWLIST = [
1396
1557
  "EXT_mesh_gpu_instancing",
1397
- "EXT_meshopt_compression"
1398
- ];
1399
- var SUPPORTED_EXTENSIONS = [
1400
- ...EXTENSION_ALLOWLIST,
1558
+ "EXT_meshopt_compression",
1401
1559
  "KHR_lights_punctual",
1402
1560
  "KHR_texture_transform",
1403
1561
  "KHR_materials_transmission",
1404
1562
  "KHR_materials_ior",
1405
- "KHR_materials_volume"
1563
+ "KHR_materials_volume",
1564
+ "KHR_materials_clearcoat",
1565
+ "KHR_materials_anisotropy",
1566
+ "KHR_materials_sheen",
1567
+ "KHR_materials_iridescence",
1568
+ "KHR_materials_specular"
1569
+ ];
1570
+ var SUPPORTED_EXTENSIONS = [
1571
+ ...EXTENSION_ALLOWLIST,
1572
+ "MSFT_lod",
1573
+ "MSFT_screencoverage"
1406
1574
  ];
1407
1575
  function checkExtensions(json) {
1408
1576
  const required = json.extensionsRequired ?? [];
@@ -1426,6 +1594,81 @@ function checkExtensions(json) {
1426
1594
  return ok({ unsupportedUsed });
1427
1595
  }
1428
1596
 
1597
+ // src/lod/parse-lod.ts
1598
+ function parseGltfLodExtension(json) {
1599
+ const doc = json;
1600
+ const nodes = doc.nodes ?? [];
1601
+ const rawCoverage = doc.extensions?.MSFT_screencoverage?.scales;
1602
+ const groups = [];
1603
+ for (let rootNode = 0; rootNode < nodes.length; rootNode += 1) {
1604
+ if (nodes[rootNode]?.extensions?.MSFT_lod === void 0) continue;
1605
+ const idsRaw = nodes[rootNode]?.extensions?.MSFT_lod?.ids;
1606
+ if (!Array.isArray(idsRaw)) {
1607
+ return err(gltfErr("gltf-lod-invalid", { rootNode, ids: [], reason: "not-integer" }));
1608
+ }
1609
+ const ids = idsRaw.filter((value) => typeof value === "number");
1610
+ if (ids.length !== idsRaw.length || ids.some((id) => !Number.isInteger(id) || id < 0 || id >= nodes.length)) {
1611
+ return err(gltfErr("gltf-lod-invalid", { rootNode, ids, reason: "missing-node" }));
1612
+ }
1613
+ if (new Set(ids).size !== ids.length || ids.includes(rootNode)) {
1614
+ return err(gltfErr("gltf-lod-invalid", { rootNode, ids, reason: "duplicate-node" }));
1615
+ }
1616
+ const hasMesh = (node) => {
1617
+ if (typeof node?.mesh !== "number" || !Number.isInteger(node.mesh) || node.mesh < 0) {
1618
+ return false;
1619
+ }
1620
+ const mesh = doc.meshes?.[node.mesh];
1621
+ return mesh !== void 0 && Array.isArray(mesh.primitives) && mesh.primitives.length > 0;
1622
+ };
1623
+ if (!hasMesh(nodes[rootNode]) || ids.some((id) => !hasMesh(nodes[id]))) {
1624
+ return err(gltfErr("gltf-lod-invalid", { rootNode, ids, reason: "missing-node" }));
1625
+ }
1626
+ const nodeCoverage = nodes[rootNode]?.extras?.MSFT_screencoverage;
1627
+ const coverage = normalizeScreenCoverages(
1628
+ nodeCoverage === void 0 ? rawCoverage : nodeCoverage,
1629
+ ids.length,
1630
+ nodeCoverage !== void 0
1631
+ );
1632
+ if (coverage === "invalid") {
1633
+ return err(gltfErr("gltf-lod-invalid", { rootNode, ids, reason: "coverage" }));
1634
+ }
1635
+ groups.push({
1636
+ rootNode,
1637
+ lodNodeIds: ids,
1638
+ screenCoverages: coverage ?? []
1639
+ });
1640
+ }
1641
+ const first = groups[0];
1642
+ if (first === void 0) {
1643
+ if (doc.extensionsRequired?.some((extension) => extension === "MSFT_lod")) {
1644
+ return err(gltfErr("gltf-lod-invalid", { rootNode: 0, ids: [], reason: "missing-node" }));
1645
+ }
1646
+ return ok({ rootNode: 0, lodNodeIds: [], screenCoverages: [], groups: [] });
1647
+ }
1648
+ return ok({
1649
+ rootNode: first.rootNode,
1650
+ lodNodeIds: first.lodNodeIds,
1651
+ screenCoverages: first.screenCoverages,
1652
+ groups
1653
+ });
1654
+ }
1655
+ function normalizeScreenCoverages(raw, lodCount, officialNodeForm) {
1656
+ if (raw === void 0) return void 0;
1657
+ if (!Array.isArray(raw)) return "invalid";
1658
+ if (raw.some(
1659
+ (value) => typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > 1
1660
+ )) {
1661
+ return "invalid";
1662
+ }
1663
+ const expectedLengths = officialNodeForm ? [lodCount + 1] : [lodCount, lodCount + 1];
1664
+ if (!expectedLengths.includes(raw.length)) return "invalid";
1665
+ const values = raw.slice(0, lodCount);
1666
+ for (let index = 1; index < values.length; index += 1) {
1667
+ if (values[index] >= values[index - 1]) return "invalid";
1668
+ }
1669
+ return values;
1670
+ }
1671
+
1429
1672
  // src/material/parse-material.ts
1430
1673
  function tuple2(values) {
1431
1674
  if (values === void 0 || values.length < 2) return void 0;
@@ -1470,6 +1713,11 @@ function parseMaterial(matJson, textures) {
1470
1713
  const transmission = matJson.extensions?.KHR_materials_transmission;
1471
1714
  const ior = matJson.extensions?.KHR_materials_ior;
1472
1715
  const volume = matJson.extensions?.KHR_materials_volume;
1716
+ const clearcoat = matJson.extensions?.KHR_materials_clearcoat;
1717
+ const anisotropy = matJson.extensions?.KHR_materials_anisotropy;
1718
+ const sheen = matJson.extensions?.KHR_materials_sheen;
1719
+ const iridescence = matJson.extensions?.KHR_materials_iridescence;
1720
+ const specular = matJson.extensions?.KHR_materials_specular;
1473
1721
  const attenuationColor = tuple3(volume?.attenuationColor);
1474
1722
  const invalid = (extension, field, reason, actual) => err(
1475
1723
  gltfErr("gltf-material-transmission-invalid", {
@@ -1479,6 +1727,14 @@ function parseMaterial(matJson, textures) {
1479
1727
  ...actual === void 0 ? {} : { actual }
1480
1728
  })
1481
1729
  );
1730
+ const physicalInvalid = (extension, field, reason, actual) => err(
1731
+ gltfErr("gltf-material-physical-invalid", {
1732
+ extension,
1733
+ field,
1734
+ reason,
1735
+ ...actual === void 0 ? {} : { actual }
1736
+ })
1737
+ );
1482
1738
  if (transmission?.transmissionFactor !== void 0 && (typeof transmission.transmissionFactor !== "number" || !Number.isFinite(transmission.transmissionFactor))) {
1483
1739
  return invalid(
1484
1740
  "KHR_materials_transmission",
@@ -1529,6 +1785,98 @@ function parseMaterial(matJson, textures) {
1529
1785
  if (alphaMode === "BLEND" && (transmission?.transmissionFactor ?? 0) > 0) {
1530
1786
  return invalid("KHR_materials_transmission", "alphaMode", "blend", alphaMode);
1531
1787
  }
1788
+ if (clearcoat?.clearcoatFactor !== void 0 && (typeof clearcoat.clearcoatFactor !== "number" || !Number.isFinite(clearcoat.clearcoatFactor))) {
1789
+ return physicalInvalid(
1790
+ "KHR_materials_clearcoat",
1791
+ "clearcoatFactor",
1792
+ "non-finite",
1793
+ clearcoat.clearcoatFactor
1794
+ );
1795
+ }
1796
+ if (clearcoat?.clearcoatFactor !== void 0 && (clearcoat.clearcoatFactor < 0 || clearcoat.clearcoatFactor > 1)) {
1797
+ return physicalInvalid(
1798
+ "KHR_materials_clearcoat",
1799
+ "clearcoatFactor",
1800
+ "range",
1801
+ clearcoat.clearcoatFactor
1802
+ );
1803
+ }
1804
+ if (clearcoat?.clearcoatRoughnessFactor !== void 0 && (typeof clearcoat.clearcoatRoughnessFactor !== "number" || !Number.isFinite(clearcoat.clearcoatRoughnessFactor))) {
1805
+ return physicalInvalid(
1806
+ "KHR_materials_clearcoat",
1807
+ "clearcoatRoughnessFactor",
1808
+ "non-finite",
1809
+ clearcoat.clearcoatRoughnessFactor
1810
+ );
1811
+ }
1812
+ if (clearcoat?.clearcoatRoughnessFactor !== void 0 && (clearcoat.clearcoatRoughnessFactor < 0 || clearcoat.clearcoatRoughnessFactor > 1)) {
1813
+ return physicalInvalid(
1814
+ "KHR_materials_clearcoat",
1815
+ "clearcoatRoughnessFactor",
1816
+ "range",
1817
+ clearcoat.clearcoatRoughnessFactor
1818
+ );
1819
+ }
1820
+ if (clearcoat?.clearcoatNormalTexture?.scale !== void 0 && (typeof clearcoat.clearcoatNormalTexture.scale !== "number" || !Number.isFinite(clearcoat.clearcoatNormalTexture.scale) || clearcoat.clearcoatNormalTexture.scale < 0)) {
1821
+ return physicalInvalid(
1822
+ "KHR_materials_clearcoat",
1823
+ "clearcoatNormalTexture.scale",
1824
+ "range",
1825
+ clearcoat.clearcoatNormalTexture.scale
1826
+ );
1827
+ }
1828
+ const validateUnit = (extension, field, value) => {
1829
+ if (value === void 0) return void 0;
1830
+ if (typeof value !== "number" || !Number.isFinite(value))
1831
+ return physicalInvalid(extension, field, "non-finite", value);
1832
+ if (value < 0 || value > 1) return physicalInvalid(extension, field, "range", value);
1833
+ return void 0;
1834
+ };
1835
+ const validateFinite = (extension, field, value, minimum) => {
1836
+ if (value === void 0) return void 0;
1837
+ if (typeof value !== "number" || !Number.isFinite(value))
1838
+ return physicalInvalid(extension, field, "non-finite", value);
1839
+ if (minimum !== void 0 && value < minimum)
1840
+ return physicalInvalid(extension, field, "range", value);
1841
+ return void 0;
1842
+ };
1843
+ const validateColor = (extension, field, value) => {
1844
+ if (value === void 0) return void 0;
1845
+ if (value.length < 3 || value.slice(0, 3).some((channel) => typeof channel !== "number" || !Number.isFinite(channel))) {
1846
+ return physicalInvalid(extension, field, "type", value);
1847
+ }
1848
+ if (value.slice(0, 3).some((channel) => channel < 0 || channel > 1)) {
1849
+ return physicalInvalid(extension, field, "range", value);
1850
+ }
1851
+ return void 0;
1852
+ };
1853
+ const physicalChecks = [
1854
+ validateUnit("KHR_materials_anisotropy", "anisotropyStrength", anisotropy?.anisotropyStrength),
1855
+ validateFinite(
1856
+ "KHR_materials_anisotropy",
1857
+ "anisotropyRotation",
1858
+ anisotropy?.anisotropyRotation
1859
+ ),
1860
+ validateUnit("KHR_materials_sheen", "sheenRoughnessFactor", sheen?.sheenRoughnessFactor),
1861
+ validateColor("KHR_materials_sheen", "sheenColorFactor", sheen?.sheenColorFactor),
1862
+ validateUnit("KHR_materials_iridescence", "iridescenceFactor", iridescence?.iridescenceFactor),
1863
+ validateFinite("KHR_materials_iridescence", "iridescenceIor", iridescence?.iridescenceIor, 1),
1864
+ validateFinite(
1865
+ "KHR_materials_iridescence",
1866
+ "iridescenceThicknessMinimum",
1867
+ iridescence?.iridescenceThicknessMinimum,
1868
+ 0
1869
+ ),
1870
+ validateFinite(
1871
+ "KHR_materials_iridescence",
1872
+ "iridescenceThicknessMaximum",
1873
+ iridescence?.iridescenceThicknessMaximum,
1874
+ 0
1875
+ ),
1876
+ validateUnit("KHR_materials_specular", "specularFactor", specular?.specularFactor),
1877
+ validateColor("KHR_materials_specular", "specularColorFactor", specular?.specularColorFactor)
1878
+ ];
1879
+ for (const check of physicalChecks) if (check !== void 0) return check;
1532
1880
  return ok({
1533
1881
  ...matJson.name === void 0 ? {} : { name: matJson.name },
1534
1882
  baseColorFactor: baseColor4,
@@ -1565,6 +1913,65 @@ function parseMaterial(matJson, textures) {
1565
1913
  ...attenuationColor === void 0 ? {} : { attenuationColor },
1566
1914
  ...finiteOrUndefined(volume.attenuationDistance) === void 0 ? {} : { attenuationDistance: volume.attenuationDistance }
1567
1915
  },
1916
+ ...clearcoat === void 0 ? {} : {
1917
+ clearcoatFactor: clearcoat.clearcoatFactor ?? 0,
1918
+ clearcoatChannel: 0,
1919
+ clearcoatRoughnessFactor: clearcoat.clearcoatRoughnessFactor ?? 0,
1920
+ clearcoatRoughnessChannel: 1,
1921
+ clearcoatNormalChannel: [0, 1],
1922
+ ...clearcoat.clearcoatTexture === void 0 ? {} : { clearcoatTexture: parseTextureInfo(clearcoat.clearcoatTexture, textures) },
1923
+ ...clearcoat.clearcoatRoughnessTexture === void 0 ? {} : {
1924
+ clearcoatRoughnessTexture: parseTextureInfo(
1925
+ clearcoat.clearcoatRoughnessTexture,
1926
+ textures
1927
+ )
1928
+ },
1929
+ ...clearcoat.clearcoatNormalTexture === void 0 ? {} : {
1930
+ clearcoatNormalTexture: {
1931
+ ...parseTextureInfo(clearcoat.clearcoatNormalTexture, textures),
1932
+ ...clearcoat.clearcoatNormalTexture.scale === void 0 ? {} : { scale: clearcoat.clearcoatNormalTexture.scale }
1933
+ }
1934
+ }
1935
+ },
1936
+ ...anisotropy === void 0 ? {} : {
1937
+ anisotropyStrength: anisotropy.anisotropyStrength ?? 0,
1938
+ anisotropyRotation: anisotropy.anisotropyRotation ?? 0,
1939
+ anisotropyChannel: [0, 1, 2],
1940
+ ...anisotropy.anisotropyTexture === void 0 ? {} : { anisotropyTexture: parseTextureInfo(anisotropy.anisotropyTexture, textures) }
1941
+ },
1942
+ ...sheen === void 0 ? {} : {
1943
+ sheenColorFactor: tuple3(sheen.sheenColorFactor) ?? [0, 0, 0],
1944
+ sheenColorChannel: [0, 1, 2],
1945
+ sheenRoughnessFactor: sheen.sheenRoughnessFactor ?? 0,
1946
+ sheenRoughnessChannel: 3,
1947
+ ...sheen.sheenColorTexture === void 0 ? {} : { sheenColorTexture: parseTextureInfo(sheen.sheenColorTexture, textures) },
1948
+ ...sheen.sheenRoughnessTexture === void 0 ? {} : { sheenRoughnessTexture: parseTextureInfo(sheen.sheenRoughnessTexture, textures) }
1949
+ },
1950
+ ...iridescence === void 0 ? {} : {
1951
+ iridescenceFactor: iridescence.iridescenceFactor ?? 0,
1952
+ iridescenceIor: iridescence.iridescenceIor ?? 1.3,
1953
+ iridescenceThicknessMinimum: iridescence.iridescenceThicknessMinimum ?? 100,
1954
+ iridescenceThicknessMaximum: iridescence.iridescenceThicknessMaximum ?? 400,
1955
+ iridescenceChannel: 0,
1956
+ iridescenceThicknessChannel: 1,
1957
+ ...iridescence.iridescenceTexture === void 0 ? {} : { iridescenceTexture: parseTextureInfo(iridescence.iridescenceTexture, textures) },
1958
+ ...iridescence.iridescenceThicknessTexture === void 0 ? {} : {
1959
+ iridescenceThicknessTexture: parseTextureInfo(
1960
+ iridescence.iridescenceThicknessTexture,
1961
+ textures
1962
+ )
1963
+ }
1964
+ },
1965
+ ...specular === void 0 ? {} : {
1966
+ specularFactor: specular.specularFactor ?? 1,
1967
+ specularChannel: 3,
1968
+ specularColorFactor: tuple3(specular.specularColorFactor) ?? [1, 1, 1],
1969
+ specularColorChannel: [0, 1, 2],
1970
+ ...specular.specularTexture === void 0 ? {} : { specularTexture: parseTextureInfo(specular.specularTexture, textures) },
1971
+ ...specular.specularColorTexture === void 0 ? {} : {
1972
+ specularColorTexture: parseTextureInfo(specular.specularColorTexture, textures)
1973
+ }
1974
+ },
1568
1975
  ...alphaMode === void 0 ? {} : { alphaMode },
1569
1976
  ...alphaCutoff === void 0 ? {} : { alphaCutoff },
1570
1977
  ...matJson.doubleSided === true ? { doubleSided: true } : {},
@@ -2210,6 +2617,8 @@ async function parseGltfWithBin(json, ctx) {
2210
2617
  const extResult = checkExtensions(json);
2211
2618
  if (!extResult.ok) return err(extResult.error);
2212
2619
  const unsupportedExtensions = extResult.value.unsupportedUsed;
2620
+ const lodResult = parseGltfLodExtension(json);
2621
+ if (!lodResult.ok) return err(lodResult.error);
2213
2622
  const lightsResult = parsePunctualLights(json, ctx.filePath);
2214
2623
  if (!lightsResult.ok) return err(lightsResult.error);
2215
2624
  const meshesJson = json.meshes ?? [];
@@ -2750,7 +3159,8 @@ async function parseGltfWithBin(json, ctx) {
2750
3159
  matrixTrsCoexistNodes: diagnostics.matrixTrsCoexistNodes
2751
3160
  },
2752
3161
  meshPrimitiveCount,
2753
- lights: lightsResult.value
3162
+ lights: lightsResult.value,
3163
+ ...lodResult.value.lodNodeIds.length === 0 ? {} : { lod: lodResult.value }
2754
3164
  });
2755
3165
  }
2756
3166
  async function parseGltfForImporter(json, externalLoader, filePath, options = {}) {
@@ -3052,6 +3462,16 @@ function materialRefsForPack(mat, doc, textureGuidByIndex, samplerGuidByIndex =
3052
3462
  pushRefsForSlot(mat.emissiveTexture, "emissiveTexture");
3053
3463
  pushRefsForSlot(mat.transmissionTexture, "transmissionTexture");
3054
3464
  pushRefsForSlot(mat.thicknessTexture, "thicknessTexture");
3465
+ pushRefsForSlot(mat.clearcoatTexture, "clearcoatTexture");
3466
+ pushRefsForSlot(mat.clearcoatRoughnessTexture, "clearcoatRoughnessTexture");
3467
+ pushRefsForSlot(mat.clearcoatNormalTexture, "clearcoatNormalTexture");
3468
+ pushRefsForSlot(mat.anisotropyTexture, "anisotropyTexture");
3469
+ pushRefsForSlot(mat.sheenColorTexture, "sheenColorTexture");
3470
+ pushRefsForSlot(mat.sheenRoughnessTexture, "sheenRoughnessTexture");
3471
+ pushRefsForSlot(mat.iridescenceTexture, "iridescenceTexture");
3472
+ pushRefsForSlot(mat.iridescenceThicknessTexture, "iridescenceThicknessTexture");
3473
+ pushRefsForSlot(mat.specularTexture, "specularTexture");
3474
+ pushRefsForSlot(mat.specularColorTexture, "specularColorTexture");
3055
3475
  return refs;
3056
3476
  }
3057
3477
  function availableUvSets(mesh) {
@@ -3072,7 +3492,17 @@ function rewriteMaterialAssetRefs(matAsset, mat, doc, maps) {
3072
3492
  ["occlusionTexture", mat.occlusionTexture],
3073
3493
  ["emissiveTexture", mat.emissiveTexture],
3074
3494
  ["transmissionTexture", mat.transmissionTexture],
3075
- ["thicknessTexture", mat.thicknessTexture]
3495
+ ["thicknessTexture", mat.thicknessTexture],
3496
+ ["clearcoatTexture", mat.clearcoatTexture],
3497
+ ["clearcoatRoughnessTexture", mat.clearcoatRoughnessTexture],
3498
+ ["clearcoatNormalTexture", mat.clearcoatNormalTexture],
3499
+ ["anisotropyTexture", mat.anisotropyTexture],
3500
+ ["sheenColorTexture", mat.sheenColorTexture],
3501
+ ["sheenRoughnessTexture", mat.sheenRoughnessTexture],
3502
+ ["iridescenceTexture", mat.iridescenceTexture],
3503
+ ["iridescenceThicknessTexture", mat.iridescenceThicknessTexture],
3504
+ ["specularTexture", mat.specularTexture],
3505
+ ["specularColorTexture", mat.specularColorTexture]
3076
3506
  ];
3077
3507
  let cursor = 0;
3078
3508
  for (const [slot, rawBinding] of slots) {
@@ -3167,7 +3597,55 @@ async function importGltf(ctx, meshopt) {
3167
3597
  })
3168
3598
  };
3169
3599
  }
3170
- const meshPayload = stabilizeMeshMaterialSlots(bridged.value, ctx, sub.guid, sub.sourceKey);
3600
+ const stabilizedMesh = stabilizeMeshMaterialSlots(
3601
+ bridged.value,
3602
+ ctx,
3603
+ sub.guid,
3604
+ sub.sourceKey
3605
+ );
3606
+ const lodGroup = doc.lod?.groups?.find(
3607
+ (group) => doc.nodes[group.rootNode]?.meshIndex === sub.sourceIndex
3608
+ ) ?? (doc.lod?.rootNode !== void 0 && doc.nodes[doc.lod.rootNode]?.meshIndex === sub.sourceIndex ? doc.lod : void 0);
3609
+ const rootMeshIndex = lodGroup === void 0 ? void 0 : sub.sourceIndex;
3610
+ if (lodGroup !== void 0) {
3611
+ const referencedMeshIndices = [
3612
+ sub.sourceIndex,
3613
+ ...lodGroup.lodNodeIds.map((nodeIndex) => doc.nodes[nodeIndex]?.meshIndex)
3614
+ ];
3615
+ const missingMeshIndex = referencedMeshIndices.find(
3616
+ (meshIndex) => !Number.isInteger(meshIndex) || meshIndex === null || meshIndex === void 0 || maps.meshGuidByIndex.get(meshIndex) === void 0
3617
+ );
3618
+ if (missingMeshIndex !== void 0) {
3619
+ return {
3620
+ ok: false,
3621
+ error: new ImportError({
3622
+ code: "import-internal-error",
3623
+ expected: "every MSFT_lod node to resolve a cooked mesh sub-asset",
3624
+ actual: String(missingMeshIndex),
3625
+ hint: "repair the referenced glTF mesh primitives and re-run the importer",
3626
+ detail: {
3627
+ reason: `MSFT_lod group for mesh ${sub.sourceIndex} references an unprojected mesh`
3628
+ }
3629
+ })
3630
+ };
3631
+ }
3632
+ }
3633
+ const authoredLods = sub.sourceKey === void 0 ? void 0 : ctx.sourceOverrides?.[sub.sourceKey]?.lods;
3634
+ const lodEntries = Array.isArray(authoredLods) ? authoredLods : [];
3635
+ const lodLevels = rootMeshIndex === sub.sourceIndex && lodGroup !== void 0 ? lodGroup.lodNodeIds.flatMap((nodeIndex, index) => {
3636
+ const meshIndex = doc.nodes[nodeIndex]?.meshIndex;
3637
+ const guid = meshIndex === void 0 || meshIndex === null ? void 0 : maps.meshGuidByIndex.get(meshIndex);
3638
+ if (guid === void 0) return [];
3639
+ const authored = lodEntries[index];
3640
+ const authoredCoverage = authored !== null && typeof authored === "object" ? authored.screenCoverage : void 0;
3641
+ const coverage = typeof authoredCoverage === "number" ? authoredCoverage : lodGroup.screenCoverages[index] ?? deriveDefaultLodScreenCoverages(lodGroup.lodNodeIds.length + 1)[index];
3642
+ const parsed2 = AssetGuid.parse(guid);
3643
+ return parsed2.ok && coverage !== void 0 ? [{ mesh: parsed2.value, screenCoverage: coverage, guid }] : [];
3644
+ }) : [];
3645
+ const meshPayload = lodLevels.length === 0 ? stabilizedMesh : {
3646
+ ...stabilizedMesh,
3647
+ lods: lodLevels.map(({ mesh, screenCoverage }) => ({ mesh, screenCoverage }))
3648
+ };
3171
3649
  const materialRefs = [];
3172
3650
  const seenMaterialGuids = /* @__PURE__ */ new Set();
3173
3651
  for (let slotIndex = 0; slotIndex < meshPayload.materialSlots.length; slotIndex++) {
@@ -3181,6 +3659,15 @@ async function importGltf(ctx, meshopt) {
3181
3659
  });
3182
3660
  }
3183
3661
  }
3662
+ for (const [lodIndex, level] of lodLevels.entries()) {
3663
+ if (!seenMaterialGuids.has(level.guid.toLowerCase())) {
3664
+ seenMaterialGuids.add(level.guid.toLowerCase());
3665
+ materialRefs.push({
3666
+ guid: level.guid,
3667
+ sourceField: { fieldName: "lods", arrayIndex: lodIndex }
3668
+ });
3669
+ }
3670
+ }
3184
3671
  out.push({
3185
3672
  guid: sub.guid,
3186
3673
  kind: "mesh",
@@ -3232,6 +3719,10 @@ async function importGltf(ctx, meshopt) {
3232
3719
  if (!uvResult.ok) {
3233
3720
  throw Object.assign(new Error(uvResult.error.message), uvResult.error);
3234
3721
  }
3722
+ const tangentResult = validateMaterialTangentInputs(mat, meshIr);
3723
+ if (!tangentResult.ok) {
3724
+ throw Object.assign(new Error(tangentResult.error.message), tangentResult.error);
3725
+ }
3235
3726
  }
3236
3727
  const matAsset = toMaterialAsset(mat, {
3237
3728
  textureHandles: maps.textureHandles,