3D渲染中远处物体Z-Fighting(闪烁)问题

测试在测编辑器的时候,干了个很骚的事情,把far plane的值调得非常非常大(near plane没有改变),然后将编辑器摄像头拉近拉远,发现当编辑器摄像头拉的很远的时候,视口中显示的物体就开始闪烁起来了,于是拉我们开发来对问题……于是某位开发的一天就这样口干舌燥地过去了……

我们先明确这个问题是Z-Fighting问题。

Z-Fighting就是深度冲突,当两个待渲染物体的三角形网格比较接近,几乎平行排列在一起时会发生深度冲突,其本质是深度缓冲没有足够的精度来决定哪个三角形在前面显示,于是这两个三角形会不断地在切换前后顺序导致闪烁。

深度冲突不是只在远处的物体间才有,近处的物体们如果平行摆放,三角形平行重合的地方也会产生深度冲突的现象。只是远处物体由于接近远平面,而我们在做归一化深度的时候由于归一化函数是非线性的,在远平面处曲线几乎水平,导致归一化后的深度值非常接近,此时如果用来存深度的深度缓冲的浮点精度不够,两个有前后关系的物体就会被截断成同一个深度值。因此远处物体比近处物体更容易产生深度冲突的现象。尤其是当我们的测试把far plane的值调得非常大,near plane和far plane间距很大的时候(此时在远平面附近的曲线更水平,斜率更小了)。另外我们有时候在游戏中会发现,远处高山上接近消失点地方的一棵树和一栋房子在我们轻微移动人物的视角的时候有可能会在闪,这大概率也是深度冲突的问题。

我们在将物体渲染到屏幕上时,必不可少地需要经过下面的坐标系变化,以将3D场景中的物体画到2D的显示窗口中:

物体的局部坐标系 -> 物体的世界坐标系 -> 物体在摄像机视角下的相机坐标系 -> 摄像机底片的投影坐标系 -> 规格化设备坐标(NDC)(归一化的深度值)

这些坐标变换在顶点着色器阶段(VS)中完成,“物体在摄像机视角下的相机坐标系 -> 摄像机底片的投影坐标系 -> 规格化设备坐标(NDC)(归一化的深度值)”这一步就是MVP矩阵变换中的P矩阵做的事情。

我们不在此处去推MVP矩阵了,网上随便搜一下都有。直接给出P矩阵的公式(其中:近平面n,远平面f,垂直视场角v,透视纵横比r):

pic_farplane_zfighting_1

任何一个物体的一个顶点经过“物体的局部坐标系 ->M-> 物体的世界坐标系 ->V-> 物体在摄像机视角下的相机坐标系”变换后,都会得到该顶点在相机坐标系下的坐标,此时的该坐标中的z是该顶点在相机视角下的深度,该深度不是归一化的,从前面的P矩阵中我们可以得知z的归一化函数g(z)(事实上,这个g(z)是在推导P矩阵的过程中获得的,这里我们只是反过来将它拿出来):

pic_farplane_zfighting_2

我们重点来关注这个用来归一化深度的非线性函数g(z)。来点“科学”的分析工具,我写了一段matlab代码来画出near plane、far plane在不同值时的g(z)曲线。

% gen_gz.m

n = [  1,  1,   1 ];
f = [ 10, 20, 100 ];

z = 0:0.01:max(f);

% size(..) returns [col, row]
size_n = size(n); size_f = size(f);
group = min([size_n(2), size_f(2)]);

info = cell(1, group);
for index = 1:group
    crnt_n = n(index);
    crnt_f = f(index);
    temp_a = crnt_f / (crnt_f - crnt_n);
    temp_b = - (crnt_n * crnt_f) ./ ((crnt_f - crnt_n) * z);
    g = temp_a + temp_b;
    % draw curves in a figure
    info{index} = ['g(z)', ',n=', num2str(crnt_n), ',f=', num2str(crnt_f)];
    plot(z, g, 'LineWidth', 1.2);
    hold on
end
axis([0 max(f) 0 1]);
legend(info);
grid on

clear crnt_n crnt_f temp_a temp_b index g

运行一下,得到下图:

pic_farplane_zfighting_3

根据图片我们可以看到,当near plane不变,far plane变大时,靠近远平面的大部分的深度值都被集中映射到了归一化区间中的一段很小的区域内(当n=1,f=100时,在50到100的相机深度值被映射到了0.98到1.0的范围内,而0到50的相机深度值占据了0.0到0.98范围)。

当far plane继续变大时,我们再来看看:

pic_farplane_zfighting_4

阿西吧,都成一个直角了!

来点狠的,把far plane搞成999999999(别数了,9个9),我们调整一下显示的坐标轴限制在0.99999998到1之间:

pic_farplane_zfighting_5

为什么选择0.99999998到1之间呢,因为大多数情况下我们会给深度缓冲的格式为FORMAT_D24_UNORM_S8_UINT,深度值用24bits的浮点数来存储,有效位数只达到8位。即0.99999998和0.99999999是两个深度值。而0.999999981、0.999999982、0.999999985…这些个在0.99999998和0.99999999间的值,由于精度不够,都会变成同一个深度值。

pic_farplane_zfighting_6

反映在图中,就是所有在A深度范围内的物体归一化深度值都是同一个(0.99999998),而所有在B深度范围内的物体,归一化的深度值也是同一个(0.99999999)。

于是,当测试把far plane的值调得非常大的时候,再把摄像头拉远,本来有前后关系的物体们由于归一化过程中丢精度了,就被计算机当成平行的物体了,两个物体的三角形网格一重合,这一块渲染出来的画面就开始闪了。

深度冲突问题目前没有很好的解决方法,基本是根据具体的业务情况选择相应的规避措施,规避的方法主要有:

使用更高精度的深度缓冲,比如采用FORMAT_D32_FLOAT;

根据具体的业务场景选择合适的near plane和far plane的值,避免相差过大;

根据具体的业务场景计算出物体距离摄像机的距离,离摄像机较远的物体避免深度上相距太近。

终于阐述清楚了。所以,这个问题别再提问题单了啊…w(゚Д゚)w


本文为原创内容,遵循CC BY-ND 4.0协议,署名-禁止演绎。
转载请注明出处:https://tis.ac.cn/blog/kongdeyou/3d_rendering_farplane_zfighting/

发表评论

您的电子邮箱地址不会被公开。