@forgeax/engine-gltf 0.1.23 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +373 -24
- 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 +374 -25
- 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 +84 -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 +52 -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 +258 -13
- 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.24",
|
|
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.24",
|
|
42
|
+
"@forgeax/engine-geometry": "0.1.24",
|
|
43
|
+
"@forgeax/engine-import": "0.1.24",
|
|
44
|
+
"@forgeax/engine-math": "0.1.24",
|
|
45
|
+
"@forgeax/engine-pack": "0.1.24",
|
|
46
|
+
"@forgeax/engine-scene": "0.1.24",
|
|
47
|
+
"@forgeax/engine-types": "0.1.24",
|
|
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.24",
|
|
53
|
+
"@forgeax/engine-picking": "0.1.24",
|
|
54
|
+
"@forgeax/engine-render": "0.1.24",
|
|
55
|
+
"@forgeax/engine-runtime": "0.1.24",
|
|
56
56
|
"@types/node": "^20.14.0"
|
|
57
57
|
},
|
|
58
58
|
"forgeax": {
|
|
@@ -1,4 +1,4 @@
|
|
|
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';
|
|
@@ -149,4 +149,87 @@ describe('glTF material bridge values', () => {
|
|
|
149
149
|
},
|
|
150
150
|
});
|
|
151
151
|
});
|
|
152
|
+
|
|
153
|
+
it('owns a complete five-extension Standard root and admits only authored physical textures', () => {
|
|
154
|
+
const material = toMaterialAsset(
|
|
155
|
+
{
|
|
156
|
+
name: 'physical-root',
|
|
157
|
+
baseColorFactor: [0.8, 0.7, 0.6, 1],
|
|
158
|
+
metallicFactor: 0.1,
|
|
159
|
+
roughnessFactor: 0.35,
|
|
160
|
+
clearcoatFactor: 0.75,
|
|
161
|
+
clearcoatRoughnessFactor: 0.2,
|
|
162
|
+
clearcoatNormalTexture: { texture: 0 },
|
|
163
|
+
anisotropyStrength: 0.5,
|
|
164
|
+
anisotropyRotation: 0.25,
|
|
165
|
+
anisotropyTexture: { texture: 1 },
|
|
166
|
+
sheenColorFactor: [0.2, 0.1, 0.05],
|
|
167
|
+
sheenRoughnessFactor: 0.3,
|
|
168
|
+
sheenColorTexture: { texture: 2 },
|
|
169
|
+
iridescenceFactor: 0.6,
|
|
170
|
+
iridescenceIor: 1.4,
|
|
171
|
+
iridescenceThicknessMinimum: 120,
|
|
172
|
+
iridescenceThicknessMaximum: 380,
|
|
173
|
+
iridescenceTexture: { texture: 3 },
|
|
174
|
+
specularFactor: 0.8,
|
|
175
|
+
specularColorFactor: [0.9, 0.85, 0.75],
|
|
176
|
+
specularColorTexture: { texture: 4 },
|
|
177
|
+
} as unknown as GltfMaterialIr,
|
|
178
|
+
{
|
|
179
|
+
standardRootGuid: 'standard-root' as unknown as AssetGuid,
|
|
180
|
+
textureHandles: new Map([
|
|
181
|
+
[0, 100],
|
|
182
|
+
[1, 101],
|
|
183
|
+
[2, 102],
|
|
184
|
+
[3, 103],
|
|
185
|
+
[4, 104],
|
|
186
|
+
]) as unknown as ReadonlyMap<number, Handle<'TextureAsset', 'shared'>>,
|
|
187
|
+
},
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
expect(material.parent).toBeUndefined();
|
|
191
|
+
const names = new Set((material.parameters ?? []).map((parameter) => parameter.name));
|
|
192
|
+
for (const name of [
|
|
193
|
+
'clearcoat',
|
|
194
|
+
'clearcoatRoughness',
|
|
195
|
+
'clearcoatNormalTexture',
|
|
196
|
+
'anisotropyStrength',
|
|
197
|
+
'anisotropyRotation',
|
|
198
|
+
'anisotropyTexture',
|
|
199
|
+
'sheenColor',
|
|
200
|
+
'sheenRoughness',
|
|
201
|
+
'sheenColorTexture',
|
|
202
|
+
'iridescence',
|
|
203
|
+
'iridescenceIor',
|
|
204
|
+
'iridescenceThicknessMinimum',
|
|
205
|
+
'iridescenceThicknessMaximum',
|
|
206
|
+
'iridescenceTexture',
|
|
207
|
+
'specular',
|
|
208
|
+
'specularColor',
|
|
209
|
+
'specularColorTexture',
|
|
210
|
+
]) {
|
|
211
|
+
expect(names.has(name)).toBe(true);
|
|
212
|
+
}
|
|
213
|
+
for (const name of [
|
|
214
|
+
'clearcoatTexture',
|
|
215
|
+
'clearcoatRoughnessTexture',
|
|
216
|
+
'sheenRoughnessTexture',
|
|
217
|
+
'iridescenceThicknessTexture',
|
|
218
|
+
'specularTexture',
|
|
219
|
+
]) {
|
|
220
|
+
expect(names.has(name)).toBe(false);
|
|
221
|
+
}
|
|
222
|
+
expect(material.values).toMatchObject({
|
|
223
|
+
clearcoat: 0.75,
|
|
224
|
+
anisotropyStrength: 0.5,
|
|
225
|
+
sheenColor: [0.2, 0.1, 0.05],
|
|
226
|
+
iridescenceIor: 1.4,
|
|
227
|
+
specularColor: [0.9, 0.85, 0.75],
|
|
228
|
+
clearcoatNormalTexture: { texture: 100 },
|
|
229
|
+
anisotropyTexture: { texture: 101 },
|
|
230
|
+
sheenColorTexture: { texture: 102 },
|
|
231
|
+
iridescenceTexture: { texture: 103 },
|
|
232
|
+
specularColorTexture: { texture: 104 },
|
|
233
|
+
});
|
|
234
|
+
});
|
|
152
235
|
});
|
|
@@ -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) => {
|
|
@@ -1934,6 +1948,25 @@ function unwrapReimport(result: ReturnType<typeof reimportReuseMeta>) {
|
|
|
1934
1948
|
expect(map.get(0)).toBe('srgb');
|
|
1935
1949
|
});
|
|
1936
1950
|
|
|
1951
|
+
it('physical extension texture slots keep their glTF color domains', () => {
|
|
1952
|
+
const map = deriveTextureColorSpace({
|
|
1953
|
+
imageCount: 4,
|
|
1954
|
+
textures: [{ source: 0 }, { source: 1 }, { source: 2 }, { source: 3 }],
|
|
1955
|
+
materials: [
|
|
1956
|
+
{
|
|
1957
|
+
sheenColorTexture: 0,
|
|
1958
|
+
specularColorTexture: 1,
|
|
1959
|
+
anisotropyTexture: 2,
|
|
1960
|
+
thicknessTexture: 3,
|
|
1961
|
+
},
|
|
1962
|
+
],
|
|
1963
|
+
});
|
|
1964
|
+
expect(map.get(0)).toBe('srgb');
|
|
1965
|
+
expect(map.get(1)).toBe('srgb');
|
|
1966
|
+
expect(map.get(2)).toBe('linear');
|
|
1967
|
+
expect(map.get(3)).toBe('linear');
|
|
1968
|
+
});
|
|
1969
|
+
|
|
1937
1970
|
it('occlusionTexture image classifies as linear', () => {
|
|
1938
1971
|
const map = deriveTextureColorSpace({
|
|
1939
1972
|
imageCount: 1,
|
|
@@ -2023,12 +2056,20 @@ function unwrapReimport(result: ReturnType<typeof reimportReuseMeta>) {
|
|
|
2023
2056
|
stderrSpy.mockRestore();
|
|
2024
2057
|
});
|
|
2025
2058
|
|
|
2026
|
-
it('
|
|
2059
|
+
it('public allowlist contains every currently supported extension (literal)', () => {
|
|
2027
2060
|
expect(EXTENSION_ALLOWLIST).toEqual([
|
|
2028
2061
|
'EXT_mesh_gpu_instancing',
|
|
2029
2062
|
'EXT_meshopt_compression',
|
|
2030
|
-
'
|
|
2031
|
-
'
|
|
2063
|
+
'KHR_lights_punctual',
|
|
2064
|
+
'KHR_texture_transform',
|
|
2065
|
+
'KHR_materials_transmission',
|
|
2066
|
+
'KHR_materials_ior',
|
|
2067
|
+
'KHR_materials_volume',
|
|
2068
|
+
'KHR_materials_clearcoat',
|
|
2069
|
+
'KHR_materials_anisotropy',
|
|
2070
|
+
'KHR_materials_sheen',
|
|
2071
|
+
'KHR_materials_iridescence',
|
|
2072
|
+
'KHR_materials_specular',
|
|
2032
2073
|
]);
|
|
2033
2074
|
});
|
|
2034
2075
|
|
|
@@ -2095,6 +2136,11 @@ function unwrapReimport(result: ReturnType<typeof reimportReuseMeta>) {
|
|
|
2095
2136
|
'KHR_materials_transmission',
|
|
2096
2137
|
'KHR_materials_ior',
|
|
2097
2138
|
'KHR_materials_volume',
|
|
2139
|
+
'KHR_materials_clearcoat',
|
|
2140
|
+
'KHR_materials_anisotropy',
|
|
2141
|
+
'KHR_materials_sheen',
|
|
2142
|
+
'KHR_materials_iridescence',
|
|
2143
|
+
'KHR_materials_specular',
|
|
2098
2144
|
])('%s in extensionsRequired routes ok (supported)', (extension) => {
|
|
2099
2145
|
const result = checkExtensions({ extensionsRequired: [extension] });
|
|
2100
2146
|
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
|
+
});
|