@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.
- package/NOTICE +35 -0
- package/README.md +39 -0
- package/dist/ShaderRegistry.d.ts +8 -0
- package/dist/ShaderRegistry.d.ts.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +312 -71
- package/dist/index.mjs.map +1 -1
- package/dist/material/artifact-types.d.ts +12 -2
- package/dist/material/artifact-types.d.ts.map +1 -1
- package/dist/material-schemas.d.ts +5 -0
- package/dist/material-schemas.d.ts.map +1 -1
- package/package.json +5 -4
- package/src/ShaderRegistry.ts +10 -0
- package/src/__tests__/auto-exposure-graph.unit.test.ts +59 -0
- package/src/__tests__/bloom-fxaa-tonemap.unit.test.ts +30 -15
- package/src/__tests__/builtin-texture-sampling-contract.test.ts +1 -1
- package/src/__tests__/default-standard-pbr-alpha.unit.test.ts +2 -1
- package/src/__tests__/default-standard-pbr-transmission.unit.test.ts +167 -6
- package/src/__tests__/direct-light-layout.unit.test.ts +1 -1
- package/src/__tests__/lighting-punctual.unit.test.ts +29 -1
- package/src/__tests__/ltc-provenance.unit.test.ts +13 -2
- package/src/__tests__/material-builtins.unit.test.ts +1 -0
- package/src/__tests__/material-contract.unit.test.ts +42 -9
- package/src/__tests__/material-derived-builtins.integration.test.ts +18 -2
- package/src/__tests__/probe-lighting-composition.unit.test.ts +1 -1
- package/src/__tests__/reflection-probe-sampling.unit.test.ts +23 -2
- package/src/__tests__/scene-temporal.unit.test.ts +10 -0
- package/src/__tests__/shader.unit.test.ts +1 -0
- package/src/__tests__/sprite-variants.unit.test.ts +1 -0
- package/src/__tests__/ssr-artifact.unit.test.ts +141 -0
- package/src/__tests__/ssr-bgl.integration.test.ts +185 -0
- package/src/__tests__/standard-output-domain.unit.test.ts +36 -0
- package/src/__tests__/standard-pbr-artifact-receipt.unit.test.ts +93 -0
- package/src/__tests__/standard-surface-pass-evaluation.integration.test.ts +16 -4
- package/src/__tests__/surface-v1.unit.test.ts +6 -0
- package/src/__tests__/taa-resolve.unit.test.ts +20 -9
- package/src/__tests__/transmission-thickness-scale.unit.test.ts +6 -1
- package/src/__tests__/transparent-pbr.unit.test.ts +1 -1
- package/src/__tests__/vertex-color-variant.unit.test.ts +4 -1
- package/src/atmosphere-background.wgsl +3 -3
- package/src/auto-exposure-meter.wgsl +179 -0
- package/src/color-lut.wgsl +17 -0
- package/src/common.wgsl +7 -5
- package/src/default-standard-pbr-skin.wgsl +142 -40
- package/src/default-standard-pbr.wgsl +198 -82
- package/src/default_standard_surface.wgsl +56 -19
- package/src/fxaa.wgsl +9 -5
- package/src/ibl-sampling.wgsl +30 -6
- package/src/index.ts +189 -0
- package/src/lighting-punctual.wgsl +5 -6
- package/src/material/artifact-types.ts +109 -63
- package/src/material-schemas.ts +84 -8
- package/src/output-encoding.wgsl +10 -0
- package/src/pbr-temporal.wgsl +9 -1
- package/src/scene-temporal.wgsl +2 -0
- package/src/shadow-pcf.wgsl +6 -8
- package/src/shadow-surface.wgsl +16 -0
- package/src/shadow_caster.wgsl +99 -61
- package/src/sprite-lit.wgsl +4 -4
- package/src/sprite.wgsl +3 -3
- package/src/ssr-compose.wgsl +129 -0
- package/src/ssr-hiz-reduce.wgsl +53 -0
- package/src/ssr-hiz.wgsl +66 -0
- package/src/ssr-temporal.wgsl +305 -0
- package/src/ssr-trace.wgsl +784 -0
- package/src/standard-cluster.wgsl +6 -4
- package/src/standard-surface.wgsl +696 -0
- package/src/surface_v1.wgsl +6 -0
- package/src/taa-resolve.wgsl +222 -28
- package/src/tbn.wgsl +1 -1
- package/src/tonemap.wgsl +37 -18
- package/src/unlit.wgsl +3 -3
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
#define_import_path forgeax_ssr::trace
|
|
2
|
+
|
|
3
|
+
// The spatial trace is bounded by the public M1 contract.
|
|
4
|
+
const SSR_TRACE_MAX_COARSE_STEPS : u32 = 48u;
|
|
5
|
+
const SSR_TRACE_MAX_REFINE_STEPS : u32 = 5u;
|
|
6
|
+
|
|
7
|
+
@group(0) @binding(0) var sceneDepth : texture_depth_2d;
|
|
8
|
+
@group(0) @binding(1) var sceneNormal : texture_2d<f32>;
|
|
9
|
+
@group(0) @binding(2) var sceneColor : texture_2d<f32>;
|
|
10
|
+
@group(0) @binding(3) var hizPyramid : texture_2d<f32>;
|
|
11
|
+
@group(0) @binding(4) var traceOutput : texture_storage_2d<rgba16float, write>;
|
|
12
|
+
|
|
13
|
+
// This is the existing Standard View UBO, not a second camera state source.
|
|
14
|
+
// The shader only consumes world/inverse projection and the validated SSR
|
|
15
|
+
// parameter tail published by the record stage.
|
|
16
|
+
#import forgeax_view::common::View
|
|
17
|
+
@group(0) @binding(5) var<uniform> view : View;
|
|
18
|
+
// RGB belongs to lighting; alpha is its exact Standard material coverage.
|
|
19
|
+
@group(0) @binding(6) var reflectionFallback : texture_2d<f32>;
|
|
20
|
+
@group(0) @binding(7) var sceneTemporal : texture_2d<f32>;
|
|
21
|
+
// Motion at a radiance source invalidates reflection history even when the
|
|
22
|
+
// receiver is stationary. This scalar is reactivity, not reflected velocity.
|
|
23
|
+
@group(0) @binding(8) var hitReactivityOutput : texture_storage_2d<r32float, write>;
|
|
24
|
+
|
|
25
|
+
fn isFinite(value : f32) -> bool {
|
|
26
|
+
return value == value && abs(value) < 3.402823e+38;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fn finiteConfidence(value : f32) -> f32 {
|
|
30
|
+
return select(0.0, clamp(value, 0.0, 1.0), isFinite(value));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fn traceConfidence(
|
|
34
|
+
hit : f32,
|
|
35
|
+
thickness : f32,
|
|
36
|
+
facing : f32,
|
|
37
|
+
edge : f32,
|
|
38
|
+
roughness : f32,
|
|
39
|
+
temporal : f32,
|
|
40
|
+
) -> f32 {
|
|
41
|
+
let factors = vec3<f32>(hit, thickness, facing);
|
|
42
|
+
let spatial = vec3<f32>(edge, roughness, temporal);
|
|
43
|
+
if (!isFinite(factors.x) || !isFinite(factors.y) || !isFinite(factors.z) ||
|
|
44
|
+
!isFinite(spatial.x) || !isFinite(spatial.y) || !isFinite(spatial.z)) {
|
|
45
|
+
return 0.0;
|
|
46
|
+
}
|
|
47
|
+
return finiteConfidence(factors.x) * finiteConfidence(factors.y) *
|
|
48
|
+
finiteConfidence(factors.z) * finiteConfidence(spatial.x) *
|
|
49
|
+
finiteConfidence(spatial.y) * finiteConfidence(spatial.z);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn traceEdge(uv : vec2<f32>) -> f32 {
|
|
53
|
+
let distanceToEdge = min(min(uv.x, 1.0 - uv.x), min(uv.y, 1.0 - uv.y));
|
|
54
|
+
return clamp(distanceToEdge * 8.0, 0.0, 1.0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn reconstructWorldPosition(uv : vec2<f32>, depth : f32) -> vec3<f32> {
|
|
58
|
+
let clip = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, depth, 1.0);
|
|
59
|
+
let world = view.inverseViewProj * clip;
|
|
60
|
+
if (!isFinite(world.w) || abs(world.w) <= 1e-5) {
|
|
61
|
+
return vec3<f32>(0.0);
|
|
62
|
+
}
|
|
63
|
+
return world.xyz / world.w;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fn projectWorldPosition(worldPosition : vec3<f32>) -> vec2<f32> {
|
|
67
|
+
let clip = view.worldViewProj * vec4<f32>(worldPosition, 1.0);
|
|
68
|
+
if (!isFinite(clip.w) || clip.w <= 1e-5) {
|
|
69
|
+
return vec2<f32>(-1.0);
|
|
70
|
+
}
|
|
71
|
+
let ndc = clip.xy / clip.w;
|
|
72
|
+
return vec2<f32>(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fn projectWorldViewDistance(worldPosition : vec3<f32>) -> f32 {
|
|
76
|
+
let clip = view.worldViewProj * vec4<f32>(worldPosition, 1.0);
|
|
77
|
+
if (!isFinite(clip.w) || clip.w <= 1e-5) {
|
|
78
|
+
return 0.0;
|
|
79
|
+
}
|
|
80
|
+
// Hi-Z stores positive linear view distance. Perspective clip.w is that
|
|
81
|
+
// distance; orthographic clip.w is constant and needs the projection range.
|
|
82
|
+
// Comparing clip.z / clip.w with Hi-Z mixes normalized depth and world units
|
|
83
|
+
// and rejects every visible ray in ordinary scenes more than one unit away.
|
|
84
|
+
return select(
|
|
85
|
+
clip.w,
|
|
86
|
+
view.temporalProjection.x + (clip.z / clip.w) *
|
|
87
|
+
(view.temporalProjection.y - view.temporalProjection.x),
|
|
88
|
+
view.temporalProjection.z > 0.5,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fn maxSsrHiZMip(fullSize : vec2<u32>) -> u32 {
|
|
93
|
+
let extent = max(fullSize.x, fullSize.y);
|
|
94
|
+
if (extent <= 1u) {
|
|
95
|
+
return 0u;
|
|
96
|
+
}
|
|
97
|
+
return u32(floor(log2(f32(extent))));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Lookup uses the physical mip's normalized coordinate. The producer's
|
|
101
|
+
// conservative ceil-end footprint makes the neighboring lookup cells share
|
|
102
|
+
// an odd source boundary instead of dropping it between floor-sized cells.
|
|
103
|
+
fn ssrHiZCoordinate(uv : vec2<f32>, destinationSize : vec2<u32>) -> vec2<u32> {
|
|
104
|
+
let destinationLast = destinationSize - vec2<u32>(1u);
|
|
105
|
+
let destinationCoordinate = min(
|
|
106
|
+
vec2<u32>(clamp(uv * vec2<f32>(destinationSize), vec2<f32>(0.0), vec2<f32>(destinationLast))),
|
|
107
|
+
destinationLast,
|
|
108
|
+
);
|
|
109
|
+
return destinationCoordinate;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fn ssrHiZCoverageStart(index : u32, sourceSize : u32, destinationSize : u32) -> u32 {
|
|
113
|
+
return index * sourceSize / destinationSize;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fn ssrHiZCoverageEnd(index : u32, sourceSize : u32, destinationSize : u32) -> u32 {
|
|
117
|
+
return min(
|
|
118
|
+
((index + 1u) * sourceSize + destinationSize - 1u) / destinationSize,
|
|
119
|
+
sourceSize,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fn sampleSsrHiZ(uv : vec2<f32>, mip : u32) -> f32 {
|
|
124
|
+
if (mip == 0u) {
|
|
125
|
+
let size = textureDimensions(sceneDepth, 0);
|
|
126
|
+
let pixel = clamp(vec2<i32>(uv * vec2<f32>(size)), vec2<i32>(0), vec2<i32>(size) - vec2<i32>(1));
|
|
127
|
+
let depth = textureLoad(sceneDepth, pixel, 0);
|
|
128
|
+
if (depth <= 0.0 || depth >= 1.0) { return 3.402823e+38; }
|
|
129
|
+
let centerUv = (vec2<f32>(pixel) + vec2<f32>(0.5)) / vec2<f32>(size);
|
|
130
|
+
return projectWorldViewDistance(reconstructWorldPosition(centerUv, depth));
|
|
131
|
+
}
|
|
132
|
+
let physicalMip = min(mip - 1u, textureNumLevels(hizPyramid) - 1u);
|
|
133
|
+
let size = textureDimensions(hizPyramid, physicalMip);
|
|
134
|
+
let coordinate = ssrHiZCoordinate(uv, size);
|
|
135
|
+
let depth = textureLoad(hizPyramid, vec2<i32>(coordinate), i32(physicalMip)).r;
|
|
136
|
+
return select(3.402823e+38, depth, isFinite(depth) && depth > 0.0);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
struct SsrTraceHit {
|
|
140
|
+
hit : f32,
|
|
141
|
+
uv : vec2<f32>,
|
|
142
|
+
thickness : f32,
|
|
143
|
+
reactivity : f32,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
struct SsrDepthCandidate {
|
|
147
|
+
valid : bool,
|
|
148
|
+
pixel : vec2<u32>,
|
|
149
|
+
uv : vec2<f32>,
|
|
150
|
+
depth : f32,
|
|
151
|
+
fraction : f32,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
fn ssrInvalidDepthCandidate() -> SsrDepthCandidate {
|
|
155
|
+
return SsrDepthCandidate(false, vec2<u32>(0u), vec2<f32>(0.0), 1.0, 0.0);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fn ssrRayFraction(uv : vec2<f32>, startUv : vec2<f32>, deltaUv : vec2<f32>,
|
|
159
|
+
fallback : f32, endFraction : f32) -> f32 {
|
|
160
|
+
let useX = abs(deltaUv.x) >= abs(deltaUv.y) && abs(deltaUv.x) > 1e-8;
|
|
161
|
+
let useY = !useX && abs(deltaUv.y) > 1e-8;
|
|
162
|
+
let xFraction = select(fallback, (uv.x - startUv.x) / deltaUv.x, useX);
|
|
163
|
+
let yFraction = select(fallback, (uv.y - startUv.y) / deltaUv.y, useY);
|
|
164
|
+
let fraction = select(xFraction, yFraction, useY);
|
|
165
|
+
return clamp(select(fallback, fraction, isFinite(fraction)), 0.0, endFraction);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
fn ssrRescueCandidateInRay(origin : vec3<f32>, direction : vec3<f32>,
|
|
169
|
+
rayLength : f32, surface : vec3<f32>, normal : vec3<f32>,
|
|
170
|
+
fullSize : vec2<u32>, pixel : vec2<u32>) -> bool {
|
|
171
|
+
// A rescue may bypass the generic bisection, so it must still prove that
|
|
172
|
+
// the candidate's tangent plane intersects the finite reflected segment.
|
|
173
|
+
// The projected pixel check rejects a neighboring silhouette plane whose
|
|
174
|
+
// depth happens to be within the broad thickness radius.
|
|
175
|
+
let denominator = dot(direction, normal);
|
|
176
|
+
let planeDistance = dot(surface - origin, normal) / denominator;
|
|
177
|
+
if (!isFinite(denominator) || abs(denominator) <= 1e-5 ||
|
|
178
|
+
!isFinite(planeDistance) || planeDistance <= 0.0 || planeDistance > rayLength) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
let projected = projectWorldPosition(origin + direction * planeDistance);
|
|
182
|
+
if (any(projected < vec2<f32>(0.0)) || any(projected >= vec2<f32>(1.0))) { return false; }
|
|
183
|
+
let projectedPixel = min(vec2<u32>(projected * vec2<f32>(fullSize)), fullSize - vec2<u32>(1u));
|
|
184
|
+
return all(projectedPixel == pixel);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// When the coarse march lands on an empty full-resolution texel, descend the
|
|
188
|
+
// already-produced Hi-Z minimum hierarchy to recover the child footprint that
|
|
189
|
+
// contains the nearest surface. This keeps the public 48-step budget while
|
|
190
|
+
// retaining thin projected features between coarse samples.
|
|
191
|
+
fn locateSsrHiZCandidate(uv : vec2<f32>, coarseMip : u32, fullSize : vec2<u32>,
|
|
192
|
+
startUv : vec2<f32>, deltaUv : vec2<f32>, fallbackFraction : f32,
|
|
193
|
+
endFraction : f32) -> SsrDepthCandidate {
|
|
194
|
+
if (coarseMip == 0u || textureNumLevels(hizPyramid) == 0u) {
|
|
195
|
+
return ssrInvalidDepthCandidate();
|
|
196
|
+
}
|
|
197
|
+
let physicalMip = coarseMip - 1u;
|
|
198
|
+
let size = textureDimensions(hizPyramid, i32(physicalMip));
|
|
199
|
+
var coordinate = ssrHiZCoordinate(uv, size);
|
|
200
|
+
let depth = textureLoad(hizPyramid, vec2<i32>(coordinate), i32(physicalMip)).r;
|
|
201
|
+
if (!isFinite(depth) || depth <= 0.0) { return ssrInvalidDepthCandidate(); }
|
|
202
|
+
|
|
203
|
+
// Each reduction level stores the minimum of its complete integer-normalized
|
|
204
|
+
// child footprint. Select the child that owns that minimum rather than
|
|
205
|
+
// assuming the parent's center is the hit location. The final mip-0
|
|
206
|
+
// candidate is still validated against the real depth/normal/coverage
|
|
207
|
+
// buffers below.
|
|
208
|
+
var parentSize = size;
|
|
209
|
+
for (var descend = coarseMip; descend > 1u; descend -= 1u) {
|
|
210
|
+
let nextPhysicalMip = descend - 2u;
|
|
211
|
+
let nextSize = textureDimensions(hizPyramid, i32(nextPhysicalMip));
|
|
212
|
+
let childStart = vec2<u32>(
|
|
213
|
+
ssrHiZCoverageStart(coordinate.x, nextSize.x, parentSize.x),
|
|
214
|
+
ssrHiZCoverageStart(coordinate.y, nextSize.y, parentSize.y),
|
|
215
|
+
);
|
|
216
|
+
let childEnd = vec2<u32>(
|
|
217
|
+
ssrHiZCoverageEnd(coordinate.x, nextSize.x, parentSize.x),
|
|
218
|
+
ssrHiZCoverageEnd(coordinate.y, nextSize.y, parentSize.y),
|
|
219
|
+
);
|
|
220
|
+
var childCoordinate = vec2<u32>(0u);
|
|
221
|
+
var childDepth = 3.402823e+38;
|
|
222
|
+
var childFound = false;
|
|
223
|
+
for (var childY = childStart.y; childY < childEnd.y; childY += 1u) {
|
|
224
|
+
for (var childX = childStart.x; childX < childEnd.x; childX += 1u) {
|
|
225
|
+
let candidate = min(vec2<u32>(childX, childY), nextSize - vec2<u32>(1u));
|
|
226
|
+
let candidateDepth = textureLoad(
|
|
227
|
+
hizPyramid,
|
|
228
|
+
vec2<i32>(candidate),
|
|
229
|
+
i32(nextPhysicalMip),
|
|
230
|
+
).r;
|
|
231
|
+
if (isFinite(candidateDepth) && candidateDepth > 0.0 && candidateDepth < childDepth) {
|
|
232
|
+
childCoordinate = candidate;
|
|
233
|
+
childDepth = candidateDepth;
|
|
234
|
+
childFound = true;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (!childFound) { return ssrInvalidDepthCandidate(); }
|
|
239
|
+
coordinate = childCoordinate;
|
|
240
|
+
parentSize = nextSize;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Hi-Z mip 0 is seeded from the integer source footprint used by
|
|
244
|
+
// `ssr_hiz_seed`: floor(cell * fullSize / hizSize) through the next cell
|
|
245
|
+
// boundary. Even dimensions are 2x2, while odd dimensions use the same
|
|
246
|
+
// conservative ceil-end overlap as every reduction level. Its minimum depth
|
|
247
|
+
// is not necessarily the footprint center, so select the actual nearest
|
|
248
|
+
// valid source texel before validating coverage/normal.
|
|
249
|
+
let hizSize = textureDimensions(hizPyramid, 0);
|
|
250
|
+
let sourceStart = vec2<u32>(
|
|
251
|
+
ssrHiZCoverageStart(coordinate.x, fullSize.x, hizSize.x),
|
|
252
|
+
ssrHiZCoverageStart(coordinate.y, fullSize.y, hizSize.y),
|
|
253
|
+
);
|
|
254
|
+
let sourceEnd = vec2<u32>(
|
|
255
|
+
ssrHiZCoverageEnd(coordinate.x, fullSize.x, hizSize.x),
|
|
256
|
+
ssrHiZCoverageEnd(coordinate.y, fullSize.y, hizSize.y),
|
|
257
|
+
);
|
|
258
|
+
var candidatePixel = sourceStart;
|
|
259
|
+
var candidateRawDepth = 3.402823e+38;
|
|
260
|
+
for (var sourceY = sourceStart.y; sourceY < sourceEnd.y; sourceY += 1u) {
|
|
261
|
+
for (var sourceX = sourceStart.x; sourceX < sourceEnd.x; sourceX += 1u) {
|
|
262
|
+
let sourcePixel = min(
|
|
263
|
+
vec2<u32>(sourceX, sourceY),
|
|
264
|
+
fullSize - vec2<u32>(1u),
|
|
265
|
+
);
|
|
266
|
+
let sourceDepth = textureLoad(sceneDepth, vec2<i32>(sourcePixel), 0);
|
|
267
|
+
// Perspective and orthographic depth are monotonic with view distance,
|
|
268
|
+
// so comparing the raw depth values preserves the Hi-Z nearest owner.
|
|
269
|
+
if (isFinite(sourceDepth) && sourceDepth > 0.0 && sourceDepth < 1.0 &&
|
|
270
|
+
sourceDepth < candidateRawDepth) {
|
|
271
|
+
candidatePixel = sourcePixel;
|
|
272
|
+
candidateRawDepth = sourceDepth;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (!isFinite(candidateRawDepth) || candidateRawDepth <= 0.0 || candidateRawDepth >= 1.0) {
|
|
277
|
+
return ssrInvalidDepthCandidate();
|
|
278
|
+
}
|
|
279
|
+
let candidateDepth = textureLoad(sceneDepth, vec2<i32>(candidatePixel), 0);
|
|
280
|
+
let candidateCenterUv = (vec2<f32>(candidatePixel) + vec2<f32>(0.5)) / vec2<f32>(fullSize);
|
|
281
|
+
return SsrDepthCandidate(
|
|
282
|
+
true,
|
|
283
|
+
candidatePixel,
|
|
284
|
+
candidateCenterUv,
|
|
285
|
+
candidateDepth,
|
|
286
|
+
ssrRayFraction(candidateCenterUv, startUv, deltaUv, fallbackFraction, endFraction),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// A structural/direct trace may be supplied without a Hi-Z hierarchy. Keep
|
|
291
|
+
// that path bounded too, but inspect a short projected neighborhood so a thin
|
|
292
|
+
// depth texel between two coarse samples is not silently lost.
|
|
293
|
+
fn locateSsrLocalCandidate(uv : vec2<f32>, pixel : vec2<u32>, fullSize : vec2<u32>,
|
|
294
|
+
startUv : vec2<f32>, deltaUv : vec2<f32>, fallbackFraction : f32,
|
|
295
|
+
endFraction : f32) -> SsrDepthCandidate {
|
|
296
|
+
let span = max(abs(deltaUv.x) * f32(fullSize.x), abs(deltaUv.y) * f32(fullSize.y));
|
|
297
|
+
if (!isFinite(span) || span <= 1e-5) { return ssrInvalidDepthCandidate(); }
|
|
298
|
+
var bestOffset = 9;
|
|
299
|
+
var bestDepth = 3.402823e+38;
|
|
300
|
+
var bestPixel = pixel;
|
|
301
|
+
for (var offset = -8; offset <= 8; offset += 1) {
|
|
302
|
+
let sampleUv = uv + deltaUv * (f32(offset) / span);
|
|
303
|
+
if (any(sampleUv < vec2<f32>(0.0)) || any(sampleUv >= vec2<f32>(1.0))) { continue; }
|
|
304
|
+
let samplePixel = min(
|
|
305
|
+
vec2<u32>(sampleUv * vec2<f32>(fullSize)),
|
|
306
|
+
fullSize - vec2<u32>(1u),
|
|
307
|
+
);
|
|
308
|
+
let sampleDepth = textureLoad(sceneDepth, vec2<i32>(samplePixel), 0);
|
|
309
|
+
let distanceFromCenter = abs(offset);
|
|
310
|
+
if (sampleDepth > 0.0 && sampleDepth < 1.0 &&
|
|
311
|
+
(distanceFromCenter < bestOffset ||
|
|
312
|
+
(distanceFromCenter == bestOffset && sampleDepth < bestDepth))) {
|
|
313
|
+
bestOffset = distanceFromCenter;
|
|
314
|
+
bestDepth = sampleDepth;
|
|
315
|
+
bestPixel = samplePixel;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (bestOffset > 8) { return ssrInvalidDepthCandidate(); }
|
|
319
|
+
let candidateUv = (vec2<f32>(bestPixel) + vec2<f32>(0.5)) / vec2<f32>(fullSize);
|
|
320
|
+
return SsrDepthCandidate(
|
|
321
|
+
true,
|
|
322
|
+
bestPixel,
|
|
323
|
+
candidateUv,
|
|
324
|
+
bestDepth,
|
|
325
|
+
ssrRayFraction(candidateUv, startUv, deltaUv, fallbackFraction, endFraction),
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
fn ssrSourceReactivity(pixel : vec2<i32>) -> f32 {
|
|
330
|
+
let temporal = textureLoad(sceneTemporal, pixel, 0);
|
|
331
|
+
let speed = length(temporal.xy);
|
|
332
|
+
if (!isFinite(speed) || !isFinite(temporal.z) || temporal.z < 0.0 || !isFinite(temporal.w)) {
|
|
333
|
+
return 1.0;
|
|
334
|
+
}
|
|
335
|
+
return clamp(max(speed * 64.0, temporal.w), 0.0, 1.0);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
fn ssrShadingNormalVaries(pixel : vec2<u32>, normal : vec3<f32>) -> bool {
|
|
339
|
+
let size = vec2<i32>(textureDimensions(sceneDepth, 0));
|
|
340
|
+
// Only a locally varying, similarly oriented field makes the tangent plane
|
|
341
|
+
// uncertain. A flat face ending at sky must retain its exact silhouette;
|
|
342
|
+
// thickness alone is not permission to extend that face into empty space.
|
|
343
|
+
for (var axis = 0u; axis < 2u; axis++) {
|
|
344
|
+
for (var sign = -1; sign <= 1; sign += 2) {
|
|
345
|
+
var offset = vec2<i32>(0);
|
|
346
|
+
offset[axis] = sign;
|
|
347
|
+
let tap = vec2<i32>(pixel) + offset;
|
|
348
|
+
if (any(tap < vec2<i32>(0)) || any(tap >= size)) { continue; }
|
|
349
|
+
if (textureLoad(sceneDepth, tap, 0) >= 1.0 ||
|
|
350
|
+
textureLoad(reflectionFallback, tap, 0).a <= 0.5) { continue; }
|
|
351
|
+
let neighbor = textureLoad(sceneNormal, tap, 0).xyz * 2.0 - vec3<f32>(1.0);
|
|
352
|
+
let delta = neighbor - normal;
|
|
353
|
+
let agreement = dot(neighbor, normal) * inverseSqrt(max(dot(neighbor, neighbor) * dot(normal, normal), 1e-8));
|
|
354
|
+
if (agreement > 0.9 && dot(delta, delta) > 1e-6) { return true; }
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
fn ssrGeometricNormal(pixel : vec2<u32>, normal : vec3<f32>) -> vec3<f32> {
|
|
361
|
+
// Recover a depth tangent from the nearest same-facing neighbor per axis.
|
|
362
|
+
// Missing support on either axis retains the original normal; it does not
|
|
363
|
+
// authorize a depth-only hit through a silhouette or uncovered surface.
|
|
364
|
+
let size = textureDimensions(sceneDepth, 0);
|
|
365
|
+
let center = reconstructWorldPosition((vec2<f32>(pixel) + 0.5) / vec2<f32>(size),
|
|
366
|
+
textureLoad(sceneDepth, vec2<i32>(pixel), 0));
|
|
367
|
+
var derivatives : array<vec3<f32>, 2>;
|
|
368
|
+
for (var axis = 0u; axis < 2u; axis++) {
|
|
369
|
+
var shortest = 3.402823e+38;
|
|
370
|
+
for (var sign = -1; sign <= 1; sign += 2) {
|
|
371
|
+
var offset = vec2<i32>(0);
|
|
372
|
+
offset[axis] = sign;
|
|
373
|
+
let tap = vec2<i32>(pixel) + offset;
|
|
374
|
+
if (any(tap < vec2<i32>(0)) || any(tap >= vec2<i32>(size))) { continue; }
|
|
375
|
+
let depth = textureLoad(sceneDepth, tap, 0);
|
|
376
|
+
let tapNormal = textureLoad(sceneNormal, tap, 0).xyz * 2.0 - 1.0;
|
|
377
|
+
if (depth >= 1.0 || dot(normal, tapNormal) < 0.9) { continue; }
|
|
378
|
+
let point = reconstructWorldPosition((vec2<f32>(tap) + 0.5) / vec2<f32>(size), depth);
|
|
379
|
+
let delta = (point - center) * f32(sign);
|
|
380
|
+
let distance = dot(delta, delta);
|
|
381
|
+
if (distance < shortest) { shortest = distance; derivatives[axis] = delta; }
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
let product = cross(derivatives[0], derivatives[1]);
|
|
385
|
+
let lengthSquared = dot(product, product);
|
|
386
|
+
if (lengthSquared <= 1e-16) { return normal; }
|
|
387
|
+
let geometric = product * inverseSqrt(lengthSquared);
|
|
388
|
+
return select(-geometric, geometric, dot(geometric, normal) >= 0.0);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
fn ssrRefineHit(origin : vec3<f32>, direction : vec3<f32>, maxDistance : f32,
|
|
392
|
+
thickness : f32, fullSize : vec2<u32>, pixel : vec2<u32>, depth : f32,
|
|
393
|
+
normal : vec3<f32>, result : SsrTraceHit, geometric : bool) -> SsrTraceHit {
|
|
394
|
+
// A depth texel describes its center, not the subpixel march coordinate.
|
|
395
|
+
// Refine against that sample's tangent plane, then validate the projected
|
|
396
|
+
// intersection against the actual depth buffer. Otherwise a stair-stepped
|
|
397
|
+
// depth crossing displaces high-frequency reflected texture coordinates.
|
|
398
|
+
let centerUv = (vec2<f32>(pixel) + vec2<f32>(0.5)) / vec2<f32>(fullSize);
|
|
399
|
+
let surface = reconstructWorldPosition(centerUv, depth);
|
|
400
|
+
var planeNormal = normal;
|
|
401
|
+
if (geometric) { planeNormal = ssrGeometricNormal(pixel, normal); }
|
|
402
|
+
let planeDistance = dot(surface - origin, planeNormal) / dot(direction, planeNormal);
|
|
403
|
+
if (!isFinite(planeDistance) || planeDistance <= 0.0 || planeDistance > maxDistance) { return result; }
|
|
404
|
+
let intersection = origin + direction * planeDistance;
|
|
405
|
+
let hitUv = projectWorldPosition(intersection);
|
|
406
|
+
if (any(hitUv < vec2<f32>(0.0)) || any(hitUv >= vec2<f32>(1.0))) { return result; }
|
|
407
|
+
let hitPixel = min(vec2<u32>(hitUv * vec2<f32>(fullSize)), fullSize - vec2<u32>(1u));
|
|
408
|
+
let hitDepth = textureLoad(sceneDepth, vec2<i32>(hitPixel), 0);
|
|
409
|
+
let hitNormal = textureLoad(sceneNormal, vec2<i32>(hitPixel), 0).xyz * 2.0 - vec3<f32>(1.0);
|
|
410
|
+
if (hitDepth >= 1.0 || dot(hitNormal, -direction) <= 0.0 ||
|
|
411
|
+
textureLoad(reflectionFallback, vec2<i32>(hitPixel), 0).a <= 0.5) { return result; }
|
|
412
|
+
// Depth belongs to the texel center. Compare the intersection with that
|
|
413
|
+
// sample's plane, not a constant-depth point fabricated at hitUv. The latter
|
|
414
|
+
// lowers confidence on an exact oblique-plane hit as jitter moves inside
|
|
415
|
+
// the texel, periodically exposing fallback beneath a static reflection.
|
|
416
|
+
let hitCenterUv = (vec2<f32>(hitPixel) + vec2<f32>(0.5)) / vec2<f32>(fullSize);
|
|
417
|
+
let hitSurface = reconstructWorldPosition(hitCenterUv, hitDepth);
|
|
418
|
+
let neighbor = reconstructWorldPosition(hitCenterUv + vec2<f32>(1.0 / f32(fullSize.x), 0.0), hitDepth);
|
|
419
|
+
let radius = max(thickness, distance(hitSurface, neighbor) * 3.0);
|
|
420
|
+
var validationNormal = hitNormal;
|
|
421
|
+
if (geometric) { validationNormal = ssrGeometricNormal(hitPixel, hitNormal); }
|
|
422
|
+
let separation = abs(dot(hitSurface - intersection, normalize(validationNormal)));
|
|
423
|
+
if (!isFinite(separation) || separation > radius) { return result; }
|
|
424
|
+
return SsrTraceHit(1.0, hitUv, clamp(1.0 - separation / radius, 0.0, 1.0), result.reactivity);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Algorithm reference: Three.js SSRShader / SSRNode (MIT), inspected 2026-09-08:
|
|
428
|
+
// https://github.com/mrdoob/three.js/blob/4457aa3c5de4a05bbae4e0bee2d99d994c38a2bb/examples/jsm/tsl/display/SSRNode.js
|
|
429
|
+
// Projected-space stepping, reciprocal-depth interpolation, ray-distance
|
|
430
|
+
// thickness, and a separate crossing refinement avoid world-step holes.
|
|
431
|
+
// Mirror rays spend steps in proportion to their projected pixel span, as in
|
|
432
|
+
// Three's non-stochastic path. A small fixed budget stretches samples over
|
|
433
|
+
// thin silhouettes and can jump directly from in-front-of-wall to sky.
|
|
434
|
+
// The 1024/5 bound, Hi-Z, material coverage, and BRDF owner remain explicit.
|
|
435
|
+
fn traceScreenRay(
|
|
436
|
+
origin : vec3<f32>,
|
|
437
|
+
direction : vec3<f32>,
|
|
438
|
+
maxDistance : f32,
|
|
439
|
+
thickness : f32,
|
|
440
|
+
fullSize : vec2<u32>,
|
|
441
|
+
hizDepth : f32,
|
|
442
|
+
hizMaxMip : u32,
|
|
443
|
+
) -> SsrTraceHit {
|
|
444
|
+
var result = SsrTraceHit(0.0, vec2<f32>(0.0), 0.0, 0.0);
|
|
445
|
+
if (!isFinite(maxDistance) || maxDistance <= 0.0 ||
|
|
446
|
+
!isFinite(thickness) || thickness <= 0.0 || maxDistance < thickness ||
|
|
447
|
+
!isFinite(hizDepth) || hizDepth <= 0.0) { return result; }
|
|
448
|
+
let startDepth = projectWorldViewDistance(origin);
|
|
449
|
+
let depthDirection = dot(vec3<f32>(view.worldViewProj[0].w,
|
|
450
|
+
view.worldViewProj[1].w, view.worldViewProj[2].w), direction);
|
|
451
|
+
var rayLength = maxDistance;
|
|
452
|
+
if (depthDirection < -1e-5) {
|
|
453
|
+
rayLength = min(rayLength, (startDepth - view.temporalProjection.x * 1.01) / -depthDirection);
|
|
454
|
+
}
|
|
455
|
+
if (rayLength <= 0.0) { return result; }
|
|
456
|
+
let end = origin + direction * rayLength;
|
|
457
|
+
let endDepth = projectWorldViewDistance(end);
|
|
458
|
+
let startUv = projectWorldPosition(origin);
|
|
459
|
+
let deltaUv = projectWorldPosition(end) - startUv;
|
|
460
|
+
// Clip the projected segment to the viewport before spending the fixed budget.
|
|
461
|
+
var endFraction = 1.0;
|
|
462
|
+
if (deltaUv.x > 1e-6) { endFraction = min(endFraction, (1.0 - startUv.x) / deltaUv.x); }
|
|
463
|
+
if (deltaUv.x < -1e-6) { endFraction = min(endFraction, -startUv.x / deltaUv.x); }
|
|
464
|
+
if (deltaUv.y > 1e-6) { endFraction = min(endFraction, (1.0 - startUv.y) / deltaUv.y); }
|
|
465
|
+
if (deltaUv.y < -1e-6) { endFraction = min(endFraction, -startUv.y / deltaUv.y); }
|
|
466
|
+
let span = abs(deltaUv * endFraction * vec2<f32>(fullSize));
|
|
467
|
+
let count = min(SSR_TRACE_MAX_COARSE_STEPS, max(1u, u32(ceil(max(span.x, span.y)))));
|
|
468
|
+
let coarseMip = min(hizMaxMip, u32(floor(log2(max(1.0, max(span.x, span.y) / f32(count))))));
|
|
469
|
+
let inverseStart = 1.0 / max(startDepth, 1e-5);
|
|
470
|
+
let inverseEnd = 1.0 / max(endDepth, 1e-5);
|
|
471
|
+
var lower = 0.0;
|
|
472
|
+
var upper = 0.0;
|
|
473
|
+
var found = false;
|
|
474
|
+
var rescued = false;
|
|
475
|
+
var rescuedUv = vec2<f32>(0.0);
|
|
476
|
+
var rescuedThickness = 0.0;
|
|
477
|
+
for (var step = 1u; step <= SSR_TRACE_MAX_COARSE_STEPS; step++) {
|
|
478
|
+
if (step > count) { break; }
|
|
479
|
+
let fraction = f32(step) / f32(count) * endFraction;
|
|
480
|
+
let uv = startUv + deltaUv * fraction;
|
|
481
|
+
let pixel = min(vec2<u32>(clamp(uv, vec2<f32>(0.0), vec2<f32>(1.0)) *
|
|
482
|
+
vec2<f32>(fullSize)), fullSize - vec2<u32>(1u));
|
|
483
|
+
let rayDepth = 1.0 / mix(inverseStart, inverseEnd, fraction);
|
|
484
|
+
// The ray can cross the surface after this sample but before leaving its
|
|
485
|
+
// depth texel. Testing only rayDepth loses that crossing when the next
|
|
486
|
+
// sample is sky. Extend the bracket to this texel's exit, not its neighbor;
|
|
487
|
+
// the small pixel-space inset keeps the upper endpoint on the same texel.
|
|
488
|
+
let moving = abs(deltaUv) > vec2<f32>(1e-8);
|
|
489
|
+
let exitUv = (vec2<f32>(pixel) + select(vec2<f32>(0.0), vec2<f32>(1.0),
|
|
490
|
+
deltaUv > vec2<f32>(0.0))) / vec2<f32>(fullSize);
|
|
491
|
+
let exitFractions = select(vec2<f32>(endFraction),
|
|
492
|
+
(exitUv - startUv) / select(vec2<f32>(1.0), deltaUv, moving), moving);
|
|
493
|
+
let pixelSpan = max(abs(deltaUv.x) * f32(fullSize.x), abs(deltaUv.y) * f32(fullSize.y));
|
|
494
|
+
let exitFraction = max(fraction,
|
|
495
|
+
min(endFraction, min(exitFractions.x, exitFractions.y)) - 1e-4 / max(pixelSpan, 1.0));
|
|
496
|
+
let exitDepth = 1.0 / mix(inverseStart, inverseEnd, exitFraction);
|
|
497
|
+
let intervalDepth = max(rayDepth, exitDepth);
|
|
498
|
+
// Hi-Z contains point depths, not the minimum of an oblique surface's
|
|
499
|
+
// entire texel footprint. Keep the authored thickness in this broad-phase
|
|
500
|
+
// bound; the in-texel tangent intersection below still decides new hits.
|
|
501
|
+
// A zero logical mip means no reduced hierarchy is available. Its mip-0
|
|
502
|
+
// sample is the current full-resolution texel, so an empty texel must not
|
|
503
|
+
// discard the bounded neighborhood rescue below.
|
|
504
|
+
if (coarseMip > 0u && sampleSsrHiZ(uv, coarseMip) > intervalDepth + thickness) { continue; }
|
|
505
|
+
var samplePixel = pixel;
|
|
506
|
+
var sampleUv = uv;
|
|
507
|
+
var sampleDepth = textureLoad(sceneDepth, vec2<i32>(pixel), 0);
|
|
508
|
+
var sampleFraction = fraction;
|
|
509
|
+
var candidate = ssrInvalidDepthCandidate();
|
|
510
|
+
if (sampleDepth <= 0.0 || sampleDepth >= 1.0) {
|
|
511
|
+
candidate = locateSsrHiZCandidate(
|
|
512
|
+
uv,
|
|
513
|
+
coarseMip,
|
|
514
|
+
fullSize,
|
|
515
|
+
startUv,
|
|
516
|
+
deltaUv,
|
|
517
|
+
fraction,
|
|
518
|
+
endFraction,
|
|
519
|
+
);
|
|
520
|
+
// A local neighborhood is only a rescue for a genuinely undersampled
|
|
521
|
+
// projected step. On short rays the normal full-resolution march
|
|
522
|
+
// already visits each texel; scanning a wide neighborhood there would
|
|
523
|
+
// turn a nearby wall into a false hit on an otherwise empty sky ray.
|
|
524
|
+
let projectedStepPixels = max(span.x, span.y) / f32(max(count, 1u));
|
|
525
|
+
if (!candidate.valid && coarseMip == 0u && projectedStepPixels > 2.0) {
|
|
526
|
+
candidate = locateSsrLocalCandidate(
|
|
527
|
+
uv,
|
|
528
|
+
pixel,
|
|
529
|
+
fullSize,
|
|
530
|
+
startUv,
|
|
531
|
+
deltaUv,
|
|
532
|
+
fraction,
|
|
533
|
+
endFraction,
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
if (candidate.valid) {
|
|
537
|
+
samplePixel = candidate.pixel;
|
|
538
|
+
sampleUv = candidate.uv;
|
|
539
|
+
sampleDepth = candidate.depth;
|
|
540
|
+
sampleFraction = candidate.fraction;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (sampleDepth >= 1.0 || textureLoad(reflectionFallback, vec2<i32>(samplePixel), 0).a <= 0.5) { continue; }
|
|
544
|
+
let sampleRayDepth = 1.0 / mix(inverseStart, inverseEnd, sampleFraction);
|
|
545
|
+
let surface = reconstructWorldPosition(sampleUv, sampleDepth);
|
|
546
|
+
if (intervalDepth + thickness < projectWorldViewDistance(surface)) { continue; }
|
|
547
|
+
// A moving occluder can invalidate yesterday's hit even when its back
|
|
548
|
+
// face or depth separation rejects today's radiance. Preserve that
|
|
549
|
+
// evidence on misses; sky and samples beyond the ray interval contribute
|
|
550
|
+
// nothing. This does not admit the occluder as a reflection source.
|
|
551
|
+
result.reactivity = max(result.reactivity, ssrSourceReactivity(vec2<i32>(samplePixel)));
|
|
552
|
+
let normal = textureLoad(sceneNormal, vec2<i32>(samplePixel), 0).xyz * 2.0 - vec3<f32>(1.0);
|
|
553
|
+
if (dot(normal, -direction) <= 0.0) { continue; }
|
|
554
|
+
if (candidate.valid && !ssrRescueCandidateInRay(
|
|
555
|
+
origin,
|
|
556
|
+
direction,
|
|
557
|
+
rayLength,
|
|
558
|
+
surface,
|
|
559
|
+
normal,
|
|
560
|
+
fullSize,
|
|
561
|
+
samplePixel,
|
|
562
|
+
)) { continue; }
|
|
563
|
+
if (sampleRayDepth < projectWorldViewDistance(surface)) {
|
|
564
|
+
// Extending the depth interval must not extend the sampled surface.
|
|
565
|
+
// At a silhouette the neighboring texel may belong to another plane.
|
|
566
|
+
if (!candidate.valid && !ssrRescueCandidateInRay(
|
|
567
|
+
origin,
|
|
568
|
+
direction,
|
|
569
|
+
rayLength,
|
|
570
|
+
surface,
|
|
571
|
+
normal,
|
|
572
|
+
fullSize,
|
|
573
|
+
samplePixel,
|
|
574
|
+
)) { continue; }
|
|
575
|
+
}
|
|
576
|
+
let neighbor = reconstructWorldPosition(sampleUv + vec2<f32>(1.0 / f32(fullSize.x), 0.0), sampleDepth);
|
|
577
|
+
let radius = max(thickness, distance(surface, neighbor) * 3.0);
|
|
578
|
+
let separation = length(cross(surface - origin, direction));
|
|
579
|
+
if (!isFinite(separation) || separation > radius) { continue; }
|
|
580
|
+
if (candidate.valid) {
|
|
581
|
+
rescued = true;
|
|
582
|
+
rescuedUv = sampleUv;
|
|
583
|
+
rescuedThickness = clamp(1.0 - separation / radius, 0.0, 1.0);
|
|
584
|
+
}
|
|
585
|
+
let bracketLower = min(f32(step - 1u) / f32(count) * endFraction, sampleFraction);
|
|
586
|
+
let bracketUpper = max(exitFraction, sampleFraction);
|
|
587
|
+
lower = select(sampleFraction, bracketLower,
|
|
588
|
+
sampleRayDepth >= projectWorldViewDistance(surface));
|
|
589
|
+
upper = select(bracketUpper, sampleFraction, sampleRayDepth >= projectWorldViewDistance(surface));
|
|
590
|
+
found = true;
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
if (!found) { return result; }
|
|
594
|
+
// Depth crossing refinement is intentionally outside the march loop.
|
|
595
|
+
for (var refine = 0u; refine < SSR_TRACE_MAX_REFINE_STEPS; refine++) {
|
|
596
|
+
let middle = (lower + upper) * 0.5;
|
|
597
|
+
let uv = startUv + deltaUv * middle;
|
|
598
|
+
let pixel = min(vec2<u32>(clamp(uv, vec2<f32>(0.0), vec2<f32>(1.0)) *
|
|
599
|
+
vec2<f32>(fullSize)), fullSize - vec2<u32>(1u));
|
|
600
|
+
let surfaceDepth = sampleSsrHiZ(uv, 0u);
|
|
601
|
+
let rayDepth = 1.0 / mix(inverseStart, inverseEnd, middle);
|
|
602
|
+
// A depth crossing on a rejected surface is not the accepted bracket.
|
|
603
|
+
// Near contact points, depth-only bisection can walk backward onto the
|
|
604
|
+
// receiver even though the coarse step correctly found the facing wall.
|
|
605
|
+
let normal = textureLoad(sceneNormal, vec2<i32>(pixel), 0).xyz * 2.0 - vec3<f32>(1.0);
|
|
606
|
+
let covered = textureLoad(reflectionFallback, vec2<i32>(pixel), 0).a > 0.5;
|
|
607
|
+
if (rayDepth >= surfaceDepth && covered && dot(normal, -direction) > 0.0) {
|
|
608
|
+
upper = middle;
|
|
609
|
+
} else {
|
|
610
|
+
lower = middle;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
let uv = startUv + deltaUv * upper;
|
|
614
|
+
let pixel = min(vec2<u32>(clamp(uv, vec2<f32>(0.0), vec2<f32>(1.0)) *
|
|
615
|
+
vec2<f32>(fullSize)), fullSize - vec2<u32>(1u));
|
|
616
|
+
let depth = textureLoad(sceneDepth, vec2<i32>(pixel), 0);
|
|
617
|
+
let normal = textureLoad(sceneNormal, vec2<i32>(pixel), 0).xyz * 2.0 - vec3<f32>(1.0);
|
|
618
|
+
let covered = textureLoad(reflectionFallback, vec2<i32>(pixel), 0).a > 0.5;
|
|
619
|
+
// The generic bisection can walk from a thin, valid rescue texel into the
|
|
620
|
+
// adjacent sky texel. The rescue was already validated against the actual
|
|
621
|
+
// depth/normal/coverage/thickness gates in the march, so preserve it before
|
|
622
|
+
// the final endpoint rejection discards the whole trace.
|
|
623
|
+
if (rescued && (depth >= 1.0 || dot(normal, -direction) <= 0.0 || !covered)) {
|
|
624
|
+
return SsrTraceHit(1.0, rescuedUv, rescuedThickness, result.reactivity);
|
|
625
|
+
}
|
|
626
|
+
if (depth >= 1.0 || dot(normal, -direction) <= 0.0 || !covered) { return result; }
|
|
627
|
+
// Preserve successful refinement and exact flat-face silhouettes. Smooth
|
|
628
|
+
// shading can point a failed correction away from the depth surface; retry
|
|
629
|
+
// with depth geometry, under the same source, facing and thickness checks.
|
|
630
|
+
let refined = ssrRefineHit(origin, direction, maxDistance, thickness, fullSize,
|
|
631
|
+
pixel, depth, normal, result, false);
|
|
632
|
+
if (refined.hit > 0.0 || !ssrShadingNormalVaries(pixel, normal)) {
|
|
633
|
+
if (refined.hit <= 0.0 && rescued) {
|
|
634
|
+
// Hi-Z/local rescue already validated the candidate against the full
|
|
635
|
+
// depth, normal, coverage, ray-distance, and thickness gates. Preserve
|
|
636
|
+
// that exact texel instead of letting the generic bisection walk back
|
|
637
|
+
// onto the empty coarse sample.
|
|
638
|
+
return SsrTraceHit(1.0, rescuedUv, rescuedThickness, result.reactivity);
|
|
639
|
+
}
|
|
640
|
+
return refined;
|
|
641
|
+
}
|
|
642
|
+
if (rescued) {
|
|
643
|
+
return SsrTraceHit(1.0, rescuedUv, rescuedThickness, result.reactivity);
|
|
644
|
+
}
|
|
645
|
+
return ssrRefineHit(origin, direction, maxDistance, thickness, fullSize,
|
|
646
|
+
pixel, depth, normal, result, true);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
fn ssrHitTapAdmitted(pixel : vec2<i32>, rayDirection : vec3<f32>) -> bool {
|
|
650
|
+
let normal = textureLoad(sceneNormal, pixel, 0).xyz * 2.0 - vec3<f32>(1.0);
|
|
651
|
+
return textureLoad(reflectionFallback, pixel, 0).a > 0.5 && dot(normal, -rayDirection) > 0.0;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
struct SsrHitSample {
|
|
655
|
+
color : vec4<f32>,
|
|
656
|
+
reactivity : f32,
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
fn sampleSsrHitSample(uv : vec2<f32>, rayDirection : vec3<f32>) -> SsrHitSample {
|
|
660
|
+
// Like Three's colorNode.sample(hitUv), retain the refined subpixel hit.
|
|
661
|
+
// Explicit bilinear loads preserve the existing sampler-free compute ABI.
|
|
662
|
+
// Color and source reactivity use the same four admitted texels, so keep one
|
|
663
|
+
// footprint walk instead of reloading normal/coverage for a second
|
|
664
|
+
// independent reactivity pass. Preserve the old zero-weight guard for the
|
|
665
|
+
// reactivity max; a zero-weight moving neighbor must not invalidate history.
|
|
666
|
+
// Roughness filtering still belongs to the reflection-only mip pyramid.
|
|
667
|
+
let size = vec2<i32>(textureDimensions(sceneColor, 0));
|
|
668
|
+
let position = uv * vec2<f32>(size) - vec2<f32>(0.5);
|
|
669
|
+
let first = vec2<i32>(floor(position));
|
|
670
|
+
let fraction = fract(position);
|
|
671
|
+
let last = size - vec2<i32>(1);
|
|
672
|
+
var sum = vec4<f32>(0.0);
|
|
673
|
+
var reactivity = 0.0;
|
|
674
|
+
for (var y = 0; y < 2; y++) {
|
|
675
|
+
for (var x = 0; x < 2; x++) {
|
|
676
|
+
let pixel = clamp(first + vec2<i32>(x, y), vec2<i32>(0), last);
|
|
677
|
+
// Match the trace's Standard source-coverage admission for every tap.
|
|
678
|
+
// Sky/background color is not hit radiance. Preserve its missing area
|
|
679
|
+
// as confidence so composition supplies the receiver's own fallback.
|
|
680
|
+
if (!ssrHitTapAdmitted(pixel, rayDirection)) {
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
let weight = select(1.0 - fraction.x, fraction.x, x == 1) * select(1.0 - fraction.y, fraction.y, y == 1);
|
|
684
|
+
let color = textureLoad(sceneColor, pixel, 0).rgb;
|
|
685
|
+
sum += vec4<f32>(color * weight, weight);
|
|
686
|
+
if (weight > 0.0) {
|
|
687
|
+
reactivity = max(reactivity, ssrSourceReactivity(pixel));
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return SsrHitSample(vec4<f32>(sum.rgb / max(sum.a, 1e-6), sum.a), reactivity);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
@compute @workgroup_size(8, 8, 1)
|
|
695
|
+
fn ssr_trace(@builtin(global_invocation_id) globalId : vec3<u32>) {
|
|
696
|
+
let fullSize = textureDimensions(sceneColor, 0);
|
|
697
|
+
let traceSize = max(fullSize / vec2<u32>(2u), vec2<u32>(1u));
|
|
698
|
+
if (globalId.x >= traceSize.x || globalId.y >= traceSize.y) {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
textureStore(hitReactivityOutput, vec2<i32>(globalId.xy), vec4<f32>(0.0));
|
|
702
|
+
let fullPixel = min(globalId.xy * vec2<u32>(2u), fullSize - vec2<u32>(1u));
|
|
703
|
+
let physicalMaxMip = textureNumLevels(hizPyramid) - 1u;
|
|
704
|
+
let hizMaxMip = min(maxSsrHiZMip(fullSize), physicalMaxMip + 1u);
|
|
705
|
+
let hizSize = textureDimensions(hizPyramid, physicalMaxMip);
|
|
706
|
+
let hizPixel = min(globalId.xy, hizSize - vec2<u32>(1u));
|
|
707
|
+
let depth = textureLoad(sceneDepth, vec2<i32>(fullPixel), 0);
|
|
708
|
+
let normalData = textureLoad(sceneNormal, vec2<i32>(fullPixel), 0);
|
|
709
|
+
let normalUnnormalized = normalData.xyz * 2.0 - vec3<f32>(1.0);
|
|
710
|
+
let normalLength = length(normalUnnormalized);
|
|
711
|
+
if (!isFinite(depth) || depth >= 1.0 || !isFinite(normalLength) || normalLength <= 1e-5 ||
|
|
712
|
+
textureLoad(reflectionFallback, vec2<i32>(fullPixel), 0).a <= 0.5 ||
|
|
713
|
+
!isFinite(view.ssrParams.w) || view.ssrParams.w <= 0.5 ||
|
|
714
|
+
!isFinite(normalData.a) || normalData.a >= view.ssrParams.z) {
|
|
715
|
+
textureStore(traceOutput, vec2<i32>(globalId.xy), vec4<f32>(0.0));
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
let normal = select(
|
|
719
|
+
vec3<f32>(0.0, 0.0, 1.0),
|
|
720
|
+
normalUnnormalized / normalLength,
|
|
721
|
+
isFinite(normalLength) && normalLength > 1e-5,
|
|
722
|
+
);
|
|
723
|
+
let hizDepth = textureLoad(hizPyramid, vec2<i32>(hizPixel), i32(physicalMaxMip)).r;
|
|
724
|
+
let uv = (vec2<f32>(fullPixel) + vec2<f32>(0.5)) / vec2<f32>(fullSize);
|
|
725
|
+
// The record-side sentinel is deliberately binary: a finite zero means the
|
|
726
|
+
// authored effect is disabled, while any non-finite value must fail closed.
|
|
727
|
+
// Do not turn a disabled-but-valid View tail into a one-frame SSR request.
|
|
728
|
+
let enabled = select(0.0, 1.0, isFinite(view.ssrParams.w) && view.ssrParams.w > 0.5);
|
|
729
|
+
let maxDistance = enabled * view.ssrParams.x;
|
|
730
|
+
let thickness = enabled * view.ssrParams.y;
|
|
731
|
+
let roughnessLimit = enabled * clamp(view.ssrParams.z, 0.0, 1.0);
|
|
732
|
+
let worldPosition = reconstructWorldPosition(uv, depth);
|
|
733
|
+
let viewDirectionRaw = view.cameraPos - worldPosition;
|
|
734
|
+
let viewDirectionLength = length(viewDirectionRaw);
|
|
735
|
+
let viewDirection = select(
|
|
736
|
+
vec3<f32>(0.0, 0.0, 1.0),
|
|
737
|
+
viewDirectionRaw / viewDirectionLength,
|
|
738
|
+
isFinite(viewDirectionLength) && viewDirectionLength > 1e-5,
|
|
739
|
+
);
|
|
740
|
+
let facing = clamp(dot(normal, viewDirection) * 8.0, 0.0, 1.0);
|
|
741
|
+
let reflectionDirection = normalize(reflect(-viewDirection, normal));
|
|
742
|
+
// Keep the reflected line anchored to the shaded receiver. A normal offset
|
|
743
|
+
// moves the projected hit across fine source texels. The march starts beyond
|
|
744
|
+
// the origin and rejects back-facing sources, including this receiver plane.
|
|
745
|
+
let hitResult = traceScreenRay(
|
|
746
|
+
worldPosition,
|
|
747
|
+
reflectionDirection,
|
|
748
|
+
maxDistance,
|
|
749
|
+
thickness,
|
|
750
|
+
fullSize,
|
|
751
|
+
hizDepth,
|
|
752
|
+
hizMaxMip,
|
|
753
|
+
);
|
|
754
|
+
let edge = traceEdge(hitResult.uv);
|
|
755
|
+
textureStore(hitReactivityOutput, vec2<i32>(globalId.xy), vec4<f32>(hitResult.reactivity));
|
|
756
|
+
let roughness = finiteConfidence(
|
|
757
|
+
select(
|
|
758
|
+
0.0,
|
|
759
|
+
1.0 - smoothstep(roughnessLimit * 0.8, max(roughnessLimit, 1e-5), clamp(normalData.a, 0.0, 1.0)),
|
|
760
|
+
roughnessLimit > 0.0,
|
|
761
|
+
),
|
|
762
|
+
);
|
|
763
|
+
let temporal = 1.0;
|
|
764
|
+
let confidence = traceConfidence(
|
|
765
|
+
hitResult.hit,
|
|
766
|
+
hitResult.thickness,
|
|
767
|
+
facing,
|
|
768
|
+
edge,
|
|
769
|
+
roughness,
|
|
770
|
+
temporal,
|
|
771
|
+
);
|
|
772
|
+
if (confidence <= 0.0) {
|
|
773
|
+
textureStore(traceOutput, vec2<i32>(globalId.xy), vec4<f32>(0.0));
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
let hitSample = sampleSsrHitSample(hitResult.uv, reflectionDirection);
|
|
777
|
+
let hitColor = hitSample.color;
|
|
778
|
+
if (!all(vec3<bool>(isFinite(hitColor.r), isFinite(hitColor.g), isFinite(hitColor.b)))) {
|
|
779
|
+
textureStore(traceOutput, vec2<i32>(globalId.xy), vec4<f32>(0.0));
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
textureStore(traceOutput, vec2<i32>(globalId.xy), vec4<f32>(hitColor.rgb, confidence * hitColor.a));
|
|
783
|
+
textureStore(hitReactivityOutput, vec2<i32>(globalId.xy), vec4<f32>(max(hitResult.reactivity, hitSample.reactivity)));
|
|
784
|
+
}
|