.:[ Please always use http://www.32bits.co.uk to access the site. Thanks! ]:.
Last updated: 10:55 AM Monday October 2008
![]()
You're going to like this tutorial. You're going to like it a lot, especially if you're a bloke. This tutorial is going to teach you all about texturemapping. Let's clarify something up front - texturemapping is the process of placing an image on a primitive to make it look like a realistic object. The normal use for texturemapping is to give objects in games a nice look, such as surfaces in Counterstrike. But I'm going to do something a little more fun. This tutorial will show you texturemapping techniques with the help of my lovely assistant, Jessica Alba:
Fig 4.1: Jessica Alba, texturemapped on a quad.
That was originally going to be Sarah Michelle Gellar mapped nicely onto a quad, but I couldn't find a picture that fit. What we have in Fig4.1 is a quad created with a TriStrip, with the vertices specified in a clockwise order for CCW culling. If you're not 100% confident you understand that sentance, please go back and reread the first 3 tutorials in this series.Before I can show you the code for this, we need to cover a little bit of theory. Don't worry, because it's extremely easy - texturemapping is probably one of the easiest and most obvious effects you can use. If you've been following these tutorials in order, we've already touched the basics of texturemapping in the CD3DSprite class in Series 2, however in that tutorial we used functions that hid most of the true texturemapping work from us. This time around we're going to learn how to map a bitmap onto a quad, which is exactly what the D3D sprite functions do.
The basic idea behind texturemapping is to map each pixel of a bitmap image (from here I'll call this a "texture") to pixels inside a primitive. The most basic primitive is of course a triangle, but you'll find that you spend more time using quads for texturemapping, because it's easier. So how do we map our textures onto our primitives? We simply assign texture coordinates to each vertice of our primitives, which tells D3D which part of our texture should go where.
A little terminology for you. Just as each part of an image is called a pixel (for "picture element"), each part of an image used for texturemapping is called a "texel" for "texture element". So whenever you see any document discussing texels, you know they are talking about texturemapped images. Texture coordinates are also called "texcoords", and texturemapping is called "texmap".
Let's take a look at a practical example. Here we have the quad (in object space) that I used in Fig4.1. In black you can see the normal vertice coordinates, and in red you can see the texture coordinates:
Fig 4.2: Texcoords for a quad
As you can see, the texcoords range from 0.0 to 1.0 for both the X and Y axis. They directly represent portions of the image, with (0.0, 0.0) representing the top left of an image, and (1.0, 1.0) representing the bottom right:
Fig 4.3: Texcoords on the image
Note that the texcoord system is actually counterintuitive. As we already know, in object space 0,0,0 is the conceptual middle of the screen, and we try to center our vertices around that point. This means that the top left vertice of a quad will be (-1.0, 1.0), and the bottom left vertice of the quad will be (-1.0, -1.0). This means that the Y coordinates start at +1.0 for the top of the quad, and range to -1.0 for the bottom. However when we texturemap the quad the texcoords start at 0.0 at the top, and range to +1.0 at the bottom. Notice the difference? Keep it in mind, because you'll be scratching your head when your first texturemapping code produces all upside down images ;).Now you may now be wondering what the point of texcoords are, if they range from 0.0 to 1.0 and cover the entire image. Surely we could just pass D3D the texture and it can do the work for us? Well, not exactly. If we're mapping quads, it's very simple - we just map the 4 corners of the texture to the 4 corners of the quad. But what if we're mapping a triangle? It's just as easy, we just extend the logic behind texcoords. If (0.0, 0.0) is the top left of the texture, and (1.0, 0.0) is the top right, (0.5, 0.0) must therefore be the top middle:
Fig 4.4: Texturemapping a triangle
The left image is our source bitmap for texturemapping. I've assigned each part of the image a texcoord. The middle image is our standard triangle in object space, with the red lines showing which set of texcoords is associated with each vertice. The right image is the final result of using the texcoords - a correctly mapped (and clipped to the triangle) Jessica.The important concept you should understand from this is that we're not forced to limit ourselves to 1.0 and 0.0 texcoords. If we changed the (0.5, 0.0) texcoord in Fig4.3 to (0.8, 0.0) we could produce a strange slanting effect with the texture. We'll cover some effects far better than this shortly, though.
Now that you understand the theory behind texturemapping, let's look at a practical example. Here's the code to produce the screenshot in Fig4.1:
[ Download complete VC6 workspace as .zip ]
Texturemap.cpp [ View source code in this window ] [ View source code in a new window ] [ Source code in plain ascii .cpp file ]
As usual, let's go through the code changes step by step. As we know, we create texturemapping by mapping texcoords onto vertices. And as you've probably guessed, this means we need to make a little change to our custom vertex struct:
// Change our normal vertex struct to include members for texturemapping...
typedef struct _tagSimpleTexMapVertex
{
float x,y,z;
D3DCOLOR dwDiffuse;
float tu,tv;
} SIMPLETEXMAPVERTEX;
Here I've added in 2 extra float's to represent the X and Y component of each texcoord. In case you're wondering, "tu" and "tv" are the generic varnames given to texcoords, with tu representing the X texcoord component, and tv representing the Y texcoord component. I've also changed the name of the custom struct to reflect the fact that we've now added extra members for texturemapping - clear naming of our structs makes sense, as using the wrong struct (a texturemapping struct instead of a standard diffuse colour only struct for example) can waste a fair bit of memory.Any change to our vertex structure must be reflected in our FVF define, and here I've added 2 extra identifiers to tell D3D about the type of texturemapping we're doing:
// ...and update our FVF define to match.
#define FVF_SIMPLETEXMAPVERTEX (D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1 | D3DFVF_TEXCOORDSIZE2(1))
The D3DFVF_TEX1 define tells D3D that the second set of float's in our vertex struct are to be used as a set of texcoords. You can have up to 8 seperate sets of texture coordinates per vertex, with the appropriate #define's ranging from D3DFVF_TEX1 to D3DFVF_TEX8. Each set of texcoords is directly related to something called a "Texture Stage State", where each texture stage represents one action you can perform on a texture. If you have 8 texture coordinates, you could have 8 different stages of texturing where each stage applies one texture to the object you're texmapping. And if you didn't want all 8 of your textures to be applied to the same place, you could specify 8 different sets of texcoords. This is where single and multi-pass rendering comes in, and we're going to cover this later in the tutorial.
Note that I've started the FVF define with D3DFVF_TEX1 and not D3DFVF_TEX0. This is because Microsoft have decided to do as much as they possibly can to confuse you. Whereas everything else in the coding world generally begins at index 0 (so a 10 component array would be Array[0] to Array[9]), Microsoft have decided that the D3DFVF_TEX0 define actually means "I don't want to use any texture coordinates". Why the hell you would ever want to use a define to say you didn't want to use a define is beyond me, but that's the way it is. Accept it, and only ever use D3DFVF_TEX1 to D3DFVF_TEX8.
The D3DFVF_TEXCOORDSIZE2(1) define is a little trickier to understand. The purpose of this #define macro is to tell D3D how many texture coordinates we're supplying for each texture stage. The number at the end of TEXCOORDSIZE tells D3D how many texcoords we're supplying, and the number in brackets is the texture stage we're talking about. For example, if we wanted to tell D3D we're supplying 3 float's for the texcoords for texture stage 5, we would use the D3DFVF_TEXCOORDSIZE3(5) macro. Be very careful with this, as it's easy to get mixed up in which order to specify the values. The number of floats we specify actually relate to the dimensions of the texture we're using - D3DFVF_TEXCOORDSIZE3() means we're using a 3 dimensional texture, whereas D3DFVF_TEXCOORDSIZE2() means we're using a 2 dimensional texture. The explanation of 3D textures is way, way outside the scope of this tutorial, so for now just accept that we will only be using 2D textures (which of course we are, because bitmaps are flat images and have no 3rd dimension).
Don't worry about 2D and 3D texture coordinates for the moment. By default, if you do not specify a D3DFVF_TEXCOORDSIZEn() define in your custom FVF, D3D will use a 2 dimensional texture coordinate set. This will almost always be what you want, I just introduced the macro here so you would be aware of it.
That's the vertex struct and FVF define covered, now lets take a look at how we specify our texcoords in practice. In GameInit():
SIMPLETEXMAPVERTEX Quad[4];
Quad[0].x=-1.0f; Quad[0].y=-1.7f; Quad[0].z=0.0f;
Quad[0].tu=0.0f;
Quad[0].tv=1.0f;
Quad[0].dwDiffuse=D3DCOLOR_XRGB(255,255,255);
Quad[1].x=-1.0f; Quad[1].y=1.0f; Quad[1].z=0.0f;
Quad[1].tu=0.0f;
Quad[1].tv=0.0f;
Quad[1].dwDiffuse=D3DCOLOR_XRGB(255,255,255);
Quad[2].x=1.0f; Quad[2].y=-1.7f; Quad[2].z=0.0f;
Quad[2].tu=1.0f;
Quad[2].tv=1.0f;
Quad[2].dwDiffuse=D3DCOLOR_XRGB(255,255,255);
Quad[3].x=1.0f; Quad[3].y=1.0f; Quad[3].z=0.0f;
Quad[3].tu=1.0f;
Quad[3].tv=0.0f;
Quad[3].dwDiffuse=D3DCOLOR_XRGB(255,255,255);
Here I've created a quad from a triangle strip, and assigned texcoords to each vertice. Fig4.2 and Fig4.3 show graphic examples of how the texcoords match up to the vertices, so I won't recap it here. If you're still not sure, reread the tutorial and plot the coordinates (both texcoord and vertex) on some paper.Now is a good time to talk about the effect of the diffuse colour on texturemapping. In D3D, it's not an exclusive choice between diffuse colours or texturemapping - your diffuse colours will directly affect your texturemapping & final output. As a quick example, here's what happened when I changed the diffuse colours for all the vertices:
Fig 4.5: Texturemapping with diffuse colours blended
Note how it's not just the white background that's had it's colour affected by the diffuse colours. Jessica herself has also been blended with the diffuse colours. For reference, setting the vertex diffuse colour to white (D3DCOLOR_XRGB(255,255,255)) makes sure that the diffuse colours do not change the texture colours, as shown in the example code provided above.
As you can see from the jpg included in the complete workspace zip, the dimensions of jessica.jpg are not equal. As you can also see, I set some of the vertex coords in the example code to (x.x, -1.7). Note the Y component here. This is really, completely and absolutely NOT the way you should create a quad that texturemaps an image retaining it's correct aspect ratio. Mapping texels to pixels is a whole different topic, and I'll be covering it in a later, more advanced tutorial. So, please don't rip your hair out wondering how to get an accurate aspect ratio for your texturemapping - I literally took a wild guess at some vertex coords and kept changing them until the image looked correct.
I won't show the code to place the vertex array in a vertex buffer, as we've covered that code many times previously, and adding texturemapping to the equasion doesn't require any changes. For completeness, here's the code to load the image for texturemapping:
rslt=D3DXCreateTextureFromFileEx(g_pDevice, "jessica.jpg", D3DX_DEFAULT, D3DX_DEFAULT,
D3DX_DEFAULT, 0, D3DFMT_UNKNOWN, D3DPOOL_MANAGED,
D3DX_DEFAULT, D3DX_DEFAULT, 0, NULL, NULL, &g_pTexture);
Just a few points to note about this code. First, I set the Usage parameter (parameter 6) to 0, as we aren't using this texture as a render target or as a dynamic texture. Second, I set the pool (parameter 8) to D3DPOOL_MANAGED, to tell D3D to manage the allocation and restoration of our texture should a lost device occur (we discussed lost devices back in Series 1). I'll be making small optimisation changes like this in future tutorials, as now that you understand the basics it's time to make our code that little bit better! The final note is the LPDIRECT3DTEXTURE9 var g_pTexture was created at global scope (as shown by the g_ prefix) at the top of Texturemap.cpp.Now we have our vertex buffer full of vertices loaded with texcoords and our texture loaded and ready, we're all set to render the quad onscreen and texturemap it. This is the easiest part of the lot, as we've already done the hard work. Let's take a look at the changes to the Render() function:
// Tell D3D we want to use our vertex buffer as the "source stream"
g_pDevice->SetStreamSource(0, g_pVertexBuffer, 0, sizeof(SIMPLETEXMAPVERTEX));
// Tell D3D we want to use our custom FVF define
g_pDevice->SetFVF(FVF_SIMPLETEXMAPVERTEX);
// Tell D3D to use the following texture for texturemapping the next set of primitives
g_pDevice->SetTexture(0, g_pTexture);
// And finally tell D3D to draw our triangle strip!
g_pDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);
The code is absolutely identical, except for the g_pDevice->SetTexture() call. It tells D3D to use the texture we specify for the texture stage we specify. Here's it's prototype:
HRESULT SetTexture(DWORD Stage, IDirect3DBaseTexture9* pTexture);
Parameter one tells D3D which stage we want to use the texture for. As discussed above, you can specify up to 8 seperate texture coordinate sets, each of which relates to a texture stage. As we only have one set of texcoords and we simply want to render the texture straight onto the quad, we're only using the first texture stage so we specify 0 for this parameter. Don't worry about texture stages just yet, as we'll cover them shortly. Parameter 2 is just a pointer to an IDirect3DBaseTexture9 instance, for which we supply the texture we created.You have to admit, that was pretty easy. Very little effort, and we ended up with a massive result - a nicely texturemapped quad! Before we move on, I'd like to make one thing clear. If you remember back to tutorial 2, we learnt that primitives that do not share vertices also do not blend diffuse colours. The same principles do not apply to texturemapping. Texturemapping is done in stages, and not primitives. This means that you can create a quad out of 2 triangles in a TriList, and still texturemap correctly (whereas the diffuse colours would be clearly split between the 2 triangles). As long as you specify the correct texture coordinates, sharing (TriStrip) or not sharing (TriList) vertices is irrelevant.
Now that you know the basics of texturemapping, you know half the story. There's a lot more we can do with texturemapping, and I'll show you some of them now. First of all, lets talk about texture coords. As you know, to correctly texturemap a bitmap onto a primitive, you specify texcoords between 0.0 and 1.0. But what happens if you use coords outside of this range? You get some pretty interesting effects, that's what!
By specifying texcoords that do not lie in the 0.0-1.0 range, we can produce one of the following effects: Wrapping, Mirroring, Mirror Once, Border and Clamping. Here are some screenshots. First up, wrapping mode:
Fig 4.6: Texturemapping in Wrap addressing mode
To produce this effect, I specified texcoords from 0.0 to 3.0, which tells D3D to wrap the texture 3 times in both directions. Next up is mirroring mode:
Fig 4.6: Texturemapping in Mirror addressing mode
This one is a little harder to see, but the texture is mirrored in both the X and Y axis 3 times. To produce this effect, I specified texcoords from 0.0 to 3.0 (to tell D3D to mirror 3 times), and used texture stage states to tell D3D I wanted mirroring mode. We'll cover texture stage states in a moment.The next effect is mirror once mode, which I can't give you a screenshot of. It's not supported by the GeForce card range, and I've got a GF4. Anyway, it looks like a cross between mirror mode, and clamp addressing mode.
Next is border colour mode:
Fig 4.6: Texturemapping in border colour mode
Here the texture is scaled by the factor we supply - in this case my texcoords range from 0.0 to 3.0, so the texture is scaled down by a factor of 3 - and the remaining space in the primitive is filled with the colour we specify. Again, the border colour mode and the colour itself are both specified using texture stage states. Finally we have clamp addressing mode:
Fig 4.6: Texturemapping in clamp addressing mode
Again, the texture is scaled by the factor we supply as texcoords - 0.0 to 3.0 as usual. Then the last pixel on the edge of the texture is extruded across the remaining surface of the primitive.So you can see all of this for yourself and play around with it, here's some code. It's completely identical to the source code provided above, except I've added some extra sections to GameInit() that you can comment/uncomment to see each of the above effects.
[ Download complete VC6 workspace as .zip ]
TexturemapCoords.cpp [ View source code in this window ] [ View source code in a new window ] [ Source code in plain ascii .cpp file ]
I'd recommend that you download the workspace and play around with the sections of code (and the texcoords) so that you understand exactly what you can achieve. No doubt you'll notice the code that does something with texture stages in there, so just hold on and we'll get to that in a second.Before we move on to texture stages, there is one more thing I want to discuss. Let's take a look at the prototype for D3DXCreateTextureFromFileEx() again:
HRESULT D3DXCreateTextureFromFileEx(LPDIRECT3DDEVICE9 pDevice, LPCTSTR pSrcFile, UINT Width, UINT Height, UINT MipLevels, DWORD Usage, D3DFORMAT Format, D3DPOOL Pool, DWORD Filter, DWORD MipFilter, D3DCOLOR ColorKey, D3DXIMAGE_INFO* pSrcInfo, PALETTEENTRY* pPalette, LPDIRECT3DTEXTURE9* ppTexture);
The parameter I specifically want to look at is parameter 5 - UINT MipLevels. This parameter directly affects something called a "Mipmap chain". So what's one of those? Let's switch to a boring game box/floor type texture for the purposes of this example:
Fig 4.7: A standard texture at normal size
Fig4.7 shows a texture at it's normal size. We could use this texture on a cube in a first person shooter game to produce an object that looks like a box, for example. So, when we're right up close to the box, we can see the above texture in all it's detailed glory. But what happens when we move back from the box? Because we're effectively moving the camera backwards (we discussed the various transformations in tutorial 3), the box will scale and grow smaller as part of the view & perspective transforms. If we're reducing the size of the 3D object that represents our box, we're also reducing the size of the texture we're mapping onto it. And here lies a problem.Scaling bitmaps in realtime is very slow and "computationally expensive" (as stated by the SDK docs). Scaling them so that they remain proportionally correct and do not produce "artifacts" (errors in the bitmap due to the scaling), is even slower. So to get around this problem, D3D creates a "mipmap chain" of pre-scaled images it can choose from when required.
A mipmap chain is a list of images, each of which is exactly half the size of the previous image. For example, assuming our box texture in Fig4.7 has dimensions of 256*256, D3D will produce a chain of images sized 128*128, 64*64, 32*32, 16*16 and so on:
Fig 4.7: A mipmap chain created from a single source image
The "half-size" rule is exactly the same for non-symetrical images too - a 256*128 texture will have 128*64, 64*32, 32*16 etc mipmaps created. As 3D objects are scaled during the transformation stage of the 3D pipeline, D3D checks the pixel size of any texturemapped objects. It then picks the mipmap image that is closest in size to the pixel size of the 3D object. For example, if we place the camera right up against the 3D box, the pixel size of the box would no doubt be at least 256*256, therefore D3D would use the original sized image. If we were to now move the camera back by 20 units, the pixel size of the box might be 72*72. As this pixel size is closest to the 64*64 mipmap, D3D uses that mipmap and scales it slightly. This is a really clever way of ensuring good quality and memory/speed-efficient onscreen graphics. The effect is even more obvious if your texture contains text - far away and using the smallest mipmap, you may only be able to see a small black scribble. But move up close to the texture, and the largest mipmap is used providing crystal clear text up close.Of course these mipmaps come at a price, because you're increasing the amount of memory you're using for textures. D3D allows you to configure exactly how many mipmaps you want created, which is exactly what parameter 5 of D3DXCreateTextureFromFileEx() is for. If you know for a fact that you will never need more than 2 mipmaps for a texture (for example, your game is set in a room that the player cannot move out of, and will therefore always be no more than a short distance from any surface with this texture), you can tell D3D to only create 2 mipmaps for you. It's a small but useful memory-saver. Most of the time though you only need to be aware that mipmaps exist, and let D3D handle all the details for you.
Okay, it's time we took a look at texture stage states. This topic is pretty tricky, so keep a close eye on what's going on.
The general drive for games since the start of time has been to include better and more realistic graphics, to make the game environment more "real". Obviously one of the main ways this has been done is via texturemapping. However there is one small but very important problem with basic texturemapping, because what you're really doing with texturemapping is repeating the same bitmap over multiple 3D objects. If you imagine a 3D game such as Counterstrike, you are effectively repeating the same texture for the floor across an entire map. But if Counterstrike was "real", you wouldn't have exactly the same texture for the ground across the entire map - the ground at your feet does not look exactly the same as the ground 100 meters away, it's got unique marks and damage that makes it look different and most importantly, gives it variety.
The obvious aim is for us to replicate this variety in our games. But how do we do it? The first thought would be to include lots of unique bitmaps for each section of ground. The problem with this is the memory required to do it - think how big a single Quake or Counterstrike map is, and then imagine just how many unique 256*256 textures you would need to cover the whole map. And that's before you even consider the walls, ceilings and other scenery. So this method is not possible due to todays hardware limitations.
And this is where we turn to multitexturing. If we combine 2 textures together we can produce a third, different texture. This means that if we have 4 different ground textures, and 4 different "detail" textures, we have 24 different possible texture combinations from just 8 textures (4 ground textures, plus 4 detail textures, plus 4 ground textures multiplied by 4 detail textures). This is nice and memory efficient, and if we use them appropriately it gives our game a lot of realism and variety. Using multiple textures in this way has many names, the one I've described here is called "detail mapping". We'll look at some more shortly.
Some older graphics cards do not support multitexturing. Therefore the only way to blend textures together is by drawing the scene once with the first texture, and then drawing it a second time with the second texture. This is really slow, because you're drawing all the primitives in your scene twice, thus halving your throughput. Rendering the same scene multiple times with different textures is called multipass rendering, whilst using texture stage states and blending textures with only one render pass is unsurprisingly called single pass rendering. If you're coding a game for different hardware platforms, make sure you check the capabilities of the graphics cards in use.
To blend textures together (and produce some other texture & colour related effects), we use texture stage states. Think of texture stages as a little logic machine. You stick some data in the top, such as a texture and a colour, perform an operation on them such as blend them together, and out the bottom pops a texture blended with your colour. Each texture stage takes 2 inputs - one colour and one alpha, performs 2 operations ("ops"), one on the colour input and one on the alpha input, and produces one output - a colour value of the colour op and the alpha op combined. We haven't discussed "alpha" yet, so for the moment just accept that the alpha value is the level of transparency of a colour, with 255 being opaque and 0 being transparent. For example, an RGB(255,0,0) colour with an alpha value of 128 is a red, half transparent colour. Instead of the D3DCOLOR_XRGB(R, G, B) macro we've been using up until now, D3D represents a colour with alpha as a D3DCOLOR_ARGB(A, R, G, B) macro, where A is the alpha value from 0-255. So, a half transparent red pixel would be represented as D3DCOLOR_ARGB(128, 255, 0, 0). To make absolutely clear, when you set up a texture stage state, the operations that you specify are applied to every pixel passing through the pipeline, and not primitives or objects as a whole.
Texture stages are kind of the precursor to vertex shaders. As you may know, only GeForce 3 and above cards support vertex shaders, also known as the "programmable pipeline". Other, older cards do not support shaders but do support texture stages, which is part of the "Fixed function pipeline".
There are a lot of operations you can perform on texture stage inputs, and which ones are available depends on your graphics card. To find out the manual way, load the DirectX Caps Viewer from the DirectX Utilities program group, and navigate down the tree as shown below. Here's a screenshot of the Caps viewer for my GF4Ti4200:
Fig 4.7: Supported TSS by the GF4Ti range
In fact, the GF4Ti range supports all texture stage ops except one - "D3DTEXOPCAPS_PREMODULATE". You can of course check support for any op at runtime by calling IDirect3DDevice9::GetDeviceCaps(), then checking the TextureOpCaps DWORD of the returned D3DCAPS9 struct, just as you can do to check for support for any D3D9 function. For an example, take a look in the D3DFuncs.cpp source file included with every tutorial - in the InitDirect3D() function, you'll see the code that checks for multisampling support.Here's a graphical representation of how texture stage operations work:
Fig 4.7: Logical diagram of texture stage operations
We supply 2 arguments for each texture stage, one colour argument and one alpha argument. The texture stage then performs the selected colour operation on the colour argument, and the selected alpha operation on the alpha argument. The final output is a D3DCOLOR_ARGB() value, which combines both the colour and the alpha operation results.We can use up to 8 texture stages in any one render pass, which is more than adequate! This, incidentally, is where the D3DFVF_TEX1 to D3DFVF_TEX8 FVF texcoord flags come in. In these 8 stages we can choose to blend up to 8 textures together, one blend operation per stage. It is more than likely that you won't want all 8 textures to have exactly the same texture coordinates as each other, so you are able to supply a different set of texcoords per texture stage. It is as simple as that!
Note that each texture stage assumes that by default it has it's own set of texture coordinates in the vertex struct. Therefore if you are using 3 texture stages, D3D will assume that you have 3 different sets of X & Y floats, one for each of the 3 texture stages. D3D also assumes you have D3DFVF_TEX1 to D3DFVF_TEX3 defines in your custom vertex struct. This is fine if you choose to give each texture stage a different set of texcoords, but what if you want all your texture stages to use the same texcoords? To do this, you must associate each texture stage with stage 1's texcoords, by using the IDirect3DDevice9::SetTextureStageState(Stage, D3DTSS_TEXCOORDINDEX, TexCoords) method. I'll cover this in practice shortly, but it's important enough to highlight here - if you forget to associate texcoords with a texture stage, you won't see any result from the operations.
As I've already mentioned, you set up a texture stage state by using IDirect3DDevice9::SetTextureStageState(). Here's it's prototype:
HRESULT SetTextureStageState(DWORD Stage, D3DTEXTURESTAGESTATETYPE Type, DWORD Value)
Parameter one is the stage you wish to set the operation for, so to set the operation for texture stage 3, we would supply "2" for this parameter (yep, back to 0 index again!). Parameter 2 is the type of data you're currently setting for this stage - it can either be the alpha or colour operation, the first or second colour input or the first or second alpha input. Parameter 3 is the data you're passing to parameter 2. For example, if you set parameter 2 to be a colour operation, you would set the operation you wish to perform here. Similarly if parameter 2 was a colour input, you'd set the source for the input here. This will become much clearer when you see it in practice.
Before I show you a practical example, I'll show you some of the possible parameters for this method. First, here's a table of possible "types" for parameter 2:
Stage Type Description D3DTSS_COLOROP Tells D3D that the value parameter is the colour operation you want to perform. D3DTSS_ALPHAOP Tells D3D that the value parameter is the alpha operation you want to perform. D3DTSS_COLORARG1 Tells D3D that the value parameter is the first colour input for this stage D3DTSS_COLORARG2 Tells D3D that the value parameter is the second colour input for this stage D3DTSS_ALPHAARG1 Tells D3D that the value parameter is the first alpha input for this stage D3DTSS_ALPHAARG2 Tells D3D that the value parameter is the second alpha input for this stage
Here's a table of the possible inputs for both the colour and alpha input. Remember, although all of these just look like colours, they have alpha components too. And, as noted above, where I've written "colour" or "alpha", we're talking about the pixel currently being processed by the 3D pipeline. You can only supply these values for parameter 3 of SetTextureStageState() when you've set parameter 2 to either D3DTSS_COLORARG1/2 or D3DTSS_ALPHAARG1/2, because they are just data inputs for the stage.
Value Description D3DTA_DIFFUSE Take the colour or alpha value from the vertex diffuse colour. If your vertex struct doesn't include a diffuse colour, D3D will use D3DCOLOR_ARGB(255,255,255,255) instead (opaque white). D3DTA_SPECULAR Use the specular colour as the input. This relates to lighting and materials, which we'll cover in a later tutorial D3DTA_TEXTURE Use the texture colour as the input. Note that only .PNG and .TGA image formats currently support alpha, so you'll probably never use this as the alpha argument. D3DTA_CURRENT Tells D3D to use the output of the previous texture stage as the input. If you specify this for stage 0, where there was no previous stage, D3D uses the vertex diffuse colour instead. You'll be using this a lot in every texture stage (except 0), to access the output of the previous stage. D3DTA_TEMP Newer gfx cards use this for temporary storage for anything you like. Check the device caps before using it.
Next, here's a list of the some of the more common operations you can perform, again for both colour and alpha operations. You can only supply these values for parameter 3 of SetTextureStageState() when you've set parameter 2 to either D3DTSS_COLOROP or D3DTSS_ALPHAOP, because of course these are operations to perform, rather than data:
Value Description D3DTOP_DISABLE Do not do anything for this operation D3DTOP_SELECTARG1 Use this stage's first input as the output for this operation, without changing it. D3DTOP_SELECTARG2 Use this stage's second input as the output for this operation, without changing it. D3DTOP_MODULATE Multiply the 2 inputs together D3DTOP_MODULATE2X Multiply the 2 inputs together, and double the result. D3DTOP_MODULATE4X Multiply the 2 inputs together, and multiply the result by 4 D3DTOP_ADD Add the 2 inputs together D3DTOP_ADDSIGNED Add the 2 inputs, and subtract half of the result (gives a -result/2 to +result/2 spread) D3DTOP_ADDSIGNED2X Add the 2 inputs, subtract 0.5 and multiply the result by 2 D3DTOP_SUBTRACT Subtract the 2 inputs D3DTOP_BLENDDIFFUSEALPHA Blend the 2 inputs using the interpolated alpha value of the vertex diffuse colours D3DTOP_BLENDTEXTUREALPHA Blend the 2 inputs using the interpolated alpha value of the texture colour D3DTOP_BLENDFACTORALPHA Blend the 2 inputs using a constant alpha set by SetRenderState(D3DRS_TEXTUREFACTOR, D3DCOLOR_RGBA(0, 0, 0, AlphaConstant). D3DTOP_BLENDCURRENTALPHA Blends the 2 inputs using the alpha output from the previous stage.
There are a lot more operations you can perform, check the SDK docs for D3DTEXTUREOP for a complete list & description.Okay, at this point I'm assuming you're a bit confused by the whole texture stage concept, so I'm going to show you a couple of basic examples. First up, let's see how we take a texture and give it a blue tint.
The basic operation we're performing is mapping a texture onto a quad. That's easy enough, we saw how to do that in the first part of this tutorial. The harder part is tinting it blue. To do this, first we make sure the vertices of the quad all have an RGB value of (0, 0, 255) for their diffuse colour. Again, diffuse colours for vertices aren't new, we've been using them all through this series. The difference here is that we want to blend the diffuse colour with the texture. To do this, we must add some transparency. We want the blend between the blue colour and the texture to be equal, so we actually need to set the diffuse colour of every vertice in the quad to D3DCOLOR_ARGB(128, 0, 0, 255). Now we have a quad with the correct colour and the right amount of transparency for each vertex. Next, we look in the table above and pick the correct operator, in this case D3DTOP_BLENDDIFFUSEALPHA, because it blends the 2 inputs of the texture stage together, using the alpha value (we just set it to 128 in the vertex diffuse colour) to determine the transparency. Now that we've set the texture stage, D3D will take the texture and diffuse colours and blend them together for us. Here's a graphical representation:
Fig 4.7: Practical example of texture stage basics
Here's the same example in code:
// Assuming g_pTexture1 is our first texture
// Associate our texture with texture stage 0
g_pDevice->SetTexture(0, g_pTexture1);
// Set the texture as the first input for stage 0
g_pDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
// Set the vertex diffuse colour as the second input for stage 0
g_pDevice->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
// Set D3DTOP_BLENDDIFFUSEALPHA for the colour operation for stage 0
g_pDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_BLENDDIFFUSEALPHA);
// Do not perform any operations for the alpha operation for stage 0
g_pDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_DISABLE);
It's a lot easier when you see it in code, isn't it! Some points to note about this example. Firstly, although we are using alphablending to produce this effect, we're blending the colours of input 1 and input 2 together. It looks tempting to use D3DTSS_ALPHAOP here, but remember that it only works on alpha values, not the colours themselves. So, as a rule, D3DTSS_ALPHAOP operations modify transparency, and D3DTSS_COLOROP operations change the actual colour/appearance.
An interesting point here. One of our regular readers Kitty Kong pointed out that the DirectX SDK documentation states "disabling alpha operations when doing colour operations produces undefined behaviour" (search the SDK for D3DTOP_DISABLE, and read the first link listed). I say this is interesting, because it's never caused me a problem. So, my advice is to follow the code I've shown you here, but remember what the SDK says. And if you start getting problems, just remove the SetTextureStageState() line of code that contains the D3DTSS_ALPHAOP, D3DTOP_DISABLE parameters.
Make sure you understand this example before we move on, because now I'm going to show you something really complicated. Let's see how we blend 2 textures together.
Texture stage 0 is the ONLY stage that should take 2 inputs. Every other stage should use the output of the previous stage as it's first input. For example, Stage 0 takes 2 inputs you specify. Stage 1 then takes the output of Stage 0, plus one input you specify. Stage 2 then takes the output of Stage 1, plus the input you specify. This is the same for the remaining texture stages. Therefore, for every stage other than the first, you should have the following code: g_pDevice->SetTextureStageState(Current_Stage_Number, D3DTSS_COLORARG1, D3DTA_CURRENT);
Now, before we even start we have a problem. One of the restrictions of texture stages is that you can only ever use a texture for one argument of a stage operation. For example, blending 2 textures together would be extremely easy if all we had to do was the following:
// Set the texture as the first input for stage 0
g_pDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
// Set the texture diffuse colour as the second input for stage 0
g_pDevice->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_TEXTURE);
But unfortunately this doesn't work. You can probably see the reason why - we can only associate one texture with any one stage (using IDirect3DDevice9::SetTexture()). So how do we do it?The answer lies in D3DTOP_SELECTARGx. As you can see from the tables above, this operation allows us to use an input from the previous texture stage as an input for this stage, without changing it. So what we actually do is a little trickery. We set the first texture to be the first argument for the colour op of stage 0, and the vertex diffuse colour to be the second argument. We then discard the diffuse colour, and use D3DTOP_SELECTARG1 as the first operation for the colour op of stage 0. We're now free to use the second texture for the second argument for the colour op of stage 1, because we sneaked the first texture through from stage 0 by using D3DTOP_SELECTARG1 (which as we know sets the output of the current stage to be the first input, unchanged). We're now free to perform any operation on the 2 textures that we want - in our case, to blend them together we could use D3DTOP_MODULATE or D3DTOP_BLENDFACTORALPHA. Here's a graphical example to clarify.
Fig 4.7: Practical example of multiple texture blending
Here's the same example in code:// Assuming g_pTexture1 is our first texture, and g_pTexture2 is our second texture
// Associate our first texture with texture stage 0
g_pDevice->SetTexture(0, g_pTexture1);
// Set the texture as the first input for stage 0
g_pDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
// Set the vertex diffuse colour as the second input for stage 0
g_pDevice->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
// Use D3DTOP_SELECTARG1 to pass our first texture to stage 2, unchanged
g_pDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
// Do not perform any operations for the alpha operation for stage 0
g_pDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_DISABLE);
// --------------------STAGE 0 is now finished----------------------------------
// Associate our second texture with texture stage 1
g_pDevice->SetTexture(1, g_pTexture2);
// Because we didn't specify 2 sets of texture coordinates in our vertex struct, and we
// want to use the same texcoords for both textures, associate stage 0 texcoords with
// stage 1
g_pDevice->SetTextureStageState(1, D3DTSS_TEXCOORDINDEX, 0);
// Use the output of stage 0 for the first input of stage 1
g_pDevice->SetTextureStageState(1, D3DTSS_COLORARG1, D3DTA_CURRENT);
// Set the second texturestage's texture as the second colour input for stage 1
g_pDevice->SetTextureStageState(1, D3DTSS_COLORARG2, D3DTA_TEXTURE);
// We want to blend the 2 textures equally using D3DTOP_BLENDFACTORALPHA, so set the alpha factor
g_pDevice->SetRenderState(D3DRS_TEXTUREFACTOR, D3DCOLOR_RGBA(0, 0, 0, 128));
// Finally perform the alpha blend colour op for Stage 1
g_pDevice->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_BLENDFACTORALPHA);
// Do not perform any operations for the alpha operation for stage 0
g_pDevice->SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE);
With the description and diagram, you should be able to follow that code through logically. I just want to draw your attention to one line of code:
g_pDevice->SetTextureStageState(1, D3DTSS_TEXCOORDINDEX, 0)
As I noted in an "Important" boxout above, you need to be aware of texcoord sets when using multiple texture stages. For every texture stage you use, D3D expects to find a set of texcoords. For example, at the start of this tutorial when we did a simple texturemap, we were only using one set of texcoords (float tu, tv in the vertex struct, and D3DFVF_TEX1 in the FVF define). This was fine, because we were only using one texture stage - draw the texture on screen with no changes. The same principle goes for the first example above, where we blended the diffuse colour with the texture - we only used one texture stage, therefore we only required one set of texture coordinates. However in the above example, we're using 2 texture stages. Because of this, D3D expects to find a second set of texcoords in our vertex struct - if it can't, it won't render any texture output on-screen. This is fine if you want different textures to use different sets of texcoords, you just change the vertex struct and FVF define. For example, if you want the output of texture stage 2 to use it's own texture coordinates, you might use the following struct & FVF:
typedef struct _tagSimpleTexMapVertex
{
float x,y,z;
D3DCOLOR dwDiffuse;
float tu1,tv1;
float tu2,tv2;
} SIMPLETEXMAPVERTEX;
#define FVF_SIMPLETEXMAPVERTEX (D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1 | D3DFVF_TEX1
| D3DFVF_TEX2 | D3DFVF_TEXCOORDSIZE2(1) | D3DFVF_TEXCOORDSIZE2(2))
You would then be able to fill out the quad's tu2 and tv2 as you wished. But if you want the output of texture stage 2 to use the same texcoords as texture stage 1, there's no point in adding the extra float's to the vertex struct and filling them out with the same values - it wastes memory. So to get around this, we can call IDirect3DDevice9::SetTextureStageState() to tell a texture stage to use a different stage's texcoords. By calling it with D3DTSS_TEXCOORDINDEX as it's 2nd parameter, we can tell D3D which stage should use which set of texcoords:HRESULT SetTextureStageState(StageToChange, D3DTSS_TEXCOORDINDEX, UseThisStage'sTexCoords)
Here's the code I picked out above:
g_pDevice->SetTextureStageState(1, D3DTSS_TEXCOORDINDEX, 0)
Now you can see that we're telling D3D to use Stage 0's texcoords (parameter 3) for Stage 1 as well (parameter 1). If a stage does not have texcoords, you won't see it's output.
And that is all there is to it (ho ho). It's pretty complex when you first see it, but after a while everything will click and it will all make sense. If you understand the above 2 examples of texturestages, you should have no problems applying the same principles to as many stages as you require. Just remember these rules:
1. Stage 0 is the only stage that takes 2 arguments
2. Every other stage should use the previous stage's output as it's first input
3. You can only supply ONE D3DTA_TEXTURE argument per stage, use SELECTARGn instead
4. D3DTSS_COLOROP acts on the actual colour, D3DTSS_ALPHAOP only acts on transparency
5. If in doubt, read the SDK docs!
6. Make sure every stage has a set of texcoords (explicitly specified, or D3DTSS_TEXCOORDINDEX)
Now, before we wrap this long tutorial up, I'm going to provide you with some .zip files containing complete workspaces that demonstrate various texture stage effects. They are only very basic add and blend examples as I want to make sure you understand the very basics of texture stages before we move on to some really kewl stuff. Yup, texture stages can be used to produce some really neat effects, as we'll see soon. They are the cornerstone of lightmapping, darkmapping, additive blending and much more.
[ Download an example of diffuse colour and texturemap alphablending ]
[ Download an example of multiple texture alphablending ]
Have a play around with them to make sure you're happy you understand the principles. Then, without cheating by looking at the example code here, see if you can complete the following exercises.
Basic
Exercise 1: Take any bitmap image, and texturemap it onto a quad.Intermediate
Exercise 2: Modify your code in exercise 1 to produce examples of the basic texture effects (clamping, border colour, mirroring etc).Advanced
Exercise 3: Without copying directly from the example code in this tutorial (ie: using the descriptions and the SDK docs), try and use texture stages to blend your bitmap image texturemap from exercise one with your quad's vertex diffuse colour.Advanced
Exercise 4: If you complete exercise 3 correctly, try and blend 2 textures together using texture stages.
Click here to send feedback, bug reports, comments and ideas etc!