Entries:
Comments:
Posts:

Loading User Information from Channel 9

Something went wrong getting user information from Channel 9

Latest Achievement:

Loading User Information from MSDN

Something went wrong getting user information from MSDN

Visual Studio Achievements

Latest Achievement:

Loading Visual Studio Achievements

Something went wrong getting the Visual Studio Achievements

Ask the ZMan:Applying Textures, Part 1

  Part 1 of a three part series explaining the addition of textures to DirectX Primitive shapes.This column covers how texture coordinates are stored inside the mesh object, the relationship between the texture coordinates and the texture, and the most useful of the texture sampling states.
The Z Buffer

Difficulty: Intermediate
Time Required: 1-3 hours
Cost: Free
Software: Visual Studio Express Editions
Hardware:
Download:

Welcome to Ask The ZMan, a new column on the Coding4Fun game portal. The ZMan is here to solve your Managed DirectX programming problems. If you have a question for the ZMan send it to zman@thezbuffer.com

Since Coding4Fun has only just launched, there aren't any specific questions to answer so I thought I would start with a question that occurs frequently on the Managed DirectX newsgroup:

"I've created my first Managed DirectX project and can successfully draw shapes created using the Mesh.{shape} primitives. However, when I try to apply a texture to them a) nothing happens b) the shapes go black or c) the shapes disappear. How can I add a texture to the DirectX primitive shapes?"

This is often a stumbling block for beginners in managed DirectX. The texture tutorial (tutorial 5) in the SDK creates a cylinder mesh in code rather than using Mesh.Cylinder and there are not many examples of how to add texture to an existing mesh, especially in managed code.

The first thing to point out is that, for the most part, manually adding textures is a futile thing to do. In the today's games there are very few boxes, cylinders, tori or teapots. All of the objects that are used in modern games are created by artists in modeling programs like Maya. Designers also create the textures and texture the models. By the time the programmer gets the artwork they have both a shape and a texture. As you will also see in future columns, the more complex the shape becomes the less likely you can add a texture programmatically. Of course, there is an exception to this rule: you can add programmatic textures, for example representing wood and marble with mathematical equations, but this requires you to dip into the world of pixel shaders.

However, it IS a worthwhile exercise to learn about the innards of the Mesh object, how textures connect to objects, and which settings you can apply to textures to to change the way they look.

To start, download textures0.zip using the link at the top of this article. You will need Beta2 of Visual C# Express and the April 2005 DirectX SDK. You can get Visual C# 2005 Express Edition Beta 2 by visiting http://msdn.microsoft.com/express. You can also use other Express Editions depending on the language of your choice, but our demonstration will utilize C#.


Figure 1. Rendering of a simple box in Managed DirectX

How Did You Create This Project?
This project began life as the EmptyProject from the Managed DirectX sample browser. I removed the effects framework code and some of the extraneous UI elements. Then I added Mesh and Material creation in OnCreateDevice() and disposal in OnDestroyDevice(), and light creation in OnResetDevice(). There's also a slight change to the camera eye point in OnCreateDevice() to give it a slightly more interesting point of view. In OnFrameMove(),  I set the view and projection matrices from the camera class in the framework which allows you to use the right mouse button for moving the camera. Finally, I turned on the lights - including some ambient lighting - and draw the box in the correct place with the correct material in the OnFrameRender() function. 

What's Inside a Mesh?
Put a break point on the Mesh.Box() call and run your code. Press F10 to execute that line of code and take a look at the MyBox variable using the watch window (Figure 2).


Figure 2. The watch window of a Mesh.Box() object

Interesting things to remember:

  • 12 faces: In 3-D graphics programming, everything is made up of triangles. Each face of the box takes 2 triangles, there are 6 sides to the box, so there are 12 faces needed to draw the box.

  • 24 vertices: Because each face needs to have its own normals, each of the 6 faces of the box has to have its own set of 4 vertices, totaling 24.

  • The VertexFormat is PositionNormal: Each vertex stores just the x,y,z position in space and the normal for that vertex.

  • VertexBuffer: The actual vertices are stored here. However, this buffer cannot be opened and browsed because the values are actually stored in an unmanaged part of DirectX.

