PCF是通过多次深度比较取平均实现软阴影的硬件加速策略,必须用sampler2DShadow和texture函数触发深度比较逻辑;若误用texture2D读原始深度值手动比较,将失去硬件优化并引发锯齿或漏光。

PCF 是什么,为什么不能直接用 texture2D 采样阴影图
PCF(Percentage-Closer Filtering)不是滤波器类型,而是对深度值做多次比较后取平均的策略。它本质是硬件加速的“手动双线性插值 + 深度比较”组合,但现代 OpenGL/Vulkan 中必须用 sampler2DShadow 和 texture(非 texture2D)才能触发深度比较逻辑。如果错用 texture2D 读原始深度值再手比,不仅得不到 PCF 的硬件优化(如 4-tap 并行比较),还会因浮点精度和采样坐标偏移导致阴影边缘出现明显锯齿或漏光。
手写 PCF 的核心是:在 fragment shader 中,用偏移后的纹理坐标做多次 texture(sampler, coord) 调用(注意是带深度比较的版本),每次返回 0.0 或 1.0,最后求均值。
常见错误现象:shadowCoord.z > texture(shadowMap, shadowCoord.xy).r 这种写法看似合理,实则绕过了深度比较硬件路径,且 texture2D 返回的是未归一化深度值(可能超出 [0,1]),比较结果不可靠。
手写 4×4 PCF 的标准采样布局与坐标偏移计算
4×4 PCF 即在像素中心周围均匀采样 16 个点,最常用的是以当前 shadowCoord.xy 为原点,叠加 [-1.5, -0.5, 0.5, 1.5] × texelSize 的偏移。关键在于 texelSize 必须由 CPU 传入或在 shader 中通过 textureSize 计算,不能硬编码 1.0 / 1024 —— 否则缩放或不同分辨率阴影图下偏移量失效。
立即学习“C++免费学习笔记(深入)”;
实操建议:
- 在 shader 中声明
uniform vec2 uShadowMapSize,传入阴影图宽高(如 2048.0, 2048.0) - 计算
vec2 texelSize = 1.0 / uShadowMapSize,再构造偏移数组 - 使用
vec2 offset[16]静态数组或循环展开(避免动态索引导致部分驱动降级为标量执行) - 每个采样调用形如
texture(uShadowMap, vec3(coord.xy + offset[i], coord.z)),注意第三个分量是深度值,不是w
示例片段(GLSL):
float computePCF4x4(sampler2DShadow shadowMap, vec3 coord) {
vec2 texelSize = 1.0 / uShadowMapSize;
float sum = 0.0;
for (int i = -1; i <= 2; ++i) {
for (int j = -1; j <= 2; ++j) {
sum += texture(shadowMap, vec3(coord.xy + vec2(i, j) * texelSize, coord.z));
}
}
return sum / 16.0;
}为什么 PCF 偏移必须用 texelSize 而不是固定像素值
阴影图分辨率变化时,固定偏移(如 0.001)会导致:小分辨率图上偏移过大、采样区域远超邻近纹素,结果过软甚至全亮;大分辨率图上偏移过小、等效于单点采样,失去软边效果。只有基于实际纹素尺寸的偏移才能保持物理一致的“覆盖半径”。
性能与兼容性影响:
- OpenGL ES 2.0 不支持
sampler2DShadow,必须退化为手动深度比较 +texture2D,此时需自行 clamp 坐标并处理边界采样(否则采样越界返回 0 导致伪影) - Vulkan 中若用
VK_COMPARE_OP_LESS_OR_EQUAL创建 depth-stencil 状态,对应 shader 中texture调用默认比较逻辑为 ≤,需确保 shadowCoord.z 是经过 NDC 到 [0,1] 映射的线性深度,而非原始 z/w - 部分 Intel 集成显卡对非常规偏移(如非整数倍 texelSize)的 PCF 优化较差,建议坚持 ±0.5/±1.5 等经典偏移模式
手写 PCF 最容易被忽略的深度值预处理问题
阴影坐标 shadowCoord 从世界坐标经 light-view-projection 变换而来,其 z 分量默认是齐次除法后的 NDC z(范围 [-1,1] 或 [0,1],取决于 API)。但 sampler2DShadow 要求深度值在 [0,1] 区间,且比较逻辑假设输入是“待测试点的深度”,而阴影图中存的是“光源视角下最近遮挡物的深度”。如果没做正确映射,会出现整个阴影全黑或全白。
关键步骤:
- 在顶点 shader 中输出
vec4 lightSpacePos = lightViewProj * worldPos - 在 fragment shader 中归一化:
vec3 shadowCoord = lightSpacePos.xyz / lightSpacePos.w - 将 NDC z 转 [0,1]:
shadowCoord.z = shadowCoord.z * 0.5 + 0.5(OpenGL 默认 [-1,1],DirectX/Vulkan 默认 [0,1],需按实际管线调整) - 确保 shadowCoord.xy 也做了 viewport 缩放:
shadowCoord.xy = shadowCoord.xy * 0.5 + 0.5
漏掉最后一步会导致采样坐标超出 [0,1],texture 返回 0(即“无遮挡”),阴影大面积消失——这种 bug 表现为阴影只在模型中心一小块存在,边缘全亮,极难定位。


















