Skip to content

Commit 5a5c565

Browse files
authored
Better shadows: soft shadows using percentage-closer filtering (PCF), tighter shadow frustum (from scene AABBs) (#71)
* shaders : PbrBasic : just use glsl `textureSize` to get total atlas size * shaders : tone_mapping.glsl : do not gamma correct exposureToneMapping * shaders : pcf soft shadows * core : Collision : add function `getAABBCorners()` * core/DepthAndShadowPass.cpp : tighter (and correct) light-space AABB bounds * Update CHANGELOG
1 parent e6a4f8c commit 5a5c565

File tree

8 files changed

+117
-61
lines changed

8 files changed

+117
-61
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- core/DepthAndShadowPass.h : handle multiple shadow maps using a texture atlas (https://github.com/Simple-Robotics/candlewick/pull/69)
1616
- multibody : handle two-light setup w/ shadow mapping (https://github.com/Simple-Robotics/candlewick/pull/69)
1717
- multibody/Visualizer : use `H` key to toggle GUI (https://github.com/Simple-Robotics/candlewick/pull/70)
18+
- shaders : Soft shadows in PBR using percentage-closer filtering (PCF) (https://github.com/Simple-Robotics/candlewick/pull/71)
19+
- core : Tighter shadow frustum around the world-scene AABB (https://github.com/Simple-Robotics/candlewick/pull/71)
20+
- core : add `getAABBCorners()` util function in `Collision.h` header (https://github.com/Simple-Robotics/candlewick/pull/71)
1821

1922
## [0.1.0] - 2025-05-21
2023

shaders/compiled/PbrBasic.frag.msl

Lines changed: 54 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ struct PbrMaterial
5555
struct ShadowAtlasInfo
5656
{
5757
int4 lightRegions[2];
58-
uint2 atlasSize;
5958
};
6059

6160
struct LightBlock
@@ -170,57 +169,57 @@ float3 calculatePbrLighting(thread const float3& normal, thread const float3& V,
170169
static inline __attribute__((always_inline))
171170
bool isCoordsInRange(thread const float3& uv)
172171
{
173-
bool _69 = uv.x >= 0.0;
174-
bool _76;
175-
if (_69)
172+
bool _72 = uv.x >= 0.0;
173+
bool _79;
174+
if (_72)
176175
{
177-
_76 = uv.y >= 0.0;
176+
_79 = uv.y >= 0.0;
178177
}
179178
else
180179
{
181-
_76 = _69;
180+
_79 = _72;
182181
}
183-
bool _83;
184-
if (_76)
182+
bool _86;
183+
if (_79)
185184
{
186-
_83 = uv.x <= 1.0;
185+
_86 = uv.x <= 1.0;
187186
}
188187
else
189188
{
190-
_83 = _76;
189+
_86 = _79;
191190
}
192-
bool _89;
193-
if (_83)
191+
bool _92;
192+
if (_86)
194193
{
195-
_89 = uv.y <= 1.0;
194+
_92 = uv.y <= 1.0;
196195
}
197196
else
198197
{
199-
_89 = _83;
198+
_92 = _86;
200199
}
201-
bool _96;
202-
if (_89)
200+
bool _99;
201+
if (_92)
203202
{
204-
_96 = uv.z >= 0.0;
203+
_99 = uv.z >= 0.0;
205204
}
206205
else
207206
{
208-
_96 = _89;
207+
_99 = _92;
209208
}
210-
bool _102;
211-
if (_96)
209+
bool _105;
210+
if (_99)
212211
{
213-
_102 = uv.z <= 1.0;
212+
_105 = uv.z <= 1.0;
214213
}
215214
else
216215
{
217-
_102 = _96;
216+
_105 = _99;
218217
}
219-
return _102;
218+
return _105;
220219
}
221220

222221
static inline __attribute__((always_inline))
223-
float calcShadowmap(thread const int& lightIndex, thread const float& NdotL, thread spvUnsafeArray<float3, 2>& fragLightPos, constant ShadowAtlasInfo& _420, depth2d<float> shadowMap, sampler shadowMapSmplr)
222+
float calcShadowmap(thread const int& lightIndex, thread const float& NdotL, thread const int2& atlasSize, thread spvUnsafeArray<float3, 2>& fragLightPos, constant ShadowAtlasInfo& _422, depth2d<float> shadowMap, sampler shadowMapSmplr)
224223
{
225224
float bias0 = 0.004999999888241291046142578125;
226225
float3 lightSpacePos = fragLightPos[lightIndex];
@@ -233,11 +232,24 @@ float calcShadowmap(thread const int& lightIndex, thread const float& NdotL, thr
233232
{
234233
return 1.0;
235234
}
236-
int4 region = _420.lightRegions[lightIndex];
235+
int4 region = _422.lightRegions[lightIndex];
237236
uv = float2(region.xy) + (uv * float2(region.zw));
238-
uv /= float2(_420.atlasSize);
239-
float3 texCoords = float3(uv, depthRef);
240-
return shadowMap.sample_compare(shadowMapSmplr, texCoords.xy, texCoords.z);
237+
uv /= float2(atlasSize);
238+
float2 regionMin = float2(region.xy) / float2(atlasSize);
239+
float2 regionMax = float2(region.xy + region.zw) / float2(atlasSize);
240+
float value = 0.0;
241+
float2 offsets = float2(1.0) / float2(atlasSize);
242+
for (int i = -1; i <= 1; i++)
243+
{
244+
for (int j = -1; j <= 1; j++)
245+
{
246+
float2 offUV = uv + (float2(float(i), float(j)) * offsets);
247+
offUV = fast::clamp(offUV, regionMin, regionMax);
248+
float3 texCoords = float3(offUV, depthRef);
249+
value += (0.111111111938953399658203125 * shadowMap.sample_compare(shadowMapSmplr, texCoords.xy, texCoords.z));
250+
}
251+
}
252+
return value;
241253
}
242254

243255
static inline __attribute__((always_inline))
@@ -264,14 +276,15 @@ float3 uncharted2ToneMapping(thread const float3& color)
264276
return curr * white_scale;
265277
}
266278

267-
fragment main0_out main0(main0_in in [[stage_in]], constant Material& _505 [[buffer(0)]], constant LightBlock& light [[buffer(1)]], constant EffectParams& params [[buffer(2)]], constant ShadowAtlasInfo& _420 [[buffer(3)]], depth2d<float> shadowMap [[texture(0)]], texture2d<float> ssaoTex [[texture(1)]], sampler shadowMapSmplr [[sampler(0)]], sampler ssaoTexSmplr [[sampler(1)]], bool gl_FrontFacing [[front_facing]], float4 gl_FragCoord [[position]])
279+
fragment main0_out main0(main0_in in [[stage_in]], constant Material& _571 [[buffer(0)]], constant LightBlock& light [[buffer(1)]], constant EffectParams& params [[buffer(2)]], constant ShadowAtlasInfo& _422 [[buffer(3)]], depth2d<float> shadowMap [[texture(0)]], texture2d<float> ssaoTex [[texture(1)]], sampler shadowMapSmplr [[sampler(0)]], sampler ssaoTexSmplr [[sampler(1)]], bool gl_FrontFacing [[front_facing]], float4 gl_FragCoord [[position]])
268280
{
269281
main0_out out = {};
270282
spvUnsafeArray<float3, 2> fragLightPos = {};
271283
fragLightPos[0] = in.fragLightPos_0;
272284
fragLightPos[1] = in.fragLightPos_1;
273285
float3 normal = fast::normalize(in.fragViewNormal);
274286
float3 V = fast::normalize(-in.fragViewPos);
287+
int2 atlasSize = int2(shadowMap.get_width(), shadowMap.get_height());
275288
if (!gl_FrontFacing)
276289
{
277290
normal = -normal;
@@ -283,34 +296,35 @@ fragment main0_out main0(main0_in in [[stage_in]], constant Material& _505 [[buf
283296
float3 param = normal;
284297
float3 param_1 = V;
285298
float3 param_2 = lightDir;
286-
PbrMaterial _518;
287-
_518.baseColor = _505.material.baseColor;
288-
_518.metalness = _505.material.metalness;
289-
_518.roughness = _505.material.roughness;
290-
_518.ao = _505.material.ao;
291-
PbrMaterial param_3 = _518;
299+
PbrMaterial _584;
300+
_584.baseColor = _571.material.baseColor;
301+
_584.metalness = _571.material.metalness;
302+
_584.roughness = _571.material.roughness;
303+
_584.ao = _571.material.ao;
304+
PbrMaterial param_3 = _584;
292305
float3 param_4 = light.color[i];
293306
float param_5 = light.intensity[i].x;
294307
float3 _lo = calculatePbrLighting(param, param_1, param_2, param_3, param_4, param_5);
295308
float NdotL = fast::max(dot(normal, lightDir), 0.0);
296309
int param_6 = i;
297310
float param_7 = NdotL;
298-
float shadowValue = calcShadowmap(param_6, param_7, fragLightPos, _420, shadowMap, shadowMapSmplr);
311+
int2 param_8 = atlasSize;
312+
float shadowValue = calcShadowmap(param_6, param_7, param_8, fragLightPos, _422, shadowMap, shadowMapSmplr);
299313
_lo *= shadowValue;
300314
Lo += _lo;
301315
}
302-
float3 ambient = (float3(0.100000001490116119384765625) * _505.material.baseColor.xyz) * _505.material.ao;
316+
float3 ambient = (float3(0.100000001490116119384765625) * _571.material.baseColor.xyz) * _571.material.ao;
303317
if (params.useSsao == 1u)
304318
{
305319
float2 ssaoTexSize = float2(int2(ssaoTex.get_width(), ssaoTex.get_height()));
306320
float2 ssaoUV = gl_FragCoord.xy / ssaoTexSize;
307321
ambient *= ssaoTex.sample(ssaoTexSmplr, ssaoUV).x;
308322
}
309323
float3 color = ambient + Lo;
310-
float3 param_8 = color;
311-
color = uncharted2ToneMapping(param_8);
324+
float3 param_9 = color;
325+
color = uncharted2ToneMapping(param_9);
312326
color = powr(color, float3(0.4545454680919647216796875));
313-
out.fragColor = float4(color, _505.material.baseColor.w);
327+
out.fragColor = float4(color, _571.material.baseColor.w);
314328
out.outNormal = in.fragViewNormal.xy;
315329
return out;
316330
}

shaders/compiled/PbrBasic.frag.spv

1.49 KB
Binary file not shown.

shaders/src/PbrBasic.frag

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ layout(set=3, binding=2) uniform EffectParams {
3232

3333
layout(set = 3, binding = 3) uniform ShadowAtlasInfo {
3434
ivec4 lightRegions[NUM_LIGHTS];
35-
uvec2 atlasSize;
3635
};
3736

3837
#ifdef HAS_SHADOW_MAPS
@@ -49,7 +48,7 @@ layout(location=0) out vec4 fragColor;
4948
#endif
5049

5150
#ifdef HAS_SHADOW_MAPS
52-
float calcShadowmap(int lightIndex, float NdotL) {
51+
float calcShadowmap(int lightIndex, float NdotL, ivec2 atlasSize) {
5352
// float bias = max(0.05 * (1.0 - NdotL), 0.005);
5453
float bias = 0.005;
5554
vec3 lightSpacePos = fragLightPos[lightIndex];
@@ -62,17 +61,35 @@ float calcShadowmap(int lightIndex, float NdotL) {
6261
}
6362

6463
ivec4 region = lightRegions[lightIndex];
64+
vec2 regionMin = vec2(region.xy) / atlasSize;
65+
vec2 regionMax = vec2(region.xy + region.zw) / atlasSize;
6566
uv = region.xy + uv * region.zw;
6667
uv = uv / atlasSize;
6768

68-
vec3 texCoords = vec3(uv, depthRef);
69-
return texture(shadowMap, texCoords);
69+
float value = 0.0;
70+
const vec2 offsets = 1.0 / atlasSize;
71+
// pcf loop
72+
const int halfKernel = 1;
73+
const float weight = 1.0 / pow(2 * halfKernel + 1, 2);
74+
for (int i = -halfKernel; i <= halfKernel; i++) {
75+
for (int j = -halfKernel; j <= halfKernel; j++) {
76+
vec2 offUV = uv + vec2(i, j) * offsets;
77+
offUV = clamp(offUV, regionMin, regionMax);
78+
vec3 texCoords = vec3(offUV, depthRef);
79+
value += weight * texture(shadowMap, texCoords);
80+
}
81+
}
82+
83+
return value;
7084
}
7185
#endif
7286

7387
void main() {
7488
vec3 normal = normalize(fragViewNormal);
75-
vec3 V = normalize(-fragViewPos);
89+
const vec3 V = normalize(-fragViewPos);
90+
#ifdef HAS_SHADOW_MAPS
91+
const ivec2 atlasSize = textureSize(shadowMap, 0).xy;
92+
#endif
7693

7794
if (!gl_FrontFacing) {
7895
// Flip normal for back faces
@@ -91,8 +108,8 @@ void main() {
91108
light.intensity[i]
92109
);
93110
#ifdef HAS_SHADOW_MAPS
94-
float NdotL = max(dot(normal, lightDir), 0.0);
95-
float shadowValue = calcShadowmap(i, NdotL);
111+
const float NdotL = max(dot(normal, lightDir), 0.0);
112+
const float shadowValue = calcShadowmap(i, NdotL, atlasSize);
96113
_lo *= shadowValue;
97114
#endif
98115
Lo += _lo;

shaders/src/tone_mapping.glsl

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@ vec3 reinhardToneMapping(vec3 color) {
55
return color / (color + vec3(1.0));
66
}
77

8-
// Exposure + Gamma (Simplified)
98
vec3 exposureToneMapping(vec3 color, float exposure) {
10-
// Exposure adjustment followed by gamma correction
11-
return pow(1.0 - exp(-color * exposure), vec3(1.0/2.2));
9+
// Exposure adjustment
10+
return 1.0 - exp(-color * exposure);
1211
}
1312

1413
// ACES Filmic Tone Mapping (Academy Color Encoding System)

src/candlewick/core/Collision.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,20 @@ inline Mat4f toTransformationMatrix(const OBB &obb) {
2424
return T;
2525
}
2626

27+
inline std::array<Float3, 8> getAABBCorners(const AABB &aabb) {
28+
const Float3 min = aabb.min_.cast<float>();
29+
const Float3 max = aabb.max_.cast<float>();
30+
31+
return {
32+
Float3{min.x(), min.y(), min.z()}, // 000
33+
Float3{max.x(), min.y(), min.z()}, // 100
34+
Float3{min.x(), max.y(), min.z()}, // 010
35+
Float3{max.x(), max.y(), min.z()}, // 110
36+
Float3{min.x(), min.y(), max.z()}, // 001
37+
Float3{max.x(), min.y(), max.z()}, // 101
38+
Float3{min.x(), max.y(), max.z()}, // 011
39+
Float3{max.x(), max.y(), max.z()}, // 111
40+
};
41+
}
42+
2743
} // namespace candlewick

src/candlewick/core/DepthAndShadowPass.cpp

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -273,23 +273,32 @@ void renderShadowPassFromFrustum(CommandBuffer &cmdBuf, ShadowMapPass &passInfo,
273273
void renderShadowPassFromAABB(CommandBuffer &cmdBuf, ShadowMapPass &passInfo,
274274
std::span<const DirectionalLight> dirLight,
275275
std::span<const OpaqueCastable> castables,
276-
const AABB &worldSceneBounds) {
276+
const AABB &worldAABB) {
277277
using Eigen::Vector3d;
278278

279-
Float3 center = worldSceneBounds.center().cast<float>();
280-
float radius = 0.5f * float(worldSceneBounds.size());
281-
radius = std::ceil(radius * 16.f) / 16.f;
279+
Float3 center = worldAABB.center().cast<float>();
282280

283281
for (size_t i = 0; i < passInfo.numLights(); i++) {
284-
const Float3 eye = center - radius * dirLight[i].direction.normalized();
282+
Float3 tmpEye = center - 100.0f * dirLight[i].direction.normalized();
283+
Mat4f tmplightView = lookAt(tmpEye, center, Float3::UnitZ());
284+
285285
auto &lightView = passInfo.cam[i].view;
286286
auto &lightProj = passInfo.cam[i].projection;
287-
lightView = lookAt(eye, center, Float3::UnitZ());
288287

289-
AABB bounds{Vector3d::Constant(-radius), Vector3d::Constant(radius)};
288+
Mat3f R = tmplightView.topLeftCorner<3, 3>();
289+
Float3 t = tmplightView.topRightCorner<3, 1>();
290+
AABB bounds = coal::translate(coal::rotate(worldAABB, R.cast<double>()),
291+
t.cast<double>());
292+
293+
Float3 lightSpaceCenter = bounds.center().cast<float>();
294+
float radius = float(bounds.max_.z());
295+
296+
Float3 finalEye = center - radius * dirLight[i].direction.normalized();
297+
298+
lightView = lookAt(finalEye, center, Float3::UnitZ());
290299
lightProj = shadowOrthographicMatrix({bounds.width(), bounds.height()},
291-
float(bounds.min_.z()),
292-
float(bounds.max_.z()));
300+
float(bounds.max_.z()),
301+
float(bounds.min_.z()));
293302
}
294303
passInfo.render(cmdBuf, castables);
295304
}

src/candlewick/multibody/RobotScene.cpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ struct alignas(16) LightSpaceMatricesUbo {
3333
struct alignas(16) ShadowAtlasInfoUbo {
3434
using Vec4u = Eigen::Matrix<Uint32, 4, 1, Eigen::DontAlign>;
3535
std::array<Vec4u, kNumLights> regions;
36-
std::array<Uint32, 2> atlasSize;
3736
};
3837

3938
void updateRobotTransforms(entt::registry &registry,
@@ -455,7 +454,6 @@ void RobotScene::renderPBRTriangleGeometry(CommandBuffer &command_buffer,
455454
const bool enable_shadows = m_config.enable_shadows;
456455
ShadowAtlasInfoUbo shadowAtlasUbo{
457456
.regions{},
458-
.atlasSize{shadowPass.atlasDims()},
459457
};
460458
Mat4f lightViewProj[kNumLights];
461459
for (size_t i = 0; i < numLights; i++) {

0 commit comments

Comments
 (0)