《翻译》 原文链接:http://www.codeproject.com/Articles/8195/Rendering-terrains-with-Managed-DirectX
(忽然发现自己翻译的自己都不知道说的什么。。。也罢,一个小时的产物,不要要求太高)
(丫的,翻译完也不不知道怎么实现的,还是老老实实去看代码吧,话说我还没用过c#,容我编程c++的,龙书第13章不就讲的地形么,只不过没用HLSL而已。。。)
DirectX地形渲染
没有一个3D游戏不使用地形的,创建和渲染一个地形以及相关的物理模型,例如,在其上面开车是非常困难的。这篇文章将会demo一个创建地形的技术:非常简单的方式,但非常有效。
为了编译这篇文章的code,你需要:
C#编译器,最好是VS.NET
D3D9.0c SDK
支持像素着色器2.0的显卡,否则将会变的非常慢,大约10fps
我建议读者有会C#及相关DirectX经验
BEGINNING
渲染一个地形,首先需要用一种方式描述它,通常使用灰度高度图。这篇文章将会使用灰度图,这是一种简便的方式。(作者好啰嗦:P)。下图是我使用的一个灰度图,你可以改变成任意你喜欢的样子。
我们将使用两种纹理。一种是草地的,另一种是石头的。地形中较高的位置使用石头,较低的地方使用草地。(作者说的是pixel)。我们根据pixel的高度决定采用哪种纹理。
HLSL
为了达到这样的效果,我们使用HLSL。使用这种语言,它可以写入它们自己的顶点和像素着色器,更好控制定点和像素进行渲染。HLSL非常接近C。所以并不用花太多时间学习HLSL。下面是我们渲染地形用的顶点着色器。
1 float4x4 WorldViewProj;float4 light; 2 void Transform( 3 in float4 inPos : POSITION0, 4 in float2 inCoord : TEXCOORD0, 5 in float4 blend : TEXCOORD1, 6 in float3 normal : NORMAL, float4x4 WorldViewProj;float4 light; 7 void Transform( 8 in float4 inPos : POSITION0, 9 in float2 inCoord : TEXCOORD0, 10 in float4 blend : TEXCOORD1, 11 in float3 normal : NORMAL, 12 out float4 outPos : POSITION0, 13 out float2 outCoord : TEXCOORD0, 14 out float4 Blend : TEXCOORD1, 15 out float3 Normal : TEXCOORD2, 16 out float3 lightDir : TEXCOORD3 ) 17 { 18 outPos = mul(inPos, WorldViewProj); 19 outCoord = inCoord; 20 Blend = blend; 21 Normal = normalize(mul(normal,WorldViewProj)); 22 lightDir = inPos.xyz - light; 23 } 24 25 out float4 outPos : POSITION0, 26 out float2 outCoord : TEXCOORD0, 27 out float4 Blend : TEXCOORD1, 28 out float3 Normal : TEXCOORD2, 29 out float3 lightDir : TEXCOORD3 ) 30 { 31 outPos = mul(inPos, WorldViewProj); 32 outCoord = inCoord; 33 Blend = blend; 34 Normal = normalize(mul(normal,WorldViewProj)); 35 lightDir = inPos.xyz - light; 36 }
它非常接近C函数,除了语法中的Input、output变量名字。通过应用程序传入顶点数据,顶点着色器输出output。你可能已经注意到TEXCOORD被用了好多次,那是因为TEXCOORD可以被用来传输应用程序的特殊数据,并非是位置和顶点法线(不就是纹理坐标啦,作者好啰嗦)。HLSL包含许多数学算法,如mul(),normalize()。完整的列表详见MSDN。欲知更多HLSL的细节,我建议你自己百度,因为网上有很多好东西,如果我说的过多,这篇文章就会变的too long。(:p,忍不住吐槽,作者真的好啰嗦)。
我将简单的说明顶点着色器是干嘛的。首先,传入的顶点乘以模型视图投影矩阵。这样顶点就从局部坐标系转换到视图坐标系下了。输入的纹理坐标和blend变量将会传入像素着色器。顶点法线同样需要变换并变成单位向量。最后,(这句话好长,容我好好理解) At last, the direction of the light is calculated by subtracting the position of the light from the position of the vector in world space (in this case, world space is the same as object space since there isn’t any translation, rotation, or scaling) to pass it to the pixelshader.。
下面是像素着色器:
1 Texture Texture1; 2 Texture Texture2; 3 4 sampler samp1 = sampler_state { texture = <Texture1>; 5 minfilter = LINEAR; mipfilter = LINEAR; magfilter = LINEAR;}; 6 sampler samp2 = sampler_state { texture = <Texture2>; 7 minfilter = LINEAR; mipfilter = LINEAR; magfilter = LINEAR;}; 8 float4 TextureColor( 9 in float2 texCoord : TEXCOORD0, 10 in float4 blend : TEXCOORD1, 11 in float3 normal : TEXCOORD2, 12 in float3 lightDir : TEXCOORD3) : COLOR0 13 { 14 float4 texCol1 = tex2D(samp1, texCoord*4) * blend[0]; 15 float4 texCol2 = tex2D(samp2, texCoord) * blend[1]; 16 return (texCol1 + texCol2) * (saturate(dot(normalize(normal), 17 normalize(light)))* (1-ambient) + ambient); 18 }
如你所见,像素着色器从顶点着色器接收了几乎全部变量,除了POSITION0变量。那是因为这个变量是所有顶点着色器都会输出的变量,但是我们的像素着色器并不使用它。首先,两个纹理颜色是由tex2D()计算的,tex2D并不直接操作纹理,而是采样器(sampler)。像素颜色信息是由blend变量、光源强度、采样器所决定的(估计翻译错了。。。)。像素着色器返回float4类型的COLOR0.每个像素着色器必须返回一个COLOR0变量,否则将不会编译。
Back to C#
为了使你的应用程序与shader交互,你必须先声明一个顶点,这将告诉DirectC顶点缓存中有什么数据及它是如何与顶点着色器的输入变量关联的。下面是地形中使用的顶点声明。
1 VertexElement[] v = new VertexElement[] 2 { 3 new VertexElement(0,0,DeclarationType.Float3,DeclarationMethod.Default, 4 DeclarationUsage.Position,0), 5 new VertexElement(0,12,DeclarationType.Float3,DeclarationMethod.Default, 6 DeclarationUsage.Normal,0), 7 new VertexElement(0,24,DeclarationType.Float2,DeclarationMethod.Default, 8 DeclarationUsage.TextureCoordinate,0), 9 new VertexElement(0,32,DeclarationType.Float4,DeclarationMethod.Default, 10 DeclarationUsage.TextureCoordinate,1), 11 VertexElement.VertexDeclarationEnd 12 }; 13 14 decl = new VertexDeclaration(device,v);
顶点声明包括VertexElements向量,它描述了我使用的struct。说到struct,我们并不能用传统的顶点成员,因为我们想添加具有相关纹理的顶点。(我去,啥意思?),所以下面就是我们使用的struct。
1 public struct Vertex 2 { 3 Vector3 pos; 4 Vector3 nor; 5 float tu,tv; 6 float b1,b2,b3,b4; 7 public Vertex(Vector3 p,Vector3 n, 8 float u,float v,float B1,float B2, 9 float B3, float B4, bool normalize) 10 { 11 pos = p;nor = n;tu = u;tv = v; 12 b1=B1; b2=B2; b3=B3;b4 = B4; 13 float total = b1 + b2 + b3 + b4; 14 if ( normalize) 15 { 16 b1 /= total; 17 b2 /= total; 18 b3 /= total; 19 b4 /= total; 20 } 21 } 22 public static VertexFormats Format = 23 VertexFormats.Position | VertexFormats.Normal | 24 VertexFormats.Texture0 | VertexFormats.Texture1; 25 }
这个结构包包含了vector3类型的位置、顶点法线,float类型的坐标和blend变量。为了给这个struct分配成员,它同样需要一个constructor。它包括了format变量到顶点缓存。
为了使DirectX与effect交互,我们还需要一个东西,Effect类,下面我们创建一个effect。
1 String s = null; 2 3 effect = Effect.FromFile(device, @"..\..\simple.fx", null, 4 ShaderFlags.None, null, out s); 5 if ( s != null) 6 { 7 MessageBox.Show(s); 8 return; 9 }
因为我们不能debug一个shader,所以当你有个地方type incorrectly,你可能需要好久才能找到what’s wrong。避免我们使用重载的effect constructor,因为那将会引起编译错误。因此,除非你编译shader失败,重载将会成功。(我勒个去,什么玩意啊)。总之,如果这个地方有个问题,MESSAGEBOX将会显示这些错误。
Well 除了有关点的类之外,app同样还有地形类,这个类读取所有的位图数据并创建顶点缓存和索引缓存,我们规定地形的最小和最大值to the constructor。并且弄清能达到的最小和最大值。我们得到最亮和最暗的像素。每个位图的像素就是一个顶点,将顶点连接起来就会将四边形划分为三角形,形成一个三角形list。DRAW函数被调用的时候effect.beginscene已经被调用了。Effect.BeginScene告诉effect我们要开始接收东西渲染了。Effect.EndScene将会停止处理顶点数据。另外,the VertexDeclaration, VertexBuffer, and IndexBuffer are set, and theDrawPrimitives is finally called.。
为了更改你的shader中的全局变量,effect类包含setvalue函数,你可以传入你先更改的变量。
1 effect.SetValue("Texture1", t1);
另外我们这样创建effect句柄。
1 EffectHandle handle = effect.GetParameter(null,"ambient"); 2 effect.SetValue(handle,0.5f);
这样你就不用给函数传入数据了,更快哦。
So, variables that have to be assigned only once, can be assigned using the first way, but if a variable is changed multiple times, it would be better to use the second way. Note that this doesn’t only work with variables but also on techniques. With a technique, you choose which shaders you want to use; since an Effect file can contain multiple vertex- and pixelshaders, every HLSL file must have at least one technique. A technique is declared as follows:
1 technique TransformTexture 2 3 { 4 5 pass P0 6 7 { 8 9 VertexShader = compile vs_2_0 Transform(); 10 11 PixelShader = compile ps_2_0 TextureColor(); 12 13 } 14 15 }
A technique consists of one or more passes, in this case, only one with the name P0; and for each pass, you assign a Vertex- and Pixelshader. The compilation target is set (there are quite a lot of versions of HLSL compilers: 1_1, 2_0, 3_0. The higher the version numbers, the more possibilities they offer but the less support there will be among the graphics cards). Transform() and TextureColor() are the names of the vertex- and pixelshaders.
In a pass, you can also set RenderState values. If you would like to set the Device.RenderState.Cullmode toCull.None, you would have to insert this line as the first line of the pass:
1 CullMode = None;