@forgeax/engine-shader 0.1.6 → 0.1.19
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 +61 -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 +181 -0
- package/src/__tests__/lighting-punctual.unit.test.ts +1 -1
- 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__/points-lines-shader.unit.test.ts +43 -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 +43 -3
- package/src/__tests__/sprite-variants.unit.test.ts +19 -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 +30 -30
- package/src/default-standard-pbr-skin.wgsl +27 -9
- package/src/default-standard-pbr.wgsl +209 -28
- package/src/fxaa.wgsl +5 -6
- package/src/index.ts +3 -0
- package/src/material-schemas.ts +7 -0
- package/src/motion-blur.wgsl +77 -0
- package/src/pbr-temporal.wgsl +15 -2
- package/src/points-lines.wgsl +103 -0
- package/src/scene-temporal.wgsl +54 -0
- package/src/shadow_caster.wgsl +27 -2
- package/src/sprite-lit.wgsl +5 -4
- package/src/sprite.wgsl +5 -4
- package/src/taa-resolve.wgsl +76 -81
- package/src/tonemap.wgsl +7 -9
- package/src/unlit.wgsl +6 -5
- 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__/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,181 @@
|
|
|
1
|
+
import { ok, type ShaderModule } from '@forgeax/engine-rhi';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { findVariantByKey, ShaderRegistry, type ShaderRegistryDevice } from '../index.js';
|
|
4
|
+
|
|
5
|
+
type EngineShaderManifest = Awaited<
|
|
6
|
+
ReturnType<typeof import('@forgeax/engine-vite-plugin-shader').buildEngineShaderManifest>
|
|
7
|
+
>;
|
|
8
|
+
|
|
9
|
+
function dataUrl(payload: unknown): string {
|
|
10
|
+
return `data:application/json,${encodeURIComponent(JSON.stringify(payload))}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function device(): ShaderRegistryDevice {
|
|
14
|
+
return {
|
|
15
|
+
createShaderModule(): ReturnType<ShaderRegistryDevice['createShaderModule']> {
|
|
16
|
+
return ok({} as ShaderModule);
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function engineManifest(): Promise<EngineShaderManifest> {
|
|
22
|
+
const { buildEngineShaderManifest } = await import('@forgeax/engine-vite-plugin-shader');
|
|
23
|
+
return buildEngineShaderManifest();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('default Standard PBR transmission manifest contract', () => {
|
|
27
|
+
it('publishes both TRANSMISSION_AVAILABLE variants from the cooked producer', {
|
|
28
|
+
timeout: 20_000,
|
|
29
|
+
}, async () => {
|
|
30
|
+
const manifest = await engineManifest();
|
|
31
|
+
const standard = manifest.materialShaders.find(
|
|
32
|
+
(entry) => entry.identifier === 'forgeax::default-standard-pbr',
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
expect(standard).toBeDefined();
|
|
36
|
+
if (standard === undefined) return;
|
|
37
|
+
const transmissionVariants = standard.variants.filter(
|
|
38
|
+
(variant) => 'TRANSMISSION_AVAILABLE' in variant.defines,
|
|
39
|
+
);
|
|
40
|
+
expect(transmissionVariants).toHaveLength(16);
|
|
41
|
+
expect(new Set(transmissionVariants.map((variant) => variant.definesKey)).size).toBe(16);
|
|
42
|
+
expect(new Set(transmissionVariants.map((variant) => variant.composedWgsl)).size).toBe(16);
|
|
43
|
+
for (const storageBufferAvailable of [false, true]) {
|
|
44
|
+
for (const transmissionAvailable of [false, true]) {
|
|
45
|
+
const expectedKey = [
|
|
46
|
+
'CLUSTER_FORWARD_AVAILABLE=false',
|
|
47
|
+
`STORAGE_BUFFER_AVAILABLE=${storageBufferAvailable}`,
|
|
48
|
+
`TRANSMISSION_AVAILABLE=${transmissionAvailable}`,
|
|
49
|
+
'VERTEX_COLOR_AVAILABLE=false',
|
|
50
|
+
].join('+');
|
|
51
|
+
expect(transmissionVariants).toContainEqual(
|
|
52
|
+
expect.objectContaining({
|
|
53
|
+
definesKey: expectedKey,
|
|
54
|
+
defines: expect.objectContaining({
|
|
55
|
+
CLUSTER_FORWARD_AVAILABLE: false,
|
|
56
|
+
STORAGE_BUFFER_AVAILABLE: storageBufferAvailable,
|
|
57
|
+
TRANSMISSION_AVAILABLE: transmissionAvailable,
|
|
58
|
+
VERTEX_COLOR_AVAILABLE: false,
|
|
59
|
+
}),
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('omits transmission-only texture bindings from the fallback variant', {
|
|
67
|
+
timeout: 20_000,
|
|
68
|
+
}, async () => {
|
|
69
|
+
const manifest = await engineManifest();
|
|
70
|
+
const standard = manifest.materialShaders.find(
|
|
71
|
+
(entry) => entry.identifier === 'forgeax::default-standard-pbr',
|
|
72
|
+
);
|
|
73
|
+
expect(standard).toBeDefined();
|
|
74
|
+
if (standard === undefined) return;
|
|
75
|
+
|
|
76
|
+
const fallback = standard.variants.find(
|
|
77
|
+
(variant) =>
|
|
78
|
+
variant.defines.CLUSTER_FORWARD_AVAILABLE === false &&
|
|
79
|
+
variant.defines.STORAGE_BUFFER_AVAILABLE === true &&
|
|
80
|
+
variant.defines.TRANSMISSION_AVAILABLE === false &&
|
|
81
|
+
variant.defines.VERTEX_COLOR_AVAILABLE === false,
|
|
82
|
+
);
|
|
83
|
+
const transmission = standard.variants.find(
|
|
84
|
+
(variant) =>
|
|
85
|
+
variant.defines.CLUSTER_FORWARD_AVAILABLE === false &&
|
|
86
|
+
variant.defines.STORAGE_BUFFER_AVAILABLE === true &&
|
|
87
|
+
variant.defines.TRANSMISSION_AVAILABLE === true &&
|
|
88
|
+
variant.defines.VERTEX_COLOR_AVAILABLE === false,
|
|
89
|
+
);
|
|
90
|
+
expect(fallback?.composedWgsl).not.toContain('@binding(13)');
|
|
91
|
+
expect(fallback?.composedWgsl).not.toContain('@binding(24)');
|
|
92
|
+
expect(transmission?.composedWgsl).toContain('@binding(13)');
|
|
93
|
+
expect(transmission?.composedWgsl).toContain('@binding(24)');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('registers one Standard row and resolves a declared variant by exact key', {
|
|
97
|
+
timeout: 20_000,
|
|
98
|
+
}, async () => {
|
|
99
|
+
const manifest = await engineManifest();
|
|
100
|
+
const standard = manifest.materialShaders.find(
|
|
101
|
+
(entry) => entry.identifier === 'forgeax::default-standard-pbr',
|
|
102
|
+
);
|
|
103
|
+
expect(standard).toBeDefined();
|
|
104
|
+
if (standard === undefined) return;
|
|
105
|
+
|
|
106
|
+
const registry = new ShaderRegistry({
|
|
107
|
+
device: device(),
|
|
108
|
+
manifestUrl: dataUrl({ entries: [], materialShaders: [standard] }),
|
|
109
|
+
});
|
|
110
|
+
const loaded = await registry.loadManifest();
|
|
111
|
+
expect(loaded.ok).toBe(true);
|
|
112
|
+
const rows = Array.from(registry.materialShaderManifestEntries()).filter(
|
|
113
|
+
(entry) => entry.identifier === 'forgeax::default-standard-pbr',
|
|
114
|
+
);
|
|
115
|
+
expect(rows).toHaveLength(1);
|
|
116
|
+
const registered = rows[0];
|
|
117
|
+
expect(registered).toBeDefined();
|
|
118
|
+
if (registered === undefined) return;
|
|
119
|
+
|
|
120
|
+
const transmission = registered.variants.find(
|
|
121
|
+
(variant) => variant.defines.TRANSMISSION_AVAILABLE === true,
|
|
122
|
+
);
|
|
123
|
+
expect(transmission).toBeDefined();
|
|
124
|
+
if (transmission === undefined) return;
|
|
125
|
+
expect(findVariantByKey(registered, transmission.definesKey)).toBe(transmission);
|
|
126
|
+
expect(
|
|
127
|
+
findVariantByKey(registered, `${transmission.definesKey}+UNDECLARED=true`),
|
|
128
|
+
).toBeUndefined();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('rejects a manifest with a missing transmission variant before prewarm', async () => {
|
|
132
|
+
const missingVariant = {
|
|
133
|
+
identifier: 'forgeax::default-standard-pbr',
|
|
134
|
+
sourcePath: 'default-standard-pbr.wgsl',
|
|
135
|
+
composedWgsl: 'standard-default',
|
|
136
|
+
paramSchema: '[]',
|
|
137
|
+
variants: [
|
|
138
|
+
{
|
|
139
|
+
definesKey: 'TRANSMISSION_AVAILABLE=false',
|
|
140
|
+
defines: { TRANSMISSION_AVAILABLE: false },
|
|
141
|
+
composedWgsl: 'standard-no-transmission',
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
const registry = new ShaderRegistry({
|
|
146
|
+
device: device(),
|
|
147
|
+
manifestUrl: dataUrl({ entries: [], materialShaders: [missingVariant] }),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const loaded = await registry.loadManifest();
|
|
151
|
+
expect(loaded.ok).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('rejects duplicate variant keys instead of allowing ambiguous prewarm', async () => {
|
|
155
|
+
const duplicateVariant = {
|
|
156
|
+
identifier: 'forgeax::default-standard-pbr',
|
|
157
|
+
sourcePath: 'default-standard-pbr.wgsl',
|
|
158
|
+
composedWgsl: 'standard-default',
|
|
159
|
+
paramSchema: '[]',
|
|
160
|
+
variants: [
|
|
161
|
+
{
|
|
162
|
+
definesKey: 'TRANSMISSION_AVAILABLE=true',
|
|
163
|
+
defines: { TRANSMISSION_AVAILABLE: true },
|
|
164
|
+
composedWgsl: 'standard-transmission-a',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
definesKey: 'TRANSMISSION_AVAILABLE=true',
|
|
168
|
+
defines: { TRANSMISSION_AVAILABLE: true },
|
|
169
|
+
composedWgsl: 'standard-transmission-b',
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
const registry = new ShaderRegistry({
|
|
174
|
+
device: device(),
|
|
175
|
+
manifestUrl: dataUrl({ entries: [], materialShaders: [duplicateVariant] }),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const loaded = await registry.loadManifest();
|
|
179
|
+
expect(loaded.ok).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -59,7 +59,7 @@ describe('direct punctual lighting shader contract', () => {
|
|
|
59
59
|
const source = await readFile(pbrShaderPath, 'utf8');
|
|
60
60
|
|
|
61
61
|
expect(source).toContain(
|
|
62
|
-
'evaluate_cluster_lights(in.ndc, in.viewZ, in.worldPos, n, v,
|
|
62
|
+
'evaluate_cluster_lights(in.ndc, in.viewZ, in.worldPos, n, v, diffuseAlbedo, metallic, a, f0)',
|
|
63
63
|
);
|
|
64
64
|
expect(source).toContain('coatAlpha, vec3<f32>(0.04)');
|
|
65
65
|
});
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
const source = readFileSync(
|
|
6
|
+
fileURLToPath(new URL('../points-lines.wgsl', import.meta.url)),
|
|
7
|
+
'utf8',
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
describe('points-lines triangle expansion shader contract', () => {
|
|
11
|
+
it('declares one engine-owned material module and physical viewport input', () => {
|
|
12
|
+
expect(source).toContain('#define_import_path forgeax_material::points_lines');
|
|
13
|
+
expect(source).toContain('struct PointsLinesView');
|
|
14
|
+
expect(source).toContain('physicalViewport');
|
|
15
|
+
expect(source).toContain('model : mat4x4<f32>');
|
|
16
|
+
expect(source).toContain('pointsLinesView.model');
|
|
17
|
+
expect(source).toMatch(/@group\(0\)\s*@binding\(10\)\s+var<uniform>\s+pointsLinesView/);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('expands square and circle points from clip-space pixel deltas', () => {
|
|
21
|
+
expect(source).toContain('fn expandPoint');
|
|
22
|
+
expect(source).toContain('sizePx');
|
|
23
|
+
expect(source).toContain('square');
|
|
24
|
+
expect(source).toContain('circleCoverage');
|
|
25
|
+
expect(source).toContain('sampleCenter');
|
|
26
|
+
expect(source).toContain('physicalViewport');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('expands each butt segment independently without native wide primitive state', () => {
|
|
30
|
+
expect(source).toContain('fn expandLine');
|
|
31
|
+
expect(source).toContain('widthPx');
|
|
32
|
+
expect(source).toContain('butt');
|
|
33
|
+
expect(source).toContain('line-list');
|
|
34
|
+
expect(source).not.toMatch(/pointSize|lineWidth/);
|
|
35
|
+
expect(source).not.toContain('primitive-wide');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('keeps the portable source free of compute and runtime compiler dependencies', () => {
|
|
39
|
+
expect(source).not.toMatch(/@compute|var<storage>|storage-read|indirect/);
|
|
40
|
+
expect(source).not.toMatch(/engine-(?:naga|shader-compiler|wgpu-wasm)/);
|
|
41
|
+
expect(source).not.toMatch(/fn\s+(?:compile|compose|reflect)/);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -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
|
+
});
|