很多3D游戏都需要地形,建立并渲染地形,再加上物理效果,比如驾驶一辆小汽车在场景行使,你会觉得这很困难。而这篇文章将讲解这种技术。
为了编译其中的程序代码,你需要:
一个C#编译器,最好是Visual Studio .NET
DIRECTX9.0c开发工具
一块显卡并且支持shader2.0以上,因为除非你不想它运行的很慢。(比如只有10FPS)
我只是推荐你,最好精通C#,和Managed DirectX。
开始,
首先你在渲染地形时,通常是用到一张带灰度信息的位图。这通常是一个容易的手段。这篇文章介绍的是使用一种灰度高度比例贴图。这就是我用到的贴图,你也可以自己去修改它。
我们将用两个纹理,一个草地,一个土地。地形比较高的部分用草地文理。我们将两种纹理混合在一起,这就是根据位图的灰度做到的。
这里用到一种语言High Level Shader Language
熟悉它你就会知道这是使用更高级的顶点和纹理的渲染方式。由于HLSL和C很相似,所以懂C的可以不费劲掌握它。以下的就是将两个纹理混合的HLSL程序:
float4x4 WorldViewProj;float4 light;
void Transform(
in float4 inPos : POSITION0,
in float2 inCoord : TEXCOORD0,
in float4 blend : TEXCOORD1,
in float3 normal : NORMAL,
out float4 outPos : POSITION0,
out float2 outCoord : TEXCOORD0,
out float4 Blend : TEXCOORD1,
out float3 Normal : TEXCOORD2,
out float3 lightDir : TEXCOORD3 )
{
outPos = mul(inPos, WorldViewProj);
outCoord = inCoord;
Blend = blend;
Normal = normalize(mul(normal,WorldViewProj));
lightDir = inPos.xyz - light;
}
看上去HLSL和C的语法结构相似:再调节名称时,input,output变量前经常会有语意的定义。input通常是从程序中接受变量然后传导output。你也许注意到TEXCOORD被使用了很多次。这是因为TEXCOORD在程序中使用了多次,一个变量不提供位置,法线等。而HLSL包含了计算函数如mul(),normalize().在MSDN里可以知道使用方法。想知道更多关于HLSL方面的知识。推荐去访问相关的一些网站。
现在解释一下HLSL代码含义:首先input接受position,position在这里是和投影矩阵相乘之后得到的。顶点将被移动到相机的空间。input接受纹理坐标并混合两种纹理。法线也被移动。最后平行光被减去光源的坐标(该例,世界空间是和对象空间一致的所以不会有改变)将其传给像素渲染,以下是像素渲染代码:
Texture Texture1;
Texture Texture2;
sampler samp1 = sampler_state { texture = <Texture1>;
minfilter = LINEAR; mipfilter = LINEAR; magfilter = LINEAR;};
sampler samp2 = sampler_state { texture = <Texture2>;
minfilter = LINEAR; mipfilter = LINEAR; magfilter = LINEAR;};
float4 TextureColor(
in float2 texCoord : TEXCOORD0,
in float4 blend : TEXCOORD1,
in float3 normal : TEXCOORD2,
in float3 lightDir : TEXCOORD3) : COLOR0
{
float4 texCol1 = tex2D(samp1, texCoord*4) * blend[0];
float4 texCol2 = tex2D(samp2, texCoord) * blend[1];
return (texCol1 + texCol2) * (saturate(dot(normalize(normal),
normalize(light)))* (1-ambient) + ambient);
}
正象你所看到的,像素渲染器使用了所有顶点渲染器中的变量,除了POSITION0,因为POSITION0是所有输出所用到的。首先两个纹理将用tex2D()做计算,tex2D()不影响文理只是一个处理器而已。这些颜色根据混合值相乘并加上光线的密度,然后反回结果。返回值是float4由COLOR0语法做了标记。每个像素渲染器都要返回一个这样的值。
再回到C#
为了使程序和SHADER通信,必须有顶点声明。将通知DIRECTX数据在顶点缓冲以及相关的input顶点渲染变量,以下是顶点声明代码:
VertexElement[] v = new VertexElement[]
{
new VertexElement(0,0,DeclarationType.Float3,DeclarationMethod.Default,
DeclarationUsage.Position,0),
new VertexElement(0,12,DeclarationType.Float3,DeclarationMethod.Default,
DeclarationUsage.Normal,0),
new VertexElement(0,24,DeclarationType.Float2,DeclarationMethod.Default,
DeclarationUsage.TextureCoordinate,0),
new VertexElement(0,32,DeclarationType.Float4,DeclarationMethod.Default,
DeclarationUsage.TextureCoordinate,1),
VertexElement.VertexDeclarationEnd
};
decl = new VertexDeclaration(device,v);
正如你看到的,顶点声明包含一个数组VertexElements描述了顶点的结构。说道结构,你不能自己定义一个CustomVertex,因为每一个文理参数必须精确到每一顶点。所以结构必须是以下:
public struct Vertex
{
Vector3 pos;
Vector3 nor;
float tu,tv;
float b1,b2,b3,b4;
public Vertex(Vector3 p,Vector3 n,
float u,float v,float B1,float B2,
float B3, float B4, bool normalize)
{
pos = p;nor = n;tu = u;tv = v;
b1=B1; b2=B2; b3=B3;b4 = B4;
float total = b1 + b2 + b3 + b4;
if ( normalize)
{
b1 /= total;
b2 /= total;
b3 /= total;
b4 /= total;
}
}
public static VertexFormats Format =
VertexFormats.Position | VertexFormats.Normal |
VertexFormats.Texture0 | VertexFormats.Texture1;
}
它包含了Vector3描述位置,法线。floats描述文理坐标和混合后的值。标记到结构成员,还包含了一个构造器。还包含了一个变量格式。
为了和DIRECTX的EFFECT通信。要用Effect类建立Effect:
The Effect class
String s = null;
effect = Effect.FromFile(device, @"../../simple.fx", null,
ShaderFlags.None, null, out s);
if ( s != null)
{
MessageBox.Show(s);
return;
}
通常,不需要调试shaders,如果其中代码有bug,可能要花几小时去找,因为没有任何的线索。为了避免如此,需要Effect过载构造器,这将输出参数并且虽然有effect抛出CompilationErrors。过载也会成功编译,只是在output不是null时会有出错的对话框出现。
好,除了包含所有入口点的类,程序还包含地形类。该类读取位图数据,然后建立VertexBuffer 和IndexBuffer。我们指定地形高度,包围盒。 获得暗度和亮度,每个位图的像素成为了地形顶点高度。所有矩形分成三角形,假如索引面列表。渲染时使用effect.BeginScene(),当渲染被调用时effect.BeginScene()将通知effect要得到数据来渲染,effect.EndScene()将停止顶点数据的处理。另外VertexDeclaration, VertexBuffer, 和 IndexBuffer被设置,最后DrawPrimitives渲染地形。
为了修改shader的全局变量,Effect类包含SetValue()方法,你可以输入一段字符做标记:
effect.SetValue("Texture1", t1);
An other way is to create an EffectHandle, like this:
EffectHandle handle = effect.GetParameter(null,"ambient");
effect.SetValue(handle,0.5f);
用这种方法你不必向方法赋值,所以会更快。所以变量只能设置一次,但如果需要变化多次,最好使用techniques,当使用了techniques,shaders将自动调用每一次。Effect文件能包含许多顶点和像素处理器,而且至少要有一个technique,technique是如下做声明:
technique TransformTexture
{
pass P0
{
VertexShader = compile vs_2_0 Transform();
PixelShader = compile ps_2_0 TextureColor();
}
}
一个technique由一个或多个passes构成,这里只有一个P0;每个pass,你要安排顶点和像素处理器(这里有很多HLSL处理器的版本:1_1,2_0,3_0。更高版本,更可能有人提供,但是显卡能支持的却很少)Transform() 和 TextureColor() 是顶点和像素处理器的名称。
在一个pass中,你也可以设置RenderState,如果你想这样要将Device.RenderState.Cullmode 设为 Cull.None,然后放到pass的第一行:
CullMode = None;