1. 反走样
在计算机图形学中,在屏幕上显示对象时,可能会出现许多的“锯齿”,这些锯齿是由顶点数据像素化之后成为片段的方式所引起的,由于将数学意义上的坐标转换到物理的显示器硬件上进行显示,显示器是有一个个像素点构成的,并不能实现数学意义上的“无限小”的描述。关于产生锯齿的更详细的介绍可以参考OpenGL学习脚印: 反走样初步(Anti-aliasing basic)
为了消除“锯齿”,图形工作者提出了许多抗锯齿的算法(也称为反走样[Anti-aliasing]算法),本文主要介绍OpenGL中提到的一种反走样方法——多重采样(Multisample antialiasing简称 MSAA)。接触到这一主题时,查阅了很多资料,发现网络上许多内容都大同小异,很多内容并没有参考价值。整体的思路说清楚了,但是一旦涉及到某一项技术,最难的往往是技术中的细节部分。本文记录我个人在理解MSAA时候比较困惑的点,在理解之后记录下来。
2. Supersampling
在理解MSAA之前,有必要先理解一下什么是SuperSampling (字面翻译是超采样,简称是SSAA),超采样就是加大采样的点,提供比屏幕分辨率更多的采样点。是渲染的时候按照显示器分辨率的若干倍来渲染,例如显示器1024x768,那么SSAA 4X就是4096x3072。
上图是OpenGL决定一个像素颜色采用的算法,如果红色的采样点(像素中心位置)落在三角形区域,那么这个像素就被认为属于三角形,从而会着色成为三角形的颜色。但是边缘部分的像素并没有“完全属于”三角形,那么这些像素的颜色应该是某种介于边缘多种颜色的“过渡色”比较合理一点。
SSAA的想法比较简单,就是扩大采样点,生成一些次像素级别的采样点(sub-pixel)。有点类似股份公司的感觉,把像素理解成为一家公司,那么这家公司归属谁呢?在中心区域,由于完全被三角形区域所包含,那么属于三角形没有什么争议,但是边缘部分的像素由于并没有完全被三角形占据,那么它的所有权问题就应该是多个股东决议的结果,根据大家股份占用的大小来中和最后的结果。
在像素中如何添加新的次像素级别的采样点,方法很多。不同的采样点设计方式最终对场景渲染的结果也不同。以下是一些采样点的选择方式:
得到次级像素的采样点之后,把这个场景渲染到一个更高分辨率的缓冲区A中,然后再从A缓冲区中采样出和屏幕分辨率一样的像素,像素的颜色使用A缓冲区中的像素颜色插值得到。整体的思路如下图所示:
上图中使用4个采样点,如果不使用SSAA技术,那么最终屏幕上这个像素的颜色是中心位置的黄色,使用了SSAA之后,这个像素的颜色是淡淡的棕黄色,这个颜色综合了像素中其他的颜色值计算而来的。(图示中使用的是简单的求平均值的方法,在实际中可能会有其他的算法来计算,但是整体思路还是一样的)
在SSAA方法中,有一个需要注意的地方是:所有的次级采样点和未使用SSAA中采样点的地位是一样的,假设有片元shader需要在每一个像素上运行,那么SSAA中片元shader也会在每一个次级像素上运行。理解这一点非常的关键,它是SSAA和MSAA最重要的一个区别。
SSAA的缺点:通过上面的分析可以知道SSAA需要占用更大的显存空间,它需要更大缓冲区,采用4x、8x、16x那么使用的空间是未开启SSAA的4倍,8倍和16倍。另外SSAA需要每一个shader在所有次级片元上都同样的运行一遍,这也是一笔可观的计算开销,会显著地降低FPS。
3. MSAA
有了SSAA的基础,那么理解多重采样(MSAA)就简单很多了。MSAA中增加采样的方式和SSAA是一样的,MSAA同样也需要一个分辨率更高的缓冲区(假设命名是Anti-Buffer),但是MSAA并不是像SSAA一样把次级采样点当成类似未开启反走样中采样点同等地位来对待,它采用另一种方式。
示意图如下:
图示中两个三角形的图元都包含了像素的采样点(圆形位置),4个叉代表着该像素的4个子采样点。在开启MSAA之后的过程如下:
首先由于屏幕背景清空色是白色,那么Anti-Buffer中的每一个子采样点的颜色都设置成了白色,
假设蓝色的三角形首先绘制,由于它包含像素的采样点,因此在跑了一次shader之后,左下角的这个2个子采样点在Anti-Buffer缓冲区的值被蓝色填充,然后开始绘制黄色三角形,由于它也包含像素的采样点,因此也会跑一次shader,假设计算的结果是黄色。由于右边的采样点被它包含,因此被设置成黄色,最终这个像素对应在Anti-Buffer缓冲区中4个子采样点的颜色是白色、黄色、蓝色、蓝色。最终的颜色由这4个颜色计算平均得到。
也就是说:MSAA每一个像素上shader只运行一次,运行的结果会复制到每一个包含了子采样点的缓冲区中。而不是像SSAA那样每一个子采样点都运行一次shader。大大减少了shader的运行次数。
4. MSAA在OpenGL中的使用
网络上许多关于MSAA在OpenGL中的使用方式都没有说的很清楚,这里就介绍一下OpenGL中的MSAA。
OpenGL中有很多状态变量,默认情况下基本都是关闭的(比如光照计算、混合计算、深度测试等等),但是只有两个状态默认是开启的,MSAA就是其中之一(另一个是Dither),要使用OpenGL提供的MSAA,需要申请一个可以进行超采样的缓冲区(也就是我们上文说的Anti-Buffer),申请的方式和窗口系统有关,在GLUT中申请的方式很简单,只需要添加一个枚举值的变量GLUT_MULTISAMPLE
即可:
glutInitDisplayMode(GLUT_DOUBLE | GLUT_DEPTH |GLUT_RGBA | GLUT_MULTISAMPLE);
添加缓冲区之后,开启多重采样即可(默认是开启的可以不用显式设置),开启方式:glEnable(GL_MULTISAMPLE);
使用方式就是这么简单,另外在OpenGL中还有一些多重采样控制的参数需要设置,相关的API如下:
- 开启多重采样覆盖率的设置
在MSAA介绍部分,我们列举的例子中,说到了最后的颜色由白色、黄色、蓝色、蓝色。这4个颜色计算平均得到。这里的描述是不完全准确,OpenGL默认情况下是会直接取4个颜色的平均值,但是也可以添加更多的控制。
默认计算算法((color1+color2+color3+...+colorn) / n)
,需要注意这时候color的alpha值是不参与计算的。
如果进行下面的设置,那么计算算法会改变,
4.1. glEnable(GL_SAMPLE_ALPHA_TO_COVERAGE);
设置之后alpha值参与计算
由于多重采样的实现方式很多,OpenGL也没有规定硬件应该怎么实现多重采样,都是交给硬件自身处理。因此这里我们描述一种可能的多重采样的处理过程:
当我们使用4x,8x等MSAA时,OpenGL会为每一个像素分配一个掩码值,这个掩码值的位数是由子采样的点数决定的,也就是说4x是分配4位,8x是分配8位。分配完成之后,这些位记录了一个子采样点是否被其他几何图元覆盖到了,以3中的情况为例,由于那4个采样点有3个被覆盖到了,因此那个像素中的Coverage掩码是 0 1 1 1
A B C D ————> 4个采样掩码位
0 1 1 1 ————–>覆盖掩码值
白色 蓝色 蓝色 黄色 ——>MSAA采样缓冲区值
然后再计算平均值的时候考虑到某一个掩码是0,那么该子采样点的颜色就不参与最后的运算了。
上面说的是正常的处理过程,一旦我们设置了glEnable(GL_SAMPLE_ALPHA_TO_COVERAGE),那么这个掩码就要开始运算了:设置之后,OpenGL会根据MSAA缓冲区中颜色值的Alpha成分,生成一个掩码值,并与这个原来的掩码进行按位与的运算。(alpha值是一个浮点数的值,怎么进行按位与的运算呢?原来OpenGL可以通过映射,将某一范围内的值映射成一个整数),生成一个新的掩码值。具体来说是:
一个fragment的Alpha值在0~1间,它对应着一个值。还是以4XMSAA为例,这个值也是xxxx的形式,Alpha为0对应了0000,alpha为1对应了1111,至于中间的值的对应关系,OpenGL是交由显卡制造商决定的——其实一般就是类似[0~0.249 -> 0000, 0.25~0.499 -> 0001, 0.5~0.749 -> 0011, 0.75~0.99-> 0111]这样(在D3D11中,就可以自定义这个值)。这个值与与coverage mask作一次逻辑按位与操作获得新的coverage mask。
4.2 glEnable(GL_SAMPLE_ALPHA_TO_ONE);
设置多重采样中每一个子采样点的颜色alpha成分是1。
4.3 . glSampleCoverage
开启自定义设置,需要2个函数调用
glEnable(GL_SAMPLE_COVERAGE)
glSampleCoverage
这里面glSampleCoverage的函数原型如下:
void glSampleCoverage(
GLclampf value,
GLboolean invert);
第一个参数是 value,表示掩码值
第二个参数是 invert,表示是否翻转计算
第一次看到这个接口API时,发现是一个float的value值,OpenGL的spec文档中一直提到要做按位的与运算,我就一脸懵逼了,按位运算不是整型才有的运算操作吗?是不是说这个value会被cast成一个整型,既然要cast成一个整型为什么不设置参数类型就是整型呢?
事实上是这样的:这个浮点数的值会被映射成一个整型数,和之前alpha_to_coverage类似
glSampleCoverage的效果和 glEnable(GL_SAMPLE_ALPHA_TO_COVERAGE);有点类似,都是设置掩码和原来像素的coverage掩码作按位与的运算,不同之处是后者使用颜色成分的alpha值运算。而这个函数是自己指定一个值,这个值是value映射后的整型值。第二个参数invert是对这个value映射后的整型值作按位取反操作。然后再与像素的coverage掩码作运算。
以上是所有OpenGL中和MSAA反走样相关的内容。反走样是一个十分庞大的主题,读者也可以通过FBO自己实现反走样的算法,关于在FBO中作反走样,可以阅读参考资料中的内容。
5. 参考资料
1. OpenGL学习脚印: 反走样初步(Anti-aliasing basic)
2. 抗锯齿
3. Supersampling
4. Multisample Anti-Aliasing
5. Rasterization Rules
6. 乱弹纪录II:Alpha To Coverage