3.7 合并阶段
如节2.4.4所述,合并阶段是各片段(由顶点着色器生成)的深度和颜色和帧缓冲进行组合的地方。另外,此阶段还可以发生模板缓冲和z缓冲的操作,以及颜色混合。颜色混合通常用于透明和合成操作(参考节5.7)。
合并阶段是介于固定功能阶段(如剪切)和完全可编程着色器阶段之间的一个有趣的中间点。虽然它不可编程,但其操作却是高度可配置的。特别地,可以配置颜色混合来执行大量不同的操作。最常用的是涉及颜色和alpha值的加、减、乘;其他还有一些操作,比如最大和最小值、位逻辑等操作。DirecX10添加了一种叫做双色混合的功能,它可以将来自像素着色器的两个颜色和帧缓冲颜色进行混合。
如果使用了MRT功能,则可以对多个缓冲执行混合。DirecX10.1引入了对每个MRT缓冲执行不同混合操作的能力。在以前的版本,总是对所有的缓冲执行相同的混合操作(注意,双色混合和MRT不兼容)。
3.8 效果
到目前位置,管道之旅一直聚焦于各种各样的可编程阶段。为控制这些阶段,顶点、几何和像素着色器程序是必须的,但它们也不是从石头里蹦出来的。首先,一个孤立的着色器程序并没有什么用处:顶点着色器程序将其结果填充到像素着色器;必须加载所有的程序才能做成事情。程序员必须让顶点着色器的输出和像素着色器的输入相匹配。一个特别渲染的效果可能产生于在好几个通道上执行的、未知数目的着色器程序。除了着色器程序自身,有时还必须特别地设置状态变量以让这些程序正确地工作。比如,渲染器状态包括是否以及如何使用z缓冲和模板缓冲,片段如何影响已经存在的像素的值(如,替换,相加或混合)。
正是由于这些原因,各种组织才开发了效果语言,如HLSL FX,CgFX,和COLLADA FX。效果文件尝试封装用于执行一个特殊渲染算法[261, 971]的所有相关信息。它通常定义了一些全局参数,这些参数可以由应用程序指定。例如,某个效果文件可能定义顶点和像素着色器所需的、用来渲染一个非常逼真的塑料材质。它将公开塑料颜色、粗糙度之类的参数,这样,我们就可以根据各个被渲染的模型更改这些值,但使用同一个效果文件。
为显示效果文件的作用,我们将给出一个精简的示例,该示例来自于NVIDIA的FX Composer 2效果系统。该DirectX 9 HLSL效果文件实现了一个非常简化形式的Gooch着色[423]。其中的一部分使用表面法线,并将之和光照位置进行比较。如果法线指向光照,则使用暖色调着色表面;否则使用冷色调。
图 3.8 Gooch着色器,从暖橙色变化到冷蓝色。
在这两个用户定义色之间对内部的角度进行插值。该着色器技术是非逼真渲染的一种,它是第十一章的主题。这种效果的一个例子显示于图3.8。
效果变量在效果文件的开始处定义。最先的几个变量是不可调整的,是和镜头位置相关的参数:
float4x4 WorldXf : World;
float4x4 WorldlTXf : WorldlnverseTranspose;
float4x4 WvpXf : WorldViewProjection;
语法是:type id : semantic。类型float4x4用于矩阵,名称由用户定义,semantic段是内建的名称。正如semantic的名字所显示,WorldXf 是一个模型到世界的转换矩阵;WorldlTXf 是WorldXf的逆转矩阵;WvpXf 是模型空间到相机剪切空间的转换矩阵。 这些拥有可辨识semantic的值需要由应用程序提供,但不会显示在用户界面中。
接下来是用户定义的变量:
此处,在尖括号<>之间提供了一些额外的注解,然后指定默认值。注解属于特定应用,对效果或着色器编译器没有任何效果。它们可以被应用程序查询。在此处的示例中,注解描述如何在用户界面中公开这些变量。
之后,定义了着色器输入和输出的数据结构:
appdata定义了模型中的单个顶点的数据,因此也就定义了顶点着色器程序的输入数据。vertexOutput 是顶点着色器生成、像素着色器使用的数据定义。输出名称中使用的TEXCOORD* 是管道演变的人工产物。起初,表面可以附加多个纹理,因此这些额外的数据字段被叫做纹理坐标。在实际使用中,这些字段可以保存任何顶点着色器传递给像素着色器的数据。
再接下来,定义了各种着色器程序代码元素。我们此处只有一个顶点着色器程序:
vertexOutput std_VS(appdata IN) {
vertexOutput OUT;
float4 No = float4(IN.Normal,0);
OUT.WorldNormal = mul(No,WorldITXf).xyz;
float4 Po = float4(IN.Position,1);
float4 Pw = mul(Po,WorldXf);
OUT.LightVec = (LampOPos - Pw.xyz);
OUT.HPosition = mul(Po,WvpXf);
return OUT;
}
该程序首先使用矩阵相乘计算表面在世界空间中的法线。转换是下章的主题,因此,我们不会解释此处为什么使用逆转。世界空间中的位置也被使用离屏转换进行计算。该位置减去光的位置,就是表面到光的方向向量。最后,对象的位置被转换到剪切空间,用于光栅化。它是顶点着色器程序的一个必须的输出。
知道世界空间中的光方向和表面法线后,像素着色器程序计算表面颜色:
float4 gooch_PS(vertexOutput IN) : COLOR
{
float3 Ln = normalize(IN.LightVec);
float3 Nn = normalize(IN.WorldNormal);
float ldn = dot(Ln,Nn);
float mixer = 0.5 * (ldn + 1.0);
float4 result = lerp(CoolColor, WarmColor, mixer);
return result;
}
向量Ln是标准化的光方向,Nn是标准化的表面法线。通过标准化,这两个向量的点积ldn就是它们之间的夹角的余弦。我们想要在冷暖色调之间使用余弦值进行线性插值。函数lerp()接受一个0到1之间的混合值。其中0表示使用CoolColor,1表示使用WarmColor,0和1之间的值则表示它们的混合。余弦的值范围是[-1, 1],混合值将这个范围转换到[0,1]。转换后的值随后用来混合两种颜色,生成了一个片段。这些着色器是函数。一个效果文件可以由任意数目的函数组成,还可以包含其他效果文件中使用的函数。
一个pass通常由一个顶点和像素(和几何)着色器以及各种所需的状态配置组成。一个technique是生成所需效果的一个或多个pass的集合。我们的这个文件很简单,只有一个technique,且这个technique只有一个pass:
technique Gooch < string Script = "Pass=p0;n; > {
pass pO < string Script = "Draw=geometry;"; > {
VertexShader = compile vs_2_0 std_VS();
PixelShader = compile ps_2_a gooch_PS();
ZEnable = true;
ZWriteEnable = true;
ZFunc = LessEqual;
AlphaBlendEnable = false;
}
}
这些状态配置强制以普通方式使用z缓冲--启用z缓冲,使之可读可写,并且,如果片段的深度小于等于现存的z深度,则传递。alpha混合被关闭,因为使用该technique的模型被假定为不透明的。这些规则意味着,如果片段的z深度等于现存的物体或者比现存的物体离相机更近,则使用计算出的片段颜色替换相应的像素的颜色。换句话说,就是使用z缓冲的标准用法。
在同一个效果文件中,可以存储若干个technique。这些technique通常是同一个效果的变种(对应不同的着色器模型)。可以有广泛的效果。图3.9展现了现代可编程着色器管道的强大功能,这还只是九牛一毛。一个效果通常封装了相关的technique。有各种各样的方法被开发出来以管理着色器集[845, 847, 887, 974, 1271]。
图 3.9 可编程着色器可以实现广泛的材质和后处理效果。
关于GPU本身的介绍到此就结束了。但GPPU还有其他很多事情可以做,还有很多方式我们可以使用和组合。利用这些能力的相关原理和算法是本书的中心主题。有了这些基本概念之后,我们将聚焦于提供对转换和可视化外观(它们是管道中的关键元素)的深入理解。
进一步阅读和资源
略...