Google
 

Thursday, June 28, 2007

Engine Design - Terrain (Bump/Normal Mapping)

Well, looks like my post on fire is on hold for now. So here is my bump/normal mapped terrain shaders and the modifications I made to my existing terrain object.

Changes to the Terrain Class
First thing I had to do was figure out how to add tangents to the terrain Vertex Element, so I put a post on the XNA Creators Club (no laughing at my game tag, I have been trying to change it but to no avail..) and Shawn Hargreaves replied with the solution. I think I may have applied it too literally as there is another post on there pointing to an article on ziggyware describing how to generate tangents and it is quite a lump of code! Also, I have tested my stuff with the tangent elements removed...and it still seems to work, odd. But what the hell I am new to all this and I got the bump mapping working and that is all I am bothered about. Though, if you can point me in the direction for creating the tangent data correctly then please post a comment :)

So, the code changes. I added a tangent vector3 to the Vertex Element.

       protected struct VertexMultitextured
       {
           public Vector3 Position;
           public Vector3 Normal;            
           public Vector4 TextureCoordinate;
           public Vector4 TexWeights;
           public Vector3 Tangent;

           public static int SizeInBytes = (3 + 3 + 3 + 4 + 4) * 4;
           public static VertexElement[] VertexElements = new VertexElement[]
            {
                new VertexElement( 0, 0, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Position, 0 ),
                new VertexElement( 0, sizeof(float) * 3, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Normal, 0 ),
                new VertexElement( 0, sizeof(float) * 6, VertexElementFormat.Vector4, VertexElementMethod.Default, VertexElementUsage.TextureCoordinate, 0 ),
                new VertexElement( 0, sizeof(float) * 10, VertexElementFormat.Vector4, VertexElementMethod.Default, VertexElementUsage.TextureCoordinate, 1 ),
                new VertexElement( 0, sizeof(float) * 14, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Tangent, 0 ),
            };
       }



Once this was added I had to calcualte the tangent data per vertex. So in my BuildTerrain method of the class I added a line to the loop that calulates the vertex normal. (second loop in the method)

           for (int x = 1; x < myWidth - 1; x++)
           {
               for (int y = 1; y < myHeight - 1; y++)
               {
                   Vector3 normX = new Vector3((myVertices[x - 1 + y * myWidth].Position.Y - myVertices[x + 1 + y * myWidth].Position.Y) / 2, 0, 1);
                   Vector3 normY = new Vector3(0, 1, (myVertices[x + (y - 1) * myWidth].Position.Y - myVertices[x + (y + 1) * myWidth].Position.Y) / 2);
                   myVertices[x + y * myWidth].Normal = normX + normY;
                   myVertices[x + y * myWidth].Normal.Normalize();

                   myVertices[x + y * myWidth].Tangent = myVertices[x - 1 + y * myWidth].Position - myVertices[x + 1 + y * myWidth].Position;                    
               }
           }



Now that was all set up I needed to get the normals passed to the class and store them. So I have added these fields and properties.

   private bool bumpOn;
   private string[] myBumpMapAssets;

   public bool BumpOn
   {
      get { return bumpOn; }
      set { bumpOn = value; }
   }



Now I altered the constructor to handle the new texture feed.

       public RCTerrain(string[] textureAssets,string[] bumpAssets,string heightAsset,string name):base(name)
       {          
           myHeightAsset = heightAsset;

           myAssets = new string[textureAssets.Length];
           for(int ass=0;ass<textureAssets.Length;ass++)
               myAssets[ass] = textureAssets[ass];

           myBumpMapAssets = new string[bumpAssets.Length];
           for (int ass = 0; ass < bumpAssets.Length; ass++)
               myBumpMapAssets[ass] = bumpAssets[ass];

           bumpOn = true;
       }



And added this loop to the LoadGraphicsContent method

           bumpMaps = new Texture2D[myBumpMapAssets.Length];
           for (int ass = 0; ass < myBumpMapAssets.Length; ass++)
               bumpMaps[ass] = myLoader.Load<Texture2D>(myBumpMapAssets[ass]);



