Shader编程教程
2010-05-13 11:37:14| 分类: DirectX 3D学习|举报|字号 订阅
您好,欢迎来到XNA Shader教程1。我的名字叫Petri Wilhelmsen,是Dark Codex Studios的成员。我们经常会参加各种图形/游戏开发的竞赛,如Gathering,Assembly,Solskogen,Dream-Build-Play和NGA等。
本XNA Shaders编程教程将讨论XNA的不同方面的知识以及如何使用XNA和GPU编写HLSL。我将从一些基本理论开始,然后深入到shader编程的实际方法。
理论部分不会面面俱到,但足以让你开始使用shader并自己实践。它将涵盖HLSL的基础,HLSL语言如何工作和一些必须知道的关键字。
今天,我将介绍XNA和HLSL以及一个简单的环境光照算法。
先决条件
XNA的编程基础,因为我们将涉及到加载纹理,三维模型,矩阵和一些数学知识。
Shader简史
在DirectX8之前,GPU使用固定方式变换像素和顶点,即所谓的“固定管道”。这使得开发者不可能改变像素和顶点转化和处理的进程,使大多数游戏的图像表现看起来非常相似。
DirectX8提出了顶点和像素着色器,这让开发者可以在管道中决定如何处理顶点和像素,使他们获得了很强的灵活性。
一开始shader编程使用汇编语言程序使用的着色器,这对shader开发者来说相当困难,Shader Model 1.0是唯一支持的版本。但DirectX9发布后这一切改变了,开发者能够使用高级着色语言(HLSL)取代了汇编语言,HLSL语法类似C语言,这使shader更容易编写,阅读和学习。
DirectX 10.0提出了一个新的shader——Geometry Shader作为Shader Model 4.0的组成部分。但这需要一个最先进的显卡和Windows Vista才能支持。
XNA支持Shader Model 1.0至3.0,可以在XP,Vista和XBox360!上运行。
Shader?
嗯,历史已经说得够多了……那么什么是shader?
正如我所说的,shader可以用来定制管道的步骤,使开发者能够决定如何处理像素/顶点。
如下图所示,应用程序在渲染时启动并使用shader,顶点缓冲区通过向pixel shader发送所需的顶点数据与pixel shader协同工作,并在帧缓冲中创建了一个图像。
但请注意许多GPU不支持所有的shader模式,在开发shader时应引起足够重视。一个shader最好要有一个类似/简单的效果,使程序在较旧的计算机上也能工作正常。
Vertex shader
Vertex shaders用来逐顶点地处理顶点数据。例如可以通过将模型中的每个顶点沿着法线方向移动到一个新位置使一个模型变“胖”(这称之为deform shaders)。
Vertex shaders从应用程序代码中定义的一个顶点结构获取数据,并从顶点缓冲区加载这个结构传递到shader。这个结构描述了每个顶点的属性:位置,颜色,法线,切线等。
接着Vertex shader将输出传递到pixel shader。可以通过在shader中定义一个结构包含你想要存储的数据,并让Vertex shader返回这个实例来决定传递什么数据,或通过在shader中定义参数,使用out关键字来实现。输出可以是位置,雾化,颜色,纹理坐标,切线,光线位置等。
struct VS_OUTPUT { float4 Pos: POSITION; }; VS_OUTPUT VS( float4 Pos: POSITION ) { VS_OUTPUT Out = (VS_OUTPUT) 0; ... return Out; } // or float3 VS(out float2 tex : TEXCOORD0) : POSITION { tex = float2(1.0, 1.0); return float3(0.0, 1.0, 0.0); }
Pixel Shader
Pixel Shader对给定的模型/对象/一组顶点处理所有像素(逐像素)。这可能是一个金属盒,我们要自定义照明的算法,色彩等等。Pixel Shader从vertex shaders的输出值获取数据,包括位置,法线和纹理坐标:
float4 PS(float vPos : VPOS, float2 tex : TEXCOORD0) : COLOR { ... return float4(1.0f, 0.3f, 0.7f, 1.0f); }
pixel shader可以有两个输出值:颜色和深度。
HLSL
HLSL是用来开发shader的。在HLSL中,您可以声明变量,函数,数据类型,测试(if/else/for/do/while+)以及更多功能以建立一个顶点和像素的处理逻辑。下面是一些HLSL的关键字。这不是全部,但是最重要的。
数据类型:
- bool true or false
- int 32-bit integer
- half 16bit integer
- float 32bit float
- double 64bit double
向量:
- float3 vectorTest
- float vectorTest[3]
- vector vectorTest
- float2 vectorTest
- bool3 vectorTest
矩阵:
- float3x3: 3x3矩阵,float类型
- float2x2: 2x2矩阵, float类型
还有很多辅助函数处理复杂的数学表达式:
- cos( x ) 返回x的余弦值
- sin( x) 返回x的正弦值
- cross( a, b ) 返回向量a和向量b的叉乘
- dot( a,b ) 返回向量a和向量b的点乘
- normalize( v ) 返回一个归一化的向量v(v / |v|)
完整列表请看:http://msdn2.microsoft.com/en-us/library/bb509611.aspx (译者:推荐看clayman的博客中的The Complete Effect and HLSL Guide连载)
HSLS提供了大量的函数让你使用!它们能帮助你解决不同的问题。
Effect文件
Effect文件(.fx)让开发shader变得更容易,你可以在.fx文件中存储几乎所有关于着色的东西,包括全局变量,函数,结构,vertex shader,pixel shader,不同的techniques/passes,纹理等等。
我们前面已经讨论了在shader中声明变量和结构,但什么是technique/passes?这很简单。一个Shader可以有一个或一个以上的technique。每个technique都有一个唯一的名称,我们可以通过设置Effect类中的CurrentTechnique属性选择使用哪个technique。
effect.CurrentTechnique = effect.Techniques["AmbientLight"];
在这里,我们设置“effect”使用technique“AmbientLight”。一个technique可以有一个或多个passes,但请确保处理所有passes以获得我们希望的结果。
这个例子包含一个technique和一个pass:
technique Shader { pass P0 { VertexShader = compile vs_1_1 VS(); PixelShader = compile ps_1_1 PS(); } }
这个例子包含一个technique和两个pass:
technique Shader { pass P0 { VertexShader = compile vs_1_1 VS(); PixelShader = compile ps_1_1 PS(); } pass P1 { VertexShader = compile vs_1_1 VS_Other(); PixelShader = compile ps_1_1 PS_Other(); } }
这个例子包含二个technique和一个pass:
technique Shader_11 { pass P0 { VertexShader = compile vs_1_1 VS(); PixelShader = compile ps_1_1 PS(); } } technique Shader_2a { pass P0 { VertexShader = compile vs_1_1 VS2(); PixelShader = compile ps_2_a PS2(); } }
我们可以看到,一个technique有两个函数,一个是pixel shader,另一个是vertex shader。
VertexShader = compile vs_1_1 VS2(); PixelShader = compile ps_1_1 PS2();
这告诉我们,这个technique将使用VS2()作为vertex shader,PS2()作为pixel shader,并且支持Shader Model 1.1或更高版本。这就让GPU支持更高版本的shader变得可能。在XNA中实现Shader 在XNA中实现Shader很简单。事实上,只需几行代码就可以加载和使用shader。下面是步骤:
1. 编写shader
2. 把shader文件(.fx)导入到“Contents”
3. 创建一个Effect类的实例
4. 初始化Effect类的实例。
5. 选择使用的technique
6. 开始shader
7. 传递不同的参数至shader
8. 绘制场景
9. 结束shader
更详细的步骤:
1.记事本和Visual Studio等都可以用来编写shader。也有一些shader的IDE可用,我个人喜欢使用nVidias的FX Composer:http://developer.nvidia.com/object/fx_composer_home.html 。(译者:还推荐一个shader的IDE:AMD公司的RenderMonkey,可在http://ati.amd.com/developer/rendermonkey/downloads.html下载最新版本1.81(93.9MB,2008年4月8日),个人用下来的感觉好像nvidia实力更强一些,文档也很详实,而RenderMonkey上手更容易。)
2.当shader建立后,将其拖放到“Content”目录,自动生成素材名称。
3.XNA框架有一个Effect类用于加载和编译shader。要创建这个类的实例可用以下代码:
Effect effect;
Effext属于Microsoft.Xna.Framework.Graphics类库,因此,记得添加using语句块:
using Microsoft.Xna.Framework.Graphics
4.要初始化shader,我们可以使用Content从项目或文件中加载:
effect = Content.Load("Shader");
“Shader”是你添加到Content目录的shader名称。
5.选择使用何种technique:
effect.CurrentTechnique = effect.Techniques ["AmbientLight" ];
6.要使用Effect,请调用Begin()函数:
effect.Begin();
此外,您必须启动所有passes。
foreach (EffectPass pass in effect.CurrentTechnique.Passes) { // Begin current pass pass.Begin();
7.有很多方法可以设置shader的参数,但对这个教程来说下列方法够用了。注:这不是最快的方法,在以后的教程中我将回到这里:
effect.Parameters["matWorldViewProj"].SetValue(worldMatrix * viewMatrix*projMatrix);
其中“matWorldViewProj”是在shader中定义的:
float4x4 matWorldViewProj;
将matWorldViewProj设置为worldMatrix * viewMatrix * projMatrix。
SetValue设置参数并将它传递到shader,GetValue 从shader获取值,Type是获取的数据类型。例如,GetValueInt32()得到一个整数。
8.渲染你想要这个shader处理/转换的场景/对象。
9.要停止pass,调用pass.End(),要停止shader,调用Effect的End()方法:
pass.End(); effect.End();
为了更好地理解步骤,可参见源代码。
环境光照(Ambient light)
OK,我们终于到了最后一步,实现shader!不坏吧?首先,什么是“Ambient light” ? 环境光是场景中的基本光源。如果你进入一个漆黑的屋子,环境光通常是零,但走到外面时,总是有光能让你看到。环境光没有方向(译者:所以也将其称为“全局光照模型”),在这里应确保对象不会自己发光,它有一个基本的颜色。环境光的公式是:
I = Aintensity* Acolor
其中I是光的实际颜色, Aintensity是光的强度(通常在0.0和1.0之间),Acolor环境光的颜色,这个颜色可以是固定值,参数或纹理。好吧,现在开始实现Shader。首先,我们需要一个矩阵表示世界矩阵:
float4x4 matWorldViewProj ;
在shader顶端声明这个矩阵(作为全局变量)然后,我们需要知道vertex shader向pixel shader传递了哪些值。这可以通过建立一个结构(可以命名为任何值)实现:
struct OUT { float4 Pos: POSITION; };
我们创建了一个名为OUT的结构,其中包含一个float4类型的名叫Pos的变量。“:”后面的POSITION告诉GPU在哪个寄存器(register)放置这个值?嗯,什么是寄存器?寄存器是GPU中保存数据的一个容器。GPU使用不同的寄存器保存位置,法线,纹理坐标等数据,当定义一个shader传递到pixel shader的变量时,我们必须决定在GPU的何处保存这个值。 看一下vertex shader:
OUT VertexShader( float4 Pos: POSITION ) { OUT Out = (OUT) 0; Out.Pos = mul(Pos, matWorldViewProj); return Out; }
我们创建了一个OUT类型的函数,它的参数是float4类型的Pos:POSITION。这是模型文件/应用程序/游戏中定义的顶点位置。然后,我们建立一个名叫OUT的OUT结构实例。这个结构必须被填充并从函数返回,以便后继过程处理。输入参数中的Pos不参与后继过程的处理,但需要乘以worldviewprojection矩阵使之以正确放置在屏幕上。由于Pos是OUT中的唯一变量,我们已经返回它并继续前进。现在开始处理pixel shaders,我们声明为一个float4类型的函数,返回存储在GPU中的COLOR寄存器上的float4值。我们在pixel shader中进行环境光的算法:
float4 PixelShader() : COLOR { float Ai = 0.8f; float4 Ac = float4(0.075, 0.075, 0.2, 1.0); return Ai * Ac; }
这里我们使用上面的公式计算目前像素的颜色。Ai是环境光强度,Ac是环境光颜色。最后,我们必须定义technique并将pixel shader和vertex shader函数绑定到technique上:
technique AmbientLight { pass P0 { VertexShader = compile vs_1_1 VertexShader(); PixelShader = compile ps_1_1 PixelShader(); } }
好了,完成了!现在,我建议你看看源代码,并调整各个参数更好地理解如何使用XNA实现shader。