@forgeax/engine-shader 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 +14 -5
- package/dist/ShaderRegistry.d.ts.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +33042 -55
- package/dist/index.mjs.map +1 -1
- package/dist/ltc/provenance.d.ts +17 -0
- package/dist/ltc/provenance.d.ts.map +1 -0
- package/dist/ltc/tables.d.ts +22 -0
- package/dist/ltc/tables.d.ts.map +1 -0
- package/dist/material/artifact-types.d.ts +52 -0
- package/dist/material/artifact-types.d.ts.map +1 -1
- package/dist/material-schemas.d.ts +36 -0
- package/dist/material-schemas.d.ts.map +1 -1
- package/dist/register-default-sprite-lit.d.ts +3 -6
- package/dist/register-default-sprite-lit.d.ts.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/types.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 +67 -122
- package/src/__tests__/deferred-lighting-ssao.test.ts +3 -3
- package/src/__tests__/direct-light-layout.unit.test.ts +40 -0
- package/src/__tests__/ibl-irradiance.unit.test.ts +16 -4
- package/src/__tests__/ibl-sampling.unit.test.ts +3 -4
- package/src/__tests__/lighting-directional.unit.test.ts +36 -0
- package/src/__tests__/lighting-point-spot-volume.unit.test.ts +8 -2
- package/src/__tests__/lighting-punctual.unit.test.ts +40 -5
- package/src/__tests__/lighting-spot-volume-attenuation.unit.test.ts +1 -1
- package/src/__tests__/ltc-provenance.unit.test.ts +33 -0
- package/src/__tests__/manifest.fixture.json +33 -0
- package/src/__tests__/material-builtins.unit.test.ts +21 -3
- package/src/__tests__/material-contract.unit.test.ts +188 -14
- package/src/__tests__/physical-clearcoat.unit.test.ts +65 -0
- package/src/__tests__/probe-lighting-composition.unit.test.ts +39 -0
- package/src/__tests__/rect-area-brdf.unit.test.ts +36 -0
- package/src/__tests__/reflection-probe-sampling.unit.test.ts +24 -2
- package/src/__tests__/shader-manifest-transmission.unit.test.ts +8 -10
- package/src/__tests__/shader.unit.test.ts +54 -85
- package/src/__tests__/spot-cookie-composition.unit.test.ts +58 -0
- package/src/__tests__/spot-modifier-composition.unit.test.ts +19 -0
- package/src/__tests__/sprite-variants.unit.test.ts +4 -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/brdf.wgsl +115 -1
- package/src/common.wgsl +53 -79
- package/src/default-standard-pbr-skin.wgsl +687 -144
- package/src/default-standard-pbr.wgsl +600 -247
- package/src/default_standard_surface.wgsl +88 -0
- package/src/hdrp-cluster-forward.wgsl +225 -0
- package/src/ibl-irradiance.wgsl +47 -9
- package/src/ibl-prefilter.wgsl +13 -3
- package/src/ibl-sampling.wgsl +2 -2
- package/src/ibl-shared.wgsl +9 -2
- package/src/index.ts +23 -0
- package/src/lighting-directional.wgsl +9 -117
- package/src/lighting-probe.wgsl +51 -0
- package/src/lighting-punctual.wgsl +21 -11
- package/src/lighting-rect-area.wgsl +162 -0
- package/src/lighting-spot-modifiers.wgsl +100 -0
- package/src/lighting-spot-projector.wgsl +31 -0
- package/src/ltc/provenance.ts +19 -0
- package/src/ltc/tables.ts +2831 -0
- package/src/material/artifact-types.ts +137 -0
- 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 +77 -30
- package/src/register-default-sprite-lit.ts +3 -6
- package/src/register-default-standard-pbr-skin.ts +2 -2
- package/src/sprite-lit.wgsl +3 -3
- package/src/sprite.wgsl +1 -1
- package/src/standard-cluster.wgsl +80 -78
- package/src/surface_v1.wgsl +26 -0
- package/src/types.ts +2 -1
- package/src/volume/volume-inject.wgsl +34 -36
- package/src/volume/volume-integrate.wgsl +48 -38
- package/dist/.tsbuildinfo +0 -1
|
@@ -29,12 +29,11 @@ beforeAll(async () => {
|
|
|
29
29
|
describe('IBL diffuse sampling payload contract', () => {
|
|
30
30
|
it('names the sampled payload as E over pi at the canonical sampling owner', () => {
|
|
31
31
|
expect(samplingSource).toContain('irradianceEOverPi');
|
|
32
|
-
expect(samplingSource).toMatch(/irradianceEOverPi\s
|
|
32
|
+
expect(samplingSource).toMatch(/return\s+irradianceEOverPi\s*;/);
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
it('does not divide the
|
|
35
|
+
it('does not divide the already Lambert-normalized diffuse payload again', () => {
|
|
36
36
|
const diffuseBody = samplingSource.match(/fn sampleIblDiffuse\([\s\S]*?\n\}/)?.[0] ?? '';
|
|
37
|
-
expect(diffuseBody.
|
|
38
|
-
expect(diffuseBody).not.toMatch(/irradianceEOverPi\s*\/\s*PI[\s\S]*\/\s*PI/);
|
|
37
|
+
expect(diffuseBody).not.toMatch(/\/\s*PI/);
|
|
39
38
|
});
|
|
40
39
|
});
|
|
@@ -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
|
+
});
|
|
@@ -24,8 +24,14 @@ describe('shared punctual optics for volumetric fog', () => {
|
|
|
24
24
|
expect(inject).toContain('evalVolumeSpot');
|
|
25
25
|
expect(integrate).toContain('evalVolumePoint');
|
|
26
26
|
expect(integrate).toContain('evalVolumeSpot');
|
|
27
|
-
expect(inject).toContain('
|
|
28
|
-
expect(inject).toContain('
|
|
27
|
+
expect(inject).toContain('light_data');
|
|
28
|
+
expect(inject).toContain('cluster_uniform');
|
|
29
|
+
expect(inject).not.toContain('pointLightsBuffer');
|
|
30
|
+
expect(inject).not.toContain('spotLightsBuffer');
|
|
31
|
+
expect(integrate).toContain('light_data');
|
|
32
|
+
expect(integrate).toContain('cluster_uniform');
|
|
33
|
+
expect(integrate).not.toContain('pointLightsBuffer');
|
|
34
|
+
expect(integrate).not.toContain('spotLightsBuffer');
|
|
29
35
|
});
|
|
30
36
|
|
|
31
37
|
it('keeps volume View aligned with the directional filter carrier', () => {
|
|
@@ -8,7 +8,12 @@ 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');
|
|
16
|
+
const brdfShaderPath = join(dirname(fileURLToPath(import.meta.url)), '../brdf.wgsl');
|
|
12
17
|
const directionalShaderPath = join(
|
|
13
18
|
dirname(fileURLToPath(import.meta.url)),
|
|
14
19
|
'../lighting-directional.wgsl',
|
|
@@ -42,10 +47,30 @@ describe('direct punctual lighting shader contract', () => {
|
|
|
42
47
|
expect(source).not.toContain('normalize(lightDir)');
|
|
43
48
|
});
|
|
44
49
|
|
|
50
|
+
it('shares Three r184 direct-PBR Fresnel and multiscatter ownership', async () => {
|
|
51
|
+
const [source, brdf, directional] = await Promise.all([
|
|
52
|
+
readFile(shaderPath, 'utf8'),
|
|
53
|
+
readFile(brdfShaderPath, 'utf8'),
|
|
54
|
+
readFile(directionalShaderPath, 'utf8'),
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
expect(brdf).toContain('exp2((-5.55473 * vDotH - 6.98316) * vDotH)');
|
|
58
|
+
expect(brdf).toContain('fn threeR184DirectMultiScatter(');
|
|
59
|
+
expect(brdf).toContain('energyLoss * favg * favg + vec3<f32>(1e-6)');
|
|
60
|
+
expect(brdf).not.toContain('energyLoss * favg + vec3<f32>(1e-6)');
|
|
61
|
+
expect(source).toContain('threeR184DirectMultiScatter(roughness, nDotV, nDotL, F0)');
|
|
62
|
+
expect(source).toContain('let diffuse = (1.0 - metallic) * baseColor / 3.14159265;');
|
|
63
|
+
expect(source).toContain('return factor * factor / dSquared;');
|
|
64
|
+
expect(source).not.toContain('let kd = (vec3<f32>(1.0) - f) * (1.0 - metallic);');
|
|
65
|
+
expect(directional).toContain('threeR184DirectMultiScatter');
|
|
66
|
+
expect(directional).not.toContain('THREE_R184_DFG_LUT');
|
|
67
|
+
});
|
|
68
|
+
|
|
45
69
|
it('keeps cluster punctual evaluation in the shared lighting owner', async () => {
|
|
46
70
|
const source = await readFile(standardClusterShaderPath, 'utf8');
|
|
47
71
|
|
|
48
|
-
expect(source).toContain('
|
|
72
|
+
expect(source).toContain('DirectLightSlot');
|
|
73
|
+
expect(source).toContain('light.metadata.x');
|
|
49
74
|
expect(source).toContain('evalPoint');
|
|
50
75
|
expect(source).toContain('evalSpot');
|
|
51
76
|
expect(source).toContain('evalSpotShadowed');
|
|
@@ -57,23 +82,33 @@ describe('direct punctual lighting shader contract', () => {
|
|
|
57
82
|
it('decodes HDRP spot cone lanes in the shared evaluator order', async () => {
|
|
58
83
|
const source = await readFile(standardClusterShaderPath, 'utf8');
|
|
59
84
|
|
|
60
|
-
expect(source).toContain('light.
|
|
85
|
+
expect(source).toContain('light.colorTimesIntensity.w, light.direction.w, light.position.w');
|
|
61
86
|
expect(source).not.toContain('light.direction.w, light.color.w, light.position.w');
|
|
62
87
|
});
|
|
63
88
|
|
|
64
89
|
it('keeps the unshadowed spot sentinel out of the projector flag branch', async () => {
|
|
65
|
-
const source = await
|
|
90
|
+
const [source, projector] = await Promise.all([
|
|
91
|
+
readFile(standardClusterShaderPath, 'utf8'),
|
|
92
|
+
readFile(spotProjectorShaderPath, 'utf8'),
|
|
93
|
+
]);
|
|
66
94
|
|
|
67
95
|
expect(source).toContain('if (encoded_tile >= 0 && (encoded_tile & PROJECTOR_FLAG) != 0)');
|
|
96
|
+
expect(source).toContain(
|
|
97
|
+
'sampleStandardSpotProjector(view.spotLightViewProj[tile], world_pos, light.metadata)',
|
|
98
|
+
);
|
|
99
|
+
expect(projector).toContain(
|
|
100
|
+
'textureSampleLevel(cookieTexture, spotModifierSampler, uv, metadata.w, 0.0).rgb',
|
|
101
|
+
);
|
|
68
102
|
});
|
|
69
103
|
|
|
70
104
|
it('passes precomputed base and clearcoat roughness/F0 facts to cluster lights', async () => {
|
|
71
105
|
const source = await readFile(pbrShaderPath, 'utf8');
|
|
72
106
|
|
|
73
107
|
expect(source).toMatch(
|
|
74
|
-
/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,
|
|
75
109
|
);
|
|
76
|
-
expect(source).toContain('
|
|
110
|
+
expect(source).toContain('clearcoatAlpha,');
|
|
111
|
+
expect(source).toContain('vec3<f32>(0.04),');
|
|
77
112
|
});
|
|
78
113
|
|
|
79
114
|
it('requires one shared Directional PCSS orchestration owner', async () => {
|
|
@@ -14,7 +14,7 @@ describe('volume Spot attenuation precision', () => {
|
|
|
14
14
|
expect(volumeInject).toContain('spot_shadow_visibility_at');
|
|
15
15
|
expect(volumeIntegrate).toContain('evalSpotAttenuation');
|
|
16
16
|
expect(volumeIntegrate).toContain('spot_attenuation_at');
|
|
17
|
-
expect(volumeIntegrate).toContain('light.
|
|
17
|
+
expect(volumeIntegrate).toContain('light.position.w');
|
|
18
18
|
// The paired Point+Spot owner applies visibility only to the Spot term;
|
|
19
19
|
// the Point radiance remains independent of the spot shadow. Keep the
|
|
20
20
|
// assertion at the semantic call boundary so WGSL formatting changes do
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
generateLtcTables,
|
|
4
|
+
hashLtcTable,
|
|
5
|
+
LTC_SOURCE_PROVENANCE,
|
|
6
|
+
LTC_TABLE_HASHES,
|
|
7
|
+
LTC_TABLE_HEIGHT,
|
|
8
|
+
LTC_TABLE_WIDTH,
|
|
9
|
+
LTC_TABLES,
|
|
10
|
+
} from '../ltc/tables';
|
|
11
|
+
|
|
12
|
+
describe('LTC provenance and tracked data contract', () => {
|
|
13
|
+
it('pins the fixed Three.js reference and two 64 by 64 rgba16float tables', () => {
|
|
14
|
+
expect(LTC_SOURCE_PROVENANCE.source).toBe('Three.js');
|
|
15
|
+
expect(LTC_SOURCE_PROVENANCE.revision).toBe('r184');
|
|
16
|
+
expect(LTC_SOURCE_PROVENANCE.generator).toContain('deterministic');
|
|
17
|
+
expect({ width: LTC_TABLE_WIDTH, height: LTC_TABLE_HEIGHT }).toEqual({ width: 64, height: 64 });
|
|
18
|
+
expect(LTC_TABLES.lambert.byteLength).toBe(64 * 64 * 4 * 2);
|
|
19
|
+
expect(LTC_TABLES.ggx.byteLength).toBe(64 * 64 * 4 * 2);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('keeps generation deterministic and hashes the tracked outputs', () => {
|
|
23
|
+
const first = generateLtcTables();
|
|
24
|
+
const second = generateLtcTables();
|
|
25
|
+
|
|
26
|
+
expect(first).toEqual(second);
|
|
27
|
+
expect(first).toEqual(LTC_TABLES);
|
|
28
|
+
expect(hashLtcTable(first.lambert)).toBe(LTC_TABLE_HASHES.lambert);
|
|
29
|
+
expect(hashLtcTable(first.ggx)).toBe(LTC_TABLE_HASHES.ggx);
|
|
30
|
+
expect(LTC_TABLE_HASHES.lambert).toMatch(/^[a-f0-9]{64}$/);
|
|
31
|
+
expect(LTC_TABLE_HASHES.ggx).toMatch(/^[a-f0-9]{64}$/);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -18,5 +18,38 @@
|
|
|
18
18
|
"glsl": null,
|
|
19
19
|
"bindings": "[]"
|
|
20
20
|
}
|
|
21
|
+
],
|
|
22
|
+
"materialShaders": [
|
|
23
|
+
{
|
|
24
|
+
"identifier": "forgeax::default-standard-pbr",
|
|
25
|
+
"sourcePath": "default-standard-pbr.wgsl",
|
|
26
|
+
"composedWgsl": "@vertex fn vs_main() -> @builtin(position) vec4<f32> { return vec4<f32>(0.0); }",
|
|
27
|
+
"paramSchema": "[]",
|
|
28
|
+
"variants": [],
|
|
29
|
+
"directEntry": "vs_main",
|
|
30
|
+
"sceneIndexEntry": "vs_scene_index",
|
|
31
|
+
"materialRow": { "byteLength": 384, "fields": ["baseColor", "alphaCutoff"] },
|
|
32
|
+
"resourceSlots": [],
|
|
33
|
+
"uvSets": [],
|
|
34
|
+
"vertexInputs": [
|
|
35
|
+
{ "semantic": "position", "location": 0, "format": "float32x3" },
|
|
36
|
+
{ "semantic": "normal", "location": 1, "format": "float32x3" },
|
|
37
|
+
{ "semantic": "uv", "location": 2, "format": "float32x2" },
|
|
38
|
+
{ "semantic": "tangent", "location": 3, "format": "float32x4" }
|
|
39
|
+
],
|
|
40
|
+
"alphaMask": { "cutoff": "alphaCutoff", "source": "baseColor.a" },
|
|
41
|
+
"reflection": {
|
|
42
|
+
"layoutIdentity": "standard-pbr/material-row-v1",
|
|
43
|
+
"resourceSlots": [],
|
|
44
|
+
"vertexInputs": [
|
|
45
|
+
{ "semantic": "position", "location": 0, "format": "float32x3" },
|
|
46
|
+
{ "semantic": "normal", "location": 1, "format": "float32x3" },
|
|
47
|
+
{ "semantic": "uv", "location": 2, "format": "float32x2" },
|
|
48
|
+
{ "semantic": "tangent", "location": 3, "format": "float32x4" }
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
"receiptIdentity": "standard-pbr/material-row-v1",
|
|
52
|
+
"generation": 1
|
|
53
|
+
}
|
|
21
54
|
]
|
|
22
55
|
}
|
|
@@ -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', () => {
|
|
@@ -102,7 +102,11 @@ describe('standard PBR transmission material contract', () => {
|
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
it('keeps the seven transmission inputs in the reflected UBO and coordinate contract', {
|
|
105
|
-
|
|
105
|
+
// This test intentionally invokes the full engine shader manifest
|
|
106
|
+
// producer. Keep its budget local to the producer-backed contract so a
|
|
107
|
+
// concurrent coverage child cannot turn host CPU contention into a false
|
|
108
|
+
// product failure.
|
|
109
|
+
timeout: 60_000,
|
|
106
110
|
}, async () => {
|
|
107
111
|
const derived = derive(DEFAULT_STANDARD_PBR_PARAM_SCHEMA);
|
|
108
112
|
const transmissionNames = [
|
|
@@ -146,6 +150,114 @@ describe('standard PBR transmission material contract', () => {
|
|
|
146
150
|
kind: 'texture',
|
|
147
151
|
binding: 16,
|
|
148
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
|
+
},
|
|
149
261
|
]);
|
|
150
262
|
|
|
151
263
|
const { buildEngineShaderManifest } = await import('@forgeax/engine-vite-plugin-shader');
|
|
@@ -155,27 +267,89 @@ describe('standard PBR transmission material contract', () => {
|
|
|
155
267
|
);
|
|
156
268
|
expect(material).toBeDefined();
|
|
157
269
|
if (material === undefined) return;
|
|
158
|
-
const
|
|
270
|
+
const baseVariant = material.variants.find(
|
|
159
271
|
(variant) =>
|
|
160
272
|
variant.defines.CLUSTER_FORWARD_AVAILABLE === false &&
|
|
161
273
|
variant.defines.STORAGE_BUFFER_AVAILABLE === true &&
|
|
162
|
-
variant.defines.TRANSMISSION_AVAILABLE ===
|
|
274
|
+
variant.defines.TRANSMISSION_AVAILABLE === false &&
|
|
163
275
|
variant.defines.VERTEX_COLOR_AVAILABLE === false,
|
|
164
276
|
);
|
|
165
|
-
expect(
|
|
166
|
-
if (
|
|
167
|
-
expect(derived.totalBytes).toBe(
|
|
168
|
-
|
|
169
|
-
|
|
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,
|
|
170
286
|
);
|
|
171
|
-
expect(
|
|
172
|
-
/@group\(1\)\s*@binding\(14\)\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,
|
|
173
289
|
);
|
|
174
|
-
expect(
|
|
175
|
-
/@group\(1\)\s*@binding\(15\)\s*var
|
|
290
|
+
expect(baseVariant.composedWgsl).toMatch(
|
|
291
|
+
/@group\(1\)\s*@binding\(15\)\s*\n?\s*var\s+irradianceMap/u,
|
|
176
292
|
);
|
|
177
|
-
|
|
178
|
-
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('publishes one receipt for direct and scene-index consumers', {
|
|
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,
|
|
302
|
+
}, async () => {
|
|
303
|
+
const { buildEngineShaderManifest } = await import('@forgeax/engine-vite-plugin-shader');
|
|
304
|
+
const manifest = await buildEngineShaderManifest();
|
|
305
|
+
const standard = manifest.materialShaders.find(
|
|
306
|
+
(entry) => entry.identifier === 'forgeax::default-standard-pbr',
|
|
307
|
+
);
|
|
308
|
+
const skin = manifest.materialShaders.find((entry) => entry.identifier === 'forgeax::pbr-skin');
|
|
309
|
+
expect(standard).toBeDefined();
|
|
310
|
+
expect(skin).toBeDefined();
|
|
311
|
+
expect(standard).toMatchObject({
|
|
312
|
+
directEntry: expect.any(String),
|
|
313
|
+
sceneIndexEntry: expect.any(String),
|
|
314
|
+
materialRow: expect.any(Object),
|
|
315
|
+
resourceSlots: expect.any(Array),
|
|
316
|
+
uvSets: expect.any(Array),
|
|
317
|
+
vertexInputs: expect.any(Array),
|
|
318
|
+
alphaMask: expect.any(Object),
|
|
319
|
+
reflection: expect.any(Object),
|
|
320
|
+
});
|
|
321
|
+
expect(skin?.receiptIdentity).toBe(standard?.receiptIdentity);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('composes the scene-index entry into the same validated PBR artifact as the direct entry', {
|
|
325
|
+
timeout: 60_000,
|
|
326
|
+
}, async () => {
|
|
327
|
+
const { buildEngineShaderManifest } = await import('@forgeax/engine-vite-plugin-shader');
|
|
328
|
+
const manifest = await buildEngineShaderManifest();
|
|
329
|
+
const standard = manifest.materialShaders.find(
|
|
330
|
+
(entry) => entry.identifier === 'forgeax::default-standard-pbr',
|
|
331
|
+
);
|
|
332
|
+
expect(standard).toBeDefined();
|
|
333
|
+
if (standard === undefined) return;
|
|
334
|
+
expect(standard.composedWgsl).toMatch(/@vertex\s+fn\s+vs_main\s*\(/);
|
|
335
|
+
expect(standard.composedWgsl).toMatch(/@vertex\s+fn\s+vs_scene_index\s*\(/);
|
|
336
|
+
expect(standard.composedWgsl).toMatch(/@fragment\s+fn\s+fs_main\s*\(/);
|
|
337
|
+
expect(standard.sceneIndexEntry).toBe('vs_scene_index');
|
|
338
|
+
expect(standard.reflection?.layoutIdentity).toBe(standard.receiptIdentity);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('rejects a receipt that loses a required prepared ABI field', {
|
|
342
|
+
timeout: 60_000,
|
|
343
|
+
}, async () => {
|
|
344
|
+
const { buildEngineShaderManifest } = await import('@forgeax/engine-vite-plugin-shader');
|
|
345
|
+
const manifest = await buildEngineShaderManifest();
|
|
346
|
+
const standard = manifest.materialShaders.find(
|
|
347
|
+
(entry) => entry.identifier === 'forgeax::default-standard-pbr',
|
|
179
348
|
);
|
|
349
|
+
expect(standard).toBeDefined();
|
|
350
|
+
if (standard === undefined) return;
|
|
351
|
+
const malformed = { ...standard } as Record<string, unknown>;
|
|
352
|
+
delete malformed.reflection;
|
|
353
|
+
expect(malformed).not.toHaveProperty('reflection');
|
|
180
354
|
});
|
|
181
355
|
});
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
const shaderRoot = resolve(import.meta.dirname, '..');
|
|
6
|
+
const probe = readFileSync(resolve(shaderRoot, 'lighting-probe.wgsl'), 'utf8');
|
|
7
|
+
const standard = readFileSync(resolve(shaderRoot, 'default-standard-pbr.wgsl'), 'utf8');
|
|
8
|
+
const skin = readFileSync(resolve(shaderRoot, 'default-standard-pbr-skin.wgsl'), 'utf8');
|
|
9
|
+
|
|
10
|
+
describe('Probe diffuse shader composition', () => {
|
|
11
|
+
it('evaluates one SH9 record and derives Sky as the residual', () => {
|
|
12
|
+
expect(probe).toContain('array<vec4<f32>, 9>');
|
|
13
|
+
expect(probe).toContain('sh9');
|
|
14
|
+
expect(probe).toContain('1.0 - localBlendFraction');
|
|
15
|
+
expect(probe).toContain('max(');
|
|
16
|
+
expect(standard).toContain('#ifdef PROBE_BLEND_AVAILABLE');
|
|
17
|
+
expect(standard).toContain('@group(3) @binding(1) var<storage, read> probeBlendRecords');
|
|
18
|
+
expect(standard).not.toContain('@group(2) @binding(1) var<storage, read> probeBlendRecords');
|
|
19
|
+
expect(standard).toContain('fn composeProbeDiffuse(');
|
|
20
|
+
expect(skin).toContain('fn composeProbeDiffuse(');
|
|
21
|
+
expect(probe).toContain('fn evaluateProbeDiffuse(');
|
|
22
|
+
expect(skin).toContain('@group(3) @binding(1) var<storage, read> probeBlendRecords');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('keeps Sky out of probe admission and preserves the existing specular owner', () => {
|
|
26
|
+
expect(probe).not.toContain('q_sky');
|
|
27
|
+
expect(probe).not.toContain('for (var probe');
|
|
28
|
+
expect(standard).toContain('sampleIblSpecular');
|
|
29
|
+
expect(skin).toContain('sampleIblSpecular');
|
|
30
|
+
expect(standard).not.toContain('sampleProbeSpecular');
|
|
31
|
+
expect(skin).not.toContain('sampleProbeSpecular');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('suppresses probe diffuse for metal while retaining Skylight IBL inputs', () => {
|
|
35
|
+
expect(probe).toContain('(1.0 - metallic)');
|
|
36
|
+
expect(standard).toContain('sampleIblDiffuse');
|
|
37
|
+
expect(skin).toContain('sampleIblDiffuse');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('RectAreaLight LTC BRDF shader contract', () => {
|
|
5
|
+
it('provides one-sided Lambert and GGX LTC polygon evaluators', async () => {
|
|
6
|
+
const source = await readFile(new URL('../lighting-rect-area.wgsl', import.meta.url), 'utf8');
|
|
7
|
+
|
|
8
|
+
expect(source).toContain('fn evalRectAreaLtcLambert(');
|
|
9
|
+
expect(source).toContain('fn evalRectAreaLtcGgx(');
|
|
10
|
+
expect(source).toContain('fn ltcUv(');
|
|
11
|
+
expect(source).toContain('fn ltcEdgeVectorFormFactor(');
|
|
12
|
+
expect(source).toContain('fn ltcClippedSphereFormFactor(');
|
|
13
|
+
expect(source).toContain('fn ltcEvaluate(');
|
|
14
|
+
expect(source).toContain('ltcLambertTexture');
|
|
15
|
+
expect(source).toContain('ltcGgxTexture');
|
|
16
|
+
expect(source).not.toContain('LTC_LAMBERT');
|
|
17
|
+
expect(source).not.toContain('LTC_GGX');
|
|
18
|
+
expect(source).toContain('let rect0 = lightPos - halfX - halfY;');
|
|
19
|
+
expect(source).toContain(
|
|
20
|
+
'let transform = mInv * transpose(mat3x3<f32>(tangent, bitangent, n));',
|
|
21
|
+
);
|
|
22
|
+
expect(source).toContain('sqrt(1.0 - nDotV)');
|
|
23
|
+
expect(source).toContain('let diffuse = baseColor * (1.0 - metallic);');
|
|
24
|
+
expect(source).not.toContain('viewWeight');
|
|
25
|
+
expect(source).not.toContain('rectAreaSolidAngle');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('does not route Rect through punctual inverse-square or shadow sampling', async () => {
|
|
29
|
+
const source = await readFile(new URL('../lighting-rect-area.wgsl', import.meta.url), 'utf8');
|
|
30
|
+
|
|
31
|
+
expect(source).not.toContain('/ distanceSquared');
|
|
32
|
+
expect(source).not.toContain('shadowAtlas');
|
|
33
|
+
expect(source).not.toContain('evalPoint(');
|
|
34
|
+
expect(source).not.toContain('evalSpot(');
|
|
35
|
+
});
|
|
36
|
+
});
|