Click here to Skip to main content
Click here to Skip to main content

Rendering terrains with Managed DirectX

, 5 Sep 2004
Rate this:
Please Sign up or sign in to vote.
With use of the High Level Shader Language, this article will help you create almost photorealistic terrains.

Sample Image - screen.jpg

Rendering terrains with Managed DirectX

There aren't many 3D games without a terrain. Creating and rendering a terrain, and the physics involved when, for example, driving on it with a car can be quite difficult. This article will demonstrate one technique to create a terrain: a simple technique to implement, but one that will look good.

In order to compile the code from this article, you’ll need the following:

  • A C# compiler, preferably Visual Studio .NET.
  • The DirectX 9.0c Software Development Kit.
  • A graphics card that supports Pixelshader 2.0 would come in handy, because else you would have to use a Reference device (Very, very slow: around 10 seconds per frame).

I expect you, as the reader, to understand the C# language, and some Managed DirectX experience would be appropriate.

The Beginning

The first thing that you’ll need when rendering a terrain would be a way to represent the terrain. Often, grayscale height maps are used. Since it’s an easy way, this article will use a grayscale height map. This is a height map I’ve used, but you can easily modify it with whatever program you like.

Sample screenshot

We will use two textures. One grass, the other some sort of rock or stone. The idea is that the higher a pixel from the terrain is, the less that pixel is grass. So, we sort of blend the two textures dependent of the height of the pixel.

The High Level Shader Language

In order to do this, we’ll use the High Level Shader Language. With this language, one can write their own pixelshaders and vertexshaders to have greater control over how the vertices and pixels are rendered. The HLSL is a language very similar to C. So, it wouldn’t take much time to learn that language. Here’s the vertexshader we’ll use to render the terrain:

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; 
}

It looks like an ordinary C method except for one thing: in addition to just the names, the input and output variables are also marked with a semantic. This links the vertexshader input with the vertexdata from the application and the vertexshader output with the pixelshader input. You’ll probably notice that the semantic TEXCOORD is used a lot; this is because the TEXCOORD semantic can be used to pass application specific data, a variable that doesn’t represent the position, normal etc. The HLSL contains a number of math intrinsic, e.g., mul(), normalize(). The complete list of them can be found at MSDN. For more information about the High Level Shader Language, I would recommend you to look at some websites, because there’s a lot to say about it and this article would get a little too long when I would go deeper on it here.

I‘ll describe briefly what the vertexshader does: first, the input position is multiplied with the world view projection matrix. So the vertex is transformed from object space to camera space. The input texture coordinate and the blend value are passed to the pixelshader. The normal is also transformed and then normalized. 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. This is the pixelshader:

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);
}

As you can see, the pixelshader takes almost every variable from the vertexshader, except for the POSITION0 variable, since that’s a variable that every vertexshader must output and our pixelshader wouldn’t use it. First, the two texture colors are calculated using the tex2D() intrinsic, note that this tex2D method doesn’t take a texture but a sampler. These colors are multiplied by the blend values and the addition of this two multiplied with the intensity of the light at that pixel, that value is returned. Instead of void, this pixelshader returns a float4 marked with the COLOR0 semantic, every pixelshader must return a variable marked as COLOR0 or it won’t compile.

Back to C#

In order to get your app communicating with the shaders, you must also have a VertexDeclaration. This will tell DirectX what the data in the VertexBuffer represents and how it relates to the input variables of the vertexshader. This is the VertexDeclaration used for the terrain:

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);

As you can see, this VertexDeclaration contains an array of VertexElements describing the struct that I use. Speaking of that struct, we can’t use one of the CustomVertex members because we want to have the possibility to add the proportions of the textures in relation to each other for every vertex. So, this is the struct we’ll use:

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; 
}

It contains a Vector3 for position, a Vector3 for a normal and, floats for the texture coordinates and the blend values. In order to assign to the members of this struct, it also contains a constructor. It also contains a Format variable to pass to the VertexBuffer.

In order to get DirectX communicating with the effect, we’ll need just one thing though: the Effect class. Create an effect as follows:

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;
}