Hit F10 one more time and look in the output or immediate window for the debug output. The DumpMesh function converts the unmanaged vertex buffer to a managed array and then writes the coordinates out to debug. You should see something like this:

Vertices
Vert 0: -1, -1, -1
Vert 1: -1, -1, 1
Vert 2: -1, 1, 1
...
Vert 23: 1, -1, -1

If you were to sketch those coordinates out, you would find that each set of 4 describes one of the faces of the box.

How Does a Mesh Know What Texture to Draw Where?
Imagine that Figure 3 shows one of the faces of the cube and the crate texture I want to place on it. Besides the obvious "fill the whole face with the texture," there are many ways that the texture can be placed, for example the texture can be:

  • Rotated by 10 degrees
  • Scaled by 1/4 so that there are 16 copies of the texture tiled on each face
  • Roated and scales in any combination with different angles and scales

Each vertex on each triangle has to be mapped to a particular pixel on the texture so the DirectX knows what color to draw. In Figure 3, I have chosen to map the vertices of the box face in such a way that it picks up a rotated area in the top left of the texture. The resultant face when rendered would look something like the final picture:


Figure 3. Mapping vertices to a texture

Textures are mapped through the use of texture coordinates, which are also known as u,v coordinates. Each vertex in the triangle has a u,v coordinate which corresponds to a point on the texture. As you can see from Figure 3, texture coordinates floating point values that always range from 0.0 to 1.0. In this example the u,v coordinates for the top left vertex of the face are probably (0.0, 0.9) and the lower right vertex is (0.4, 0.7).

Adding Texture Coordinates to the Box
You will remember that the VertexFormat of the box was PostitionNormal. There are no texture coordinates in there. Before I can add them,  I need to modify the Mesh so there is space for the u,v coordinates. This is done through the use of the Mesh.Clone function.

Add declarations for the TexturedBox and the Texture:

Visual C#

private Mesh TexturedBox;  
private Texture texture = null;

Visual Basic

Private TexturedBox As Mesh
Private texture As Direct3D.Texture = Nothing

Create a function called TextureBox(). This function takes the untextured mesh and adds the texture, so add a call to that after the DumpInfo() call:

Visual C#

MyBox = Mesh.Box(e.Device, 2.0f, 2.0f, 2.0f);
DumpInfo(MyBox);
TexturedBox = TextureBox(MyBox);

Visual Basic

MyBox = Mesh.Box(e.Device, 2.0F, 2.0F, 2.0F)
DumpInfo(MyBox)
TexturedBox = TextureBox(MyBox)

Make sure to tidy it up in OnDestroyDevice():

Visual C#

TexturedBox.Dispose();

Visual Basic

TexturedBox.Dispose()

Add code to render it after MyBox.DrawSubset() in OnFrameRender(). I will draw this cube off to the right of the first one so that you can compare them:

Visual C#

MyBox.DrawSubset(0);
device.Transform.World = Matrix.Translation(1.2f, 0f, 0f);
TexturedBox.DrawSubset(0);

Visual Basic

MyBox.DrawSubset(0)
device.Transform.World = Matrix.Translation(-1.2F, 0.0F, 0.0F)
TexturedBox.DrawSubset(0)

Finally, add the TextureBox function somewhere in the textures class:

Visual C#

private Mesh TextureBox(Mesh box)
{
//First make sure this is a box
//There's no quick test, so we will count the vertices and faces
Debug.Assert(box.NumberVertices == 24 && box.NumberFaces == 12,
"TextureBox called with a mesh which is not box like");
//Take a copy of the Mesh, adding space for texture coordinates
Mesh TexturedBox = box.Clone(box.Options.Value,
VertexFormats.Position | VertexFormats.Normal | VertexFormats.Texture0 |
VertexFormats.Texture1, box.Device);
return TexturedBox;
}

Visual Basic

Private Function TextureBox(ByVal box As Mesh) As Mesh
'First make sure this is a box
'There's no quick test, so we will count the vertices and faces
Debug.Assert(box.NumberVertices = 24 AndAlso box.NumberFaces = 12, _
"TextureBox called with a mesh which is not box like")