Will all that set up I now altered the Render method to use the new textures

           if (effect.Parameters["bumpOn"] != null)
               effect.Parameters["bumpOn"].SetValue(bumpOn);

           if (effect.Parameters["tileSizeMod"] != null)
               effect.Parameters["tileSizeMod"].SetValue(1f);

           if (effect.Parameters["BumpMap0"] != null)
               effect.Parameters["BumpMap0"].SetValue(bumpMaps[0]);
           if (effect.Parameters["BumpMap1"] != null)
               effect.Parameters["BumpMap1"].SetValue(bumpMaps[1]);
           if (effect.Parameters["BumpMap2"] != null)
               effect.Parameters["BumpMap2"].SetValue(bumpMaps[2]);
           if (effect.Parameters["BumpMap3"] != null)
               effect.Parameters["BumpMap3"].SetValue(bumpMaps[3]);



Shaders
Now to the shaders, the first shader was Reimers origional terrain shader the only change I have really made is to add the bump/normal mapping. The second shader is mine, I have put a few more comments in mine so it should be a bit more readable.

Reimers Shader modified by me

//////////////////////////////////////////////////////////////////
//                                                                //
//    This terrain shader was origionaly written by                //
//  Reimer (http://www.riemers.net/)                            //
//  I have modified it so it now had a bump map effect            //
//                                                                //
//  26/06/2007 C.Humphrey (http://randomchaosuk.blogspot.com/)    //
//                                                                //
//////////////////////////////////////////////////////////////////

struct MultiTexVertexToPixel
{
   float4 Position         : POSITION;    
   float4 Color            : COLOR0;
   float3 Normal            : TEXCOORD0;
   float4 TextureCoords    : TEXCOORD1;
   float4 LightDirection    : TEXCOORD2;
   float4 TextureWeights    : TEXCOORD3;
   float    Depth            : TEXCOORD4;
   float shade : TEXCOORD5;
   float3 lView : TEXCOORD6;
};


struct MultiTexPixelToFrame
{
   float4 Color : COLOR0;
};

//------- Constants --------
float4x4 World : World;
float4x4 wvp : WorldViewProjection;
float3 LightPosition : LightDirection;
float Ambient : Ambient;
float3 EyePosition : CAMERAPOSITION;

Texture SandTexture;
sampler SandTextureSampler = sampler_state
{
   texture = <SandTexture> ;
   magfilter = LINEAR;
   minfilter = LINEAR;
   mipfilter=LINEAR;
   AddressU = mirror;
   AddressV = mirror;
};

Texture GrassTexture;
sampler GrassTextureSampler = sampler_state
{
   texture = <GrassTexture> ;
   magfilter = LINEAR;
   minfilter = LINEAR;
   mipfilter=LINEAR;
   AddressU = mirror;
   AddressV = mirror;
};

Texture RockTexture;
sampler RockTextureSampler = sampler_state
{
   texture = <RockTexture> ;
   magfilter = LINEAR;
   minfilter = LINEAR;
   mipfilter=LINEAR;
   AddressU = mirror;
   AddressV = mirror;
};

Texture SnowTexture;
sampler SnowTextureSampler = sampler_state
{
   texture = <SnowTexture>;
   magfilter = LINEAR;
   minfilter = LINEAR;
   mipfilter=LINEAR;
   AddressU = mirror;
   AddressV = mirror;
};

texture BumpMap0;
sampler BumpMap0Sampler = sampler_state
{
   Texture = <BumpMap0>;
   MinFilter = Linear;
   MagFilter = Linear;
   MipFilter = Linear;
   AddressU = mirror;
   AddressV = mirror;
};
texture BumpMap1;
sampler BumpMap1Sampler = sampler_state
{
   Texture = <BumpMap1>;
   MinFilter = Linear;
   MagFilter = Linear;
   MipFilter = Linear;
   AddressU = mirror;
   AddressV = mirror;
};

texture BumpMap2;
sampler BumpMap2Sampler = sampler_state
{
   Texture = <BumpMap2>;
   MinFilter = Linear;
   MagFilter = Linear;
   MipFilter = Linear;
   AddressU = mirror;
   AddressV = mirror;
};

