@forgeax/engine-shader 0.1.7 → 0.1.20
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 +34 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/ShaderRegistry.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +53 -2
- package/dist/index.mjs.map +1 -1
- package/dist/material-schemas.d.ts.map +1 -1
- package/package.json +6 -3
- package/src/ShaderRegistry.ts +50 -0
- package/src/__tests__/bloom-fxaa-tonemap.unit.test.ts +86 -12
- package/src/__tests__/default-standard-pbr-transmission.unit.test.ts +185 -0
- package/src/__tests__/fog-oracle.unit.test.ts +110 -114
- package/src/__tests__/fog-orthographic-ray.unit.test.ts +12 -37
- package/src/__tests__/fog-producer-matrix.integration.test.ts +16 -51
- package/src/__tests__/lighting-point-spot-volume.unit.test.ts +48 -0
- package/src/__tests__/lighting-punctual.unit.test.ts +1 -1
- package/src/__tests__/lighting-spot-volume-attenuation.unit.test.ts +26 -0
- package/src/__tests__/lighting-spot-volume.unit.test.ts +25 -0
- package/src/__tests__/material-builtins.unit.test.ts +38 -1
- package/src/__tests__/material-contract.unit.test.ts +123 -0
- package/src/__tests__/motion-blur.unit.test.ts +44 -0
- package/src/__tests__/scene-temporal.unit.test.ts +23 -5
- package/src/__tests__/shader-manifest-transmission.unit.test.ts +75 -0
- package/src/__tests__/shader.unit.test.ts +44 -3
- package/src/__tests__/sprite-variants.unit.test.ts +20 -0
- package/src/__tests__/taa-resolve.unit.test.ts +75 -3
- package/src/__tests__/transmission-thickness-scale.unit.test.ts +19 -0
- package/src/common.wgsl +34 -65
- package/src/default-standard-pbr-skin.wgsl +29 -28
- package/src/default-standard-pbr.wgsl +257 -57
- package/src/fxaa.wgsl +5 -6
- package/src/hdrp-cluster-forward.wgsl +41 -3
- package/src/hdrp-deferred-lighting.wgsl +3 -27
- package/src/index.ts +3 -0
- package/src/lighting-attenuation.wgsl +58 -0
- package/src/lighting-directional.wgsl +31 -3
- package/src/lighting-punctual.wgsl +57 -12
- package/src/material-schemas.ts +7 -0
- package/src/motion-blur.wgsl +77 -0
- package/src/msdf-text.wgsl +4 -23
- package/src/pbr-temporal.wgsl +15 -2
- package/src/scene-temporal.wgsl +54 -0
- package/src/shadow-pcf.wgsl +31 -10
- package/src/shadow_caster.wgsl +27 -2
- package/src/skybox.wgsl +2 -7
- package/src/sprite-lit.wgsl +7 -25
- package/src/sprite.wgsl +7 -25
- package/src/taa-resolve.wgsl +76 -81
- package/src/tonemap.wgsl +7 -9
- package/src/unlit.wgsl +7 -28
- package/src/volume/volume-composite.wgsl +113 -0
- package/src/volume/volume-inject.wgsl +249 -0
- package/src/volume/volume-integrate.wgsl +342 -0
- package/src/volume/volume-temporal.wgsl +70 -0
- package/dist/__tests__/atmosphere-background.test.d.ts +0 -2
- package/dist/__tests__/atmosphere-background.test.d.ts.map +0 -1
- package/dist/__tests__/atmosphere-cubemap-orientation.test.d.ts +0 -2
- package/dist/__tests__/atmosphere-cubemap-orientation.test.d.ts.map +0 -1
- package/dist/__tests__/atmosphere-preetham-numeric.test.d.ts +0 -2
- package/dist/__tests__/atmosphere-preetham-numeric.test.d.ts.map +0 -1
- package/dist/__tests__/atmosphere-preetham-red.test.d.ts +0 -2
- package/dist/__tests__/atmosphere-preetham-red.test.d.ts.map +0 -1
- package/dist/__tests__/atmosphere-preetham.test.d.ts +0 -2
- package/dist/__tests__/atmosphere-preetham.test.d.ts.map +0 -1
- package/dist/__tests__/atmosphere-three-r184-reference.test.d.ts +0 -2
- package/dist/__tests__/atmosphere-three-r184-reference.test.d.ts.map +0 -1
- package/dist/__tests__/bloom-fxaa-tonemap.unit.test.d.ts +0 -2
- package/dist/__tests__/bloom-fxaa-tonemap.unit.test.d.ts.map +0 -1
- package/dist/__tests__/builtin-texture-sampling-contract.test.d.ts +0 -2
- package/dist/__tests__/builtin-texture-sampling-contract.test.d.ts.map +0 -1
- package/dist/__tests__/default-standard-pbr-alpha.unit.test.d.ts +0 -2
- package/dist/__tests__/default-standard-pbr-alpha.unit.test.d.ts.map +0 -1
- package/dist/__tests__/deferred-lighting-ssao.test.d.ts +0 -2
- package/dist/__tests__/deferred-lighting-ssao.test.d.ts.map +0 -1
- package/dist/__tests__/fog-oracle.unit.test.d.ts +0 -2
- package/dist/__tests__/fog-oracle.unit.test.d.ts.map +0 -1
- package/dist/__tests__/fog-orthographic-ray.unit.test.d.ts +0 -2
- package/dist/__tests__/fog-orthographic-ray.unit.test.d.ts.map +0 -1
- package/dist/__tests__/fog-producer-matrix.integration.test.d.ts +0 -2
- package/dist/__tests__/fog-producer-matrix.integration.test.d.ts.map +0 -1
- package/dist/__tests__/hdrp-ssao-compile.test.d.ts +0 -2
- package/dist/__tests__/hdrp-ssao-compile.test.d.ts.map +0 -1
- package/dist/__tests__/ibl-irradiance.unit.test.d.ts +0 -2
- package/dist/__tests__/ibl-irradiance.unit.test.d.ts.map +0 -1
- package/dist/__tests__/ibl-sampling.unit.test.d.ts +0 -2
- package/dist/__tests__/ibl-sampling.unit.test.d.ts.map +0 -1
- package/dist/__tests__/lighting-punctual.unit.test.d.ts +0 -2
- package/dist/__tests__/lighting-punctual.unit.test.d.ts.map +0 -1
- package/dist/__tests__/material-artifact-dedup.unit.test.d.ts +0 -2
- package/dist/__tests__/material-artifact-dedup.unit.test.d.ts.map +0 -1
- package/dist/__tests__/material-builtins.unit.test.d.ts +0 -2
- package/dist/__tests__/material-builtins.unit.test.d.ts.map +0 -1
- package/dist/__tests__/material-contract.unit.test.d.ts +0 -2
- package/dist/__tests__/material-contract.unit.test.d.ts.map +0 -1
- package/dist/__tests__/material-derived-builtins.integration.test.d.ts +0 -2
- package/dist/__tests__/material-derived-builtins.integration.test.d.ts.map +0 -1
- package/dist/__tests__/points-lines-shader.unit.test.d.ts +0 -2
- package/dist/__tests__/points-lines-shader.unit.test.d.ts.map +0 -1
- package/dist/__tests__/public-surface.unit.test.d.ts +0 -2
- package/dist/__tests__/public-surface.unit.test.d.ts.map +0 -1
- package/dist/__tests__/scene-temporal.unit.test.d.ts +0 -2
- package/dist/__tests__/scene-temporal.unit.test.d.ts.map +0 -1
- package/dist/__tests__/shader-registry-transaction.unit.test.d.ts +0 -2
- package/dist/__tests__/shader-registry-transaction.unit.test.d.ts.map +0 -1
- package/dist/__tests__/shader.unit.test.d.ts +0 -2
- package/dist/__tests__/shader.unit.test.d.ts.map +0 -1
- package/dist/__tests__/sprite-lit-shader.test.d.ts +0 -2
- package/dist/__tests__/sprite-lit-shader.test.d.ts.map +0 -1
- package/dist/__tests__/sprite-variants.unit.test.d.ts +0 -2
- package/dist/__tests__/sprite-variants.unit.test.d.ts.map +0 -1
- package/dist/__tests__/taa-resolve.unit.test.d.ts +0 -2
- package/dist/__tests__/taa-resolve.unit.test.d.ts.map +0 -1
- package/dist/__tests__/transparent-pbr.unit.test.d.ts +0 -2
- package/dist/__tests__/transparent-pbr.unit.test.d.ts.map +0 -1
- package/dist/__tests__/vertex-color-variant.unit.test.d.ts +0 -2
- package/dist/__tests__/vertex-color-variant.unit.test.d.ts.map +0 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
const volumeInject = readFileSync(new URL('../volume/volume-inject.wgsl', import.meta.url), 'utf8');
|
|
5
|
+
const volumeIntegrate = readFileSync(
|
|
6
|
+
new URL('../volume/volume-integrate.wgsl', import.meta.url),
|
|
7
|
+
'utf8',
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
describe('volume Spot attenuation precision', () => {
|
|
11
|
+
it('keeps the shared finite-range attenuation monotonic', () => {
|
|
12
|
+
expect(volumeInject).not.toContain('evalSpotAttenuation');
|
|
13
|
+
expect(volumeInject).not.toContain('attenuation * cone_distance');
|
|
14
|
+
expect(volumeInject).toContain('spot_shadow_visibility_at');
|
|
15
|
+
expect(volumeIntegrate).toContain('evalSpotAttenuation');
|
|
16
|
+
expect(volumeIntegrate).toContain('spot_attenuation_at');
|
|
17
|
+
expect(volumeIntegrate).toContain('light.invRangeSquared');
|
|
18
|
+
// The paired Point+Spot owner applies visibility only to the Spot term;
|
|
19
|
+
// the Point radiance remains independent of the spot shadow. Keep the
|
|
20
|
+
// assertion at the semantic call boundary so WGSL formatting changes do
|
|
21
|
+
// not turn this contract test into a stale substring check.
|
|
22
|
+
expect(volumeIntegrate).toContain('volume_light_radiance_pair(world_position, visibility)');
|
|
23
|
+
expect(volumeIntegrate).toContain('volume_spot_radiance(world_position) *');
|
|
24
|
+
expect(volumeIntegrate).toContain('mix(1.0, spot_visibility, volume_spot_shadow_intensity())');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
const attenuation = readFileSync(new URL('../lighting-attenuation.wgsl', import.meta.url), 'utf8');
|
|
5
|
+
const punctual = readFileSync(new URL('../lighting-punctual.wgsl', import.meta.url), 'utf8');
|
|
6
|
+
|
|
7
|
+
describe('shared Spot volume optics', () => {
|
|
8
|
+
it('uses finite squared distance attenuation and a smooth cosine cone', () => {
|
|
9
|
+
expect(attenuation).toContain('evalSpotAttenuation');
|
|
10
|
+
expect(attenuation).toContain('invRangeSquared');
|
|
11
|
+
expect(attenuation).toContain('smoothstep(cosOuter, cosInner');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('is consumed by the surface punctual owner', () => {
|
|
15
|
+
expect(punctual).toContain('forgeax_pbr::lighting_attenuation');
|
|
16
|
+
expect(punctual).toContain('evalSpotAttenuation');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('keeps invalid range and cone inputs finite at the shared boundary', () => {
|
|
20
|
+
expect(attenuation).toContain('max(dSquared, 1e-4)');
|
|
21
|
+
expect(attenuation).toContain('clamp');
|
|
22
|
+
expect(attenuation).toContain('cosInner');
|
|
23
|
+
expect(attenuation).toContain('cosOuter');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -70,6 +70,41 @@ describe('built-in MaterialAsset sources', () => {
|
|
|
70
70
|
{ ...material, values: { ...material.values, baseColor: [0.2, 0.3, 0.4, 1] } }.values,
|
|
71
71
|
).toMatchObject({ baseColor: [0.2, 0.3, 0.4, 1] });
|
|
72
72
|
});
|
|
73
|
+
|
|
74
|
+
it.each([
|
|
75
|
+
'default-standard-pbr.wgsl',
|
|
76
|
+
'default-standard-pbr-skin.wgsl',
|
|
77
|
+
] as const)('keeps the Standard user-region ABI before Skylight in %s', (file) => {
|
|
78
|
+
const text = source(file);
|
|
79
|
+
const bindings = [
|
|
80
|
+
'baseColorSampler',
|
|
81
|
+
'baseColorTexture',
|
|
82
|
+
'metallicRoughnessSampler',
|
|
83
|
+
'metallicRoughnessTexture',
|
|
84
|
+
'normalSampler',
|
|
85
|
+
'normalTexture',
|
|
86
|
+
'specularTintSampler',
|
|
87
|
+
'specularTintTexture',
|
|
88
|
+
'emissiveSampler',
|
|
89
|
+
'emissiveTexture',
|
|
90
|
+
'occlusionSampler',
|
|
91
|
+
'occlusionTexture',
|
|
92
|
+
'transmissionSampler',
|
|
93
|
+
'transmissionTexture',
|
|
94
|
+
'thicknessSampler',
|
|
95
|
+
'thicknessTexture',
|
|
96
|
+
'irradianceMap',
|
|
97
|
+
];
|
|
98
|
+
let previous = -1;
|
|
99
|
+
for (const name of bindings) {
|
|
100
|
+
const index = text.indexOf(`var ${name}`);
|
|
101
|
+
expect(index, `${file} must declare ${name}`).toBeGreaterThan(previous);
|
|
102
|
+
previous = index;
|
|
103
|
+
}
|
|
104
|
+
expect(text).toContain('@binding(13) var transmissionSampler');
|
|
105
|
+
expect(text).toContain('@binding(16) var thicknessTexture');
|
|
106
|
+
expect(text).toContain('@binding(17) var irradianceMap');
|
|
107
|
+
});
|
|
73
108
|
});
|
|
74
109
|
|
|
75
110
|
describe('built-in and custom material derived layout matrix', () => {
|
|
@@ -77,9 +112,11 @@ describe('built-in and custom material derived layout matrix', () => {
|
|
|
77
112
|
const names = DEFAULT_STANDARD_PBR_PARAM_SCHEMA.map((entry) => entry.name);
|
|
78
113
|
expect(names).toContain('emissiveTexture');
|
|
79
114
|
expect(names).toContain('occlusionTexture');
|
|
115
|
+
expect(names).toContain('transmissionTexture');
|
|
116
|
+
expect(names).toContain('thicknessTexture');
|
|
80
117
|
expect(
|
|
81
118
|
DEFAULT_STANDARD_PBR_PARAM_SCHEMA.filter((entry) => entry.type === 'texture2d'),
|
|
82
|
-
).toHaveLength(
|
|
119
|
+
).toHaveLength(8);
|
|
83
120
|
});
|
|
84
121
|
|
|
85
122
|
it('derives a coordinate record for every texture parameter in an interleaved custom schema', () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { derive } from '@forgeax/engine-types';
|
|
3
4
|
import Ajv2020 from 'ajv/dist/2020.js';
|
|
4
5
|
import { describe, expect, it } from 'vitest';
|
|
5
6
|
import { DEFAULT_STANDARD_PBR_PARAM_SCHEMA } from '../material-schemas.js';
|
|
@@ -56,3 +57,125 @@ describe('standard PBR material alpha contract', () => {
|
|
|
56
57
|
expect(validateAlpha({ baseColor: [1, 1, 1, 0.5], alphaMode: 'MASK' })).toBe(true);
|
|
57
58
|
});
|
|
58
59
|
});
|
|
60
|
+
|
|
61
|
+
describe('standard PBR transmission material contract', () => {
|
|
62
|
+
it('declares the seven transmission and volume parameters once, separate from CPU and GPU evidence', () => {
|
|
63
|
+
const entries = DEFAULT_STANDARD_PBR_PARAM_SCHEMA.filter((entry) =>
|
|
64
|
+
[
|
|
65
|
+
'transmission',
|
|
66
|
+
'ior',
|
|
67
|
+
'thickness',
|
|
68
|
+
'attenuationColor',
|
|
69
|
+
'attenuationDistance',
|
|
70
|
+
'transmissionTexture',
|
|
71
|
+
'thicknessTexture',
|
|
72
|
+
].includes(entry.name),
|
|
73
|
+
);
|
|
74
|
+
expect(entries).toEqual([
|
|
75
|
+
{ name: 'transmission', type: 'f32', default: 0 },
|
|
76
|
+
{ name: 'ior', type: 'f32', default: 1.5 },
|
|
77
|
+
{ name: 'thickness', type: 'f32', default: 0 },
|
|
78
|
+
{ name: 'attenuationColor', type: 'vec3', colorSpace: 'linear', default: [1, 1, 1] },
|
|
79
|
+
{ name: 'attenuationDistance', type: 'f32' },
|
|
80
|
+
{ name: 'transmissionTexture', type: 'texture2d' },
|
|
81
|
+
{ name: 'thicknessTexture', type: 'texture2d' },
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('derives numeric layout and paired texture bindings from the same schema', () => {
|
|
86
|
+
const derived = derive(DEFAULT_STANDARD_PBR_PARAM_SCHEMA);
|
|
87
|
+
expect(derived.numericMembers.map((member) => member.name)).toEqual(
|
|
88
|
+
expect.arrayContaining([
|
|
89
|
+
'transmission',
|
|
90
|
+
'ior',
|
|
91
|
+
'thickness',
|
|
92
|
+
'attenuationColor',
|
|
93
|
+
'attenuationDistance',
|
|
94
|
+
]),
|
|
95
|
+
);
|
|
96
|
+
expect(derived.textureFieldNames.has('transmissionTexture')).toBe(true);
|
|
97
|
+
expect(derived.textureFieldNames.has('thicknessTexture')).toBe(true);
|
|
98
|
+
expect(derived.samplerForTexture.get('transmissionTexture')).toBe(
|
|
99
|
+
'transmissionTexture_sampler',
|
|
100
|
+
);
|
|
101
|
+
expect(derived.samplerForTexture.get('thicknessTexture')).toBe('thicknessTexture_sampler');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('keeps the seven transmission inputs in the reflected UBO and coordinate contract', {
|
|
105
|
+
timeout: 20_000,
|
|
106
|
+
}, async () => {
|
|
107
|
+
const derived = derive(DEFAULT_STANDARD_PBR_PARAM_SCHEMA);
|
|
108
|
+
const transmissionNames = [
|
|
109
|
+
'transmission',
|
|
110
|
+
'ior',
|
|
111
|
+
'thickness',
|
|
112
|
+
'attenuationColor',
|
|
113
|
+
'attenuationDistance',
|
|
114
|
+
'transmissionTexture',
|
|
115
|
+
'thicknessTexture',
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
expect(derived.numericMembers.map((member) => member.name)).toEqual(
|
|
119
|
+
expect.arrayContaining(transmissionNames.slice(0, 5)),
|
|
120
|
+
);
|
|
121
|
+
expect(derived.coordinateRecords.map((record) => record.parameter)).toEqual(
|
|
122
|
+
expect.arrayContaining(transmissionNames.slice(5)),
|
|
123
|
+
);
|
|
124
|
+
expect(derived.resourceBindings.filter((binding) => binding.binding >= 13)).toEqual([
|
|
125
|
+
{
|
|
126
|
+
name: 'transmissionTexture_sampler',
|
|
127
|
+
parameter: 'transmissionTexture',
|
|
128
|
+
kind: 'sampler',
|
|
129
|
+
binding: 13,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'transmissionTexture',
|
|
133
|
+
parameter: 'transmissionTexture',
|
|
134
|
+
kind: 'texture',
|
|
135
|
+
binding: 14,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'thicknessTexture_sampler',
|
|
139
|
+
parameter: 'thicknessTexture',
|
|
140
|
+
kind: 'sampler',
|
|
141
|
+
binding: 15,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'thicknessTexture',
|
|
145
|
+
parameter: 'thicknessTexture',
|
|
146
|
+
kind: 'texture',
|
|
147
|
+
binding: 16,
|
|
148
|
+
},
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
const { buildEngineShaderManifest } = await import('@forgeax/engine-vite-plugin-shader');
|
|
152
|
+
const manifest = await buildEngineShaderManifest();
|
|
153
|
+
const material = manifest.materialShaders.find(
|
|
154
|
+
(entry) => entry.identifier === 'forgeax::default-standard-pbr',
|
|
155
|
+
);
|
|
156
|
+
expect(material).toBeDefined();
|
|
157
|
+
if (material === undefined) return;
|
|
158
|
+
const transmissionVariant = material.variants.find(
|
|
159
|
+
(variant) =>
|
|
160
|
+
variant.defines.CLUSTER_FORWARD_AVAILABLE === false &&
|
|
161
|
+
variant.defines.STORAGE_BUFFER_AVAILABLE === true &&
|
|
162
|
+
variant.defines.TRANSMISSION_AVAILABLE === true &&
|
|
163
|
+
variant.defines.VERTEX_COLOR_AVAILABLE === false,
|
|
164
|
+
);
|
|
165
|
+
expect(transmissionVariant).toBeDefined();
|
|
166
|
+
if (transmissionVariant === undefined) return;
|
|
167
|
+
expect(derived.totalBytes).toBe(384);
|
|
168
|
+
expect(transmissionVariant.composedWgsl).toMatch(
|
|
169
|
+
/@group\(1\)\s*@binding\(13\)\s*var transmissionSampler/,
|
|
170
|
+
);
|
|
171
|
+
expect(transmissionVariant.composedWgsl).toMatch(
|
|
172
|
+
/@group\(1\)\s*@binding\(14\)\s*var transmissionTexture/,
|
|
173
|
+
);
|
|
174
|
+
expect(transmissionVariant.composedWgsl).toMatch(
|
|
175
|
+
/@group\(1\)\s*@binding\(15\)\s*var thicknessSampler/,
|
|
176
|
+
);
|
|
177
|
+
expect(transmissionVariant.composedWgsl).toMatch(
|
|
178
|
+
/@group\(1\)\s*@binding\(16\)\s*var thicknessTexture/,
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
const source = readFileSync(resolve(import.meta.dirname, '../motion-blur.wgsl'), 'utf8');
|
|
6
|
+
|
|
7
|
+
describe('motion-blur.wgsl', () => {
|
|
8
|
+
it('declares the portable raster gather contract', () => {
|
|
9
|
+
expect(source).toContain('@fragment');
|
|
10
|
+
expect(source).toContain('symmetric');
|
|
11
|
+
expect(source).toContain('maxRadiusPixels');
|
|
12
|
+
expect(source).toContain('shutterAngle');
|
|
13
|
+
expect(source).toContain('sampleCount');
|
|
14
|
+
expect(source).toContain('textureSample');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('samples scene temporal depth through the portable color path', () => {
|
|
18
|
+
expect(source).toContain('@group(1) @binding(3) var sceneTemporal : texture_2d<f32>');
|
|
19
|
+
expect(source).toContain('textureLoad(sceneTemporal, samplePixel, 0)');
|
|
20
|
+
expect(source).not.toContain('texture_depth_2d');
|
|
21
|
+
expect(source).not.toContain('sceneDepth');
|
|
22
|
+
expect(source).not.toContain('depthSampler');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('keeps depth, reactive, reset, and alpha guards in the shader owner', () => {
|
|
26
|
+
expect(source).toContain('reactive');
|
|
27
|
+
expect(source).toContain('invalidDepth');
|
|
28
|
+
expect(source).toContain('depthReject');
|
|
29
|
+
expect(source).toContain('reset');
|
|
30
|
+
expect(source).toContain('output.a');
|
|
31
|
+
expect(source).toContain('#import forgeax_scene_temporal::{unpackSceneTemporalV1}');
|
|
32
|
+
expect(source).toContain('let blurWeight = (1.0 - reactive)');
|
|
33
|
+
expect(source).toContain('for (var index = 0u; index < 16u; index += 1u)');
|
|
34
|
+
expect(source).not.toContain('positiveUv');
|
|
35
|
+
expect(source).not.toContain('negativeUv');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('does not introduce compute, storage, or temporal history writes', () => {
|
|
39
|
+
expect(source).not.toContain('@compute');
|
|
40
|
+
expect(source).not.toContain('var<storage');
|
|
41
|
+
expect(source).not.toContain('history');
|
|
42
|
+
expect(source).not.toContain('atomic');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -10,10 +10,25 @@ const pbrTemporalConsumers = [
|
|
|
10
10
|
] as const;
|
|
11
11
|
|
|
12
12
|
describe('scene temporal varyings', () => {
|
|
13
|
-
it
|
|
13
|
+
it('defines the temporal-v1 accessor ABI in one owner module', () => {
|
|
14
|
+
const source = readFileSync(resolve(import.meta.dirname, '..', 'scene-temporal.wgsl'), 'utf8');
|
|
15
|
+
expect(source).toContain('struct SceneTemporalV1');
|
|
16
|
+
expect(source).toContain('fn unpackSceneTemporalV1');
|
|
17
|
+
expect(source).toContain('motionUv = packed.xy');
|
|
18
|
+
expect(source).toContain('exp2(packed.z) - 1.0');
|
|
19
|
+
expect(source).toContain('reactive = clamp(packed.w');
|
|
20
|
+
expect(source).toContain('packed.z < 0.0');
|
|
21
|
+
expect(source).toContain('SCENE_DATA_TEMPORAL_V1_CLEAR');
|
|
22
|
+
expect(source).toContain('rgba16float');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it.each(localTemporalShaders)('%s perspective-interpolates both clip authorities', (file) => {
|
|
14
26
|
const source = readFileSync(resolve(import.meta.dirname, '..', file), 'utf8');
|
|
15
|
-
expect(source).toMatch(/@interpolate\(
|
|
16
|
-
expect(source).toMatch(/@interpolate\(
|
|
27
|
+
expect(source).toMatch(/@interpolate\(perspective\) currentClip : vec4<f32>/);
|
|
28
|
+
expect(source).toMatch(/@interpolate\(perspective\) previousClip : vec4<f32>/);
|
|
29
|
+
expect(source).not.toMatch(
|
|
30
|
+
/@interpolate\((?:linear|noperspective)\) (?:currentClip|previousClip)/,
|
|
31
|
+
);
|
|
17
32
|
});
|
|
18
33
|
|
|
19
34
|
it('keeps PBR UV selection and fragment projection in one module', () => {
|
|
@@ -25,8 +40,11 @@ describe('scene temporal varyings', () => {
|
|
|
25
40
|
it.each(pbrTemporalConsumers)('%s imports the shared PBR temporal authority', (file) => {
|
|
26
41
|
const source = readFileSync(resolve(import.meta.dirname, '..', file), 'utf8');
|
|
27
42
|
expect(source).toContain('#import forgeax_pbr::temporal::{projectPbrSceneTemporal}');
|
|
28
|
-
expect(source).toMatch(/@interpolate\(
|
|
29
|
-
expect(source).toMatch(/@interpolate\(
|
|
43
|
+
expect(source).toMatch(/@interpolate\(perspective\) currentClip : vec4<f32>/);
|
|
44
|
+
expect(source).toMatch(/@interpolate\(perspective\) previousClip : vec4<f32>/);
|
|
45
|
+
expect(source).not.toMatch(
|
|
46
|
+
/@interpolate\((?:linear|noperspective)\) (?:currentClip|previousClip)/,
|
|
47
|
+
);
|
|
30
48
|
expect(source).not.toContain('fn transformedTemporalUv');
|
|
31
49
|
});
|
|
32
50
|
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ok, type ShaderModule } from '@forgeax/engine-rhi';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { ShaderRegistry, type ShaderRegistryDevice } from '../index.js';
|
|
4
|
+
|
|
5
|
+
function dataUrl(payload: unknown): string {
|
|
6
|
+
return `data:application/json,${encodeURIComponent(JSON.stringify(payload))}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function device(): ShaderRegistryDevice {
|
|
10
|
+
return {
|
|
11
|
+
createShaderModule(): ReturnType<ShaderRegistryDevice['createShaderModule']> {
|
|
12
|
+
return ok({} as ShaderModule);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function variant(definesKey: string, transmission: boolean) {
|
|
18
|
+
return {
|
|
19
|
+
definesKey,
|
|
20
|
+
defines: {
|
|
21
|
+
CLUSTER_FORWARD_AVAILABLE: false,
|
|
22
|
+
STORAGE_BUFFER_AVAILABLE: true,
|
|
23
|
+
TRANSMISSION_AVAILABLE: transmission,
|
|
24
|
+
VERTEX_COLOR_AVAILABLE: false,
|
|
25
|
+
},
|
|
26
|
+
composedWgsl: `standard-${transmission}`,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function standardRow(variants: readonly unknown[]) {
|
|
31
|
+
return {
|
|
32
|
+
identifier: 'forgeax::default-standard-pbr',
|
|
33
|
+
sourcePath: 'default-standard-pbr.wgsl',
|
|
34
|
+
composedWgsl: 'standard-default',
|
|
35
|
+
paramSchema: '[]',
|
|
36
|
+
variants,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function load(materialShaders: readonly unknown[]) {
|
|
41
|
+
const registry = new ShaderRegistry({
|
|
42
|
+
device: device(),
|
|
43
|
+
manifestUrl: dataUrl({ entries: [], materialShaders }),
|
|
44
|
+
});
|
|
45
|
+
return registry.loadManifest();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('Standard transmission manifest validation', () => {
|
|
49
|
+
const falseKey =
|
|
50
|
+
'CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=true+TRANSMISSION_AVAILABLE=false+VERTEX_COLOR_AVAILABLE=false';
|
|
51
|
+
const trueKey =
|
|
52
|
+
'CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=true+TRANSMISSION_AVAILABLE=true+VERTEX_COLOR_AVAILABLE=false';
|
|
53
|
+
|
|
54
|
+
it('accepts one Standard row with both exact transmission states', async () => {
|
|
55
|
+
const result = await load([standardRow([variant(falseKey, false), variant(trueKey, true)])]);
|
|
56
|
+
expect(result.ok).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('rejects a Standard row when either transmission state is missing', async () => {
|
|
60
|
+
const result = await load([standardRow([variant(falseKey, false)])]);
|
|
61
|
+
expect(result.ok).toBe(false);
|
|
62
|
+
if (!result.ok) expect(result.error.code).toBe('manifest-malformed');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('rejects duplicate material rows and duplicate variant keys', async () => {
|
|
66
|
+
const row = standardRow([variant(falseKey, false), variant(trueKey, true)]);
|
|
67
|
+
const duplicateRow = await load([row, row]);
|
|
68
|
+
expect(duplicateRow.ok).toBe(false);
|
|
69
|
+
|
|
70
|
+
const duplicateVariant = await load([
|
|
71
|
+
standardRow([variant(trueKey, true), variant(trueKey, true)]),
|
|
72
|
+
]);
|
|
73
|
+
expect(duplicateVariant.ok).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// Paradigm: each block-scoped describe('<source-filename>.test.ts', ...) preserves
|
|
17
17
|
// source as ancestorTitles[0]. Top-level imports merged + deduped.
|
|
18
18
|
|
|
19
|
+
import { readFileSync } from 'node:fs';
|
|
19
20
|
import type { Result, RhiError, ShaderModule } from '@forgeax/engine-rhi';
|
|
20
21
|
import { beforeAll, describe, expect, it } from 'vitest';
|
|
21
22
|
import type { ShaderError } from '../errors.js';
|
|
@@ -31,6 +32,19 @@ import {
|
|
|
31
32
|
ShaderRegistry,
|
|
32
33
|
} from '../ShaderRegistry.js';
|
|
33
34
|
|
|
35
|
+
{
|
|
36
|
+
const sceneTemporalSource = readFileSync(
|
|
37
|
+
new URL('../scene-temporal.wgsl', import.meta.url),
|
|
38
|
+
'utf8',
|
|
39
|
+
);
|
|
40
|
+
describe('scene-temporal accessor ABI', () => {
|
|
41
|
+
it('keeps the public shader module free of compiler and backend policy imports', () => {
|
|
42
|
+
expect(sceneTemporalSource).toContain('forgeax_scene_temporal');
|
|
43
|
+
expect(sceneTemporalSource).not.toMatch(/naga|shader-compiler|engine-rhi/i);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
34
48
|
{
|
|
35
49
|
// --- from fullscreen-triangle.test.ts ---
|
|
36
50
|
// @forgeax/engine-shader/__tests__/fullscreen-triangle.test.ts -
|
|
@@ -179,7 +193,7 @@ import {
|
|
|
179
193
|
});
|
|
180
194
|
});
|
|
181
195
|
|
|
182
|
-
describe('fxaa.wgsl SSOT gate (AC-05)', () => {
|
|
196
|
+
describe('fxaa.wgsl display-encoded SSOT gate (AC-05)', () => {
|
|
183
197
|
it('does NOT define fullscreen_triangle (SSOT is in common.wgsl)', () => {
|
|
184
198
|
expect(fxaaSource).not.toMatch(/fn fullscreen_triangle/);
|
|
185
199
|
});
|
|
@@ -191,6 +205,17 @@ import {
|
|
|
191
205
|
it('imports forgeax_view::common::FullscreenOutput', () => {
|
|
192
206
|
expect(fxaaSource).toContain('#import forgeax_view::common::FullscreenOutput');
|
|
193
207
|
});
|
|
208
|
+
|
|
209
|
+
it('declares display-encoded input/output without an OETF owner', () => {
|
|
210
|
+
expect(fxaaSource).toContain('display-encoded');
|
|
211
|
+
expect(fxaaSource).not.toContain('linearLdrColorDomain');
|
|
212
|
+
expect(fxaaSource).not.toContain('linearToSrgbOetf');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('returns encoded colors directly from early and final paths', () => {
|
|
216
|
+
expect(fxaaSource).toMatch(/return vec4<f32>\(centerColor, 1\.0\);/);
|
|
217
|
+
expect(fxaaSource).toMatch(/return vec4<f32>\(finalColor, 1\.0\);/);
|
|
218
|
+
});
|
|
194
219
|
});
|
|
195
220
|
|
|
196
221
|
describe('fxaa.wgsl bindings', () => {
|
|
@@ -779,6 +804,8 @@ import {
|
|
|
779
804
|
'#pragma variant_axis STORAGE_BUFFER_AVAILABLE',
|
|
780
805
|
'#pragma variant_axis CLUSTER_FORWARD_AVAILABLE',
|
|
781
806
|
'#pragma variant_axis VERTEX_COLOR_AVAILABLE',
|
|
807
|
+
'#pragma variant_axis TRANSMISSION_AVAILABLE',
|
|
808
|
+
'#pragma variant_axis PROJECTOR_AVAILABLE',
|
|
782
809
|
]);
|
|
783
810
|
});
|
|
784
811
|
|
|
@@ -923,12 +950,19 @@ import {
|
|
|
923
950
|
'clearcoatRoughness',
|
|
924
951
|
'specularTint',
|
|
925
952
|
'normalScale',
|
|
953
|
+
'transmission',
|
|
954
|
+
'ior',
|
|
955
|
+
'thickness',
|
|
956
|
+
'attenuationColor',
|
|
957
|
+
'attenuationDistance',
|
|
926
958
|
'baseColorTexture',
|
|
927
959
|
'metallicRoughnessTexture',
|
|
928
960
|
'normalTexture',
|
|
929
961
|
'specularTintTexture',
|
|
930
962
|
'emissiveTexture',
|
|
931
963
|
'occlusionTexture',
|
|
964
|
+
'transmissionTexture',
|
|
965
|
+
'thicknessTexture',
|
|
932
966
|
] as const;
|
|
933
967
|
|
|
934
968
|
const STUB_WGSL = '// stub composed pbr-skin wgsl\n';
|
|
@@ -987,7 +1021,7 @@ import {
|
|
|
987
1021
|
const registry = makeMockRegistry();
|
|
988
1022
|
registerDefaultStandardPbrSkin(registry, STUB_WGSL, STORAGE_CAPS);
|
|
989
1023
|
const entry = lookup(registry);
|
|
990
|
-
expect(entry.paramSchema).toHaveLength(
|
|
1024
|
+
expect(entry.paramSchema).toHaveLength(28);
|
|
991
1025
|
});
|
|
992
1026
|
|
|
993
1027
|
it('paramSchema names match default-standard-pbr schema field-for-field', () => {
|
|
@@ -1020,7 +1054,12 @@ import {
|
|
|
1020
1054
|
'f32',
|
|
1021
1055
|
'f32',
|
|
1022
1056
|
'f32',
|
|
1023
|
-
// specular tint and
|
|
1057
|
+
// specular tint, transmission, and attenuation controls.
|
|
1058
|
+
'vec3',
|
|
1059
|
+
'f32',
|
|
1060
|
+
'f32',
|
|
1061
|
+
'f32',
|
|
1062
|
+
'f32',
|
|
1024
1063
|
'vec3',
|
|
1025
1064
|
'f32',
|
|
1026
1065
|
'texture2d',
|
|
@@ -1029,6 +1068,8 @@ import {
|
|
|
1029
1068
|
'texture2d',
|
|
1030
1069
|
'texture2d',
|
|
1031
1070
|
'texture2d',
|
|
1071
|
+
'texture2d',
|
|
1072
|
+
'texture2d',
|
|
1032
1073
|
]);
|
|
1033
1074
|
});
|
|
1034
1075
|
});
|
|
@@ -117,6 +117,8 @@ describe('w6 (b) -- pbr / unlit / sprite-adjacent shaders DO NOT pick up PER_INS
|
|
|
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 TRANSMISSION_AVAILABLE',
|
|
121
|
+
'#pragma variant_axis PROJECTOR_AVAILABLE',
|
|
120
122
|
]);
|
|
121
123
|
});
|
|
122
124
|
|
|
@@ -207,3 +209,21 @@ describe('w6 (d) -- MAX_UNIFORM_INSTANCES=128 stays unchanged under the new axis
|
|
|
207
209
|
expect(src).toMatch(/MAX_UNIFORM_INSTANCES\s*=\s*128/);
|
|
208
210
|
});
|
|
209
211
|
});
|
|
212
|
+
|
|
213
|
+
describe('HDR/display fragment contract', () => {
|
|
214
|
+
for (const file of ['sprite.wgsl', 'sprite-lit.wgsl'] as const) {
|
|
215
|
+
it(`${file} keeps LDR OETF in fs_main and HDR output linear`, () => {
|
|
216
|
+
const src = stripComments(readWgsl(file));
|
|
217
|
+
const ldrStart = src.indexOf('fn fs_main(');
|
|
218
|
+
const hdrStart = src.indexOf('fn fs_main_hdr(');
|
|
219
|
+
expect(ldrStart).toBeGreaterThanOrEqual(0);
|
|
220
|
+
expect(hdrStart).toBeGreaterThan(ldrStart);
|
|
221
|
+
|
|
222
|
+
const ldrBody = src.slice(ldrStart, hdrStart);
|
|
223
|
+
const hdrBody = src.slice(hdrStart);
|
|
224
|
+
expect(ldrBody).toContain('linear_to_srgb(');
|
|
225
|
+
expect(hdrBody).not.toContain('linear_to_srgb(');
|
|
226
|
+
expect(hdrBody).toContain('return vec4<f32>');
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
});
|
|
@@ -3,24 +3,29 @@ import { resolve } from 'node:path';
|
|
|
3
3
|
import { describe, expect, it } from 'vitest';
|
|
4
4
|
|
|
5
5
|
const source = readFileSync(resolve(import.meta.dirname, '../taa-resolve.wgsl'), 'utf8');
|
|
6
|
+
const pbrTemporal = readFileSync(resolve(import.meta.dirname, '../pbr-temporal.wgsl'), 'utf8');
|
|
7
|
+
const sceneTemporal = readFileSync(resolve(import.meta.dirname, '../scene-temporal.wgsl'), 'utf8');
|
|
6
8
|
|
|
7
9
|
describe('taa-resolve.wgsl', () => {
|
|
8
10
|
it('keeps the fixed packed temporal and two-attachment ABI', () => {
|
|
9
11
|
expect(source).toContain('struct TaaResolveParams');
|
|
10
12
|
expect(source).toContain('@location(0) color');
|
|
11
13
|
expect(source).toContain('@location(1) temporal');
|
|
12
|
-
expect(source).toContain('
|
|
14
|
+
expect(source).toContain('return TaaResolveOutput(');
|
|
15
|
+
expect(source).toContain('temporal,');
|
|
13
16
|
expect(source).toContain('current.a');
|
|
17
|
+
expect(source).toContain('1.0 - clamp(temporal.w');
|
|
18
|
+
expect(source).not.toContain('1.0 - clamp(current.a');
|
|
14
19
|
});
|
|
15
20
|
|
|
16
21
|
it('contains every v1 rejection and adaptive weighting term', () => {
|
|
17
|
-
expect(source).toContain('uv + params.currentJitterUv');
|
|
22
|
+
expect(source).toContain('in.uv + params.currentJitterUv');
|
|
18
23
|
expect(source).toContain('in.uv - temporal.xy');
|
|
19
24
|
expect(source).toContain('closestCurrentTemporal(pixel, dimensions)');
|
|
20
25
|
expect(source).toContain('historyInBounds');
|
|
21
26
|
expect(source).toContain('depthDelta > depthThreshold');
|
|
22
27
|
expect(source).toContain('neighborhoodSquareMean');
|
|
23
|
-
expect(source).toContain('clamp(rgbToYCoCg(
|
|
28
|
+
expect(source).toContain('clamp(rgbToYCoCg(history.rgb), clipMin, clipMax)');
|
|
24
29
|
expect(source).toContain('reactiveFactor');
|
|
25
30
|
expect(source).toContain('velocityFactor');
|
|
26
31
|
expect(source).toContain('depthFactor');
|
|
@@ -28,5 +33,72 @@ describe('taa-resolve.wgsl', () => {
|
|
|
28
33
|
expect(source).toContain('mix(0.88, 0.97, lumaFactor)');
|
|
29
34
|
expect(source).toContain('params.temporalFrameIndex < 8u');
|
|
30
35
|
expect(source).toContain('progressiveWeight');
|
|
36
|
+
expect(source).toContain('let dimensions = vec2<i32>(textureDimensions(currentColor, 0));');
|
|
37
|
+
expect(source).toContain('closestCurrentTemporal(pixel, dimensions)');
|
|
38
|
+
expect(source).toContain('vec2<f32>(dimensions)');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('compiles the PBR temporal seam with coverage-aware reactive output', async () => {
|
|
42
|
+
const compilerPath = resolve(import.meta.dirname, '../../../shader-compiler/dist/index.mjs');
|
|
43
|
+
const { compileShader } = await import(/* @vite-ignore */ compilerPath);
|
|
44
|
+
const common = `
|
|
45
|
+
fn sampleMaterialTexture(
|
|
46
|
+
texture : texture_2d<f32>,
|
|
47
|
+
textureSampler : sampler,
|
|
48
|
+
uv : vec2<f32>,
|
|
49
|
+
uvScale : vec2<f32>,
|
|
50
|
+
) -> vec4<f32> {
|
|
51
|
+
return textureSample(texture, textureSampler, uv * uvScale);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
`;
|
|
55
|
+
const sceneTemporalBody = sceneTemporal
|
|
56
|
+
.replace(/^#define_import_path.*$/gm, '')
|
|
57
|
+
.replace(/^#import.*$/gm, '');
|
|
58
|
+
const entry = `${common}
|
|
59
|
+
${sceneTemporalBody}
|
|
60
|
+
${pbrTemporal.replace(/^#define_import_path.*$/gm, '').replace(/^#import.*$/gm, '')}
|
|
61
|
+
@group(0) @binding(0) var colorTexture : texture_2d<f32>;
|
|
62
|
+
@group(0) @binding(1) var colorSampler : sampler;
|
|
63
|
+
@fragment
|
|
64
|
+
fn fs_temporal_probe() -> @location(0) vec4<f32> {
|
|
65
|
+
return projectPbrSceneTemporal(
|
|
66
|
+
0.75, 0.5, colorTexture, colorSampler,
|
|
67
|
+
vec4<f32>(0.0, 0.0, 1.0, 1.0), vec4<f32>(0.0),
|
|
68
|
+
vec4<f32>(0.0, 0.0, 1.0, 1.0), vec4<f32>(0.0),
|
|
69
|
+
vec4<f32>(0.0, 0.0, 0.0, 0.0), 0.0,
|
|
70
|
+
vec2<f32>(0.5), vec2<f32>(0.5), vec2<f32>(0.5), vec2<f32>(0.5),
|
|
71
|
+
vec2<f32>(0.5), vec2<f32>(0.5), vec2<f32>(0.5), vec2<f32>(0.5),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
`;
|
|
75
|
+
const compiled = await compileShader(entry, {
|
|
76
|
+
id: 'test::pbr-temporal-reactive',
|
|
77
|
+
defines: { PER_INSTANCE_REGION: false, STORAGE_BUFFER_AVAILABLE: true },
|
|
78
|
+
});
|
|
79
|
+
expect(compiled.ok, compiled.ok ? undefined : String(compiled.error)).toBe(true);
|
|
80
|
+
if (!compiled.ok) return;
|
|
81
|
+
expect(compiled.value.wgsl).toContain('resolvePbrTemporalReactive');
|
|
82
|
+
expect(compiled.value.wgsl).toContain('max(clamp(reactive');
|
|
83
|
+
const discardIndex = pbrTemporal.indexOf('discard;');
|
|
84
|
+
const reactiveIndex = pbrTemporal.indexOf('let reactiveCoverage');
|
|
85
|
+
expect(discardIndex).toBeGreaterThanOrEqual(0);
|
|
86
|
+
expect(discardIndex).toBeLessThan(reactiveIndex);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('rejects a dev-only forced-reactive-zero falsifier', () => {
|
|
90
|
+
const reactiveCoverageLine =
|
|
91
|
+
'let reactiveCoverage = resolvePbrTemporalReactive(reactive, baseColorAlpha, baseSample.a);';
|
|
92
|
+
const falsified = pbrTemporal.replace(reactiveCoverageLine, 'let reactiveCoverage = 0.0;');
|
|
93
|
+
expect(() => {
|
|
94
|
+
expect(falsified).toContain(reactiveCoverageLine);
|
|
95
|
+
expect(falsified).toContain('let reactiveCoverage = 0.0;');
|
|
96
|
+
}).toThrow();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('keeps the reactive coverage contract explicit', () => {
|
|
100
|
+
expect(pbrTemporal).toContain('resolvePbrTemporalReactive');
|
|
101
|
+
expect(pbrTemporal).toContain('coverageReactive');
|
|
102
|
+
expect(pbrTemporal).not.toContain('return packSceneTemporal(currentClip, previousClip, 0.0)');
|
|
31
103
|
});
|
|
32
104
|
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
const shader = readFileSync(resolve(import.meta.dirname, '../default-standard-pbr.wgsl'), 'utf8');
|
|
6
|
+
|
|
7
|
+
describe('standard transmission thickness scale contract', () => {
|
|
8
|
+
it('converts local authored thickness through the combined local-to-world basis', () => {
|
|
9
|
+
expect(shader).toContain('let localToWorld = entityWorld * instanceLocal;');
|
|
10
|
+
expect(shader).toContain(
|
|
11
|
+
'let localToWorld = meshes[0].worldFromLocal * instances[in.instanceIdx].localFromInstance;',
|
|
12
|
+
);
|
|
13
|
+
expect(shader).toContain('let worldToLocal0 = vec3<f32>(');
|
|
14
|
+
expect(shader).toContain('let worldRefractedDirection =');
|
|
15
|
+
expect(shader).toMatch(
|
|
16
|
+
/finiteScalar\(material\.thickness, 0\.0\) \* length\(worldRefractedDirection\) \*\s+finiteScalar\(thicknessSample, 1\.0\)/,
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
});
|