@forgeax/engine-gltf 0.1.7 → 0.1.20

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 (39) hide show
  1. package/README.md +9 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/transmission-pack-carrier.integration.test.d.ts +2 -0
  4. package/dist/__tests__/transmission-pack-carrier.integration.test.d.ts.map +1 -0
  5. package/dist/bridge.d.ts.map +1 -1
  6. package/dist/check-extensions.d.ts +3 -3
  7. package/dist/check-extensions.d.ts.map +1 -1
  8. package/dist/cli-gltf.mjs +161 -69
  9. package/dist/cli-gltf.mjs.map +1 -1
  10. package/dist/errors.d.ts +7 -0
  11. package/dist/errors.d.ts.map +1 -1
  12. package/dist/gltf-importer.d.ts.map +1 -1
  13. package/dist/importer-entry.mjs +177 -72
  14. package/dist/importer-entry.mjs.map +1 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.mjs +177 -72
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/material/parse-material.d.ts +118 -0
  20. package/dist/material/parse-material.d.ts.map +1 -0
  21. package/dist/node-file-entry.mjs +161 -69
  22. package/dist/node-file-entry.mjs.map +1 -1
  23. package/dist/parse-gltf.d.ts +17 -103
  24. package/dist/parse-gltf.d.ts.map +1 -1
  25. package/package.json +12 -12
  26. package/src/__tests__/bridge-material-values.unit.test.ts +46 -0
  27. package/src/__tests__/gltf-error-derived-views.test-d.ts +4 -0
  28. package/src/__tests__/gltf-error-policy-owner.unit.test.ts +26 -178
  29. package/src/__tests__/gltf.unit.test.ts +34 -13
  30. package/src/__tests__/material-pack-refs.integration.test.ts +12 -0
  31. package/src/__tests__/parse-gltf.unit.test.ts +121 -0
  32. package/src/__tests__/transmission-pack-carrier.integration.test.ts +82 -0
  33. package/src/bridge.ts +12 -1
  34. package/src/check-extensions.ts +14 -9
  35. package/src/errors.ts +12 -0
  36. package/src/gltf-importer.ts +6 -0
  37. package/src/index.ts +1 -0
  38. package/src/material/parse-material.ts +342 -0
  39. package/src/parse-gltf.ts +50 -277