texture BumpMap3;
sampler BumpMap3Sampler = sampler_state
{
   Texture = <BumpMap3>;
   MinFilter = Linear;
   MagFilter = Linear;
   MipFilter = Linear;
   AddressU = mirror;
   AddressV = mirror;
};

MultiTexVertexToPixel MultiTexturedVS( float4 inPos : POSITION, float3 inNormal: NORMAL, float4 inTexCoords: TEXCOORD0, float4 inTexWeights: TEXCOORD1, float3 Tangent : TANGENT)
{
   MultiTexVertexToPixel Output = (MultiTexVertexToPixel)0;
   
   Output.Position = mul(inPos, wvp);
   Output.Normal = mul(normalize(inNormal), World);
   Output.TextureCoords = inTexCoords;
   Output.LightDirection.xyz = LightPosition;
   Output.LightDirection.w = 1;
   Output.TextureWeights = inTexWeights;
   Output.Depth = Output.Position.y;
   Output.shade = saturate(saturate(dot(Output.Normal, normalize(Output.LightDirection))) + Ambient);    
   
   float3x3 worldToTangentSpace;
   worldToTangentSpace[0] = mul(Tangent,World);
   worldToTangentSpace[1] = mul(cross(Tangent,inNormal),World);
   worldToTangentSpace[2] = mul(inNormal,World);
   
   Output.lView = mul(worldToTangentSpace,EyePosition - Output.Position);    
   
   return Output;    
}

MultiTexPixelToFrame MultiTexturedPS(MultiTexVertexToPixel PSIn)
{
   MultiTexPixelToFrame Output = (MultiTexPixelToFrame)0;
 
   float blendDistance = 30;
   float blendWidth = 50;
   float blendFactor = clamp((PSIn.Depth-blendDistance)/blendWidth, 0, 1);

   float4 farColor = 0;
   farColor = tex2D(SandTextureSampler, PSIn.TextureCoords)*PSIn.TextureWeights.x;
   farColor += tex2D(GrassTextureSampler, PSIn.TextureCoords)*PSIn.TextureWeights.y;
   farColor += tex2D(RockTextureSampler, PSIn.TextureCoords)*PSIn.TextureWeights.z;
   farColor += tex2D(SnowTextureSampler, PSIn.TextureCoords)*PSIn.TextureWeights.w;

   float4 nearColor;
   float2 nearTextureCoords = PSIn.TextureCoords*3;
   nearColor = tex2D(SandTextureSampler, nearTextureCoords)*PSIn.TextureWeights.x;
   nearColor += tex2D(GrassTextureSampler, nearTextureCoords)*PSIn.TextureWeights.y;
   nearColor += tex2D(RockTextureSampler, nearTextureCoords)*PSIn.TextureWeights.z;
   nearColor += tex2D(SnowTextureSampler, nearTextureCoords)*PSIn.TextureWeights.w;
 
   float lightingFactor = saturate(saturate(dot(PSIn.Normal, normalize(PSIn.LightDirection))) + Ambient);      
   
   float4 LightDir = normalize(PSIn.LightDirection);
   float3 ViewDir = normalize(PSIn.lView);

   float3 Normal;
   Normal = tex2D(BumpMap0Sampler,nearTextureCoords)*PSIn.TextureWeights.x;
   Normal += tex2D(BumpMap1Sampler,nearTextureCoords)*PSIn.TextureWeights.y;
   Normal += tex2D(BumpMap2Sampler,nearTextureCoords)*PSIn.TextureWeights.z;
   Normal += tex2D(BumpMap3Sampler,nearTextureCoords)*PSIn.TextureWeights.w;
   
   Normal = 2 * Normal - 1.0;    
   float4 Color = nearColor + farColor;
   
   float Diffuse = saturate(dot(Normal,LightDir));    
   float Reflect = normalize(2 * Diffuse * Normal - LightDir);    
   float Specular = min(pow(saturate(dot(Reflect,ViewDir)),3),Color.w);
   
   Output.Color = (0.2 * ((farColor*blendFactor + nearColor*(1-blendFactor))*4) * (Diffuse + Specular)) * (PSIn.shade*3);    
   
   return Output;
}

