【译】XNA Shader 程序设计(一)
前些天看到有人在博客里连载的,我边学边译吧。
时间原因,翻译的很仓促,加上我对shader也不怎么了解,所以大家尽量看原文吧,在http://digierr.spaces.live.com/blog/
XNA Shader 程序设计
教程 1 – 环境光
XNA Shader系列教程将包含XNA的许多方面,如何使用GPU,如何在XNA下编写HLSL shader。开始我会将一些基础理论,然后更多的是实践。
理论部分不会非常详细,但对于学习和实践编写shader足够了。我会讲一些HLSL的基础知识,HLSL语言如何工作还有一些值得学习的关键字。
今天我会介绍XNA和HLSL,也会讲一个简单的环境光理论。
前置知识
XNA编程的知识,我不会讲太多关于如何加载贴图、3D模型、矩阵和数学。
shader简史
在DirectX8之前,GPU变换顶点像素的方法是固定的,叫做“固定渲染管线”。这样,开发者在把顶点和像素传给GPU后,就无法控制处理器的工作,所以游戏图像看起来都差不多。
DirectX8 引入了顶点着色器(vertex
shader)和像素着色器(pixel shader),于是开发者可以控制渲染管线中的顶点和像素,灵活性大大提高。
开始汇编语言用于shader的开发,这就让shader开发变得十分困难,并且仅支持shader model 1.0。但DirectX9的发布改变了这种状况,开发者有机会一个叫做High Level Shading Language(HLSL)的C语言风格的高级语言代替汇编语言。这让shader更容易读、写和学习。
DirectX10.0引入了一个新的shader,几何着色器(Geometry Shader),是Shader Model 4.0的一部分。但这需要高档的显卡还有Windows Vista。
XNA支持Shader Model 1.0到3.0,可以在XP, VISTA和Xbox360上工作!
Shaders?
好了,历史讲够了……那么,什么是shader?
让我说,shader就是用来让开发者定制那些顶点和像素在渲染管线中被怎样处理。
如下图,程序渲染的时候会使用shader,顶点缓冲通过顶点着色器与把数据交给像素着色器,它们共同工作创建出帧缓冲内的图片。
嘱咐大家一个重要问题,许多GPU并不支持所有的shader model,这在shader开发时要考虑到。一个shader应该有不用的方法来实现相似的特效,这样程序才能在老机器上跑起来。
顶点着色器(Vertex Shaders)
顶点着色器用于逐顶点操作顶点数据。例如,可以通过把模型的每个顶点沿法线方向平移使模型在渲染时看起来变“胖”(deform shaders)。
顶点着色器从程序定义的顶点结构体中获取数据,是在顶点缓冲中读取并传递给shader。这就确定了顶点会包含什么属性:位置、颜色、法线、正切值等。
顶点着色器把输出数据传递给一会要用到的像素着色器。可以通过在shader中定义一个结构体来确定顶点着色器要传递给下一层的数据,或者通过在shader中定义带关键字out的参数。输出可以是位置、雾、颜色、纹理坐标、正切值、光照方向等等。
struct VS_OUTPUT
{
float4 Pos: POSITION;
};
VS_OUTPUT VS( float4 Pos: POSITION )
{
VS_OUTPUT Out = (VS_OUTPUT) 0;
...
return Out;
}
// 或者
float3 VS(out float2 tex : TEXCOORD0) :
POSITION
{
tex = float2(1.0, 1.0);
return float3(0.0, 1.0, 0.0);
}
像素着色器
像素着色器逐像素的处理给定的模型、物体、顶点集。这就像一个金属盒,我们可以在里面制定自己的光照法则等等。像素着色器从顶点着色器的输出中读取数据,例如位置、法线、纹理坐标。
float4 PS(float vPos : VPOS, float2 tex :
TEXCOORD0) : COLOR
{
...
return float4(1.0f, 0.3f, 0.7f, 1.0f);
}
像素着色器可以有两个输出,颜色和深度。
HLSL
HLSL用来开发shader。在HLSL中,你可以定义变量、函数、数据类型等用来编写顶点和像素的处理逻辑。下面是HLSL中部分关键字的列表,并不全面,但是最常见。
HSLS数据类型
bool true 或 false
int 32位整数
half 16位整数
float 32位浮点数
double 64位浮点数
HSLS向量
float3 vectorTest
float vectorTest[3]
vector vectorTest
float2 vectorTest
bool3 vectorTest
HSLS矩阵
float3x3: 3x3浮点数矩阵
float2x2: 2x2浮点数矩阵
还有一些辅助函数,用来帮助我们进行复杂的数学运算。
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
HLSL 提供了大量函数,就等你用了!学习它们,你会知道如何解决各种问题。
Effect文件
Effect文件(.fx)让HLSL编写shader变得简单,你可以在fx文件中存储任何东西。包括变量、函数、结构体、顶点着色器、像素着色器、techiques/passes、贴图等等。
我们已经知道了如何在shader中声明变量和结构体,但是technique/passes是什么?其实很简单,一个shader可以包含若干technique,每个technique在游戏中有唯一的名字,我们可以选择使用shader中的哪个technique,通过设置Effect类的CurrentTechnique属性。
effect.CurrentTechnique = effect.Techniques["AmbientLight"];
这里,我们让“effect”使用technique“AmbientLight”。一个technique可以包含一个或多个pass,我们需要使用所有的pass才能达到最终效果。
这是一个包含一个technique一个pass的shader的例子:
technique Shader
{
pass P0
{
VertexShader = compile vs_1_1 VS();
PixelShader = compile ps_1_1 PS();
}
}
这是一个包含一个technique两个pass的shader的例子:
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的shader的例子:
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有两个函数,一个是像素着色器,一个是顶点着色器。
VertexShader = compile vs_1_1 VS2();
PixelShader = compile ps_1_1 PS2();
这是指这个technique将使用VS2()作为顶点着色器,PS2()作为像素着色器,支持shader model 1.1或更高。如果GPU支持更高版本的shader model就可以写更复杂的shader。
在XNA中实现shader
在XNA中实现shader很简单,加载使用一个shader只需要几行代码。下面列出了使用shader的步骤:
1. 编写shader
2. 把shader文件(.fx)加到“Contents”里面
3. 实例化一个Effect类
4. 初始化Effect对象
5. 选择要使用的technique
6. 开始shader
7. 向shader传递参数
8. 绘制场景、物体
9. 结束shader
详细说明:
1. 编写shader时,可以记事本、visual studio这样的编辑软件,也可以使用shader IDE。我个人喜欢nVidia的FX Composer: http://developer.nvidia.com/object/fx_composer_home.html
2. 编写好shader后,把它拖到“Content”文件夹,获取属性名。
3. XNA Framework提供了一个Effect类,用来加载和编译shader。用下面几行代码实例化这个类。
Effect effect;
Effect包含于“Microsoft.Xna.Framework.Graphics”命名空间,所以添加下面一行:
using Microsoft.Xna.Framework.Graphics
4. 初始化shader,我们可以使用Content从文件或者从项目中加载shader:
effect = Content.Load<Effect>("Shader");
Shader是你加到Content中的shader的属性名
5. 选择你要使用的technique:
effect.CurrentTechnique = effect.Techniques["AmbientLight"];
6. 开始使用一个Effect,要调用Begin()方法:
effect.Begin();
记住使用shader中的所有的pass。
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
// 开始当前pass
pass.Begin();
}
7. 给shader传递参数有很多方法,下面的方法适合初学使用。
注:这不是最快的方法,我在后面的教程中会介绍
effect.Parameters["matWorldViewProj"].SetValue( worldMatrix *
viewMatrix * projMatrix);
“matWorldViewProj”是在shader中定义的:float4*4 matWorldViewProj;matWorldViewProj被设成了worldMatrix * viewMatrix * projMatrix。
SetValue设置了参数的值并传递给shader,GetValue<Type>可以从shader取回值,Type是要取回的值的类型。例如,GetValueInt32()从shader中取回一个整数。
8.渲染你想用当前shader渲染的物体。
9.结束这个pass,调用pass.End()。然后结束shader,调用Effect的End()方法:
pass.End();
effect.End();
为了更好的理解,打开提供的源码实际地去读一下。
环境光
好了,我们终于要进行最后一步,实现这个shader!不错吧。
首先,什么事“环境光”?
环境光是存在于场景中的基础的光线。对于一个几乎全黑的屋子,那里的环境光将近于零,但当走在屋外,总会有点光使我们能看到东西。这个光没有方向,只是让物体能被看到,但是环境光有一个基本的颜色。
环境光的计算式是:
I = Aintensity x Acolor ( 1.1)
l就是环境光,Aintensity是光强(通常在0.0到1.0之间),Acolor是环境光的颜色。这个颜色可以使硬编码的。
好了,我们来实现这个shader。首先,我们需要一个变换矩阵:
float4x4 matWorldViewProj;
在shader的最开始声明,作为一个全局变量。
下面,我们需要知道顶点着色器把什么值传递给像素着色器,通过定义一个结构体来实现(你可以任意命名)
struct OUT
{
float4 Pos: POSITION;
};
我们创建了一个叫做OUT的结构体,包含了一个叫做Pos的类型为float4的变量。后面的:POSITION告诉GPU该把这个值放到哪个寄存器中。那么,寄存器是什么?寄存器就是GPU存储数据的一个容器。GPU用不同的寄存器存储位置、法线、纹理坐标等等。当在shader中定义变量时,我们必须同时指明GPU在哪里存储它。
来看一下顶点着色器:
OUT VertexShader( float4 Pos: POSITION )
{
OUT Out = (OUT) 0;
Out.Pos = mul(Pos, matWorldViewProj);
return Out;
}
我们创建了返回OUT类型的顶点着色器函数,参数为float4 Pos: POSITION。这是模型文件、程序中定义的顶点坐标位置。
然后我们实例化了一个OUT结构体,叫做Out。这个结构体必须填充好数据,一遍后面的函数处理。
输入的位置数据是没有处理过的,所以需要乘上worldviewprojection矩阵,把顶点变换到屏幕上正确的位置。
OUT结构体只有这一个变量,所以我们的函数可以返回了。
下面轮到像素着色器了。我们声明一个float4型的函数,返回的float4值存储在GPU的COLOR寄存器中。
在像素着色器中我们会按公式计算环境光:
float4 PixelShader() : COLOR
{
float Ai = 0.8f;
float4 Ac = float4(0.075, 0.075, 0.2, 1.0);
return Ai * Ac;
}
我们使用前面的环境光公式计算每个像素的颜色值。Ai是环境光强度,Ac是环境光颜色。
最后,我们需要定义一个technique,并把像素着色器和顶点着色器绑定到这个technique上:
technique AmbientLight
{
pass P0
{
VertexShader = compile vs_1_1 VertexShader();
PixelShader = compile ps_1_1
PixelShader();
}
}
好了,就是这样!
我建议大家亲自阅读源码,尝试修改一些数据,彻底的理解在XNA中实现shader。
Download: Executable + Source