66#define SAMPLES_PCSS 1
77#endif
88
9- float samplePoissonPCSS(sampler2DArrayShadow shadow_map, vec4 shadowUV, float maxUVOffset)
10- {
9+ // Full 16-tap Poisson disc. Unconditional (independent of SMOOTH_PCSS) because it has three
10+ // consumers with different needs: samplePoissonPCSS() takes the first SAMPLES_PCSS of these,
11+ // pcssBlockerSearch() always wants the first 8 regardless of SAMPLES_PCSS (a blocker search
12+ // only needs a coarse estimate), and the raytraced cone/AO sampling further below always
13+ // wants the full spread. Keeping one table means none of those three can end up indexing
14+ // past a table sized for a different consumer.
15+ const vec2 poissonDisc16[16] = vec2[](
16+ vec2(-0.76275, -0.3432573),
17+ vec2(-0.5226235, -0.8277544),
18+ vec2(-0.3780261, 0.01528688),
19+ vec2(-0.7742821, 0.4245702),
20+ vec2(0.04196143, -0.02622231),
21+ vec2(-0.2974772, -0.4722782),
22+ vec2(-0.516093, 0.71495),
23+ vec2(-0.3257416, 0.3910343),
24+ vec2(0.2705966, 0.6670476),
25+ vec2(0.4918377, 0.1853267),
26+ vec2(0.4428544, -0.6251478),
27+ vec2(-0.09204347, 0.9267113),
28+ vec2(0.391505, -0.2558275),
29+ vec2(0.05605913, -0.7570801),
30+ vec2(0.81772, -0.02475523),
31+ vec2(0.6890262, 0.5191521)
32+ );
33+
34+ // Low-quality raster PCSS fallback: a single centered sample (i.e. no offset at all) rather
35+ // than poissonDisc16[0], which is just one arbitrary tap among sixteen and no better centered
36+ // than any other.
37+ const vec2 poissonDiscCenter[1] = vec2[](vec2(0.0, 0.0));
38+
39+ // samplePoissonPCSS()'s tap table: the full disc when taking all SAMPLES_PCSS == 16 of them,
40+ // or the single centered sample when SAMPLES_PCSS == 1.
1141#if SMOOTH_PCSS
12- vec2 poissonDisc[16] = vec2[](
13- vec2(-0.76275, -0.3432573),
14- vec2(-0.5226235, -0.8277544),
15- vec2(-0.3780261, 0.01528688),
16- vec2(-0.7742821, 0.4245702),
17- vec2(0.04196143, -0.02622231),
18- vec2(-0.2974772, -0.4722782),
19- vec2(-0.516093, 0.71495),
20- vec2(-0.3257416, 0.3910343),
21- vec2(0.2705966, 0.6670476),
22- vec2(0.4918377, 0.1853267),
23- vec2(0.4428544, -0.6251478),
24- vec2(-0.09204347, 0.9267113),
25- vec2(0.391505, -0.2558275),
26- vec2(0.05605913, -0.7570801),
27- vec2(0.81772, -0.02475523),
28- vec2(0.6890262, 0.5191521)
29- );
42+ #define poissonDisc poissonDisc16
3043#else
31- vec2 poissonDisc[1] = vec2[](
32- vec2(0.0,0.0)
33- );
44+ #define poissonDisc poissonDiscCenter
3445#endif
3546
47+ float samplePoissonPCSS(sampler2DArrayShadow shadow_map, vec4 shadowUV, float maxUVOffset)
48+ {
3649 float visibility = 0.0f;
3750 for (int i=0; i<SAMPLES_PCSS; i++) {
3851 vec4 uv = shadowUV;
@@ -42,8 +55,80 @@ float samplePoissonPCSS(sampler2DArrayShadow shadow_map, vec4 shadowUV, float ma
4255 return visibility * (1.0f/float(SAMPLES_PCSS));
4356}
4457
45- float getShadowValue(sampler2DArrayShadow shadow_map, float depth, vec4 shadowUV[NUM_SHADOW_CASCADES],
58+ // PCSS blocker search: raw (non-compare) depth taps within searchRadiusUV, using the first 8
59+ // of poissonDisc16 (always the full-spread table, not samplePoissonPCSS()'s poissonDisc --
60+ // a blocker search only needs a coarse estimate, not the final filtered result, but it needs
61+ // that estimate regardless of how many taps the raster PCSS path itself is set to take).
62+ // "Blocker" = stored < receiverDepth, matching the GL_LEQUAL/eLessOrEqual compare convention
63+ // the hardware PCF sampler already uses (visible iff Dref <= stored) -- see the compare-sampler
64+ // setup in gropengltnl.cpp / VulkanPostProcessingShadow.cpp.
65+ // A negative return means "no blockers found", which the average blocker depth (always >= 0)
66+ // can't otherwise be distinguished from "found some at depth 0".
67+ float pcssBlockerSearch(sampler2DArray shadow_map_raw, vec4 shadowUV, float searchRadiusUV)
68+ {
69+ float sum = 0.0;
70+ int blockerCount = 0;
71+ for (int i = 0; i < 8; i++) {
72+ vec2 uv = shadowUV.xy + poissonDisc16[i] * searchRadiusUV;
73+ float sampled = texture(shadow_map_raw, vec3(uv, shadowUV.z)).r;
74+ if (sampled < shadowUV.w) {
75+ sum += sampled;
76+ blockerCount++;
77+ }
78+ }
79+ return blockerCount > 0 ? (sum / float(blockerCount)) : -1.0;
80+ }
81+
82+ // Computes the PCSS penumbra radius for one cascade sample: a blocker search followed by
83+ // the orthographic-light penumbra formula (see shadow_smoothness_scale()'s doc comment in
84+ // shadows.cpp for the derivation), clamped to [1 texel, smoothnessCeiling]. penumbraScale is
85+ // Shadow_penumbra_scale[cascade] -- a negative value is the "unsupported or user-disabled"
86+ // sentinel (see shadow_contact_hardening_enabled() in shadows.cpp), in which case this
87+ // returns smoothnessCeiling immediately: bit-identical to the fixed-width behavior from
88+ // before contact hardening existed, and skips the blocker search's extra texture reads.
89+ float pcssPenumbraRadius(sampler2DArrayShadow shadow_map, sampler2DArray shadow_map_raw,
90+ vec4 shadowUV, float penumbraScale, float smoothnessCeiling)
91+ {
92+ float texelFloor = 1.0 / float(textureSize(shadow_map, 0).x);
93+
94+ if (penumbraScale < 0.0) {
95+ return smoothnessCeiling;
96+ }
97+
98+ float avgBlockerDepth = pcssBlockerSearch(shadow_map_raw, shadowUV, smoothnessCeiling);
99+ if (avgBlockerDepth < 0.0) {
100+ // The sparse 8-tap search found nothing, but that doesn't prove there's no thin
101+ // occluder nearby -- run the full PCF at the hardest radius rather than assuming
102+ // fully lit, so a missed thin blocker still reads as an edge instead of popping to
103+ // no shadow at all.
104+ return texelFloor;
105+ }
106+
107+ float deltaDepth = max(shadowUV.w - avgBlockerDepth, 0.0);
108+ return clamp(deltaDepth * penumbraScale, texelFloor, smoothnessCeiling);
109+ }
110+
111+ // PCSS-filtered shadow visibility for a single cascade: looks up its penumbra radius (see
112+ // pcssPenumbraRadius()) and samples the shadow map at that radius. Factored out of
113+ // getShadowValue() below so the cascade-index unpacking (v/i from cascade/4, cascade%4) and
114+ // the penumbra-radius lookup each appear once, whichever of getShadowValue()'s two return
115+ // paths runs.
116+ float shadowVisibilityForCascade(sampler2DArrayShadow shadow_map, sampler2DArray shadow_map_raw,
117+ vec4 shadowUV, int cascade, vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4],
118+ vec4 penumbra_scale[(NUM_SHADOW_CASCADES + 4 - 1) / 4])
119+ {
120+ int v = cascade / 4;
121+ int i = cascade % 4;
122+
123+ float radius = pcssPenumbraRadius(shadow_map, shadow_map_raw, shadowUV,
124+ penumbra_scale[v][i], smoothness_factors[v][i]);
125+
126+ return samplePoissonPCSS(shadow_map, shadowUV, radius);
127+ }
128+
129+ float getShadowValue(sampler2DArrayShadow shadow_map, sampler2DArray shadow_map_raw, float depth, vec4 shadowUV[NUM_SHADOW_CASCADES],
46130 vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4], vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4],
131+ vec4 penumbra_scale[(NUM_SHADOW_CASCADES + 4 - 1) / 4],
47132 int cascade_offset, int cascade_count)
48133{
49134 int cascade = cascade_offset + cascade_count;
@@ -52,19 +137,19 @@ float getShadowValue(sampler2DArrayShadow shadow_map, float depth, vec4 shadowUV
52137 }
53138 if (cascade >= cascade_offset + cascade_count || cascade < cascade_offset) return 1.0;
54139
55- int cascade_v = cascade / 4;
56- int cascade_i = cascade % 4;
57-
58140 float cascade_start = (cascade > cascade_offset) ? cascade_distances[(cascade - 1) / 4][(cascade - 1) % 4] : 0.0;
59- float cascade_end = cascade_distances[cascade_v][cascade_i ];
141+ float cascade_end = cascade_distances[cascade / 4][cascade % 4 ];
60142 float dist_threshold = (cascade_end - cascade_start) * 0.2;
61143
62144 if (cascade_end - dist_threshold > depth || cascade >= cascade_offset + cascade_count - 1)
63- return samplePoissonPCSS(shadow_map, shadowUV[cascade], smoothness_factors[cascade_v][cascade_i]);
145+ return shadowVisibilityForCascade(shadow_map, shadow_map_raw, shadowUV[cascade], cascade,
146+ smoothness_factors, penumbra_scale);
64147
65148 return mix(
66- samplePoissonPCSS(shadow_map, shadowUV[cascade], smoothness_factors[cascade_v][cascade_i]),
67- samplePoissonPCSS(shadow_map, shadowUV[cascade + 1], smoothness_factors[(cascade + 1) / 4][(cascade + 1) % 4]),
149+ shadowVisibilityForCascade(shadow_map, shadow_map_raw, shadowUV[cascade], cascade,
150+ smoothness_factors, penumbra_scale),
151+ shadowVisibilityForCascade(shadow_map, shadow_map_raw, shadowUV[cascade + 1], cascade + 1,
152+ smoothness_factors, penumbra_scale),
68153 smoothstep(cascade_end - dist_threshold, cascade_end, depth));
69154}
70155// Raytraced shadow test via inline ray query against the shadow TLAS. Only
@@ -127,50 +212,13 @@ float traceShadowRay(accelerationStructureEXT tlas, vec3 worldPos, vec3 worldNor
127212 return (rayQueryGetIntersectionTypeEXT(rq, true) == gl_RayQueryCommittedIntersectionNoneEXT) ? 1.0 : 0.0;
128213}
129214
130- // Penumbra/AO sampling: at most this many rays per fragment per light, matching
131- // the disc pattern size below. rtShadowSampleCount/rtaoSampleCount
132- // (shadowCascadeParams) are clamped against it, so a bad uniform value can't
215+ // Penumbra/AO sampling: at most this many rays per fragment per light, matching the size of
216+ // poissonDisc16 above (the RT cone/AO sampling below always wants the full 16-tap spread,
217+ // independent of SAMPLES_PCSS/SMOOTH_PCSS -- see the comment on poissonDisc16). rtShadowSampleCount/
218+ // rtaoSampleCount (shadowCascadeParams) are clamped against it, so a bad uniform value can't
133219// index out of the pattern.
134220#define RT_SHADOW_MAX_SAMPLES 16
135221
136- // Calibration factor applied to a *sun's* angular radius before it becomes a penumbra
137- // cone, so that a given $SunAngularSize: reads as roughly the same softness whichever
138- // shadow method is active. Local lights don't get it -- their penumbra is already
139- // consistent, and they have no shadow-mapped counterpart to match.
140- //
141- // It exists because the two methods disagree about occluder distance. traceShadowRayCone()
142- // uses the real one, so a Sol-sized sun gives a penumbra of only ~0.1 world units at the
143- // 20-unit separations typical of a fighter shadowing itself. The shadow map has no blocker
144- // search and can't: shadow_smoothness_scale() in shadows.cpp calibrates the tabled
145- // $Shadow Smoothness Factor: values against a blocker distance pinned to the cascade's own
146- // extent, which is a couple of hundred units out for the near cascade -- an order of
147- // magnitude more, and a correspondingly wider penumbra. Scaling the ray cone up meets it
148- // there.
149- //
150- // So this deliberately trades physical accuracy for cross-method consistency, and it is the
151- // number to revisit if the shadow map ever grows a real blocker search -- at that point both
152- // methods have a true occluder distance and the factor should go back to 1.
153- const float RT_SHADOW_SUN_SIZE_CALIBRATION = 10.0;
154-
155- const vec2 rtPoissonDisc[RT_SHADOW_MAX_SAMPLES] = vec2[](
156- vec2(-0.76275, -0.3432573),
157- vec2(-0.5226235, -0.8277544),
158- vec2(-0.3780261, 0.01528688),
159- vec2(-0.7742821, 0.4245702),
160- vec2(0.04196143, -0.02622231),
161- vec2(-0.2974772, -0.4722782),
162- vec2(-0.516093, 0.71495),
163- vec2(-0.3257416, 0.3910343),
164- vec2(0.2705966, 0.6670476),
165- vec2(0.4918377, 0.1853267),
166- vec2(0.4428544, -0.6251478),
167- vec2(-0.09204347, 0.9267113),
168- vec2(0.391505, -0.2558275),
169- vec2(0.05605913, -0.7570801),
170- vec2(0.81772, -0.02475523),
171- vec2(0.6890262, 0.5191521)
172- );
173-
174222// Orthonormal basis spanning the plane perpendicular to axis (a unit vector).
175223void rtBuildOnb(vec3 axis, out vec3 tangent, out vec3 bitangent)
176224{
@@ -225,7 +273,7 @@ float traceShadowRayCone(accelerationStructureEXT tlas, vec3 worldPos, vec3 worl
225273 int samples = min(sampleCount, RT_SHADOW_MAX_SAMPLES);
226274 float visibility = 0.0;
227275 for (int i = 0; i < samples; ++i) {
228- vec2 p = rtPoissonDisc [i];
276+ vec2 p = poissonDisc16 [i];
229277 p = vec2(p.x * cosR - p.y * sinR, p.x * sinR + p.y * cosR);
230278 vec3 sampleDir = normalize(direction + (tangent * p.x + bitangent * p.y) * coneHalfAngleTan);
231279
@@ -277,7 +325,7 @@ float traceAmbientOcclusion(accelerationStructureEXT tlas, vec3 worldPos, vec3 w
277325 int samples = min(sampleCount, RT_SHADOW_MAX_SAMPLES);
278326 float occlusion = 0.0;
279327 for (int i = 0; i < samples; ++i) {
280- vec2 p = rtPoissonDisc [i];
328+ vec2 p = poissonDisc16 [i];
281329 p = vec2(p.x * cosR - p.y * sinR, p.x * sinR + p.y * cosR);
282330 vec3 sampleDir = normalize(tangent * p.x + bitangent * p.y
283331 + worldNormal * sqrt(max(1.0 - dot(p, p), 0.0)));
0 commit comments