@forgeax/engine-shader 0.1.27 → 0.1.29

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 (73) hide show
  1. package/NOTICE +35 -0
  2. package/README.md +39 -0
  3. package/dist/ShaderRegistry.d.ts +8 -0
  4. package/dist/ShaderRegistry.d.ts.map +1 -1
  5. package/dist/index.d.ts +6 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.mjs +312 -71
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/material/artifact-types.d.ts +12 -2
  10. package/dist/material/artifact-types.d.ts.map +1 -1
  11. package/dist/material-schemas.d.ts +5 -0
  12. package/dist/material-schemas.d.ts.map +1 -1
  13. package/package.json +5 -4
  14. package/src/ShaderRegistry.ts +10 -0
  15. package/src/__tests__/auto-exposure-graph.unit.test.ts +59 -0
  16. package/src/__tests__/bloom-fxaa-tonemap.unit.test.ts +30 -15
  17. package/src/__tests__/builtin-texture-sampling-contract.test.ts +1 -1
  18. package/src/__tests__/default-standard-pbr-alpha.unit.test.ts +2 -1
  19. package/src/__tests__/default-standard-pbr-transmission.unit.test.ts +167 -6
  20. package/src/__tests__/direct-light-layout.unit.test.ts +1 -1
  21. package/src/__tests__/lighting-punctual.unit.test.ts +29 -1
  22. package/src/__tests__/ltc-provenance.unit.test.ts +13 -2
  23. package/src/__tests__/material-builtins.unit.test.ts +1 -0
  24. package/src/__tests__/material-contract.unit.test.ts +42 -9
  25. package/src/__tests__/material-derived-builtins.integration.test.ts +18 -2
  26. package/src/__tests__/probe-lighting-composition.unit.test.ts +1 -1
  27. package/src/__tests__/reflection-probe-sampling.unit.test.ts +23 -2
  28. package/src/__tests__/scene-temporal.unit.test.ts +10 -0
  29. package/src/__tests__/shader.unit.test.ts +1 -0
  30. package/src/__tests__/sprite-variants.unit.test.ts +1 -0
  31. package/src/__tests__/ssr-artifact.unit.test.ts +141 -0
  32. package/src/__tests__/ssr-bgl.integration.test.ts +185 -0
  33. package/src/__tests__/standard-output-domain.unit.test.ts +36 -0
  34. package/src/__tests__/standard-pbr-artifact-receipt.unit.test.ts +93 -0
  35. package/src/__tests__/standard-surface-pass-evaluation.integration.test.ts +16 -4
  36. package/src/__tests__/surface-v1.unit.test.ts +6 -0
  37. package/src/__tests__/taa-resolve.unit.test.ts +20 -9
  38. package/src/__tests__/transmission-thickness-scale.unit.test.ts +6 -1
  39. package/src/__tests__/transparent-pbr.unit.test.ts +1 -1
  40. package/src/__tests__/vertex-color-variant.unit.test.ts +4 -1
  41. package/src/atmosphere-background.wgsl +3 -3
  42. package/src/auto-exposure-meter.wgsl +179 -0
  43. package/src/color-lut.wgsl +17 -0
  44. package/src/common.wgsl +7 -5
  45. package/src/default-standard-pbr-skin.wgsl +142 -40
  46. package/src/default-standard-pbr.wgsl +198 -82
  47. package/src/default_standard_surface.wgsl +56 -19
  48. package/src/fxaa.wgsl +9 -5
  49. package/src/ibl-sampling.wgsl +30 -6
  50. package/src/index.ts +189 -0
  51. package/src/lighting-punctual.wgsl +5 -6
  52. package/src/material/artifact-types.ts +109 -63
  53. package/src/material-schemas.ts +84 -8
  54. package/src/output-encoding.wgsl +10 -0
  55. package/src/pbr-temporal.wgsl +9 -1
  56. package/src/scene-temporal.wgsl +2 -0
  57. package/src/shadow-pcf.wgsl +6 -8
  58. package/src/shadow-surface.wgsl +16 -0
  59. package/src/shadow_caster.wgsl +99 -61
  60. package/src/sprite-lit.wgsl +4 -4
  61. package/src/sprite.wgsl +3 -3
  62. package/src/ssr-compose.wgsl +129 -0
  63. package/src/ssr-hiz-reduce.wgsl +53 -0
  64. package/src/ssr-hiz.wgsl +66 -0
  65. package/src/ssr-temporal.wgsl +305 -0
  66. package/src/ssr-trace.wgsl +784 -0
  67. package/src/standard-cluster.wgsl +6 -4
  68. package/src/standard-surface.wgsl +696 -0
  69. package/src/surface_v1.wgsl +6 -0
  70. package/src/taa-resolve.wgsl +222 -28
  71. package/src/tbn.wgsl +1 -1
  72. package/src/tonemap.wgsl +37 -18
  73. package/src/unlit.wgsl +3 -3
