.fx文件
2008-08-04 11:23:17| 分类:|字号 订阅
在计算机3维场景中,物体表面的材质代表了其光学特性。最简单的材质可以表现为Diffuse颜色,Specular颜色,Emissive颜色等信息的集合;而为了表现物体表面的细节,可以 在材质中加入一张纹理——这些就构成了最基本的材质信息。在以前的Direct3D程序中,这些信息可以直接传送给设备,由设备自动根据它们来计算物体表面的光学效果。但是, 仅仅有这些基本的材质信息,已经不足以满足游戏制作者的要求和游戏玩家的要求了——他们希望场景中的材质更加复杂,具有更多的细节,更加逼真。
在Direct3D中,除了材质的概念,还存在一个渲染状态(Render State)的概念。在Direct3D Device中存在很多的渲染状态,它们可以在Direct3D进行渲染时控制渲染的流程和效 果,从而实现某些带有特效的材质。程序员可以通过IDirect3DDevice*::SetRenderState()方法来设置这些状态。所有的渲染状态都是一些特定的数值。对状态的设置可以通过硬 编码完成,即在程序中调用SetRenderState()方法,将设置什么样的状态“写死”在程序里,但是这样做的缺点就是太不灵活了——如果想要实现一种新的渲染状态,就需要修改 程序代码。所以更好的一种方法是将为了实现某一种特效材质的一些渲染状态值记录到一个“效果文件”中,通过在程序运行时读取该文件,从中分析出这些值,并将它们作为参 数调用SetRenderState()。这样,要想实现一种新的特效,只需修改“效果文件”而不用更改代码。
Direct3D SDK是通过Effect Framework来支持这种机制的。而前面所述的“效果文件”在Direct3D中是以*.fx文件存在的。在fx文件中保存了为实现某一特效的渲染状态,包括状态名 称和它们的对应值。所以在9.0以前版本的DirectX中就已经有Effect Framework和FX文件了,早期的Effect Framework仅仅是为了实现对渲染状态进行控制。 但是随着计算机显示硬件技术的发展,图形处理单元(GPU)正在重复CPU所走过的路——新一代的GPU已经具有了可编程特性,程序员可以通过对GPU编写一段程序来控制其渲染的 输出效果。这种程序一般称为Shader程序。目前的Shader程序分成两种,Vertex Shader和Pixel Shader。Vertex Shader主要用于对3维网格模型的每一个顶点进行处理,而Pixel Shader主要用于对要绘制到屏幕上的每一个象素进行处理。通过Shader,程序员可以制作出相当丰富的实时渲染效果。
同CPU一样,对GPU编程也是借助一定的编程语言来进行的。 在DirectX 8中,对GPU的编程是通过一种类似于汇编的语言进行的。而在DirectX 9中,使用了一种类似于C语言的高级语言——Microsoft High Level Shading Language (HLSL) 。
无论使用什么语言对GPU编程,都需要把编译好的Shader程序输送到显卡去。Direct3D Device可以通过SetVertexShader()和SetPixelShader()来向显卡输送Shader程序。然而,由 于Shader和材质具有十分紧密的关系——一般来说,一个Shader就是为了实现一种特殊的材质——最好是能够将Shader程序与材质进行整合。 所以,在DirectX 8和DirectX 9中,原来的Effect Framework发生了扩充——在原来的基础上加入了对Shader程序的支持。程序员可以把Vertex Shader和Pixel Shader程序以函数 的方式直接书写在FX文件中。
.FX文件
FX文件中的内容大致可以分成几部分:
预编译标志 变量表 结构定义 函数 Technique 预编译标志:预编译标志包括
#define #elif #else #endif #error #if #ifdef #ifndef #include #line #pragma #undef 其中最常用到的是#include和#define,同C语言中的意义很相似:用#include可以在一个FX文件中引入另外一个或多个文件。#define可以定义FX文件中的宏替换。例如:
#include "helper_Funcs.fx" //引入一个名为helper_funcs.fx的fx文件
#include "public_variables.fh" //引入一个名为public_variables.fh的文件
#define MATRICES_COUNT 25 //定义宏MATRICES_COUNT为25
#define VSHADER VShader_2_0 //定义宏VSHADER为VShader_2_0
#include带来的好处和C中也是一样的-您可以在一个头文件中定义一些公有变量、函数等,在其他文件中引用它们-就不用写很多遍了。例如:
文件public.fh:
mat4x4matWorldViewProj; // 4x4世界-视-投影变换矩阵
float3lightPosisiton; // 三维光源位置向量
float4 lightColor; // 光源的颜色
float time; // 当前时间
//定义一个名为VS_OUTPUT的结构。关于结构体定义,下文中会有介绍
structVS_INPUT
{
float4 LocalPos : POSITION;
float3 Normal : NORMAL;
float4 Color : COLOR;
float2 Texcoord : TEXCOORD0;
};
struct VS_OUTPUT
{
float4 WorldPos : POSITION;
float4 Color : COLOR;
float2 Texcoord : TEXCOORD0;
};
//定义一个名为CaculateWorldPosition的函数。关于函数定义,下文中会有介绍
float3CaculateWorldPosition( float4 LocalPos )
{
return mul( LocalPos, matWorldViewProj);
}
这样,当我们在另外一个文件中include这个头文件时,上面所有的定义都可以直接使用了。
文件client.fx:
#include "public.fh"
VS_OUTPUT VS_main( VS_INPUT In ) // 可以直接使用结构体定义VS_OUTPUT和VS_INPUT
{
VS_OUTPUT Out = 0;
Out.WorldPos = CaculateWorldPosition( In.LocalPos ); // 可以直接使用函数
Out.Color = In.Color;
Out.Texcoord = In.Texcoord;
}
这样不仅可以复用代码,还可以使变量名、结构体、函数名的定义统一。
而#define不仅仅可以使某些常量具有比较有意义的名称,通过与#ifdef,#ifndef, #else, #endif等结合使用,还可以用来根据一些配置控制编译过程。
变量表
每个FX文件都可以有若干参数变量,通过Effect Framework可以在程序中识别出这些参数的类型、名称和用途,这样就可以将程序中的一些参数输送到Effect中去,从而更加灵活 的控制效果。参数的类型很多,可以是int, float, matrix, texture等等。例如:
matrixmatWorld; //定义一个名为matWorld的矩阵类型参数变量
float time; //定义一个名为time的浮点类型参数变量
texture texDiffuse; //定义一个名为texDiffuse的纹理类型参数变量
另外,每个变量还有前缀修饰符、Semantic、Annotation等,限于篇幅,在这里不再赘述,具体的介绍请参考DirectX C++帮助文档的DirectX Graphics > Reference > HLSL Shader Reference > Variable Declaration Syntax条目。
结构定义
在FX文件中可以定义结构体,这些结构体一般用于Shader函数的参数和返回值。结构体的定义与C语言方式及其类似,例如:
structVS_OUTPUT //结构名称
{
float4 Pos : POSITION; //成员变量。
float4 Color : COLOR;
float2 Texcoord : TEXCOORD0;
};
上面每个成员变量后面的标识符是该变量的semantic,HLSL编译器根据这个标识符来确定该变量的用处。不一定非得是结构体成员变量才有semantic,一般来说Shader的输入输出参数变量都可以有semantic。
函数
函数部分是在Effect Framework加入了对Vertex Shader和Pixel Shader的扩充后才加入到FX文件中的。FX文件中的函数的内容可以用汇编形式书写,也可以用HLSL编写。目前一般 都是使用HLSL。用这种语言书写的函数与C语言函数十分类似,可以说,只要学过C语言,书写Shader函数就绰绰有余了。在同一个fx文件中可以定义很多函数,在函数中也可以互 相调用,但是最终Shader程序的入口将在fx文件的Technique部分中指定。对于哪个函数是Vertex shader函数,哪个是Pixel shader函数,也是在Technique中指定的。关于Technique,将在下文中介绍。 例子:
float4CalcDiffuseColor( float3 Normal )
{
float4 Color; ...//用于实现该函数功能的多条语句
return Color;
}
VS_OUTPUTVertex_Shader( float4 InPos : POSITION,
float3 InNor : NORMAL,
float3 InTexcoord : TEXCOORD )
{
VS_OUTPUT Out; ...//用于实现该函数功能的多条语句
Out.Color = CalcDiffuseColor(InNor); //函数调用
return Out;
}
正如上文中所说,Shader程序分为两种:Vertex shader和Pixel shader。在fx函数中就有两种相应的函数:Vertex shader函数和Pixel Shader函数。Vertex shader函数的输入参数是网格模型中的每一个顶点数据,其输出是经过该函数特殊处理的顶点数据;而Pixel Shader函数的输入,则是经过硬件光栅化过程后经过插值的Vertex shader输出结果。至于Pixel shader的输出,一般就是经过该函数计算得到的一个颜色值,即要画到后备缓冲中一个象素上的颜色值。但是这个“颜色值”有时并不一定代表颜色。而且对于支持多RenderTarget的硬件,Pixel shader还可以有多个输出,分别对应不同的RenderTarget。
technique
technique是FX文件的主体,是真正设置各种渲染状态的地方,也是指定所使用的Shader程序入口的地方。在理解Technique前首先要理解Pass的概念。Pass是Technique的组成部分 ,一个Pass就代表了绘制时的一遍。通常为了达到一种效果,仅仅绘制一遍网格模型是不够的,需要向framebuffer中多次绘制,并利用设置渲染状态中的BlendState进行Alpha混合。这就是经常提到的“Muli-pass Rendering”。但是随着硬件越来越强大,Shader程序的功能越来越强,目前的趋势是所有的特效材质都将可以用越来越少个Pass来完成。 一个technique中,可以存在一个或多个Pass,但是至少要有一个Pass时该technique才会起实际作用。当存在多个pass时,默认情况下在渲染时将会按照pass在文件中的前后顺序 作为渲染时的前后顺序。technique和Pass中的内容都是以大括号括起来。
technique的例子:
techniqueTec_Shader_1_X //定义一个名为Tec_Shader_1_X的technique
{
pass P0 //一个名为P0的pass
{
VertexShader = compile vs_1_1 Vertex_Shader(); //设置Vertex Shader程序入口函数
PixelShader = compile ps_1_1 Pixel_Shader(); //设置Pixel Shader程序入口函数
AlphaBlendEnable= true; //设置渲染状态
SrcBlend = SrcAlpha;
DestBlend = InvSrcAlpha;
... //其他设置
}
pass P1 //一个名为P1的pass
{
...
}
}