@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.
Files changed (117) hide show
  1. package/README.md +34 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/ShaderRegistry.d.ts.map +1 -1
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.mjs +53 -2
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/material-schemas.d.ts.map +1 -1
  9. package/package.json +6 -3
  10. package/src/ShaderRegistry.ts +50 -0
  11. package/src/__tests__/bloom-fxaa-tonemap.unit.test.ts +86 -12
  12. package/src/__tests__/default-standard-pbr-transmission.unit.test.ts +185 -0
  13. package/src/__tests__/fog-oracle.unit.test.ts +110 -114
  14. package/src/__tests__/fog-orthographic-ray.unit.test.ts +12 -37
  15. package/src/__tests__/fog-producer-matrix.integration.test.ts +16 -51
  16. package/src/__tests__/lighting-point-spot-volume.unit.test.ts +48 -0
  17. package/src/__tests__/lighting-punctual.unit.test.ts +1 -1
  18. package/src/__tests__/lighting-spot-volume-attenuation.unit.test.ts +26 -0
  19. package/src/__tests__/lighting-spot-volume.unit.test.ts +25 -0
  20. package/src/__tests__/material-builtins.unit.test.ts +38 -1
  21. package/src/__tests__/material-contract.unit.test.ts +123 -0
  22. package/src/__tests__/motion-blur.unit.test.ts +44 -0
  23. package/src/__tests__/scene-temporal.unit.test.ts +23 -5
  24. package/src/__tests__/shader-manifest-transmission.unit.test.ts +75 -0
  25. package/src/__tests__/shader.unit.test.ts +44 -3
  26. package/src/__tests__/sprite-variants.unit.test.ts +20 -0
  27. package/src/__tests__/taa-resolve.unit.test.ts +75 -3
  28. package/src/__tests__/transmission-thickness-scale.unit.test.ts +19 -0
  29. package/src/common.wgsl +34 -65
  30. package/src/default-standard-pbr-skin.wgsl +29 -28
  31. package/src/default-standard-pbr.wgsl +257 -57
  32. package/src/fxaa.wgsl +5 -6
  33. package/src/hdrp-cluster-forward.wgsl +41 -3
  34. package/src/hdrp-deferred-lighting.wgsl +3 -27
  35. package/src/index.ts +3 -0
  36. package/src/lighting-attenuation.wgsl +58 -0
  37. package/src/lighting-directional.wgsl +31 -3
  38. package/src/lighting-punctual.wgsl +57 -12
  39. package/src/material-schemas.ts +7 -0
  40. package/src/motion-blur.wgsl +77 -0
  41. package/src/msdf-text.wgsl +4 -23
  42. package/src/pbr-temporal.wgsl +15 -2
  43. package/src/scene-temporal.wgsl +54 -0
  44. package/src/shadow-pcf.wgsl +31 -10
  45. package/src/shadow_caster.wgsl +27 -2
  46. package/src/skybox.wgsl +2 -7
  47. package/src/sprite-lit.wgsl +7 -25
  48. package/src/sprite.wgsl +7 -25
  49. package/src/taa-resolve.wgsl +76 -81
  50. package/src/tonemap.wgsl +7 -9
  51. package/src/unlit.wgsl +7 -28
  52. package/src/volume/volume-composite.wgsl +113 -0
  53. package/src/volume/volume-inject.wgsl +249 -0
  54. package/src/volume/volume-integrate.wgsl +342 -0
  55. package/src/volume/volume-temporal.wgsl +70 -0
  56. package/dist/__tests__/atmosphere-background.test.d.ts +0 -2
  57. package/dist/__tests__/atmosphere-background.test.d.ts.map +0 -1
  58. package/dist/__tests__/atmosphere-cubemap-orientation.test.d.ts +0 -2
  59. package/dist/__tests__/atmosphere-cubemap-orientation.test.d.ts.map +0 -1
  60. package/dist/__tests__/atmosphere-preetham-numeric.test.d.ts +0 -2
  61. package/dist/__tests__/atmosphere-preetham-numeric.test.d.ts.map +0 -1
  62. package/dist/__tests__/atmosphere-preetham-red.test.d.ts +0 -2
  63. package/dist/__tests__/atmosphere-preetham-red.test.d.ts.map +0 -1
  64. package/dist/__tests__/atmosphere-preetham.test.d.ts +0 -2
  65. package/dist/__tests__/atmosphere-preetham.test.d.ts.map +0 -1
  66. package/dist/__tests__/atmosphere-three-r184-reference.test.d.ts +0 -2
  67. package/dist/__tests__/atmosphere-three-r184-reference.test.d.ts.map +0 -1
  68. package/dist/__tests__/bloom-fxaa-tonemap.unit.test.d.ts +0 -2
  69. package/dist/__tests__/bloom-fxaa-tonemap.unit.test.d.ts.map +0 -1
  70. package/dist/__tests__/builtin-texture-sampling-contract.test.d.ts +0 -2
  71. package/dist/__tests__/builtin-texture-sampling-contract.test.d.ts.map +0 -1
  72. package/dist/__tests__/default-standard-pbr-alpha.unit.test.d.ts +0 -2
  73. package/dist/__tests__/default-standard-pbr-alpha.unit.test.d.ts.map +0 -1
  74. package/dist/__tests__/deferred-lighting-ssao.test.d.ts +0 -2
  75. package/dist/__tests__/deferred-lighting-ssao.test.d.ts.map +0 -1
  76. package/dist/__tests__/fog-oracle.unit.test.d.ts +0 -2
  77. package/dist/__tests__/fog-oracle.unit.test.d.ts.map +0 -1
  78. package/dist/__tests__/fog-orthographic-ray.unit.test.d.ts +0 -2
  79. package/dist/__tests__/fog-orthographic-ray.unit.test.d.ts.map +0 -1
  80. package/dist/__tests__/fog-producer-matrix.integration.test.d.ts +0 -2
  81. package/dist/__tests__/fog-producer-matrix.integration.test.d.ts.map +0 -1
  82. package/dist/__tests__/hdrp-ssao-compile.test.d.ts +0 -2
  83. package/dist/__tests__/hdrp-ssao-compile.test.d.ts.map +0 -1
  84. package/dist/__tests__/ibl-irradiance.unit.test.d.ts +0 -2
  85. package/dist/__tests__/ibl-irradiance.unit.test.d.ts.map +0 -1
  86. package/dist/__tests__/ibl-sampling.unit.test.d.ts +0 -2
  87. package/dist/__tests__/ibl-sampling.unit.test.d.ts.map +0 -1
  88. package/dist/__tests__/lighting-punctual.unit.test.d.ts +0 -2
  89. package/dist/__tests__/lighting-punctual.unit.test.d.ts.map +0 -1
  90. package/dist/__tests__/material-artifact-dedup.unit.test.d.ts +0 -2
  91. package/dist/__tests__/material-artifact-dedup.unit.test.d.ts.map +0 -1
  92. package/dist/__tests__/material-builtins.unit.test.d.ts +0 -2
  93. package/dist/__tests__/material-builtins.unit.test.d.ts.map +0 -1
  94. package/dist/__tests__/material-contract.unit.test.d.ts +0 -2
  95. package/dist/__tests__/material-contract.unit.test.d.ts.map +0 -1
  96. package/dist/__tests__/material-derived-builtins.integration.test.d.ts +0 -2
  97. package/dist/__tests__/material-derived-builtins.integration.test.d.ts.map +0 -1
  98. package/dist/__tests__/points-lines-shader.unit.test.d.ts +0 -2
  99. package/dist/__tests__/points-lines-shader.unit.test.d.ts.map +0 -1
  100. package/dist/__tests__/public-surface.unit.test.d.ts +0 -2
  101. package/dist/__tests__/public-surface.unit.test.d.ts.map +0 -1
  102. package/dist/__tests__/scene-temporal.unit.test.d.ts +0 -2
  103. package/dist/__tests__/scene-temporal.unit.test.d.ts.map +0 -1
  104. package/dist/__tests__/shader-registry-transaction.unit.test.d.ts +0 -2
  105. package/dist/__tests__/shader-registry-transaction.unit.test.d.ts.map +0 -1
  106. package/dist/__tests__/shader.unit.test.d.ts +0 -2
  107. package/dist/__tests__/shader.unit.test.d.ts.map +0 -1
  108. package/dist/__tests__/sprite-lit-shader.test.d.ts +0 -2
  109. package/dist/__tests__/sprite-lit-shader.test.d.ts.map +0 -1
  110. package/dist/__tests__/sprite-variants.unit.test.d.ts +0 -2
  111. package/dist/__tests__/sprite-variants.unit.test.d.ts.map +0 -1
  112. package/dist/__tests__/taa-resolve.unit.test.d.ts +0 -2
  113. package/dist/__tests__/taa-resolve.unit.test.d.ts.map +0 -1
  114. package/dist/__tests__/transparent-pbr.unit.test.d.ts +0 -2
  115. package/dist/__tests__/transparent-pbr.unit.test.d.ts.map +0 -1
  116. package/dist/__tests__/vertex-color-variant.unit.test.d.ts +0 -2
  117. 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(6);
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.each(localTemporalShaders)('%s linearly interpolates both clip authorities', (file) => {
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\(linear\) currentClip : vec4<f32>/);
16
- expect(source).toMatch(/@interpolate\(linear\) previousClip : vec4<f32>/);
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\(linear\) currentClip : vec4<f32>/);
29
- expect(source).toMatch(/@interpolate\(linear\) previousClip : vec4<f32>/);
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(21);
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 its texture map.
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('output.temporal = temporal');
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(sampledHistory.rgb), clipMin, clipMax)');
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
+ });