@forgeax/engine-shader 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 +110 -4
- package/dist/ShaderRegistry.d.ts.map +1 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +59 -55
- package/dist/index.mjs.map +1 -1
- package/dist/material/artifact-registry.d.ts +1 -0
- package/dist/material/artifact-registry.d.ts.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__/builtin-texture-sampling-contract.test.ts +10 -1
- 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 +7 -4
- 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-pbr-webgl2-interface.unit.test.ts +33 -0
- package/src/__tests__/standard-physical-layer.unit.test.ts +16 -0
- package/src/__tests__/standard-surface-pass-evaluation.integration.test.ts +38 -0
- package/src/__tests__/standard-webgl2-varying-budget.unit.test.ts +17 -0
- package/src/__tests__/surface-lighting-provenance.integration.test.ts +59 -0
- package/src/__tests__/surface-pass-templates.integration.test.ts +28 -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 +704 -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 +37 -3
- 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-registry.ts +20 -1
- 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/standard-physical-layer.wgsl +12 -0
- package/src/material-schemas.ts +65 -30
- package/src/register-default-standard-pbr-skin.ts +2 -2
- package/src/shadow_caster.wgsl +80 -6
- package/src/standard-cluster.wgsl +1 -36
- package/src/surface_v1.wgsl +26 -0
- package/src/unlit.wgsl +12 -0
|
@@ -150,6 +150,114 @@ describe('standard PBR transmission material contract', () => {
|
|
|
150
150
|
kind: 'texture',
|
|
151
151
|
binding: 16,
|
|
152
152
|
},
|
|
153
|
+
{
|
|
154
|
+
name: 'clearcoatTexture_sampler',
|
|
155
|
+
parameter: 'clearcoatTexture',
|
|
156
|
+
kind: 'sampler',
|
|
157
|
+
binding: 17,
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'clearcoatTexture',
|
|
161
|
+
parameter: 'clearcoatTexture',
|
|
162
|
+
kind: 'texture',
|
|
163
|
+
binding: 18,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: 'clearcoatRoughnessTexture_sampler',
|
|
167
|
+
parameter: 'clearcoatRoughnessTexture',
|
|
168
|
+
kind: 'sampler',
|
|
169
|
+
binding: 19,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: 'clearcoatRoughnessTexture',
|
|
173
|
+
parameter: 'clearcoatRoughnessTexture',
|
|
174
|
+
kind: 'texture',
|
|
175
|
+
binding: 20,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: 'clearcoatNormalTexture_sampler',
|
|
179
|
+
parameter: 'clearcoatNormalTexture',
|
|
180
|
+
kind: 'sampler',
|
|
181
|
+
binding: 21,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: 'clearcoatNormalTexture',
|
|
185
|
+
parameter: 'clearcoatNormalTexture',
|
|
186
|
+
kind: 'texture',
|
|
187
|
+
binding: 22,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: 'anisotropyTexture_sampler',
|
|
191
|
+
parameter: 'anisotropyTexture',
|
|
192
|
+
kind: 'sampler',
|
|
193
|
+
binding: 23,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: 'anisotropyTexture',
|
|
197
|
+
parameter: 'anisotropyTexture',
|
|
198
|
+
kind: 'texture',
|
|
199
|
+
binding: 24,
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: 'sheenColorTexture_sampler',
|
|
203
|
+
parameter: 'sheenColorTexture',
|
|
204
|
+
kind: 'sampler',
|
|
205
|
+
binding: 25,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: 'sheenColorTexture',
|
|
209
|
+
parameter: 'sheenColorTexture',
|
|
210
|
+
kind: 'texture',
|
|
211
|
+
binding: 26,
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'sheenRoughnessTexture_sampler',
|
|
215
|
+
parameter: 'sheenRoughnessTexture',
|
|
216
|
+
kind: 'sampler',
|
|
217
|
+
binding: 27,
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: 'sheenRoughnessTexture',
|
|
221
|
+
parameter: 'sheenRoughnessTexture',
|
|
222
|
+
kind: 'texture',
|
|
223
|
+
binding: 28,
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: 'iridescenceTexture_sampler',
|
|
227
|
+
parameter: 'iridescenceTexture',
|
|
228
|
+
kind: 'sampler',
|
|
229
|
+
binding: 29,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: 'iridescenceTexture',
|
|
233
|
+
parameter: 'iridescenceTexture',
|
|
234
|
+
kind: 'texture',
|
|
235
|
+
binding: 30,
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: 'iridescenceThicknessTexture_sampler',
|
|
239
|
+
parameter: 'iridescenceThicknessTexture',
|
|
240
|
+
kind: 'sampler',
|
|
241
|
+
binding: 31,
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: 'iridescenceThicknessTexture',
|
|
245
|
+
parameter: 'iridescenceThicknessTexture',
|
|
246
|
+
kind: 'texture',
|
|
247
|
+
binding: 32,
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: 'specularTexture_sampler',
|
|
251
|
+
parameter: 'specularTexture',
|
|
252
|
+
kind: 'sampler',
|
|
253
|
+
binding: 33,
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: 'specularTexture',
|
|
257
|
+
parameter: 'specularTexture',
|
|
258
|
+
kind: 'texture',
|
|
259
|
+
binding: 34,
|
|
260
|
+
},
|
|
153
261
|
]);
|
|
154
262
|
|
|
155
263
|
const { buildEngineShaderManifest } = await import('@forgeax/engine-vite-plugin-shader');
|
|
@@ -159,32 +267,38 @@ describe('standard PBR transmission material contract', () => {
|
|
|
159
267
|
);
|
|
160
268
|
expect(material).toBeDefined();
|
|
161
269
|
if (material === undefined) return;
|
|
162
|
-
const
|
|
270
|
+
const baseVariant = material.variants.find(
|
|
163
271
|
(variant) =>
|
|
164
272
|
variant.defines.CLUSTER_FORWARD_AVAILABLE === false &&
|
|
165
273
|
variant.defines.STORAGE_BUFFER_AVAILABLE === true &&
|
|
166
|
-
variant.defines.TRANSMISSION_AVAILABLE ===
|
|
274
|
+
variant.defines.TRANSMISSION_AVAILABLE === false &&
|
|
167
275
|
variant.defines.VERTEX_COLOR_AVAILABLE === false,
|
|
168
276
|
);
|
|
169
|
-
expect(
|
|
170
|
-
if (
|
|
171
|
-
expect(derived.totalBytes).toBe(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
277
|
+
expect(baseVariant).toBeDefined();
|
|
278
|
+
if (baseVariant === undefined) return;
|
|
279
|
+
expect(derived.totalBytes).toBe(736);
|
|
280
|
+
// The base capability variant owns only the ordinary material region and
|
|
281
|
+
// the IBL injection. Transmission and second-stage physical maps are
|
|
282
|
+
// specialized in authored roots, so no transmission/physical resource
|
|
283
|
+
// declaration may leak into this base artifact.
|
|
284
|
+
expect(baseVariant.composedWgsl).not.toMatch(
|
|
285
|
+
/@group\(1\)\s*@binding\((?:13|14)\)\s*\n?\s*var\s+transmission/u,
|
|
177
286
|
);
|
|
178
|
-
expect(
|
|
179
|
-
/@group\(1\)\s*@binding\(15\)\s*var
|
|
287
|
+
expect(baseVariant.composedWgsl).not.toMatch(
|
|
288
|
+
/@group\(1\)\s*@binding\((?:13|14|15|16|24|25|26|27|28|29|30|31|32|33|34|35|36|37|38|39|40|41|42|43|44|45)\)\s*\n?\s*var\s+(?:thickness|clearcoat|anisotropy|sheen|iridescence|specular)/u,
|
|
180
289
|
);
|
|
181
|
-
expect(
|
|
182
|
-
/@group\(1\)\s*@binding\(
|
|
290
|
+
expect(baseVariant.composedWgsl).toMatch(
|
|
291
|
+
/@group\(1\)\s*@binding\(15\)\s*\n?\s*var\s+irradianceMap/u,
|
|
183
292
|
);
|
|
184
293
|
});
|
|
185
294
|
|
|
186
295
|
it('publishes one receipt for direct and scene-index consumers', {
|
|
187
|
-
|
|
296
|
+
// Manifest production is intentionally real and coverage instrumentation
|
|
297
|
+
// adds enough startup cost to exceed the old 20s budget on the shared
|
|
298
|
+
// heavy runner. Keep the same producer-backed budget as the neighboring
|
|
299
|
+
// contract cases so CPU contention cannot turn a valid compile into a
|
|
300
|
+
// timeout-only failure.
|
|
301
|
+
timeout: 60_000,
|
|
188
302
|
}, async () => {
|
|
189
303
|
const { buildEngineShaderManifest } = await import('@forgeax/engine-vite-plugin-shader');
|
|
190
304
|
const manifest = await buildEngineShaderManifest();
|
|
@@ -208,7 +322,7 @@ describe('standard PBR transmission material contract', () => {
|
|
|
208
322
|
});
|
|
209
323
|
|
|
210
324
|
it('composes the scene-index entry into the same validated PBR artifact as the direct entry', {
|
|
211
|
-
timeout:
|
|
325
|
+
timeout: 60_000,
|
|
212
326
|
}, async () => {
|
|
213
327
|
const { buildEngineShaderManifest } = await import('@forgeax/engine-vite-plugin-shader');
|
|
214
328
|
const manifest = await buildEngineShaderManifest();
|
|
@@ -225,7 +339,7 @@ describe('standard PBR transmission material contract', () => {
|
|
|
225
339
|
});
|
|
226
340
|
|
|
227
341
|
it('rejects a receipt that loses a required prepared ABI field', {
|
|
228
|
-
timeout:
|
|
342
|
+
timeout: 60_000,
|
|
229
343
|
}, async () => {
|
|
230
344
|
const { buildEngineShaderManifest } = await import('@forgeax/engine-vite-plugin-shader');
|
|
231
345
|
const manifest = await buildEngineShaderManifest();
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
const source = (name: string) =>
|
|
5
|
+
readFileSync(new URL(`../material/physical/${name}`, import.meta.url), 'utf8');
|
|
6
|
+
|
|
7
|
+
describe('clearcoat physical shader layer', () => {
|
|
8
|
+
it('owns the top-layer Fresnel attenuation and coat normal inputs', () => {
|
|
9
|
+
const clearcoat = source('clearcoat.wgsl');
|
|
10
|
+
expect(clearcoat).toContain('clearcoatRoughness');
|
|
11
|
+
expect(clearcoat).toContain('clearcoatNormalScale');
|
|
12
|
+
expect(clearcoat).toContain('fresnel');
|
|
13
|
+
expect(clearcoat).toMatch(/base.*attenuat|attenuat.*base/i);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('keeps standard and skinned roots on one physical module contract', () => {
|
|
17
|
+
const standard = readFileSync(new URL('../default-standard-pbr.wgsl', import.meta.url), 'utf8');
|
|
18
|
+
const skin = readFileSync(
|
|
19
|
+
new URL('../default-standard-pbr-skin.wgsl', import.meta.url),
|
|
20
|
+
'utf8',
|
|
21
|
+
);
|
|
22
|
+
expect(standard).toContain('clearcoat');
|
|
23
|
+
expect(skin).toContain('clearcoat');
|
|
24
|
+
expect(standard).toContain('#import forgeax_pbr::clearcoat');
|
|
25
|
+
expect(skin).toContain('#import forgeax_pbr::clearcoat');
|
|
26
|
+
expect(standard).not.toMatch(/clearcoat[A-Za-z]*Shadow/);
|
|
27
|
+
expect(skin).not.toMatch(/clearcoat[A-Za-z]*Shadow/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('routes clearcoat direct and IBL through complete lighting facts', () => {
|
|
31
|
+
const roots = [
|
|
32
|
+
readFileSync(new URL('../default-standard-pbr.wgsl', import.meta.url), 'utf8'),
|
|
33
|
+
readFileSync(new URL('../default-standard-pbr-skin.wgsl', import.meta.url), 'utf8'),
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
for (const root of roots) {
|
|
37
|
+
expect(root).toMatch(/let clearcoatDirect = directionalShadow \* evalDirectionalNoShadow\(/);
|
|
38
|
+
expect(root).toContain('clearcoatRoughnessValue * clearcoatRoughnessValue');
|
|
39
|
+
expect(root).toContain('clearcoatIbl * skyColor * skylight.intensity');
|
|
40
|
+
expect(root).not.toContain('clearcoatIbl * skylight.colorR');
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('keeps rigid and skinned physical lighting on the same spot/transmission contract', () => {
|
|
45
|
+
const standard = readFileSync(new URL('../default-standard-pbr.wgsl', import.meta.url), 'utf8');
|
|
46
|
+
const skin = readFileSync(
|
|
47
|
+
new URL('../default-standard-pbr-skin.wgsl', import.meta.url),
|
|
48
|
+
'utf8',
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
for (const root of [standard, skin]) {
|
|
52
|
+
expect(root).toContain(
|
|
53
|
+
'let directionalBase = evalDirectionalNoShadow(physicalNormal, v, diffuseAlbedo, metallic, a, f0);',
|
|
54
|
+
);
|
|
55
|
+
expect(root).toContain('in.ndc.xyz, standardViewZ(in), in.worldPos, physicalNormal, v,');
|
|
56
|
+
expect(root).toContain('let safeIor = max(finiteScalar(material.ior, 1.5), 1.0);');
|
|
57
|
+
expect(root).toContain('transmissionBackdropTexture');
|
|
58
|
+
expect(root).toContain('let transmittedEnergy = select(');
|
|
59
|
+
}
|
|
60
|
+
expect(skin).toContain(
|
|
61
|
+
'var anisotropyStrength = finiteScalar(material.anisotropyStrength, 0.0);',
|
|
62
|
+
);
|
|
63
|
+
expect(skin).toContain('var anisotropyDirection = vec2<f32>(1.0, 0.0);');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -16,8 +16,9 @@ describe('Probe diffuse shader composition', () => {
|
|
|
16
16
|
expect(standard).toContain('#ifdef PROBE_BLEND_AVAILABLE');
|
|
17
17
|
expect(standard).toContain('@group(3) @binding(1) var<storage, read> probeBlendRecords');
|
|
18
18
|
expect(standard).not.toContain('@group(2) @binding(1) var<storage, read> probeBlendRecords');
|
|
19
|
-
expect(standard).toContain('
|
|
20
|
-
expect(skin).toContain('
|
|
19
|
+
expect(standard).toContain('fn composeProbeDiffuse(');
|
|
20
|
+
expect(skin).toContain('fn composeProbeDiffuse(');
|
|
21
|
+
expect(probe).toContain('fn evaluateProbeDiffuse(');
|
|
21
22
|
expect(skin).toContain('@group(3) @binding(1) var<storage, read> probeBlendRecords');
|
|
22
23
|
});
|
|
23
24
|
|
|
@@ -14,16 +14,15 @@ function device(): ShaderRegistryDevice {
|
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function variant(definesKey: string,
|
|
17
|
+
function variant(definesKey: string, cluster: boolean) {
|
|
18
18
|
return {
|
|
19
19
|
definesKey,
|
|
20
20
|
defines: {
|
|
21
|
-
CLUSTER_FORWARD_AVAILABLE:
|
|
21
|
+
CLUSTER_FORWARD_AVAILABLE: cluster,
|
|
22
22
|
STORAGE_BUFFER_AVAILABLE: true,
|
|
23
|
-
TRANSMISSION_AVAILABLE: transmission,
|
|
24
23
|
VERTEX_COLOR_AVAILABLE: false,
|
|
25
24
|
},
|
|
26
|
-
composedWgsl: `standard-${
|
|
25
|
+
composedWgsl: `standard-${cluster}`,
|
|
27
26
|
};
|
|
28
27
|
}
|
|
29
28
|
|
|
@@ -47,19 +46,18 @@ async function load(materialShaders: readonly unknown[]) {
|
|
|
47
46
|
|
|
48
47
|
describe('Standard transmission manifest validation', () => {
|
|
49
48
|
const falseKey =
|
|
50
|
-
'CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=true+
|
|
49
|
+
'CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=true+VERTEX_COLOR_AVAILABLE=false';
|
|
51
50
|
const trueKey =
|
|
52
|
-
'CLUSTER_FORWARD_AVAILABLE=
|
|
51
|
+
'CLUSTER_FORWARD_AVAILABLE=true+STORAGE_BUFFER_AVAILABLE=true+VERTEX_COLOR_AVAILABLE=false';
|
|
53
52
|
|
|
54
|
-
it('accepts one Standard row with
|
|
53
|
+
it('accepts one Standard row with the static capability/geometry states', async () => {
|
|
55
54
|
const result = await load([standardRow([variant(falseKey, false), variant(trueKey, true)])]);
|
|
56
55
|
expect(result.ok).toBe(true);
|
|
57
56
|
});
|
|
58
57
|
|
|
59
|
-
it('
|
|
58
|
+
it('does not require a runtime transmission axis for a Standard row', async () => {
|
|
60
59
|
const result = await load([standardRow([variant(falseKey, false)])]);
|
|
61
|
-
expect(result.ok).toBe(
|
|
62
|
-
if (!result.ok) expect(result.error.code).toBe('manifest-malformed');
|
|
60
|
+
expect(result.ok).toBe(true);
|
|
63
61
|
});
|
|
64
62
|
|
|
65
63
|
it('rejects duplicate material rows and duplicate variant keys', async () => {
|
|
@@ -22,6 +22,7 @@ import { beforeAll, describe, expect, it } from 'vitest';
|
|
|
22
22
|
import type { ShaderError } from '../errors.js';
|
|
23
23
|
import { err as errResult, ok as okResult } from '../errors.js';
|
|
24
24
|
import { TONEMAP_LUMINANCE_EPSILON } from '../index.js';
|
|
25
|
+
import { STANDARD_PIPELINE_PARAM_SCHEMA } from '../material-schemas.js';
|
|
25
26
|
import {
|
|
26
27
|
registerDefaultStandardPbrSkin,
|
|
27
28
|
type SkinCaps,
|
|
@@ -412,9 +413,45 @@ import {
|
|
|
412
413
|
it('pads ibl-prefilter PrefilterUniforms to a 16-byte block', () => {
|
|
413
414
|
const src = readSource('ibl-prefilter.wgsl');
|
|
414
415
|
expect(src).toMatch(
|
|
415
|
-
/struct\s+PrefilterUniforms\s*\{[\s\S]*?roughness:\s*f32,[\s\S]*?faceSize:\s*f32,[\s\S]*?
|
|
416
|
+
/struct\s+PrefilterUniforms\s*\{[\s\S]*?roughness:\s*f32,[\s\S]*?faceSize:\s*f32,[\s\S]*?sourceMipLevelCount:\s*f32,[\s\S]*?_pad1:\s*f32,/,
|
|
416
417
|
);
|
|
417
418
|
});
|
|
419
|
+
|
|
420
|
+
it('bounds prefilter mip selection to the allocated cube levels', () => {
|
|
421
|
+
const src = readSource('ibl-prefilter.wgsl');
|
|
422
|
+
expect(src).toContain('PREFILTER_MIP_LEVEL_COUNT');
|
|
423
|
+
expect(src).toMatch(/sourceMipLevelCount:\s*f32,/);
|
|
424
|
+
expect(src).toMatch(
|
|
425
|
+
/clamp\(\s*requestedMipLevel,\s*0\.0,\s*max\(\s*prefUniforms\.sourceMipLevelCount\s*-\s*1\.0,\s*0\.0\s*\),\s*\)/s,
|
|
426
|
+
);
|
|
427
|
+
expect(src).toMatch(/max\(4\.0\s*\*\s*HdotV,\s*0\.0001\)/);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('evaluates the real Hammersley domain without a zero GGX denominator', () => {
|
|
431
|
+
const radicalInverseVdC = (bits: number) => {
|
|
432
|
+
let value = bits >>> 0;
|
|
433
|
+
value = ((value << 16) | (value >>> 16)) >>> 0;
|
|
434
|
+
value = (((value & 0x55555555) << 1) | ((value & 0xaaaaaaaa) >>> 1)) >>> 0;
|
|
435
|
+
value = (((value & 0x33333333) << 2) | ((value & 0xcccccccc) >>> 2)) >>> 0;
|
|
436
|
+
value = (((value & 0x0f0f0f0f) << 4) | ((value & 0xf0f0f0f0) >>> 4)) >>> 0;
|
|
437
|
+
value = (((value & 0x00ff00ff) << 8) | ((value & 0xff00ff00) >>> 8)) >>> 0;
|
|
438
|
+
return value * 2.3283064365386963e-10;
|
|
439
|
+
};
|
|
440
|
+
const sampleCount = 1024;
|
|
441
|
+
const roughness = 0;
|
|
442
|
+
let maximumXiY = 0;
|
|
443
|
+
let minimumDenominator = Number.POSITIVE_INFINITY;
|
|
444
|
+
for (let index = 0; index < sampleCount; index += 1) {
|
|
445
|
+
const xiY = radicalInverseVdC(index);
|
|
446
|
+
const denominator = 1 + (roughness ** 4 - 1) * xiY;
|
|
447
|
+
maximumXiY = Math.max(maximumXiY, xiY);
|
|
448
|
+
minimumDenominator = Math.min(minimumDenominator, denominator);
|
|
449
|
+
expect(Number.isFinite(denominator)).toBe(true);
|
|
450
|
+
}
|
|
451
|
+
expect(maximumXiY).toBeLessThan(1);
|
|
452
|
+
expect(minimumDenominator).toBeGreaterThan(0);
|
|
453
|
+
expect(minimumDenominator).toBeCloseTo(1 / sampleCount, 12);
|
|
454
|
+
});
|
|
418
455
|
});
|
|
419
456
|
|
|
420
457
|
describe('t41 (a) -- ibl-sampling exports sampleIblDiffuse + sampleIblSpecular', () => {
|
|
@@ -754,8 +791,7 @@ import {
|
|
|
754
791
|
// Structural source-level assertions (same pattern as tbn-lighting-helpers.test.ts):
|
|
755
792
|
// (a) common.wgsl declares @group(3) instances binding under STORAGE_BUFFER_AVAILABLE
|
|
756
793
|
// (b) default-standard-pbr.wgsl and unlit.wgsl each carry exactly one
|
|
757
|
-
// #pragma variant_axis STORAGE_BUFFER_AVAILABLE
|
|
758
|
-
// reflection-fallback axis owned by the Standard PBR shader
|
|
794
|
+
// #pragma variant_axis STORAGE_BUFFER_AVAILABLE (no second axis)
|
|
759
795
|
// (c) No shader introduces a new variant axis like INSTANCE_STORAGE_AVAILABLE (OOS-6)
|
|
760
796
|
//
|
|
761
797
|
// RED before w3 (common.wgsl has no @group(3) yet); GREEN after w3 adds it.
|
|
@@ -813,7 +849,7 @@ import {
|
|
|
813
849
|
expect(src).toMatch(/#if\s+STORAGE_BUFFER_AVAILABLE\s*==\s*true[\s\S]*@group\(3\)/);
|
|
814
850
|
});
|
|
815
851
|
|
|
816
|
-
it('(b) default-standard-pbr.wgsl declares its complete variant-axis contract', () => {
|
|
852
|
+
it('(b) default-standard-pbr.wgsl declares its complete capability variant-axis contract', () => {
|
|
817
853
|
// feat-20260609-hdrp-cluster-fragment-ggx M1: added CLUSTER_FORWARD_AVAILABLE as a
|
|
818
854
|
// second variant axis so the standard-pbr fragment can switch between URP-style
|
|
819
855
|
// (single dirlight + ambient) and HDRP-style (cluster forward 256 punctual lights).
|
|
@@ -952,42 +988,6 @@ import {
|
|
|
952
988
|
// - R-10 paramSchema reuse assertion
|
|
953
989
|
// - plan-strategy section 5.3 critical test point register
|
|
954
990
|
|
|
955
|
-
// Inline summary of the paramSchema fields for verification — mirrors
|
|
956
|
-
// default-standard-pbr.material.json paramSchema[] field-for-field.
|
|
957
|
-
// feat-20260613 fix-issue-1 (D-8): channelMap split into 4 f32 selectors;
|
|
958
|
-
// emissive / emissiveIntensity / occlusionStrength are schema-owned values;
|
|
959
|
-
// normalScale and the engine-injection textures remain in the same contract.
|
|
960
|
-
const EXPECTED_PARAM_NAMES = [
|
|
961
|
-
'baseColor',
|
|
962
|
-
'metallic',
|
|
963
|
-
'roughness',
|
|
964
|
-
'metallicChannel',
|
|
965
|
-
'roughnessChannel',
|
|
966
|
-
'aoChannel',
|
|
967
|
-
'extraChannel',
|
|
968
|
-
'emissive',
|
|
969
|
-
'emissiveIntensity',
|
|
970
|
-
'occlusionStrength',
|
|
971
|
-
'alphaCutoff',
|
|
972
|
-
'clearcoat',
|
|
973
|
-
'clearcoatRoughness',
|
|
974
|
-
'specularTint',
|
|
975
|
-
'normalScale',
|
|
976
|
-
'transmission',
|
|
977
|
-
'ior',
|
|
978
|
-
'thickness',
|
|
979
|
-
'attenuationColor',
|
|
980
|
-
'attenuationDistance',
|
|
981
|
-
'baseColorTexture',
|
|
982
|
-
'metallicRoughnessTexture',
|
|
983
|
-
'normalTexture',
|
|
984
|
-
'specularTintTexture',
|
|
985
|
-
'emissiveTexture',
|
|
986
|
-
'occlusionTexture',
|
|
987
|
-
'transmissionTexture',
|
|
988
|
-
'thicknessTexture',
|
|
989
|
-
] as const;
|
|
990
|
-
|
|
991
991
|
const STUB_WGSL = '// stub composed pbr-skin wgsl\n';
|
|
992
992
|
const STORAGE_CAPS: SkinCaps = { storageBuffer: true };
|
|
993
993
|
|
|
@@ -1044,56 +1044,23 @@ import {
|
|
|
1044
1044
|
const registry = makeMockRegistry();
|
|
1045
1045
|
registerDefaultStandardPbrSkin(registry, STUB_WGSL, STORAGE_CAPS);
|
|
1046
1046
|
const entry = lookup(registry);
|
|
1047
|
-
expect(entry.paramSchema).toHaveLength(
|
|
1047
|
+
expect(entry.paramSchema).toHaveLength(STANDARD_PIPELINE_PARAM_SCHEMA.length);
|
|
1048
1048
|
});
|
|
1049
1049
|
|
|
1050
1050
|
it('paramSchema names match default-standard-pbr schema field-for-field', () => {
|
|
1051
1051
|
const registry = makeMockRegistry();
|
|
1052
1052
|
registerDefaultStandardPbrSkin(registry, STUB_WGSL, STORAGE_CAPS);
|
|
1053
1053
|
const entry = lookup(registry);
|
|
1054
|
-
|
|
1055
|
-
expect(names).toEqual(EXPECTED_PARAM_NAMES);
|
|
1054
|
+
expect(entry.paramSchema).toEqual(STANDARD_PIPELINE_PARAM_SCHEMA);
|
|
1056
1055
|
});
|
|
1057
1056
|
|
|
1058
1057
|
it('paramSchema types match expected value-level types', () => {
|
|
1059
1058
|
const registry = makeMockRegistry();
|
|
1060
1059
|
registerDefaultStandardPbrSkin(registry, STUB_WGSL, STORAGE_CAPS);
|
|
1061
1060
|
const entry = lookup(registry);
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
'f32',
|
|
1066
|
-
'f32',
|
|
1067
|
-
// 4 channel selectors (D-8 split).
|
|
1068
|
-
'f32',
|
|
1069
|
-
'f32',
|
|
1070
|
-
'f32',
|
|
1071
|
-
'f32',
|
|
1072
|
-
// emissive vec3 + emissiveIntensity + occlusionStrength.
|
|
1073
|
-
'vec3',
|
|
1074
|
-
'f32',
|
|
1075
|
-
'f32',
|
|
1076
|
-
// alpha cutoff + clearcoat controls.
|
|
1077
|
-
'f32',
|
|
1078
|
-
'f32',
|
|
1079
|
-
'f32',
|
|
1080
|
-
// specular tint, transmission, and attenuation controls.
|
|
1081
|
-
'vec3',
|
|
1082
|
-
'f32',
|
|
1083
|
-
'f32',
|
|
1084
|
-
'f32',
|
|
1085
|
-
'f32',
|
|
1086
|
-
'vec3',
|
|
1087
|
-
'f32',
|
|
1088
|
-
'texture2d',
|
|
1089
|
-
'texture2d',
|
|
1090
|
-
'texture2d',
|
|
1091
|
-
'texture2d',
|
|
1092
|
-
'texture2d',
|
|
1093
|
-
'texture2d',
|
|
1094
|
-
'texture2d',
|
|
1095
|
-
'texture2d',
|
|
1096
|
-
]);
|
|
1061
|
+
expect(entry.paramSchema.map((p) => p.type)).toEqual(
|
|
1062
|
+
STANDARD_PIPELINE_PARAM_SCHEMA.map((p) => p.type),
|
|
1063
|
+
);
|
|
1097
1064
|
});
|
|
1098
1065
|
});
|
|
1099
1066
|
|
|
@@ -1764,11 +1731,10 @@ import {
|
|
|
1764
1731
|
expect(src).toMatch(/applyTBN/);
|
|
1765
1732
|
});
|
|
1766
1733
|
|
|
1767
|
-
it('default-standard-pbr.wgsl imports directional lighting and the shared
|
|
1734
|
+
it('default-standard-pbr.wgsl imports directional lighting and the shared cluster owner', () => {
|
|
1768
1735
|
const src = readSource('default-standard-pbr.wgsl');
|
|
1769
1736
|
expect(src).toMatch(/#import\s+forgeax_pbr::lighting_directional::/);
|
|
1770
1737
|
expect(src).toMatch(/#import\s+forgeax_standard::cluster::/);
|
|
1771
|
-
expect(src).toMatch(/evaluateStandardClusterLights/);
|
|
1772
1738
|
});
|
|
1773
1739
|
|
|
1774
1740
|
it('default-standard-pbr.wgsl no longer defines fn evalDirectional / evalPoint / evalSpot inline', () => {
|
|
@@ -1779,7 +1745,7 @@ import {
|
|
|
1779
1745
|
expect(codeOnly).not.toMatch(/fn\s+evalPunctualBody\s*\(/);
|
|
1780
1746
|
});
|
|
1781
1747
|
|
|
1782
|
-
it('built-in PBR shaders evaluate directional CSM
|
|
1748
|
+
it('built-in PBR shaders evaluate directional CSM for base and clearcoat contributions', () => {
|
|
1783
1749
|
for (const file of ['default-standard-pbr.wgsl', 'default-standard-pbr-skin.wgsl']) {
|
|
1784
1750
|
const codeOnly = stripComments(readSource(file));
|
|
1785
1751
|
expect(codeOnly.match(/evalDirectionalShadowFactor\s*\(/g)).toHaveLength(1);
|
|
@@ -1867,10 +1833,9 @@ import {
|
|
|
1867
1833
|
// ground-truth table (research F1 section 6 - "extended Reinhard
|
|
1868
1834
|
// matches Reinhard 2002 luminance variant"). The TS port shadowed
|
|
1869
1835
|
// below is the formula manifest.
|
|
1870
|
-
// 2. TonemapParams std140 layout is 16 B (exposure + whitePoint
|
|
1871
|
-
// +
|
|
1872
|
-
// keeps the same byte layout
|
|
1873
|
-
// ditherEnabled slot in a per-pass copy.
|
|
1836
|
+
// 2. TonemapParams std140 layout is 16 B (exposure + whitePoint
|
|
1837
|
+
// + 2 padding f32 = 16 B). The shader-side struct in tonemap.wgsl
|
|
1838
|
+
// keeps the same byte layout.
|
|
1874
1839
|
// 3. TONEMAP_LUMINANCE_EPSILON is the shared TS / WGSL const
|
|
1875
1840
|
// (D-O3, 1e-5).
|
|
1876
1841
|
//
|
|
@@ -1964,10 +1929,10 @@ import {
|
|
|
1964
1929
|
});
|
|
1965
1930
|
|
|
1966
1931
|
describe('TonemapParams std140 layout', () => {
|
|
1967
|
-
it('TonemapParams stride is 16 B (exposure + whitePoint +
|
|
1932
|
+
it('TonemapParams stride is 16 B (4 f32 = exposure + whitePoint + 2 padding)', () => {
|
|
1968
1933
|
// The host writes `Float32Array` of length 4 to the UBO; the shader
|
|
1969
1934
|
// declares `struct TonemapParams { exposure: f32, whitePoint: f32,
|
|
1970
|
-
//
|
|
1935
|
+
// _pad0: f32, _pad1: f32 }` — total = 16 B.
|
|
1971
1936
|
const stride = 4 * 4;
|
|
1972
1937
|
expect(stride).toBe(16);
|
|
1973
1938
|
});
|
|
@@ -31,7 +31,8 @@ describe('Cookie RGBA Spot modifier composition', () => {
|
|
|
31
31
|
expect(modifiers).toContain('cookieSample.rgb * cookieSample.a');
|
|
32
32
|
expect(modifiers).not.toMatch(/textureSampleLevel\(cookieTexture[^\n]*\)\.r/);
|
|
33
33
|
expect(modifiers).toContain('cookieMatrices[metadata.w]');
|
|
34
|
-
expect(modifiers).toContain('metadata.w != 0xffffffffu
|
|
34
|
+
expect(modifiers).toContain('metadata.w != 0xffffffffu');
|
|
35
|
+
expect(modifiers).toContain('metadata.y == 0xffffffffu || (metadata.y & PROJECTOR_FLAG) == 0u');
|
|
35
36
|
expect(modifiers).toContain('let right = normalize(cross(forward, referenceUp));');
|
|
36
37
|
expect(modifiers).toContain('let depth = dot(outgoing, forward);');
|
|
37
38
|
expect(modifiers).toContain(
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
const standardPbrSource = readFileSync(
|
|
6
|
+
fileURLToPath(new URL('../default-standard-pbr.wgsl', import.meta.url)),
|
|
7
|
+
'utf8',
|
|
8
|
+
);
|
|
9
|
+
const standardPbrSkinSource = readFileSync(
|
|
10
|
+
fileURLToPath(new URL('../default-standard-pbr-skin.wgsl', import.meta.url)),
|
|
11
|
+
'utf8',
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
describe('standard PBR WebGL2 interface', () => {
|
|
15
|
+
it('packs the non-transmission position and view depth into one varying', () => {
|
|
16
|
+
for (const source of [standardPbrSource, standardPbrSkinSource]) {
|
|
17
|
+
expect(source).toContain('@location(7) positionOSAndViewZ : vec4<f32>');
|
|
18
|
+
expect(source).not.toContain('@location(4) positionOS : vec3<f32>');
|
|
19
|
+
expect(source).not.toContain('@location(15) positionOS : vec3<f32>');
|
|
20
|
+
expect(source).not.toContain('@location(4) @interpolate(flat) instanceIdx : u32');
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('retains the compact transmission basis locations for adapters that advertise it', () => {
|
|
25
|
+
for (const source of [standardPbrSource, standardPbrSkinSource]) {
|
|
26
|
+
expect(source).toContain('@location(4) @interpolate(flat) transmissionBasis0 : vec4<f32>');
|
|
27
|
+
expect(source).toContain('@location(13) @interpolate(flat) transmissionBasis1 : vec4<f32>');
|
|
28
|
+
expect(source).not.toContain(
|
|
29
|
+
'@location(15) @interpolate(flat) transmissionBasis1 : vec4<f32>',
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
const source = readFileSync(
|
|
6
|
+
fileURLToPath(new URL('../material/standard-physical-layer.wgsl', import.meta.url)),
|
|
7
|
+
'utf8',
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
describe('standard physical layer module', () => {
|
|
11
|
+
it('is an Engine-owned layer module after Surface evaluation', () => {
|
|
12
|
+
expect(source).toContain('standard_physical_layer');
|
|
13
|
+
expect(source).toContain('evaluate_standard_physical_layer');
|
|
14
|
+
expect(source).not.toContain('moduleSlots');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
function source(name: string): string {
|
|
6
|
+
return readFileSync(fileURLToPath(new URL(`../${name}`, import.meta.url)), 'utf8');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function entryBody(sourceText: string, entry: string): string {
|
|
10
|
+
const start = sourceText.indexOf(`fn ${entry}`);
|
|
11
|
+
const end = sourceText.indexOf('\n@fragment', start + 1);
|
|
12
|
+
return sourceText.slice(start, end < 0 ? sourceText.length : end);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('Standard Surface pass evaluation', () => {
|
|
16
|
+
it('evaluates the selected Surface once in every rigid and skinned material pass', () => {
|
|
17
|
+
for (const name of ['default-standard-pbr.wgsl', 'default-standard-pbr-skin.wgsl']) {
|
|
18
|
+
const shader = source(name);
|
|
19
|
+
// ShadowCaster is a shared `shadow_caster.wgsl` material entry, while
|
|
20
|
+
// each Standard material template owns the forward and deferred entries.
|
|
21
|
+
for (const entry of ['fs_main', 'fs_gbuffer']) {
|
|
22
|
+
const body = entryBody(shader, entry);
|
|
23
|
+
expect(body, `${name}:${entry} must exist`).toContain(`fn ${entry}`);
|
|
24
|
+
expect(body, `${name}:${entry}`).toMatch(
|
|
25
|
+
/(?:evaluateStandardSurface|evaluate_surface|forgeax_evaluate_surface_from_fragments)\s*\(/,
|
|
26
|
+
);
|
|
27
|
+
expect(body).toContain('alphaTestSurface(surface)');
|
|
28
|
+
if (entry === 'fs_main') expect(body).toContain('surface.opacity');
|
|
29
|
+
expect(body).toContain('surface.baseColor');
|
|
30
|
+
expect(body).toContain('surface.normalWS');
|
|
31
|
+
expect(body).toContain('surface.metallic');
|
|
32
|
+
expect(body).toContain('surface.roughness');
|
|
33
|
+
expect(body).toContain('surface.emissive');
|
|
34
|
+
expect(body).toContain('surface.occlusion');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
const shaderFiles = ['default-standard-pbr.wgsl', 'default-standard-pbr-skin.wgsl'] as const;
|
|
6
|
+
|
|
7
|
+
describe('Standard WebGL2 inter-stage varying budget', () => {
|
|
8
|
+
it.each(shaderFiles)('%s packs Surface position and view depth into the last varying', (file) => {
|
|
9
|
+
const source = readFileSync(resolve(import.meta.dirname, '..', file), 'utf8');
|
|
10
|
+
|
|
11
|
+
expect(source).toContain('@location(7) positionOSAndViewZ : vec4<f32>');
|
|
12
|
+
expect(source).not.toMatch(/@location\(15\)\s+positionOS\s*:/u);
|
|
13
|
+
expect(source).toContain('out.positionOSAndViewZ = vec4<f32>(in.pos, sceneViewZ(');
|
|
14
|
+
expect(source).toContain('in.positionOSAndViewZ.xyz');
|
|
15
|
+
expect(source).toContain('in.positionOSAndViewZ.w');
|
|
16
|
+
});
|
|
17
|
+
});
|