technique MultiTextured
{
   pass Pass0
   {
       VertexShader = compile vs_2_0 MultiTexturedVS();
       PixelShader = compile ps_2_0 MultiTexturedPS();
   }    
}



My Shader

//////////////////////////////////////////////////////////////////////////////
//                                                                            //
//    NemoKradBumpTerrain.fx Terrain shader by C.Humphrey 02/05/2007          //
//                                                                            //
//    This shader is based on terrain shaders by Riemer and Frank Luna.       //
//  Riemer: http://www.riemers.net                                            //
//  Frank Luna: http://www.moon-labs.com                                    //
//                                                                            //
//    http://randomchaos.co.uk                                                //
//    http://randomchaosuk.blogspot.com                                        //
//                                                                            //
// 25/06/2007 - Adde bumpmap functionlaity                                    //
//                                                                            //
//////////////////////////////////////////////////////////////////////////////

float4x4 wvp : WorldViewProjection;
float4x4 world : World;
float3 LightPosition : LightDirection;
float3 EyePosition : CAMERAPOSITION;

// Do we want to use bump map feature?
bool bumpOn = true;

// Texture size moifier 1 = no change.
float tileSizeMod = 1;

// Terrain Textures.
texture  LayerMap0;
texture  LayerMap1;
texture  LayerMap2;
texture  LayerMap3;

// Terrain Normals for above texture.
texture BumpMap0;
texture BumpMap1;
texture BumpMap2;
texture BumpMap3;

// Normal samplers
sampler BumpMap0Sampler = sampler_state
{
   Texture = <BumpMap0>;
   MinFilter = Linear;
   MagFilter = Linear;
   MipFilter = Linear;
   AddressU = mirror;
   AddressV = mirror;
};

sampler BumpMap1Sampler = sampler_state
{
   Texture = <BumpMap1>;
   MinFilter = Linear;
   MagFilter = Linear;
   MipFilter = Linear;
   AddressU = mirror;
   AddressV = mirror;
};

sampler BumpMap2Sampler = sampler_state
{
   Texture = <BumpMap2>;
   MinFilter = Linear;
   MagFilter = Linear;
   MipFilter = Linear;
   AddressU = mirror;
   AddressV = mirror;
};

sampler BumpMap3Sampler = sampler_state
{
   Texture = <BumpMap3>;
   MinFilter = Linear;
   MagFilter = Linear;
   MipFilter = Linear;
   AddressU = mirror;
   AddressV = mirror;
};

// Texture Samplers
sampler LayerMap0Sampler = sampler_state
{
   Texture   = <LayerMap0>;
   MinFilter = LINEAR;
   MagFilter = LINEAR;
   MipFilter = LINEAR;
   AddressU  = WRAP;
   AddressV  = WRAP;
};

sampler LayerMap1Sampler = sampler_state
{
   Texture   = <LayerMap1>;
   MinFilter = LINEAR;
   MagFilter = LINEAR;
   MipFilter = LINEAR;
   AddressU  = WRAP;
   AddressV  = WRAP;
};

sampler LayerMap2Sampler = sampler_state
{
   Texture   = <LayerMap2>;
   MinFilter = LINEAR;
   MagFilter = LINEAR;
   MipFilter = LINEAR;
   AddressU  = WRAP;
   AddressV  = WRAP;
};
sampler LayerMap3Sampler = sampler_state
{
   Texture   = <LayerMap3>;
   MinFilter = LINEAR;
   MagFilter = LINEAR;
   MipFilter = LINEAR;
   AddressU  = WRAP;
   AddressV  = WRAP;
};

// Vertex Shader input structure.
struct VS_INPUT
{
   float4 posL         : POSITION0;
   float3 normalL      : NORMAL0;
   float4 tiledTexC    : TEXCOORD0;
   float4 TextureWeights : TEXCOORD1;
   float3 Tangent : TANGENT;
};

