Oculus工程师分享:常见渲染问题,及发现问题、解决问题的方法
关卡已经设计完成,asset已经准备妥当,而性能是稳定的72FPS。但当戴上头显时,一起都非常糟糕。对于Oculus软件工程师特雷弗·达什(Trevor Dasch)而言,他已经对这样的问题见惯不怪。日前,达什撰文介绍了常见的渲染问题,以及发现问题和解决问题的方法。下面是具体整理:
1. 随处可见的锯齿
加载游戏后,你的菜单看起来参差不齐,而环境充斥着仿佛轻轻一碰就会割破皮肤的齿状痕迹。到底发生了什么事情呢?
1.1 何谓折叠失真及其产生原因
这种锯齿是由于折叠失真造成:将图像光栅化至2D像素网格时出现的现象。每个像素只能有一种颜色,所以GPU会在绘制每个形状时选择像素正中心的颜色。当计算机渲染几何体时,它会将3D网格转换为一系列的2D三角形。这时,每个像素的中心要么位于其中心或要么不是,从而产生了锯齿图案。
我们如何避免这种情况呢?名为MSAA(多重采样抗锯齿)的技术可以确定每个像素的多个点是否位于多边形之内,然后根据覆盖点数的比例将一定比例的多边形颜色应用于最终渲染像素。 对于2X MSAA,系统将采样两个点,如果三角形覆盖两者,则应用100%的颜色;如果只有一个采样位于在三角形内,则50%的像素颜色来自所述三角形。对于所有采样,三角形的颜色仅确定一次。所以与单个采样相比,GPU的计算负载稍微更高。我们推荐4X MSAA,因为质量方面的增益值得付出这样的成本。
好了,你启用了MSAA,现在画面和角色看起来都十分平滑,但用户界面依然锯齿状十足。你不是说这将能够解决我的问题吗?
事实证明,MSAA不是一颗万能药。它不适用于透明对象,因为这个实现需要写入深度缓冲区,而透明度不支持。对于任何立体UI四边形,只需将它们变成不透明即可。这一举两得:你解决了混叠失真问题,同时消除了过度调用的情况。但如果背景是半透明或者角落圆滑,最简单的解决方法是在所有用户界面spires的周围添加透明像素边框。重新捣鼓美术是一份痛苦的差事,但最终效果会比MSAA清晰,因为每个边缘像素的alpha将与sprites的像素覆盖完全相关。例如,额外的一行透明像素可以产生巨大的差异,你只需在Unity TextMeshPro组件勾选“Extra Padding”复选框即可。
MSAA同时不适用于Alpha Cutout材质,因为每个像素都会发生裁剪。事实证明,有一种名为Alpha-To-Coverage的优秀工具,或者说Unity的AlphaToMask。通过设置alpha值,你可以告诉GPU要用像素覆盖的MSAA采样数量。例如,对于MSAA 4X,如果我希望进行50%的混合,我只需将alpha设置为0.5,而对于75%不透明度,alpha则为0.75。这听起来相当复杂,但实际上它使得交换alpha clip变得轻而易举,你只需令着色器输出从纹理采样中检索到的Alpha通道值。这是因为原始纹理像素是0或1 alpha,所以双线性插值的alpha结果直接对应于像素的覆盖范围。
如果你希望进一步探索这一主题,我强烈推荐你阅读技术美术本·格拉斯(Ben Golus)关于Alpha-To-Coverage的介绍文章。
2. 亮晶晶的纹理
现在已经解决了锯齿问题,但你依然会看到图像出现奇怪的闪光。发生了什么事情呢?
2.1 纹理折叠失真
与几何混叠失真类似,纹理混叠失真会在图像光栅化至2D像素网格时发生。当然,这完全可以避免。大多数纹理(Unity中的默认设置)是双线性采样。如果像素对齐,输出纹理将匹配源图像的像素布局:
如果对源图像的采样出现稍微的偏差(自由camera时经常会出现),则输出的每个像素将是源图像中四个交叠渲染目标中的结果像素的像素的混合。
这意味着,如果你采样的图像分辨率与渲染纹理的图像分辨率匹配,则每个像素将对输出图像像素具有100%的贡献(即使源纹理与目标渲染目标不完美对齐)。如果你采样的纹理分辨率低于输出图像,则源图像中的每个像素将对输出图像中的更多像素产生影响,但每个像素依然具有相同的贡献。但如果你正在采样的纹理分辨率高于渲染纹理,则系统不会以相同的程度对像素进行采样,并最终根本不对某些像素进行采样。这种不均匀的采样会导致混叠失真。
你如何利用这些信息呢?如果你的纹理处于固定距离,你可以选择完美的分辨率并以1:1像素分辨率匹配进行采样。例如,带有自定义图标的Unity Splash Screen。我已经看过太多存在混叠失真的屏幕图像,而实际上,只需确保纹理大小正确,画面看起来就会非常出色。另一方面,如果你可以接近或远离纹理(VR中99.9%的纹理是这样),你将根本无法选择一个完美的分辨率。
所以,我们可以利用全分辨率图像,然后当你远离纹理时创建一个半尺寸版本,再远离则再一半,依此类推。我们将其称为mipmaps。GPU支持自动交换它们,所以你始终可以对正确尺寸的图像进行采样。在Unity中,你只需要对每个纹理点击“Generate Mipmaps”复选框即可执操作。但动态加载的纹理呢?你可以在运行时生成mipmap,这通常是正确的选择,但有时你不能,比方说播放视频时纹理每帧都会改变。在这种情况下,有时最合理的做法是更改着色器,并针对每个片段执行多次采样,从而减少混叠失真。这需要GPU付出相当高的开销,所以你应该在真正需要的时候使用。
你可以通过片段着色器实现:使用ddx/dfdx和ddy/dfdy来选择代表像素四个象限的四个采样本点。对于Unity着色器,你可以直接插入这个函数,然后通过tex2Dmultisample将你的调用换成tex2D。
3. 模糊的一切
你已经解决了游戏中的所有混淆失真问题,而且所有线条都十分平滑,但由于某些原因,你的纹理非常模糊,除非你靠近查看。更糟糕的是,对于某些纹理,你会看到奇怪的锯齿边缘从稍微模糊变得更加模糊。另外,当它们从模糊转换为清晰时,你会看到砰的一下,一种非常突然的转变。别担心,你可以轻松解决所有问题。
3.1 砰的一下
在第一次启用mipmap时,如果GPU决定切换不同的mipmap level,你就会会看到“砰的一下”。这在VR中尤为明显。这是因为mipmap选择函数设置为“最近”,而Unity在将滤波设置为默认的“双线性”设置时会执行此操作。但系统提供了一个三线性滤波选项。由于你几乎永远不会位于正确的距离,三线滤波将为你当前的距离采样两个最接近的mipmap,一个稍高的res,一个稍低的res,然后线性混合距离。它的作用是在所有mipmap level之间提供一个平滑过渡,并且带来一个稍微更清晰的图像。这需要投入一定的性能成本,但实际上你可能不会注意到它。当将不同的mipmap用于同一个对象时,这可以平滑你有时会看到的奇怪接缝。
3.2 模糊的地板
你是否有注意到,当你看着地板时,尤其是看向远方的地板时,画面看起来会比应有水平更加模糊。但当你是站在它上面并往下看时,一切都十分正常。分辨率较低的原因是,当你以锐角看向某物时,纹理一个维度的像素采样率要高得多,而其他维度则较低,所以系统将选择两者中的较小者并用它来选择mipmap。但有一种方法可以更灵活地选择每个mipmap level,即使用各向异性滤波。各向异性滤波是一种开销稍高的技术,它可以采取多个样本并避免仅在一个方向出现混叠失真。没有必要对每个对象使用,但适合大多数能够以倾斜角度观察的对象。
下面这个GIF动图并排对比了三线性滤波器(左),以及三线性滤波器+各向异性滤波器(右):
3.3 有时候你需要帮助
设置已经妥当,但有些地方比你想要中更模糊,你依然需要非常靠近它们才能看清楚。庆幸的是,当涉及到mipmap选择时,你可以寻求工具的帮助。我们是通过指定mipmap bias来做到这一点。小于零的偏移量将令GPU为更远距离选择更高分辨率的纹理,而大于零的偏移量则相反。根据我的经验,-0.7的偏移量相当适合细节纹理,能够在更远距离保持更高的清晰度。当然,一旦超过特定点就会再次看到锯齿。你可以尝试这一方案,而如果你能够付出相应的代价,同时体验需要质量的提升,你可以结合mipmap bias与多重采样着色器。下面的gif对比了无偏移量+三线性滤波器(左),以及-0.7偏移量+三线性滤波器:
对于增加的渲染成本,为了获得更高的质量,你甚至可以结合mipmap bias和超级采样着色器。本·格拉斯对这一主题同样有撰文介绍。
4. 精简版
如果你嫌上面过于复杂冗长,下面是精简版教程:
启用4X MSAA,绝对值得
Alpha混合无用,请通过透明边框解决问题
如果你使用的是Unity的Text Mesh Pro,请在所有实例上启用Extra Padding
Alpha To Coverage看起来比Alpha Cutout更好
对所有纹理执行这一操作:启用mipmap;启用三线性滤波
对环境纹理执行这一操作:启用各向异性滤波
对于高细节纹理:将mipmap bias设置为-0.7
为Unity Splah Screen选择正确的分辨率