'Take a copy of the Mesh, adding space for texture coordinates
Dim TexturedBox As Mesh = box.Clone(box.Options.Value, _
VertexFormats.Position Or VertexFormats.Normal Or _
VertexFormats.Texture0 Or VertexFormats.Texture1, _
box.Device)

Return SetTextureCoordinates(TexturedBox)
End Function

After some sanity checking of the input mesh, this function clones the mesh that is passed in but tells DirectX to change the vertex format from PositionNormal to PositionNormalTextured. Notice the logical ORing together of the flags that make up the various possible parts of a vertex. See MSDN VertexFormats and MSDN CustomVertex for the full lists.

If you run the program now you will see 2 identical cubes (Figure 4). I have added space for the texture coordinates but I haven't set them to reasonable values or loaded a texture.


Figure 4. Identical cubes without textures

Load the texture in OnResetDevice():

Visual C#

//Load the texture
texture = TextureLoader.FromFile(e.Device,
System.Windows.Forms.Application.StartupPath +
@"\..\..\media\woodcrate.bmp");

Visual Basic

'Load the texture
texture = TextureLoader.FromFile(e.Device, _
System.Windows.Forms.Application.StartupPath _
& "\..\..\media\woodcrate.bmp")

Tidy up in OnLostDevice():

Visual C#

if (texture !=null) texture.Dispose();

Visual Basic

If (Me.texture IsNot Nothing) Then
Me.texture.Dispose()
End If

Change the rendering code to set the correct texture for each box. Notice how I use null in the SetTexture() call to say "do not use a texture" for the first box. Try commenting out that line later to see the effect:

Visual C#

//Render our scene
device.SetTexture(0, null);
device.Transform.World = Matrix.Translation(-1.2f, 0f, 0f);
MyBox.DrawSubset(0);
device.SetTexture(0, texture);
device.Transform.World = Matrix.Translation(1.2f, 0f, 0f);
TexturedBox.DrawSubset(0);

Visual Basic

With device
.SetTexture(0, Nothing)
.Transform.World = Matrix.Translation(-1.2F, 0.0F, 0.0F)
MyBox.DrawSubset(0)
.SetTexture(0, texture)
.Transform.World = Matrix.Translation(1.2F, 0.0F, 0.0F)
TexturedBox.DrawSubset(0)
End With

Finally, add the updated TextureBox function and its helper SetTextureCoordinates. This function locks the vertex buffer so that it can safely make changes and then adds identical texture coordinates to each of the 6 box faces. The texture coordinates I have chosen are the simple "cover the whole face" ones, which are (0,0) (1,0) (1,1) and (0,1) representing the 4 corners of the texture. Notice that the u,v coordinates are properties called tu and tv in DirectX:

Visual C#

private Mesh TextureBox(Mesh box)
{
//First make sure this is a box
//There's no quick test, so we will count the vertices and faces
Debug.Assert(box.NumberVertices == 24 && box.NumberFaces == 12,
"TextureBox called with a mesh which is not box like");
//Take a copy of the Mesh, adding space for texture coordinates
Mesh TexturedBox = box.Clone(box.Options.Value,
VertexFormats.Position | VertexFormats.Normal | VertexFormats.Texture0 |
VertexFormats.Texture1,box.Device);
return SetTextureCoordinates(TexturedBox);
}

private Mesh SetTextureCoordinates(Mesh TexturedBox)
{
//The following code makes the assumption that the vertices of the box are
// generated the same way as they are in the April 2005 SDK
using (VertexBuffer vb = TexturedBox.VertexBuffer)
{
CustomVertex.PositionNormalTextured[] verts =
(CustomVertex.PositionNormalTextured[])vb.Lock(0,
typeof(CustomVertex.PositionNormalTextured), LockFlags.None,
TexturedBox.NumberVertices);
try
{
for (int i = 0; i < verts.Length; i += 4)
{
verts[i + 0].Tu = 0.0f;
verts[i + 0].Tv = 0.0f;
verts[i + 1].Tu = 1.0f;
verts[i + 1].Tv = 0.0f;
verts[i + 2].Tu = 1.0f;
verts[i + 2].Tv = 1.0f;
verts[i + 3].Tu = 0.0f;
verts[i + 3].Tv = 1.0f;
}
}
finally
{
vb.Unlock();
}
}
return TexturedBox;
}