// Vertex Shader output structure/Pixel Shaer input structure
struct VS_OUTPUT
{
   float4 posH         : POSITION0;
   float  shade        : TEXCOORD0;
   float4 tiledTexC    : TEXCOORD1;  
   float4 TextureWeights : TEXCOORD2;    
   float4 Light : TEXCOORD3;
   float3 lView : TEXCOORD4;
};

// Vetex Shader
VS_OUTPUT BumpVS(VS_INPUT input)
{
   // Clean the output structure.
   VS_OUTPUT Out = (VS_OUTPUT)0;
       
   // Calculate tangent space.
   float3x3 worldToTangentSpace;
   worldToTangentSpace[0] = mul(input.Tangent,world);
   worldToTangentSpace[1] = mul(cross(input.Tangent,input.normalL),world);
   worldToTangentSpace[2] = mul(input.normalL,world);    
   
   // Get world pos for texture and normal.
   float4 PosWorld = mul(input.posL,world);    
   
   // Get light position.
   Out.Light.xyz = LightPosition;
   Out.Light.w = 1;
   
   // Set position for pixel shader
   Out.posH  = mul(input.posL, wvp);        
   
   // Set lighting.
   Out.shade = saturate(saturate(dot(input.normalL, normalize(LightPosition))));    
   
   // Set view direction for normals.
   Out.lView = mul(worldToTangentSpace,EyePosition - Out.posH);    
   
   // Set tile TexCoord.
   Out.tiledTexC = input.tiledTexC * tileSizeMod;
       
   // Set Texture weight.
   Out.TextureWeights = input.TextureWeights;    
   
   return Out;
}
// Output to screen.
struct PixelToFrame
{
   float4 Color : COLOR0;
};

// Pixel shader.
PixelToFrame BumpPS(VS_OUTPUT input) : COLOR
{
   // Clean output structure.
   PixelToFrame Out = (PixelToFrame)0;    
   
   // Get pixel color.
   float4 Col = tex2D(LayerMap0Sampler, input.tiledTexC)*input.TextureWeights.x;
   Col += tex2D(LayerMap1Sampler, input.tiledTexC)*input.TextureWeights.y;
   Col += tex2D(LayerMap2Sampler, input.tiledTexC)*input.TextureWeights.z;
   Col += tex2D(LayerMap3Sampler, input.tiledTexC)*input.TextureWeights.w;
   
   // Set light directon amd view direction.
   float4 LightDir = normalize(input.Light);
   float3 ViewDir = normalize(input.lView);
   
   // Get prominent normal.    
   float2 nearTextureCoords = input.tiledTexC*3;
   float3 Normal;
   Normal = tex2D(BumpMap0Sampler,nearTextureCoords)*input.TextureWeights.x;
   Normal += tex2D(BumpMap1Sampler,nearTextureCoords)*input.TextureWeights.y;
   Normal += tex2D(BumpMap2Sampler,nearTextureCoords)*input.TextureWeights.z;
   Normal += tex2D(BumpMap3Sampler,nearTextureCoords)*input.TextureWeights.w;    
   Normal = 2 * Normal - 1.0;    
       
   // Set diffuse, reflection and specular effect for Normal.
   float Diffuse = saturate(dot(Normal,LightDir));    
   float Reflect = normalize(2 * Diffuse * Normal - LightDir);
   float Specular = min(pow(saturate(dot(Reflect,ViewDir)),3),Col.w);    
   
   float4 final;
   
   // Do color calculation depending if bump feature is on or off.
   if(bumpOn)
       final = (0.2 * Col * (Diffuse + Specular)) * (input.shade * 12);
   else
       final = Col * input.shade;

   Out.Color = final;
   
   return Out;
}

technique Terrain_MultiTex
{
   pass P0
   {
       vertexShader = compile vs_2_0 BumpVS();
       pixelShader  = compile ps_2_0 BumpPS();
   }    
}



Screen Shots
Right here are a number of screen shots from different angles so you can see how the two shaders render, I have also put up my FPS counter so you can see the performance.
In each set I do my best to get the same camera position. Oh and I don't want you to think that Reimers shader is the worst of the two, my shader is quicker because it does the job in a much cruder way. I guess you have to go for speed or beuty, unless your machine is supper fast.

Riemers with Bump/Normal mapping Pic 1


