Detailing a Terrain With Multitexturing

  CU_MDX_Multitexture.zip (924.4 KiB, 3,271 hits)


  CUnitFramework.zip (101.1 KiB, 20,443 hits)

Extract the C-Unit Framework
inside the project (not solution) directory.

If you looked at the terrain from previous tutorials, you probably noticed that when you get close to the terrain, it becomes very blurry and pixelated. Even with a 1024×1024 or even a 2048×2048 terrain texture, if you view a section of the terrain up close, it will appear very blocky. Another way to texture a terrain is to blend the terrain texture with another detail texture. If we repeat the detail texture over smaller patches of the terrain, the terrain will have the detail of the detail texture and the color characteristics of the base texture.

Base texture
Base Texture
Detail texture
Detail Texture
Without Detail
Base Texture Alone
With Detail
Base Texture Blended With Detail Texture

To use texture blending, we have to make use of texture stages. Texture stages are used to perform separate blending operations to a pixel. Think of each stage as a blending station. We perform a blending operation in one stage and then pass the result on to the next stage or, if there are no more stages, to the rasterizer. When multiple stages are used, the blending flow between stages is often called the texture blending cascade because the blended color cascades through all the stages as shown below.

Texture stages

If you recall from my texturing tutorial, when we render textures, we have to assign a texture to a texture stage by calling Device.SetTexture. When more than one stage is used, we usually perform a blending operation on the texture in one stage and then pass the result on to the next stage so it can be blended with another texture in that stage.

I created both the base and detail textures with Terragen. You have to make sure the detail texture is tileable or else you’ll be able to see the borders of the detail texture when it repeats. A little run through Photoshop helps make the texture tileable.

In order to use two textures, we have to use a vertex format with two sets of texture coordinates. Unfortunately, There is no vertex format with two sets of texture coordinates in the CustomVertex namespace. Therefore, we have to create_ our own vertex format.

namespace CUnit.Vertex
{
/// Vertex with Position and two sets of texture coordinates
public struct Position2Textured
{
public float X;
public float Y;
public float Z;
public float Tu1;
public float Tv1;
public float Tu2;
public float Tv2;
public static readonly VertexFormats Format = VertexFormats.Position | VertexFormats.Texture2;
public static readonly VertexElement[] Declarator = new VertexElement[]
{
new VertexElement( 0, 0, DeclarationType.Float3,
DeclarationMethod.Default, DeclarationUsage.Position, 0 ),
new VertexElement( 0, 12, DeclarationType.Float2,
DeclarationMethod.Default, DeclarationUsage.TextureCoordinate, 0 ),
new VertexElement( 0, 20, DeclarationType.Float2,
DeclarationMethod.Default, DeclarationUsage.TextureCoordinate, 0 ),
VertexElement.VertexDeclarationEnd
};
public static readonly int StrideSize =
VertexInformation.GetDeclarationVertexSize( Declarator, 0 );

/// Creates a vertex with a position and two texture coordinates.
/// X position
/// Y position
/// Z position
/// First texture coordinate U
/// First texture coordinate V
/// Second texture coordinate U
/// Second texture coordinate V
public Position2Textured( float x, float y, float z, float u1, float v1, float u2, float v2 ) Toggle
{
X = x;
Y = y;
Z = z;
Tu1 = u1;
Tv1 = v1;
Tu2 = u2;
Tv2 = v2;
}


/// Creates a vertex with a position and two texture coordinates.
/// Position
/// First texture coordinate U
/// First texture coordinate V
/// Second texture coordinate U
/// Second texture coordinate V
public Position2Textured( Vector3 position, float u1, float v1, float u2, float v2 ) Toggle
{
X = position.X;
Y = position.Y;
Z = position.Z;
Tu1 = u1;
Tv1 = v1;
Tu2 = u2;
Tv2 = v2;
}


/// Gets and sets the position
public Vector3 Position Toggle
{
get
{
return new Vector3( X, Y, Z );
}
set
{
X = value.X;
Y = value.Y;
Z = value.Z;
}
}

}
}

Defining a custom vertex is simple. To create_ our new terrain vertex, we simply add an extra set of texture coordinates to our struct. When defining a vertex, we also need to specify a vertex format. Using the VertexFormats enumeration, we mix and match the members of VertexFormats until we create_ a format that matches the vertex we have defined. Since our vertex has a position and 2 sets of texture coordinates, we set the format to VertexFormats.Position | VertexFormats.Texture2. Vertex declarations should also specify a stride size, which tells DirectX how big a vertex is. To create_ the stride size, we first need to create_ and array of VertexElements. This array specifies what data is stored in the vertex. Once we have this declaration, we can calculate the stride size by calling VertexInformation.GetDeclarationVertexSize.