@@ -278,6 +278,7 @@ function unwrapReimport(result: ReturnType<typeof reimportReuseMeta>) {
278
278
  'gltf-version-unsupported',
279
279
  'gltf-buffer-out-of-bounds',
280
280
  'gltf-extension-unsupported',
281
+ 'gltf-material-transmission-invalid',
281
282
  'gltf-accessor-type-mismatch',
282
283
  'gltf-texture-load-failed',
283
284
  'gltf-meta-missing',
@@ -308,6 +309,8 @@ function unwrapReimport(result: ReturnType<typeof reimportReuseMeta>) {
308
309
  return 'oob';
309
310
  case 'gltf-extension-unsupported':
310
311
  return 'ext';
312
+ case 'gltf-material-transmission-invalid':
313
+ return 'material-transmission-invalid';
311
314
  case 'gltf-accessor-type-mismatch':
312
315
  return 'accessor';
313
316
  case 'gltf-texture-load-failed':
@@ -357,6 +360,13 @@ function unwrapReimport(result: ReturnType<typeof reimportReuseMeta>) {
357
360
  return gltfErr(code, { accessor: 0, byteOffset: 0, byteLength: 0, bufferIndex: 0 });
358
361
  case 'gltf-extension-unsupported':
359
362
  return gltfErr(code, { extension: 'KHR_x', source: 'extensionsRequired' });
363
+ case 'gltf-material-transmission-invalid':
364
+ return gltfErr(code, {
365
+ extension: 'KHR_materials_transmission',
366
+ field: 'transmissionFactor',
367
+ reason: 'range',
368
+ actual: 2,
369
+ });
360
370
  case 'gltf-accessor-type-mismatch':
361
371
  return gltfErr(code, { accessorIndex: 0, reason: 'sparse' });
362
372
  case 'gltf-texture-load-failed':
@@ -446,8 +456,8 @@ function unwrapReimport(result: ReturnType<typeof reimportReuseMeta>) {
446
456
  });
447
457
 
448
458
  describe('GltfErrorCode roster', () => {
449
- it('GLTF_ERROR_HINTS exposes exactly 22 keys', () => {
450
- expect(Object.keys(GLTF_ERROR_HINTS).length).toBe(22);
459
+ it('GLTF_ERROR_HINTS exposes exactly 23 keys', () => {
460
+ expect(Object.keys(GLTF_ERROR_HINTS).length).toBe(23);
451
461
  });
452
462
 
453
463
  it.each(ALL_CODES)('hint for %s is a non-empty string', (code) => {
@@ -1219,12 +1229,11 @@ function unwrapReimport(result: ReturnType<typeof reimportReuseMeta>) {
1219
1229
 
1220
1230
  const MOCK_TEXTURE: TextureAsset = {
1221
1231
  kind: 'texture',
1222
- width: 1,
1223
- height: 1,
1232
+ shape: { viewDimension: '2d', extent: { width: 1, height: 1 } },
1224
1233
  format: 'rgba8unorm-srgb',
1225
1234
  data: new Uint8Array([0, 0, 0, 0]),
1226
1235
  colorSpace: 'srgb',
1227
- mipmap: true,
1236
+ mips: { kind: 'generate' },
1228
1237
  };
1229
1238
 
1230
1239
  function buildSelfContainedGltfWithDataUriImage(): Uint8Array {
@@ -1371,12 +1380,11 @@ function unwrapReimport(result: ReturnType<typeof reimportReuseMeta>) {
1371
1380
 
1372
1381
  const EXT_MOCK_TEXTURE: TextureAsset = {
1373
1382
  kind: 'texture',
1374
- width: 1,
1375
- height: 1,
1383
+ shape: { viewDimension: '2d', extent: { width: 1, height: 1 } },
1376
1384
  format: 'rgba8unorm-srgb',
1377
1385
  data: new Uint8Array([200, 100, 50, 255]),
1378
1386
  colorSpace: 'srgb',
1379
- mipmap: true,
1387
+ mips: { kind: 'generate' },
1380
1388
  };
1381
1389
 
1382
1390
  interface SiblingCall {
@@ -1481,12 +1489,11 @@ function unwrapReimport(result: ReturnType<typeof reimportReuseMeta>) {
1481
1489
 
1482
1490
  const GLB_MOCK_TEXTURE: TextureAsset = {
1483
1491
  kind: 'texture',
1484
- width: 1,
1485
- height: 1,
1492
+ shape: { viewDimension: '2d', extent: { width: 1, height: 1 } },
1486
1493
  format: 'rgba8unorm-srgb',
1487
1494
  data: new Uint8Array([255, 128, 64, 255]),
1488
1495
  colorSpace: 'srgb',
1489
- mipmap: true,
1496
+ mips: { kind: 'generate' },
1490
1497
  };
1491
1498
 
1492
1499
  interface GlbDecodeCall {
@@ -2016,7 +2023,7 @@ function unwrapReimport(result: ReturnType<typeof reimportReuseMeta>) {
2016
2023
  expect(EXTENSION_ALLOWLIST).toEqual(['EXT_mesh_gpu_instancing', 'EXT_meshopt_compression']);
2017
2024
  });
2018
2025
 
2019
- it('(a) rejects extensionsRequired entries outside the allowlist', () => {
2026
+ it('(a) rejects unsupported extensionsRequired entries', () => {
2020
2027
  const result = checkExtensions({
2021
2028
  extensionsRequired: ['KHR_materials_pbrSpecularGlossiness'],
2022
2029
  });
@@ -2029,7 +2036,9 @@ function unwrapReimport(result: ReturnType<typeof reimportReuseMeta>) {
2029
2036
  });
2030
2037
 
2031
2038
  it('(b) accepts extensionsUsed (not required) into diagnostics list with no stderr', () => {
2032
- const result = checkExtensions({ extensionsUsed: ['KHR_materials_pbrSpecularGlossiness'] });
2039
+ const result = checkExtensions({
2040
+ extensionsUsed: ['KHR_materials_pbrSpecularGlossiness', 'KHR_materials_transmission'],
2041
+ });
2033
2042
  expect(result.ok).toBe(true);
2034
2043
  if (!result.ok) return;
2035
2044
  expect(result.value.unsupportedUsed).toEqual(['KHR_materials_pbrSpecularGlossiness']);
@@ -2073,6 +2082,18 @@ function unwrapReimport(result: ReturnType<typeof reimportReuseMeta>) {
2073
2082
  expect(stderrSpy).not.toHaveBeenCalled();
2074
2083
  });
2075
2084
 
2085
+ it.each([
2086
+ 'KHR_materials_transmission',
2087
+ 'KHR_materials_ior',
2088
+ 'KHR_materials_volume',
2089
+ ])('%s in extensionsRequired routes ok (supported)', (extension) => {
2090
+ const result = checkExtensions({ extensionsRequired: [extension] });
2091
+ expect(result.ok).toBe(true);
2092
+ if (!result.ok) return;
2093
+ expect(result.value.unsupportedUsed).toEqual([]);
2094
+ expect(stderrSpy).not.toHaveBeenCalled();
2095
+ });
2096
+
2076
2097
  it('EXT_mesh_gpu_instancing in extensionsUsed (not required) routes ok with no warn', () => {
2077
2098
  const result = checkExtensions({ extensionsUsed: ['EXT_mesh_gpu_instancing'] });
2078
2099
  expect(result.ok).toBe(true);
@@ -14,6 +14,8 @@ describe('glTF material Pack refs', () => {
14
14
  normalTexture: { texture: 2, sampler: 2 },
15
15
  occlusionTexture: { texture: 3, sampler: 3 },
16
16
  emissiveTexture: { texture: 4, sampler: 4 },
17
+ transmissionTexture: { texture: 5, sampler: 5 },
18
+ thicknessTexture: { texture: 6, sampler: 6 },
17
19
  } as unknown as GltfMaterialIr;
18
20
  const doc = {
19
21
  textures: [
@@ -22,6 +24,8 @@ describe('glTF material Pack refs', () => {
22
24
  { source: 2, sampler: 2 },
23
25
  { source: 3, sampler: 3 },
24
26
  { source: 4, sampler: 4 },
27
+ { source: 5, sampler: 5 },
28
+ { source: 6, sampler: 6 },
25
29
  ],
26
30
  } as unknown as GltfDoc;
27
31
  const refs = materialRefsForPack(
@@ -33,6 +37,8 @@ describe('glTF material Pack refs', () => {
33
37
  [2, 'texture-2'],
34
38
  [3, 'texture-3'],
35
39
  [4, 'texture-4'],
40
+ [5, 'texture-5'],
41
+ [6, 'texture-6'],
36
42
  ]),
37
43
  new Map([
38
44
  [0, 'sampler-0'],
@@ -40,6 +46,8 @@ describe('glTF material Pack refs', () => {
40
46
  [2, 'sampler-2'],
41
47
  [3, 'sampler-3'],
42
48
  [4, 'sampler-4'],
49
+ [5, 'sampler-5'],
50
+ [6, 'sampler-6'],
43
51
  ]),
44
52
  );
45
53
  expect(refs.map((ref) => ref.guid)).toEqual([
@@ -53,6 +61,10 @@ describe('glTF material Pack refs', () => {
53
61
  'sampler-3',
54
62
  'texture-4',
55
63
  'sampler-4',
64
+ 'texture-5',
65
+ 'sampler-5',
66
+ 'texture-6',
67
+ 'sampler-6',
56
68
  ]);
57
69
  });
58
70
  });
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { describe, expect, it } from 'vitest';
3
3
  import { dataUriBase64Payload, decodeBase64 } from '../data-uri.js';
4
+ import { parseMaterial as parseMaterialIr } from '../material/parse-material.js';
4
5
  import { parseGltf } from '../parse-gltf.js';
5
6
 
6
7
  const noopLoader = async (_uri: string) => new ArrayBuffer(0);
@@ -41,6 +42,126 @@ describe('glTF MASK alpha cutoff parsing', () => {
41
42
  });
42
43
  });
43
44
 
45
+ describe('glTF transmission material IR', () => {
46
+ it('rejects BLEND materials with effective transmission as a structured error', () => {
47
+ const result = parseMaterialIr(
48
+ {
49
+ alphaMode: 'BLEND',
50
+ extensions: { KHR_materials_transmission: { transmissionFactor: 1 } },
51
+ },
52
+ [],
53
+ );
54
+
55
+ expect(result.ok).toBe(false);
56
+ if (result.ok) return;
57
+ expect(result.error.code).toBe('gltf-material-transmission-invalid');
58
+ expect(result.error.detail).toEqual({
59
+ extension: 'KHR_materials_transmission',
60
+ field: 'alphaMode',
61
+ reason: 'blend',
62
+ actual: 'BLEND',
63
+ });
64
+ });
65
+
66
+ it('projects transmission, IOR, volume, channel, and texture transform facts', async () => {
67
+ const result = await parseGltf(
68
+ {
69
+ asset: { version: '2.0' },
70
+ extensionsUsed: ['KHR_materials_transmission', 'KHR_materials_ior', 'KHR_materials_volume'],
71
+ materials: [
72
+ {
73
+ pbrMetallicRoughness: {
74
+ baseColorTexture: {
75
+ index: 0,
76
+ texCoord: 1,
77
+ extensions: {
78
+ KHR_texture_transform: {
79
+ offset: [0.25, 0.5],
80
+ rotation: 0.25,
81
+ scale: [2, 3],
82
+ },
83
+ },
84
+ },
85
+ },
86
+ extensions: {
87
+ KHR_materials_transmission: {
88
+ transmissionFactor: 0.75,
89
+ transmissionTexture: { index: 1, texCoord: 2 },
90
+ },
91
+ KHR_materials_ior: { ior: 1.33 },
92
+ KHR_materials_volume: {
93
+ thicknessFactor: 0.04,
94
+ thicknessTexture: { index: 2, texCoord: 3 },
95
+ attenuationColor: [0.2, 0.4, 0.8],
96
+ attenuationDistance: Number.POSITIVE_INFINITY,
97
+ },
98
+ },
99
+ },
100
+ ],
101
+ textures: [
102
+ { sampler: 7, source: 0 },
103
+ { sampler: 8, source: 1 },
104
+ { sampler: 9, source: 2 },
105
+ ],
106
+ },
107
+ noopLoader,
108
+ '/material-transmission.gltf',
109
+ );
110
+
111
+ expect(result.ok).toBe(true);
112
+ if (!result.ok) return;
113
+ expect(result.value.materials[0]).toMatchObject({
114
+ transmissionFactor: 0.75,
115
+ transmissionTexture: { texture: 1, sampler: 8, texCoord: 2 },
116
+ ior: 1.33,
117
+ thicknessFactor: 0.04,
118
+ thicknessTexture: { texture: 2, sampler: 9, texCoord: 3 },
119
+ attenuationColor: [0.2, 0.4, 0.8],
120
+ });
121
+ expect(result.value.materials[0]).not.toHaveProperty('attenuationDistance');
122
+ expect(result.value.materials[0]?.baseColorTexture).toMatchObject({
123
+ transform: { offset: [0.25, 0.5], rotation: 0.25, scale: [2, 3] },
124
+ });
125
+ });
126
+
127
+ it('rejects an IOR at the non-positive boundary', async () => {
128
+ const result = await parseGltf(
129
+ {
130
+ asset: { version: '2.0' },
131
+ extensionsUsed: ['KHR_materials_ior'],
132
+ materials: [{ extensions: { KHR_materials_ior: { ior: 0 } } }],
133
+ },
134
+ noopLoader,
135
+ '/material-invalid-ior.gltf',
136
+ );
137
+
138
+ expect(result.ok).toBe(false);
139
+ if (result.ok) return;
140
+ expect(result.error.code).toBe('gltf-material-transmission-invalid');
141
+ expect(result.error.detail).toMatchObject({ extension: 'KHR_materials_ior', field: 'ior' });
142
+ });
143
+
144
+ it('accepts all transmission material required extensions through the parse path', async () => {
145
+ const result = await parseGltf(
146
+ {
147
+ asset: { version: '2.0' },
148
+ extensionsUsed: ['KHR_materials_transmission', 'KHR_materials_ior', 'KHR_materials_volume'],
149
+ extensionsRequired: [
150
+ 'KHR_materials_transmission',
151
+ 'KHR_materials_ior',
152
+ 'KHR_materials_volume',
153
+ ],
154
+ },
155
+ noopLoader,
156
+ '/material-required-transmission.gltf',
157
+ );
158
+
159
+ expect(result.ok).toBe(true);
160
+ if (!result.ok) return;
161
+ expect(result.value.diagnostics.unsupportedExtensions).toEqual([]);
162
+ });
163
+ });
164
+
44
165
  describe('glTF COLOR_0 importer carrier', () => {
45
166
  it('publishes normalized VEC3 FLOAT colors as importer-owned RGBA', async () => {
46
167
  const fixture = JSON.parse(
@@ -0,0 +1,82 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { toMaterialAsset } from '../bridge.js';
3
+ import { parseGltf, toAssetPack } from '../parse-gltf.js';
4
+
5
+ const noopLoader = async (_uri: string) => new ArrayBuffer(0);
6
+
7
+ describe('glTF transmission carrier route', () => {
8
+ it('keeps required transmission extensions through parse, pack identity, and Standard bridge', async () => {
9
+ const source = {
10
+ asset: { version: '2.0' },
11
+ extensionsRequired: [
12
+ 'KHR_materials_transmission',
13
+ 'KHR_materials_ior',
14
+ 'KHR_materials_volume',
15
+ ],
16
+ extensionsUsed: ['KHR_materials_transmission', 'KHR_materials_ior', 'KHR_materials_volume'],
17
+ materials: [
18
+ {
19
+ name: 'TransmissionSphere',
20
+ pbrMetallicRoughness: {
21
+ baseColorFactor: [0.76, 0.9, 1, 1],
22
+ metallicFactor: 0,
23
+ roughnessFactor: 0.06,
24
+ },
25
+ extensions: {
26
+ KHR_materials_transmission: { transmissionFactor: 1 },
27
+ KHR_materials_ior: { ior: 1.45 },
28
+ KHR_materials_volume: {
29
+ thicknessFactor: 0.12,
30
+ attenuationColor: [0.9, 0.97, 1],
31
+ attenuationDistance: 3,
32
+ },
33
+ },
34
+ },
35
+ ],
36
+ };
37
+
38
+ const parsed = await parseGltf(source, noopLoader, 'carrier/transmission.gltf');
39
+ expect(parsed.ok).toBe(true);
40
+ if (!parsed.ok) return;
41
+
42
+ const material = parsed.value.materials[0];
43
+ if (material === undefined) throw new Error('transmission carrier did not parse a material');
44
+ expect(material).toMatchObject({
45
+ name: 'TransmissionSphere',
46
+ transmissionFactor: 1,
47
+ ior: 1.45,
48
+ thicknessFactor: 0.12,
49
+ attenuationColor: [0.9, 0.97, 1],
50
+ attenuationDistance: 3,
51
+ });
52
+
53
+ const pack = toAssetPack(parsed.value, undefined, 'transmission.gltf');
54
+ expect(pack.ok).toBe(true);
55
+ if (!pack.ok) return;
56
+ expect(pack.value.subAssets).toEqual([
57
+ expect.objectContaining({
58
+ kind: 'material',
59
+ name: 'TransmissionSphere',
60
+ sourceKey: 'material:TransmissionSphere',
61
+ }),
62
+ ]);
63
+
64
+ const bridged = toMaterialAsset(material);
65
+ const pass = bridged.passes?.[0];
66
+ if (pass === undefined) throw new Error('transmission bridge did not emit a Forward pass');
67
+ if (pass.renderState === undefined)
68
+ throw new Error('transmission bridge pass has no render state');
69
+ expect(pass.program.module).toBe('forgeax::default-standard-pbr');
70
+ expect(pass.renderState.queue).toBe(2000);
71
+ expect(bridged.values).toMatchObject({
72
+ baseColor: [0.76, 0.9, 1, 1],
73
+ metallic: 0,
74
+ roughness: 0.06,
75
+ transmission: 1,
76
+ ior: 1.45,
77
+ thickness: 0.12,
78
+ attenuationColor: [0.9, 0.97, 1],
79
+ attenuationDistance: 3,
80
+ });
81
+ });
82
+ });
package/src/bridge.ts CHANGED
@@ -776,7 +776,9 @@ type MaterialTextureSlot =
776
776
  | 'metallicRoughnessTexture'
777
777
  | 'normalTexture'
778
778
  | 'occlusionTexture'
779
- | 'emissiveTexture';
779
+ | 'emissiveTexture'
780
+ | 'transmissionTexture'
781
+ | 'thicknessTexture';
780
782
 
781
783
  function textureValue(
782
784
  info: GltfTextureInfoIr | number | undefined,
@@ -830,6 +832,8 @@ export function validateMaterialUvSets(
830
832
  ['normalTexture', mat.normalTexture],
831
833
  ['occlusionTexture', mat.occlusionTexture],
832
834
  ['emissiveTexture', mat.emissiveTexture],
835
+ ['transmissionTexture', mat.transmissionTexture],
836
+ ['thicknessTexture', mat.thicknessTexture],
833
837
  ];
834
838
  for (const [slot, rawBinding] of slots) {
835
839
  const binding = textureInfo(rawBinding);
@@ -867,6 +871,8 @@ export function toMaterialAsset(mat: GltfMaterialIr, ctx?: MaterialBridgeContext
867
871
  ['normalTexture', mat.normalTexture],
868
872
  ['occlusionTexture', mat.occlusionTexture],
869
873
  ['emissiveTexture', mat.emissiveTexture],
874
+ ['transmissionTexture', mat.transmissionTexture],
875
+ ['thicknessTexture', mat.thicknessTexture],
870
876
  ];
871
877
  for (const [slot, info] of textureSlots) {
872
878
  const value = textureValue(info, slot, ctx);
@@ -875,6 +881,11 @@ export function toMaterialAsset(mat: GltfMaterialIr, ctx?: MaterialBridgeContext
875
881
  if (mat.occlusionTexture !== undefined && values.occlusionStrength === undefined) {
876
882
  values.occlusionStrength = 1;
877
883
  }
884
+ if (mat.transmissionFactor !== undefined) values.transmission = mat.transmissionFactor;
885
+ if (mat.ior !== undefined) values.ior = mat.ior;
886
+ if (mat.thicknessFactor !== undefined) values.thickness = mat.thicknessFactor;
887
+ if (mat.attenuationColor !== undefined) values.attenuationColor = mat.attenuationColor;
888
+ if (mat.attenuationDistance !== undefined) values.attenuationDistance = mat.attenuationDistance;
878
889
 
879
890
  const module = ctx?.skinned === true ? 'forgeax::pbr-skin' : 'forgeax::default-standard-pbr';
880
891
 
@@ -1,13 +1,15 @@
1
1
  // check-extensions.ts - KHR / vendor extension gate.
2
2
  //
3
- // v1 required-extension support contains EXT_mesh_gpu_instancing,
4
- // KHR_lights_punctual, and KHR_texture_transform. The exported legacy list remains the original mesh
5
- // extension list for callers that display the v1 mesh-only surface.
3
+ // Required-extension support contains EXT_mesh_gpu_instancing,
4
+ // EXT_meshopt_compression, KHR_lights_punctual, KHR_texture_transform, and
5
+ // the KHR material transmission/IOR/volume extensions. The exported legacy
6
+ // list remains the original mesh extension list for callers that display the
7
+ // v1 mesh-only surface.
6
8
  // (feat-20260518-gltf-instancing-and-name-component plan-strategy section
7
9
  // 2 D-1 / D-3). Any extension listed in `extensionsRequired[]` outside
8
- // this allowlist triggers `gltf-extension-unsupported` (hard fail).
10
+ // the supported extension set triggers `gltf-extension-unsupported` (hard fail).
9
11
  // Extensions listed in `extensionsUsed[]` (but not required and not in
10
- // allowlist) are recorded in `importSettings.diagnostics
12
+ // supported extension set) are recorded in `importSettings.diagnostics
11
13
  // .unsupportedExtensions` so AI users can observe them downstream — and
12
14
  // nothing else: `extensionsUsed` is purely informational per the glTF spec
13
15
  // (only `extensionsRequired` is binding), and exporters routinely
@@ -16,12 +18,12 @@
16
18
  // diagnostics list is the single channel (no `console.error`).
17
19
  //
18
20
  // Future expansion (KHR_materials_unlit, ...) extends
19
- // `EXTENSION_ALLOWLIST` in place; each addition lands under its own feat-*
21
+ // `SUPPORTED_EXTENSIONS` in place; each addition lands under its own feat-*
20
22
  // loop with breaking-change registry entry.
21
23
 
22
24
  import { err, type GltfError, gltfErr, ok, type Result } from './errors.js';
23
25
 
24
- /** Hard-coded v1 allowlist (plan-strategy decision section 2 D-1 / D-3). */
26
+ /** Legacy v1 mesh-only list kept for callers that display that surface. */
25
27
  export const EXTENSION_ALLOWLIST: readonly string[] = [
26
28
  'EXT_mesh_gpu_instancing',
27
29
  'EXT_meshopt_compression',
@@ -30,10 +32,13 @@ const SUPPORTED_EXTENSIONS: readonly string[] = [
30
32
  ...EXTENSION_ALLOWLIST,
31
33
  'KHR_lights_punctual',
32
34
  'KHR_texture_transform',
35
+ 'KHR_materials_transmission',
36
+ 'KHR_materials_ior',
37
+ 'KHR_materials_volume',
33
38
  ];
34
39
 
35
40
  export interface ExtensionsCheckResult {
36
- /** Names listed in extensionsUsed but not in the allowlist. */
41
+ /** Names listed in extensionsUsed but not in the supported extension set. */
37
42
  readonly unsupportedUsed: readonly string[];
38
43
  }
39
44
 
@@ -43,7 +48,7 @@ export interface GltfExtensionsJson {
43
48
  }
44
49
 
45
50
  /**
46
- * Validate the glTF JSON's extension declarations against the v1 allowlist.
51
+ * Validate the glTF JSON's extension declarations against the supported set.
47
52
  *
48
53
  * Returns:
49
54
  * - `Result.err(gltf-extension-unsupported)` for the FIRST entry of
package/src/errors.ts CHANGED
@@ -60,6 +60,13 @@ export interface GltfExtensionUnsupportedDetail {
60
60
  readonly source: 'extensionsRequired' | 'extensionsUsed';
61
61
  }
62
62
 
63
+ export interface GltfMaterialTransmissionInvalidDetail {
64
+ readonly extension: 'KHR_materials_transmission' | 'KHR_materials_ior' | 'KHR_materials_volume';
65
+ readonly field: string;
66
+ readonly reason: 'type' | 'range' | 'non-finite' | 'blend';
67
+ readonly actual?: unknown;
68
+ }
69
+
63
70
  /** `gltf-accessor-type-mismatch` payload: 4-member closed reason discriminator. */
64
71
  export interface GltfAccessorTypeMismatchDetail {
65
72
  readonly accessorIndex: number;
@@ -256,6 +263,10 @@ const gltfErrorPolicy = {
256
263
  expected: 'extension listed in v1 allowlist (see EXTENSION_ALLOWLIST in @forgeax/engine-gltf)',
257
264
  hint: 'see feat-future-gltf-extensions-allowlist; remove this extension or wait for the allowlist to expand',
258
265
  },
266
+ 'gltf-material-transmission-invalid': {
267
+ expected: 'KHR transmission, IOR, and volume values are finite and within their glTF ranges',
268
+ hint: 'repair the named glTF material extension value and re-import the source',
269
+ },
259
270
  'gltf-accessor-type-mismatch': {
260
271
  expected: 'dense fixed-stride accessor with supported componentType',
261
272
  hint: 'sparse: see feat-future-gltf-sparse-accessor; morph: see feat-future-gltf-morph; interleaved: see feat-future-gltf-mesh-multi-section',
@@ -364,6 +375,7 @@ interface DetailFor {
364
375
  readonly 'gltf-color-accessor-unsupported': GltfColorAccessorUnsupportedDetail;
365
376
  readonly 'gltf-color-accessor-malformed': GltfColorAccessorMalformedDetail;
366
377
  readonly 'gltf-mesh-bridge-invalid': GltfMeshBridgeInvalidDetail;
378
+ readonly 'gltf-material-transmission-invalid': GltfMaterialTransmissionInvalidDetail;
367
379
  }
368
380
 
369
381
  /**
@@ -412,6 +412,8 @@ export function materialRefsForPack(
412
412
  pushRefsForSlot(mat.normalTexture, 'normalTexture');
413
413
  pushRefsForSlot(mat.occlusionTexture, 'occlusionTexture');
414
414
  pushRefsForSlot(mat.emissiveTexture, 'emissiveTexture');
415
+ pushRefsForSlot(mat.transmissionTexture, 'transmissionTexture');
416
+ pushRefsForSlot(mat.thicknessTexture, 'thicknessTexture');
415
417
  return refs;
416
418
  }
417
419
 
@@ -439,6 +441,8 @@ function rewriteMaterialAssetRefs(
439
441
  | 'normalTexture'
440
442
  | 'occlusionTexture'
441
443
  | 'emissiveTexture'
444
+ | 'transmissionTexture'
445
+ | 'thicknessTexture'
442
446
  ),
443
447
  GltfTextureInfoIr | number | undefined,
444
448
  ][] = [
@@ -447,6 +451,8 @@ function rewriteMaterialAssetRefs(
447
451
  ['normalTexture', mat.normalTexture],
448
452
  ['occlusionTexture', mat.occlusionTexture],
449
453
  ['emissiveTexture', mat.emissiveTexture],
454
+ ['transmissionTexture', mat.transmissionTexture],
455
+ ['thicknessTexture', mat.thicknessTexture],
450
456
  ];
451
457
  let cursor = 0;
452
458
  for (const [slot, rawBinding] of slots) {
package/src/index.ts CHANGED
@@ -65,6 +65,7 @@ export type {
65
65
  GltfImageMimeUnsupportedDetail,
66
66
  GltfInstancingCountMismatchDetail,
67
67
  GltfMalformedHeaderDetail,
68
+ GltfMaterialTransmissionInvalidDetail,
68
69
  GltfMetaMissingDetail,
69
70
  GltfMorphInvalidDetail,
70
71
  GltfMorphUnsupportedDetail,