@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.
- package/README.md +115 -3
- package/dist/__tests__/lod-import.integration.test.d.ts +2 -0
- package/dist/__tests__/lod-import.integration.test.d.ts.map +1 -0
- package/dist/__tests__/lod-meta-publication.integration.test.d.ts +2 -0
- package/dist/__tests__/lod-meta-publication.integration.test.d.ts.map +1 -0
- package/dist/__tests__/physical-clearcoat.unit.test.d.ts +2 -0
- package/dist/__tests__/physical-clearcoat.unit.test.d.ts.map +1 -0
- package/dist/__tests__/physical-material-import.integration.test.d.ts +2 -0
- package/dist/__tests__/physical-material-import.integration.test.d.ts.map +1 -0
- package/dist/bridge.d.ts +1 -0
- package/dist/bridge.d.ts.map +1 -1
- package/dist/check-extensions.d.ts +1 -1
- package/dist/check-extensions.d.ts.map +1 -1
- package/dist/cli-gltf.mjs +362 -14
- package/dist/cli-gltf.mjs.map +1 -1
- package/dist/errors.d.ts +13 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/gltf-importer.d.ts.map +1 -1
- package/dist/image-color-space.d.ts +12 -0
- package/dist/image-color-space.d.ts.map +1 -1
- package/dist/importer-entry.mjs +516 -25
- package/dist/importer-entry.mjs.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +609 -28
- package/dist/index.mjs.map +1 -1
- package/dist/lod/parse-lod.d.ts +15 -0
- package/dist/lod/parse-lod.d.ts.map +1 -0
- package/dist/lod/project-meta.d.ts +23 -0
- package/dist/lod/project-meta.d.ts.map +1 -0
- package/dist/material/parse-material.d.ts +66 -0
- package/dist/material/parse-material.d.ts.map +1 -1
- package/dist/node-file-entry.mjs +265 -8
- package/dist/node-file-entry.mjs.map +1 -1
- package/dist/parse-gltf.d.ts +3 -1
- package/dist/parse-gltf.d.ts.map +1 -1
- package/dist/reimport-reuse-meta.d.ts +1 -1
- package/dist/reimport-reuse-meta.d.ts.map +1 -1
- package/package.json +12 -12
- package/src/__tests__/bridge-material-values.unit.test.ts +84 -1
- package/src/__tests__/bridge-skin-stride.unit.test.ts +22 -0
- package/src/__tests__/fixtures/lod/core-only.gltf +1 -0
- package/src/__tests__/fixtures/lod/msft-lod-malformed.gltf +1 -0
- package/src/__tests__/fixtures/lod/msft-lod-multi-root.gltf +1 -0
- package/src/__tests__/fixtures/lod/msft-lod-node-extras.gltf +1 -0
- package/src/__tests__/fixtures/lod/msft-lod.gltf +1 -0
- package/src/__tests__/gltf-error-derived-views.test-d.ts +8 -0
- package/src/__tests__/gltf-error-migration.test.ts +3 -3
- package/src/__tests__/gltf.unit.test.ts +60 -5
- package/src/__tests__/lod-import.integration.test.ts +106 -0
- package/src/__tests__/lod-meta-publication.integration.test.ts +24 -0
- package/src/__tests__/material-pack-refs.integration.test.ts +53 -0
- package/src/__tests__/parse-gltf.unit.test.ts +50 -0
- package/src/__tests__/physical-clearcoat.unit.test.ts +110 -0
- package/src/__tests__/physical-material-import.integration.test.ts +239 -0
- package/src/__tests__/source-key-producer.integration.test.ts +6 -2
- package/src/bridge.ts +259 -14
- package/src/check-extensions.ts +16 -8
- package/src/errors.ts +32 -2
- package/src/gltf-importer.ts +122 -1
- package/src/image-color-space.ts +29 -4
- package/src/index.ts +10 -1
- package/src/lod/parse-lod.ts +129 -0
- package/src/lod/project-meta.ts +48 -0
- package/src/material/parse-material.ts +307 -0
- package/src/parse-gltf.ts +97 -1
- package/src/reimport-reuse-meta.ts +3 -1
- package/dist/.tsbuildinfo +0 -1
package/src/bridge.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// (B3) child world pos accumulates parent transform
|
|
20
20
|
// (B6) camera detection via GltfNodeIr.camera field (not legacy nodes[1] heuristic)
|
|
21
21
|
|
|
22
|
-
import { packInterleavedVertexAttributes } from '@forgeax/engine-geometry';
|
|
22
|
+
import { computeTangentVec4, packInterleavedVertexAttributes } from '@forgeax/engine-geometry';
|
|
23
23
|
import type { Mat4 } from '@forgeax/engine-math';
|
|
24
24
|
import { box3, mat4, quat, vec3 } from '@forgeax/engine-math';
|
|
25
25
|
import { AssetGuid as AssetGuidCodec } from '@forgeax/engine-pack/guid';
|
|
@@ -40,6 +40,13 @@ import type {
|
|
|
40
40
|
Submesh,
|
|
41
41
|
VertexAttributeMap,
|
|
42
42
|
} from '@forgeax/engine-types';
|
|
43
|
+
import {
|
|
44
|
+
STANDARD_LAYER_PARAMETER_GROUPS,
|
|
45
|
+
STANDARD_MATERIAL_PARAM_SCHEMA,
|
|
46
|
+
STANDARD_PHYSICAL_PARAMETER_NAMES,
|
|
47
|
+
STANDARD_TRANSMISSION_PARAMETER_NAMES,
|
|
48
|
+
standardMaterialParameters,
|
|
49
|
+
} from '@forgeax/engine-types';
|
|
43
50
|
import { createMaterialError, err, type GltfError, gltfErr, ok } from './errors.js';
|
|
44
51
|
import type {
|
|
45
52
|
GltfDoc,
|
|
@@ -216,6 +223,21 @@ export function meshIrToMeshAsset(
|
|
|
216
223
|
const materialSlot = slotFor(mesh.materialIndex);
|
|
217
224
|
const primVertexCount = mesh.positions.length / 3;
|
|
218
225
|
const primIndexCount = mesh.indices === undefined ? 0 : mesh.indices.length;
|
|
226
|
+
// A tangent admission check may already have computed a valid frame for a
|
|
227
|
+
// physical glTF material. Persist that producer result into the canonical
|
|
228
|
+
// MeshAsset instead of silently replacing it with the historical identity
|
|
229
|
+
// tangent. Non-physical meshes keep the identity fallback when the source
|
|
230
|
+
// lacks the inputs required by the tangent producer.
|
|
231
|
+
let generatedTangents: Float32Array | undefined;
|
|
232
|
+
if (mesh.tangents === undefined && mesh.normals !== undefined && mesh.texcoord0 !== undefined) {
|
|
233
|
+
const generated = computeTangentVec4(
|
|
234
|
+
mesh.positions,
|
|
235
|
+
mesh.normals,
|
|
236
|
+
mesh.texcoord0,
|
|
237
|
+
mesh.indices,
|
|
238
|
+
);
|
|
239
|
+
if (generated.ok) generatedTangents = generated.value;
|
|
240
|
+
}
|
|
219
241
|
if (mesh.colors0 !== undefined && mesh.colors0.length !== primVertexCount * 4) {
|
|
220
242
|
return err(
|
|
221
243
|
gltfErr('gltf-mesh-bridge-invalid', {
|
|
@@ -247,12 +269,13 @@ export function meshIrToMeshAsset(
|
|
|
247
269
|
uvsCat[(vertexCursor + i) * 2 + 0] = mesh.texcoord0[t + 0] as number;
|
|
248
270
|
uvsCat[(vertexCursor + i) * 2 + 1] = mesh.texcoord0[t + 1] as number;
|
|
249
271
|
}
|
|
250
|
-
|
|
272
|
+
const sourceTangents = mesh.tangents ?? generatedTangents;
|
|
273
|
+
if (sourceTangents !== undefined) {
|
|
251
274
|
const g = i * 4;
|
|
252
|
-
tangentsCat[(vertexCursor + i) * 4 + 0] =
|
|
253
|
-
tangentsCat[(vertexCursor + i) * 4 + 1] =
|
|
254
|
-
tangentsCat[(vertexCursor + i) * 4 + 2] =
|
|
255
|
-
tangentsCat[(vertexCursor + i) * 4 + 3] =
|
|
275
|
+
tangentsCat[(vertexCursor + i) * 4 + 0] = sourceTangents[g + 0] as number;
|
|
276
|
+
tangentsCat[(vertexCursor + i) * 4 + 1] = sourceTangents[g + 1] as number;
|
|
277
|
+
tangentsCat[(vertexCursor + i) * 4 + 2] = sourceTangents[g + 2] as number;
|
|
278
|
+
tangentsCat[(vertexCursor + i) * 4 + 3] = sourceTangents[g + 3] as number;
|
|
256
279
|
} else {
|
|
257
280
|
tangentsCat[(vertexCursor + i) * 4 + 0] = 1;
|
|
258
281
|
tangentsCat[(vertexCursor + i) * 4 + 3] = 1;
|
|
@@ -541,7 +564,7 @@ export function gltfDocToSceneAsset(doc: GltfDoc, ctx: GltfBridgeContext): Scene
|
|
|
541
564
|
|
|
542
565
|
// bug-20260613: SceneAsset entities also emit ChildOf when a glTF node has a
|
|
543
566
|
// parent (line 547 below). Runtime propagateTransforms then derives
|
|
544
|
-
//
|
|
567
|
+
// GlobalTransform.world via `parent.world * compose(child.local TRS)`. If we
|
|
545
568
|
// wrote the *world* TRS into Transform here, every child node would get
|
|
546
569
|
// baked twice -- once at importer time and again at propagate time --
|
|
547
570
|
// collapsing the skin so vertices fly to (parent.world)^2 space and the
|
|
@@ -778,7 +801,17 @@ type MaterialTextureSlot =
|
|
|
778
801
|
| 'occlusionTexture'
|
|
779
802
|
| 'emissiveTexture'
|
|
780
803
|
| 'transmissionTexture'
|
|
781
|
-
| 'thicknessTexture'
|
|
804
|
+
| 'thicknessTexture'
|
|
805
|
+
| 'clearcoatTexture'
|
|
806
|
+
| 'clearcoatRoughnessTexture'
|
|
807
|
+
| 'clearcoatNormalTexture'
|
|
808
|
+
| 'anisotropyTexture'
|
|
809
|
+
| 'sheenColorTexture'
|
|
810
|
+
| 'sheenRoughnessTexture'
|
|
811
|
+
| 'iridescenceTexture'
|
|
812
|
+
| 'iridescenceThicknessTexture'
|
|
813
|
+
| 'specularTexture'
|
|
814
|
+
| 'specularColorTexture';
|
|
782
815
|
|
|
783
816
|
function textureValue(
|
|
784
817
|
info: GltfTextureInfoIr | number | undefined,
|
|
@@ -811,6 +844,12 @@ function textureValue(
|
|
|
811
844
|
return { ...value, normalScale: normal.scale };
|
|
812
845
|
}
|
|
813
846
|
}
|
|
847
|
+
if (slot === 'clearcoatNormalTexture') {
|
|
848
|
+
const normal = info as GltfMaterialIr['clearcoatNormalTexture'];
|
|
849
|
+
if (typeof normal === 'object' && normal?.scale !== undefined) {
|
|
850
|
+
return { ...value, normalScale: normal.scale };
|
|
851
|
+
}
|
|
852
|
+
}
|
|
814
853
|
if (slot === 'occlusionTexture') {
|
|
815
854
|
const occlusion = info as GltfMaterialIr['occlusionTexture'];
|
|
816
855
|
if (typeof occlusion === 'object' && occlusion?.strength !== undefined) {
|
|
@@ -820,6 +859,102 @@ function textureValue(
|
|
|
820
859
|
return value;
|
|
821
860
|
}
|
|
822
861
|
|
|
862
|
+
/**
|
|
863
|
+
* Select the exact Standard root contract emitted by the glTF producer.
|
|
864
|
+
* Numeric/base fields come from the shared schema; physical texture slots are
|
|
865
|
+
* admitted only when the source actually declares them. A parent root is
|
|
866
|
+
* retained for the legacy base-only path, while an extended glTF material
|
|
867
|
+
* becomes its own root so its extension declarations own cook/ABI identity.
|
|
868
|
+
*/
|
|
869
|
+
function standardRootParameterNames(mat: GltfMaterialIr): {
|
|
870
|
+
readonly names: ReadonlySet<string>;
|
|
871
|
+
readonly extended: boolean;
|
|
872
|
+
} {
|
|
873
|
+
const names = new Set(
|
|
874
|
+
STANDARD_MATERIAL_PARAM_SCHEMA.filter(
|
|
875
|
+
(entry) =>
|
|
876
|
+
!STANDARD_PHYSICAL_PARAMETER_NAMES.has(entry.name) &&
|
|
877
|
+
!STANDARD_TRANSMISSION_PARAMETER_NAMES.has(entry.name),
|
|
878
|
+
).map((entry) => entry.name),
|
|
879
|
+
);
|
|
880
|
+
// IOR is consumed for the base dielectric F0 fallback even when the
|
|
881
|
+
// transmission/volume extension is absent.
|
|
882
|
+
names.add('ior');
|
|
883
|
+
|
|
884
|
+
const addLayer = (layer: keyof typeof STANDARD_LAYER_PARAMETER_GROUPS): void => {
|
|
885
|
+
for (const name of STANDARD_LAYER_PARAMETER_GROUPS[layer]) names.add(name);
|
|
886
|
+
};
|
|
887
|
+
const addTexture = (name: string, info: GltfTextureInfoIr | number | undefined): void => {
|
|
888
|
+
if (info !== undefined) names.add(name);
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
const clearcoat =
|
|
892
|
+
mat.clearcoatFactor !== undefined ||
|
|
893
|
+
mat.clearcoatRoughnessFactor !== undefined ||
|
|
894
|
+
mat.clearcoatNormalTexture !== undefined ||
|
|
895
|
+
mat.clearcoatTexture !== undefined ||
|
|
896
|
+
mat.clearcoatRoughnessTexture !== undefined;
|
|
897
|
+
const anisotropy =
|
|
898
|
+
mat.anisotropyStrength !== undefined ||
|
|
899
|
+
mat.anisotropyRotation !== undefined ||
|
|
900
|
+
mat.anisotropyTexture !== undefined;
|
|
901
|
+
const sheen =
|
|
902
|
+
mat.sheenColorFactor !== undefined ||
|
|
903
|
+
mat.sheenRoughnessFactor !== undefined ||
|
|
904
|
+
mat.sheenColorTexture !== undefined ||
|
|
905
|
+
mat.sheenRoughnessTexture !== undefined;
|
|
906
|
+
const iridescence =
|
|
907
|
+
mat.iridescenceFactor !== undefined ||
|
|
908
|
+
mat.iridescenceIor !== undefined ||
|
|
909
|
+
mat.iridescenceThicknessMinimum !== undefined ||
|
|
910
|
+
mat.iridescenceThicknessMaximum !== undefined ||
|
|
911
|
+
mat.iridescenceTexture !== undefined ||
|
|
912
|
+
mat.iridescenceThicknessTexture !== undefined;
|
|
913
|
+
if (clearcoat) addLayer('clearcoat');
|
|
914
|
+
if (anisotropy) addLayer('anisotropy');
|
|
915
|
+
if (sheen) addLayer('sheen');
|
|
916
|
+
if (iridescence) addLayer('iridescence');
|
|
917
|
+
if (clearcoat && mat.clearcoatNormalTexture !== undefined) names.add('clearcoatNormalScale');
|
|
918
|
+
|
|
919
|
+
addTexture('clearcoatTexture', mat.clearcoatTexture);
|
|
920
|
+
addTexture('clearcoatRoughnessTexture', mat.clearcoatRoughnessTexture);
|
|
921
|
+
addTexture('clearcoatNormalTexture', mat.clearcoatNormalTexture);
|
|
922
|
+
addTexture('anisotropyTexture', mat.anisotropyTexture);
|
|
923
|
+
addTexture('sheenColorTexture', mat.sheenColorTexture);
|
|
924
|
+
addTexture('sheenRoughnessTexture', mat.sheenRoughnessTexture);
|
|
925
|
+
addTexture('iridescenceTexture', mat.iridescenceTexture);
|
|
926
|
+
addTexture('iridescenceThicknessTexture', mat.iridescenceThicknessTexture);
|
|
927
|
+
addTexture('specularTexture', mat.specularTexture);
|
|
928
|
+
addTexture('specularColorTexture', mat.specularColorTexture);
|
|
929
|
+
|
|
930
|
+
const transmission =
|
|
931
|
+
mat.transmissionFactor !== undefined ||
|
|
932
|
+
mat.transmissionTexture !== undefined ||
|
|
933
|
+
mat.ior !== undefined ||
|
|
934
|
+
mat.thicknessFactor !== undefined ||
|
|
935
|
+
mat.thicknessTexture !== undefined ||
|
|
936
|
+
mat.attenuationColor !== undefined ||
|
|
937
|
+
mat.attenuationDistance !== undefined;
|
|
938
|
+
if (transmission) {
|
|
939
|
+
// Transmission/volume keeps its existing paired backdrop ABI. The
|
|
940
|
+
// producer may leave either value at its neutral fallback, but the pair
|
|
941
|
+
// remains part of the pre-existing transmission root contract.
|
|
942
|
+
for (const name of STANDARD_TRANSMISSION_PARAMETER_NAMES) names.add(name);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
return {
|
|
946
|
+
names,
|
|
947
|
+
extended:
|
|
948
|
+
clearcoat ||
|
|
949
|
+
anisotropy ||
|
|
950
|
+
sheen ||
|
|
951
|
+
iridescence ||
|
|
952
|
+
transmission ||
|
|
953
|
+
mat.specularTexture !== undefined ||
|
|
954
|
+
mat.specularColorTexture !== undefined,
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
|
|
823
958
|
export function validateMaterialUvSets(
|
|
824
959
|
mat: GltfMaterialIr,
|
|
825
960
|
primitive: string,
|
|
@@ -834,6 +969,16 @@ export function validateMaterialUvSets(
|
|
|
834
969
|
['emissiveTexture', mat.emissiveTexture],
|
|
835
970
|
['transmissionTexture', mat.transmissionTexture],
|
|
836
971
|
['thicknessTexture', mat.thicknessTexture],
|
|
972
|
+
['clearcoatTexture', mat.clearcoatTexture],
|
|
973
|
+
['clearcoatRoughnessTexture', mat.clearcoatRoughnessTexture],
|
|
974
|
+
['clearcoatNormalTexture', mat.clearcoatNormalTexture],
|
|
975
|
+
['anisotropyTexture', mat.anisotropyTexture],
|
|
976
|
+
['sheenColorTexture', mat.sheenColorTexture],
|
|
977
|
+
['sheenRoughnessTexture', mat.sheenRoughnessTexture],
|
|
978
|
+
['iridescenceTexture', mat.iridescenceTexture],
|
|
979
|
+
['iridescenceThicknessTexture', mat.iridescenceThicknessTexture],
|
|
980
|
+
['specularTexture', mat.specularTexture],
|
|
981
|
+
['specularColorTexture', mat.specularColorTexture],
|
|
837
982
|
];
|
|
838
983
|
for (const [slot, rawBinding] of slots) {
|
|
839
984
|
const binding = textureInfo(rawBinding);
|
|
@@ -854,8 +999,67 @@ export function validateMaterialUvSets(
|
|
|
854
999
|
return ok(undefined);
|
|
855
1000
|
}
|
|
856
1001
|
|
|
1002
|
+
export function validateMaterialTangentInputs(
|
|
1003
|
+
mat: GltfMaterialIr,
|
|
1004
|
+
mesh: GltfMeshIr,
|
|
1005
|
+
layer = 'clearcoat',
|
|
1006
|
+
): Result<void, MaterialError> {
|
|
1007
|
+
// Anisotropy changes the base GGX lobe even when it has no texture map, so
|
|
1008
|
+
// its tangent direction is part of the scalar layer contract. Select UV 0
|
|
1009
|
+
// for the scalar-only form; a mapped form keeps the map's explicit UV set.
|
|
1010
|
+
const anisotropyDeclared =
|
|
1011
|
+
mat.anisotropyStrength !== undefined ||
|
|
1012
|
+
mat.anisotropyRotation !== undefined ||
|
|
1013
|
+
mat.anisotropyTexture !== undefined;
|
|
1014
|
+
const tangentSlot = anisotropyDeclared
|
|
1015
|
+
? { layer: 'anisotropy', info: mat.anisotropyTexture }
|
|
1016
|
+
: mat.clearcoatNormalTexture !== undefined
|
|
1017
|
+
? { layer: 'clearcoat', info: mat.clearcoatNormalTexture }
|
|
1018
|
+
: undefined;
|
|
1019
|
+
if (tangentSlot === undefined) return ok(undefined);
|
|
1020
|
+
const selected = textureInfo(tangentSlot.info);
|
|
1021
|
+
const uvSet = selected?.texCoord ?? 0;
|
|
1022
|
+
const uv = mesh[`texcoord${uvSet === 0 ? '0' : uvSet}` as keyof GltfMeshIr];
|
|
1023
|
+
const attributes = ['NORMAL', `TEXCOORD_${uvSet}`, 'TANGENT'];
|
|
1024
|
+
const fail = (reason: string): Result<void, MaterialError> =>
|
|
1025
|
+
err(
|
|
1026
|
+
createMaterialError('material-tangent-required', {
|
|
1027
|
+
code: 'material-tangent-required',
|
|
1028
|
+
material: mat.name ?? '<unnamed>',
|
|
1029
|
+
mesh: mesh.name ?? '<unnamed>',
|
|
1030
|
+
layer: tangentSlot.layer ?? layer,
|
|
1031
|
+
uv: `TEXCOORD_${uvSet}`,
|
|
1032
|
+
attributes,
|
|
1033
|
+
reason,
|
|
1034
|
+
}),
|
|
1035
|
+
);
|
|
1036
|
+
if (mesh.tangents !== undefined) {
|
|
1037
|
+
if (
|
|
1038
|
+
mesh.tangents.length !== (mesh.positions.length / 3) * 4 ||
|
|
1039
|
+
mesh.tangents.some((value) => !Number.isFinite(value))
|
|
1040
|
+
) {
|
|
1041
|
+
return fail('imported TANGENT must be finite vec4 per vertex');
|
|
1042
|
+
}
|
|
1043
|
+
return ok(undefined);
|
|
1044
|
+
}
|
|
1045
|
+
if (mesh.normals === undefined) return fail('NORMAL is required to generate tangent');
|
|
1046
|
+
if (!(uv instanceof Float32Array))
|
|
1047
|
+
return fail('the selected UV set is required to generate tangent');
|
|
1048
|
+
const generated = computeTangentVec4(mesh.positions, mesh.normals, uv, mesh.indices);
|
|
1049
|
+
if (!generated.ok) {
|
|
1050
|
+
const detail = generated.error.detail;
|
|
1051
|
+
const reason =
|
|
1052
|
+
detail !== undefined && 'reason' in detail
|
|
1053
|
+
? String(detail.reason)
|
|
1054
|
+
: 'tangent producer rejected topology';
|
|
1055
|
+
return fail(reason);
|
|
1056
|
+
}
|
|
1057
|
+
return ok(undefined);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
857
1060
|
/** Convert a parsed GltfMaterialIr into a standard-root derived MaterialAsset. */
|
|
858
1061
|
export function toMaterialAsset(mat: GltfMaterialIr, ctx?: MaterialBridgeContext): MaterialAsset {
|
|
1062
|
+
const rootContract = standardRootParameterNames(mat);
|
|
859
1063
|
const values: Record<string, NonNullable<MaterialAsset['values']>[string]> = {
|
|
860
1064
|
baseColor: mat.baseColorFactor,
|
|
861
1065
|
metallic: mat.metallicFactor,
|
|
@@ -873,6 +1077,16 @@ export function toMaterialAsset(mat: GltfMaterialIr, ctx?: MaterialBridgeContext
|
|
|
873
1077
|
['emissiveTexture', mat.emissiveTexture],
|
|
874
1078
|
['transmissionTexture', mat.transmissionTexture],
|
|
875
1079
|
['thicknessTexture', mat.thicknessTexture],
|
|
1080
|
+
['clearcoatTexture', mat.clearcoatTexture],
|
|
1081
|
+
['clearcoatRoughnessTexture', mat.clearcoatRoughnessTexture],
|
|
1082
|
+
['clearcoatNormalTexture', mat.clearcoatNormalTexture],
|
|
1083
|
+
['anisotropyTexture', mat.anisotropyTexture],
|
|
1084
|
+
['sheenColorTexture', mat.sheenColorTexture],
|
|
1085
|
+
['sheenRoughnessTexture', mat.sheenRoughnessTexture],
|
|
1086
|
+
['iridescenceTexture', mat.iridescenceTexture],
|
|
1087
|
+
['iridescenceThicknessTexture', mat.iridescenceThicknessTexture],
|
|
1088
|
+
['specularTexture', mat.specularTexture],
|
|
1089
|
+
['specularColorTexture', mat.specularColorTexture],
|
|
876
1090
|
];
|
|
877
1091
|
for (const [slot, info] of textureSlots) {
|
|
878
1092
|
const value = textureValue(info, slot, ctx);
|
|
@@ -881,11 +1095,37 @@ export function toMaterialAsset(mat: GltfMaterialIr, ctx?: MaterialBridgeContext
|
|
|
881
1095
|
if (mat.occlusionTexture !== undefined && values.occlusionStrength === undefined) {
|
|
882
1096
|
values.occlusionStrength = 1;
|
|
883
1097
|
}
|
|
884
|
-
if (
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1098
|
+
if (rootContract.extended && rootContract.names.has('transmission')) {
|
|
1099
|
+
values.transmission = mat.transmissionFactor ?? 0;
|
|
1100
|
+
values.ior = mat.ior ?? 1.5;
|
|
1101
|
+
values.thickness = mat.thicknessFactor ?? 0;
|
|
1102
|
+
values.attenuationColor = mat.attenuationColor ?? [1, 1, 1];
|
|
1103
|
+
if (mat.attenuationDistance !== undefined) values.attenuationDistance = mat.attenuationDistance;
|
|
1104
|
+
}
|
|
1105
|
+
if (rootContract.names.has('clearcoat')) {
|
|
1106
|
+
values.clearcoat = mat.clearcoatFactor ?? 0;
|
|
1107
|
+
values.clearcoatRoughness = mat.clearcoatRoughnessFactor ?? 0;
|
|
1108
|
+
}
|
|
1109
|
+
const clearcoatNormal = textureInfo(mat.clearcoatNormalTexture) as
|
|
1110
|
+
| (GltfTextureInfoIr & { readonly scale?: number })
|
|
1111
|
+
| undefined;
|
|
1112
|
+
if (clearcoatNormal?.scale !== undefined) values.clearcoatNormalScale = clearcoatNormal.scale;
|
|
1113
|
+
if (rootContract.names.has('anisotropyStrength')) {
|
|
1114
|
+
values.anisotropyStrength = mat.anisotropyStrength ?? 0;
|
|
1115
|
+
values.anisotropyRotation = mat.anisotropyRotation ?? 0;
|
|
1116
|
+
}
|
|
1117
|
+
if (rootContract.names.has('sheenColor')) {
|
|
1118
|
+
values.sheenColor = mat.sheenColorFactor ?? [0, 0, 0];
|
|
1119
|
+
values.sheenRoughness = mat.sheenRoughnessFactor ?? 0;
|
|
1120
|
+
}
|
|
1121
|
+
if (rootContract.names.has('iridescence')) {
|
|
1122
|
+
values.iridescence = mat.iridescenceFactor ?? 0;
|
|
1123
|
+
values.iridescenceIor = mat.iridescenceIor ?? 1.3;
|
|
1124
|
+
values.iridescenceThicknessMinimum = mat.iridescenceThicknessMinimum ?? 100;
|
|
1125
|
+
values.iridescenceThicknessMaximum = mat.iridescenceThicknessMaximum ?? 400;
|
|
1126
|
+
}
|
|
1127
|
+
if (mat.specularFactor !== undefined) values.specular = mat.specularFactor;
|
|
1128
|
+
if (mat.specularColorFactor !== undefined) values.specularColor = mat.specularColorFactor;
|
|
889
1129
|
|
|
890
1130
|
const module = ctx?.skinned === true ? 'forgeax::pbr-skin' : 'forgeax::default-standard-pbr';
|
|
891
1131
|
|
|
@@ -932,8 +1172,13 @@ export function toMaterialAsset(mat: GltfMaterialIr, ctx?: MaterialBridgeContext
|
|
|
932
1172
|
return {
|
|
933
1173
|
kind: 'material',
|
|
934
1174
|
colorSpace: 'linear',
|
|
935
|
-
...(ctx?.standardRootGuid
|
|
1175
|
+
...(!rootContract.extended && ctx?.standardRootGuid !== undefined
|
|
1176
|
+
? { parent: ctx.standardRootGuid }
|
|
1177
|
+
: {}),
|
|
936
1178
|
passes: [pass],
|
|
1179
|
+
...(ctx?.standardRootGuid !== undefined && !rootContract.extended
|
|
1180
|
+
? {}
|
|
1181
|
+
: { parameters: standardMaterialParameters(rootContract.names) }),
|
|
937
1182
|
values,
|
|
938
1183
|
};
|
|
939
1184
|
}
|
package/src/check-extensions.ts
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Required-extension support contains EXT_mesh_gpu_instancing,
|
|
4
4
|
// EXT_meshopt_compression, KHR_lights_punctual, KHR_texture_transform, and
|
|
5
|
-
// the KHR material transmission/IOR/volume
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// the KHR material transmission/IOR/volume/clearcoat/anisotropy/sheen/
|
|
6
|
+
// iridescence/specular extensions. The exported list is the public supported
|
|
7
|
+
// extension surface consumed by both the required-extension gate and callers
|
|
8
|
+
// that need to discover support.
|
|
8
9
|
// (feat-20260518-gltf-instancing-and-name-component plan-strategy section
|
|
9
10
|
// 2 D-1 / D-3). Any extension listed in `extensionsRequired[]` outside
|
|
10
11
|
// the supported extension set triggers `gltf-extension-unsupported` (hard fail).
|
|
@@ -18,23 +19,30 @@
|
|
|
18
19
|
// diagnostics list is the single channel (no `console.error`).
|
|
19
20
|
//
|
|
20
21
|
// Future expansion (KHR_materials_unlit, ...) extends
|
|
21
|
-
// `
|
|
22
|
+
// `EXTENSION_ALLOWLIST` in place; each addition lands under its own feat-*
|
|
22
23
|
// loop with breaking-change registry entry.
|
|
23
24
|
|
|
24
25
|
import { err, type GltfError, gltfErr, ok, type Result } from './errors.js';
|
|
25
26
|
|
|
26
|
-
/**
|
|
27
|
+
/** Public supported extension list used by the required/used declaration gate. */
|
|
27
28
|
export const EXTENSION_ALLOWLIST: readonly string[] = [
|
|
28
29
|
'EXT_mesh_gpu_instancing',
|
|
29
30
|
'EXT_meshopt_compression',
|
|
30
|
-
];
|
|
31
|
-
const SUPPORTED_EXTENSIONS: readonly string[] = [
|
|
32
|
-
...EXTENSION_ALLOWLIST,
|
|
33
31
|
'KHR_lights_punctual',
|
|
34
32
|
'KHR_texture_transform',
|
|
35
33
|
'KHR_materials_transmission',
|
|
36
34
|
'KHR_materials_ior',
|
|
37
35
|
'KHR_materials_volume',
|
|
36
|
+
'KHR_materials_clearcoat',
|
|
37
|
+
'KHR_materials_anisotropy',
|
|
38
|
+
'KHR_materials_sheen',
|
|
39
|
+
'KHR_materials_iridescence',
|
|
40
|
+
'KHR_materials_specular',
|
|
41
|
+
];
|
|
42
|
+
const SUPPORTED_EXTENSIONS: readonly string[] = [
|
|
43
|
+
...EXTENSION_ALLOWLIST,
|
|
44
|
+
'MSFT_lod',
|
|
45
|
+
'MSFT_screencoverage',
|
|
38
46
|
];
|
|
39
47
|
|
|
40
48
|
export interface ExtensionsCheckResult {
|
package/src/errors.ts
CHANGED
|
@@ -60,6 +60,12 @@ export interface GltfExtensionUnsupportedDetail {
|
|
|
60
60
|
readonly source: 'extensionsRequired' | 'extensionsUsed';
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
export interface GltfLodInvalidDetail {
|
|
64
|
+
readonly rootNode: number;
|
|
65
|
+
readonly ids: readonly number[];
|
|
66
|
+
readonly reason: 'missing-node' | 'duplicate-node' | 'not-integer' | 'coverage';
|
|
67
|
+
}
|
|
68
|
+
|
|
63
69
|
export interface GltfMaterialTransmissionInvalidDetail {
|
|
64
70
|
readonly extension: 'KHR_materials_transmission' | 'KHR_materials_ior' | 'KHR_materials_volume';
|
|
65
71
|
readonly field: string;
|
|
@@ -67,6 +73,18 @@ export interface GltfMaterialTransmissionInvalidDetail {
|
|
|
67
73
|
readonly actual?: unknown;
|
|
68
74
|
}
|
|
69
75
|
|
|
76
|
+
export interface GltfMaterialPhysicalInvalidDetail {
|
|
77
|
+
readonly extension:
|
|
78
|
+
| 'KHR_materials_clearcoat'
|
|
79
|
+
| 'KHR_materials_anisotropy'
|
|
80
|
+
| 'KHR_materials_sheen'
|
|
81
|
+
| 'KHR_materials_iridescence'
|
|
82
|
+
| 'KHR_materials_specular';
|
|
83
|
+
readonly field: string;
|
|
84
|
+
readonly reason: 'type' | 'range' | 'non-finite';
|
|
85
|
+
readonly actual?: unknown;
|
|
86
|
+
}
|
|
87
|
+
|
|
70
88
|
/** `gltf-accessor-type-mismatch` payload: 4-member closed reason discriminator. */
|
|
71
89
|
export interface GltfAccessorTypeMismatchDetail {
|
|
72
90
|
readonly accessorIndex: number;
|
|
@@ -260,13 +278,23 @@ const gltfErrorPolicy = {
|
|
|
260
278
|
hint: 'rebuild .gltf with valid bufferViews; check accessor index; ensure accessor.byteOffset + EFFECTIVE_STRIDE * (count - 1) + element_size <= bufferView.byteLength',
|
|
261
279
|
},
|
|
262
280
|
'gltf-extension-unsupported': {
|
|
263
|
-
expected:
|
|
264
|
-
|
|
281
|
+
expected:
|
|
282
|
+
'extension listed in the supported allowlist (see EXTENSION_ALLOWLIST in @forgeax/engine-gltf)',
|
|
283
|
+
hint: 'remove the unsupported required extension or use a supported glTF extension; extensionsUsed-only entries remain diagnostic',
|
|
284
|
+
},
|
|
285
|
+
'gltf-lod-invalid': {
|
|
286
|
+
expected: 'MSFT_lod ids to reference unique existing node indices with valid coverage',
|
|
287
|
+
hint: 'repair the MSFT_lod node relation or remove it from extensionsRequired before re-importing',
|
|
265
288
|
},
|
|
266
289
|
'gltf-material-transmission-invalid': {
|
|
267
290
|
expected: 'KHR transmission, IOR, and volume values are finite and within their glTF ranges',
|
|
268
291
|
hint: 'repair the named glTF material extension value and re-import the source',
|
|
269
292
|
},
|
|
293
|
+
'gltf-material-physical-invalid': {
|
|
294
|
+
expected:
|
|
295
|
+
'KHR clearcoat, anisotropy, sheen, iridescence, and specular values are finite and within their glTF ranges',
|
|
296
|
+
hint: 'repair the named physical material extension value and re-import the source',
|
|
297
|
+
},
|
|
270
298
|
'gltf-accessor-type-mismatch': {
|
|
271
299
|
expected: 'dense fixed-stride accessor with supported componentType',
|
|
272
300
|
hint: 'sparse: see feat-future-gltf-sparse-accessor; morph: see feat-future-gltf-morph; interleaved: see feat-future-gltf-mesh-multi-section',
|
|
@@ -357,6 +385,7 @@ interface DetailFor {
|
|
|
357
385
|
readonly 'gltf-version-unsupported': GltfVersionUnsupportedDetail;
|
|
358
386
|
readonly 'gltf-buffer-out-of-bounds': GltfBufferOutOfBoundsDetail;
|
|
359
387
|
readonly 'gltf-extension-unsupported': GltfExtensionUnsupportedDetail;
|
|
388
|
+
readonly 'gltf-lod-invalid': GltfLodInvalidDetail;
|
|
360
389
|
readonly 'gltf-accessor-type-mismatch': GltfAccessorTypeMismatchDetail;
|
|
361
390
|
readonly 'gltf-texture-load-failed': GltfTextureLoadFailedDetail;
|
|
362
391
|
readonly 'gltf-meta-missing': GltfMetaMissingDetail;
|
|
@@ -376,6 +405,7 @@ interface DetailFor {
|
|
|
376
405
|
readonly 'gltf-color-accessor-malformed': GltfColorAccessorMalformedDetail;
|
|
377
406
|
readonly 'gltf-mesh-bridge-invalid': GltfMeshBridgeInvalidDetail;
|
|
378
407
|
readonly 'gltf-material-transmission-invalid': GltfMaterialTransmissionInvalidDetail;
|
|
408
|
+
readonly 'gltf-material-physical-invalid': GltfMaterialPhysicalInvalidDetail;
|
|
379
409
|
}
|
|
380
410
|
|
|
381
411
|
/**
|
package/src/gltf-importer.ts
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
// gap as `import-produced-no-assets`. A texture sub-asset that fails byte
|
|
33
33
|
// extraction surfaces as `gltf-image-extract-failed` (D-6).
|
|
34
34
|
|
|
35
|
+
import { deriveDefaultLodScreenCoverages } from '@forgeax/engine-import';
|
|
35
36
|
import { packMeshBinV4 } from '@forgeax/engine-import/mesh-bin';
|
|
36
37
|
import { AssetGuid as AssetGuidCodec } from '@forgeax/engine-pack/guid';
|
|
37
38
|
import type {
|
|
@@ -58,6 +59,7 @@ import {
|
|
|
58
59
|
gltfDocToSceneAsset,
|
|
59
60
|
meshIrToMeshAsset,
|
|
60
61
|
toMaterialAsset,
|
|
62
|
+
validateMaterialTangentInputs,
|
|
61
63
|
validateMaterialUvSets,
|
|
62
64
|
} from './bridge.js';
|
|
63
65
|
import { gltfErr } from './errors.js';
|
|
@@ -414,6 +416,16 @@ export function materialRefsForPack(
|
|
|
414
416
|
pushRefsForSlot(mat.emissiveTexture, 'emissiveTexture');
|
|
415
417
|
pushRefsForSlot(mat.transmissionTexture, 'transmissionTexture');
|
|
416
418
|
pushRefsForSlot(mat.thicknessTexture, 'thicknessTexture');
|
|
419
|
+
pushRefsForSlot(mat.clearcoatTexture, 'clearcoatTexture');
|
|
420
|
+
pushRefsForSlot(mat.clearcoatRoughnessTexture, 'clearcoatRoughnessTexture');
|
|
421
|
+
pushRefsForSlot(mat.clearcoatNormalTexture, 'clearcoatNormalTexture');
|
|
422
|
+
pushRefsForSlot(mat.anisotropyTexture, 'anisotropyTexture');
|
|
423
|
+
pushRefsForSlot(mat.sheenColorTexture, 'sheenColorTexture');
|
|
424
|
+
pushRefsForSlot(mat.sheenRoughnessTexture, 'sheenRoughnessTexture');
|
|
425
|
+
pushRefsForSlot(mat.iridescenceTexture, 'iridescenceTexture');
|
|
426
|
+
pushRefsForSlot(mat.iridescenceThicknessTexture, 'iridescenceThicknessTexture');
|
|
427
|
+
pushRefsForSlot(mat.specularTexture, 'specularTexture');
|
|
428
|
+
pushRefsForSlot(mat.specularColorTexture, 'specularColorTexture');
|
|
417
429
|
return refs;
|
|
418
430
|
}
|
|
419
431
|
|
|
@@ -443,6 +455,16 @@ function rewriteMaterialAssetRefs(
|
|
|
443
455
|
| 'emissiveTexture'
|
|
444
456
|
| 'transmissionTexture'
|
|
445
457
|
| 'thicknessTexture'
|
|
458
|
+
| 'clearcoatTexture'
|
|
459
|
+
| 'clearcoatRoughnessTexture'
|
|
460
|
+
| 'clearcoatNormalTexture'
|
|
461
|
+
| 'anisotropyTexture'
|
|
462
|
+
| 'sheenColorTexture'
|
|
463
|
+
| 'sheenRoughnessTexture'
|
|
464
|
+
| 'iridescenceTexture'
|
|
465
|
+
| 'iridescenceThicknessTexture'
|
|
466
|
+
| 'specularTexture'
|
|
467
|
+
| 'specularColorTexture'
|
|
446
468
|
),
|
|
447
469
|
GltfTextureInfoIr | number | undefined,
|
|
448
470
|
][] = [
|
|
@@ -453,6 +475,16 @@ function rewriteMaterialAssetRefs(
|
|
|
453
475
|
['emissiveTexture', mat.emissiveTexture],
|
|
454
476
|
['transmissionTexture', mat.transmissionTexture],
|
|
455
477
|
['thicknessTexture', mat.thicknessTexture],
|
|
478
|
+
['clearcoatTexture', mat.clearcoatTexture],
|
|
479
|
+
['clearcoatRoughnessTexture', mat.clearcoatRoughnessTexture],
|
|
480
|
+
['clearcoatNormalTexture', mat.clearcoatNormalTexture],
|
|
481
|
+
['anisotropyTexture', mat.anisotropyTexture],
|
|
482
|
+
['sheenColorTexture', mat.sheenColorTexture],
|
|
483
|
+
['sheenRoughnessTexture', mat.sheenRoughnessTexture],
|
|
484
|
+
['iridescenceTexture', mat.iridescenceTexture],
|
|
485
|
+
['iridescenceThicknessTexture', mat.iridescenceThicknessTexture],
|
|
486
|
+
['specularTexture', mat.specularTexture],
|
|
487
|
+
['specularColorTexture', mat.specularColorTexture],
|
|
456
488
|
];
|
|
457
489
|
let cursor = 0;
|
|
458
490
|
for (const [slot, rawBinding] of slots) {
|
|
@@ -591,7 +623,83 @@ async function importGltf(
|
|
|
591
623
|
}),
|
|
592
624
|
};
|
|
593
625
|
}
|
|
594
|
-
const
|
|
626
|
+
const stabilizedMesh = stabilizeMeshMaterialSlots(
|
|
627
|
+
bridged.value,
|
|
628
|
+
ctx,
|
|
629
|
+
sub.guid,
|
|
630
|
+
sub.sourceKey,
|
|
631
|
+
);
|
|
632
|
+
const lodGroup =
|
|
633
|
+
doc.lod?.groups?.find(
|
|
634
|
+
(group) => doc.nodes[group.rootNode]?.meshIndex === sub.sourceIndex,
|
|
635
|
+
) ??
|
|
636
|
+
(doc.lod?.rootNode !== undefined &&
|
|
637
|
+
doc.nodes[doc.lod.rootNode]?.meshIndex === sub.sourceIndex
|
|
638
|
+
? doc.lod
|
|
639
|
+
: undefined);
|
|
640
|
+
const rootMeshIndex = lodGroup === undefined ? undefined : sub.sourceIndex;
|
|
641
|
+
if (lodGroup !== undefined) {
|
|
642
|
+
const referencedMeshIndices = [
|
|
643
|
+
sub.sourceIndex,
|
|
644
|
+
...lodGroup.lodNodeIds.map((nodeIndex) => doc.nodes[nodeIndex]?.meshIndex),
|
|
645
|
+
];
|
|
646
|
+
const missingMeshIndex = referencedMeshIndices.find(
|
|
647
|
+
(meshIndex) =>
|
|
648
|
+
!Number.isInteger(meshIndex) ||
|
|
649
|
+
meshIndex === null ||
|
|
650
|
+
meshIndex === undefined ||
|
|
651
|
+
maps.meshGuidByIndex.get(meshIndex as number) === undefined,
|
|
652
|
+
);
|
|
653
|
+
if (missingMeshIndex !== undefined) {
|
|
654
|
+
return {
|
|
655
|
+
ok: false,
|
|
656
|
+
error: new ImportError({
|
|
657
|
+
code: 'import-internal-error',
|
|
658
|
+
expected: 'every MSFT_lod node to resolve a cooked mesh sub-asset',
|
|
659
|
+
actual: String(missingMeshIndex),
|
|
660
|
+
hint: 'repair the referenced glTF mesh primitives and re-run the importer',
|
|
661
|
+
detail: {
|
|
662
|
+
reason: `MSFT_lod group for mesh ${sub.sourceIndex} references an unprojected mesh`,
|
|
663
|
+
},
|
|
664
|
+
}),
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
const authoredLods =
|
|
669
|
+
sub.sourceKey === undefined ? undefined : ctx.sourceOverrides?.[sub.sourceKey]?.lods;
|
|
670
|
+
const lodEntries = Array.isArray(authoredLods) ? authoredLods : [];
|
|
671
|
+
const lodLevels =
|
|
672
|
+
rootMeshIndex === sub.sourceIndex && lodGroup !== undefined
|
|
673
|
+
? lodGroup.lodNodeIds.flatMap((nodeIndex, index) => {
|
|
674
|
+
const meshIndex = doc.nodes[nodeIndex]?.meshIndex;
|
|
675
|
+
const guid =
|
|
676
|
+
meshIndex === undefined || meshIndex === null
|
|
677
|
+
? undefined
|
|
678
|
+
: maps.meshGuidByIndex.get(meshIndex);
|
|
679
|
+
if (guid === undefined) return [];
|
|
680
|
+
const authored = lodEntries[index];
|
|
681
|
+
const authoredCoverage =
|
|
682
|
+
authored !== null && typeof authored === 'object'
|
|
683
|
+
? (authored as { readonly screenCoverage?: unknown }).screenCoverage
|
|
684
|
+
: undefined;
|
|
685
|
+
const coverage =
|
|
686
|
+
typeof authoredCoverage === 'number'
|
|
687
|
+
? authoredCoverage
|
|
688
|
+
: (lodGroup.screenCoverages[index] ??
|
|
689
|
+
deriveDefaultLodScreenCoverages(lodGroup.lodNodeIds.length + 1)[index]);
|
|
690
|
+
const parsed = AssetGuidCodec.parse(guid);
|
|
691
|
+
return parsed.ok && coverage !== undefined
|
|
692
|
+
? [{ mesh: parsed.value, screenCoverage: coverage, guid }]
|
|
693
|
+
: [];
|
|
694
|
+
})
|
|
695
|
+
: [];
|
|
696
|
+
const meshPayload: MeshAsset =
|
|
697
|
+
lodLevels.length === 0
|
|
698
|
+
? stabilizedMesh
|
|
699
|
+
: {
|
|
700
|
+
...stabilizedMesh,
|
|
701
|
+
lods: lodLevels.map(({ mesh, screenCoverage }) => ({ mesh, screenCoverage })),
|
|
702
|
+
};
|
|
595
703
|
const materialRefs: AssetRef[] = [];
|
|
596
704
|
const seenMaterialGuids = new Set<string>();
|
|
597
705
|
for (let slotIndex = 0; slotIndex < meshPayload.materialSlots.length; slotIndex++) {
|
|
@@ -606,6 +714,15 @@ async function importGltf(
|
|
|
606
714
|
});
|
|
607
715
|
}
|
|
608
716
|
}
|
|
717
|
+
for (const [lodIndex, level] of lodLevels.entries()) {
|
|
718
|
+
if (!seenMaterialGuids.has(level.guid.toLowerCase())) {
|
|
719
|
+
seenMaterialGuids.add(level.guid.toLowerCase());
|
|
720
|
+
materialRefs.push({
|
|
721
|
+
guid: level.guid,
|
|
722
|
+
sourceField: { fieldName: 'lods', arrayIndex: lodIndex },
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
}
|
|
609
726
|
out.push({
|
|
610
727
|
guid: sub.guid,
|
|
611
728
|
kind: 'mesh',
|
|
@@ -664,6 +781,10 @@ async function importGltf(
|
|
|
664
781
|
if (!uvResult.ok) {
|
|
665
782
|
throw Object.assign(new Error(uvResult.error.message), uvResult.error);
|
|
666
783
|
}
|
|
784
|
+
const tangentResult = validateMaterialTangentInputs(mat, meshIr);
|
|
785
|
+
if (!tangentResult.ok) {
|
|
786
|
+
throw Object.assign(new Error(tangentResult.error.message), tangentResult.error);
|
|
787
|
+
}
|
|
667
788
|
}
|
|
668
789
|
const matAsset = toMaterialAsset(mat, {
|
|
669
790
|
textureHandles: maps.textureHandles,
|