这是典型的 Shadow Acne,本质是深度采样时片元深度与阴影贴图纹素深度误判;因贴图分辨率有限,一个纹素覆盖区域过大,表面近似平行光方向时误差放大,导致本该受照点被判入阴影。

为什么加 depthBias 后表面出现密密麻麻的黑点?
这是典型的 Shadow Acne,本质是深度采样时,片元在光源空间的深度值和阴影贴图中对应纹素(Texel)存储的深度值发生误判。因为阴影贴图分辨率有限,一个纹素覆盖世界空间中一块有面积的区域,而片元计算的是单点精确深度——当表面近乎平行于光源视线时,这个误差会被放大,导致本该被照亮的点被判定为在阴影中。
常见错误是直接硬塞一个常量偏移,比如 shadowDepth += 0.001。这在平地可能有效,但斜面、曲面、远距离物体上立刻失效:偏移太小压不住粉刺,太大又引发 Peter Panning。
- 不要用固定浮点数做全局
depthBias,它不随几何朝向变化 - 避免在片元着色器里对
shadowSpace.z做简单加法修正(如+ 0.0001),这破坏了深度比较的几何一致性 - 确保深度纹理格式是
VK_FORMAT_D32_SFLOAT或等效高精度格式,D16_UNORM在中远距离极易加剧粉刺
怎么让阴影紧贴地面又不飘起来?
Peter Panning 的根因是偏移量过大,把本该落在几何体表面的阴影判定点“推”到了表面之上。真正可靠的解法不是调 bias 数值,而是改采样逻辑:用法向偏移(Normal Bias)替代深度偏移。
核心思想是:把采样点沿表面法线方向往外挪一点,而不是沿光方向统一推深度。这样既避开自遮挡误判,又不破坏接触边界的几何咬合。
立即学习“C++免费学习笔记(深入)”;
- 在顶点着色器中传入世界空间法线
worldNormal,并在片元着色器中归一化 - 计算偏移量:
float normalBias = max(0.0, dot(worldNormal, lightDir)) * biasScale;(lightDir是归一化的光源方向) - 最终采样深度用:
shadowSpace.z - normalBias * worldDepthScale,其中worldDepthScale是将法向位移映射到光源空间深度单位的系数,通常取光源正交投影的zRange倒数 - 务必开启正面剔除(
frontFaceCull)渲染阴影贴图——否则背面三角形会写入深度,直接导致悬浮
SAMPLE_TEXTURE2D_SHADOW 采样时为什么还漏光或闪烁?
即使用了法向偏移,SAMPLE_TEXTURE2D_SHADOW 本身只做一次深度比较,对锯齿和采样抖动毫无抵抗能力。它输出的是 0/1 硬阴影,在边缘必然产生高频噪点,尤其在动态视角下表现为闪烁。
这不是 bug,是设计使然。要稳定,必须叠加滤波层。
- 优先用 5×5 PCF(Percentage-Closer Filtering)代替单点采样:循环遍历邻近 25 个纹素,逐个比较后取平均值
- PCF 的采样坐标需做随机抖动或泊松分布偏移,避免规律性摩尔纹
- 若性能敏感,可用 3×3 PCF + 法向偏移组合,已能覆盖绝大多数中近距离场景
- 禁用各向异性过滤(Anisotropic Filtering)用于阴影贴图——它会让斜面采样模糊,反而加重粉刺感
薄几何体(如墙、地板)阴影异常怎么办?
当模型厚度小于你设定的偏移量时,阴影判定点会落到几何体内部甚至背面,造成漏光或边界错位。这不是参数没调好,是建模约束没满足。
引擎不会替你“猜”哪里该厚、哪里可以薄;它只按你给的顶点和法线算。
- 地面、墙面等承影面厚度至少保持在 0.01 单位以上(按你的世界单位尺度调整)
- 避免用单面平面(flat plane)做地面;改用带厚度的 box 或 extruded mesh
- 若必须用薄几何,可在阴影 Pass 中临时加厚:顶点着色器里沿法线方向外推顶点,仅用于深度图生成,不影响主渲染
- 检查是否启用了
depthClamp或depthBoundsTest,它们在近裁剪面附近可能截断本该参与阴影计算的深度值
法向偏移不是“多加一行代码就完事”的技巧,它要求你同步控制建模厚度、光照方向、深度纹理精度和采样方式。最容易被忽略的一点是:所有偏移量都必须在光源空间中保持单位一致——世界法线、光源方向、投影矩阵的 z 范围,三者换算错一个,整套就退化回常量 bias。


















