@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.
- package/README.md +56 -3
- 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 +181 -8
- 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/image-color-space.d.ts +12 -0
- package/dist/image-color-space.d.ts.map +1 -1
- package/dist/importer-entry.mjs +377 -26
- package/dist/importer-entry.mjs.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +378 -27
- package/dist/index.mjs.map +1 -1
- 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 +181 -8
- package/dist/node-file-entry.mjs.map +1 -1
- package/package.json +12 -12
- package/src/__tests__/bridge-material-values.unit.test.ts +107 -1
- package/src/__tests__/bridge-skin-stride.unit.test.ts +22 -0
- package/src/__tests__/gltf-error-derived-views.test-d.ts +4 -0
- package/src/__tests__/gltf-error-migration.test.ts +3 -3
- package/src/__tests__/gltf.unit.test.ts +61 -6
- package/src/__tests__/material-pack-refs.integration.test.ts +53 -0
- package/src/__tests__/parse-gltf.unit.test.ts +29 -1
- package/src/__tests__/physical-clearcoat.unit.test.ts +110 -0
- package/src/__tests__/physical-material-import.integration.test.ts +239 -0
- package/src/bridge.ts +260 -15
- package/src/check-extensions.ts +16 -10
- package/src/errors.ts +21 -2
- package/src/gltf-importer.ts +35 -0
- package/src/image-color-space.ts +29 -4
- package/src/index.ts +8 -1
- package/src/material/parse-material.ts +307 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgeax/engine-gltf",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.25",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -38,21 +38,21 @@
|
|
|
38
38
|
"LICENSE"
|
|
39
39
|
],
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@forgeax/engine-animation": "0.1.
|
|
42
|
-
"@forgeax/engine-geometry": "0.1.
|
|
43
|
-
"@forgeax/engine-import": "0.1.
|
|
44
|
-
"@forgeax/engine-math": "0.1.
|
|
45
|
-
"@forgeax/engine-pack": "0.1.
|
|
46
|
-
"@forgeax/engine-scene": "0.1.
|
|
47
|
-
"@forgeax/engine-types": "0.1.
|
|
41
|
+
"@forgeax/engine-animation": "0.1.25",
|
|
42
|
+
"@forgeax/engine-geometry": "0.1.25",
|
|
43
|
+
"@forgeax/engine-import": "0.1.25",
|
|
44
|
+
"@forgeax/engine-math": "0.1.25",
|
|
45
|
+
"@forgeax/engine-pack": "0.1.25",
|
|
46
|
+
"@forgeax/engine-scene": "0.1.25",
|
|
47
|
+
"@forgeax/engine-types": "0.1.25",
|
|
48
48
|
"@webgpu/types": "^0.1.71",
|
|
49
49
|
"meshoptimizer": "^0.20.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@forgeax/engine-assets-runtime": "0.1.
|
|
53
|
-
"@forgeax/engine-picking": "0.1.
|
|
54
|
-
"@forgeax/engine-render": "0.1.
|
|
55
|
-
"@forgeax/engine-runtime": "0.1.
|
|
52
|
+
"@forgeax/engine-assets-runtime": "0.1.25",
|
|
53
|
+
"@forgeax/engine-picking": "0.1.25",
|
|
54
|
+
"@forgeax/engine-render": "0.1.25",
|
|
55
|
+
"@forgeax/engine-runtime": "0.1.25",
|
|
56
56
|
"@types/node": "^20.14.0"
|
|
57
57
|
},
|
|
58
58
|
"forgeax": {
|
|
@@ -1,9 +1,32 @@
|
|
|
1
|
-
import type { Handle } from '@forgeax/engine-types';
|
|
1
|
+
import type { AssetGuid, Handle } from '@forgeax/engine-types';
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
3
|
import { toMaterialAsset } from '../bridge.js';
|
|
4
4
|
import type { GltfMaterialIr } from '../parse-gltf.js';
|
|
5
5
|
|
|
6
6
|
describe('glTF material bridge values', () => {
|
|
7
|
+
it('publishes a parent-bearing child with only inherited contract and authored values', () => {
|
|
8
|
+
const standardRootGuid = '01a065d4-47dd-789f-96b4-36b9326abc02' as unknown as AssetGuid;
|
|
9
|
+
const material = toMaterialAsset(
|
|
10
|
+
{
|
|
11
|
+
baseColorFactor: [0.5, 0.75, 0.25, 1],
|
|
12
|
+
metallicFactor: 0.5,
|
|
13
|
+
roughnessFactor: 0.7,
|
|
14
|
+
} as unknown as GltfMaterialIr,
|
|
15
|
+
{ standardRootGuid },
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
expect(material).toEqual({
|
|
19
|
+
kind: 'material',
|
|
20
|
+
parent: standardRootGuid,
|
|
21
|
+
values: {
|
|
22
|
+
baseColor: [0.5, 0.75, 0.25, 1],
|
|
23
|
+
metallic: 0.5,
|
|
24
|
+
roughness: 0.7,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
expect(Object.keys(material).sort()).toEqual(['kind', 'parent', 'values']);
|
|
28
|
+
});
|
|
29
|
+
|
|
7
30
|
it('preserves all four base-color channels for factor and texture composition', () => {
|
|
8
31
|
const factor = [0.5, 0.75, 0.25, 0.4] as const;
|
|
9
32
|
const material = toMaterialAsset({
|
|
@@ -149,4 +172,87 @@ describe('glTF material bridge values', () => {
|
|
|
149
172
|
},
|
|
150
173
|
});
|
|
151
174
|
});
|
|
175
|
+
|
|
176
|
+
it('owns a complete five-extension Standard root and admits only authored physical textures', () => {
|
|
177
|
+
const material = toMaterialAsset(
|
|
178
|
+
{
|
|
179
|
+
name: 'physical-root',
|
|
180
|
+
baseColorFactor: [0.8, 0.7, 0.6, 1],
|
|
181
|
+
metallicFactor: 0.1,
|
|
182
|
+
roughnessFactor: 0.35,
|
|
183
|
+
clearcoatFactor: 0.75,
|
|
184
|
+
clearcoatRoughnessFactor: 0.2,
|
|
185
|
+
clearcoatNormalTexture: { texture: 0 },
|
|
186
|
+
anisotropyStrength: 0.5,
|
|
187
|
+
anisotropyRotation: 0.25,
|
|
188
|
+
anisotropyTexture: { texture: 1 },
|
|
189
|
+
sheenColorFactor: [0.2, 0.1, 0.05],
|
|
190
|
+
sheenRoughnessFactor: 0.3,
|
|
191
|
+
sheenColorTexture: { texture: 2 },
|
|
192
|
+
iridescenceFactor: 0.6,
|
|
193
|
+
iridescenceIor: 1.4,
|
|
194
|
+
iridescenceThicknessMinimum: 120,
|
|
195
|
+
iridescenceThicknessMaximum: 380,
|
|
196
|
+
iridescenceTexture: { texture: 3 },
|
|
197
|
+
specularFactor: 0.8,
|
|
198
|
+
specularColorFactor: [0.9, 0.85, 0.75],
|
|
199
|
+
specularColorTexture: { texture: 4 },
|
|
200
|
+
} as unknown as GltfMaterialIr,
|
|
201
|
+
{
|
|
202
|
+
standardRootGuid: 'standard-root' as unknown as AssetGuid,
|
|
203
|
+
textureHandles: new Map([
|
|
204
|
+
[0, 100],
|
|
205
|
+
[1, 101],
|
|
206
|
+
[2, 102],
|
|
207
|
+
[3, 103],
|
|
208
|
+
[4, 104],
|
|
209
|
+
]) as unknown as ReadonlyMap<number, Handle<'TextureAsset', 'shared'>>,
|
|
210
|
+
},
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
expect(material.parent).toBeUndefined();
|
|
214
|
+
const names = new Set((material.parameters ?? []).map((parameter) => parameter.name));
|
|
215
|
+
for (const name of [
|
|
216
|
+
'clearcoat',
|
|
217
|
+
'clearcoatRoughness',
|
|
218
|
+
'clearcoatNormalTexture',
|
|
219
|
+
'anisotropyStrength',
|
|
220
|
+
'anisotropyRotation',
|
|
221
|
+
'anisotropyTexture',
|
|
222
|
+
'sheenColor',
|
|
223
|
+
'sheenRoughness',
|
|
224
|
+
'sheenColorTexture',
|
|
225
|
+
'iridescence',
|
|
226
|
+
'iridescenceIor',
|
|
227
|
+
'iridescenceThicknessMinimum',
|
|
228
|
+
'iridescenceThicknessMaximum',
|
|
229
|
+
'iridescenceTexture',
|
|
230
|
+
'specular',
|
|
231
|
+
'specularColor',
|
|
232
|
+
'specularColorTexture',
|
|
233
|
+
]) {
|
|
234
|
+
expect(names.has(name)).toBe(true);
|
|
235
|
+
}
|
|
236
|
+
for (const name of [
|
|
237
|
+
'clearcoatTexture',
|
|
238
|
+
'clearcoatRoughnessTexture',
|
|
239
|
+
'sheenRoughnessTexture',
|
|
240
|
+
'iridescenceThicknessTexture',
|
|
241
|
+
'specularTexture',
|
|
242
|
+
]) {
|
|
243
|
+
expect(names.has(name)).toBe(false);
|
|
244
|
+
}
|
|
245
|
+
expect(material.values).toMatchObject({
|
|
246
|
+
clearcoat: 0.75,
|
|
247
|
+
anisotropyStrength: 0.5,
|
|
248
|
+
sheenColor: [0.2, 0.1, 0.05],
|
|
249
|
+
iridescenceIor: 1.4,
|
|
250
|
+
specularColor: [0.9, 0.85, 0.75],
|
|
251
|
+
clearcoatNormalTexture: { texture: 100 },
|
|
252
|
+
anisotropyTexture: { texture: 101 },
|
|
253
|
+
sheenColorTexture: { texture: 102 },
|
|
254
|
+
iridescenceTexture: { texture: 103 },
|
|
255
|
+
specularColorTexture: { texture: 104 },
|
|
256
|
+
});
|
|
257
|
+
});
|
|
152
258
|
});
|
|
@@ -102,6 +102,28 @@ describe('meshIrToMeshAsset pure-unskinned path (AC-04 / AC-08 / OOS-9)', () =>
|
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
describe('glTF bridge uses geometry-owned packing', () => {
|
|
105
|
+
it('persists a generated tangent frame when source TANGENT is absent', () => {
|
|
106
|
+
const asset = meshIrToMeshAsset([
|
|
107
|
+
{
|
|
108
|
+
meshIndex: 0,
|
|
109
|
+
materialIndex: null,
|
|
110
|
+
positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
|
|
111
|
+
normals: new Float32Array([0, 0, 1, 0, 0, 1, 0, 0, 1]),
|
|
112
|
+
texcoord0: new Float32Array([0, 0, 1, 0, 0, 1]),
|
|
113
|
+
indices: new Uint16Array([0, 1, 2]),
|
|
114
|
+
},
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
const tangent = asset.attributes.tangent;
|
|
118
|
+
expect(tangent).toBeInstanceOf(Float32Array);
|
|
119
|
+
if (!(tangent instanceof Float32Array)) throw new Error('generated tangent stream missing');
|
|
120
|
+
expect(tangent?.length).toBe(12);
|
|
121
|
+
expect(tangent?.[0]).toBeCloseTo(1, 5);
|
|
122
|
+
expect(tangent?.[1]).toBeCloseTo(0, 5);
|
|
123
|
+
expect(tangent?.[2]).toBeCloseTo(0, 5);
|
|
124
|
+
expect(Math.abs(tangent?.[3] ?? 0)).toBeCloseTo(1, 5);
|
|
125
|
+
});
|
|
126
|
+
|
|
105
127
|
it('does not own a parallel 12F/18F stride or uv count calculation', () => {
|
|
106
128
|
const asset = meshIrToMeshAsset([makeUnskinnedPrim(0, 4)]);
|
|
107
129
|
expect(asset.vertices.length / 12).toBe(4);
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type GltfInstancingCountMismatchDetail,
|
|
16
16
|
type GltfLodInvalidDetail,
|
|
17
17
|
type GltfMalformedHeaderDetail,
|
|
18
|
+
type GltfMaterialPhysicalInvalidDetail,
|
|
18
19
|
type GltfMaterialTransmissionInvalidDetail,
|
|
19
20
|
type GltfMeshBridgeInvalidDetail,
|
|
20
21
|
type GltfMeshoptDecodeFailedDetail,
|
|
@@ -55,6 +56,7 @@ type ExpectedDetails = {
|
|
|
55
56
|
readonly 'gltf-mesh-bridge-invalid': GltfMeshBridgeInvalidDetail;
|
|
56
57
|
readonly 'gltf-lod-invalid': GltfLodInvalidDetail;
|
|
57
58
|
readonly 'gltf-material-transmission-invalid': GltfMaterialTransmissionInvalidDetail;
|
|
59
|
+
readonly 'gltf-material-physical-invalid': GltfMaterialPhysicalInvalidDetail;
|
|
58
60
|
};
|
|
59
61
|
|
|
60
62
|
type ExpectedCodes = keyof ExpectedDetails;
|
|
@@ -117,6 +119,8 @@ function exhaustive(error: GltfError): string {
|
|
|
117
119
|
return error.detail.reason;
|
|
118
120
|
case 'gltf-material-transmission-invalid':
|
|
119
121
|
return `${error.detail.extension}:${error.detail.field}:${error.detail.reason}`;
|
|
122
|
+
case 'gltf-material-physical-invalid':
|
|
123
|
+
return `${error.detail.extension}:${error.detail.field}:${error.detail.reason}`;
|
|
120
124
|
}
|
|
121
125
|
return error;
|
|
122
126
|
}
|
|
@@ -17,7 +17,7 @@ import { resolve } from 'node:path';
|
|
|
17
17
|
import { describe, expect, it } from 'vitest';
|
|
18
18
|
|
|
19
19
|
const REPO_ROOT = resolve(import.meta.dirname ?? '.', '..', '..', '..', '..');
|
|
20
|
-
const TYPES_INDEX = resolve(REPO_ROOT, 'packages', 'types', 'src', '
|
|
20
|
+
const TYPES_INDEX = resolve(REPO_ROOT, 'packages', 'types', 'src', 'core-contracts.ts');
|
|
21
21
|
const GLTF_ERRORS = resolve(REPO_ROOT, 'packages', 'gltf', 'src', 'errors.ts');
|
|
22
22
|
|
|
23
23
|
function grepCount(pattern: string, file: string): number {
|
|
@@ -125,12 +125,12 @@ function grepCountImportGltfErrorFromTypes(): number {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
describe('M1 GltfErrorCode migration grep gate (AC-28)', () => {
|
|
128
|
-
it("(a) types/src/
|
|
128
|
+
it("(a) types/src/core-contracts.ts contains zero 'gltf-' string literals", () => {
|
|
129
129
|
const count = grepCount("'gltf-", TYPES_INDEX);
|
|
130
130
|
expect(count).toBe(0);
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
-
it('(b) types/src/
|
|
133
|
+
it('(b) types/src/core-contracts.ts contains zero export type GltfError* exports', () => {
|
|
134
134
|
const count = grepCount('export type GltfError', TYPES_INDEX);
|
|
135
135
|
expect(count).toBe(0);
|
|
136
136
|
});
|
|
@@ -278,7 +278,9 @@ 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-lod-invalid',
|
|
281
282
|
'gltf-material-transmission-invalid',
|
|
283
|
+
'gltf-material-physical-invalid',
|
|
282
284
|
'gltf-accessor-type-mismatch',
|
|
283
285
|
'gltf-texture-load-failed',
|
|
284
286
|
'gltf-meta-missing',
|
|
@@ -300,7 +302,8 @@ function unwrapReimport(result: ReturnType<typeof reimportReuseMeta>) {
|
|
|
300
302
|
];
|
|
301
303
|
|
|
302
304
|
function classifyByExhaustiveSwitch(err: GltfError): string {
|
|
303
|
-
|
|
305
|
+
const code = err.code;
|
|
306
|
+
switch (code) {
|
|
304
307
|
case 'gltf-malformed-header':
|
|
305
308
|
return 'malformed';
|
|
306
309
|
case 'gltf-version-unsupported':
|
|
@@ -313,6 +316,8 @@ function unwrapReimport(result: ReturnType<typeof reimportReuseMeta>) {
|
|
|
313
316
|
return 'lod';
|
|
314
317
|
case 'gltf-material-transmission-invalid':
|
|
315
318
|
return 'material-transmission-invalid';
|
|
319
|
+
case 'gltf-material-physical-invalid':
|
|
320
|
+
return 'material-physical-invalid';
|
|
316
321
|
case 'gltf-accessor-type-mismatch':
|
|
317
322
|
return 'accessor';
|
|
318
323
|
case 'gltf-texture-load-failed':
|
|
@@ -350,6 +355,7 @@ function unwrapReimport(result: ReturnType<typeof reimportReuseMeta>) {
|
|
|
350
355
|
case 'gltf-mesh-bridge-invalid':
|
|
351
356
|
return 'mesh-bridge-invalid';
|
|
352
357
|
}
|
|
358
|
+
throw new Error(`unhandled glTF error code: ${code}`);
|
|
353
359
|
}
|
|
354
360
|
|
|
355
361
|
function buildErrSample(code: GltfErrorCode): GltfError {
|
|
@@ -371,6 +377,13 @@ function unwrapReimport(result: ReturnType<typeof reimportReuseMeta>) {
|
|
|
371
377
|
reason: 'range',
|
|
372
378
|
actual: 2,
|
|
373
379
|
});
|
|
380
|
+
case 'gltf-material-physical-invalid':
|
|
381
|
+
return gltfErr(code, {
|
|
382
|
+
extension: 'KHR_materials_clearcoat',
|
|
383
|
+
field: 'clearcoatFactor',
|
|
384
|
+
reason: 'range',
|
|
385
|
+
actual: 2,
|
|
386
|
+
});
|
|
374
387
|
case 'gltf-accessor-type-mismatch':
|
|
375
388
|
return gltfErr(code, { accessorIndex: 0, reason: 'sparse' });
|
|
376
389
|
case 'gltf-texture-load-failed':
|
|
@@ -452,6 +465,7 @@ function unwrapReimport(result: ReturnType<typeof reimportReuseMeta>) {
|
|
|
452
465
|
case 'gltf-mesh-bridge-invalid':
|
|
453
466
|
return gltfErr(code, { reason: 'empty-input', primitiveCount: 0 });
|
|
454
467
|
}
|
|
468
|
+
throw new Error(`unhandled glTF error code: ${code}`);
|
|
455
469
|
}
|
|
456
470
|
|
|
457
471
|
describe('errors.test.ts', () => {
|
|
@@ -460,8 +474,8 @@ function unwrapReimport(result: ReturnType<typeof reimportReuseMeta>) {
|
|
|
460
474
|
});
|
|
461
475
|
|
|
462
476
|
describe('GltfErrorCode roster', () => {
|
|
463
|
-
it('GLTF_ERROR_HINTS exposes exactly
|
|
464
|
-
expect(Object.keys(GLTF_ERROR_HINTS).length).toBe(
|
|
477
|
+
it('GLTF_ERROR_HINTS exposes exactly 25 keys', () => {
|
|
478
|
+
expect(Object.keys(GLTF_ERROR_HINTS).length).toBe(25);
|
|
465
479
|
});
|
|
466
480
|
|
|
467
481
|
it.each(ALL_CODES)('hint for %s is a non-empty string', (code) => {
|
|
@@ -1826,6 +1840,15 @@ function unwrapReimport(result: ReturnType<typeof reimportReuseMeta>) {
|
|
|
1826
1840
|
expect(mat).toBeDefined();
|
|
1827
1841
|
const parentRefs = mat?.refs.filter((r) => r.sourceField?.fieldName === 'parent');
|
|
1828
1842
|
expect(parentRefs?.map((ref) => ref.guid)).toEqual(['019e2cc6-0c86-79da-aa76-b0984c86d4ff']);
|
|
1843
|
+
expect(mat?.payload).toMatchObject({
|
|
1844
|
+
kind: 'material',
|
|
1845
|
+
parent: '019e2cc6-0c86-79da-aa76-b0984c86d4ff',
|
|
1846
|
+
});
|
|
1847
|
+
expect(Object.keys((mat?.payload ?? {}) as Record<string, unknown>).sort()).toEqual([
|
|
1848
|
+
'kind',
|
|
1849
|
+
'parent',
|
|
1850
|
+
'values',
|
|
1851
|
+
]);
|
|
1829
1852
|
});
|
|
1830
1853
|
|
|
1831
1854
|
it('(e) material refs texture edges: sourceField.componentName="<material>" and fieldName is texture slot', async () => {
|
|
@@ -1934,6 +1957,25 @@ function unwrapReimport(result: ReturnType<typeof reimportReuseMeta>) {
|
|
|
1934
1957
|
expect(map.get(0)).toBe('srgb');
|
|
1935
1958
|
});
|
|
1936
1959
|
|
|
1960
|
+
it('physical extension texture slots keep their glTF color domains', () => {
|
|
1961
|
+
const map = deriveTextureColorSpace({
|
|
1962
|
+
imageCount: 4,
|
|
1963
|
+
textures: [{ source: 0 }, { source: 1 }, { source: 2 }, { source: 3 }],
|
|
1964
|
+
materials: [
|
|
1965
|
+
{
|
|
1966
|
+
sheenColorTexture: 0,
|
|
1967
|
+
specularColorTexture: 1,
|
|
1968
|
+
anisotropyTexture: 2,
|
|
1969
|
+
thicknessTexture: 3,
|
|
1970
|
+
},
|
|
1971
|
+
],
|
|
1972
|
+
});
|
|
1973
|
+
expect(map.get(0)).toBe('srgb');
|
|
1974
|
+
expect(map.get(1)).toBe('srgb');
|
|
1975
|
+
expect(map.get(2)).toBe('linear');
|
|
1976
|
+
expect(map.get(3)).toBe('linear');
|
|
1977
|
+
});
|
|
1978
|
+
|
|
1937
1979
|
it('occlusionTexture image classifies as linear', () => {
|
|
1938
1980
|
const map = deriveTextureColorSpace({
|
|
1939
1981
|
imageCount: 1,
|
|
@@ -2023,12 +2065,20 @@ function unwrapReimport(result: ReturnType<typeof reimportReuseMeta>) {
|
|
|
2023
2065
|
stderrSpy.mockRestore();
|
|
2024
2066
|
});
|
|
2025
2067
|
|
|
2026
|
-
it('
|
|
2068
|
+
it('public allowlist contains every currently supported extension (literal)', () => {
|
|
2027
2069
|
expect(EXTENSION_ALLOWLIST).toEqual([
|
|
2028
2070
|
'EXT_mesh_gpu_instancing',
|
|
2029
2071
|
'EXT_meshopt_compression',
|
|
2030
|
-
'
|
|
2031
|
-
'
|
|
2072
|
+
'KHR_lights_punctual',
|
|
2073
|
+
'KHR_texture_transform',
|
|
2074
|
+
'KHR_materials_transmission',
|
|
2075
|
+
'KHR_materials_ior',
|
|
2076
|
+
'KHR_materials_volume',
|
|
2077
|
+
'KHR_materials_clearcoat',
|
|
2078
|
+
'KHR_materials_anisotropy',
|
|
2079
|
+
'KHR_materials_sheen',
|
|
2080
|
+
'KHR_materials_iridescence',
|
|
2081
|
+
'KHR_materials_specular',
|
|
2032
2082
|
]);
|
|
2033
2083
|
});
|
|
2034
2084
|
|
|
@@ -2095,6 +2145,11 @@ function unwrapReimport(result: ReturnType<typeof reimportReuseMeta>) {
|
|
|
2095
2145
|
'KHR_materials_transmission',
|
|
2096
2146
|
'KHR_materials_ior',
|
|
2097
2147
|
'KHR_materials_volume',
|
|
2148
|
+
'KHR_materials_clearcoat',
|
|
2149
|
+
'KHR_materials_anisotropy',
|
|
2150
|
+
'KHR_materials_sheen',
|
|
2151
|
+
'KHR_materials_iridescence',
|
|
2152
|
+
'KHR_materials_specular',
|
|
2098
2153
|
])('%s in extensionsRequired routes ok (supported)', (extension) => {
|
|
2099
2154
|
const result = checkExtensions({ extensionsRequired: [extension] });
|
|
2100
2155
|
expect(result.ok).toBe(true);
|
|
@@ -67,4 +67,57 @@ describe('glTF material Pack refs', () => {
|
|
|
67
67
|
'sampler-6',
|
|
68
68
|
]);
|
|
69
69
|
});
|
|
70
|
+
|
|
71
|
+
it('keeps the five physical extension texture edges in producer slot order', () => {
|
|
72
|
+
const material = {
|
|
73
|
+
name: 'PhysicalPackMaterial',
|
|
74
|
+
baseColorFactor: [1, 1, 1, 1],
|
|
75
|
+
metallicFactor: 0,
|
|
76
|
+
roughnessFactor: 0.5,
|
|
77
|
+
clearcoatNormalTexture: { texture: 0, sampler: 0 },
|
|
78
|
+
anisotropyTexture: { texture: 1, sampler: 1 },
|
|
79
|
+
sheenColorTexture: { texture: 2, sampler: 2 },
|
|
80
|
+
iridescenceTexture: { texture: 3, sampler: 3 },
|
|
81
|
+
specularColorTexture: { texture: 4, sampler: 4 },
|
|
82
|
+
} as unknown as GltfMaterialIr;
|
|
83
|
+
const doc = {
|
|
84
|
+
textures: [
|
|
85
|
+
{ source: 0, sampler: 0 },
|
|
86
|
+
{ source: 1, sampler: 1 },
|
|
87
|
+
{ source: 2, sampler: 2 },
|
|
88
|
+
{ source: 3, sampler: 3 },
|
|
89
|
+
{ source: 4, sampler: 4 },
|
|
90
|
+
],
|
|
91
|
+
} as unknown as GltfDoc;
|
|
92
|
+
const refs = materialRefsForPack(
|
|
93
|
+
material,
|
|
94
|
+
doc,
|
|
95
|
+
new Map([
|
|
96
|
+
[0, 'clearcoat-normal-texture'],
|
|
97
|
+
[1, 'anisotropy-texture'],
|
|
98
|
+
[2, 'sheen-color-texture'],
|
|
99
|
+
[3, 'iridescence-texture'],
|
|
100
|
+
[4, 'specular-color-texture'],
|
|
101
|
+
]),
|
|
102
|
+
new Map([
|
|
103
|
+
[0, 'clearcoat-normal-sampler'],
|
|
104
|
+
[1, 'anisotropy-sampler'],
|
|
105
|
+
[2, 'sheen-color-sampler'],
|
|
106
|
+
[3, 'iridescence-sampler'],
|
|
107
|
+
[4, 'specular-color-sampler'],
|
|
108
|
+
]),
|
|
109
|
+
);
|
|
110
|
+
expect(refs.map((ref) => ref.guid)).toEqual([
|
|
111
|
+
'clearcoat-normal-texture',
|
|
112
|
+
'clearcoat-normal-sampler',
|
|
113
|
+
'anisotropy-texture',
|
|
114
|
+
'anisotropy-sampler',
|
|
115
|
+
'sheen-color-texture',
|
|
116
|
+
'sheen-color-sampler',
|
|
117
|
+
'iridescence-texture',
|
|
118
|
+
'iridescence-sampler',
|
|
119
|
+
'specular-color-texture',
|
|
120
|
+
'specular-color-sampler',
|
|
121
|
+
]);
|
|
122
|
+
});
|
|
70
123
|
});
|
|
@@ -162,6 +162,35 @@ describe('glTF transmission material IR', () => {
|
|
|
162
162
|
});
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
+
describe('glTF specular material IR', () => {
|
|
166
|
+
it('projects specularTexture through the alpha channel', () => {
|
|
167
|
+
const result = parseMaterialIr(
|
|
168
|
+
{
|
|
169
|
+
extensions: {
|
|
170
|
+
KHR_materials_specular: {
|
|
171
|
+
specularFactor: 0.75,
|
|
172
|
+
specularTexture: { index: 0 },
|
|
173
|
+
specularColorFactor: [0.8, 0.7, 0.6],
|
|
174
|
+
specularColorTexture: { index: 1 },
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
[{ source: 0 }, { source: 1 }],
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
expect(result.ok).toBe(true);
|
|
182
|
+
if (!result.ok) return;
|
|
183
|
+
expect(result.value).toMatchObject({
|
|
184
|
+
specularFactor: 0.75,
|
|
185
|
+
specularChannel: 3,
|
|
186
|
+
specularTexture: { texture: 0 },
|
|
187
|
+
specularColorFactor: [0.8, 0.7, 0.6],
|
|
188
|
+
specularColorChannel: [0, 1, 2],
|
|
189
|
+
specularColorTexture: { texture: 1 },
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
165
194
|
describe('glTF required extension admission', () => {
|
|
166
195
|
it.each([
|
|
167
196
|
'KHR_materials_transmission',
|
|
@@ -183,7 +212,6 @@ describe('glTF required extension admission', () => {
|
|
|
183
212
|
expect(result.value.diagnostics.unsupportedExtensions).toEqual([]);
|
|
184
213
|
});
|
|
185
214
|
});
|
|
186
|
-
|
|
187
215
|
describe('glTF COLOR_0 importer carrier', () => {
|
|
188
216
|
it('publishes normalized VEC3 FLOAT colors as importer-owned RGBA', async () => {
|
|
189
217
|
const fixture = JSON.parse(
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { validateMaterialTangentInputs } from '../bridge.js';
|
|
3
|
+
import { checkExtensions } from '../check-extensions.js';
|
|
4
|
+
import { parseMaterial } from '../material/parse-material.js';
|
|
5
|
+
|
|
6
|
+
describe('KHR_materials_clearcoat projection', () => {
|
|
7
|
+
it('projects factor, R/G textures, coat normal RG, scale, and transform facts', () => {
|
|
8
|
+
const result = parseMaterial(
|
|
9
|
+
{
|
|
10
|
+
extensions: {
|
|
11
|
+
KHR_materials_clearcoat: {
|
|
12
|
+
clearcoatFactor: 0.75,
|
|
13
|
+
clearcoatTexture: {
|
|
14
|
+
index: 0,
|
|
15
|
+
texCoord: 2,
|
|
16
|
+
extensions: { KHR_texture_transform: { offset: [0.25, 0.5] } },
|
|
17
|
+
},
|
|
18
|
+
clearcoatRoughnessFactor: 0.25,
|
|
19
|
+
clearcoatRoughnessTexture: { index: 1, texCoord: 3 },
|
|
20
|
+
clearcoatNormalTexture: { index: 2, texCoord: 4, scale: 0.5 },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
} as never,
|
|
24
|
+
[{ source: 0 }, { source: 1 }, { source: 2 }],
|
|
25
|
+
);
|
|
26
|
+
expect(result.ok).toBe(true);
|
|
27
|
+
if (result.ok) {
|
|
28
|
+
expect(result.value).toMatchObject({
|
|
29
|
+
clearcoatFactor: 0.75,
|
|
30
|
+
clearcoatChannel: 0,
|
|
31
|
+
clearcoatRoughnessFactor: 0.25,
|
|
32
|
+
clearcoatRoughnessChannel: 1,
|
|
33
|
+
clearcoatNormalTexture: { texture: 2, texCoord: 4, scale: 0.5 },
|
|
34
|
+
clearcoatNormalChannel: [0, 1],
|
|
35
|
+
});
|
|
36
|
+
expect((result.value as unknown as Record<string, unknown>).clearcoatTexture).toMatchObject({
|
|
37
|
+
texture: 0,
|
|
38
|
+
texCoord: 2,
|
|
39
|
+
transform: { offset: [0.25, 0.5] },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('admits the clearcoat extension as a required extension', () => {
|
|
45
|
+
const result = checkExtensions({ extensionsRequired: ['KHR_materials_clearcoat'] });
|
|
46
|
+
expect(result.ok).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('reports tangent admission with material, mesh, layer, UV, and attributes', () => {
|
|
50
|
+
const result = validateMaterialTangentInputs(
|
|
51
|
+
{
|
|
52
|
+
name: 'coat',
|
|
53
|
+
baseColorFactor: [1, 1, 1, 1],
|
|
54
|
+
metallicFactor: 0,
|
|
55
|
+
roughnessFactor: 0.5,
|
|
56
|
+
clearcoatNormalTexture: { texture: 0 },
|
|
57
|
+
} as never,
|
|
58
|
+
{
|
|
59
|
+
name: 'mesh',
|
|
60
|
+
positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
|
|
61
|
+
texcoord0: new Float32Array([0, 0, 0, 0, 0, 0]),
|
|
62
|
+
materialIndex: 0,
|
|
63
|
+
meshIndex: 0,
|
|
64
|
+
indices: new Uint32Array([0, 1, 2]),
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
expect(result.ok).toBe(false);
|
|
68
|
+
if (!result.ok) {
|
|
69
|
+
expect(result.error.code).toBe('material-tangent-required');
|
|
70
|
+
expect(result.error.detail).toMatchObject({
|
|
71
|
+
material: 'coat',
|
|
72
|
+
mesh: 'mesh',
|
|
73
|
+
layer: 'clearcoat',
|
|
74
|
+
uv: 'TEXCOORD_0',
|
|
75
|
+
attributes: ['NORMAL', 'TEXCOORD_0', 'TANGENT'],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('requires a tangent frame for scalar-only anisotropy', () => {
|
|
81
|
+
const result = validateMaterialTangentInputs(
|
|
82
|
+
{
|
|
83
|
+
name: 'anisotropic-scalar',
|
|
84
|
+
baseColorFactor: [1, 1, 1, 1],
|
|
85
|
+
metallicFactor: 0,
|
|
86
|
+
roughnessFactor: 0.5,
|
|
87
|
+
anisotropyStrength: 0.4,
|
|
88
|
+
anisotropyRotation: 0,
|
|
89
|
+
} as never,
|
|
90
|
+
{
|
|
91
|
+
name: 'mesh-without-frame',
|
|
92
|
+
positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
|
|
93
|
+
materialIndex: 0,
|
|
94
|
+
meshIndex: 0,
|
|
95
|
+
indices: new Uint32Array([0, 1, 2]),
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
expect(result.ok).toBe(false);
|
|
99
|
+
if (!result.ok) {
|
|
100
|
+
expect(result.error.code).toBe('material-tangent-required');
|
|
101
|
+
expect(result.error.detail).toMatchObject({
|
|
102
|
+
material: 'anisotropic-scalar',
|
|
103
|
+
mesh: 'mesh-without-frame',
|
|
104
|
+
layer: 'anisotropy',
|
|
105
|
+
uv: 'TEXCOORD_0',
|
|
106
|
+
attributes: ['NORMAL', 'TEXCOORD_0', 'TANGENT'],
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|