@forgeax/engine-shader 0.1.28 → 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
@@ -9,6 +9,12 @@ struct SurfaceInput {
9
9
  viewDirectionWS : vec3<f32>,
10
10
  uv0 : vec2<f32>,
11
11
  uv1 : vec2<f32>,
12
+ uv2 : vec2<f32>,
13
+ uv3 : vec2<f32>,
14
+ uv4 : vec2<f32>,
15
+ uv5 : vec2<f32>,
16
+ uv6 : vec2<f32>,
17
+ uv7 : vec2<f32>,
12
18
  vertexColor : vec4<f32>,
13
19
  frontFacing : bool,
14
20
  };
@@ -28,11 +28,13 @@ struct TaaResolveParams {
28
28
  currentJitterUv : vec2<f32>,
29
29
  historyValid : u32,
30
30
  temporalFrameIndex : u32,
31
+ hasSecondaryReactivity : u32,
31
32
  };
32
33
 
33
34
  struct TaaResolveOutput {
34
35
  @location(0) color : vec4<f32>,
35
36
  @location(1) temporal : vec4<f32>,
37
+ @location(2) stability : f32,
36
38
  };
37
39
 
38
40
  @group(1) @binding(0) var currentColor : texture_2d<f32>;
@@ -44,6 +46,39 @@ struct TaaResolveOutput {
44
46
  @group(1) @binding(6) var currentTemporal : texture_2d<f32>;
45
47
  @group(1) @binding(7) var currentTemporalSampler : sampler;
46
48
  @group(1) @binding(8) var<uniform> params : TaaResolveParams;
49
+ @group(1) @binding(9) var historyStability : texture_2d<f32>;
50
+ @group(1) @binding(10) var secondaryReactivity : texture_2d<f32>;
51
+
52
+ fn sampleSecondaryReactivity(uv : vec2<f32>) -> f32 {
53
+ if (params.hasSecondaryReactivity == 0u) { return 0.0; }
54
+ let size = vec2<i32>(textureDimensions(secondaryReactivity));
55
+ let center = vec2<i32>(uv * vec2<f32>(size));
56
+ var reactive = 0.0;
57
+ // Conservatively cover the reconstruction footprint, including producers
58
+ // on a reduced-resolution lattice. Never interpolate a motion flag away.
59
+ for (var y = -3; y <= 3; y++) {
60
+ for (var x = -3; x <= 3; x++) {
61
+ let pixel = clamp(center + vec2<i32>(x, y), vec2<i32>(0), size - vec2<i32>(1));
62
+ let value = textureLoad(secondaryReactivity, pixel, 0).r;
63
+ if (value != value || abs(value) > 3.402823e+38) { return 1.0; }
64
+ reactive = max(reactive, clamp(value, 0.0, 1.0));
65
+ }
66
+ }
67
+ return reactive;
68
+ }
69
+
70
+ // One complete eight-phase jitter cycle must settle after motion/rejection.
71
+ // Store the state in a private r8unorm attachment; temporal-v1's
72
+ // reactive lane includes secondary-source changes; receiver velocity and depth
73
+ // remain unchanged for downstream effects such as Motion Blur.
74
+ const TAA_STABILITY_FRAMES : f32 = 8.0;
75
+ const TAA_HISTORY_SETTLE_FRAMES : f32 = 128.0;
76
+
77
+ fn taaStableAge(previous : f32, accepted : bool, temporal : vec4<f32>, priorMotion : vec2<f32>) -> f32 {
78
+ let stationary = max(length(temporal.xy), length(priorMotion)) < 1e-5;
79
+ let count = min(floor(clamp(previous, 0.0, 1.0) * 255.0 + 0.5), TAA_HISTORY_SETTLE_FRAMES);
80
+ return select(0.0, min(count + 1.0, TAA_HISTORY_SETTLE_FRAMES), accepted && temporal.w == 0.0 && stationary);
81
+ }
47
82
 
48
83
  fn rgbToYCoCg(rgb : vec3<f32>) -> vec3<f32> {
49
84
  return vec3<f32>(
@@ -61,17 +96,170 @@ fn luminance(rgb : vec3<f32>) -> f32 {
61
96
  return dot(rgb, vec3<f32>(0.2126, 0.7152, 0.0722));
62
97
  }
63
98
 
64
- fn closestCurrentTemporal(pixel : vec2<i32>, dimensions : vec2<i32>) -> vec4<f32> {
99
+ fn taaAccumulationWeight(frameIndex : u32, steadyWeight : f32) -> f32 {
100
+ let samples = f32(frameIndex);
101
+ return min(steadyWeight, samples / (samples + 1.0));
102
+ }
103
+
104
+ // Karis, High Quality Temporal Supersampling (SIGGRAPH 2014), slide 20.
105
+ // With linear luminance, these normalized weights are equivalent to
106
+ // T(C)=C/(1+luma(C)), temporal blending, then T^-1(C)=C/(1-luma(C)).
107
+ // One scalar per color preserves chroma and avoids an unstable explicit
108
+ // inverse near one. History and the post-processing output remain HDR.
109
+ fn blendTaaHistory(current : vec3<f32>, history : vec3<f32>, historyWeight : f32) -> vec3<f32> {
110
+ let currentWeight = (1.0 - historyWeight) / (1.0 + max(0.0, luminance(current)));
111
+ let previousWeight = historyWeight / (1.0 + max(0.0, luminance(history)));
112
+ return (current * currentWeight + history * previousWeight) / max(currentWeight + previousWeight, 0.00001);
113
+ }
114
+
115
+ // Render-target FP16 conversion can consistently round down. Feeding that
116
+ // error back every frame darkens history, increasingly so at high weights.
117
+ // Choose between the two exactly representable neighbors without bias before
118
+ // the attachment conversion. One shared RGB threshold preserves neutral color;
119
+ // frame/pixel hashing avoids locking rounding to the eight jitter phases.
120
+ fn taaRoundingNoise(pixel : vec2<u32>, frame : u32) -> f32 {
121
+ var bits = pixel.x * 0x9e3779b9u + pixel.y * 0x85ebca6bu + frame * 0xc2b2ae35u;
122
+ bits = (bits ^ (bits >> 16u)) * 0x7feb352du;
123
+ bits = (bits ^ (bits >> 15u)) * 0x846ca68bu;
124
+ bits = bits ^ (bits >> 16u);
125
+ return f32(bits >> 8u) / 16777216.0;
126
+ }
127
+
128
+ fn roundTaaHistory(color : vec3<f32>, noise : f32) -> vec3<f32> {
129
+ let magnitude = min(abs(color), vec3<f32>(65504.0));
130
+ let exponent = (bitcast<vec3<u32>>(magnitude) >> vec3<u32>(23u)) & vec3<u32>(255u);
131
+ // Normal spacing is 2^(exponent - 127 - 10); subnormals use 2^-24.
132
+ let step = bitcast<vec3<f32>>((max(exponent, vec3<u32>(113u)) - vec3<u32>(10u)) << vec3<u32>(23u));
133
+ let scaled = magnitude / step;
134
+ let integral = floor(scaled);
135
+ let rounded = (integral + select(vec3<f32>(0.0), vec3<f32>(1.0), vec3<f32>(noise) < scaled - integral)) * step;
136
+ return select(rounded, -rounded, color < vec3<f32>(0.0));
137
+ }
138
+
139
+ struct TaaCurrentTemporal {
140
+ temporal : vec4<f32>,
141
+ depthEdge : bool,
142
+ };
143
+
144
+ fn closestCurrentTemporal(pixel : vec2<i32>, dimensions : vec2<i32>) -> TaaCurrentTemporal {
65
145
  let clamped = clamp(pixel, vec2<i32>(0), dimensions - vec2<i32>(1));
66
- return textureLoad(currentTemporal, clamped, 0);
146
+ var closest = textureLoad(currentTemporal, clamped, 0);
147
+ var farthestDepth = closest.z;
148
+ var hasBackground = closest.z < 0.0;
149
+ // Jitter changes coverage at silhouettes and subpixel gaps. Select the
150
+ // nearest valid surface in the reconstruction footprint, carrying its
151
+ // motion and reactive flag together. The negative clear depth is absent
152
+ // geometry, not a surface closer than every real sample.
153
+ for (var y = -1; y <= 1; y++) {
154
+ for (var x = -1; x <= 1; x++) {
155
+ let samplePixel = clamp(pixel + vec2<i32>(x, y), vec2<i32>(0), dimensions - vec2<i32>(1));
156
+ let candidate = textureLoad(currentTemporal, samplePixel, 0);
157
+ hasBackground = hasBackground || candidate.z < 0.0;
158
+ farthestDepth = max(farthestDepth, candidate.z);
159
+ if (candidate.z >= 0.0 && (closest.z < 0.0 || candidate.z < closest.z)) {
160
+ closest = candidate;
161
+ }
162
+ }
163
+ }
164
+ // Three.js TRAA exempts current depth edges from the disocclusion test.
165
+ // Use the existing logarithmic-depth tolerance, not a perspective threshold.
166
+ return TaaCurrentTemporal(closest, closest.z >= 0.0 &&
167
+ (hasBackground || farthestDepth - closest.z > 0.0025 + closest.z * 0.01));
67
168
  }
68
169
 
69
- fn neighborhoodSquareMean(uv : vec2<f32>, texel : vec2<f32>) -> vec3<f32> {
70
- let north = textureSampleLevel(currentColor, currentSampler, uv + vec2<f32>(0.0, texel.y), 0.0).rgb;
71
- let south = textureSampleLevel(currentColor, currentSampler, uv - vec2<f32>(0.0, texel.y), 0.0).rgb;
72
- let east = textureSampleLevel(currentColor, currentSampler, uv + vec2<f32>(texel.x, 0.0), 0.0).rgb;
73
- let west = textureSampleLevel(currentColor, currentSampler, uv - vec2<f32>(texel.x, 0.0), 0.0).rgb;
74
- return (north + south + east + west) * 0.25;
170
+ struct TaaBounds {
171
+ minimum : vec3<f32>,
172
+ maximum : vec3<f32>,
173
+ };
174
+
175
+ fn taaBoundsSample(pixel : vec2<i32>, dimensions : vec2<i32>) -> vec3<f32> {
176
+ return rgbToYCoCg(textureLoad(currentColor, clamp(pixel, vec2<i32>(0), dimensions - vec2<i32>(1)), 0).rgb);
177
+ }
178
+
179
+ fn taaBoundsRow(pixel : vec2<i32>, dimensions : vec2<i32>, wide : bool) -> TaaBounds {
180
+ let a = taaBoundsSample(pixel + vec2<i32>(-1, 0), dimensions);
181
+ let b = taaBoundsSample(pixel, dimensions);
182
+ let c = taaBoundsSample(pixel + vec2<i32>(1, 0), dimensions);
183
+ var bounds = TaaBounds(min(min(a, b), c), max(max(a, b), c));
184
+ if (wide) {
185
+ let left = taaBoundsSample(pixel + vec2<i32>(-2, 0), dimensions);
186
+ let right = taaBoundsSample(pixel + vec2<i32>(2, 0), dimensions);
187
+ bounds.minimum = min(bounds.minimum, min(left, right));
188
+ bounds.maximum = max(bounds.maximum, max(left, right));
189
+ }
190
+ return bounds;
191
+ }
192
+
193
+ fn taaNeighborhood(uv : vec2<f32>, current : vec3<f32>, settled : bool) -> TaaBounds {
194
+ let dimensions = vec2<i32>(textureDimensions(currentColor));
195
+ let pixel = vec2<i32>(uv * vec2<f32>(dimensions));
196
+ let center = rgbToYCoCg(current);
197
+ let top = taaBoundsRow(pixel + vec2<i32>(0, -1), dimensions, settled);
198
+ let bottom = taaBoundsRow(pixel + vec2<i32>(0, 1), dimensions, settled);
199
+ var bounds = TaaBounds(
200
+ min(min(top.minimum, bottom.minimum), center),
201
+ max(max(top.maximum, bottom.maximum), center),
202
+ );
203
+ // Keep the moving eight-neighbor footprint and the settled symmetric 5x5
204
+ // support. Reduce directly in YCoCg instead of materializing 25 RGB values
205
+ // and then dynamically indexing them in a second loop.
206
+ if (settled) {
207
+ let first = taaBoundsRow(pixel + vec2<i32>(0, -2), dimensions, true);
208
+ let last = taaBoundsRow(pixel + vec2<i32>(0, 2), dimensions, true);
209
+ let middle = taaBoundsRow(pixel, dimensions, true);
210
+ bounds.minimum = min(bounds.minimum, min(min(first.minimum, last.minimum), middle.minimum));
211
+ bounds.maximum = max(bounds.maximum, max(max(first.maximum, last.maximum), middle.maximum));
212
+ } else {
213
+ let left = taaBoundsSample(pixel + vec2<i32>(-1, 0), dimensions);
214
+ let right = taaBoundsSample(pixel + vec2<i32>(1, 0), dimensions);
215
+ bounds.minimum = min(bounds.minimum, min(left, right));
216
+ bounds.maximum = max(bounds.maximum, max(left, right));
217
+ }
218
+ return bounds;
219
+ }
220
+
221
+ fn clipTaaHistory(neighbors : TaaBounds, history : vec3<f32>) -> vec3<f32> {
222
+ let clipMin = neighbors.minimum;
223
+ let clipMax = neighbors.maximum;
224
+ return yCoCgToRgb(clamp(rgbToYCoCg(history), clipMin, clipMax));
225
+ }
226
+
227
+ struct TaaClipDecision {
228
+ history : vec3<f32>,
229
+ state : f32,
230
+ };
231
+
232
+ fn resolveTaaClipping(clipped : vec3<f32>, history : vec3<f32>, previous : f32, age : f32) -> TaaClipDecision {
233
+ // Codes 0..128 are accepted stationary age. The remaining 42 codes store
234
+ // 1..7 consecutive misses in one of six signed RGB directions. Reversing
235
+ // direction or finding support breaks the streak, so one dark jitter phase
236
+ // cannot erase the accumulated coverage of a thin bright surface. Eight
237
+ // persistent misses restore clipping even without motion. Color clipping
238
+ // must not invalidate the accepted geometric reconstruction footprint.
239
+ if (age < TAA_HISTORY_SETTLE_FRAMES) {
240
+ // Release phase-local clipping while accumulation is still responsive.
241
+ // Waiting until the 0.99 history weight has engaged locks in the earlier
242
+ // clipped mean and makes a static image drift for hundreds more frames.
243
+ return TaaClipDecision(mix(clipped, history, smoothstep(TAA_STABILITY_FRAMES, 64.0, age)), age / 255.0);
244
+ }
245
+ let delta = clipped - history;
246
+ let magnitude = abs(delta);
247
+ var axis = 0u;
248
+ if (magnitude.y > magnitude.x) { axis = 1u; }
249
+ if (magnitude.z > magnitude[axis]) { axis = 2u; }
250
+ let scale = max(1.0, max(abs(history.x), max(abs(history.y), abs(history.z))));
251
+ if (magnitude[axis] <= scale * 0.00001) {
252
+ return TaaClipDecision(history, TAA_HISTORY_SETTLE_FRAMES / 255.0);
253
+ }
254
+ let direction = axis * 2u + select(0u, 1u, delta[axis] > 0.0);
255
+ let code = u32(floor(clamp(previous, 0.0, 1.0) * 255.0 + 0.5));
256
+ var streak = 1u;
257
+ if (code >= 129u && code <= 170u) {
258
+ let prior = code - 129u;
259
+ if (prior / 7u == direction) { streak = prior % 7u + 2u; }
260
+ }
261
+ if (streak >= 8u) { return TaaClipDecision(clipped, TAA_HISTORY_SETTLE_FRAMES / 255.0); }
262
+ return TaaClipDecision(history, f32(129u + direction * 7u + streak - 1u) / 255.0);
75
263
  }
76
264
 
77
265
  @vertex
@@ -81,10 +269,13 @@ fn vs_main(@builtin(vertex_index) vertexIndex : u32) -> FullscreenOutput {
81
269
 
82
270
  fn fs_taa_resolve(in : FullscreenOutput) -> TaaResolveOutput {
83
271
  let dimensions = vec2<i32>(textureDimensions(currentColor, 0));
84
- let texel = 1.0 / vec2<f32>(dimensions);
85
- let current = textureSampleLevel(currentColor, currentSampler, in.uv + params.currentJitterUv, 0.0);
86
- let pixel = vec2<i32>(in.uv * vec2<f32>(dimensions));
87
- let temporal = closestCurrentTemporal(pixel, dimensions);
272
+ let currentUv = in.uv + params.currentJitterUv;
273
+ let current = textureSampleLevel(currentColor, currentSampler, currentUv, 0.0);
274
+ let pixel = vec2<i32>(currentUv * vec2<f32>(dimensions));
275
+ let currentSample = closestCurrentTemporal(pixel, dimensions);
276
+ let sceneTemporal = currentSample.temporal;
277
+ let temporal = vec4<f32>(sceneTemporal.xyz,
278
+ max(sceneTemporal.w, sampleSecondaryReactivity(currentUv)));
88
279
  let historyUv = in.uv - temporal.xy;
89
280
  let historyInBounds = all(historyUv >= vec2<f32>(0.0)) && all(historyUv <= vec2<f32>(1.0));
90
281
  let history = textureSampleLevel(historyColor, historySampler, historyUv, 0.0);
@@ -95,26 +286,29 @@ fn fs_taa_resolve(in : FullscreenOutput) -> TaaResolveOutput {
95
286
  params.historyValid == 0u ||
96
287
  !historyInBounds ||
97
288
  temporal.z < 0.0 ||
98
- previousTemporal.z < 0.0 ||
99
- depthDelta > depthThreshold;
100
- let neighborhood = neighborhoodSquareMean(in.uv, texel);
101
- let neighborhoodDelta = abs(luminance(current.rgb) - luminance(neighborhood));
102
- let clipPadding = vec3<f32>(0.5);
103
- let clipMin = rgbToYCoCg(min(current.rgb, neighborhood - vec3<f32>(neighborhoodDelta) - clipPadding));
104
- let clipMax = rgbToYCoCg(max(current.rgb, neighborhood + vec3<f32>(neighborhoodDelta) + clipPadding));
105
- let clippedHistoryRgb = yCoCgToRgb(clamp(rgbToYCoCg(history.rgb), clipMin, clipMax));
289
+ ((previousTemporal.z < 0.0 || depthDelta > depthThreshold) && !currentSample.depthEdge);
290
+ let historyPixel = clamp(vec2<i32>(historyUv * vec2<f32>(dimensions)), vec2<i32>(0), dimensions - vec2<i32>(1));
291
+ let priorStability = textureLoad(historyStability, historyPixel, 0).r;
292
+ let stableAge = taaStableAge(priorStability, !rejected, temporal, previousTemporal.xy);
293
+ let clipped = clipTaaHistory(taaNeighborhood(currentUv, current.rgb, stableAge >= TAA_STABILITY_FRAMES), history.rgb);
294
+ let clipping = resolveTaaClipping(clipped, history.rgb, priorStability, stableAge);
295
+ let clippedHistoryRgb = clipping.history;
106
296
  let reactiveFactor = 1.0 - clamp(temporal.w, 0.0, 1.0);
107
297
  let velocityFactor = 1.0 - clamp(length(temporal.xy) * 64.0, 0.0, 1.0);
108
- let depthFactor = 1.0 - clamp(depthDelta / depthThreshold, 0.0, 1.0);
109
- let unbiasedLumaDelta = clamp(abs(luminance(current.rgb) - luminance(history.rgb)), 0.0, 1.0);
110
- let lumaFactor = (1.0 - unbiasedLumaDelta) * (1.0 - unbiasedLumaDelta);
111
- let fixedPresetWeight = mix(0.88, 0.97, lumaFactor);
112
- let progressiveWeight = select(fixedPresetWeight, 0.0, params.temporalFrameIndex < 8u);
113
- let historyWeight = progressiveWeight * reactiveFactor * velocityFactor * depthFactor;
114
- let resolved = select(mix(current.rgb, clippedHistoryRgb, historyWeight), current.rgb, rejected);
298
+ // Motion ending is not the same as settled radiance. Keep fast recovery
299
+ // for 64 stationary frames before reducing the residual jitter response.
300
+ // Any rejection, receiver motion, or secondary reactivity resets this age.
301
+ let steadyWeight = mix(0.95, 0.99, smoothstep(64.0, TAA_HISTORY_SETTLE_FRAMES, stableAge));
302
+ let progressiveWeight = taaAccumulationWeight(params.temporalFrameIndex, steadyWeight);
303
+ // Depth is a disocclusion gate, not a continuous history fade. A sloped
304
+ // static surface changes sampled depth under jitter without changing its
305
+ // identity; attenuating accepted history makes those edges oscillate.
306
+ let historyWeight = progressiveWeight * reactiveFactor * velocityFactor;
307
+ let resolved = select(blendTaaHistory(current.rgb, clippedHistoryRgb, historyWeight), current.rgb, rejected);
115
308
  return TaaResolveOutput(
116
- vec4<f32>(resolved, current.a),
309
+ vec4<f32>(roundTaaHistory(resolved, taaRoundingNoise(vec2<u32>(in.position.xy), params.temporalFrameIndex)), current.a),
117
310
  temporal,
311
+ clipping.state,
118
312
  );
119
313
  }
120
314
 
package/src/tbn.wgsl CHANGED
@@ -10,7 +10,7 @@
10
10
  //
11
11
  // RG-only tangent normal decode mirrors pbr.wgsl pre-split semantics
12
12
  // (charter P5 byte-equivalent extraction). The default 1x1 normal fallback
13
- // texture RG=(128,128) decodes to tangent (0,0,1) -- zero perturbation when
13
+ // half-float texture RG=(0.5,0.5) decodes exactly to tangent (0,0,1) when
14
14
  // normalTexture is absent (host-side defaultNormalTextureView). RG encoding
15
15
  // also matches BC5 / RG normal maps and tolerates RGB normal maps (b is
16
16
  // dropped, z is recomputed -- equivalent for unit vectors).
package/src/tonemap.wgsl CHANGED
@@ -7,9 +7,8 @@
7
7
  // `reinhard-extended` remains Forgeax's existing luminance-domain curve; the
8
8
  // distinct `reinhard` mode is Three's per-channel Reinhard curve.
9
9
 
10
- #import forgeax_view::common::FullscreenOutput
11
- #import forgeax_view::common::fullscreen_triangle
12
- #import forgeax_view::common::{linearToSrgbOetf, ditherUnorm8}
10
+ #import forgeax_view::common::{FullscreenOutput, ditherUnorm8, fullscreen_triangle}
11
+ #import forgeax_view::output_encoding::{encodeOutput}
13
12
 
14
13
  const TONEMAP_LUMINANCE_EPSILON : f32 = 1e-5;
15
14
 
@@ -37,7 +36,7 @@ fn tonemapReinhardExtended(color : vec3<f32>) -> vec3<f32> {
37
36
  return color * scale;
38
37
  }
39
38
 
40
- fn tonemapLinear(color : vec3<f32>) -> vec3<f32> {
39
+ fn toneMapLinearLdr(color : vec3<f32>) -> vec3<f32> {
41
40
  return clamp(color, vec3<f32>(0.0), vec3<f32>(1.0));
42
41
  }
43
42
 
@@ -151,30 +150,50 @@ fn tonemapNeutral(color : vec3<f32>) -> vec3<f32> {
151
150
  return mix(compressed, vec3<f32>(new_peak), g);
152
151
  }
153
152
 
154
- @fragment
155
- fn fs_main(in : FullscreenOutput) -> @location(0) vec4<f32> {
156
- // Output Transform consumes linear HDR and emits display-encoded color.
157
- let source = textureSample(hdr, samp, in.uv);
158
- let sample : vec3<f32> = source.rgb;
153
+ fn mapTonemap(sample : vec3<f32>) -> vec3<f32> {
159
154
  let exposed : vec3<f32> = sample * params.exposure;
160
155
  var mapped : vec3<f32>;
161
156
  switch (params.mode) {
162
157
  case 1u: { mapped = tonemapReinhardExtended(exposed); }
163
- case 2u: { mapped = tonemapLinear(exposed); }
158
+ case 2u: { mapped = toneMapLinearLdr(exposed); }
164
159
  case 3u: { mapped = tonemapCineon(exposed); }
165
160
  case 4u: { mapped = tonemapAcesFilmic(exposed); }
166
161
  case 5u: { mapped = tonemapAgx(exposed); }
167
162
  case 6u: { mapped = tonemapNeutral(exposed); }
168
163
  case 7u: { mapped = tonemapReinhard(exposed); }
169
- // Mode 0 skips the tone curve but still performs the output conversion.
170
- default: { mapped = sample; }
164
+ // Mode 0 uses the explicit linear HDR to linear LDR identity/clamp.
165
+ default: { mapped = toneMapLinearLdr(sample); }
171
166
  }
172
- // Every public mode reaches exactly one shared sRGB OETF.
173
- let encoded = linearToSrgbOetf(mapped);
174
- let output = select(
175
- encoded,
176
- ditherUnorm8(encoded, in.position.xy),
167
+ return mapped;
168
+ }
169
+
170
+ fn encodeFinal(linearColor : vec3<f32>, alpha : f32, position : vec4<f32>) -> vec4<f32> {
171
+ let encoded = encodeOutput(linearColor, alpha);
172
+ let outputRgb = select(
173
+ encoded.rgb,
174
+ ditherUnorm8(encoded.rgb, position.xy),
177
175
  params.ditherEnabled > 0.5,
178
176
  );
179
- return vec4<f32>(output, source.a);
177
+ return vec4<f32>(outputRgb, encoded.a);
178
+ }
179
+
180
+ @fragment
181
+ fn fs_main(in : FullscreenOutput) -> @location(0) vec4<f32> {
182
+ // Combined path: tone mapping and the one output encoding happen together.
183
+ let source = textureSample(hdr, samp, in.uv);
184
+ return encodeFinal(mapTonemap(source.rgb), source.a, in.position);
185
+ }
186
+
187
+ @fragment
188
+ fn fs_tone_only(in : FullscreenOutput) -> @location(0) vec4<f32> {
189
+ // LUT-enabled path stops at linear LDR so the LUT never samples encoded data.
190
+ let source = textureSample(hdr, samp, in.uv);
191
+ return vec4<f32>(mapTonemap(source.rgb), source.a);
192
+ }
193
+
194
+ @fragment
195
+ fn fs_encode_only(in : FullscreenOutput) -> @location(0) vec4<f32> {
196
+ // The final writer consumes linear LDR after LUT and optional FXAA.
197
+ let source = textureSample(hdr, samp, in.uv);
198
+ return encodeFinal(source.rgb, source.a, in.position);
180
199
  }
package/src/unlit.wgsl CHANGED
@@ -67,7 +67,7 @@ struct VsIn {
67
67
  #endif
68
68
  };
69
69
  struct VsOut {
70
- @builtin(position) clip : vec4<f32>,
70
+ @builtin(position) @invariant clip : vec4<f32>,
71
71
  @location(0) uv : vec2<f32>,
72
72
  @location(1) worldPos : vec3<f32>,
73
73
  #ifdef VERTEX_COLOR_AVAILABLE
@@ -124,7 +124,7 @@ fn fs_shadow(in : VsOut) {
124
124
  }
125
125
 
126
126
  struct TemporalVsOut {
127
- @builtin(position) clip : vec4<f32>,
127
+ @builtin(position) @invariant clip : vec4<f32>,
128
128
  @location(0) uv : vec2<f32>,
129
129
  @location(1) @interpolate(perspective) currentClip : vec4<f32>,
130
130
  @location(2) @interpolate(perspective) previousClip : vec4<f32>,
@@ -144,7 +144,7 @@ fn vs_temporal(in : VsIn, @builtin(instance_index) idx : u32) -> TemporalVsOut {
144
144
  #endif
145
145
  var out : TemporalVsOut;
146
146
  out.currentClip = view.temporalCurrentViewProj * currentWorld;
147
- out.clip = out.currentClip;
147
+ out.clip = view.worldViewProj * currentWorld;
148
148
  out.previousClip = view.temporalPreviousViewProj * previousWorld;
149
149
  out.uv = in.uv;
150
150
  #ifdef VERTEX_COLOR_AVAILABLE