@forgeax/engine-gltf 0.1.23 → 0.1.25

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 (43) hide show
  1. package/README.md +56 -3
  2. package/dist/__tests__/physical-clearcoat.unit.test.d.ts +2 -0
  3. package/dist/__tests__/physical-clearcoat.unit.test.d.ts.map +1 -0
  4. package/dist/__tests__/physical-material-import.integration.test.d.ts +2 -0
  5. package/dist/__tests__/physical-material-import.integration.test.d.ts.map +1 -0
  6. package/dist/bridge.d.ts +1 -0
  7. package/dist/bridge.d.ts.map +1 -1
  8. package/dist/check-extensions.d.ts +1 -1
  9. package/dist/check-extensions.d.ts.map +1 -1
  10. package/dist/cli-gltf.mjs +181 -8
  11. package/dist/cli-gltf.mjs.map +1 -1
  12. package/dist/errors.d.ts +7 -0
  13. package/dist/errors.d.ts.map +1 -1
  14. package/dist/gltf-importer.d.ts.map +1 -1
  15. package/dist/image-color-space.d.ts +12 -0
  16. package/dist/image-color-space.d.ts.map +1 -1
  17. package/dist/importer-entry.mjs +377 -26
  18. package/dist/importer-entry.mjs.map +1 -1
  19. package/dist/index.d.ts +2 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.mjs +378 -27
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/material/parse-material.d.ts +66 -0
  24. package/dist/material/parse-material.d.ts.map +1 -1
  25. package/dist/node-file-entry.mjs +181 -8
  26. package/dist/node-file-entry.mjs.map +1 -1
  27. package/package.json +12 -12
  28. package/src/__tests__/bridge-material-values.unit.test.ts +107 -1
  29. package/src/__tests__/bridge-skin-stride.unit.test.ts +22 -0
  30. package/src/__tests__/gltf-error-derived-views.test-d.ts +4 -0
  31. package/src/__tests__/gltf-error-migration.test.ts +3 -3
  32. package/src/__tests__/gltf.unit.test.ts +61 -6
  33. package/src/__tests__/material-pack-refs.integration.test.ts +53 -0
  34. package/src/__tests__/parse-gltf.unit.test.ts +29 -1
  35. package/src/__tests__/physical-clearcoat.unit.test.ts +110 -0
  36. package/src/__tests__/physical-material-import.integration.test.ts +239 -0
  37. package/src/bridge.ts +260 -15
  38. package/src/check-extensions.ts +16 -10
  39. package/src/errors.ts +21 -2
  40. package/src/gltf-importer.ts +35 -0
  41. package/src/image-color-space.ts +29 -4
  42. package/src/index.ts +8 -1
  43. package/src/material/parse-material.ts +307 -0
package/src/errors.ts CHANGED
@@ -73,6 +73,18 @@ export interface GltfMaterialTransmissionInvalidDetail {
73
73
  readonly actual?: unknown;
74
74
  }
75
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
+
76
88
  /** `gltf-accessor-type-mismatch` payload: 4-member closed reason discriminator. */
