State 
[in] Device-state variable that is being modified. This parameter can be any member of the D3DTRANSFORMSTATETYPE enumerated type, or the D3DTS_WORLDMATRIX macro.
其中的D3DTS_WORLDMATRIX是一个宏,可以像这么用D3DTS_WORLDMATRIX(0),D3DTS_WORLDMATRIX(1),这样一来就可以传递多个矩阵到固定管线中了(等到可编程管线就没这么麻烦了,可以直接通过顶点着色器常量传递矩阵)。

最后简单说一下D3DFVF_XYZBn这种顶点格式的用途:这种顶点格式往往被用在骨骼动画的顶点里,因为骨骼动画需要一个顶点受多个骨骼的影响。

定义好了FVF与顶点数据结构之后,我们需要把它设置到设备上去。我们知道,早些年的DX8也好,DX9也好,其Device都有一个SetVertexShader接口,用于设置顶点着色器程序。而在DX8中,这个接口不仅仅是可编程管线的vertex shader通过它来设置,实际上如果采用的是固定管线的话,顶点的FVF格式也是走这个接口设置的。DX8中的Vertex shader创建出来之后是一个32位的DWORD类型的handle,从而得以与FVF常量共用这同一个接口。而在DX9中,SetVertexShader的接受参数变成一个IDirect3DVertexShader9*了,而固定管线的FVF被挪走,需要通过接口SetFVF来进行设置了。

另外,由于在DX9中,IDirect3DVertexShader9在创建的时候,并没有包含顶点声明信息,因此DX9增加了一个接口IDirect3DVertexDeclaration9专门用于为可编程管线的VertexShader提供顶点声明信息。为什么一定要有顶点声明信息呢?写过HLSL的同学都知道,我们在vertex shader里是必须要定义vs input的数据结构的,顶点声明信息必须与我们的VS_INPUT数据结构保持一致。那么这个顶点声明信息在什么时候用呢?仔细思考一下就会明白,vs是一段运行于GPU上的程序,它按照我们编写的HLSL代码接受输入,经过计算后输出,输入的数据是在顶点缓冲中准备好的,在DrawPrimitive之前,通过SetVertexBuffer设置,此后这些输入数据通过驱动程序被提供给显卡,然而底层如何知晓应把多长一截的数据作为一份输入的顶点数据呢?以及应该把顶点数据中的哪一部分送到顶点寄存器里,哪些送到纹理采样单元里去?到这里,就不得不依赖顶点声明提供的信息了。

前面在材质与光照一节,我们简单介绍了一下D3D固定管线光照中,材质是如何与灯光一起起作用,并作用于物体的色泽,明暗,高光的。现在当我们手握Shader这样的利器,就拥有了绕开D3D固定管线光照的自由了(譬如如今已经很普及的基于像素的光照技术,就是把光照从顶点着色阶段推迟至像素着色器中)。我们还可以利用顶点着色器的常量传递变换矩阵,从而拥有自定义顶点变换,或者在顶点变换阶段获取一些我们想要的中间结果的能力。

4. 纹理与像素着色器

前面说了关于顶点变换与顶点光照相关的东西。接下来要说的是纹理混合,也就是SetTextureStageState函数干的事情。我们都知道DX支持纹理混合,我们可以把多个纹理按照Sampler的索引(DX9中是Sampler,而在DX8中则是Stage),通过SetTexture函数设置到设备上。

 

所谓Stage是什么一个概念呢?参考下图(来源于DX9SDK):

dx_texture_stage

在每一个Stage上设有一张纹理(如果没有设置纹理),在固定管线中,我们通过SetTextureStageState设置在当前的Stage中,纹理如何与“当前像素”混合并输出到下一个Stage,“当前像素”来源于上一个Stage的输出,0Stage的当前像素的Color及Alpha是顶点Color/Alpha插值得来的。在纹理阶段比较常见的就是DiffuseTexture,SpecularTexture,分别用于颜色与高光。在每一个Stage中,纹理颜色与上一阶段颜色采用下式进行混合:

FinalColor = TexelColor × SourceBlendFactor + PixelColor × DestBlendFactor

这里需要注意的是,纹理混合与最终的AlphaBlending是两个概念。纹理混合阶段,根据顶点颜色以及纹理决定最终要输出的像素的颜色和透明度,而到了AlphaBlending阶段,则是决定该像素与已经绘制到BackBuffer上的像素如何去混合。此外,更复杂的功能如NormalMapping,通常则需要像素着色器参与。按照我的理解,如果想要实现像素光照,则必须把光照阶段从顶点着色器中推迟到像素着色器中才可行,NormalMap提供了这一阶段每个像素的法线,从而使像素光照得以实现。(D3D9在固定管线中提供了一套BumpMap,但是老实说,D3D9文档里对此的说明看得我云里雾里的,并没有搞清楚其原理究竟是什么。)

像素着色器最初的就是为了替代固定管线的纹理混合操作而诞生的。也就是说,如果采用了PixelShader对像素进行着色,那么SetTextureStageState里面的那些颜色,Alpha混合操作就不再起作用了。

我们以一个常见的例子来解释纹理混合的用法,美术可能要求实现一种正片叠底的纹理混合效果,对于程序实现而言,实现方案就是找两张贴图,用乘法进行纹理混合。如果采用SetTextureStageState实现:

 

pD3DDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
pD3DDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);

