@forgeax/engine-gltf 0.1.21 → 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 +115 -3
- package/dist/__tests__/lod-import.integration.test.d.ts +2 -0
- package/dist/__tests__/lod-import.integration.test.d.ts.map +1 -0
- package/dist/__tests__/lod-meta-publication.integration.test.d.ts +2 -0
- package/dist/__tests__/lod-meta-publication.integration.test.d.ts.map +1 -0
- 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 +362 -14
- package/dist/cli-gltf.mjs.map +1 -1
- package/dist/errors.d.ts +13 -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 +516 -25
- package/dist/importer-entry.mjs.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +609 -28
- package/dist/index.mjs.map +1 -1
- package/dist/lod/parse-lod.d.ts +15 -0
- package/dist/lod/parse-lod.d.ts.map +1 -0
- package/dist/lod/project-meta.d.ts +23 -0
- package/dist/lod/project-meta.d.ts.map +1 -0
- 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 +265 -8
- package/dist/node-file-entry.mjs.map +1 -1
- package/dist/parse-gltf.d.ts +3 -1
- package/dist/parse-gltf.d.ts.map +1 -1
- package/dist/reimport-reuse-meta.d.ts +1 -1
- package/dist/reimport-reuse-meta.d.ts.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__/fixtures/lod/core-only.gltf +1 -0
- package/src/__tests__/fixtures/lod/msft-lod-malformed.gltf +1 -0
- package/src/__tests__/fixtures/lod/msft-lod-multi-root.gltf +1 -0
- package/src/__tests__/fixtures/lod/msft-lod-node-extras.gltf +1 -0
- package/src/__tests__/fixtures/lod/msft-lod.gltf +1 -0
- package/src/__tests__/gltf-error-derived-views.test-d.ts +8 -0
- package/src/__tests__/gltf-error-migration.test.ts +3 -3
- package/src/__tests__/gltf.unit.test.ts +60 -5
- package/src/__tests__/lod-import.integration.test.ts +106 -0
- package/src/__tests__/lod-meta-publication.integration.test.ts +24 -0
- package/src/__tests__/material-pack-refs.integration.test.ts +53 -0
- package/src/__tests__/parse-gltf.unit.test.ts +50 -0
- package/src/__tests__/physical-clearcoat.unit.test.ts +110 -0
- package/src/__tests__/physical-material-import.integration.test.ts +239 -0
- package/src/__tests__/source-key-producer.integration.test.ts +6 -2
- package/src/bridge.ts +259 -14
- package/src/check-extensions.ts +16 -8
- package/src/errors.ts +32 -2
- package/src/gltf-importer.ts +122 -1
- package/src/image-color-space.ts +29 -4
- package/src/index.ts +10 -1
- package/src/lod/parse-lod.ts +129 -0
- package/src/lod/project-meta.ts +48 -0
- package/src/material/parse-material.ts +307 -0
- package/src/parse-gltf.ts +97 -1
- package/src/reimport-reuse-meta.ts +3 -1
- package/dist/.tsbuildinfo +0 -1
package/src/image-color-space.ts
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
//
|
|
4
4
|
// glTF 2.0 spec section 6.2 ("Material → Texture and Sampler") names which
|
|
5
5
|
// texture slots are colour-encoded vs data-encoded:
|
|
6
|
-
// - sRGB : baseColorTexture, emissiveTexture
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
6
|
+
// - sRGB : baseColorTexture, emissiveTexture, sheenColorTexture,
|
|
7
|
+
// specularColorTexture
|
|
8
|
+
// - linear : metallicRoughnessTexture, normalTexture, occlusionTexture,
|
|
9
|
+
// transmission/volume/clearcoat/anisotropy/sheen-roughness/
|
|
10
|
+
// iridescence/specular-weight data textures
|
|
10
11
|
// We pre-scan the doc so the gltfImporter knows each `images[]` row's
|
|
11
12
|
// colorSpace before decoding (TextureAsset.colorSpace + .format derive
|
|
12
13
|
// from this).
|
|
@@ -36,6 +37,18 @@ export interface MaterialColorSpaceInput {
|
|
|
36
37
|
readonly normalTexture?: TextureBinding;
|
|
37
38
|
readonly emissiveTexture?: TextureBinding;
|
|
38
39
|
readonly occlusionTexture?: TextureBinding;
|
|
40
|
+
readonly transmissionTexture?: TextureBinding;
|
|
41
|
+
readonly thicknessTexture?: TextureBinding;
|
|
42
|
+
readonly clearcoatTexture?: TextureBinding;
|
|
43
|
+
readonly clearcoatRoughnessTexture?: TextureBinding;
|
|
44
|
+
readonly clearcoatNormalTexture?: TextureBinding;
|
|
45
|
+
readonly anisotropyTexture?: TextureBinding;
|
|
46
|
+
readonly sheenColorTexture?: TextureBinding;
|
|
47
|
+
readonly sheenRoughnessTexture?: TextureBinding;
|
|
48
|
+
readonly iridescenceTexture?: TextureBinding;
|
|
49
|
+
readonly iridescenceThicknessTexture?: TextureBinding;
|
|
50
|
+
readonly specularTexture?: TextureBinding;
|
|
51
|
+
readonly specularColorTexture?: TextureBinding;
|
|
39
52
|
}
|
|
40
53
|
|
|
41
54
|
/** Slim view of a parsed `textures[]` row. */
|
|
@@ -89,9 +102,21 @@ export function deriveTextureColorSpace(
|
|
|
89
102
|
for (const mat of input.materials) {
|
|
90
103
|
record(imageOfTexture(mat.baseColorTexture), 'srgb');
|
|
91
104
|
record(imageOfTexture(mat.emissiveTexture), 'srgb');
|
|
105
|
+
record(imageOfTexture(mat.sheenColorTexture), 'srgb');
|
|
106
|
+
record(imageOfTexture(mat.specularColorTexture), 'srgb');
|
|
92
107
|
record(imageOfTexture(mat.metallicRoughnessTexture), 'linear');
|
|
93
108
|
record(imageOfTexture(mat.normalTexture), 'linear');
|
|
94
109
|
record(imageOfTexture(mat.occlusionTexture), 'linear');
|
|
110
|
+
record(imageOfTexture(mat.transmissionTexture), 'linear');
|
|
111
|
+
record(imageOfTexture(mat.thicknessTexture), 'linear');
|
|
112
|
+
record(imageOfTexture(mat.clearcoatTexture), 'linear');
|
|
113
|
+
record(imageOfTexture(mat.clearcoatRoughnessTexture), 'linear');
|
|
114
|
+
record(imageOfTexture(mat.clearcoatNormalTexture), 'linear');
|
|
115
|
+
record(imageOfTexture(mat.anisotropyTexture), 'linear');
|
|
116
|
+
record(imageOfTexture(mat.sheenRoughnessTexture), 'linear');
|
|
117
|
+
record(imageOfTexture(mat.iridescenceTexture), 'linear');
|
|
118
|
+
record(imageOfTexture(mat.iridescenceThicknessTexture), 'linear');
|
|
119
|
+
record(imageOfTexture(mat.specularTexture), 'linear');
|
|
95
120
|
}
|
|
96
121
|
|
|
97
122
|
for (let i = 0; i < input.imageCount; i++) {
|
package/src/index.ts
CHANGED
|
@@ -47,7 +47,13 @@ export type { GltfBridgeContext, MaterialBridgeContext } from './bridge.js';
|
|
|
47
47
|
// hello-gltf + hello-gltf-instancing, feat-20260518 M3 w9; M3 Tier-C material
|
|
48
48
|
// bridge; meshIrToMeshAsset excised from the hello-gltf demo in
|
|
49
49
|
// feat-20260603-asset-import-loader-injection M2 w19).
|
|
50
|
-
export {
|
|
50
|
+
export {
|
|
51
|
+
gltfDocToSceneAsset,
|
|
52
|
+
meshIrToMeshAsset,
|
|
53
|
+
toMaterialAsset,
|
|
54
|
+
validateMaterialTangentInputs,
|
|
55
|
+
validateMaterialUvSets,
|
|
56
|
+
} from './bridge.js';
|
|
51
57
|
export type { ExtensionsCheckResult, GltfExtensionsJson } from './check-extensions.js';
|
|
52
58
|
// KHR extensions gate (w14).
|
|
53
59
|
export { checkExtensions, EXTENSION_ALLOWLIST } from './check-extensions.js';
|
|
@@ -65,6 +71,7 @@ export type {
|
|
|
65
71
|
GltfImageMimeUnsupportedDetail,
|
|
66
72
|
GltfInstancingCountMismatchDetail,
|
|
67
73
|
GltfMalformedHeaderDetail,
|
|
74
|
+
GltfMaterialPhysicalInvalidDetail,
|
|
68
75
|
GltfMaterialTransmissionInvalidDetail,
|
|
69
76
|
GltfMetaMissingDetail,
|
|
70
77
|
GltfMorphInvalidDetail,
|
|
@@ -81,6 +88,8 @@ export { err, GLTF_ERROR_HINTS, gltfErr, ok } from './errors.js';
|
|
|
81
88
|
// gltfImporter: the build-time `{ key: 'gltf', import }` Importer
|
|
82
89
|
// (feat-20260603-asset-import-loader-injection M2 / w19).
|
|
83
90
|
export { gltfImporter } from './gltf-importer.js';
|
|
91
|
+
export { type GltfLodRelation, parseGltfLodExtension } from './lod/parse-lod.js';
|
|
92
|
+
export { projectGltfLodMeta } from './lod/project-meta.js';
|
|
84
93
|
export type { GlbChunks } from './parse-glb-chunks.js';
|
|
85
94
|
// Header / chunk parsers (w8).
|
|
86
95
|
export { parseGlbChunks, parseGltfHeader } from './parse-glb-chunks.js';
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { err, gltfErr, ok, type Result } from '../errors.js';
|
|
2
|
+
|
|
3
|
+
export interface GltfLodRelation {
|
|
4
|
+
readonly rootNode: number;
|
|
5
|
+
readonly lodNodeIds: readonly number[];
|
|
6
|
+
readonly screenCoverages: readonly number[];
|
|
7
|
+
/** All node-level MSFT_lod groups, retained for multi-root scenes. */
|
|
8
|
+
readonly groups: readonly GltfLodGroup[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface GltfLodGroup {
|
|
12
|
+
readonly rootNode: number;
|
|
13
|
+
readonly lodNodeIds: readonly number[];
|
|
14
|
+
readonly screenCoverages: readonly number[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface RawNode {
|
|
18
|
+
readonly mesh?: unknown;
|
|
19
|
+
readonly extensions?: { readonly MSFT_lod?: { readonly ids?: unknown } };
|
|
20
|
+
readonly extras?: { readonly MSFT_screencoverage?: unknown };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface RawMesh {
|
|
24
|
+
readonly primitives?: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface RawGltf {
|
|
28
|
+
readonly extensionsRequired?: readonly unknown[];
|
|
29
|
+
readonly meshes?: readonly RawMesh[];
|
|
30
|
+
readonly nodes?: readonly RawNode[];
|
|
31
|
+
readonly extensions?: { readonly MSFT_screencoverage?: { readonly scales?: unknown } };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function parseGltfLodExtension(
|
|
35
|
+
json: unknown,
|
|
36
|
+
): Result<GltfLodRelation, ReturnType<typeof gltfErr<'gltf-lod-invalid'>>> {
|
|
37
|
+
const doc = json as RawGltf;
|
|
38
|
+
const nodes = doc.nodes ?? [];
|
|
39
|
+
const rawCoverage = doc.extensions?.MSFT_screencoverage?.scales;
|
|
40
|
+
const groups: GltfLodGroup[] = [];
|
|
41
|
+
for (let rootNode = 0; rootNode < nodes.length; rootNode += 1) {
|
|
42
|
+
if (nodes[rootNode]?.extensions?.MSFT_lod === undefined) continue;
|
|
43
|
+
const idsRaw = nodes[rootNode]?.extensions?.MSFT_lod?.ids;
|
|
44
|
+
if (!Array.isArray(idsRaw)) {
|
|
45
|
+
return err(gltfErr('gltf-lod-invalid', { rootNode, ids: [], reason: 'not-integer' }));
|
|
46
|
+
}
|
|
47
|
+
const ids = idsRaw.filter((value): value is number => typeof value === 'number');
|
|
48
|
+
if (
|
|
49
|
+
ids.length !== idsRaw.length ||
|
|
50
|
+
ids.some((id) => !Number.isInteger(id) || id < 0 || id >= nodes.length)
|
|
51
|
+
) {
|
|
52
|
+
return err(gltfErr('gltf-lod-invalid', { rootNode, ids, reason: 'missing-node' }));
|
|
53
|
+
}
|
|
54
|
+
if (new Set(ids).size !== ids.length || ids.includes(rootNode)) {
|
|
55
|
+
return err(gltfErr('gltf-lod-invalid', { rootNode, ids, reason: 'duplicate-node' }));
|
|
56
|
+
}
|
|
57
|
+
const hasMesh = (node: RawNode | undefined): boolean => {
|
|
58
|
+
if (typeof node?.mesh !== 'number' || !Number.isInteger(node.mesh) || node.mesh < 0) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const mesh = doc.meshes?.[node.mesh];
|
|
62
|
+
// An extension relation is publishable only when its target mesh has at
|
|
63
|
+
// least one primitive. The parser normally receives the source mesh
|
|
64
|
+
// table, so an empty table/primitive list is a source-contract failure;
|
|
65
|
+
// treating it as a valid relation would let toAssetPack silently drop
|
|
66
|
+
// the LOD level later.
|
|
67
|
+
return mesh !== undefined && Array.isArray(mesh.primitives) && mesh.primitives.length > 0;
|
|
68
|
+
};
|
|
69
|
+
if (!hasMesh(nodes[rootNode]) || ids.some((id) => !hasMesh(nodes[id]))) {
|
|
70
|
+
return err(gltfErr('gltf-lod-invalid', { rootNode, ids, reason: 'missing-node' }));
|
|
71
|
+
}
|
|
72
|
+
const nodeCoverage = nodes[rootNode]?.extras?.MSFT_screencoverage;
|
|
73
|
+
const coverage = normalizeScreenCoverages(
|
|
74
|
+
nodeCoverage === undefined ? rawCoverage : nodeCoverage,
|
|
75
|
+
ids.length,
|
|
76
|
+
nodeCoverage !== undefined,
|
|
77
|
+
);
|
|
78
|
+
if (coverage === 'invalid') {
|
|
79
|
+
return err(gltfErr('gltf-lod-invalid', { rootNode, ids, reason: 'coverage' }));
|
|
80
|
+
}
|
|
81
|
+
groups.push({
|
|
82
|
+
rootNode,
|
|
83
|
+
lodNodeIds: ids,
|
|
84
|
+
screenCoverages: coverage ?? [],
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const first = groups[0];
|
|
88
|
+
if (first === undefined) {
|
|
89
|
+
if (doc.extensionsRequired?.some((extension) => extension === 'MSFT_lod')) {
|
|
90
|
+
return err(gltfErr('gltf-lod-invalid', { rootNode: 0, ids: [], reason: 'missing-node' }));
|
|
91
|
+
}
|
|
92
|
+
return ok({ rootNode: 0, lodNodeIds: [], screenCoverages: [], groups: [] });
|
|
93
|
+
}
|
|
94
|
+
return ok({
|
|
95
|
+
rootNode: first.rootNode,
|
|
96
|
+
lodNodeIds: first.lodNodeIds,
|
|
97
|
+
screenCoverages: first.screenCoverages,
|
|
98
|
+
groups,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Normalize the official node `extras.MSFT_screencoverage` form and the
|
|
104
|
+
* historical ForgeaX document-extension adapter form. The official array
|
|
105
|
+
* includes one terminal threshold for the discard range, while MeshAsset
|
|
106
|
+
* stores only the thresholds that select lower geometry levels.
|
|
107
|
+
*/
|
|
108
|
+
function normalizeScreenCoverages(
|
|
109
|
+
raw: unknown,
|
|
110
|
+
lodCount: number,
|
|
111
|
+
officialNodeForm: boolean,
|
|
112
|
+
): readonly number[] | undefined | 'invalid' {
|
|
113
|
+
if (raw === undefined) return undefined;
|
|
114
|
+
if (!Array.isArray(raw)) return 'invalid';
|
|
115
|
+
if (
|
|
116
|
+
raw.some(
|
|
117
|
+
(value) => typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value > 1,
|
|
118
|
+
)
|
|
119
|
+
) {
|
|
120
|
+
return 'invalid';
|
|
121
|
+
}
|
|
122
|
+
const expectedLengths = officialNodeForm ? [lodCount + 1] : [lodCount, lodCount + 1];
|
|
123
|
+
if (!expectedLengths.includes(raw.length)) return 'invalid';
|
|
124
|
+
const values = raw.slice(0, lodCount) as number[];
|
|
125
|
+
for (let index = 1; index < values.length; index += 1) {
|
|
126
|
+
if ((values[index] as number) >= (values[index - 1] as number)) return 'invalid';
|
|
127
|
+
}
|
|
128
|
+
return values;
|
|
129
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { deriveDefaultLodScreenCoverages, reconcileMeshLodMeta } from '@forgeax/engine-import';
|
|
2
|
+
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
3
|
+
|
|
4
|
+
export interface GltfLodMetaLevel {
|
|
5
|
+
readonly sourceKey: string;
|
|
6
|
+
readonly guid: string;
|
|
7
|
+
readonly screenCoverage?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface GltfLodMetaCandidate {
|
|
11
|
+
readonly rootSourceKey: string;
|
|
12
|
+
readonly levels: readonly GltfLodMetaLevel[];
|
|
13
|
+
readonly previous?: readonly GltfLodMetaLevel[] | undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface GltfLodMetaOutput {
|
|
17
|
+
readonly sourceKey: string;
|
|
18
|
+
readonly meshGuid: string;
|
|
19
|
+
readonly screenCoverage: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function projectGltfLodMeta(
|
|
23
|
+
input: GltfLodMetaCandidate,
|
|
24
|
+
): Result<
|
|
25
|
+
{ readonly lods: readonly GltfLodMetaOutput[] },
|
|
26
|
+
{ readonly code: string; readonly reason: string }
|
|
27
|
+
> {
|
|
28
|
+
const defaults = deriveDefaultLodScreenCoverages(input.levels.length + 1);
|
|
29
|
+
const previous = (input.previous ?? []).map((level) => ({
|
|
30
|
+
sourceKey: level.sourceKey,
|
|
31
|
+
meshGuid: level.guid,
|
|
32
|
+
...(level.screenCoverage === undefined ? {} : { screenCoverage: level.screenCoverage }),
|
|
33
|
+
}));
|
|
34
|
+
const next = input.levels.map((level, index) => ({
|
|
35
|
+
sourceKey: level.sourceKey,
|
|
36
|
+
meshGuid: level.guid,
|
|
37
|
+
screenCoverage: level.screenCoverage ?? defaults[index] ?? 0,
|
|
38
|
+
}));
|
|
39
|
+
const reconciled = reconcileMeshLodMeta(previous, next);
|
|
40
|
+
if (!reconciled.ok) return err({ code: reconciled.error.code, reason: reconciled.error.reason });
|
|
41
|
+
return ok({
|
|
42
|
+
lods: reconciled.value.lods.map((level) => ({
|
|
43
|
+
sourceKey: level.sourceKey,
|
|
44
|
+
meshGuid: level.meshGuid,
|
|
45
|
+
screenCoverage: level.screenCoverage,
|
|
46
|
+
})),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -93,6 +93,38 @@ export interface GltfMaterialJson {
|
|
|
93
93
|
readonly attenuationColor?: readonly number[];
|
|
94
94
|
readonly attenuationDistance?: number;
|
|
95
95
|
};
|
|
96
|
+
readonly KHR_materials_clearcoat?: {
|
|
97
|
+
readonly clearcoatFactor?: number;
|
|
98
|
+
readonly clearcoatTexture?: TextureInfoJson;
|
|
99
|
+
readonly clearcoatRoughnessFactor?: number;
|
|
100
|
+
readonly clearcoatRoughnessTexture?: TextureInfoJson;
|
|
101
|
+
readonly clearcoatNormalTexture?: NormalTextureInfoJson;
|
|
102
|
+
};
|
|
103
|
+
readonly KHR_materials_anisotropy?: {
|
|
104
|
+
readonly anisotropyStrength?: number;
|
|
105
|
+
readonly anisotropyRotation?: number;
|
|
106
|
+
readonly anisotropyTexture?: TextureInfoJson;
|
|
107
|
+
};
|
|
108
|
+
readonly KHR_materials_sheen?: {
|
|
109
|
+
readonly sheenColorFactor?: readonly number[];
|
|
110
|
+
readonly sheenColorTexture?: TextureInfoJson;
|
|
111
|
+
readonly sheenRoughnessFactor?: number;
|
|
112
|
+
readonly sheenRoughnessTexture?: TextureInfoJson;
|
|
113
|
+
};
|
|
114
|
+
readonly KHR_materials_iridescence?: {
|
|
115
|
+
readonly iridescenceFactor?: number;
|
|
116
|
+
readonly iridescenceIor?: number;
|
|
117
|
+
readonly iridescenceThicknessMinimum?: number;
|
|
118
|
+
readonly iridescenceThicknessMaximum?: number;
|
|
119
|
+
readonly iridescenceTexture?: TextureInfoJson;
|
|
120
|
+
readonly iridescenceThicknessTexture?: TextureInfoJson;
|
|
121
|
+
};
|
|
122
|
+
readonly KHR_materials_specular?: {
|
|
123
|
+
readonly specularFactor?: number;
|
|
124
|
+
readonly specularTexture?: TextureInfoJson;
|
|
125
|
+
readonly specularColorFactor?: readonly number[];
|
|
126
|
+
readonly specularColorTexture?: TextureInfoJson;
|
|
127
|
+
};
|
|
96
128
|
};
|
|
97
129
|
}
|
|
98
130
|
|
|
@@ -119,6 +151,40 @@ export interface GltfMaterialIr {
|
|
|
119
151
|
readonly thicknessChannel?: number;
|
|
120
152
|
readonly attenuationColor?: readonly [number, number, number];
|
|
121
153
|
readonly attenuationDistance?: number;
|
|
154
|
+
readonly clearcoatFactor?: number;
|
|
155
|
+
readonly clearcoatChannel?: number;
|
|
156
|
+
readonly clearcoatTexture?: GltfTextureInfoIr | number;
|
|
157
|
+
readonly clearcoatRoughnessFactor?: number;
|
|
158
|
+
readonly clearcoatRoughnessChannel?: number;
|
|
159
|
+
readonly clearcoatRoughnessTexture?: GltfTextureInfoIr | number;
|
|
160
|
+
readonly clearcoatNormalTexture?: GltfNormalTextureInfoIr | number;
|
|
161
|
+
readonly clearcoatNormalChannel?: readonly [number, number];
|
|
162
|
+
readonly anisotropyStrength?: number;
|
|
163
|
+
readonly anisotropyRotation?: number;
|
|
164
|
+
readonly anisotropyTexture?: GltfTextureInfoIr | number;
|
|
165
|
+
/** KHR_materials_anisotropy uses RG direction and B strength. */
|
|
166
|
+
readonly anisotropyChannel?: readonly [number, number, number];
|
|
167
|
+
readonly sheenColorFactor?: readonly [number, number, number];
|
|
168
|
+
readonly sheenColorTexture?: GltfTextureInfoIr | number;
|
|
169
|
+
readonly sheenColorChannel?: readonly [number, number, number];
|
|
170
|
+
readonly sheenRoughnessFactor?: number;
|
|
171
|
+
readonly sheenRoughnessTexture?: GltfTextureInfoIr | number;
|
|
172
|
+
readonly sheenRoughnessChannel?: number;
|
|
173
|
+
readonly iridescenceFactor?: number;
|
|
174
|
+
readonly iridescenceIor?: number;
|
|
175
|
+
readonly iridescenceThicknessMinimum?: number;
|
|
176
|
+
readonly iridescenceThicknessMaximum?: number;
|
|
177
|
+
readonly iridescenceTexture?: GltfTextureInfoIr | number;
|
|
178
|
+
readonly iridescenceChannel?: number;
|
|
179
|
+
readonly iridescenceThicknessTexture?: GltfTextureInfoIr | number;
|
|
180
|
+
readonly iridescenceThicknessChannel?: number;
|
|
181
|
+
readonly specularFactor?: number;
|
|
182
|
+
readonly specularTexture?: GltfTextureInfoIr | number;
|
|
183
|
+
/** KHR_materials_specular specularTexture uses the alpha channel. */
|
|
184
|
+
readonly specularChannel?: number;
|
|
185
|
+
readonly specularColorFactor?: readonly [number, number, number];
|
|
186
|
+
readonly specularColorTexture?: GltfTextureInfoIr | number;
|
|
187
|
+
readonly specularColorChannel?: readonly [number, number, number];
|
|
122
188
|
readonly alphaMode?: 'OPAQUE' | 'MASK' | 'BLEND';
|
|
123
189
|
readonly alphaCutoff?: number;
|
|
124
190
|
readonly doubleSided?: boolean;
|
|
@@ -186,6 +252,11 @@ export function parseMaterial(
|
|
|
186
252
|
const transmission = matJson.extensions?.KHR_materials_transmission;
|
|
187
253
|
const ior = matJson.extensions?.KHR_materials_ior;
|
|
188
254
|
const volume = matJson.extensions?.KHR_materials_volume;
|
|
255
|
+
const clearcoat = matJson.extensions?.KHR_materials_clearcoat;
|
|
256
|
+
const anisotropy = matJson.extensions?.KHR_materials_anisotropy;
|
|
257
|
+
const sheen = matJson.extensions?.KHR_materials_sheen;
|
|
258
|
+
const iridescence = matJson.extensions?.KHR_materials_iridescence;
|
|
259
|
+
const specular = matJson.extensions?.KHR_materials_specular;
|
|
189
260
|
const attenuationColor = tuple3(volume?.attenuationColor);
|
|
190
261
|
const invalid = (
|
|
191
262
|
extension: 'KHR_materials_transmission' | 'KHR_materials_ior' | 'KHR_materials_volume',
|
|
@@ -201,6 +272,25 @@ export function parseMaterial(
|
|
|
201
272
|
...(actual === undefined ? {} : { actual }),
|
|
202
273
|
}),
|
|
203
274
|
);
|
|
275
|
+
const physicalInvalid = (
|
|
276
|
+
extension:
|
|
277
|
+
| 'KHR_materials_clearcoat'
|
|
278
|
+
| 'KHR_materials_anisotropy'
|
|
279
|
+
| 'KHR_materials_sheen'
|
|
280
|
+
| 'KHR_materials_iridescence'
|
|
281
|
+
| 'KHR_materials_specular',
|
|
282
|
+
field: string,
|
|
283
|
+
reason: 'type' | 'range' | 'non-finite',
|
|
284
|
+
actual?: unknown,
|
|
285
|
+
): Result<GltfMaterialIr, GltfError> =>
|
|
286
|
+
err(
|
|
287
|
+
gltfErr('gltf-material-physical-invalid', {
|
|
288
|
+
extension,
|
|
289
|
+
field,
|
|
290
|
+
reason,
|
|
291
|
+
...(actual === undefined ? {} : { actual }),
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
204
294
|
if (
|
|
205
295
|
transmission?.transmissionFactor !== undefined &&
|
|
206
296
|
(typeof transmission.transmissionFactor !== 'number' ||
|
|
@@ -270,6 +360,132 @@ export function parseMaterial(
|
|
|
270
360
|
if (alphaMode === 'BLEND' && (transmission?.transmissionFactor ?? 0) > 0) {
|
|
271
361
|
return invalid('KHR_materials_transmission', 'alphaMode', 'blend', alphaMode);
|
|
272
362
|
}
|
|
363
|
+
if (
|
|
364
|
+
clearcoat?.clearcoatFactor !== undefined &&
|
|
365
|
+
(typeof clearcoat.clearcoatFactor !== 'number' || !Number.isFinite(clearcoat.clearcoatFactor))
|
|
366
|
+
) {
|
|
367
|
+
return physicalInvalid(
|
|
368
|
+
'KHR_materials_clearcoat',
|
|
369
|
+
'clearcoatFactor',
|
|
370
|
+
'non-finite',
|
|
371
|
+
clearcoat.clearcoatFactor,
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
if (
|
|
375
|
+
clearcoat?.clearcoatFactor !== undefined &&
|
|
376
|
+
(clearcoat.clearcoatFactor < 0 || clearcoat.clearcoatFactor > 1)
|
|
377
|
+
) {
|
|
378
|
+
return physicalInvalid(
|
|
379
|
+
'KHR_materials_clearcoat',
|
|
380
|
+
'clearcoatFactor',
|
|
381
|
+
'range',
|
|
382
|
+
clearcoat.clearcoatFactor,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
if (
|
|
386
|
+
clearcoat?.clearcoatRoughnessFactor !== undefined &&
|
|
387
|
+
(typeof clearcoat.clearcoatRoughnessFactor !== 'number' ||
|
|
388
|
+
!Number.isFinite(clearcoat.clearcoatRoughnessFactor))
|
|
389
|
+
) {
|
|
390
|
+
return physicalInvalid(
|
|
391
|
+
'KHR_materials_clearcoat',
|
|
392
|
+
'clearcoatRoughnessFactor',
|
|
393
|
+
'non-finite',
|
|
394
|
+
clearcoat.clearcoatRoughnessFactor,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
if (
|
|
398
|
+
clearcoat?.clearcoatRoughnessFactor !== undefined &&
|
|
399
|
+
(clearcoat.clearcoatRoughnessFactor < 0 || clearcoat.clearcoatRoughnessFactor > 1)
|
|
400
|
+
) {
|
|
401
|
+
return physicalInvalid(
|
|
402
|
+
'KHR_materials_clearcoat',
|
|
403
|
+
'clearcoatRoughnessFactor',
|
|
404
|
+
'range',
|
|
405
|
+
clearcoat.clearcoatRoughnessFactor,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
if (
|
|
409
|
+
clearcoat?.clearcoatNormalTexture?.scale !== undefined &&
|
|
410
|
+
(typeof clearcoat.clearcoatNormalTexture.scale !== 'number' ||
|
|
411
|
+
!Number.isFinite(clearcoat.clearcoatNormalTexture.scale) ||
|
|
412
|
+
clearcoat.clearcoatNormalTexture.scale < 0)
|
|
413
|
+
) {
|
|
414
|
+
return physicalInvalid(
|
|
415
|
+
'KHR_materials_clearcoat',
|
|
416
|
+
'clearcoatNormalTexture.scale',
|
|
417
|
+
'range',
|
|
418
|
+
clearcoat.clearcoatNormalTexture.scale,
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
const validateUnit = (
|
|
422
|
+
extension: Parameters<typeof physicalInvalid>[0],
|
|
423
|
+
field: string,
|
|
424
|
+
value: number | undefined,
|
|
425
|
+
) => {
|
|
426
|
+
if (value === undefined) return undefined;
|
|
427
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
428
|
+
return physicalInvalid(extension, field, 'non-finite', value);
|
|
429
|
+
if (value < 0 || value > 1) return physicalInvalid(extension, field, 'range', value);
|
|
430
|
+
return undefined;
|
|
431
|
+
};
|
|
432
|
+
const validateFinite = (
|
|
433
|
+
extension: Parameters<typeof physicalInvalid>[0],
|
|
434
|
+
field: string,
|
|
435
|
+
value: number | undefined,
|
|
436
|
+
minimum?: number,
|
|
437
|
+
) => {
|
|
438
|
+
if (value === undefined) return undefined;
|
|
439
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
440
|
+
return physicalInvalid(extension, field, 'non-finite', value);
|
|
441
|
+
if (minimum !== undefined && value < minimum)
|
|
442
|
+
return physicalInvalid(extension, field, 'range', value);
|
|
443
|
+
return undefined;
|
|
444
|
+
};
|
|
445
|
+
const validateColor = (
|
|
446
|
+
extension: Parameters<typeof physicalInvalid>[0],
|
|
447
|
+
field: string,
|
|
448
|
+
value: readonly number[] | undefined,
|
|
449
|
+
) => {
|
|
450
|
+
if (value === undefined) return undefined;
|
|
451
|
+
if (
|
|
452
|
+
value.length < 3 ||
|
|
453
|
+
value.slice(0, 3).some((channel) => typeof channel !== 'number' || !Number.isFinite(channel))
|
|
454
|
+
) {
|
|
455
|
+
return physicalInvalid(extension, field, 'type', value);
|
|
456
|
+
}
|
|
457
|
+
if (value.slice(0, 3).some((channel) => channel < 0 || channel > 1)) {
|
|
458
|
+
return physicalInvalid(extension, field, 'range', value);
|
|
459
|
+
}
|
|
460
|
+
return undefined;
|
|
461
|
+
};
|
|
462
|
+
const physicalChecks = [
|
|
463
|
+
validateUnit('KHR_materials_anisotropy', 'anisotropyStrength', anisotropy?.anisotropyStrength),
|
|
464
|
+
validateFinite(
|
|
465
|
+
'KHR_materials_anisotropy',
|
|
466
|
+
'anisotropyRotation',
|
|
467
|
+
anisotropy?.anisotropyRotation,
|
|
468
|
+
),
|
|
469
|
+
validateUnit('KHR_materials_sheen', 'sheenRoughnessFactor', sheen?.sheenRoughnessFactor),
|
|
470
|
+
validateColor('KHR_materials_sheen', 'sheenColorFactor', sheen?.sheenColorFactor),
|
|
471
|
+
validateUnit('KHR_materials_iridescence', 'iridescenceFactor', iridescence?.iridescenceFactor),
|
|
472
|
+
validateFinite('KHR_materials_iridescence', 'iridescenceIor', iridescence?.iridescenceIor, 1),
|
|
473
|
+
validateFinite(
|
|
474
|
+
'KHR_materials_iridescence',
|
|
475
|
+
'iridescenceThicknessMinimum',
|
|
476
|
+
iridescence?.iridescenceThicknessMinimum,
|
|
477
|
+
0,
|
|
478
|
+
),
|
|
479
|
+
validateFinite(
|
|
480
|
+
'KHR_materials_iridescence',
|
|
481
|
+
'iridescenceThicknessMaximum',
|
|
482
|
+
iridescence?.iridescenceThicknessMaximum,
|
|
483
|
+
0,
|
|
484
|
+
),
|
|
485
|
+
validateUnit('KHR_materials_specular', 'specularFactor', specular?.specularFactor),
|
|
486
|
+
validateColor('KHR_materials_specular', 'specularColorFactor', specular?.specularColorFactor),
|
|
487
|
+
];
|
|
488
|
+
for (const check of physicalChecks) if (check !== undefined) return check;
|
|
273
489
|
return ok({
|
|
274
490
|
...(matJson.name === undefined ? {} : { name: matJson.name }),
|
|
275
491
|
baseColorFactor: baseColor4,
|
|
@@ -330,6 +546,97 @@ export function parseMaterial(
|
|
|
330
546
|
? {}
|
|
331
547
|
: { attenuationDistance: volume.attenuationDistance }),
|
|
332
548
|
}),
|
|
549
|
+
...(clearcoat === undefined
|
|
550
|
+
? {}
|
|
551
|
+
: {
|
|
552
|
+
clearcoatFactor: clearcoat.clearcoatFactor ?? 0,
|
|
553
|
+
clearcoatChannel: 0,
|
|
554
|
+
clearcoatRoughnessFactor: clearcoat.clearcoatRoughnessFactor ?? 0,
|
|
555
|
+
clearcoatRoughnessChannel: 1,
|
|
556
|
+
clearcoatNormalChannel: [0, 1] as const,
|
|
557
|
+
...(clearcoat.clearcoatTexture === undefined
|
|
558
|
+
? {}
|
|
559
|
+
: { clearcoatTexture: parseTextureInfo(clearcoat.clearcoatTexture, textures) }),
|
|
560
|
+
...(clearcoat.clearcoatRoughnessTexture === undefined
|
|
561
|
+
? {}
|
|
562
|
+
: {
|
|
563
|
+
clearcoatRoughnessTexture: parseTextureInfo(
|
|
564
|
+
clearcoat.clearcoatRoughnessTexture,
|
|
565
|
+
textures,
|
|
566
|
+
),
|
|
567
|
+
}),
|
|
568
|
+
...(clearcoat.clearcoatNormalTexture === undefined
|
|
569
|
+
? {}
|
|
570
|
+
: {
|
|
571
|
+
clearcoatNormalTexture: {
|
|
572
|
+
...parseTextureInfo(clearcoat.clearcoatNormalTexture, textures),
|
|
573
|
+
...(clearcoat.clearcoatNormalTexture.scale === undefined
|
|
574
|
+
? {}
|
|
575
|
+
: { scale: clearcoat.clearcoatNormalTexture.scale }),
|
|
576
|
+
},
|
|
577
|
+
}),
|
|
578
|
+
}),
|
|
579
|
+
...(anisotropy === undefined
|
|
580
|
+
? {}
|
|
581
|
+
: {
|
|
582
|
+
anisotropyStrength: anisotropy.anisotropyStrength ?? 0,
|
|
583
|
+
anisotropyRotation: anisotropy.anisotropyRotation ?? 0,
|
|
584
|
+
anisotropyChannel: [0, 1, 2] as const,
|
|
585
|
+
...(anisotropy.anisotropyTexture === undefined
|
|
586
|
+
? {}
|
|
587
|
+
: { anisotropyTexture: parseTextureInfo(anisotropy.anisotropyTexture, textures) }),
|
|
588
|
+
}),
|
|
589
|
+
...(sheen === undefined
|
|
590
|
+
? {}
|
|
591
|
+
: {
|
|
592
|
+
sheenColorFactor: tuple3(sheen.sheenColorFactor) ?? [0, 0, 0],
|
|
593
|
+
sheenColorChannel: [0, 1, 2] as const,
|
|
594
|
+
sheenRoughnessFactor: sheen.sheenRoughnessFactor ?? 0,
|
|
595
|
+
sheenRoughnessChannel: 3,
|
|
596
|
+
...(sheen.sheenColorTexture === undefined
|
|
597
|
+
? {}
|
|
598
|
+
: { sheenColorTexture: parseTextureInfo(sheen.sheenColorTexture, textures) }),
|
|
599
|
+
...(sheen.sheenRoughnessTexture === undefined
|
|
600
|
+
? {}
|
|
601
|
+
: { sheenRoughnessTexture: parseTextureInfo(sheen.sheenRoughnessTexture, textures) }),
|
|
602
|
+
}),
|
|
603
|
+
...(iridescence === undefined
|
|
604
|
+
? {}
|
|
605
|
+
: {
|
|
606
|
+
iridescenceFactor: iridescence.iridescenceFactor ?? 0,
|
|
607
|
+
iridescenceIor: iridescence.iridescenceIor ?? 1.3,
|
|
608
|
+
iridescenceThicknessMinimum: iridescence.iridescenceThicknessMinimum ?? 100,
|
|
609
|
+
iridescenceThicknessMaximum: iridescence.iridescenceThicknessMaximum ?? 400,
|
|
610
|
+
iridescenceChannel: 0,
|
|
611
|
+
iridescenceThicknessChannel: 1,
|
|
612
|
+
...(iridescence.iridescenceTexture === undefined
|
|
613
|
+
? {}
|
|
614
|
+
: { iridescenceTexture: parseTextureInfo(iridescence.iridescenceTexture, textures) }),
|
|
615
|
+
...(iridescence.iridescenceThicknessTexture === undefined
|
|
616
|
+
? {}
|
|
617
|
+
: {
|
|
618
|
+
iridescenceThicknessTexture: parseTextureInfo(
|
|
619
|
+
iridescence.iridescenceThicknessTexture,
|
|
620
|
+
textures,
|
|
621
|
+
),
|
|
622
|
+
}),
|
|
623
|
+
}),
|
|
624
|
+
...(specular === undefined
|
|
625
|
+
? {}
|
|
626
|
+
: {
|
|
627
|
+
specularFactor: specular.specularFactor ?? 1,
|
|
628
|
+
specularChannel: 3,
|
|
629
|
+
specularColorFactor: tuple3(specular.specularColorFactor) ?? [1, 1, 1],
|
|
630
|
+
specularColorChannel: [0, 1, 2] as const,
|
|
631
|
+
...(specular.specularTexture === undefined
|
|
632
|
+
? {}
|
|
633
|
+
: { specularTexture: parseTextureInfo(specular.specularTexture, textures) }),
|
|
634
|
+
...(specular.specularColorTexture === undefined
|
|
635
|
+
? {}
|
|
636
|
+
: {
|
|
637
|
+
specularColorTexture: parseTextureInfo(specular.specularColorTexture, textures),
|
|
638
|
+
}),
|
|
639
|
+
}),
|
|
333
640
|
...(alphaMode === undefined ? {} : { alphaMode }),
|
|
334
641
|
...(alphaCutoff === undefined ? {} : { alphaCutoff }),
|
|
335
642
|
...(matJson.doubleSided === true ? { doubleSided: true } : {}),
|