PCSS核心是遮挡采样、半影估计、软化采样三步流程:先在光源空间采样blocker深度估算半影宽度,再据此动态控制阴影采样范围与权重;半影半径正比于(lightSize×(receiverZ−blockerZ))/blockerZ,实现“近硬远软”。

PCSS 的核心三步:遮挡采样、半影估计、软化采样
PCSS(Percentage-Closer Soft Shadows)不是“开个开关就能软”,它本质是分三阶段的手动流程:先在光源空间对 blocker(被遮挡物)做深度采样,估算半影宽度;再用这个宽度决定阴影采样的范围和权重。C++ 里你要自己控制这三步,不能依赖 OpenGL/Vulkan 的自动 shadow sampler。
关键点在于:半影大小 penumbraRadius 不是常量,它正比于 (lightSize * (receiverZ - blockerZ)) / blockerZ —— 这就是“近硬远软”的物理来源。如果直接用固定大小的 PCF,就退化成普通软阴影,失去距离感知。
- 必须在 fragment shader 中完成 blocker 深度统计(通常用 16–32 次随机采样),CPU 端 C++ 只负责传入参数和配置纹理
-
lightSize是光源直径(单位:世界坐标),需作为 uniform 传入,别误用为光源贴图尺寸 - receiverZ 是当前片元在光源视角下的深度(即从
shadowMap采得的值),blockerZ 是 blocker 深度均值,二者必须同坐标系(都是光源 NDC 深度)
如何用 C++ 控制 blocker 采样区域与半影计算
C++ 层不参与像素级计算,但要为 shader 提供正确输入:尤其是 blocker 搜索区域大小(search region)和最终的 penumbraRadius(或让 shader 自算)。常见错误是把 search region 设成固定像素值,导致远距离时搜不到 blocker,半影为 0,阴影变硬甚至漏光。
推荐做法:在 CPU 端根据光源距离动态缩放 search region。例如:
立即学习“前端免费学习笔记(深入)”;
float lightDistance = length(worldPos - lightPos); float searchRadius = fmaxf(2.0f, 16.0f * lightDistance / 10.0f); // 基于典型场景尺度调整 glUniform1f(searchRadiusLoc, searchRadius);
- searchRadius 单位是“光源投影空间中的 texel”,不是屏幕像素,所以必须和 shadow map 分辨率解耦
- 若用硬件 PCF(
textureProj+vec4坐标),注意它的 compare mode 会过滤掉所有 >= ref 的深度,因此 blocker 采样必须用texture手动读,不能用textureProj - blocker 深度均值建议用中位数或带 depth threshold 的平均(剔除过远噪声),避免单个 outlier 拉低 blockerZ 导致 penumbraRadius 爆大
PCSS 在 OpenGL/Vulkan 中的纹理绑定陷阱
PCSS 要求同一张 shadowMap 被两种方式访问:一是作为深度纹理用 GL_DEPTH_COMPONENT 格式采样 blocker 深度;二是作为比较纹理用于最终 PCF(如果走硬件 compare)。但 OpenGL 不允许同一 texture object 同时绑定为 GL_TEXTURE_2D 和 GL_TEXTURE_2D_SHADOW —— 这会导致 undefined behavior 或黑影。
- 必须创建两个 texture object:一个用
GL_R32F或GL_DEPTH_COMPONENT32F存原始深度(供 blocker 采样),另一个用GL_DEPTH_COMPONENT+GL_TEXTURE_COMPARE_MODE(供最终 PCF) - Vulkan 更严格:两个 view 必须有兼容的 format,且 depth-stencil layout 需同步;
vkCmdPipelineBarrier必须在 blocker pass 和 shading pass 之间插入,否则读到脏数据 - 别忘了设置正确的 depth comparison function:blocker 采样用
GL_LESS(找比 receiverZ 小的遮挡物),最终阴影判定也用GL_LESS,保持语义一致
性能敏感点:随机采样模式与 LOD 选择
PCSS 性能杀手不是半影公式,而是 blocker 采样次数 × 每次采样成本。用 32 次全屏 uniform 采样,GPU cache 命中率极低;而 naive 的 Poisson disk 生成又难在 shader 里高效实现。
- 推荐用预计算的 32×1 旋转变换表(
vec2 poissonDisk[32]),C++ 端上传为 uniform buffer 或 texture buffer,shader 内用index % 32查表,避免分支和重复计算 - receiverZ 差异大的区域(如物体边缘),可对不同 MIP level 的 shadow map 采样 blocker:近处用高分辨率,远处用低分辨率 + 更大 search radius,平衡精度与速度
- 慎用
textureLod直接降级:PCSS 的 blocker 统计对深度精度敏感,MIP 0 以外的层级可能因插值丢失 blocker 边界,导致半影宽度跳变
真正难调的是 blockerZ 的稳定性——小抖动会放大成半影宽度的剧烈变化。实际项目中,往往要加帧间深度缓存(reprojection)或 temporal clamp,这部分逻辑虽在 shader,但 C++ 需提供上一帧的 view-proj 矩阵和 jitter offset。


