pD3DDevice->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_MODULATE);
pD3DDevice->SetTextureStageState(1, D3DTSS_COLORARG1, D3DTA_TEXTURE);
pD3DDevice->SetTextureStageState(1, D3DTSS_COLORARG2, D3DTA_CURRENT);

 

解释一下这里的两个StageState的设置:阶段0,我们对Color采用SELECTARG1操作,同时把COLORARG1,设置为D3DTA_TEXTURE,也就是说,完全以SetTexture(0, …)设置的纹理颜色作为这一阶段的输出;在阶段1,COLOROP为MODULATE,也就是将COLORARG1与COLORARG2相乘作为输出,公式:

 

其中COLORARG1设置为D3DTA_TEXTURE,也就是SetTexture(1, …)中设置的纹理,而COLORARG2为D3DTA_CURRENT,表示的是上一阶段的输出。这样经过这两个Stage之后,我们获得的输出就是两张贴图相乘的结果了。

接下来再用Ps2.0(需要DX9,如果采用DX8则需用ps1.4实现)实现一遍该效果:

 

texture g_Tex0;
texture g_Tex1;
sampler g_Sampler0 = sampler_state {Texture = g_Tex0; MipFilter = LINEAR; MinFilter = LINEAR; MagFilter = LINEAR;}
sampler g_Sampler1 = sampler_state {Texture = g_Tex1; MipFilter = LINEAR; MinFilter = LINEAR; MagFilter = LINEAR;}

struct VS_OUTPUT
{
    float4 Position   : POSITION;   // vertex position
    float2 TexCoord   : TEXCOORD0;  // vertex texture coords
};

float4 PS_Main_2_0( VS_OUTPUT Input ) : COLOR0
{
    float4 color0 = tex2D( g_Sampler0, Input.TexCoord );
    float4 color1 = tex2D( g_Sampler1, Input.TexCoord );
    return float4(color0 * color1);
}

 

使用ps的话,所需要做的工作非常直观,用预先定义好的Sampler以及uv坐标对纹理采样,并把颜色相乘输出即可。

这只是一个最简单的例子,实际上ps的用途非常广泛(实现ShaderMap,像素光照,诸多后期处理效果都需要采用ps实现),而更多的内容就不可能涵盖在本文的范围之内了,感兴趣的读者可以读一读GPUGems系列,ShaderX系列等经典书籍。

5. Alpha测试,深度测试,以及Alpha混合

走完了像素着色的过程之后,就到了最终的绘制步骤了。这里有几个因素决定我们是否,以及如何往BackBuffer/ZBuffer/StencilBuffer上绘制像素:AlphaTest,StencilTest,ZTest,Alpha混合模式。

关于上述测试的顺序和流程,由于OpenGl与D3D在上述测试的顺序上达成了一致,因此可以参考下图(图摘自OpenGL2.0 spec Page 199):

Pre-FragmentOperations

如果开启了AlphaTest,则首先做AlphaTest,根据像素的Alpha值与一个指定的D3DRS_ALPHAREF值比较的结果,将D3DRS_ALPHAFUNC的判断未能通过的像素直接砍掉。(不会进行后续的z-test以及z-write)

如果开启了StencilTest,则继续做StencilTest,如果不通过StencilTest,则也不会进行下一步ZTest,但是可能会修改StencilBuffer。

如果开启了ZTest,根据当前像素Z值,与DepthBuffer上该位置已有Z值做比较,如果D3DRS_ZFUNC的判断未能通过,则将该像素砍掉,但是根据D3DRS_STENCILZFAIL的状态,有可能会修改StencilBuffer。通过了ZBuffer的像素,如果开启了ZWrite,则该像素的Z值会写入DepthBuffer,否则(ZWrite关闭)该像素不会影响DepthBuffer(也就是说不会遮挡后续绘制的像素,尽管可能该像素离观察者比将来要绘制的像素更近)

最后根据D3DRS_ALPHABLENDENABLE决定是否开启Alpha混合。如果Alpha混合没有开启:则直接把像素的颜色透明度写入BackBuffer,如果Alpha混合开启了,则根据D3DRS_SRCBLEND以及D3DRS_DESTBLEND的选项,决定新写入像素如何与BackBuffer已有像素进行混合。

上述流程是标准流程,然而在较新的几代显卡里,针对StencilTest与ZTest又有了许多新的技术,比如EarlyZ优化等,由于这里牵扯到的显卡型号,各厂商可能都有不一致的地方,我就没有做更进一步的详细研究。下面有几个可以供参考的链接,感兴趣的同学可以自己了解一下:

amddeveloper的一篇文章在这里:http://developer.amd.com/media/gpu_assets/Depth_in-depth.pdf

beyond3d上的一个讨论帖看这里:http://forum.beyond3d.com/showthread.php?t=51025

 

6. 更多话题

本文介绍了D3D流水线的一般过程,对比了固定管线与可编程管线的差别(当然有很多细节未能兼顾到),对D3D渲染流程的知识结构做了一个一般性的总结。

事实上,随着图形技术的发展,如今的3D图形技术已远走出很长一段路了,比如在DX9架构下就已经发展出很多成果的ShadowMap,NormalMap,Realtime Global Illumination等。再比如到DX10新增的Geometry Shader以及DX11架构下新增的Compute Shader,Tessellation等。

有时候学得越多,才越知道自己所知甚少。在撰写本文的过程中,我也把此前有些混淆的概念做了整理,自己也有不少收获。如果读者对本文所涉的话题有更多理解,或者发现了错漏之处,还望不吝告知。 : )