
PCF 柔和阴影滤波的旋转泊松圆盘采样消除摩尔纹与采样带效应在 3D 渲染管线中阴影贴图从“硬阴影”迈向“柔和阴影”的第一道分水岭是百分比渐进滤波PCFPercentage Closer Filtering。然而很多初学者在写完基础的 PCF 着色器后看着屏幕上的效果总觉得无比别扭虽然阴影边缘不再是粗糙的直角马赛克但取而代之的是一圈圈如同等高线般极其刺眼的阶梯条带效应Banding Artifacts或者在特定网格表面产生类似水波纹一样的摩尔纹Moiré Patterns。如果你使用的是最原始的规则轴对齐网格采样Regular Box Grid比如双重for循环遍历 $[-1, 1]$ 的 9 个或 16 个网格点每一个采样点在空间中都是严整对称排列的。在阴影边缘的半影区Penumbra遮挡判定值只能在 $\frac{0}{16}, \frac{1}{16}, \frac{2}{16} \dots \frac{16}{16}$ 这几个极其离散的等级之间阶跃跳变自然会在斜坡上形成一道道清晰可辨的环形条带。要彻底抹平这些刺眼的规律性瑕疵工业界的最优解是用泊松圆盘Poisson Disk打破规则对称用逐像素旋转Per-Pixel Rotation将低频条带转化为高频白噪声。蓝噪声与泊松圆盘分布的数学本质白噪声White Noise是完全随机的它会导致采样点在某些区域扎堆聚集、在另一些区域出现空洞而规则网格Grid则具有过于强烈的周期性。在采样理论中最完美的分布是蓝噪声分布Blue Noise Distribution能量集中在高频低频能量几乎为零。其在空间几何上的具象化产物就是泊松圆盘采样Poisson Disk Sampling。泊松圆盘要求任意两个采样点之间的欧氏距离必须严格大于预先设定的最小半径 $r_{\text{min}}$。这种“彼此排斥又均匀布满整个单位圆盘”的几何特性使得有限的 16 个或 32 个采样点能够以最高效率覆盖半影区的所有方向同时完全杜绝了局部扎堆。然而如果全屏所有像素都使用同一组固定朝向的泊松圆盘采样点当玩家观察一大片平坦地面时地面阴影边缘依然会隐隐约约暴露出泊松圆盘点阵自身的星芒状指纹图案。逐像素旋转消灭结构化指纹的炼金术要让阴影半影区看起来如丝般顺滑关键一步是引入**空间交织梯度噪声Interleaved Gradient NoiseIGN**对泊松圆盘进行局部随机旋转逐像素高频随机角度生成利用当前屏幕像素的物理整数坐标 $(x, y)$通过极速算术公式生成一个均匀分布在 $[0, 2\pi]$ 之间的局部旋转角 $\theta$。IGN 噪声具有极佳的蓝噪声特征相邻两个像素的旋转角度迥然不同。二维旋转矩阵变换在片元着色器中构造极简的 $2 \times 2$ 旋转矩阵 $\mathbf{R}(\theta)$将预定义的泊松圆盘采样偏移向量实时旋转。高频离散化与 TAA 抹平旋转操作把原本连续可见的低频结构化条带彻底碎裂成了极其微细的高频沙砾状噪点。现代游戏引擎后处理管线中常驻的**时域抗锯齿TAA**能够轻而易举地在时域帧间将这些高频噪点彻底平滑最终在屏幕上呈现出媲美离线光线追踪的极致柔和半影。生产级旋转泊松圆盘 PCF 着色器实现下面展示基于标准 GLSL 实现的高性能 16 点旋转泊松圆盘 CSM 滤波函数#version 450 core // 预计算的 16 点单位泊松圆盘点阵 (范围归一化至 [-1.0, 1.0]) const vec2 POISSON_DISK[16] vec2[]( vec2(-0.94201624, -0.39906216), vec2(0.94558609, -0.76890725), vec2(-0.09418410, -0.92938870), vec2(0.34495938, 0.29387760), vec2(-0.91588581, 0.45771432), vec2(-0.81544232, -0.87912464), vec2(-0.38277543, 0.27676845), vec2(0.97484398, 0.75648377), vec2(0.44323325, -0.97511554), vec2(0.53742981, -0.47373420), vec2(-0.26496911, -0.41893023), vec2(0.79197514, 0.19090188), vec2(-0.24188840, 0.99706507), vec2(-0.81409955, 0.91437590), vec2(0.19984126, 0.78641367), vec2(0.14383161, -0.14100790) ); // 极速空间交织梯度噪声 (Interleaved Gradient Noise / IGN) float GetInterleavedGradientNoise(vec2 screenPos) { vec3 magic vec3(0.06711056, 0.00583715, 52.9829189); return fract(magic.z * fract(dot(screenPos, magic.xy))); } // 旋转泊松圆盘柔和滤波核心 float SampleShadowRotatedPoisson( sampler2D shadowMap, vec3 projCoords, // 光源投影空间 [0, 1] 坐标z 为当前深度 vec2 screenPixelPos, // 屏幕像素物理整数坐标 (gl_FragCoord.xy) float filterRadiusTexels, // 滤波半影区半径 (以纹素为单位如 2.5) float bias ) { if (projCoords.z 1.0) return 1.0; // 1. 生成当前像素专属的随机旋转角度 float randomAngle GetInterleavedGradientNoise(screenPixelPos) * 6.2831853f; // 2 * PI float cosAngle cos(randomAngle); float sinAngle sin(randomAngle); // 构造二维旋转矩阵 mat2 rotMatrix mat2(cosAngle, -sinAngle, sinAngle, cosAngle); vec2 texelSize 1.0 / vec2(textureSize(shadowMap, 0)); float shadowAccum 0.0; float currentDepth projCoords.z - bias; // 2. 遍历 16 个旋转后的泊松采样点 for (int i 0; i 16; i) { // 旋转局部采样偏移 vec2 rotatedOffset rotMatrix * POISSON_DISK[i]; vec2 sampleUV projCoords.xy (rotatedOffset * filterRadiusTexels * texelSize); float mapDepth texture(shadowMap, sampleUV).r; shadowAccum (currentDepth mapDepth) ? 0.0 : 1.0; } return shadowAccum / 16.0; }工业化落地避坑实录移动端严禁过大的滤波半径如果把filterRadiusTexels设为 6.0 以上虽然阴影极其柔和但采样跨度跨越了几十个纹素会导致 GPU 纹理缓存Texture Cache命中率断崖式下跌显存控制器带宽瞬间被打满。通常推荐将基础滤波半径控制在1.5 到 3.0 纹素之间配合硬件自带的双线性比较器采样sampler2DShadow获得最佳性价比。利用硬件阴影采样器Hardware PCF白嫖二次插值如果使用标准深度纹理搭配sampler2DShadow以及textureProj显卡硬件会在单次采样内自动对周围 $2 \times 2$ 的深度比较结果执行双线性滤波。这意味着 16 次旋转泊松采样实际上相当于执行了 $16 \times 4 64$ 次虚拟比较半影平滑度成倍提升而开销几乎与普通采样持平。时域抖动抑制Temporal Super-Sampling在启用 TAA 的项目中可以在逐帧驱动时给 IGN 噪声的初始相位叠加一个与帧号相关的黄金比例偏置如frameIndex * 0.6180339887让高频噪点在时域上完美交错抵消彻底消除画面静止时的细微颗粒感。用数学打破死板规则用高频噪声消解低频结构。掌握旋转泊松圆盘的精妙才能在有限的硬件采样预算内把每一道阴影打磨得温润自然、如梦似幻。