@@ -0,0 +1,305 @@
1
+ #define_import_path forgeax_ssr::temporal
2
+
3
+ // SSR temporal resolve keeps history as a renderer-owned consumer resource.
4
+ // The alpha channel stores the source view depth, so a history sample can be
5
+ // rejected without creating a second depth-history allocation.
6
+ struct SsrTemporalParams {
7
+ historyValid : u32,
8
+ maxHistoryWeight : f32,
9
+ depthThreshold : f32,
10
+ normalThreshold : f32,
11
+ currentJitterUv : vec2<f32>,
12
+ previousJitterUv : vec2<f32>,
13
+ };
14
+
15
+ @group(0) @binding(0) var currentTrace : texture_2d<f32>;
16
+ @group(0) @binding(1) var currentDepth : texture_depth_2d;
17
+ @group(0) @binding(2) var currentNormal : texture_2d<f32>;
18
+ @group(0) @binding(3) var previousHistory : texture_2d<f32>;
19
+ @group(0) @binding(4) var currentTemporal : texture_2d<f32>;
20
+ @group(0) @binding(5) var historyOutput : texture_storage_2d<rgba16float, write>;
21
+ @group(0) @binding(6) var<uniform> params : SsrTemporalParams;
22
+ // Keep the resolved confidence separate from the history depth lane. History
23
+ // alpha remains the source view depth required for rejection.
24
+ @group(0) @binding(7) var resolvedOutput : texture_storage_2d<rgba16float, write>;
25
+ #import forgeax_view::common::View
26
+ @group(0) @binding(8) var<uniform> view : View;
27
+ // Four bytes retain the actual world normal (RGB) and confidence (A).
28
+ // Reconstructing a prior shading normal from neighboring half-float depths
29
+ // confuses thin side faces with the adjoining top surface.
30
+ @group(0) @binding(9) var previousSurface : texture_2d<f32>;
31
+ @group(0) @binding(10) var surfaceOutput : texture_storage_2d<rgba8unorm, write>;
32
+ @group(0) @binding(11) var currentHitReactivity : texture_2d<f32>;
33
+
34
+ fn isFinite(value : f32) -> bool {
35
+ return value == value && abs(value) < 3.402823e+38;
36
+ }
37
+
38
+ fn finiteUnit(value : f32) -> f32 {
39
+ return select(0.0, clamp(value, 0.0, 1.0), isFinite(value));
40
+ }
41
+
42
+ fn finitePositive(value : f32) -> f32 {
43
+ return select(0.0, value, isFinite(value) && value >= 0.0);
44
+ }
45
+
46
+ fn reprojectUv(uv : vec2<f32>, motion : vec2<f32>) -> vec2<f32> {
47
+ // Both history slots use the fixed unjittered lattice. Jitter belongs
48
+ // only to reconstruction of this frame's trace, never to history feedback.
49
+ return uv - motion;
50
+ }
51
+
52
+ fn depthReject(current : f32, previous : f32) -> bool {
53
+ if (!isFinite(current) || !isFinite(previous) || current < 0.0 || previous < 0.0) {
54
+ return true;
55
+ }
56
+ let threshold = max(params.depthThreshold, 1e-4) + current * 0.01;
57
+ return abs(current - previous) > threshold;
58
+ }
59
+
60
+ fn normalReject(current : vec3<f32>, previous : vec3<f32>) -> bool {
61
+ let currentLength = length(current);
62
+ let previousLength = length(previous);
63
+ if (!isFinite(currentLength) || !isFinite(previousLength) || currentLength <= 1e-4 || previousLength <= 1e-4) {
64
+ return true;
65
+ }
66
+ let agreement = dot(current / currentLength, previous / previousLength);
67
+ return !isFinite(agreement) || agreement < clamp(params.normalThreshold, -1.0, 1.0);
68
+ }
69
+
70
+ fn viewDistance(depth : f32) -> f32 {
71
+ let near = view.temporalProjection.x;
72
+ let far = view.temporalProjection.y;
73
+ return select(0.0, near * far / max(far - depth * (far - near), 1e-5),
74
+ isFinite(depth) && depth >= 0.0 && depth < 1.0);
75
+ }
76
+
77
+ fn ssrLatticeCoordinate(uv : vec2<f32>, fullSize : vec2<u32>) -> vec2<f32> {
78
+ // Half-resolution centers correspond to full pixels 2*p + 0.5.
79
+ return (uv * vec2<f32>(fullSize) - vec2<f32>(0.5)) * 0.5;
80
+ }
81
+
82
+ fn ssrHistoryTap(pixel : vec2<i32>, expectedDepth : f32, normal : vec3<f32>, footprintWeight : f32) -> vec4<f32> {
83
+ let history = textureLoad(previousHistory, pixel, 0);
84
+ let surface = textureLoad(previousSurface, pixel, 0);
85
+ if (depthReject(expectedDepth, history.a) || normalReject(normal, surface.xyz * 2.0 - 1.0)) {
86
+ return vec4<f32>(0.0);
87
+ }
88
+ let weight = footprintWeight * finiteUnit(surface.a);
89
+ return vec4<f32>(history.rgb * weight, weight);
90
+ }
91
+
92
+ fn sampleSsrHistory(uv : vec2<f32>, fullSize : vec2<u32>, expectedDepth : f32, normal : vec3<f32>) -> vec4<f32> {
93
+ let coordinate = ssrLatticeCoordinate(uv, fullSize);
94
+ let first = vec2<i32>(floor(coordinate));
95
+ let fraction = fract(coordinate);
96
+ let last = vec2<i32>(textureDimensions(previousHistory, 0)) - vec2<i32>(1);
97
+ // Fixed taps expose the same two-dimensional footprint without dynamic
98
+ // loop control. Keep accumulation order and receiver rejection unchanged.
99
+ var sum = vec4<f32>(0.0);
100
+ sum += ssrHistoryTap(clamp(first + vec2<i32>(0, 0), vec2<i32>(0), last),
101
+ expectedDepth, normal, (1.0 - fraction.x) * (1.0 - fraction.y));
102
+ sum += ssrHistoryTap(clamp(first + vec2<i32>(1, 0), vec2<i32>(0), last),
103
+ expectedDepth, normal, fraction.x * (1.0 - fraction.y));
104
+ sum += ssrHistoryTap(clamp(first + vec2<i32>(0, 1), vec2<i32>(0), last),
105
+ expectedDepth, normal, (1.0 - fraction.x) * fraction.y);
106
+ sum += ssrHistoryTap(clamp(first + vec2<i32>(1, 1), vec2<i32>(0), last),
107
+ expectedDepth, normal, fraction.x * fraction.y);
108
+ return vec4<f32>(sum.rgb / max(sum.a, 1e-6), sum.a);
109
+ }
110
+
111
+ struct SsrCurrentSample {
112
+ color : vec4<f32>,
113
+ reactivity : f32,
114
+ };
115
+
116
+ // A tap carries confidence-premultiplied color until the footprint is summed.
117
+ fn ssrCurrentTap(pixel : vec2<i32>, expectedDepth : f32, normal : vec3<f32>, footprintWeight : f32) -> SsrCurrentSample {
118
+ let sourceNormal = textureLoad(currentNormal, pixel * 2, 0).xyz * 2.0 - 1.0;
119
+ let sourceDepth = viewDistance(textureLoad(currentDepth, pixel * 2, 0));
120
+ if (depthReject(expectedDepth, sourceDepth) || normalReject(normal, sourceNormal)) {
121
+ return SsrCurrentSample(vec4<f32>(0.0), 0.0);
122
+ }
123
+ let sample = textureLoad(currentTrace, pixel, 0);
124
+ let weight = footprintWeight * finiteUnit(sample.a);
125
+ // Source reactivity is independent of radiance confidence, including misses.
126
+ var reactivity = 0.0;
127
+ if (footprintWeight > 0.0) {
128
+ reactivity = finiteUnit(textureLoad(currentHitReactivity, pixel, 0).r);
129
+ }
130
+ return SsrCurrentSample(vec4<f32>(sample.rgb * weight, weight), reactivity);
131
+ }
132
+
133
+ fn sampleCurrentSsr(uv : vec2<f32>, fullSize : vec2<u32>, expectedDepth : f32, normal : vec3<f32>) -> SsrCurrentSample {
134
+ let coordinate = ssrLatticeCoordinate(uv, fullSize);
135
+ let first = vec2<i32>(floor(coordinate));
136
+ let fraction = fract(coordinate);
137
+ let last = vec2<i32>(textureDimensions(currentTrace, 0)) - vec2<i32>(1);
138
+ var sum = vec4<f32>(0.0);
139
+ var reactivity = 0.0;
140
+ let tap00 = ssrCurrentTap(clamp(first + vec2<i32>(0, 0), vec2<i32>(0), last),
141
+ expectedDepth, normal, (1.0 - fraction.x) * (1.0 - fraction.y));
142
+ sum += tap00.color;
143
+ reactivity = max(reactivity, tap00.reactivity);
144
+ let tap10 = ssrCurrentTap(clamp(first + vec2<i32>(1, 0), vec2<i32>(0), last),
145
+ expectedDepth, normal, fraction.x * (1.0 - fraction.y));
146
+ sum += tap10.color;
147
+ reactivity = max(reactivity, tap10.reactivity);
148
+ let tap01 = ssrCurrentTap(clamp(first + vec2<i32>(0, 1), vec2<i32>(0), last),
149
+ expectedDepth, normal, (1.0 - fraction.x) * fraction.y);
150
+ sum += tap01.color;
151
+ reactivity = max(reactivity, tap01.reactivity);
152
+ let tap11 = ssrCurrentTap(clamp(first + vec2<i32>(1, 1), vec2<i32>(0), last),
153
+ expectedDepth, normal, fraction.x * fraction.y);
154
+ sum += tap11.color;
155
+ reactivity = max(reactivity, tap11.reactivity);
156
+ return SsrCurrentSample(vec4<f32>(sum.rgb / max(sum.a, 1e-6), sum.a), reactivity);
157
+ }
158
+
159
+ // Reflection-only mip reduction. Confidence weighting avoids dark fringes
160
+ // where an SSR hit borders an unavailable screen-space sample.
161
+ @compute @workgroup_size(8, 8, 1)
162
+ fn ssr_reflection_mip(@builtin(global_invocation_id) id : vec3<u32>) {
163
+ let size = textureDimensions(resolvedOutput);
164
+ if (any(id.xy >= size)) { return; }
165
+ let sourceSize = textureDimensions(currentTrace, 0);
166
+ // For exact even 2x2 footprints, use four explicit loads. Hardware linear
167
+ // filtering is intentionally avoided here: rgba16float filtering is allowed
168
+ // to round differently between browser and Dawn backends, which would make
169
+ // the captured SSR result non-portable at high-contrast reflection edges.
170
+ if (sourceSize.x == size.x * 2u && sourceSize.y == size.y * 2u) {
171
+ let source = id.xy * vec2<u32>(2u);
172
+ let topLeft = textureLoad(currentTrace, vec2<i32>(source), 0);
173
+ let topRight = textureLoad(currentTrace, vec2<i32>(source + vec2<u32>(1u, 0u)), 0);
174
+ let bottomLeft = textureLoad(currentTrace, vec2<i32>(source + vec2<u32>(0u, 1u)), 0);
175
+ let bottomRight = textureLoad(currentTrace, vec2<i32>(source + vec2<u32>(1u, 1u)), 0);
176
+ textureStore(resolvedOutput, vec2<i32>(id.xy),
177
+ (topLeft + topRight + bottomLeft + bottomRight) * 0.25);
178
+ return;
179
+ }
180
+ let first = vec2<i32>(id.xy * sourceSize / size);
181
+ let end = vec2<i32>((id.xy + vec2<u32>(1)) * sourceSize / size);
182
+ var sum = vec4<f32>(0.0);
183
+ var count = 0.0;
184
+ for (var y = first.y; y < end.y; y++) {
185
+ for (var x = first.x; x < end.x; x++) {
186
+ let sample = textureLoad(currentTrace, vec2<i32>(x, y), 0);
187
+ let coverage = clamp(sample.a, 0.0, 1.0);
188
+ // The presentation level stores premultiplied radiance. Keep that
189
+ // representation through every pyramid level so a filtered miss does
190
+ // not pull black/invalid color into a valid hit.
191
+ sum += vec4<f32>(sample.rgb, coverage);
192
+ count += 1.0;
193
+ }
194
+ }
195
+ textureStore(resolvedOutput, vec2<i32>(id.xy), vec4<f32>(sum.rgb / max(count, 1.0), sum.a / max(count, 1.0)));
196
+ }
197
+
198
+ struct NeighborhoodBounds {
199
+ lower : vec3<f32>,
200
+ upper : vec3<f32>,
201
+ };
202
+
203
+ fn ssrNeighborhoodRow(pixel : vec2<i32>, last : vec2<i32>) -> NeighborhoodBounds {
204
+ let left = textureLoad(currentTrace, clamp(pixel + vec2<i32>(-1, 0), vec2<i32>(0), last), 0).rgb;
205
+ let center = textureLoad(currentTrace, clamp(pixel, vec2<i32>(0), last), 0).rgb;
206
+ let right = textureLoad(currentTrace, clamp(pixel + vec2<i32>(1, 0), vec2<i32>(0), last), 0).rgb;
207
+ return NeighborhoodBounds(min(min(left, center), right), max(max(left, center), right));
208
+ }
209
+
210
+ fn neighborhoodClamp(pixel : vec2<i32>, size : vec2<u32>) -> NeighborhoodBounds {
211
+ let last = vec2<i32>(size) - vec2<i32>(1);
212
+ let top = ssrNeighborhoodRow(pixel + vec2<i32>(0, -1), last);
213
+ let center = ssrNeighborhoodRow(pixel, last);
214
+ let bottom = ssrNeighborhoodRow(pixel + vec2<i32>(0, 1), last);
215
+ return NeighborhoodBounds(
216
+ min(min(top.lower, center.lower), bottom.lower),
217
+ max(max(top.upper, center.upper), bottom.upper),
218
+ );
219
+ }
220
+
221
+ fn resolveSsrTemporal(
222
+ current : vec4<f32>,
223
+ history : vec4<f32>,
224
+ historyConfidence : f32,
225
+ lower : vec3<f32>,
226
+ upper : vec3<f32>,
227
+ historyInBounds : bool,
228
+ depthCompatible : bool,
229
+ normalCompatible : bool,
230
+ temporal : vec4<f32>,
231
+ ) -> vec4<f32> {
232
+ let clampedHistory = select(history.rgb, clamp(history.rgb, lower, upper), current.a > 0.0);
233
+ let reactiveFactor = 1.0 - finiteUnit(temporal.w);
234
+ let velocityFactor = 1.0 - finiteUnit(length(temporal.xy) * 64.0);
235
+ let requestedWeight = finiteUnit(params.maxHistoryWeight);
236
+ let accepted = params.historyValid != 0u && historyInBounds && depthCompatible && normalCompatible && historyConfidence > 0.001;
237
+ let weight = select(0.0, min(requestedWeight, 0.9) * reactiveFactor * velocityFactor, accepted);
238
+ // Accumulate premultiplied confidence, not black radiance from a miss.
239
+ // An accepted history survives a transient miss with bounded decay, while
240
+ // a new hit with no valid history is immediately visible.
241
+ let currentMass = (1.0 - weight) * finiteUnit(current.a);
242
+ let historyMass = weight * finiteUnit(historyConfidence);
243
+ let confidence = currentMass + historyMass;
244
+ let resolved = (current.rgb * currentMass + clampedHistory * historyMass) / max(confidence, 1e-6);
245
+ return vec4<f32>(resolved, confidence);
246
+ }
247
+
248
+ struct SsrTemporalSample {
249
+ color : vec4<f32>,
250
+ normal : vec3<f32>,
251
+ depth : f32,
252
+ };
253
+
254
+ fn resolveSsrAt(uv : vec2<f32>, fullSize : vec2<u32>) -> SsrTemporalSample {
255
+ let currentUv = uv + params.currentJitterUv;
256
+ let fullPixel = clamp(vec2<i32>(currentUv * vec2<f32>(fullSize)), vec2<i32>(0), vec2<i32>(fullSize) - vec2<i32>(1));
257
+ let depthNdc = textureLoad(currentDepth, vec2<i32>(fullPixel), 0);
258
+ let currentDepthSample = viewDistance(depthNdc);
259
+ let temporal = textureLoad(currentTemporal, fullPixel, 0);
260
+ let historyUv = reprojectUv(uv, temporal.xy);
261
+ let historyInBounds = all(historyUv >= vec2<f32>(0.0)) && all(historyUv <= vec2<f32>(1.0));
262
+ let normal = textureLoad(currentNormal, vec2<i32>(fullPixel), 0).xyz * 2.0 - vec3<f32>(1.0);
263
+ let sourceUv = (vec2<f32>(fullPixel) + vec2<f32>(0.5)) / vec2<f32>(fullSize);
264
+ let worldH = view.inverseViewProj * vec4<f32>(sourceUv.x * 2.0 - 1.0, 1.0 - sourceUv.y * 2.0, depthNdc, 1.0);
265
+ let priorClip = view.temporalPreviousViewProj * vec4<f32>(worldH.xyz / worldH.w, 1.0);
266
+ let current = sampleCurrentSsr(currentUv, fullSize, currentDepthSample, normal);
267
+ let history = sampleSsrHistory(historyUv, fullSize, priorClip.w, normal);
268
+ let size = textureDimensions(currentTrace, 0);
269
+ let pixel = clamp(vec2<i32>(floor(ssrLatticeCoordinate(currentUv, fullSize) + vec2<f32>(0.5))), vec2<i32>(0), vec2<i32>(size) - vec2<i32>(1));
270
+ let bounds = neighborhoodClamp(pixel, size);
271
+ let resolved = resolveSsrTemporal(
272
+ current.color,
273
+ history,
274
+ history.a,
275
+ bounds.lower,
276
+ bounds.upper,
277
+ historyInBounds,
278
+ currentDepthSample > 0.0,
279
+ true,
280
+ vec4<f32>(temporal.xyz, max(temporal.w, current.reactivity)),
281
+ );
282
+ return SsrTemporalSample(resolved, normal, finitePositive(currentDepthSample));
283
+ }
284
+
285
+ @compute @workgroup_size(8, 8, 1)
286
+ fn ssr_temporal(@builtin(global_invocation_id) globalId : vec3<u32>) {
287
+ let size = textureDimensions(currentTrace, 0);
288
+ if (any(globalId.xy >= size)) { return; }
289
+ let pixel = vec2<i32>(globalId.xy);
290
+ let fullSize = textureDimensions(currentDepth, 0);
291
+ let uv = (vec2<f32>(globalId.xy * 2u) + vec2<f32>(0.5)) / vec2<f32>(fullSize);
292
+ let history = resolveSsrAt(uv, fullSize);
293
+ textureStore(historyOutput, pixel, vec4<f32>(history.color.rgb, history.depth));
294
+ textureStore(surfaceOutput, pixel, vec4<f32>(history.normal * 0.5 + 0.5, history.color.a));
295
+ // Presentation remains on the current raster lattice expected by material
296
+ // composition and the reflection mips. Store its radiance premultiplied by
297
+ // confidence so the presentation pyramid can use hardware filtering without
298
+ // darkening miss footprints. This output never feeds persistent history back
299
+ // into itself; historyOutput above remains straight radiance + depth.
300
+ var presented = history.color;
301
+ if (any(params.currentJitterUv != vec2<f32>(0.0))) {
302
+ presented = resolveSsrAt(uv - params.currentJitterUv, fullSize).color;
303
+ }
304
+ textureStore(resolvedOutput, pixel, vec4<f32>(presented.rgb * presented.a, presented.a));
305
+ }