77
89
  export interface GltfAccessorTypeMismatchDetail {
78
90
  readonly accessorIndex: number;
@@ -266,8 +278,9 @@ const gltfErrorPolicy = {
266
278
  hint: 'rebuild .gltf with valid bufferViews; check accessor index; ensure accessor.byteOffset + EFFECTIVE_STRIDE * (count - 1) + element_size <= bufferView.byteLength',
267
279
  },
268
280
  'gltf-extension-unsupported': {
269
- expected: 'extension listed in v1 allowlist (see EXTENSION_ALLOWLIST in @forgeax/engine-gltf)',
270
- hint: 'see feat-future-gltf-extensions-allowlist; remove this extension or wait for the allowlist to expand',
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',
271
284
  },
272
285
  'gltf-lod-invalid': {
273
286
  expected: 'MSFT_lod ids to reference unique existing node indices with valid coverage',
@@ -277,6 +290,11 @@ const gltfErrorPolicy = {
277
290
  expected: 'KHR transmission, IOR, and volume values are finite and within their glTF ranges',
278
291
  hint: 'repair the named glTF material extension value and re-import the source',
279
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
+ },
280
298
  'gltf-accessor-type-mismatch': {
281
299
  expected: 'dense fixed-stride accessor with supported componentType',
282
300
  hint: 'sparse: see feat-future-gltf-sparse-accessor; morph: see feat-future-gltf-morph; interleaved: see feat-future-gltf-mesh-multi-section',
@@ -387,6 +405,7 @@ interface DetailFor {
387
405
  readonly 'gltf-color-accessor-malformed': GltfColorAccessorMalformedDetail;
388
406
  readonly 'gltf-mesh-bridge-invalid': GltfMeshBridgeInvalidDetail;
389
407
  readonly 'gltf-material-transmission-invalid': GltfMaterialTransmissionInvalidDetail;
408
+ readonly 'gltf-material-physical-invalid': GltfMaterialPhysicalInvalidDetail;
390
409
  }
391
410
 
392
411
  /**
@@ -59,6 +59,7 @@ import {
59
59
  gltfDocToSceneAsset,
60
60
  meshIrToMeshAsset,
61
61
  toMaterialAsset,
62
+ validateMaterialTangentInputs,
62
63
  validateMaterialUvSets,
63
64
  } from './bridge.js';
64
65
  import { gltfErr } from './errors.js';
@@ -415,6 +416,16 @@ export function materialRefsForPack(
415
416
  pushRefsForSlot(mat.emissiveTexture, 'emissiveTexture');
416
417
  pushRefsForSlot(mat.transmissionTexture, 'transmissionTexture');
417
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');
418
429
  return refs;
419
430
  }
420
431
 
@@ -444,6 +455,16 @@ function rewriteMaterialAssetRefs(
444
455
  | 'emissiveTexture'
445
456
  | 'transmissionTexture'
446
457
  | 'thicknessTexture'
458
+ | 'clearcoatTexture'
459
+ | 'clearcoatRoughnessTexture'
460
+ | 'clearcoatNormalTexture'
461
+ | 'anisotropyTexture'
462
+ | 'sheenColorTexture'
463
+ | 'sheenRoughnessTexture'
464
+ | 'iridescenceTexture'
465
+ | 'iridescenceThicknessTexture'
466
+ | 'specularTexture'
467
+ | 'specularColorTexture'
447
468
  ),
448
469
  GltfTextureInfoIr | number | undefined,
449
470
  ][] = [
@@ -454,6 +475,16 @@ function rewriteMaterialAssetRefs(
454
475
  ['emissiveTexture', mat.emissiveTexture],
455
476
  ['transmissionTexture', mat.transmissionTexture],
456
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],
457
488
  ];
458
489
  let cursor = 0;
459
490
  for (const [slot, rawBinding] of slots) {
@@ -750,6 +781,10 @@ async function importGltf(
750
781
  if (!uvResult.ok) {
751
782
  throw Object.assign(new Error(uvResult.error.message), uvResult.error);
752
783
  }
784
+ const tangentResult = validateMaterialTangentInputs(mat, meshIr);
785
+ if (!tangentResult.ok) {
786
+ throw Object.assign(new Error(tangentResult.error.message), tangentResult.error);
787
+ }
753
788
  }
754
789
  const matAsset = toMaterialAsset(mat, {
755
790
  textureHandles: maps.textureHandles,
@@ -3,10 +3,11 @@
3
3
  //
4
4
  // glTF 2.0 spec section 6.2 ("Material → Texture and Sampler") names which
5
5
  // texture slots are colour-encoded vs data-encoded:
6
- // - sRGB : baseColorTexture, emissiveTexture
7
- // - linear : metallicRoughnessTexture, normalTexture, occlusionTexture
8
- // MaterialIr carries baseColor / metallicRoughness / normal / emissive;
9
- // occlusion remains outside this Tier-C subset.
6
+ // - sRGB : baseColorTexture, emissiveTexture, sheenColorTexture,
7
+ // specularColorTexture
8
+ // - linear : metallicRoughnessTexture, normalTexture, occlusionTexture,
9
+ // transmission/volume/clearcoat/anisotropy/sheen-roughness/
10
+ // iridescence/specular-weight data textures
10
11
  // We pre-scan the doc so the gltfImporter knows each `images[]` row's
11
12
  // colorSpace before decoding (TextureAsset.colorSpace + .format derive
12
13
  // from this).
@@ -36,6 +37,18 @@ export interface MaterialColorSpaceInput {
36
37
  readonly normalTexture?: TextureBinding;
37
38
  readonly emissiveTexture?: TextureBinding;
38
39
  readonly occlusionTexture?: TextureBinding;
40
+ readonly transmissionTexture?: TextureBinding;
41
+ readonly thicknessTexture?: TextureBinding;
42
+ readonly clearcoatTexture?: TextureBinding;
43
+ readonly clearcoatRoughnessTexture?: TextureBinding;
44
+ readonly clearcoatNormalTexture?: TextureBinding;
45
+ readonly anisotropyTexture?: TextureBinding;
46
+ readonly sheenColorTexture?: TextureBinding;
47
+ readonly sheenRoughnessTexture?: TextureBinding;
48
+ readonly iridescenceTexture?: TextureBinding;
49
+ readonly iridescenceThicknessTexture?: TextureBinding;
50
+ readonly specularTexture?: TextureBinding;
51
+ readonly specularColorTexture?: TextureBinding;
39
52
  }
40
53
 
41
54
  /** Slim view of a parsed `textures[]` row. */
@@ -89,9 +102,21 @@ export function deriveTextureColorSpace(
89
102
  for (const mat of input.materials) {
90
103
  record(imageOfTexture(mat.baseColorTexture), 'srgb');
91
104
  record(imageOfTexture(mat.emissiveTexture), 'srgb');
105
+ record(imageOfTexture(mat.sheenColorTexture), 'srgb');
106
+ record(imageOfTexture(mat.specularColorTexture), 'srgb');
92
107
  record(imageOfTexture(mat.metallicRoughnessTexture), 'linear');
93
108
  record(imageOfTexture(mat.normalTexture), 'linear');
94
109
  record(imageOfTexture(mat.occlusionTexture), 'linear');
110
+ record(imageOfTexture(mat.transmissionTexture), 'linear');
111
+ record(imageOfTexture(mat.thicknessTexture), 'linear');
112
+ record(imageOfTexture(mat.clearcoatTexture), 'linear');
113
+ record(imageOfTexture(mat.clearcoatRoughnessTexture), 'linear');
114
+ record(imageOfTexture(mat.clearcoatNormalTexture), 'linear');
115
+ record(imageOfTexture(mat.anisotropyTexture), 'linear');
116
+ record(imageOfTexture(mat.sheenRoughnessTexture), 'linear');
117
+ record(imageOfTexture(mat.iridescenceTexture), 'linear');
118
+ record(imageOfTexture(mat.iridescenceThicknessTexture), 'linear');
119
+ record(imageOfTexture(mat.specularTexture), 'linear');
95
120
  }
96
121
 
97
122
  for (let i = 0; i < input.imageCount; i++) {
package/src/index.ts CHANGED
@@ -47,7 +47,13 @@ export type { GltfBridgeContext, MaterialBridgeContext } from './bridge.js';
47
47
  // hello-gltf + hello-gltf-instancing, feat-20260518 M3 w9; M3 Tier-C material
48
48
  // bridge; meshIrToMeshAsset excised from the hello-gltf demo in
49
49
  // feat-20260603-asset-import-loader-injection M2 w19).
50
- export { gltfDocToSceneAsset, meshIrToMeshAsset, toMaterialAsset } from './bridge.js';
50
+ export {
51
+ gltfDocToSceneAsset,
52
+ meshIrToMeshAsset,
53
+ toMaterialAsset,
54
+ validateMaterialTangentInputs,
55
+ validateMaterialUvSets,
56
+ } from './bridge.js';
51
57
  export type { ExtensionsCheckResult, GltfExtensionsJson } from './check-extensions.js';
52
58
  // KHR extensions gate (w14).
53
59
  export { checkExtensions, EXTENSION_ALLOWLIST } from './check-extensions.js';
@@ -65,6 +71,7 @@ export type {
65
71
  GltfImageMimeUnsupportedDetail,
66
72
  GltfInstancingCountMismatchDetail,
67
73
  GltfMalformedHeaderDetail,
74
+ GltfMaterialPhysicalInvalidDetail,
68
75
  GltfMaterialTransmissionInvalidDetail,
69
76
  GltfMetaMissingDetail,
70
77
  GltfMorphInvalidDetail,
@@ -93,6 +93,38 @@ export interface GltfMaterialJson {
93
93
  readonly attenuationColor?: readonly number[];
94
94
  readonly attenuationDistance?: number;
95
95
  };
96
+ readonly KHR_materials_clearcoat?: {
97
+ readonly clearcoatFactor?: number;
98
+ readonly clearcoatTexture?: TextureInfoJson;
99
+ readonly clearcoatRoughnessFactor?: number;
100
+ readonly clearcoatRoughnessTexture?: TextureInfoJson;
101
+ readonly clearcoatNormalTexture?: NormalTextureInfoJson;
102
+ };
103
+ readonly KHR_materials_anisotropy?: {
104
+ readonly anisotropyStrength?: number;
105
+ readonly anisotropyRotation?: number;
106
+ readonly anisotropyTexture?: TextureInfoJson;
107
+ };
108
+ readonly KHR_materials_sheen?: {
109
+ readonly sheenColorFactor?: readonly number[];
110
+ readonly sheenColorTexture?: TextureInfoJson;
111
+ readonly sheenRoughnessFactor?: number;
112
+ readonly sheenRoughnessTexture?: TextureInfoJson;
113
+ };
114
+ readonly KHR_materials_iridescence?: {
115
+ readonly iridescenceFactor?: number;
116
+ readonly iridescenceIor?: number;
117
+ readonly iridescenceThicknessMinimum?: number;
118
+ readonly iridescenceThicknessMaximum?: number;
119
+ readonly iridescenceTexture?: TextureInfoJson;
120
+ readonly iridescenceThicknessTexture?: TextureInfoJson;
121
+ };
122
+ readonly KHR_materials_specular?: {
123
+ readonly specularFactor?: number;
124
+ readonly specularTexture?: TextureInfoJson;
125
+ readonly specularColorFactor?: readonly number[];
126
+ readonly specularColorTexture?: TextureInfoJson;
127
+ };
96
128
  };
97
129
  }
98
130
 
@@ -119,6 +151,40 @@ export interface GltfMaterialIr {
119
151
  readonly thicknessChannel?: number;
120
152
  readonly attenuationColor?: readonly [number, number, number];
121
153
  readonly attenuationDistance?: number;
154
+ readonly clearcoatFactor?: number;
155
+ readonly clearcoatChannel?: number;
156
+ readonly clearcoatTexture?: GltfTextureInfoIr | number;
157
+ readonly clearcoatRoughnessFactor?: number;
158
+ readonly clearcoatRoughnessChannel?: number;
159
+ readonly clearcoatRoughnessTexture?: GltfTextureInfoIr | number;
160
+ readonly clearcoatNormalTexture?: GltfNormalTextureInfoIr | number;
161
+ readonly clearcoatNormalChannel?: readonly [number, number];
162
+ readonly anisotropyStrength?: number;
163
+ readonly anisotropyRotation?: number;
164
+ readonly anisotropyTexture?: GltfTextureInfoIr | number;
165
+ /** KHR_materials_anisotropy uses RG direction and B strength. */
166
+ readonly anisotropyChannel?: readonly [number, number, number];
167
+ readonly sheenColorFactor?: readonly [number, number, number];
168
+ readonly sheenColorTexture?: GltfTextureInfoIr | number;
169
+ readonly sheenColorChannel?: readonly [number, number, number];
170
+ readonly sheenRoughnessFactor?: number;
171
+ readonly sheenRoughnessTexture?: GltfTextureInfoIr | number;
172
+ readonly sheenRoughnessChannel?: number;
173
+ readonly iridescenceFactor?: number;
174
+ readonly iridescenceIor?: number;
175
+ readonly iridescenceThicknessMinimum?: number;
176
+ readonly iridescenceThicknessMaximum?: number;
177
+ readonly iridescenceTexture?: GltfTextureInfoIr | number;
178
+ readonly iridescenceChannel?: number;
179
+ readonly iridescenceThicknessTexture?: GltfTextureInfoIr | number;
180
+ readonly iridescenceThicknessChannel?: number;
181
+ readonly specularFactor?: number;
182
+ readonly specularTexture?: GltfTextureInfoIr | number;
183
+ /** KHR_materials_specular specularTexture uses the alpha channel. */
184
+ readonly specularChannel?: number;
185
+ readonly specularColorFactor?: readonly [number, number, number];
186
+ readonly specularColorTexture?: GltfTextureInfoIr | number;
187
+ readonly specularColorChannel?: readonly [number, number, number];
122
188
  readonly alphaMode?: 'OPAQUE' | 'MASK' | 'BLEND';
123
189
  readonly alphaCutoff?: number;
124
190
  readonly doubleSided?: boolean;
@@ -186,6 +252,11 @@ export function parseMaterial(
186
252
  const transmission = matJson.extensions?.KHR_materials_transmission;
187
253
  const ior = matJson.extensions?.KHR_materials_ior;
188
254
  const volume = matJson.extensions?.KHR_materials_volume;
255
+ const clearcoat = matJson.extensions?.KHR_materials_clearcoat;
256
+ const anisotropy = matJson.extensions?.KHR_materials_anisotropy;
257
+ const sheen = matJson.extensions?.KHR_materials_sheen;
258
+ const iridescence = matJson.extensions?.KHR_materials_iridescence;
259
+ const specular = matJson.extensions?.KHR_materials_specular;
189
260
  const attenuationColor = tuple3(volume?.attenuationColor);
190
261
  const invalid = (
191
262
  extension: 'KHR_materials_transmission' | 'KHR_materials_ior' | 'KHR_materials_volume',
@@ -201,6 +272,25 @@ export function parseMaterial(
201
272
  ...(actual === undefined ? {} : { actual }),
202
273
  }),
203
274
  );
275
+ const physicalInvalid = (
276
+ extension:
277
+ | 'KHR_materials_clearcoat'
278
+ | 'KHR_materials_anisotropy'
279
+ | 'KHR_materials_sheen'
280
+ | 'KHR_materials_iridescence'
281
+ | 'KHR_materials_specular',
282
+ field: string,
283
+ reason: 'type' | 'range' | 'non-finite',
284
+ actual?: unknown,
285
+ ): Result<GltfMaterialIr, GltfError> =>
286
+ err(
287
+ gltfErr('gltf-material-physical-invalid', {
288
+ extension,
289
+ field,
290
+ reason,
291
+ ...(actual === undefined ? {} : { actual }),
292
+ }),
293
+ );
204
294
  if (
205
295
  transmission?.transmissionFactor !== undefined &&
206
296
  (typeof transmission.transmissionFactor !== 'number' ||
@@ -270,6 +360,132 @@ export function parseMaterial(
270
360
  if (alphaMode === 'BLEND' && (transmission?.transmissionFactor ?? 0) > 0) {
271
361
  return invalid('KHR_materials_transmission', 'alphaMode', 'blend', alphaMode);
272
362
  }
363
+ if (
364
+ clearcoat?.clearcoatFactor !== undefined &&
365
+ (typeof clearcoat.clearcoatFactor !== 'number' || !Number.isFinite(clearcoat.clearcoatFactor))
366
+ ) {
367
+ return physicalInvalid(
368
+ 'KHR_materials_clearcoat',
369
+ 'clearcoatFactor',
370
+ 'non-finite',
371
+ clearcoat.clearcoatFactor,
372
+ );
373
+ }
374
+ if (
375
+ clearcoat?.clearcoatFactor !== undefined &&
376
+ (clearcoat.clearcoatFactor < 0 || clearcoat.clearcoatFactor > 1)
377
+ ) {
378
+ return physicalInvalid(
379
+ 'KHR_materials_clearcoat',
380
+ 'clearcoatFactor',
381
+ 'range',
382
+ clearcoat.clearcoatFactor,
383
+ );
384
+ }
385
+ if (
386
+ clearcoat?.clearcoatRoughnessFactor !== undefined &&
387
+ (typeof clearcoat.clearcoatRoughnessFactor !== 'number' ||
388
+ !Number.isFinite(clearcoat.clearcoatRoughnessFactor))
389
+ ) {
390
+ return physicalInvalid(
391
+ 'KHR_materials_clearcoat',
392
+ 'clearcoatRoughnessFactor',
393
+ 'non-finite',
394
+ clearcoat.clearcoatRoughnessFactor,
395
+ );
396
+ }
397
+ if (
398
+ clearcoat?.clearcoatRoughnessFactor !== undefined &&
399
+ (clearcoat.clearcoatRoughnessFactor < 0 || clearcoat.clearcoatRoughnessFactor > 1)
400
+ ) {
401
+ return physicalInvalid(
402
+ 'KHR_materials_clearcoat',
403
+ 'clearcoatRoughnessFactor',
404
+ 'range',
405
+ clearcoat.clearcoatRoughnessFactor,
406
+ );
407
+ }
408
+ if (
409
+ clearcoat?.clearcoatNormalTexture?.scale !== undefined &&
410
+ (typeof clearcoat.clearcoatNormalTexture.scale !== 'number' ||
411
+ !Number.isFinite(clearcoat.clearcoatNormalTexture.scale) ||
412
+ clearcoat.clearcoatNormalTexture.scale < 0)
413
+ ) {
414
+ return physicalInvalid(
415
+ 'KHR_materials_clearcoat',
416
+ 'clearcoatNormalTexture.scale',
417
+ 'range',
418
+ clearcoat.clearcoatNormalTexture.scale,
419
+ );
420
+ }
421
+ const validateUnit = (
422
+ extension: Parameters<typeof physicalInvalid>[0],
423
+ field: string,
424
+ value: number | undefined,
425
+ ) => {
426
+ if (value === undefined) return undefined;
427
+ if (typeof value !== 'number' || !Number.isFinite(value))
428
+ return physicalInvalid(extension, field, 'non-finite', value);
429
+ if (value < 0 || value > 1) return physicalInvalid(extension, field, 'range', value);
430
+ return undefined;
431
+ };
432
+ const validateFinite = (
433
+ extension: Parameters<typeof physicalInvalid>[0],
434
+ field: string,
435
+ value: number | undefined,
436
+ minimum?: number,
437
+ ) => {
438
+ if (value === undefined) return undefined;
439
+ if (typeof value !== 'number' || !Number.isFinite(value))
440
+ return physicalInvalid(extension, field, 'non-finite', value);
441
+ if (minimum !== undefined && value < minimum)
442
+ return physicalInvalid(extension, field, 'range', value);
443
+ return undefined;
444
+ };
445
+ const validateColor = (
446
+ extension: Parameters<typeof physicalInvalid>[0],
447
+ field: string,
448
+ value: readonly number[] | undefined,
449
+ ) => {
450
+ if (value === undefined) return undefined;
451
+ if (
452
+ value.length < 3 ||
453
+ value.slice(0, 3).some((channel) => typeof channel !== 'number' || !Number.isFinite(channel))
454
+ ) {
455
+ return physicalInvalid(extension, field, 'type', value);
456
+ }
457
+ if (value.slice(0, 3).some((channel) => channel < 0 || channel > 1)) {
458
+ return physicalInvalid(extension, field, 'range', value);
459
+ }
460
+ return undefined;
461
+ };
462
+ const physicalChecks = [
463
+ validateUnit('KHR_materials_anisotropy', 'anisotropyStrength', anisotropy?.anisotropyStrength),
464
+ validateFinite(
465
+ 'KHR_materials_anisotropy',
466
+ 'anisotropyRotation',
467
+ anisotropy?.anisotropyRotation,
468
+ ),
469
+ validateUnit('KHR_materials_sheen', 'sheenRoughnessFactor', sheen?.sheenRoughnessFactor),
470
+ validateColor('KHR_materials_sheen', 'sheenColorFactor', sheen?.sheenColorFactor),
471
+ validateUnit('KHR_materials_iridescence', 'iridescenceFactor', iridescence?.iridescenceFactor),
472
+ validateFinite('KHR_materials_iridescence', 'iridescenceIor', iridescence?.iridescenceIor, 1),
473
+ validateFinite(
474
+ 'KHR_materials_iridescence',
475
+ 'iridescenceThicknessMinimum',
476
+ iridescence?.iridescenceThicknessMinimum,
477
+ 0,
478
+ ),
479
+ validateFinite(
480
+ 'KHR_materials_iridescence',
481
+ 'iridescenceThicknessMaximum',
482
+ iridescence?.iridescenceThicknessMaximum,
483
+ 0,
484
+ ),
485
+ validateUnit('KHR_materials_specular', 'specularFactor', specular?.specularFactor),
486
+ validateColor('KHR_materials_specular', 'specularColorFactor', specular?.specularColorFactor),
487
+ ];
488
+ for (const check of physicalChecks) if (check !== undefined) return check;
273
489
  return ok({
274
490
  ...(matJson.name === undefined ? {} : { name: matJson.name }),
275
491
  baseColorFactor: baseColor4,
@@ -330,6 +546,97 @@ export function parseMaterial(
330
546
  ? {}
331
547
  : { attenuationDistance: volume.attenuationDistance }),
332
548
  }),
549
+ ...(clearcoat === undefined
550
+ ? {}
551
+ : {
552
+ clearcoatFactor: clearcoat.clearcoatFactor ?? 0,
553
+ clearcoatChannel: 0,
554
+ clearcoatRoughnessFactor: clearcoat.clearcoatRoughnessFactor ?? 0,
555
+ clearcoatRoughnessChannel: 1,
556
+ clearcoatNormalChannel: [0, 1] as const,
557
+ ...(clearcoat.clearcoatTexture === undefined
558
+ ? {}
559
+ : { clearcoatTexture: parseTextureInfo(clearcoat.clearcoatTexture, textures) }),
560
+ ...(clearcoat.clearcoatRoughnessTexture === undefined
561
+ ? {}
562
+ : {
563
+ clearcoatRoughnessTexture: parseTextureInfo(
564
+ clearcoat.clearcoatRoughnessTexture,
565
+ textures,
566
+ ),
567
+ }),
568
+ ...(clearcoat.clearcoatNormalTexture === undefined
569
+ ? {}
570
+ : {
571
+ clearcoatNormalTexture: {
572
+ ...parseTextureInfo(clearcoat.clearcoatNormalTexture, textures),
573
+ ...(clearcoat.clearcoatNormalTexture.scale === undefined
574
+ ? {}
575
+ : { scale: clearcoat.clearcoatNormalTexture.scale }),
576
+ },
577
+ }),
578
+ }),
579
+ ...(anisotropy === undefined
580
+ ? {}
581
+ : {
582
+ anisotropyStrength: anisotropy.anisotropyStrength ?? 0,
583
+ anisotropyRotation: anisotropy.anisotropyRotation ?? 0,
584
+ anisotropyChannel: [0, 1, 2] as const,
585
+ ...(anisotropy.anisotropyTexture === undefined
586
+ ? {}
587
+ : { anisotropyTexture: parseTextureInfo(anisotropy.anisotropyTexture, textures) }),
588
+ }),
589
+ ...(sheen === undefined
590
+ ? {}
591
+ : {
592
+ sheenColorFactor: tuple3(sheen.sheenColorFactor) ?? [0, 0, 0],
593
+ sheenColorChannel: [0, 1, 2] as const,
594
+ sheenRoughnessFactor: sheen.sheenRoughnessFactor ?? 0,
595
+ sheenRoughnessChannel: 3,
596
+ ...(sheen.sheenColorTexture === undefined
597
+ ? {}
598
+ : { sheenColorTexture: parseTextureInfo(sheen.sheenColorTexture, textures) }),
599
+ ...(sheen.sheenRoughnessTexture === undefined
600
+ ? {}
601
+ : { sheenRoughnessTexture: parseTextureInfo(sheen.sheenRoughnessTexture, textures) }),
602
+ }),
603
+ ...(iridescence === undefined
604
+ ? {}
605
+ : {
606
+ iridescenceFactor: iridescence.iridescenceFactor ?? 0,
607
+ iridescenceIor: iridescence.iridescenceIor ?? 1.3,
608
+ iridescenceThicknessMinimum: iridescence.iridescenceThicknessMinimum ?? 100,
609
+ iridescenceThicknessMaximum: iridescence.iridescenceThicknessMaximum ?? 400,
610
+ iridescenceChannel: 0,
611
+ iridescenceThicknessChannel: 1,
612
+ ...(iridescence.iridescenceTexture === undefined
613
+ ? {}
614
+ : { iridescenceTexture: parseTextureInfo(iridescence.iridescenceTexture, textures) }),
615
+ ...(iridescence.iridescenceThicknessTexture === undefined
616
+ ? {}
617
+ : {
618
+ iridescenceThicknessTexture: parseTextureInfo(
619
+ iridescence.iridescenceThicknessTexture,
620
+ textures,
621
+ ),
622
+ }),
623
+ }),
624
+ ...(specular === undefined
625
+ ? {}
626
+ : {
627
+ specularFactor: specular.specularFactor ?? 1,
628
+ specularChannel: 3,
629
+ specularColorFactor: tuple3(specular.specularColorFactor) ?? [1, 1, 1],
630
+ specularColorChannel: [0, 1, 2] as const,
631
+ ...(specular.specularTexture === undefined
632
+ ? {}
633
+ : { specularTexture: parseTextureInfo(specular.specularTexture, textures) }),
634
+ ...(specular.specularColorTexture === undefined
635
+ ? {}
636
+ : {
637
+ specularColorTexture: parseTextureInfo(specular.specularColorTexture, textures),
638
+ }),
639
+ }),
333
640
  ...(alphaMode === undefined ? {} : { alphaMode }),
334
641
  ...(alphaCutoff === undefined ? {} : { alphaCutoff }),
335
642
  ...(matJson.doubleSided === true ? { doubleSided: true } : {}),