D3D11中的MSAA
这两年我的工作都转到了D3D11,目前新出硬件几乎全部支持此标准,加上D3D11接口清晰,概念直观,等到windows7普及,想必未来都是D3D11的天下。最近时间较空,我陆续开始写些基础文章,希望对新学者有所帮助。但文章纯属我自己随意写写,错误肯定很多,请大家多多包涵。
所谓MSAA,就是让一个像素可以同时存储多个颜色,而最终的显示结果由多个颜色重建而成。具体存储颜色的数量由DXGI_SAMPLE_DESC中的Count来决定,其中的Quality则一般用来给硬件设计厂商作为非常规发挥的余地,比如NVDIA CSAA开启方式就是用Quality某些值来实现的。在D3D9中MSAA中即使一个像素被分成多个子片段来光栅化,但实际上覆盖此像素的每个三角形依然只执行一次pixel shader,子片段的位置只用来决定各种顶点属性的插值位置,以及进行覆盖率评定,这就是MSAA相比SSAA的最大不同之处,MSAA只会增加被多个三角形同时非完全覆盖时的计算率,而且不管其覆盖率有多高,每个三角形都只执行一次Pixel shader,并将Pixel shader返回的值存入相关覆盖子像素。需要注意的是,这里存在两个细节问题,一是Pixel shader输入的值如何插值而来(插值位置和插值算法);二是子像素到底位于像素中的何处,个数如何决定,是否每个子像素都对应一个color/z/stencil存储,如何将所有这些存储的子合成为最后的结果。对于第一个问题,就牵涉到MSAA光栅化规则的问题,注意如果没有任何特殊设置,对应像素Fragment的属性插值操作都将在像素中心位置上执行,MSAA中进行覆盖率评定时,很有可能三角形并未覆盖到像素中心位置,这就牵扯到一个外部插值(extrapolate)的问题,即插值位置根本就不在三角形内,很显然这样插值出来的属性结果是错误的,为了解决这个问题,D3D引入一个配置“centroid sample”,指定在rasterize时,相关属性进行插值的位置必须位于三角形与像素相交区域内,这个通常这个位置取在某个被覆盖的子像素位置,但并不保证永远不变,可能和具体硬件设计还有关,D3D11 reference rasterizer选择centroid sample位置的具体算法,可参考D3D文档。在D3D11中想要打开centroid sample,只需在对应pixel shader input attribute上加上centroid modifier即可;属性的插值算法,就是如何用三个顶点attribute值,以及中间点A的位置,使用某种算法插值出attribute在中心点A上的值,D3D中最常用的就是带透视矫正的线性插值(linear),所有attribute默认都使用此算法插值(linear modifier),当然D3D11还提供其他几种插值方式:nointerpolation(就是不插值,使用三角形中第一个顶点的属性值作为Fragment属性值),noperspective(不带透视矫正的线性插值,只使用屏幕2D坐标位置进行插值计算),sample(在每个子像素位置进行插值)。这些modifier可以加在PS input attribute前面,不过使用起来还是有些限制和规则,比如centroid、sample明显只能在MSAA模式下才能起作用,因为普通模式下不存在非中心覆盖和子像素位置问题;而centroid很显然也不能同nointerpolation一起使用,更多信息还请参考DX文档,毕竟知道这些背后原理后,更好记忆和理解这些限制。现在讨论第二个问题,子像素分布在像素区域中的位置是因硬件设计而变的,D3D标准并没有规定具体分布的位置,而个数按道理上来讲就是DXGI_SAMPLE_DESC种的count所变量指定。是否每个子像素都会在RT surface上有相应的存储位置(color/z/stencil),这个就有点悬了,毕竟这个是要增加硬件成本的事,而且D3D标准也没强制,硬件厂商说:OK,我可以给你指定的覆盖点数,我也可以把这些点的位置进行精心设计分布,但我不一定会给每个点都分配实际的存储位置。比如CSAA就将子像素数和实际存储数分开来了,以此来节省存储和带宽,CSAA和16x实际上只有4个存储位置(但它确实有16个子像素),16个子像素(覆盖率判断)如何分享4个存储位置呢?答案是硬件设计有关。最后一个问题,每个像素中存储的多个值如何重建为最终结果?答案还是硬件设计有关,但我们可以自己resolve(http://mynameismjp.wordpress.com/2012/10/28/msaa-resolve-filters/)。
在D3D11中是可以指定pixel shader进行per-sample excution的,这个和D3D9完全不同,在pixel shader input中指定SV_SampleIndex属性或为属性指定sample modifier都会打开pixel shader逐子像素执行(这个在CSAA中就有点问题了,因为CSAA并不为每个子像素分配独立的存储)。MSAA并不由Pipeline中的一个stage完成,而牵涉到rasterization、pixel shader、output merger三个stage,D3D11对MSAA的操作进行了空前的增强,可以获取sample index, coverage mask, sub pixel value, 以及pixel shader新支持的UAV,综合这些我们可以完成一些很特别的算法。需要注意的是用centroid sample或per sample execution后会带来一个问题,就是GPU的某些地方的导数计算可能有误,比如ddx ddy以及texture lod计算,因为三角形边缘像素的采样位置会被偏置到某个sample的位置,而不再是像素中心,这样2x2像素中,变量相差之后的值就不再是基于单位的屏幕空间坐标了,这样在三角形边缘的像素上计算变量的导数就会出现跳跃起伏,这样会使ddx ddy的结果产生异常,所以要么你能容忍或解决这个问题,要么就不要在centroid sample的属性上进行导数计算。
pixel shader输出Z会给MSAA带来一些麻烦。如果pixel shader没有开启per sample exctution,但却输出了SV_Depth,这就产生一个问题,本来每个子像素在depth stencil buffer中都会输出各自独立的Z值,此Z值为光栅化时插值产生,因此每个子像素都有一个正确的Z值,但如果pixel shader人工输出了Z,而这个pixel shader只执行一次,这样被此三角形覆盖的所有子像素的Z值都将是这个单一值,此值为像素中心的Z值(没有开启centroid sample的情况下),这就会导致一个问题,所有先绘制了更近三角形的边缘像素都可能失去或产生错误的抗锯齿效果!(特别是在三角形连续交界处)请看下图,绘制顺序为红、蓝、绿,这些几何体的pixel shader都输出了SV_Depth。请注意某些边缘已经失去了抗锯齿效果。
另外D3D10引入一个新的概念ALPHA-TO-COVERAGE,以及一个SV_Coverage的pixel shader输出变量。注定要把MSAA玩出花来了!以8x的MSAA为例,在z/stencil/color buffer上每个像素均有8个子像素,如果开启了ALPHA-TO-COVERAGE,pixel shader输出的ALPHA值会被转为一个8阶的值,表示此Fragment在像素上的mask,这个主要是用来解决Alpha Test边缘锯齿问题,其原理就是将光栅化阶段产生的MASK A,AND ALPHA转化的MASK B,AND SV_Coverage MASK C。看下面的例子,三块完全重叠的面片,打开Alpha-To-Coverage,并且都输出0.5的Alpha值,从近到远分别为红、绿、蓝,发现完全不会有互相半透的效果,原因很简单,本例开的是8x msaa,0.5的ALPHA会被GPU转化为00001111B的MASK,红绿蓝三个Mesh都输出相同MASK的话,子像素的值会被最近的Mesh覆盖掉。
我们修改下输出的Alpha值,红色0.25,绿色0.5,蓝色0.75,当红绿蓝视距从近到远排列时,输出结果如下:
很简单,因为红色的MASK为00000011B,绿色为00001111B,蓝色为00111111B,互相重叠的部分,近的颜色将占据MASK相对应的子像素,较远的会被覆盖掉。如果我们再反过来看,让红绿蓝视距变为从远到近排列,结果就变成这样了:
原因大家可以自己分析。综上,Alpha-To-Coverage注定是个悲催的OIT技术!
更多MSAA资料
http://mynameismjp.wordpress.com/2012/10/24/msaa-overview/