UE4 Z-Fighting 问题总结

5,102 阅读6分钟

背景

在项目中使用 Procedural Mesh Component 在 UE4 中进行面渲染时,画面上出现了严重的闪烁问题。

经过查阅资料,我发现闪烁问题是由 Z-Fighting 导致的。Z-Fighting 是指当多个不透明的面在世界空间中处于共面时,它们的片元可能在深度缓冲区的值相同,在渲染像素时就会随机绘制,最终导致在每帧之间出现闪烁的现象。本文既介绍了一般情况下解决 Z-Fighting 问题的方法,也对我在项目中解决该问题的方案做了一个总结。

通用方案

深度测试方案

解决 Z-Fighting 问题最简单可行的方法是禁用深度测试。引擎会将每一个片元的深度值与深度缓冲的值进行测试,并抛弃深度测试失败的片元。当我们关闭深度测试时,所有的片元都会保留,最终像素将显示最后绘制的片元的颜色。我们可以将要绘制的面按照优先级进行排序,然后按照顺序进行渲染即可。在 UE4 中禁用深度测试需要对源码进行修改,然而我们的项目最终要打包成插件发布,无法修改引擎,所以这个方案在我们的项目中是不可行的。

提升深度精度方案

在一些情况下,Z-Fighting 是由于深度精度不足导致两个片元的深度值相同,此时可以通过提高深度的精度的方案来进行优化。一个方法是增加近平面的距离,由于精度在距离近平面较近时的精度更高,将近平面设置的较远可以提高整个场景的深度精度。然而,当近平面过远时,近处的物体可能会被裁切掉,实际项目中应该考虑摄像机的位置动态调节近平面。另一个方法是直接使用更高精度的深度缓冲,当然这也会带来性能上的损耗。

在我们的项目中,多个面在 Z 轴上的世界坐标是完全相同的,这导致它们的深度值也会完全相同,所以提升深度精度的方案无法解决问题。

世界坐标偏移方案

Z-Fighting 是因为多个面在世界空间中共面产生的,所以我们可以将这些面的世界坐标进行偏移,将其分开即可。在一些比较简单的 Z-Fighting 情况中,这种方案是很有效的。具体到我们的项目中,我们可以对每个面在 Z 轴上按照渲染优先级顺序添加一个偏移,最终即可解决 Z-Fighting 的现象。

这个方案主要有两个问题,首先,改变面的世界坐标会对其碰撞产生影响,后续添加其他效果时的碰撞计算也会受到影响。

第二个问题,当摄像机的俯仰角比较大时,每个面的位置会有较大的偏差,上面的面可能会遮挡住下面的面,如下图所示。

顶点坐标偏移方案

UE4 在材质中提供了 World Position Offset 通道来对顶点坐标进行偏移,原理与上一个方案是基本相同的,只是偏移转移到 GPU 上面完成。顶点坐标偏移的方案解决了影响碰撞的问题,但是俯仰角过大导致的问题仍然存在。最终的渲染效果如下所示,可以看到,面的相对位置有明显的偏差。

像素深度偏移方案

UE4 在不久前的版本中新增了 Pixel Depth Offset 材质通道来修改像素的深度。官方文档的说法是,将像素沿着摄像机向量的方向进行偏移。实际上,可以认为是直接修改了每个片元的深度值,将片元原本距离摄像机的距离加上一个偏移,再转换为该片元的深度值。由于这个过程不会影响到光栅化,每个片元的相对位置是不变的,所以不会有面遮挡与位置偏差的问题。使用此方案的渲染效果如下。

改进版像素深度偏移方案

像素深度偏移的问题

通过进行像素深度偏移,项目中大部分的 Z-Fighting 问题解决了,然而在一些特殊情况下,该问题还会再次发生:

  • 我们对每个面按照优先级进行等距偏移,当摄像机的俯仰角较大时,面上的点偏移之后可能会与原平面的距离较近,由此导致实际的片元深度值可能会与其他面的片元相同。如下图所示,点 P1 P2 经过等距离偏移之后为 P1` P2` 而 P2` 与原平面的距离更近,当摄像机向量水平时,面上的点经过偏移之后仍然会在原平面上,就可能会与其他面上的点发生深度冲突。

  • 由于离远平面较近时,深度精度越来越差。(具体原因见参考文章)当摄像机距离面过远时,偏移值如果较小,会导致片元的深度值精度不够,同样可能导致 Z-Fighting 现象。

像素深度偏移改进

  • 为了解决大俯仰角导致的问题,我们需要对面上的每个点实时计算摄像机向量带来的偏差。如下图所示,摄像机向量垂直于平面时,O 点经过偏移 offset1 后为 O1 点,而 P 点如果沿摄像机向量偏移 offset1 后,P1 点与 O1 点不会在同一个平面。所以 P 点应该偏移 offset2 = offset / Dot(CameraVector, PixelNormal) 才能与 O1 处于同一偏移面。

  • 为了解决摄像机距离导致的深度精度误差问题,我们需要对面上的每个点实时计算摄像机距离带来的偏差。如下图所示,摄像机距离点越远时,点的偏移距离也应该越大。指定一个经验值 K,点的偏移距离应该为 offset * Distance(Camera, Pixel) / K

遗留问题

通过改进版本的像素深度偏移方案,我们解决了项目中遇到的 Z-Fighting 问题。但是由于我们只是对深度缓冲进行了更改,阴影的计算依然存在问题,这个留给后续再进行解决。

参考文章:

深度测试