@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
|
@@ -15,9 +15,31 @@ describe('Standard reflection probe sampling contract', () => {
|
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
it('uses box projection and explicit Skylight fallback in the Standard shader', () => {
|
|
18
|
-
expect(
|
|
19
|
-
expect(standardSource).toMatch(/
|
|
18
|
+
expect(samplingSource).toMatch(/box_project/);
|
|
19
|
+
expect(standardSource).toMatch(/sampleReflectionProbeSpecular/);
|
|
20
|
+
expect(standardSource).toContain('skylight.intensity < 0.0');
|
|
20
21
|
expect(standardSource).toMatch(/sampleIblSpecular/);
|
|
21
22
|
expect(standardSource).toMatch(/skylight/);
|
|
22
23
|
});
|
|
24
|
+
|
|
25
|
+
it('projects only the environment specular lobe for c=1 without removing diffuse light', () => {
|
|
26
|
+
expect(standardSource).toContain(
|
|
27
|
+
'var reflectionFallback = specularIbl * (vec3<f32>(1.0) - coatF);',
|
|
28
|
+
);
|
|
29
|
+
expect(standardSource).toContain('kD * irradiance * diffuseAlbedo');
|
|
30
|
+
expect(standardSource).toContain(
|
|
31
|
+
'output.reflectionFallback = vec4<f32>(reflectionFallback, 1.0);',
|
|
32
|
+
);
|
|
33
|
+
expect(standardSource).not.toContain('output.reflectionFallback = vec4<f32>(ambient, 1.0);');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('keeps probe clearcoat on the selected probe source', () => {
|
|
37
|
+
expect(standardSource).toMatch(
|
|
38
|
+
/if \(skylight\.intensity < 0\.0\) \{[\s\S]*?clearcoatIbl = sampleReflectionProbeSpecular\(/,
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('keeps zero-intensity probes distinguishable from an absent Skylight', () => {
|
|
43
|
+
expect(standardSource).toContain('max(-skylight.intensity - 1.0, 0.0) * ao');
|
|
44
|
+
});
|
|
23
45
|
});
|
|
@@ -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', () => {
|
|
@@ -812,7 +849,7 @@ import {
|
|
|
812
849
|
expect(src).toMatch(/#if\s+STORAGE_BUFFER_AVAILABLE\s*==\s*true[\s\S]*@group\(3\)/);
|
|
813
850
|
});
|
|
814
851
|
|
|
815
|
-
it('(b) default-standard-pbr.wgsl declares
|
|
852
|
+
it('(b) default-standard-pbr.wgsl declares its complete capability variant-axis contract', () => {
|
|
816
853
|
// feat-20260609-hdrp-cluster-fragment-ggx M1: added CLUSTER_FORWARD_AVAILABLE as a
|
|
817
854
|
// second variant axis so the standard-pbr fragment can switch between URP-style
|
|
818
855
|
// (single dirlight + ambient) and HDRP-style (cluster forward 256 punctual lights).
|
|
@@ -822,9 +859,12 @@ import {
|
|
|
822
859
|
'#pragma variant_axis STORAGE_BUFFER_AVAILABLE',
|
|
823
860
|
'#pragma variant_axis CLUSTER_FORWARD_AVAILABLE',
|
|
824
861
|
'#pragma variant_axis VERTEX_COLOR_AVAILABLE',
|
|
862
|
+
'#pragma variant_axis PROBE_BLEND_AVAILABLE',
|
|
863
|
+
'#pragma variant_axis EXTENDED_LIGHTING_AVAILABLE',
|
|
825
864
|
'#pragma variant_axis TRANSMISSION_AVAILABLE',
|
|
826
865
|
'#pragma variant_axis DIRECTIONAL_PCSS_AVAILABLE',
|
|
827
866
|
'#pragma variant_axis PROJECTOR_AVAILABLE',
|
|
867
|
+
'#pragma variant_axis REFLECTION_FALLBACK_AVAILABLE',
|
|
828
868
|
]);
|
|
829
869
|
});
|
|
830
870
|
|
|
@@ -948,42 +988,6 @@ import {
|
|
|
948
988
|
// - R-10 paramSchema reuse assertion
|
|
949
989
|
// - plan-strategy section 5.3 critical test point register
|
|
950
990
|
|
|
951
|
-
// Inline summary of the paramSchema fields for verification — mirrors
|
|
952
|
-
// default-standard-pbr.material.json paramSchema[] field-for-field.
|
|
953
|
-
// feat-20260613 fix-issue-1 (D-8): channelMap split into 4 f32 selectors;
|
|
954
|
-
// emissive / emissiveIntensity / occlusionStrength are schema-owned values;
|
|
955
|
-
// normalScale and the engine-injection textures remain in the same contract.
|
|
956
|
-
const EXPECTED_PARAM_NAMES = [
|
|
957
|
-
'baseColor',
|
|
958
|
-
'metallic',
|
|
959
|
-
'roughness',
|
|
960
|
-
'metallicChannel',
|
|
961
|
-
'roughnessChannel',
|
|
962
|
-
'aoChannel',
|
|
963
|
-
'extraChannel',
|
|
964
|
-
'emissive',
|
|
965
|
-
'emissiveIntensity',
|
|
966
|
-
'occlusionStrength',
|
|
967
|
-
'alphaCutoff',
|
|
968
|
-
'clearcoat',
|
|
969
|
-
'clearcoatRoughness',
|
|
970
|
-
'specularTint',
|
|
971
|
-
'normalScale',
|
|
972
|
-
'transmission',
|
|
973
|
-
'ior',
|
|
974
|
-
'thickness',
|
|
975
|
-
'attenuationColor',
|
|
976
|
-
'attenuationDistance',
|
|
977
|
-
'baseColorTexture',
|
|
978
|
-
'metallicRoughnessTexture',
|
|
979
|
-
'normalTexture',
|
|
980
|
-
'specularTintTexture',
|
|
981
|
-
'emissiveTexture',
|
|
982
|
-
'occlusionTexture',
|
|
983
|
-
'transmissionTexture',
|
|
984
|
-
'thicknessTexture',
|
|
985
|
-
] as const;
|
|
986
|
-
|
|
987
991
|
const STUB_WGSL = '// stub composed pbr-skin wgsl\n';
|
|
988
992
|
const STORAGE_CAPS: SkinCaps = { storageBuffer: true };
|
|
989
993
|
|
|
@@ -1040,56 +1044,23 @@ import {
|
|
|
1040
1044
|
const registry = makeMockRegistry();
|
|
1041
1045
|
registerDefaultStandardPbrSkin(registry, STUB_WGSL, STORAGE_CAPS);
|
|
1042
1046
|
const entry = lookup(registry);
|
|
1043
|
-
expect(entry.paramSchema).toHaveLength(
|
|
1047
|
+
expect(entry.paramSchema).toHaveLength(STANDARD_PIPELINE_PARAM_SCHEMA.length);
|
|
1044
1048
|
});
|
|
1045
1049
|
|
|
1046
1050
|
it('paramSchema names match default-standard-pbr schema field-for-field', () => {
|
|
1047
1051
|
const registry = makeMockRegistry();
|
|
1048
1052
|
registerDefaultStandardPbrSkin(registry, STUB_WGSL, STORAGE_CAPS);
|
|
1049
1053
|
const entry = lookup(registry);
|
|
1050
|
-
|
|
1051
|
-
expect(names).toEqual(EXPECTED_PARAM_NAMES);
|
|
1054
|
+
expect(entry.paramSchema).toEqual(STANDARD_PIPELINE_PARAM_SCHEMA);
|
|
1052
1055
|
});
|
|
1053
1056
|
|
|
1054
1057
|
it('paramSchema types match expected value-level types', () => {
|
|
1055
1058
|
const registry = makeMockRegistry();
|
|
1056
1059
|
registerDefaultStandardPbrSkin(registry, STUB_WGSL, STORAGE_CAPS);
|
|
1057
1060
|
const entry = lookup(registry);
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
'f32',
|
|
1062
|
-
'f32',
|
|
1063
|
-
// 4 channel selectors (D-8 split).
|
|
1064
|
-
'f32',
|
|
1065
|
-
'f32',
|
|
1066
|
-
'f32',
|
|
1067
|
-
'f32',
|
|
1068
|
-
// emissive vec3 + emissiveIntensity + occlusionStrength.
|
|
1069
|
-
'vec3',
|
|
1070
|
-
'f32',
|
|
1071
|
-
'f32',
|
|
1072
|
-
// alpha cutoff + clearcoat controls.
|
|
1073
|
-
'f32',
|
|
1074
|
-
'f32',
|
|
1075
|
-
'f32',
|
|
1076
|
-
// specular tint, transmission, and attenuation controls.
|
|
1077
|
-
'vec3',
|
|
1078
|
-
'f32',
|
|
1079
|
-
'f32',
|
|
1080
|
-
'f32',
|
|
1081
|
-
'f32',
|
|
1082
|
-
'vec3',
|
|
1083
|
-
'f32',
|
|
1084
|
-
'texture2d',
|
|
1085
|
-
'texture2d',
|
|
1086
|
-
'texture2d',
|
|
1087
|
-
'texture2d',
|
|
1088
|
-
'texture2d',
|
|
1089
|
-
'texture2d',
|
|
1090
|
-
'texture2d',
|
|
1091
|
-
'texture2d',
|
|
1092
|
-
]);
|
|
1061
|
+
expect(entry.paramSchema.map((p) => p.type)).toEqual(
|
|
1062
|
+
STANDARD_PIPELINE_PARAM_SCHEMA.map((p) => p.type),
|
|
1063
|
+
);
|
|
1093
1064
|
});
|
|
1094
1065
|
});
|
|
1095
1066
|
|
|
@@ -1760,11 +1731,10 @@ import {
|
|
|
1760
1731
|
expect(src).toMatch(/applyTBN/);
|
|
1761
1732
|
});
|
|
1762
1733
|
|
|
1763
|
-
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', () => {
|
|
1764
1735
|
const src = readSource('default-standard-pbr.wgsl');
|
|
1765
1736
|
expect(src).toMatch(/#import\s+forgeax_pbr::lighting_directional::/);
|
|
1766
1737
|
expect(src).toMatch(/#import\s+forgeax_standard::cluster::/);
|
|
1767
|
-
expect(src).toMatch(/evaluateStandardClusterLights/);
|
|
1768
1738
|
});
|
|
1769
1739
|
|
|
1770
1740
|
it('default-standard-pbr.wgsl no longer defines fn evalDirectional / evalPoint / evalSpot inline', () => {
|
|
@@ -1775,7 +1745,7 @@ import {
|
|
|
1775
1745
|
expect(codeOnly).not.toMatch(/fn\s+evalPunctualBody\s*\(/);
|
|
1776
1746
|
});
|
|
1777
1747
|
|
|
1778
|
-
it('built-in PBR shaders evaluate directional CSM
|
|
1748
|
+
it('built-in PBR shaders evaluate directional CSM for base and clearcoat contributions', () => {
|
|
1779
1749
|
for (const file of ['default-standard-pbr.wgsl', 'default-standard-pbr-skin.wgsl']) {
|
|
1780
1750
|
const codeOnly = stripComments(readSource(file));
|
|
1781
1751
|
expect(codeOnly.match(/evalDirectionalShadowFactor\s*\(/g)).toHaveLength(1);
|
|
@@ -1863,10 +1833,9 @@ import {
|
|
|
1863
1833
|
// ground-truth table (research F1 section 6 - "extended Reinhard
|
|
1864
1834
|
// matches Reinhard 2002 luminance variant"). The TS port shadowed
|
|
1865
1835
|
// below is the formula manifest.
|
|
1866
|
-
// 2. TonemapParams std140 layout is 16 B (exposure + whitePoint
|
|
1867
|
-
// +
|
|
1868
|
-
// keeps the same byte layout
|
|
1869
|
-
// 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.
|
|
1870
1839
|
// 3. TONEMAP_LUMINANCE_EPSILON is the shared TS / WGSL const
|
|
1871
1840
|
// (D-O3, 1e-5).
|
|
1872
1841
|
//
|
|
@@ -1960,10 +1929,10 @@ import {
|
|
|
1960
1929
|
});
|
|
1961
1930
|
|
|
1962
1931
|
describe('TonemapParams std140 layout', () => {
|
|
1963
|
-
it('TonemapParams stride is 16 B (exposure + whitePoint +
|
|
1932
|
+
it('TonemapParams stride is 16 B (4 f32 = exposure + whitePoint + 2 padding)', () => {
|
|
1964
1933
|
// The host writes `Float32Array` of length 4 to the UBO; the shader
|
|
1965
1934
|
// declares `struct TonemapParams { exposure: f32, whitePoint: f32,
|
|
1966
|
-
//
|
|
1935
|
+
// _pad0: f32, _pad1: f32 }` — total = 16 B.
|
|
1967
1936
|
const stride = 4 * 4;
|
|
1968
1937
|
expect(stride).toBe(16);
|
|
1969
1938
|
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
async function readShader(name: string): Promise<string> {
|
|
5
|
+
return readFile(new URL(`../${name}`, import.meta.url), 'utf8');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function extractSpotModifierFactors(source: string): string {
|
|
9
|
+
const start = source.indexOf('fn spotModifierFactors(');
|
|
10
|
+
const end = source.indexOf('\nfn evalSpotWithModifiers', start);
|
|
11
|
+
return source.slice(start, end).replace(/\s+/g, '');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('Cookie RGBA Spot modifier composition', () => {
|
|
15
|
+
it('rejects scalar Cookie sampling and applies linear RGB times alpha', async () => {
|
|
16
|
+
const [standard, skin, modifiers, clustered, common] = await Promise.all([
|
|
17
|
+
readShader('default-standard-pbr.wgsl'),
|
|
18
|
+
readShader('default-standard-pbr-skin.wgsl'),
|
|
19
|
+
readShader('lighting-spot-modifiers.wgsl'),
|
|
20
|
+
readShader('standard-cluster.wgsl'),
|
|
21
|
+
readShader('common.wgsl'),
|
|
22
|
+
]);
|
|
23
|
+
expect(standard).not.toContain('lighting_spot_modifiers::{spotModifierFactors}');
|
|
24
|
+
expect(skin).not.toContain('lighting_spot_modifiers::{spotModifierFactors}');
|
|
25
|
+
expect(clustered).toContain('lighting_spot_modifiers::{spotModifierFactors}');
|
|
26
|
+
expect(standard).not.toContain('fn spotModifierFactors(');
|
|
27
|
+
expect(skin).not.toContain('fn spotModifierFactors(');
|
|
28
|
+
expect(modifiers).toContain(
|
|
29
|
+
'textureSampleLevel(cookieTexture, spotModifierSampler, cookieUv, metadata.w, 0.0)',
|
|
30
|
+
);
|
|
31
|
+
expect(modifiers).toContain('cookieSample.rgb * cookieSample.a');
|
|
32
|
+
expect(modifiers).not.toMatch(/textureSampleLevel\(cookieTexture[^\n]*\)\.r/);
|
|
33
|
+
expect(modifiers).toContain('cookieMatrices[metadata.w]');
|
|
34
|
+
expect(modifiers).toContain('metadata.w != 0xffffffffu');
|
|
35
|
+
expect(modifiers).toContain('metadata.y == 0xffffffffu || (metadata.y & PROJECTOR_FLAG) == 0u');
|
|
36
|
+
expect(modifiers).toContain('let right = normalize(cross(forward, referenceUp));');
|
|
37
|
+
expect(modifiers).toContain('let depth = dot(outgoing, forward);');
|
|
38
|
+
expect(modifiers).toContain(
|
|
39
|
+
'iesProfileTexture, spotModifierSampler, iesUv, metadata.z, 0.0).r',
|
|
40
|
+
);
|
|
41
|
+
expect(common).toContain('@group(0) @binding(15) var<uniform> cookieMatrices');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('keeps skin and non-skin sampling and missing-resource identity aligned', async () => {
|
|
45
|
+
const [standard, skin, modifiers, clustered] = await Promise.all([
|
|
46
|
+
readShader('default-standard-pbr.wgsl'),
|
|
47
|
+
readShader('default-standard-pbr-skin.wgsl'),
|
|
48
|
+
readShader('lighting-spot-modifiers.wgsl'),
|
|
49
|
+
readShader('standard-cluster.wgsl'),
|
|
50
|
+
]);
|
|
51
|
+
expect(extractSpotModifierFactors(standard)).toBe('');
|
|
52
|
+
expect(extractSpotModifierFactors(skin)).toBe('');
|
|
53
|
+
expect(modifiers).toContain('cookie : f32');
|
|
54
|
+
expect(modifiers).toContain('brdf * range * cone * ies * cookie * shadow');
|
|
55
|
+
expect(clustered).toContain('spotModifierFactors');
|
|
56
|
+
expect(clustered).toContain(') * modifier;');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('shared Spot modifier shader composition', () => {
|
|
5
|
+
it('names one shared evaluator with the frozen multiplication order', async () => {
|
|
6
|
+
const source = await readFile(
|
|
7
|
+
new URL('../lighting-spot-modifiers.wgsl', import.meta.url),
|
|
8
|
+
'utf8',
|
|
9
|
+
);
|
|
10
|
+
expect(source).toContain('spotModifierProduct');
|
|
11
|
+
expect(source).toMatch(/brdf[\s\S]*range[\s\S]*cone[\s\S]*ies[\s\S]*cookie[\s\S]*shadow/);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('keeps modifier sampling behind the extended-lighting topology', async () => {
|
|
15
|
+
const punctual = await readFile(new URL('../lighting-punctual.wgsl', import.meta.url), 'utf8');
|
|
16
|
+
expect(punctual).toContain('extendedLighting');
|
|
17
|
+
expect(punctual).not.toContain('modifierLight');
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -111,15 +111,18 @@ describe('w6 (b) -- pbr / unlit / sprite-adjacent shaders DO NOT pick up PER_INS
|
|
|
111
111
|
});
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
it('default-standard-pbr.wgsl keeps its
|
|
114
|
+
it('default-standard-pbr.wgsl keeps its material axes (none touch PER_INSTANCE_REGION)', () => {
|
|
115
115
|
const axes = variantAxes(readWgsl('default-standard-pbr.wgsl'));
|
|
116
116
|
expect(axes).toEqual([
|
|
117
117
|
'#pragma variant_axis STORAGE_BUFFER_AVAILABLE',
|
|
118
118
|
'#pragma variant_axis CLUSTER_FORWARD_AVAILABLE',
|
|
119
119
|
'#pragma variant_axis VERTEX_COLOR_AVAILABLE',
|
|
120
|
+
'#pragma variant_axis PROBE_BLEND_AVAILABLE',
|
|
121
|
+
'#pragma variant_axis EXTENDED_LIGHTING_AVAILABLE',
|
|
120
122
|
'#pragma variant_axis TRANSMISSION_AVAILABLE',
|
|
121
123
|
'#pragma variant_axis DIRECTIONAL_PCSS_AVAILABLE',
|
|
122
124
|
'#pragma variant_axis PROJECTOR_AVAILABLE',
|
|
125
|
+
'#pragma variant_axis REFLECTION_FALLBACK_AVAILABLE',
|
|
123
126
|
]);
|
|
124
127
|
});
|
|
125
128
|
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
const surfaceSource = readFileSync(
|
|
6
|
+
fileURLToPath(new URL('../surface_v1.wgsl', import.meta.url)),
|
|
7
|
+
'utf8',
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
describe('Surface v1 negative contract', () => {
|
|
11
|
+
it.each([
|
|
12
|
+
['missing export', 'struct SurfaceInput {}'],
|
|
13
|
+
[
|
|
14
|
+
'wrong signature',
|
|
15
|
+
'fn evaluate_surface(input: SurfaceInput) -> vec4<f32> { return vec4<f32>(); }',
|
|
16
|
+
],
|
|
17
|
+
])('rejects %s', (_name, source) => {
|
|
18
|
+
expect(source).not.toMatch(
|
|
19
|
+
/fn\s+evaluate_surface\s*\(input\s*:\s*SurfaceInput\s*\)\s*->\s*SurfaceData\s*\{/,
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it.each([
|
|
24
|
+
['fragment entry', '@fragment fn fs_main() -> @location(0) vec4<f32> { return vec4<f32>(); }'],
|
|
25
|
+
['resource binding', '@group(1) @binding(0) var surfaceTexture: texture_2d<f32>;'],
|
|
26
|
+
[
|
|
27
|
+
'vertex mutation',
|
|
28
|
+
'fn evaluate_surface(input: SurfaceInput) -> SurfaceData { input.positionWS = vec3<f32>(); }',
|
|
29
|
+
],
|
|
30
|
+
])('rejects %s', (_name, source) => {
|
|
31
|
+
expect(source).toMatch(/@(fragment|group|binding)|positionWS\s*=/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('keeps physical fields out of the versioned base ABI', () => {
|
|
35
|
+
expect(surfaceSource).not.toMatch(/clearcoat|clearcoatRoughness|anisotropy|sheen|iridescence/);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
const surfaceSource = readFileSync(
|
|
6
|
+
fileURLToPath(new URL('../surface_v1.wgsl', import.meta.url)),
|
|
7
|
+
'utf8',
|
|
8
|
+
);
|
|
9
|
+
const defaultSource = readFileSync(
|
|
10
|
+
fileURLToPath(new URL('../default_standard_surface.wgsl', import.meta.url)),
|
|
11
|
+
'utf8',
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const inputFields = [
|
|
15
|
+
['positionOS', 'vec3<f32>'],
|
|
16
|
+
['positionWS', 'vec3<f32>'],
|
|
17
|
+
['geometricNormalWS', 'vec3<f32>'],
|
|
18
|
+
['tangentWS', 'vec4<f32>'],
|
|
19
|
+
['viewDirectionWS', 'vec3<f32>'],
|
|
20
|
+
['uv0', 'vec2<f32>'],
|
|
21
|
+
['uv1', 'vec2<f32>'],
|
|
22
|
+
['vertexColor', 'vec4<f32>'],
|
|
23
|
+
['frontFacing', 'bool'],
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
const dataFields = [
|
|
27
|
+
['baseColor', 'vec3<f32>'],
|
|
28
|
+
['normalWS', 'vec3<f32>'],
|
|
29
|
+
['metallic', 'f32'],
|
|
30
|
+
['roughness', 'f32'],
|
|
31
|
+
['emissive', 'vec3<f32>'],
|
|
32
|
+
['occlusion', 'f32'],
|
|
33
|
+
['opacity', 'f32'],
|
|
34
|
+
['alphaClipThreshold', 'f32'],
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
function assertOrderedFields(source: string, fields: readonly (readonly [string, string])[]) {
|
|
38
|
+
let previous = -1;
|
|
39
|
+
for (const [name, type] of fields) {
|
|
40
|
+
const index = new RegExp(`${name}\\s*:\\s*${type.replace(/[<>]/g, '\\$&')}`).exec(
|
|
41
|
+
source,
|
|
42
|
+
)?.index;
|
|
43
|
+
expect(index, `Surface ABI must contain ${name}: ${type}`).toBeGreaterThan(previous);
|
|
44
|
+
previous = index ?? previous;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('surface_v1 ABI', () => {
|
|
49
|
+
it('publishes one versioned base-only ABI in stable field order', () => {
|
|
50
|
+
expect(surfaceSource).toContain('#define_import_path forgeax_material::surface_v1');
|
|
51
|
+
expect(surfaceSource).toContain('struct SurfaceInput');
|
|
52
|
+
expect(surfaceSource).toContain('struct SurfaceData');
|
|
53
|
+
assertOrderedFields(surfaceSource, inputFields);
|
|
54
|
+
assertOrderedFields(surfaceSource, dataFields);
|
|
55
|
+
expect(surfaceSource).not.toMatch(/clearcoat|anisotropy|sheen|iridescence|specular/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('keeps authored Surface entry points free of stage and resource ownership', () => {
|
|
59
|
+
const authoredSource = `
|
|
60
|
+
#import forgeax_material::surface_v1::{SurfaceInput, SurfaceData}
|
|
61
|
+
fn evaluate_surface(input : SurfaceInput) -> SurfaceData {
|
|
62
|
+
return SurfaceData(vec3<f32>(1.0), input.geometricNormalWS, 0.0, 0.5, vec3<f32>(0.0), 1.0, 1.0, 0.0);
|
|
63
|
+
}
|
|
64
|
+
`;
|
|
65
|
+
expect(authoredSource).toMatch(
|
|
66
|
+
/fn\s+evaluate_surface\s*\(input\s*:\s*SurfaceInput\s*\)\s*->\s*SurfaceData/,
|
|
67
|
+
);
|
|
68
|
+
expect(authoredSource.match(/fn\s+evaluate_surface\s*\(/g)).toHaveLength(1);
|
|
69
|
+
expect(authoredSource).not.toMatch(/@(fragment|vertex|compute)|@(group|binding)\s*\(/);
|
|
70
|
+
expect(surfaceSource).not.toMatch(/@(fragment|vertex|compute)|@(group|binding)\s*\(/);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('keeps the default evaluator in the same Surface ABI family', () => {
|
|
74
|
+
expect(defaultSource).toContain(
|
|
75
|
+
'#define_import_path forgeax_material::default_standard_surface',
|
|
76
|
+
);
|
|
77
|
+
expect(defaultSource).toMatch(
|
|
78
|
+
/fn\s+evaluate_surface\s*\(input\s*:\s*SurfaceInput\s*\)\s*->\s*SurfaceData/,
|
|
79
|
+
);
|
|
80
|
+
expect(defaultSource).toContain('input.geometricNormalWS');
|
|
81
|
+
expect(defaultSource).not.toMatch(/@(fragment|vertex|compute)|@(group|binding)\s*\(/);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -6,16 +6,12 @@ const shader = readFileSync(resolve(import.meta.dirname, '../default-standard-pb
|
|
|
6
6
|
|
|
7
7
|
describe('standard transmission thickness scale contract', () => {
|
|
8
8
|
it('converts local authored thickness through the combined local-to-world basis', () => {
|
|
9
|
-
expect(shader).toContain('let localToWorld = entityWorld * instanceLocal;');
|
|
10
9
|
expect(shader).toContain('@location(4) @interpolate(flat) transmissionBasis0 : vec4<f32>');
|
|
11
|
-
expect(shader).toContain('@location(
|
|
10
|
+
expect(shader).toContain('@location(13) @interpolate(flat) transmissionBasis1 : vec4<f32>');
|
|
11
|
+
expect(shader).not.toContain('@location(15)');
|
|
12
12
|
expect(shader).toContain('let localToWorld0 = in.transmissionBasis0.xyz;');
|
|
13
13
|
expect(shader).toContain('let localToWorld1 = vec3<f32>(');
|
|
14
14
|
expect(shader).toContain('let localToWorld2 = vec3<f32>(');
|
|
15
|
-
expect(shader).toContain('in.ndc.w,');
|
|
16
|
-
expect(shader).not.toContain(
|
|
17
|
-
'let localToWorld = meshes[0].worldFromLocal * instances[in.instanceIdx].localFromInstance;',
|
|
18
|
-
);
|
|
19
15
|
expect(shader).toContain('let worldToLocal0 = vec3<f32>(');
|
|
20
16
|
expect(shader).toContain('let worldRefractedDirection =');
|
|
21
17
|
expect(shader).toMatch(
|
|
@@ -23,7 +23,7 @@ describe('M4 vertex-color shader contract', () => {
|
|
|
23
23
|
/#ifdef\s+VERTEX_COLOR_AVAILABLE[\s\S]*?@location\(14\)\s+color\s*:\s*vec4<f32>/,
|
|
24
24
|
);
|
|
25
25
|
if (file !== 'unlit.wgsl') {
|
|
26
|
-
expect(text).toMatch(/@location\(
|
|
26
|
+
expect(text).toMatch(/@location\(12\)\s+uv6And7\s*:\s*vec4<f32>/);
|
|
27
27
|
}
|
|
28
28
|
});
|
|
29
29
|
|
|
@@ -38,15 +38,26 @@ describe('M4 vertex-color shader contract', () => {
|
|
|
38
38
|
|
|
39
39
|
it.each(SOURCES)('%s applies vertex color to linear RGB and all alpha consumers', (file) => {
|
|
40
40
|
const text = source(file);
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
if (file === 'unlit.wgsl') {
|
|
42
|
+
expect(text).toMatch(/baseColor\.rgb\s*\*\s*texSample\.rgb\s*\*\s*vertexColor\.rgb/);
|
|
43
|
+
expect(text).toMatch(/baseColor\.a\s*\*\s*texSample\.a\s*\*\s*vertexColor\.a/);
|
|
44
|
+
} else {
|
|
45
|
+
expect(text).toContain('#import forgeax_material::slot::surface::{evaluate_surface}');
|
|
46
|
+
}
|
|
43
47
|
expect(text).toMatch(/alphaCutoff/);
|
|
44
48
|
expect(text).toMatch(/fs_temporal/);
|
|
45
49
|
});
|
|
46
50
|
|
|
47
|
-
it('
|
|
51
|
+
it('canonical Standard surface applies vertex color to linear RGB and alpha', () => {
|
|
52
|
+
const text = readFileSync(new URL('../default_standard_surface.wgsl', import.meta.url), 'utf8');
|
|
53
|
+
expect(text).toMatch(/baseColor\.rgb\s*\*\s*baseSample\.rgb\s*\*\s*vertexColor\.rgb/);
|
|
54
|
+
expect(text).toMatch(/baseColor\.a\s*\*\s*baseSample\.a\s*\*\s*vertexColor\.a/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('packs the high UV pair while reserving color at 14', () => {
|
|
48
58
|
const text = source('default-standard-pbr.wgsl');
|
|
49
|
-
expect(text).toMatch(/@location\(
|
|
59
|
+
expect(text).toMatch(/@location\(12\)\s+uv6And7\s*:\s*vec4<f32>/);
|
|
60
|
+
expect(text).toContain('in.uv6And7.zw');
|
|
50
61
|
expect(text).toMatch(/@location\(14\)\s+color\s*:\s*vec4<f32>/);
|
|
51
62
|
});
|
|
52
63
|
});
|