Visual Basic

Private Function TextureBox(ByVal box As Mesh) As Mesh
'First make sure this is a box
'There's no quick test, so we will count the vertices and faces
Debug.Assert(box.NumberVertices = 24 AndAlso box.NumberFaces = 12, _
"TextureBox called with a mesh which is not box like")

'Take a copy of the Mesh, adding space for texture coordinates
Dim TexturedBox As Mesh = box.Clone(box.Options.Value, _
VertexFormats.Position Or VertexFormats.Normal Or _
VertexFormats.Texture0 Or VertexFormats.Texture1, _
box.Device)

Return SetTextureCoordinates(TexturedBox)
End Function

Private Function SetTextureCoordinates(ByVal TexturedBox As Mesh) As Mesh
'The following code makes the assumption that the vertices of the box
' are generated the same way as they are in the April 2005 SDK
Using vb As VertexBuffer = TexturedBox.VertexBuffer
Dim verts As CustomVertex.PositionNormalTextured() = _
DirectCast(vb.Lock(0, GetType(CustomVertex.PositionNormalTextured), _
LockFlags.None, TexturedBox.NumberVertices), _
CustomVertex.PositionNormalTextured())

Try
Dim i As Integer = 0
Do While (i < verts.Length)
verts(i).Tu = 0.0!
verts(i).Tv = 0.0!
verts((i + 1)).Tu = Me.NumXTiles
verts((i + 1)).Tv = 0.0!
verts((i + 2)).Tu = Me.NumXTiles
verts((i + 2)).Tv = Me.NumYTiles
verts((i + 3)).Tu = 0.0!
verts((i + 3)).Tv = Me.NumYTiles
i = (i + 4)
Loop
Finally
vb.Unlock()
End Try
End Using
Return TexturedBox
End Function

Now if you run the code you will see a crate, not a box (Figure 5).


Figure 5. Identical cubes, one with a crate texture applied.

The final version of the code is included in textures1.zip in the download above. The final version has some enhancements. I've added a couple of slider controls that vary the 1.0f in the texture coordinates. As you slide them you will see the textures tile. This is the default behavior for DirectX when texture coordinates are >1.0; its called wrapping. However, you can change that behavior by changing the sampler states before you render the mesh. I've added a radio button to switch between wrapping and clamping. Clamping means use the texture as far as you can but after that just repeat the last row of pixels. As you can see, it causes some strange effects.

Exercise for the Reader
Modify the solution to add a rotation factor into the texture coordinates.

Future texturing questions coming soon:
How can I put different textures on each side?
How can I texture the cylinder, sphere, torus, and teapot?

Credits:
Thanks to
Chris Zastrow (czastrow@newlogicmedia.com) for the crate textures found at http://www.planetquake.com/subverse/
Carlos Aguilar (web@carlosag.net) for the code colorizer found at http://www.carlosag.net/Tools/CodeColorizer/Default.aspx  

Copyright © 2005 TheZBuffer.com

Follow the Discussion

  • C4CypherC4Cypher

    This has been a monumental help, I want to teach myself basic procedural terrain mesh rendering, and the fact that you bothered to write this ... in both C# and VB ... is outstanding ... thank you.

  • BoboBobo

    Can You please  describe how to put texture on Mesh.Cylinder. Thanx.

  • Matthew OsoskyMatthew Ososky

    This was very useful in understanding a couple of things that were hard to grasp. Thanks for the contribution.

  • KohaiKohai

    Thanks, exactly what I was looking for.

  • ForgetMeNotForgetMeNot

    Once you get this - you should REALLY do it in C++ - C# and VB are to slow for the really high performance stuff.. Scripts are slow - compiling on demand - is lame - Learn C++ - You won't regret it - I promise! (Not some scripted version either) C++ = Fast = Portable (for non directx of course) and FAST as lightning with directX.

  • MichaelMichael

    Wow...That is the most helpful article about DirectX that I have ever found.  No one will ever explain how something works like you did.  They just tell you what to do.  Not only that, but you provided help in different languages.  Nobody ever has info regarding Visual Basic with DirectX.  Thanks a lot.

Remove this comment

Remove this thread

close

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.