游戏纹理抖动的成因分析和解决方案
游戏纹理抖动的成因分析和解决方案
最近试用了下Unity,在开发的过程中发现了一个问题,在相机随主角移动的过程中,发现地图的细节部分不停的闪烁,非常讨厌,于是对这一问题进行研究。遂成此文。
一、问题
视频很好的展示了这个问题。我们用草地图片做背景,图片滤波模式设置为point,然后让摄像头沿着X轴缓慢移动,可以看到在移动过程中,图片的某些部分抖动得很厉害,这种程度的抖动对于一般游戏来说都是不可以接受的,因此网上也有些解决方案,但大部分人对抖动的成因理解不够,这篇文章将先分析抖动的成因,然后简单介绍下抖动的解决方案。
二、成因
我总是喜欢把结论写在前面,这样没有兴趣的同学就可以直接略过后面的大部分分析:抖动的成因:
1. 屏幕实际上是一个采样器,它对图像采样
2. 而根据奈奎施特采样定理,采样频率必须至少是图像频率的两倍,采样后的图像才能完整的代表原图像。
从这里我们可以知道,屏幕分辨率越低、图片高频信号越高,越容易产生抖动现象,比如把上图的图像代替成一个单色图像,显然是不会发生抖动的。
OK,下面开始具体的分析了,我们把图像经过屏幕显示的过程数学建模,图像经过屏幕显示呈现到人们面前,实际上是分成了两个步骤:
1. 采样:图像处理器从图像中选取一个或多个点平均,得到一个RGBA值,采样在shader中用tex2D函数实现,OpenGL有几种采样方式GL_NEAREST, GL_LINEAR和GL_LINEAR_MIPMAP_LINEAR,分别对应Unity的point, bilinear和trilinear三种采样模式。
2. 滤波:采样后的RGBA值通过像素发光,这是一个低通滤波器,把一个没有大小的RGBA值显示为一个像素大小的光点,可能大家对这步体验不够清楚,然而这确实有一个低通滤波的过程,不然人们只能在屏幕上看到一个个Dirac函数阵列。
为了叙述方便,我们把讨论局限于一维空间。我们将讨论一个长度为1、高度(RGBA值)为1的线段的在一维屏幕上的显示过程,以及线段移动时,频域的变化。下图展示了小方块经过采样和低通滤波后在屏幕成像的空域和频域流程图:
图(1)
图中,宽度为1,坐落在坐标中心的小方块经过宽度为0.1(采样频率为10Hz)的采样函数采样后,再经过一个低通滤波函数,最终呈现在屏幕上。从上图中可以看到,在空域,小方块最后的样子和最初的样子宽度变化了些,在频域,小方块的高频分量被削弱了。
从上图,我们也很容易理解奈奎施特采样定理,被采样后的小方块在频域是多个波峰叠加,波峰之间的距离就是一个采样频率,如果要求两个波峰之间的频谱不叠加,那么其最高频率的2倍必须不大于采样频率。
下面我们让小方块沿着x轴移动,看看采样后的频谱是个什么样子的。假设小方块的空域函数是p(x),经过傅立叶变换,得到其频域函数:
经过采样频率为10Hz的采样函数采样:
显然,如果要让小方块在移动前和移动后,频域大小保持不变,那么可以令,从而:
频域大小为:
也就是说,只要小方块以整数倍采样周期运动,那么小方块采样频域将保持不变,这也就是我们本文一开始的结论,扯了这么多,无非是想说这个结论是有数学根据的。
三、在Unity中的应用
其实大家最关心的可能就是这个了:如何“消抖”,一个简单的方法是降低图像的频率(注意是频率不是分辨率),这个可以从两个方面想办法:
1.在图像资源的制作过程中使用各种滤波函数来模糊图像;
2.在导入资源时设置滤波模式为bilinear或者trilinear。
然而这两种方式有时并不能完全解决抖动现象,尤其是在图像缩小时,这时频域将扩大,而无视滤波。
我们还得想其他办法,根据第二节的结论,只要小方块以整数倍采样周期运动,其频域大小就不变,那么只要图像以整数倍像素大小移动,就不会发生抖动。目前的一般做法有两种:
1. Unity的sprites/Default 这个shader带有一个Pixel snap选项,选上该选项,sprite在渲染时,其顶点位置将snap到整数倍像素位置,从而保证整个sprite能以整数倍像素大小移动。
图(2)
2. 在脚本中把位移snap到整数倍像素大小。这种方式在摄像头移动时很方便,只要给摄像头加一个脚本,就可以圆满的解决这个问题。附件里有这个脚本,也很简单。
四、总结
其实对于些屏幕分辨率比较高,又不需要图像缩小,用bilinear滤波模式也就差不多了,这大概也是这个问题没有多少人提及的原因吧。
抖动问题不限于屏幕采样,在游戏对象通过一系列变换过程中,如果其运算的截断误差(一个采样过程)经过放大,能够在屏幕上显示出来,就会产生问题。