@forgeax/engine-shader 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 +13 -4
- package/dist/ShaderRegistry.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +28 -52
- package/dist/index.mjs.map +1 -1
- package/dist/material-schemas.d.ts +31 -0
- package/dist/material-schemas.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/ShaderRegistry.ts +0 -17
- package/src/__tests__/default-standard-pbr-alpha.unit.test.ts +9 -3
- package/src/__tests__/default-standard-pbr-transmission.unit.test.ts +48 -166
- package/src/__tests__/deferred-lighting-ssao.test.ts +3 -3
- package/src/__tests__/ibl-irradiance.unit.test.ts +9 -0
- package/src/__tests__/lighting-directional.unit.test.ts +36 -0
- package/src/__tests__/lighting-punctual.unit.test.ts +12 -4
- package/src/__tests__/material-builtins.unit.test.ts +21 -3
- package/src/__tests__/material-contract.unit.test.ts +131 -17
- package/src/__tests__/physical-clearcoat.unit.test.ts +65 -0
- package/src/__tests__/probe-lighting-composition.unit.test.ts +3 -2
- package/src/__tests__/shader-manifest-transmission.unit.test.ts +8 -10
- package/src/__tests__/shader.unit.test.ts +52 -87
- package/src/__tests__/spot-cookie-composition.unit.test.ts +2 -1
- package/src/__tests__/standard-webgl2-varying-budget.unit.test.ts +17 -0
- package/src/__tests__/surface-v1-negative.unit.test.ts +37 -0
- package/src/__tests__/surface-v1.unit.test.ts +83 -0
- package/src/__tests__/transmission-thickness-scale.unit.test.ts +2 -6
- package/src/__tests__/vertex-color-variant.unit.test.ts +16 -5
- package/src/default-standard-pbr-skin.wgsl +678 -192
- package/src/default-standard-pbr.wgsl +563 -279
- package/src/default_standard_surface.wgsl +88 -0
- package/src/hdrp-cluster-forward.wgsl +225 -0
- package/src/ibl-irradiance.wgsl +43 -8
- package/src/ibl-prefilter.wgsl +13 -3
- package/src/ibl-shared.wgsl +9 -2
- package/src/index.ts +6 -0
- package/src/lighting-directional.wgsl +3 -1
- package/src/lighting-spot-modifiers.wgsl +8 -1
- package/src/lighting-spot-projector.wgsl +31 -0
- package/src/material/artifact-types.ts +2 -2
- package/src/material/physical/anisotropy.wgsl +26 -0
- package/src/material/physical/clearcoat.wgsl +24 -0
- package/src/material/physical/iridescence.wgsl +23 -0
- package/src/material/physical/sheen.wgsl +17 -0
- package/src/material-schemas.ts +65 -30
- package/src/register-default-standard-pbr-skin.ts +2 -2
- package/src/standard-cluster.wgsl +1 -36
- package/src/surface_v1.wgsl +26 -0
|
@@ -3,6 +3,37 @@ import { type MaterialShaderArtifactReceipt } from './material/artifact-types.js
|
|
|
3
3
|
export declare const STANDARD_PBR_ALPHA_CUTOFF_DEFAULT = 0;
|
|
4
4
|
/** Shared material contract for the standard PBR and skinned PBR shaders. */
|
|
5
5
|
export declare const DEFAULT_STANDARD_PBR_PARAM_SCHEMA: readonly ParamSchemaEntry[];
|
|
6
|
+
export type { StandardPhysicalTextureField } from '@forgeax/engine-types';
|
|
7
|
+
/**
|
|
8
|
+
* Backward-compatible export name for callers that need the physical
|
|
9
|
+
* projection. Every entry is selected from the Standard root schema above;
|
|
10
|
+
* no independent defaults or binding inventory can drift from it.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Stable physical texture injection order. The order is part of the
|
|
14
|
+
* Standard template's resource ABI and is derived from the root schema by
|
|
15
|
+
* `standardPhysicalTextureFields`; render/cook must not maintain another
|
|
16
|
+
* field inventory.
|
|
17
|
+
*/
|
|
18
|
+
export { STANDARD_PHYSICAL_TEXTURE_FIELDS, standardPhysicalTextureFields, } from '@forgeax/engine-types';
|
|
19
|
+
export declare const STANDARD_PHYSICAL_LAYER_PARAM_SCHEMA: readonly ParamSchemaEntry[];
|
|
20
|
+
/**
|
|
21
|
+
* Canonical static Standard entry. It deliberately contains only the base
|
|
22
|
+
* contract (including the specular extension) so a base-only material keeps
|
|
23
|
+
* its Deferred path and carries no second-stage physical resources.
|
|
24
|
+
*/
|
|
25
|
+
export declare const STANDARD_BASE_PARAM_SCHEMA: readonly ParamSchemaEntry[];
|
|
26
|
+
/**
|
|
27
|
+
* Canonical boot-time user region for the shared Standard material BGL.
|
|
28
|
+
*
|
|
29
|
+
* It is still a projection of the root schema, but unlike the author-facing
|
|
30
|
+
* base-only projection it retains the pre-existing transmission texture pair.
|
|
31
|
+
* The template reserves those two pairs before the IBL/backdrop injection so
|
|
32
|
+
* a base-only shader and a transmissive shader can share one boot layout;
|
|
33
|
+
* physical texture pairs are never included here and are appended only for a
|
|
34
|
+
* root contract that declares them.
|
|
35
|
+
*/
|
|
36
|
+
export declare const STANDARD_PIPELINE_PARAM_SCHEMA: readonly ParamSchemaEntry[];
|
|
6
37
|
/** The single producer receipt shared by direct and scene-index Standard PBR. */
|
|
7
38
|
export declare const STANDARD_PBR_ARTIFACT_RECEIPT: MaterialShaderArtifactReceipt;
|
|
8
39
|
/** Skinned Standard PBR keeps the material receipt identity and adds palette ABI. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"material-schemas.d.ts","sourceRoot":"","sources":["../src/material-schemas.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"material-schemas.d.ts","sourceRoot":"","sources":["../src/material-schemas.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAK9D,OAAO,EAEL,KAAK,6BAA6B,EACnC,MAAM,8BAA8B,CAAC;AAEtC,eAAO,MAAM,iCAAiC,IAAI,CAAC;AAEnD,6EAA6E;AAC7E,eAAO,MAAM,iCAAiC,6BAAiC,CAAC;AAEhF,YAAY,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAC1E;;;;GAIG;AACH;;;;;GAKG;AACH,OAAO,EACL,gCAAgC,EAChC,6BAA6B,GAC9B,MAAM,uBAAuB,CAAC;AAE/B,eAAO,MAAM,oCAAoC,EAAE,SAAS,gBAAgB,EAGzE,CAAC;AAEJ;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,EAAE,SAAS,gBAAgB,EAY/D,CAAC;AAEJ;;;;;;;;;GASG;AACH,eAAO,MAAM,8BAA8B,EAAE,SAAS,gBAAgB,EAOnE,CAAC;AAEJ,iFAAiF;AACjF,eAAO,MAAM,6BAA6B,EAAE,6BACR,CAAC;AAErC,qFAAqF;AACrF,eAAO,MAAM,kCAAkC,EAAE,6BACT,CAAC;AAEzC,eAAO,MAAM,0BAA0B,EAAE,SAAS,gBAAgB,EAIjE,CAAC;AAEF,eAAO,MAAM,2BAA2B,EAAE,SAAS,gBAAgB,EAMlE,CAAC;AAEF,eAAO,MAAM,8BAA8B,EAAE,SAAS,gBAAgB,EAIrE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgeax/engine-shader",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
"LICENSE"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@forgeax/engine-rhi": "0.1.
|
|
26
|
-
"@forgeax/engine-types": "0.1.
|
|
25
|
+
"@forgeax/engine-rhi": "0.1.24",
|
|
26
|
+
"@forgeax/engine-types": "0.1.24",
|
|
27
27
|
"@webgpu/types": "^0.1.71"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@forgeax/engine-vite-plugin-shader": "0.1.
|
|
30
|
+
"@forgeax/engine-vite-plugin-shader": "0.1.24"
|
|
31
31
|
},
|
|
32
32
|
"forgeax": {
|
|
33
33
|
"metrics": {
|
package/src/ShaderRegistry.ts
CHANGED
|
@@ -356,23 +356,6 @@ export class ShaderRegistry {
|
|
|
356
356
|
}
|
|
357
357
|
variantKeys.add(variant.definesKey);
|
|
358
358
|
}
|
|
359
|
-
if (ms.identifier === 'forgeax::default-standard-pbr') {
|
|
360
|
-
const transmissionStates = new Set(
|
|
361
|
-
ms.variants
|
|
362
|
-
.filter((variant) => 'TRANSMISSION_AVAILABLE' in variant.defines)
|
|
363
|
-
.map((variant) => variant.defines.TRANSMISSION_AVAILABLE),
|
|
364
|
-
);
|
|
365
|
-
if (!transmissionStates.has(false) || !transmissionStates.has(true)) {
|
|
366
|
-
return err(
|
|
367
|
-
manifestMalformed({
|
|
368
|
-
message:
|
|
369
|
-
'ShaderRegistry: Standard material shader is missing a transmission variant state',
|
|
370
|
-
hint: 'forgeax::default-standard-pbr must declare TRANSMISSION_AVAILABLE=false and true',
|
|
371
|
-
reason: `declared transmission states: ${JSON.stringify([...transmissionStates])}`,
|
|
372
|
-
}),
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
359
|
validatedMaterialShaderEntries.push(ms);
|
|
377
360
|
}
|
|
378
361
|
}
|
|
@@ -6,15 +6,21 @@ const shader = readFileSync(
|
|
|
6
6
|
fileURLToPath(new URL('../default-standard-pbr.wgsl', import.meta.url)),
|
|
7
7
|
'utf8',
|
|
8
8
|
);
|
|
9
|
+
const surface = readFileSync(
|
|
10
|
+
fileURLToPath(new URL('../default_standard_surface.wgsl', import.meta.url)),
|
|
11
|
+
'utf8',
|
|
12
|
+
);
|
|
9
13
|
|
|
10
14
|
describe('standard PBR MASK alpha contract', () => {
|
|
11
15
|
it('multiplies factor alpha by sampled texture alpha before discard', () => {
|
|
12
|
-
expect(
|
|
16
|
+
expect(surface).toContain(
|
|
17
|
+
'clamp(material.baseColor.a * baseSample.a * vertexColor.a, 0.0, 1.0)',
|
|
18
|
+
);
|
|
13
19
|
});
|
|
14
20
|
|
|
15
21
|
it('matches Three r184 alphaTest by discarding values equal to or below cutoff', () => {
|
|
16
|
-
expect(shader).toContain('
|
|
17
|
-
expect(shader).not.toContain('
|
|
22
|
+
expect(shader).toContain('surface.opacity <= surface.alphaClipThreshold');
|
|
23
|
+
expect(shader).not.toContain('surface.opacity < surface.alphaClipThreshold');
|
|
18
24
|
});
|
|
19
25
|
|
|
20
26
|
it('keeps alpha PBR on the shared Directional factor path', () => {
|
|
@@ -28,7 +28,7 @@ async function engineManifest(): Promise<EngineShaderManifest> {
|
|
|
28
28
|
let engineManifestPromise: Promise<EngineShaderManifest> | undefined;
|
|
29
29
|
|
|
30
30
|
describe('default Standard PBR transmission manifest contract', () => {
|
|
31
|
-
it('publishes
|
|
31
|
+
it('publishes every supported capability combination including transmission', {
|
|
32
32
|
timeout: 60_000,
|
|
33
33
|
}, async () => {
|
|
34
34
|
const manifest = await engineManifest();
|
|
@@ -38,63 +38,14 @@ describe('default Standard PBR transmission manifest contract', () => {
|
|
|
38
38
|
|
|
39
39
|
expect(standard).toBeDefined();
|
|
40
40
|
if (standard === undefined) return;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// expected keys derived here so adding a new axis cannot silently shrink
|
|
48
|
-
// the published set.
|
|
49
|
-
const expectedKeys = new Set<string>();
|
|
50
|
-
for (const clusterForwardAvailable of [false, true]) {
|
|
51
|
-
for (const storageBufferAvailable of [false, true]) {
|
|
52
|
-
for (const vertexColorAvailable of [false, true]) {
|
|
53
|
-
for (const probeBlendAvailable of [false, true]) {
|
|
54
|
-
for (const extendedLightingAvailable of [false, true]) {
|
|
55
|
-
for (const directionalPcssAvailable of [false, true]) {
|
|
56
|
-
for (const transmissionAvailable of [false, true]) {
|
|
57
|
-
for (const projectorAvailable of [false, true]) {
|
|
58
|
-
for (const reflectionFallbackAvailable of [false, true]) {
|
|
59
|
-
if (
|
|
60
|
-
(clusterForwardAvailable && !storageBufferAvailable) ||
|
|
61
|
-
(probeBlendAvailable && !storageBufferAvailable) ||
|
|
62
|
-
(extendedLightingAvailable &&
|
|
63
|
-
(!storageBufferAvailable || !projectorAvailable))
|
|
64
|
-
) {
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
const expectedKeyEntries = [
|
|
68
|
-
`CLUSTER_FORWARD_AVAILABLE=${clusterForwardAvailable}`,
|
|
69
|
-
`DIRECTIONAL_PCSS_AVAILABLE=${directionalPcssAvailable}`,
|
|
70
|
-
`EXTENDED_LIGHTING_AVAILABLE=${extendedLightingAvailable}`,
|
|
71
|
-
`PROBE_BLEND_AVAILABLE=${probeBlendAvailable}`,
|
|
72
|
-
`PROJECTOR_AVAILABLE=${projectorAvailable}`,
|
|
73
|
-
`REFLECTION_FALLBACK_AVAILABLE=${reflectionFallbackAvailable}`,
|
|
74
|
-
`STORAGE_BUFFER_AVAILABLE=${storageBufferAvailable}`,
|
|
75
|
-
`TRANSMISSION_AVAILABLE=${transmissionAvailable}`,
|
|
76
|
-
`VERTEX_COLOR_AVAILABLE=${vertexColorAvailable}`,
|
|
77
|
-
];
|
|
78
|
-
expectedKeys.add(
|
|
79
|
-
expectedKeyEntries.every((entry) => entry.endsWith('=true'))
|
|
80
|
-
? ''
|
|
81
|
-
: expectedKeyEntries.join('+'),
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
expect(transmissionVariants).toHaveLength(expectedKeys.size);
|
|
93
|
-
expect(new Set(transmissionVariants.map((variant) => variant.definesKey))).toEqual(
|
|
94
|
-
expectedKeys,
|
|
95
|
-
);
|
|
41
|
+
// Nine declared axes produce 512 combinations. The compiler publishes
|
|
42
|
+
// the supported subset: storage is required by cluster/probe/extended
|
|
43
|
+
// lighting, and extended lighting also requires the projector lane.
|
|
44
|
+
expect(standard.variants).toHaveLength(224);
|
|
45
|
+
expect(new Set(standard.variants.map((variant) => variant.definesKey)).size).toBe(224);
|
|
46
|
+
expect(new Set(standard.variants.map((variant) => variant.composedWgsl)).size).toBe(224);
|
|
96
47
|
expect(
|
|
97
|
-
|
|
48
|
+
standard.variants.every(
|
|
98
49
|
(variant) =>
|
|
99
50
|
!(
|
|
100
51
|
variant.defines.CLUSTER_FORWARD_AVAILABLE === true &&
|
|
@@ -111,67 +62,12 @@ describe('default Standard PBR transmission manifest contract', () => {
|
|
|
111
62
|
),
|
|
112
63
|
),
|
|
113
64
|
).toBe(true);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
for (const vertexColorAvailable of [false, true]) {
|
|
118
|
-
for (const probeBlendAvailable of [false, true]) {
|
|
119
|
-
for (const extendedLightingAvailable of [false, true]) {
|
|
120
|
-
for (const directionalPcssAvailable of [false, true]) {
|
|
121
|
-
for (const transmissionAvailable of [false, true]) {
|
|
122
|
-
for (const projectorAvailable of [false, true]) {
|
|
123
|
-
for (const reflectionFallbackAvailable of [false, true]) {
|
|
124
|
-
if (
|
|
125
|
-
(probeBlendAvailable && !storageBufferAvailable) ||
|
|
126
|
-
(extendedLightingAvailable &&
|
|
127
|
-
(!storageBufferAvailable || !projectorAvailable))
|
|
128
|
-
) {
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
const expectedKeyEntries = [
|
|
132
|
-
`CLUSTER_FORWARD_AVAILABLE=${clusterForwardAvailable}`,
|
|
133
|
-
`DIRECTIONAL_PCSS_AVAILABLE=${directionalPcssAvailable}`,
|
|
134
|
-
`EXTENDED_LIGHTING_AVAILABLE=${extendedLightingAvailable}`,
|
|
135
|
-
`PROBE_BLEND_AVAILABLE=${probeBlendAvailable}`,
|
|
136
|
-
`PROJECTOR_AVAILABLE=${projectorAvailable}`,
|
|
137
|
-
`REFLECTION_FALLBACK_AVAILABLE=${reflectionFallbackAvailable}`,
|
|
138
|
-
`STORAGE_BUFFER_AVAILABLE=${storageBufferAvailable}`,
|
|
139
|
-
`TRANSMISSION_AVAILABLE=${transmissionAvailable}`,
|
|
140
|
-
`VERTEX_COLOR_AVAILABLE=${vertexColorAvailable}`,
|
|
141
|
-
];
|
|
142
|
-
const expectedKey = expectedKeyEntries.every((entry) =>
|
|
143
|
-
entry.endsWith('=true'),
|
|
144
|
-
)
|
|
145
|
-
? ''
|
|
146
|
-
: expectedKeyEntries.join('+');
|
|
147
|
-
expect(transmissionVariants).toContainEqual(
|
|
148
|
-
expect.objectContaining({
|
|
149
|
-
definesKey: expectedKey,
|
|
150
|
-
defines: expect.objectContaining({
|
|
151
|
-
CLUSTER_FORWARD_AVAILABLE: clusterForwardAvailable,
|
|
152
|
-
DIRECTIONAL_PCSS_AVAILABLE: directionalPcssAvailable,
|
|
153
|
-
EXTENDED_LIGHTING_AVAILABLE: extendedLightingAvailable,
|
|
154
|
-
PROBE_BLEND_AVAILABLE: probeBlendAvailable,
|
|
155
|
-
PROJECTOR_AVAILABLE: projectorAvailable,
|
|
156
|
-
REFLECTION_FALLBACK_AVAILABLE: reflectionFallbackAvailable,
|
|
157
|
-
STORAGE_BUFFER_AVAILABLE: storageBufferAvailable,
|
|
158
|
-
TRANSMISSION_AVAILABLE: transmissionAvailable,
|
|
159
|
-
VERTEX_COLOR_AVAILABLE: vertexColorAvailable,
|
|
160
|
-
}),
|
|
161
|
-
}),
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
65
|
+
expect(standard.variants.every((variant) => 'TRANSMISSION_AVAILABLE' in variant.defines)).toBe(
|
|
66
|
+
true,
|
|
67
|
+
);
|
|
172
68
|
});
|
|
173
69
|
|
|
174
|
-
it('omits transmission
|
|
70
|
+
it('omits transmission and physical resources from the non-transmission fallback variant', {
|
|
175
71
|
timeout: 60_000,
|
|
176
72
|
}, async () => {
|
|
177
73
|
const manifest = await engineManifest();
|
|
@@ -190,23 +86,34 @@ describe('default Standard PBR transmission manifest contract', () => {
|
|
|
190
86
|
variant.defines.TRANSMISSION_AVAILABLE === false &&
|
|
191
87
|
variant.defines.VERTEX_COLOR_AVAILABLE === false,
|
|
192
88
|
);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
variant.defines.EXTENDED_LIGHTING_AVAILABLE === false &&
|
|
198
|
-
variant.defines.PROBE_BLEND_AVAILABLE === false &&
|
|
199
|
-
variant.defines.TRANSMISSION_AVAILABLE === true &&
|
|
200
|
-
variant.defines.VERTEX_COLOR_AVAILABLE === false,
|
|
89
|
+
expect(fallback).toBeDefined();
|
|
90
|
+
if (fallback === undefined) return;
|
|
91
|
+
expect(fallback.composedWgsl).not.toMatch(
|
|
92
|
+
/var(?:<[^>]+>)?\s+(?:transmissionSampler|transmissionTexture|thicknessSampler|thicknessTexture|clearcoatTexture|clearcoatRoughnessTexture|clearcoatNormalTexture|anisotropyTexture|sheenColorTexture|sheenRoughnessTexture|iridescenceTexture|iridescenceThicknessTexture|specularTexture)\b/u,
|
|
201
93
|
);
|
|
202
|
-
expect(fallback?.composedWgsl).not.toContain('@binding(13)');
|
|
203
|
-
expect(fallback?.composedWgsl).not.toContain('@binding(24)');
|
|
204
|
-
expect(transmission?.composedWgsl).toContain('@binding(13)');
|
|
205
|
-
expect(transmission?.composedWgsl).toContain('@binding(24)');
|
|
206
94
|
});
|
|
207
95
|
|
|
208
|
-
it('
|
|
96
|
+
it('publishes the same explicit transmission axis for the skin Standard template', {
|
|
209
97
|
timeout: 60_000,
|
|
98
|
+
}, async () => {
|
|
99
|
+
const manifest = await engineManifest();
|
|
100
|
+
const skin = manifest.materialShaders.find((entry) => entry.identifier === 'forgeax::pbr-skin');
|
|
101
|
+
expect(skin).toBeDefined();
|
|
102
|
+
if (skin === undefined) return;
|
|
103
|
+
expect(skin.variants.length).toBeGreaterThan(0);
|
|
104
|
+
expect(skin.variants.every((variant) => 'TRANSMISSION_AVAILABLE' in variant.defines)).toBe(
|
|
105
|
+
true,
|
|
106
|
+
);
|
|
107
|
+
expect(skin.variants.some((variant) => variant.defines.TRANSMISSION_AVAILABLE === false)).toBe(
|
|
108
|
+
true,
|
|
109
|
+
);
|
|
110
|
+
expect(skin.variants.some((variant) => variant.defines.TRANSMISSION_AVAILABLE === true)).toBe(
|
|
111
|
+
true,
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('registers one Standard row and resolves a declared capability variant by exact key', {
|
|
116
|
+
timeout: 20_000,
|
|
210
117
|
}, async () => {
|
|
211
118
|
const manifest = await engineManifest();
|
|
212
119
|
const standard = manifest.materialShaders.find(
|
|
@@ -229,38 +136,13 @@ describe('default Standard PBR transmission manifest contract', () => {
|
|
|
229
136
|
expect(registered).toBeDefined();
|
|
230
137
|
if (registered === undefined) return;
|
|
231
138
|
|
|
232
|
-
const
|
|
233
|
-
(
|
|
139
|
+
const variant = registered.variants.find(
|
|
140
|
+
(candidate) => candidate.defines.CLUSTER_FORWARD_AVAILABLE === false,
|
|
234
141
|
);
|
|
235
|
-
expect(
|
|
236
|
-
if (
|
|
237
|
-
expect(findVariantByKey(registered,
|
|
238
|
-
expect(
|
|
239
|
-
findVariantByKey(registered, `${transmission.definesKey}+UNDECLARED=true`),
|
|
240
|
-
).toBeUndefined();
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('rejects a manifest with a missing transmission variant before prewarm', async () => {
|
|
244
|
-
const missingVariant = {
|
|
245
|
-
identifier: 'forgeax::default-standard-pbr',
|
|
246
|
-
sourcePath: 'default-standard-pbr.wgsl',
|
|
247
|
-
composedWgsl: 'standard-default',
|
|
248
|
-
paramSchema: '[]',
|
|
249
|
-
variants: [
|
|
250
|
-
{
|
|
251
|
-
definesKey: 'TRANSMISSION_AVAILABLE=false',
|
|
252
|
-
defines: { TRANSMISSION_AVAILABLE: false },
|
|
253
|
-
composedWgsl: 'standard-no-transmission',
|
|
254
|
-
},
|
|
255
|
-
],
|
|
256
|
-
};
|
|
257
|
-
const registry = new ShaderRegistry({
|
|
258
|
-
device: device(),
|
|
259
|
-
manifestUrl: dataUrl({ entries: [], materialShaders: [missingVariant] }),
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
const loaded = await registry.loadManifest();
|
|
263
|
-
expect(loaded.ok).toBe(false);
|
|
142
|
+
expect(variant).toBeDefined();
|
|
143
|
+
if (variant === undefined) return;
|
|
144
|
+
expect(findVariantByKey(registered, variant.definesKey)).toBe(variant);
|
|
145
|
+
expect(findVariantByKey(registered, `${variant.definesKey}+UNDECLARED=true`)).toBeUndefined();
|
|
264
146
|
});
|
|
265
147
|
|
|
266
148
|
it('rejects duplicate variant keys instead of allowing ambiguous prewarm', async () => {
|
|
@@ -271,14 +153,14 @@ describe('default Standard PBR transmission manifest contract', () => {
|
|
|
271
153
|
paramSchema: '[]',
|
|
272
154
|
variants: [
|
|
273
155
|
{
|
|
274
|
-
definesKey: '
|
|
275
|
-
defines: {
|
|
276
|
-
composedWgsl: 'standard-
|
|
156
|
+
definesKey: 'CLUSTER_FORWARD_AVAILABLE=false',
|
|
157
|
+
defines: { CLUSTER_FORWARD_AVAILABLE: false },
|
|
158
|
+
composedWgsl: 'standard-a',
|
|
277
159
|
},
|
|
278
160
|
{
|
|
279
|
-
definesKey: '
|
|
280
|
-
defines: {
|
|
281
|
-
composedWgsl: 'standard-
|
|
161
|
+
definesKey: 'CLUSTER_FORWARD_AVAILABLE=false',
|
|
162
|
+
defines: { CLUSTER_FORWARD_AVAILABLE: false },
|
|
163
|
+
composedWgsl: 'standard-b',
|
|
282
164
|
},
|
|
283
165
|
],
|
|
284
166
|
};
|
|
@@ -51,12 +51,12 @@ function stripComments(src: string): string {
|
|
|
51
51
|
|
|
52
52
|
describe('deferred lighting ambient SSAO synthesis (M2 / w11)', () => {
|
|
53
53
|
it('(a) ambient calculation uses occlusionStrength * bakedAO texture sample', () => {
|
|
54
|
-
// The ambient expression starts from kD * irradiance before
|
|
55
|
-
//
|
|
54
|
+
// The ambient expression starts from kD * irradiance before skylight terms
|
|
55
|
+
// are applied; ao comes from material.occlusionStrength.
|
|
56
56
|
// M7/w32 changed `let` -> `var` so the SSAO blend line `ambient *= mix(...)` can mutate it.
|
|
57
57
|
const codeOnly = stripComments(pbrSource);
|
|
58
58
|
expect(codeOnly).toMatch(/var\s+ambient\s*=\s*\(*\s*kD\s*\*\s*irradiance/);
|
|
59
|
-
expect(codeOnly).toMatch(/
|
|
59
|
+
expect(codeOnly).toMatch(/surface\.occlusion/);
|
|
60
60
|
expect(codeOnly).toMatch(/skylight\.intensity/);
|
|
61
61
|
});
|
|
62
62
|
|
|
@@ -43,4 +43,13 @@ describe('IBL irradiance payload contract', () => {
|
|
|
43
43
|
expect(diffuseRadiance).toBeCloseTo(environment, 12);
|
|
44
44
|
expect(Number.isFinite(diffuseRadiance)).toBe(true);
|
|
45
45
|
});
|
|
46
|
+
|
|
47
|
+
it('rejects NaN and infinity before a sample reaches the accumulation sum', () => {
|
|
48
|
+
expect(irradianceSource).toMatch(/const IRRADIANCE_SAMPLE_DELTA:\s*f32\s*=\s*0\.05/);
|
|
49
|
+
expect(irradianceSource).toContain('fn irradianceFiniteScalar');
|
|
50
|
+
expect(irradianceSource).toContain('abs(value) <= 65504.0');
|
|
51
|
+
expect(irradianceSource).toMatch(
|
|
52
|
+
/irradianceFiniteVec3\(sampleVec\)[\s\S]*irradianceFiniteVec3\(sampleColor\)[\s\S]*irradianceFiniteScalar\(sampleWeight\)/,
|
|
53
|
+
);
|
|
54
|
+
});
|
|
46
55
|
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
const source = readFileSync(new URL('../lighting-directional.wgsl', import.meta.url), 'utf8');
|
|
5
|
+
|
|
6
|
+
type Vec3 = readonly [number, number, number];
|
|
7
|
+
|
|
8
|
+
function unsafeNormalize(value: Vec3): Vec3 {
|
|
9
|
+
const length = Math.hypot(value[0], value[1], value[2]);
|
|
10
|
+
return [value[0] / length, value[1] / length, value[2] / length];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function directionalHalfVector(viewDir: Vec3, lightDir: Vec3): Vec3 {
|
|
14
|
+
const sum: Vec3 = [viewDir[0] + lightDir[0], viewDir[1] + lightDir[1], viewDir[2] + lightDir[2]];
|
|
15
|
+
const lengthSquared = Math.max(sum[0] ** 2 + sum[1] ** 2 + sum[2] ** 2, 1e-8);
|
|
16
|
+
const inverseLength = 1 / Math.sqrt(lengthSquared);
|
|
17
|
+
return [sum[0] * inverseLength, sum[1] * inverseLength, sum[2] * inverseLength];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('directional GGX half-vector boundary', () => {
|
|
21
|
+
it('reproduces the old normalize zero-vector failure', () => {
|
|
22
|
+
const halfVector: Vec3 = [0, 0, 0];
|
|
23
|
+
expect(unsafeNormalize(halfVector).some((value) => !Number.isFinite(value))).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it.each([
|
|
27
|
+
['anti-parallel', [0, 0, 1] as const, [0, 0, -1] as const],
|
|
28
|
+
['near anti-parallel', [0, 0, 1] as const, [1e-5, 0, -1] as const],
|
|
29
|
+
])('%s inputs remain finite in evalDirectionalNoShadow', (_name, viewDir, lightDir) => {
|
|
30
|
+
expect(directionalHalfVector(viewDir, lightDir).every(Number.isFinite)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('uses a finite lower bound for the half-vector length', () => {
|
|
34
|
+
expect(source).toContain('max(dot(halfVector, halfVector), 1e-8)');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -8,6 +8,10 @@ const standardClusterShaderPath = join(
|
|
|
8
8
|
dirname(fileURLToPath(import.meta.url)),
|
|
9
9
|
'../standard-cluster.wgsl',
|
|
10
10
|
);
|
|
11
|
+
const spotProjectorShaderPath = join(
|
|
12
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
13
|
+
'../lighting-spot-projector.wgsl',
|
|
14
|
+
);
|
|
11
15
|
const pbrShaderPath = join(dirname(fileURLToPath(import.meta.url)), '../default-standard-pbr.wgsl');
|
|
12
16
|
const brdfShaderPath = join(dirname(fileURLToPath(import.meta.url)), '../brdf.wgsl');
|
|
13
17
|
const directionalShaderPath = join(
|
|
@@ -83,13 +87,16 @@ describe('direct punctual lighting shader contract', () => {
|
|
|
83
87
|
});
|
|
84
88
|
|
|
85
89
|
it('keeps the unshadowed spot sentinel out of the projector flag branch', async () => {
|
|
86
|
-
const source = await
|
|
90
|
+
const [source, projector] = await Promise.all([
|
|
91
|
+
readFile(standardClusterShaderPath, 'utf8'),
|
|
92
|
+
readFile(spotProjectorShaderPath, 'utf8'),
|
|
93
|
+
]);
|
|
87
94
|
|
|
88
95
|
expect(source).toContain('if (encoded_tile >= 0 && (encoded_tile & PROJECTOR_FLAG) != 0)');
|
|
89
96
|
expect(source).toContain(
|
|
90
97
|
'sampleStandardSpotProjector(view.spotLightViewProj[tile], world_pos, light.metadata)',
|
|
91
98
|
);
|
|
92
|
-
expect(
|
|
99
|
+
expect(projector).toContain(
|
|
93
100
|
'textureSampleLevel(cookieTexture, spotModifierSampler, uv, metadata.w, 0.0).rgb',
|
|
94
101
|
);
|
|
95
102
|
});
|
|
@@ -98,9 +105,10 @@ describe('direct punctual lighting shader contract', () => {
|
|
|
98
105
|
const source = await readFile(pbrShaderPath, 'utf8');
|
|
99
106
|
|
|
100
107
|
expect(source).toMatch(
|
|
101
|
-
/evaluateStandardClusterLights\(\s*in\.ndc\.xyz,\s*in
|
|
108
|
+
/evaluateStandardClusterLights\(\s*in\.ndc\.xyz,\s*standardViewZ\(in\),\s*in\.worldPos,\s*physicalNormal,\s*v,\s*diffuseAlbedo,\s*metallic,\s*a,\s*f0,\s*false,?\s*\)/u,
|
|
102
109
|
);
|
|
103
|
-
expect(source).toContain('
|
|
110
|
+
expect(source).toContain('clearcoatAlpha,');
|
|
111
|
+
expect(source).toContain('vec3<f32>(0.04),');
|
|
104
112
|
});
|
|
105
113
|
|
|
106
114
|
it('requires one shared Directional PCSS orchestration owner', async () => {
|
|
@@ -83,8 +83,6 @@ describe('built-in MaterialAsset sources', () => {
|
|
|
83
83
|
'metallicRoughnessTexture',
|
|
84
84
|
'normalSampler',
|
|
85
85
|
'normalTexture',
|
|
86
|
-
'specularTintSampler',
|
|
87
|
-
'specularTintTexture',
|
|
88
86
|
'emissiveSampler',
|
|
89
87
|
'emissiveTexture',
|
|
90
88
|
'occlusionSampler',
|
|
@@ -104,6 +102,13 @@ describe('built-in MaterialAsset sources', () => {
|
|
|
104
102
|
expect(text).toContain('@binding(13) var transmissionSampler');
|
|
105
103
|
expect(text).toContain('@binding(16) var thicknessTexture');
|
|
106
104
|
expect(text).toContain('@binding(17) var irradianceMap');
|
|
105
|
+
expect(text).toMatch(
|
|
106
|
+
/#ifdef SPECULAR_COLOR_TEXTURE_AVAILABLE\s+@group\(1\) @binding\(44\) var specularColorTextureSampler/u,
|
|
107
|
+
);
|
|
108
|
+
expect(text).toMatch(
|
|
109
|
+
/#ifdef SPECULAR_COLOR_TEXTURE_AVAILABLE[\s\S]*?@group\(1\) @binding\(45\) var specularColorTexture/u,
|
|
110
|
+
);
|
|
111
|
+
expect(text).not.toMatch(/@binding\((?:7|8)\) var specularColorTexture/u);
|
|
107
112
|
});
|
|
108
113
|
});
|
|
109
114
|
|
|
@@ -116,7 +121,20 @@ describe('built-in and custom material derived layout matrix', () => {
|
|
|
116
121
|
expect(names).toContain('thicknessTexture');
|
|
117
122
|
expect(
|
|
118
123
|
DEFAULT_STANDARD_PBR_PARAM_SCHEMA.filter((entry) => entry.type === 'texture2d'),
|
|
119
|
-
).toHaveLength(
|
|
124
|
+
).toHaveLength(17);
|
|
125
|
+
expect(names).toEqual(
|
|
126
|
+
expect.arrayContaining([
|
|
127
|
+
'clearcoatTexture',
|
|
128
|
+
'clearcoatRoughnessTexture',
|
|
129
|
+
'clearcoatNormalTexture',
|
|
130
|
+
'anisotropyTexture',
|
|
131
|
+
'sheenColorTexture',
|
|
132
|
+
'sheenRoughnessTexture',
|
|
133
|
+
'iridescenceTexture',
|
|
134
|
+
'iridescenceThicknessTexture',
|
|
135
|
+
'specularTexture',
|
|
136
|
+
]),
|
|
137
|
+
);
|
|
120
138
|
});
|
|
121
139
|
|
|
122
140
|
it('derives a coordinate record for every texture parameter in an interleaved custom schema', () => {
|