My shader Pic 1


Riemers with Bump/Normal mapping Pic2


My shader Pic 2


Riemers with Bump/Normal mapping Pic 3


My shader Pic 3


Riemers with Bump/Normal mapping Pic 4


My shader Pic 4


Terrain Up Close
Here are images of the terrain up close. I think you will agree using Remiers terrain render is better looking, well especialy now it has bump mapping :)

Riemers with Bump/Normal mapping Sand


My shader Sand


Riemers with Bump/Normal mapping Grass


My shader Grass


Riemers with Bump/Normal mapping Stone


My shader Stone


Riemers with Bump/Normal mapping Snow


My shader Snow



Textures and Normals
Here are the textures used in my examples, they are not the origional DDS files as this blog wont host them, all you have to do is go to Reimers turorial, and you can down load the textures there, all you have to do then is use PhotoShop and NVIDIAs Normal tool to generate the normal. If I have time I will put a zip up on my server and link to it from here.

Sand & Normal


Grass & Normal


Stone & Normal


Snow & Normal


By all means please, if you optomize these shaders ot even the terrain class I would love to know what you did. I am planning on doing terrain culling in the future and if I ever get time to get my head into it ROAM terrain.

PHEW! That has to be my biggest post!

6 comments:

  1. Wow you do amazing work. I've been trying to find a good way to put Reimer's tutorials to use in a well-rounded engine and this is just perfect! Are you planning on putting your entire engine up somewhere for general-purpose game creation or will you eventually sell this engine? I'd love to get your code for use in my game ideas, even if it was just a built game library that I could reference.

    ReplyDelete
  2. Hi Ryan,

    Well I am going to put the whole engine up as open source for anyone to do with as they please. You can get 80% or so of the engine from the Hazy Mind site (link off the main blog), but I will post this engine in it's entirety once I have completed the documentation of it in this blog.

    Glad you are enjoying what I am doing :)

    ReplyDelete
  3. I took a different tack:

    public struct VertexMultitextured
    {
    public Vector3 Position;
    public Vector3 Normal;
    public Vector4 TextureCoordinate;
    public Vector4 TexWeights;
    public float Slope;

    public static int SizeInBytes = (3 + 3 + 4 + 4 + 1) * sizeof(float);
    public static VertexElement[] VertexElements = new VertexElement[]
    {
    new VertexElement( 0, 0, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Position, 0 ),
    new VertexElement( 0, sizeof(float) * 3, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Normal, 0 ),
    new VertexElement( 0, sizeof(float) * 9, VertexElementFormat.Vector4, VertexElementMethod.Default, VertexElementUsage.TextureCoordinate, 0 ),
    new VertexElement( 0, sizeof(float) * 13, VertexElementFormat.Vector4, VertexElementMethod.Default, VertexElementUsage.TextureCoordinate, 1 ),
    new VertexElement( 0, sizeof(float) * 17, VertexElementFormat.Single, VertexElementMethod.Default, VertexElementUsage.TextureCoordinate, 2 ),
    };
    }

    The slope is calculated with the normals:

    float angle = Vector3.Dot(vertices[i].Normal, Vector3.Up);
    vertices[i].Slope = MathHelper.Clamp((0.9f - angle) * 5.0f, 0.0f, 1.0f);

    The HLSL has 4 more textures, each with its own sampler. The idea is to have flat and steep slopes have different textures.

    Combining the two methods should be strightforward.

    ReplyDelete
  4. Armigus,

    Cool, you might want to post this on the new blog. I have not posted XNA on here for a long time and will (eventually) be made into a DX9 C/C++ blog.

    Nice job though :)

    ReplyDelete
  5. Hey Charles,

    You might want to look at using a Sobel filter to make the normal map for the terrain.

    It is a convultion filter (you can even code it in HLSL if you want - but you need to do texel lookups).

    If you email me at jonathand *dot) za *at) gmail *dot) com I can send you some code that does a simple Sobel operation.

    ReplyDelete
  6. @moitoius,

    Just mailed you and it failed. Will mail you on the address for your profile.

    ReplyDelete

Note: Only a member of this blog may post a comment.