/// Generate PositionTextured vertices with custom height values
/// Number of vertices along the width
/// Number of vertices along the length
/// Height values
/// Array of PositionTextured vertices
public static Vertex.Position2Textured[] GeneratePosition2TexturedWithHeight( int verticesAlongWidth, int verticesAlongLength, int[] height )
{
if ( verticesAlongLength < 2 || verticesAlongWidth < 2 )
{
throw new Exception( “Can’t create_ a strip with the specified dimensions” );
}

Vertex.Position2Textured[] verts = new Vertex.Position2Textured[verticesAlongWidth * verticesAlongLength];
for ( int z = 0; z < verticesAlongLength; z++ )
{
for ( int x = 0; x < verticesAlongWidth; x++ )
{
// Center the grid in model space
float halfWidth = ((float)verticesAlongWidth – 1.0f) / 2.0f;
float halfLength = ((float)verticesAlongLength – 1.0f) / 2.0f;
Vertex.Position2Textured vertex;
vertex.X = (float)x – halfWidth;
vertex.Y = (float)height[z * verticesAlongLength + x];
vertex.Z = (float)z – halfLength;
vertex.Tu1 = (float)x / (verticesAlongWidth – 1);
vertex.Tv1 = (float)z / (verticesAlongLength – 1);
vertex.Tu2 = (float)x / 10.0f;
vertex.Tv2 = (float)z / 10.0f;
verts[z * verticesAlongLength + x] = vertex;
}
}
return verts;
}

When we generate the terrain vertices, we specify two sets of texture coordinates. The first set of texture coordinates span the entire terrain. This is the base texture. The second set of texture coordinates are the coordinates for the detail texture. The detail texture will span a square subset of vertices and be repeated over the terrain. What happens when the coordinates are out of the [0.0, 1.0] range? We can control what DirectX does with texture coordinates outside of the 0.0 to 1.0 range by specifying a TextureAddress mode. By default, DirectX uses the wrap texture address mode. In this mode, the texture is simply repeated for every integer interval. This is what we’ll use for the detail terrain texture so it is repeated over the surface of the terrain.

/// Renders the terrain.
/// Direct3D device
public void Render( Device device )
{
device.Transform.World = Transform;
device.SetTexture( 0, m_textureBase );
if ( m_textureDetail != null )
{
device.SetStreamSource( 0, m_vb, 0, Vertex.Position2Textured.StrideSize );
device.SetTexture( 1, m_textureDetail );
device.SetTextureState( 0, TextureStates.ColorArgument1, (int)TextureArgument.Texture );
device.SetTextureState( 0, TextureStates.ColorOperation, (int)TextureOperation.SelectArg1 );
device.SetTextureState( 1, TextureStates.ColorArgument1, (int)TextureArgument.Current );
device.SetTextureState( 1, TextureStates.ColorArgument2, (int)TextureArgument.Texture );
device.SetTextureState( 1, TextureStates.ColorOperation, (int)TextureOperation.AddSigned );
}
else
{
device.SetStreamSource( 0, m_vb, 0, PositionTextured.StrideSize );
}
device.Indices = m_ib;
device.VertexFormat = m_format;
device.DrawIndexedPrimitives( PrimitiveType.TriangleStrip, 0, 0, m_height.Length, 0, m_numIndices – 2);
}

To use texture blending, we first need to specify how the textures will be blended. We use Device.SetTextureStageState to specify which values to blend and how to blend them. In the above example, we first assign the texel color (TextureArgument.Texture) to one of the two available arguments for the blending equation (TextureStates.ColorArgument1). Then, we send this color value straight to stage 1 (TextureOperation.SelectArg1). In stage 1, we grab the color value that we passed from stage 0 (TextureStates.ColorArgument1, TextureArgument.Current). We then assign the texel color from stage 1 to the other argument of this stage (TextureStates.ColorArgument2, TextureArgument.TextureColor). Finally, we tell the sampler what to do with the two arguments. In this case, we add the two color values together and subtract 0.5 from the value to reduce the brightness (TextureStates.ColorOperation, TextureOperation.AddSigned). There are many other TextureStates options to select from when working with texture stages so make sure you read up about them in MSDN.

You’ll usually want to query the hardware for texture blending capabilities as older hardware may not support as many texture stages as more modern hardware. As you can see, texture blending improves the visual quality of the terrain by quite a bit.

Take note that the technique in this tutorial is the fixed-function multitexturing process and is somewhat old school. Nowadays, people are more likely to use pixel shaders to perform multitexturing.