By default, you can’t debug shaders, so when you’ve typed one thing incorrectly, you can spend hours looking for what’s wrong, not having a clue where to look. To avoid that we use the overload of the Effect constructor which has an out parameter through which the effect gives the CompilationErrors. Therefore, this overload succeeds even when it has failed compiling your shaders, only then the output string isn’t null anymore but the effect still is. So, if there is an error, a MessageBox is shown showing these errors.

Well, except for the class that contains the entry point, this app also contains a Terrain class. This class reads all the data from the bitmap and creates the VertexBuffer and IndexBuffer. We specify the height of the terrain by passing min and max values to the constructor. And to assure that min and max values get reached, we get the darkest and lightest pixel first. Every pixel from the bitmap will be a vertex, and the quads that arise when these vertices are connected will be split to triangles, forming a TriangleList. The Draw method assumes that effect.BeginScene() is already called when the Draw method is called. effect.BeginScene() tells the effect that it now will receive things to render, and effect.EndScene() that it should stop processing the VertexData. Furthermore, the VertexDeclaration, VertexBuffer, and IndexBuffer are set, and the DrawPrimitives is finally called.

In order to modify the values of the global variables of your shader, the Effect class contains the SetValue() method. You could pass in a string to identify the variable you want to change:

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);

This way you don’t have to pass a string to the method, so it will be faster. 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:

technique TransformTexture
{
    pass P0
    {
        VertexShader = compile vs_2_0 Transform();
        PixelShader = compile ps_2_0 TextureColor();
    }
}

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 to Cull.None, you would have to insert this line as the first line of the pass:

CullMode = None; 

The Keys

  • Escape: Quit
  • Up: Increase ambient light.
  • Down: Decrease ambient light.
  • Space: Change camera position.
  • Enter: Render normals.

Conclusion

Well, that's about all, I think. Of course, you can email or post all questions, suggestions, or tips.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Cr@zyIv@n

Netherlands Netherlands
No Biography provided

Comments and Discussions

 
QuestionMaximum Image Size? PinmemberOleksiy Vasylyev6-Nov-09 0:14 
Questionhoe to create a parser filter. Pinmemberamiya das27-Sep-07 0:44 
Generalsimple change Pinmemberozenc oz7-Jun-07 0:25 
GeneralPosition in database Pinmembersholt415-Oct-06 23:40 
GeneralRe: Position in database PinmemberRGBjimz14-Feb-07 8:38 
GeneralFramerate Pinmembersholt415-Oct-06 20:49 
QuestionMore than twoTexture? PinmemberRobertica12-Sep-06 2:50 
GeneralWonderful article... PinmemberWiti5-Nov-05 12:52 
GeneralRe: Wonderful article... PinmemberCr@zyIv@n6-Nov-05 5:17 
Generalquestion on texturecoord PinmemberGabriyel4-Nov-05 4:30 
GeneralRe: question on texturecoord PinmemberCr@zyIv@n5-Nov-05 0:12 
GeneralRe: question on texturecoord PinmemberGabriyel5-Nov-05 4:21 
Generalgreat article, but i get a runtime error Pinmemberdp141428-Oct-05 4:26 
GeneralRe: great article, but i get a runtime error PinmemberCr@zyIv@n5-Nov-05 0:21 
GeneralTexture blending Pinmembergeorge washinton13-Sep-05 9:21 
GeneralRe: Texture blending PinmemberCr@zyIv@n5-Nov-05 0:27 
Generalnot working when FullScreen = false Pinmembermathewww3-Aug-05 16:13 
GeneralRe: not working when FullScreen = false PinsussEric Cosky14-Aug-05 21:14 
GeneralCompile Error with June SDK Update Pinmemberrs1994834-Jul-05 7:54 
GeneralRe: Compile Error with June SDK Update Pinmemberwraithdrit19-Jul-05 5:22 
GeneralOne question ... Pinmembertinusaurier23-Jun-05 10:43 
GeneralRe: One question ... PinmemberCr@zyIv@n23-Jun-05 22:31 
GeneralRe: One question ... Pinmembertinusaurier25-Jun-05 8:19 
QuestionPixelshader 1.1? PinsussAnonymous9-Jun-05 10:57 
AnswerRe: Pixelshader 1.1? PinmemberCr@zyIv@n11-Jun-05 8:28 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.141015.1 | Last Updated 6 Sep 2004
Article Copyright 2004 by Cr@zyIv@n
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid