@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.
- package/README.md +9 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/transmission-pack-carrier.integration.test.d.ts +2 -0
- package/dist/__tests__/transmission-pack-carrier.integration.test.d.ts.map +1 -0
- package/dist/bridge.d.ts.map +1 -1
- package/dist/check-extensions.d.ts +3 -3
- package/dist/check-extensions.d.ts.map +1 -1
- package/dist/cli-gltf.mjs +161 -69
- package/dist/cli-gltf.mjs.map +1 -1
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/gltf-importer.d.ts.map +1 -1
- package/dist/importer-entry.mjs +177 -72
- package/dist/importer-entry.mjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +177 -72
- package/dist/index.mjs.map +1 -1
- package/dist/material/parse-material.d.ts +118 -0
- package/dist/material/parse-material.d.ts.map +1 -0
- package/dist/node-file-entry.mjs +161 -69
- package/dist/node-file-entry.mjs.map +1 -1
- package/dist/parse-gltf.d.ts +17 -103
- package/dist/parse-gltf.d.ts.map +1 -1
- package/package.json +12 -12
- package/src/__tests__/bridge-material-values.unit.test.ts +46 -0
- package/src/__tests__/gltf-error-derived-views.test-d.ts +4 -0
- package/src/__tests__/gltf-error-policy-owner.unit.test.ts +26 -178
- package/src/__tests__/gltf.unit.test.ts +34 -13
- package/src/__tests__/material-pack-refs.integration.test.ts +12 -0
- package/src/__tests__/parse-gltf.unit.test.ts +121 -0
- package/src/__tests__/transmission-pack-carrier.integration.test.ts +82 -0
- package/src/bridge.ts +12 -1
- package/src/check-extensions.ts +14 -9
- package/src/errors.ts +12 -0
- package/src/gltf-importer.ts +6 -0
- package/src/index.ts +1 -0
- package/src/material/parse-material.ts +342 -0
- 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
|
|
450
|
-
expect(Object.keys(GLTF_ERROR_HINTS).length).toBe(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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({
|
|
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
|
|
package/src/check-extensions.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
// check-extensions.ts - KHR / vendor extension gate.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// KHR_lights_punctual,
|
|
5
|
-
//
|
|
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
|
-
//
|
|
10
|
+
// the supported extension set triggers `gltf-extension-unsupported` (hard fail).
|
|
9
11
|
// Extensions listed in `extensionsUsed[]` (but not required and not in
|
|
10
|
-
//
|
|
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
|
-
// `
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
/**
|
package/src/gltf-importer.ts
CHANGED
|
@@ -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