引言
高层着色语言(HLSL)是DirectX® 9最为强力的新组件之一。使用这种标准的高级语言, 在进行着色时编写者可以专注于算法而不用再去理会诸如寄存器的分配,寄存器读端口限制, 并行处理指令等等硬件细节. 除了把开发者从硬件细节中解放出来之外,HLSL 也具有高级语言所有的全部优势,诸如:代码重用容易, 可读性增强以及存在一个优化过的编译器。本书和 ShaderX2 - Shader Tips & Tricks 这本书的许多章节就用到了HLSL编写的着色器. 阅读完本章引言后,你会很容易理解那些着色器并在工作中用到它们。
这一章, 我们概述语言本身的基本结构以及将HLSL集成到你的应用程序中的方法。
一个简单的示例
在彻底描述HLSL之前, 让我们先看一下程序中HLSL顶点着色和HLSL像素着色的实现,这个程序渲染简单的木纹。下边显示的第一个HLSL着色是一个简单的顶点着色:
float4x4 view_proj_matrix;
float4x4 texture_matrix0;
struct VS_OUTPUT
{
float4 Pos : POSITION;
float3 Pshade : TEXCOORD0;
};
VS_OUTPUT main (float4 vPosition : POSITION)
{
VS_OUTPUT Out = (VS_OUTPUT) 0;
// Transform position to clip space
Out.Pos = mul (view_proj_matrix, vPosition);
// Transform Pshade
Out.Pshade = mul (texture_matrix0, vPosition);
return Out;
}
最开始两行声明了一对4×4的矩阵,分别命名为view_proj_matrix和texture_matrix0。在全局矩阵之后,声明了一个结构体。这个VS_OUTPUT有两个成员:Pos和Pshade。
该着色器的main函数接受一个单精度float4类型的输入参数并返回一个VS_OUTPUT结构体。float4类型的vPosition是着色器唯一的输入,返回的VS_OUTPUT结构体定义了 该顶点着色器的输出。目前不必关心在参数和结构体之后的关键字POSITION和TEXCOORD0。它们被称为语义,他们的含义将在本章后边讨论。
看一下main函数的实际代码部分,你可以看到一个内部函数mul,它被用来把输入的向量vPosition和矩阵view_proj_matrix相乘。在顶点着色器中这个函数最常被用来执行顶点-矩阵的乘法。就这样,vPosition被作为列向量,因为它是mul的第二个参数。如果向量vPosition是mul的第一个参数,它将被作为行向量。内部函数mul以及其他内部函数将在本章后边更详细的讨论。在把输入的vPosition的位置转换换到裁减空间之后,vPosition与另一个矩阵texture_matrix0相乘以产生3D纹理坐标。两次转换的结果写入返回的结构体VS_OUTPUT。一个顶点着色器必须总是以最小值输出到一个裁减空间位置。任何从顶点着色器输出的额外值都是 通过贯穿光栅化多边型插值得到的,也可用来输入到像素着色器。就这样,通过一个内插器, 三维的Pshade从顶点着色器被传递到像素着色器。
下边,我们看到一个简单的HLSL木纹像素着色器。这个像素着色器和刚才我们描述的顶点着色器一起工作,它将被编译成模型ps_2_0。
float4 lightWood; // xyz == Light Wood Color
float4 darkWood; // xyz == Dark Wood Color
float ringFreq; // ring frequency
sampler PulseTrainSampler;
float4 hlsl_rings (float4 Pshade : TEXCOORD0) : COLOR
{
float scaledDistFromZAxis = sqrt(dot(Pshade.xy, Pshade.xy)) * ringFreq;
float blendFactor = tex1D (PulseTrainSampler, scaledDistFromZAxis);
return lerp (darkWood, lightWood, blendFactor);
}
最开始几行在全局范围内声明了一对浮点类型的四元数组和一个浮点变量。在这些变量之后,声明了一个被称为PulseTrainSampler的取样器。取样器将在章节后边讨论,目前你可以把它看成一个在显存中的窗口,它与过滤状态和纹理坐标寻址模式发生关联。在变量和取样器声明后边是着色器代码的主体部分。 你可以看到有一个输入参数Pshade,它是贯穿多边形插值得到的。它的值是由顶点着色器计算每一个顶点得出的。在像素着色器中,把着色空间Z轴上的笛卡尔距离作为一维纹理坐标来计算,衡量,使用,以存取绑定于PulseTrainSampler的纹理。tex1D()取样函数返回的颜色标量被用作混合因子,以混合在着色器全局范围内声明的两种相反颜色。像素着色器最终输出一个混合的四元向量结果。所有的像素着色器至少必须返回一个四元 RGBA 颜色。我们将在稍后章节中讨论像素着色器的附加选项。
汇编语言和编译对象
既然我们已经了解了一些HLSL着色器,这里简要讨论一下如何在代码中涉及Direct3D,D3DX,汇编着色器模型和你的程序。DirectX 8中第一次把着色器引入了Direct3D。在那个时候,这些虚拟着色器是这样定义的——每一个大致相当于一个特殊的3D硬件商生产的图形处理器。每个虚拟着色器都设计有汇编语言。在DirectX 8.0和DirectX 8.1中,编写这些着色器模型的程序(被命名为vs_1_1以及ps_1_1直到ps_1_4)相对短小并且一般由开发者直接用合适的汇编语言编写。如图1左所示,凭借D3DXAssembleShader()程序把人们可读的汇编语言代码传递给D3DX库并返回着色器的二进制表示,该二进制表示由CreatePixelShader()或CreateVertexShader()依次传递给Direct3D。更多传统汇编着色模型的细节,请参考在线和离线资源,包括Shader X 和DirectX SDK。
图1. Use of D3DX for Assembly and Compilation in DirectX 8 and DirectX 9
如图1右所示,在DirectX 9中的情形非常相似,凭借D3DXCompileShader() API,程序把HLSL着色器传递给D3DX并返回编译后着色器的二进制表示,该二进制表示由CreatePixelShader()或CreateVertexShader()轮流传递给Direct3D。生成的二进制汇编代码是一个函数,它只取决于选择的编译对象,而不是什么用户或开发者系统上的特殊图形设备。就是说,生成的二进制汇编程序与平台无关, 即可在任何地方编译或运行。事实上,Direct3D运行时本身并不知道HLSL的任何内容,除了二进制汇编着色器模型。这样做很有好处因为这就意味着HLSL编译器的更新不必依赖于Direct3D运行时。事实上,在2003年夏季末本书截稿与首印期之间,Microsoft开始计划发布含有更新过的HLSL编译器的DirectX SDK更新。
除了D3DX中HLSL编译器的开发之外,DirectX 9.0也提出了另外的汇编层着色器模型以展示最新的3D图形硬件的功能。 直接使用汇编语言为新的模型(vs_2_0,vs_3_0,ps_2_0和ps_3_0) 做开发,程序开发人员会感到自由 ,不过我们希望绝大多数开发人员都转移到HLSL从而专注于着色器的开发。
实际的硬件
当然,仅仅因为你可以写一个HLSL程序来表达一个特殊的着色算法不等于它能够在硬件上运行。前面已经讨论过,应用程序通过调用D3DX中的D3DXCompileShader() API把HLSL着色器编译成二进制汇编程序。这个API的入口参数之一是这样一个参数:它定义了HLSL编译器使用哪一个汇编语言模型(或编译对象)来表示最终着色器代码。如果一个程序在运行时执行HLSL着色器编译,程序会检测Direct3D设备的性能并选择匹配的编译对象。如果HLSL着色器中的算法太复杂以至于不能在选择的编译对象上执行,编译将会失败。这意味着尽管HLSL大大有利于着色器的开发,却不会把开发人员从这么一个现实中解放出来:把游戏封装后给拥有各种性能图形设备的用户。作为一个游戏开发人员,你仍然得为你的图像处理好一系列步骤,为更好的显示卡编写更好的着色器,为较老的卡编写更基本的。不过,有了编写完善的HLSL,负担可以大大减轻。
编译失败
如上所述, 给定的HLSL着色器编译特殊对象的失败说明对于编译对象来说着色器太过复杂。这就意味着着色器需要大量的资源或是需要一些诸如动态分支(不被所选编译对象所支持)的功能。例如,某个HLSL着色器可能被编写用于在一个着色器内存取所给定的六重纹理贴图。如果这个着色器被编译成ps_1_1, 编译将会失败,因为ps_1_1模型只支持四重纹理。其他编译失败的通常原因是超过了所选编译对象的最大指令计数器。某个HLSL中表示的算法也许仅仅需要大量指令而使得给定的编译对象不能被执行。
要重点注意的是所选编译对象不会限定编写人员所使用的HLSL语法。例如,着色器编写人员会使用'for'循环,子程序,'if-else'等等 语句,编译本身不支持循换,分支或'if-else'语句的对象。这种情况下,编译器将展开循环,内联函数调用并同时执行'if-else'语句的两个分支(译者注:即if与else后的语句全都执行),根据'if-else'语句中所使用的原始值选择合适的结果。当然, 如果最后所得到的着色器(程序)太长或相反超出了编译对象的资源,编译将失败。
命令行编译器: FXC
许多开发人员选择在着色器被封装之前把它从HLSL编译成二进制汇编语言,而不是在正在使用D3DX的客户机器上当程序载入时或首次运行时编译HLSL着色器。这保证了HLSL源代码不被窥视,同时也确保所有其程序能够永久运行的着色器已经通过其内部质量确认流程。在DirectX 9.0 SDK中提供了一个方便的命令行编译程序fxc允许开发人员脱机编译着色器。该程序有许多方便的选项,你不但可以以命令行方式编译你的着色器,也能产生指定编译对象的反汇编代码。如果你想优化你的着色器或只是想更详细的了解虚拟着色器的性能,在开发期间研究输出的反汇编代码是非常有用的。表1列出了这些命令行选项。
表1. FXC 命令行选项
-T target编译对象 (默认: vs_2_0)
-E name入口点 name (默认: main)
-Od禁止优化
-Vd禁止确认
-Zi允许调试信息
-Zpr按照行顺序挑选矩阵
-Zpc按照列顺序挑选矩阵
-Fo file输出目标文件
-Fc file输出所生成代码的列表
-Fh file输出含有生成代码的头部
-D id=text定义宏
-nologo没有版权信息
既然你了解了用于着色器开发的HLSL编译器的内容,我们就可以讨论实际的语言结构了。 在我们继续下面内容的时候,头脑里要一直保留着编译对象的概念以及潜在的汇编着色器模型的不同性能,这很重要。
原文:http://msdn.microsoft.com/library/en-us/dnhlsl/html/shaderx2_introductionto.asp