您好,欢迎来到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的编程基础,因为我们将涉及到加载纹理,三维模型,矩阵和一些数学知识。
在DirectX8之前,GPU使用固定方式变换像素和顶点,即所谓的“固定管道”。这使得开发者不可能改变像素和顶点转化和处理的进程,使大多数游戏的图像表现看起来非常相似。
2001年,DirectX8提出了顶点和像素着色器,这让开发者可以在管道中决定如何处理顶点和像素,使他们获得了很强的灵活性。
一开始shader编程使用汇编语言程序使用的着色器,这对shader开发者来说相当困难,Shader Model 1.0是唯一支持的版本。但DirectX9发布后这一切改变了,开发者能够使用高级着色语言(HLSL)取代了汇编语言,HLSL语法类似C语言,这使shader更容易编写、阅读和学习。
DirectX 10.0提出了一个新的shader——Geometry Shader作为Shader Model 4.0的组成部分。但这需要一个最先进的显卡和Windows Vista才能支持。
DirectX的最新版本为DirectX 11,它包含了tesselator,用于并行编程的DirectCompute等新功能。
XNA支持Shader Model 1.0至3.0,可以在XP,Vista和XBox360!上运行。
那么什么是shader?shader一组运行在GPU上的指令,使开发者能够创建小型的程序控制图形管线的三个阶段:像素着色器阶段,几何着色器阶段和像素着色器阶段。
图1:可编程图形管道
如图1所示,你可以对绿色部分进行编程,而其他阶段是固定的,你无法控制它们。Xbox360和XNA不支持几何着色器,所以本教程不会涉及它。下面我们快速浏览一下顶点着色器和像素着色器(不理解代码也别急,我们会在后面的部分加以讨论)
Vertex shaders用来逐顶点地处理顶点数据。例如可以通过将模型中的每个顶点沿着法线方向移动到一个新位置使一个模型变“胖”(这称之为deform shaders)。
Vertex shaders从应用程序代码中定义的一个顶点结构获取数据,并从顶点缓冲区加载这个结构传递到shader。这个结构描述了每个顶点的属性:位置,颜色,法线,切线等。
接着Vertex shader将输出传递到pixel shader。可以通过在shader中定义一个结构包含你想要存储的数据,并让Vertex shader返回这个实例来决定传递什么数据,或通过在shader中定义参数,使用out关键字来实现。输出可以是位置,雾化,颜色,纹理坐标,切线,光线位置等。
将一个对象的坐标转换到屏幕空间的简单顶点着色器代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
struct
VertexShaderInput
{
float4 Position : POSITION0;
};
struct
VertexShaderOutput
{
float4 Position : POSITION0;
};
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutput output;
float4 worldPosition = mul(input.Position, World);
float4 viewPosition = mul(worldPosition, View);
output.Position = mul(viewPosition, Projection);
return
output;
}
|
Pixel Shader对给定的模型/对象/一组顶点处理所有像素(逐像素)。这可能是一个金属盒,我们要自定义照明的算法,色彩等等。Pixel Shader从vertex shaders的输出值获取数据,包括位置,法线和纹理坐标:
1
2
3
4
|
float4 PS(
float
vPos : VPOS, float2 tex : TEXCOORD0) : COLOR
{
return
float4(1.0f, 0.3f, 0.7f, 1.0f);
}
|
pixel shader可以有两个输出值:颜色和深度。
图1中所示的所有阶段整合在一起就是为了合成图像并显示在屏幕上。
高级着色语言(High Level Shading Language,HLSL)是用来开发shader的一种类似于C的语言。在HLSL中,你可以声明变量,函数,数据类型,测试(if/else/for/do/while+)以及更多功能以建立一个顶点和像素的处理逻辑。下面是一些HLSL的关键字。这不是全部,但是最重要的。
bool | true或false |
int | 32位整数 |
half | 16位整数 |
float | 32位浮点数 |
double | 64位高精度浮点数 |
float3 vectorTest | float x 3 |
float vectorTest[3] | float x 3 |
vector vectorTest | float x 3 |
float2 vectorTest | float x 2 |
bool3 vectorTest | bool x 3 |
float3x3 | 3x3矩阵,浮点类型 |
float2x2 | 2x2矩阵,浮点类型 |
HLSL提供了很多函数处理复杂的数学方程。在以后的学习中,我们会逐一介绍,下面的表格中列出的只是常用的几个。学习这些函数对创建高性能的shader程序是非常有用的,你要避免重复发明轮子。
cos(x) | 返回x的余弦值 |
sin(x) | 返回x的正弦值 |
cross(a, b) | 返回向量a和向量b的叉乘 |
dot(a,b) | 返回向量a和向量b的点乘 |
normalize(v) | 返回一个归一化的向量v(v / |v|) |
完整列表请看:http://msdn.microsoft.com/en-us/library/ff471376.aspx
Effect文件(.fx)让开发shader变得更容易,你可以在.fx文件中存储几乎所有关于着色的东西,包括全局变量,函数,结构,vertex shader,pixel shader,不同的techniques/passes,纹理等等。我们前面已经讨论了在shader中声明变量和结构,但什么是technique/passes?这很简单。一个Shader可以有一个或一个以上的technique。每个technique都有一个唯一的名称,我们可以通过设置Effect类中的CurrentTechnique属性选择使用哪个technique。
1
|
effect.CurrentTechnique = effect.Techniques[
"AmbientLight"
];
|
一个.fx文件代表一个effect。在这里,我们设置“effect”使用technique“AmbientLight”。一个technique可以有一个或多个passes,但请确保处理所有passes以获得我们希望的结果。下面的例子包含一个名为AmbientLight的technique和一个名为P0的pass:
1
2
3
4
5
6
7
8
|
technique AmbientLight
{
pass P0
{
VertexShader = compile vs_1_1 VS();
PixelShader = compile ps_1_1 PS();
}
}
|
这个例子包含一个technique和两个pass:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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。
1
2
|
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很简单。事实上,只需几行代码就可以加载和使用shader。下面是步骤:
1. 编写shader
2. 把shader文件(.fx)导入到“Contents”
3. 创建一个Effect类的实例
4. 初始化Effect类的实例。
5. 选择使用的technique
6. 传递不同的参数至shader
7. 绘制场景
1.记事本和Visual Studio等都可以用来编写shader。也有一些shader的IDE可用,我个人喜欢使用nVidia的FX Composer:http://developer.nvidia.com/object/fx_composer_home.html。
2.当shader建立后,将其拖放到“Content”目录,自动生成素材名称。素材名称与fx文件名称是相同的,你也可以修改为不同的名称。
3.XNA框架有一个Effect类用于加载和编译shader。要创建这个类的实例可用以下代码:
1
|
Effect effect;
|
Effext属于Microsoft.Xna.Framework.Graphics类库,因此,记得添加using语句块:
1
|
using
Microsoft.Xna.Framework.Graphics
|
4.要初始化shader,我们可以使用Content从项目或文件中加载:
1
|
effect = Content.Load
"Shader"
);
|
“Shader”是你添加到Content目录的shader名称。
5.选择使用何种technique:
1
|
effect.CurrentTechnique = effect.Techniques [
"AmbientLight"
] ;
|
6.设置shader中的参数。首先你需要创建EffectParamter对象:
1
|
EffectParameter projectionParameter;
|
然后在LoadContent方法中将这个参数绑定到shader中的对应变量:
1
|
projectionParameter = effect.Parameters[
"Projection"
];
|
现在你就可以使SetValue方法设置参数了:
1
|
projectionParameter.SetValue(projection);
|
8.最后遍历所有shader中的pass绘制场景:
1
2
3
4
5
6
7
8
9
10
11
|
for
(
int
i = 0; i < effect.CurrentTechnique.Passes.Count; i++)
{
//EffectPass.Apply will update the device to
//begin using the state information defined in the current pass
effect.CurrentTechnique.Passes[i].Apply();
//sampleMesh contains all of the information required to draw
//the current mesh
graphics.GraphicsDevice.DrawIndexedPrimitives( PrimitiveType.TriangleList, 0, 0,
meshPart.NumVertices, meshPart.StartIndex, meshPart.PrimitiveCount);
}
|
现在我们知道了什么是shader,现在就可以创建你的第一个shader了。这个shader非常简单,只是变换了顶点坐标并计算了模型上的环境光照。
首先,什么是“Ambient light” ?
环境光是场景中的基本光源。如果你进入一个漆黑的屋子,环境光通常是零,但走到外面时,总是有光能让你看到。环境光没有方向(译者:所以也将其称为“全局光照模型”),在这里应确保每个对象都会受到一个光照,获得一个基本的颜色。
在图2中的场景只包含一个绿色的环境光,场景由一个黑色背景和一个没有纹理的僵尸组成,在下面的教程中我们会让这个僵尸的视觉效果变得更好。
图2 被环境光照射的场景
环境光的公式是: I = Aintensity* Acolor
其中I是光的实际颜色,Aintensity是光的强度(通常在0.0和1.0之间),Acolor环境光的颜色,这个颜色可以是硬编码的值,参数或纹理。
好吧,现在开始实现Shader。
首先,我们需要矩阵表示世界矩阵、视矩阵和投影矩阵:
1
2
3
4
5
6
|
float4x4 World;
float4x4 View;
float4x4 Projection;
float4 AmbientColor;
float
AmbientIntensity;
|
然后,我们需要创建结构体包含shader的输入和输出,这个结构体用于Vertex Shader内部。结构体中包含一个float4类型的名叫Position的变量。“:”后面的POSITION告诉GPU在哪个寄存器(register)放置这个值?嗯,什么是寄存器?寄存器是GPU中保存数据的一个容器。GPU使用不同的寄存器保存位置,法线,纹理坐标等数据,当定义一个shader传递到pixel shader的变量时,我们必须决定在GPU的何处保存这个值。
在声明Vertex Shader方法VertexShaderFunction(类似于Vertex Shaders的main()函数)时,我们指定这个函数的返回值为VertexShaderOutput类型的对象(你可以自定义这个结构体的名称,例如VSOut等),参数为VertexShaderInput结构体。在VertexShaderFunction方法中必须创建一个VertexShaderOutput结构体的实例,设置它的成员并返回。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
struct
VertexShaderInput
{
float4 Position : POSITION0;
};
struct
VertexShaderOutput
{
float4 Position : POSITION0;
};
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutput output;
float4 worldPosition = mul(input.Position, World);
float4 viewPosition = mul(worldPosition, View);
output.Position = mul(viewPosition, Projection);
return
output;
}
|
上面的代码是一个基本例子,它获取顶点的位置并转换到正确的空间。
现在开始处理pixel shaders,在这里进行环境光照的计算。它将Vertex Shader的输出作为输入。我们声明为一个float4类型的函数,返回存储在GPU中的COLOR寄存器上的float4值。
1
2
3
4
|
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
return
AmbientColor*AmbientIntensity;
}
|
这里我们使用上面的公式计算目前像素的颜色。AmbientIntensity是环境光强度,AmbientColor是环境光颜色。最后,我们必须定义technique并将pixel shader和vertex shader函数绑定到technique上:
1
2
3
4
5
6
7
8
|
technique AmbientLight
{
pass P0
{
VertexShader = compile vs_1_1 VertexShader();
PixelShader = compile ps_1_1 PixelShader();
}
}
|
好了,完成了!现在,我建议你看看源代码,并调整各个参数更好地理解如何使用XNA实现shader。