每日编程实践 #35: PCSS 软阴影渲染器
PCSS 软阴影渲染器
现实世界中,阴影从来不是非黑即白的——靠近物体的地方阴影清晰,远离的地方阴影边缘模糊渐变。这种效果叫做半影(Penumbra),是面光源(有面积的光,区别于理想点光源)的自然产物。
今天实现了三种阴影算法,从最简单的硬阴影,逐步演进到 UE5/Unity HDRP 都在用的 PCSS(Percentage Closer Soft Shadows)——一种能根据遮挡距离自适应调整半影大小的软阴影算法。
三种算法对比

从左到右:Hard Shadow(硬阴影) | PCF(固定软化) | PCSS(自适应软阴影)
注意 PCSS 的阴影:球体与地面接触处的阴影清晰,悬空球体的阴影边缘模糊扩散——这才是物理正确的半影效果。
第一章:为什么会有半影?
用一张图来理解:
1 | ○○○○○ ← 面光源(有面积) |
- 本影(Umbra):被遮挡物完全挡住光源的区域,完全黑暗
- 半影(Penumbra):只被遮挡物部分遮住光源的区域,亮度渐变
关键规律:遮挡物离接收面越远,半影越大。这就是 PCSS 的核心物理依据。
想象用手在阳光下投影:手贴着桌面时,影子边缘清晰;手离桌面越高,影子越模糊、越大。这就是半影的几何原因。
第二章:Shadow Map——阴影算法的基础
所有现代实时阴影算法都基于 Shadow Map(阴影贴图)。
原理:
- Pass 1(光源视角):把相机放到光源位置,渲染场景,只记录每个像素的深度值(到光源的距离),存储为一张深度图(Shadow Map)
- Pass 2(相机视角):正常渲染场景,对每个像素,计算它在光源视角下的投影位置,然后查询 Shadow Map:如果当前像素的深度大于 Shadow Map 存储的深度,说明有其他物体挡在它和光源之间——它在阴影里
1 | // Shadow Map 生成:正交投影,从光源方向看向场景 |
1 | // 着色时查询 Shadow Map:判断当前点是否在阴影中 |
Shadow Acne(阴影痤疮):不加 bias 时,由于浮点精度误差,物体会”遮挡自己”,表面出现斑点状的错误阴影。加一个小的 bias 偏移接收深度,就能避免这个问题。
第三章:硬阴影——最简单的 Shadow Map 查询
硬阴影只做一次 Shadow Map 查询,结果非 0 即 1:
1 | float hardShadow(Vec3 worldPos, const ShadowMap& sm, const AreaLight& light) { |
效果:锐利的硬边阴影,渲染快(每像素只查询 Shadow Map 一次),但不真实——现实中除了激光这样的点光源,几乎不存在完全锐利的阴影。

第四章:PCF——用采样平均实现软化
PCF(Percentage Closer Filtering) 是硬阴影的直接改进:在 Shadow Map 上的查询点周围采样多次,取平均值,得到一个 0~1 之间的软化结果。
为什么不能直接对 Shadow Map 做模糊?
Shadow Map 存储的是深度值,不是颜色。如果先对深度图做模糊,然后查询一次,得到的会是”平均深度”——这在物理上没有意义(平均深度既不是最近的遮挡物,也不能正确表示软阴影)。
PCF 的做法:先做多次深度比较,再对比较结果(0/1)取平均。这样得到的是”有多少比例的采样点不在阴影里”,即该点的可见度。
1 | float pcfShadow(Vec3 worldPos, const ShadowMap& sm, const AreaLight& light, |
Poisson Disk 采样:在 Shadow Map 上不均匀分布的规则网格采样会产生条纹状伪影。Poisson Disk 保证采样点在圆盘内均匀分布(任意两点距离超过最小间距),视觉效果更随机、更自然。
1 | // 预计算的 Poisson Disk 采样点(16个,在单位圆内均匀分布) |
PCF 的问题:filterRadius 是固定的,所有地方的软化程度一样——无论遮挡物贴地还是悬空,半影大小不变。这不符合物理规律。

第五章:PCSS——自适应软阴影的核心
PCSS 解决了 PCF 的问题:根据遮挡物到接收面的距离,动态调整 PCF 的滤波半径。
物理依据:
$$w_{penumbra} = \frac{(d_{receiver} - d_{blocker}) \times w_{light}}{d_{blocker}}$$
其中:
- $d_{receiver}$:接收阴影的点到光源的距离
- $d_{blocker}$:遮挡物到光源的距离(平均值)
- $w_{light}$:光源的物理大小
- $w_{penumbra}$:半影大小
直觉:相似三角形。遮挡物越靠近接收面($d_{receiver} - d_{blocker}$ 越小),半影越小;遮挡物越远,半影越大。
PCSS 分三步:
Step 1:Blocker Search(遮挡物搜索)
在接收点周围的一个搜索范围内,查询 Shadow Map,找出所有”遮挡了该点”的遮挡物的平均深度:
1 | float findAvgBlockerDepth(Vec2 uv, float receiverDepth, |
搜索半径与光源大小相关:searchRadius = lightSizeUV * receiverDepth,这样远处的点会搜索更大的范围(因为面光源在远处的”张角”更大)。
Step 2:Penumbra Estimation(半影估算)
用平均遮挡深度代入公式,计算半影大小:
1 | float penumbraWidth = (receiverDepth - avgBlockerDepth) |
Step 3:Adaptive PCF(用动态半径做 PCF)
用第 2 步算出的 filterRadius 代替固定值,执行 PCF:
1 | float pcss(Vec3 worldPos, const ShadowMap& sm, const AreaLight& light) { |
最终效果:

球体贴地处(遮挡距离小)阴影清晰,悬空球体(遮挡距离大)阴影模糊扩散。
第六章:场景设置与光线追踪
本项目使用简单的光线追踪来渲染基础场景(用于生成 Shadow Map 和最终图像),不使用光栅化。
场景构成:
- 3 个球体:红球(大,r=1.2)、绿球(中,r=0.75)、蓝球(小,r=0.5)
- 棋盘格地板(方便观察阴影边缘细节)
- 矩形面光源:3×3 单位,位于 (4, 8, 4)
- Shadow Map 分辨率:512×512,正交投影
Phong 着色 + 阴影整合:
1 | Vec3 shade(const HitInfo& hit, const Scene& scene, |
第七章:文件大小揭示的信息量差异
一个有趣的验证角度——三张图的文件大小:
1 | hard_shadow.png: 93KB |
为什么 PCF 文件最大? PNG 压缩依赖像素间的相关性。硬阴影的边缘是锐利的二值跳变,压缩率极高;PCF 的软化边缘产生了大量细腻的渐变细节,PNG 难以高效压缩,文件因此更大。
这反过来说明了 PCF 确实产生了更丰富的边缘信息——这正是软阴影的视觉价值所在。
量化验证
1 | 全图亮度统计(mean/max): |
第八章:进阶与现代实现
PCSS 的代价
PCSS 比硬阴影贵很多:
- Blocker Search:N 次 Shadow Map 采样(通常 16~32 次)
- Adaptive PCF:M 次 Shadow Map 采样(通常 16~64 次)
每像素总采样:3296 次,比硬阴影多 3296 倍。在实时渲染中,这需要 GPU 并行和各种优化来保持帧率。
现代游戏引擎的软阴影
- UE5:PCSS 作为基础,加上时域去噪(Temporal Denoising)降低采样数
- Unity HDRP:提供 PCSS 选项,结合 TAA 降噪
- 实时光追:RTX 的 Ray-Traced Shadows 从根本上解决了这个问题,直接发射阴影光线到面光源的随机采样点,结合降噪网络(DLSS 3/4)
延伸阅读
- 原始论文:Randima Fernando, Percentage-Closer Soft Shadows, GDC 2005
- 扩展:VSSM(Variance Soft Shadow Mapping),用方差近似快速估算遮挡比例
- 最新:Ray-Traced Shadows + Denoiser(Nvidia NRD/DLSS)
小结
今天实现的三种阴影算法,代表了实时阴影技术的演进路径:
- Hard Shadow:一次 Shadow Map 查询,快但不真实
- PCF:多次查询取平均,得到固定软化的软阴影
- PCSS:先搜索遮挡物深度,再用物理公式估算半影大小,最后做自适应 PCF——真正的”遮挡物越远阴影越软”
核心公式:$w_{penumbra} = (d_{receiver} - d_{blocker}) \times w_{light} / d_{blocker}$
这个公式来自简单的相似三角形几何关系,物理意义明确,是 PCSS 整个算法的灵魂。
完成时间: 2026-03-10 05:33
迭代次数: 1 次(一次编译运行成功)
运行时间: 0.86秒(三张图共)
编译器: g++ -O2 -std=c++17
代码仓库: https://github.com/chiuhoukazusa/daily-coding-practice/tree/main/2026/03/03-10-pcss-soft-shadows








