One of the most interesting features introduced to Direct3D in the recent release of DirectX 6 is multiple texturing. Unfortunately, it's also one of the more confusing new features. This article will introduce multiple texture mapping into the context of the traditional pixel pipeline. We will describe the multitexture programming model, provide programming examples, and spend some time addressing the issues involved in robustly taking advantage of multitexturing hardware while maintaining fallback paths for application-level multipass methods. We have also created MulTex, a simulator that interactively illustrates this potentially puzzling new feature of Direct3D. Experimenting with MulTex is a good way to gain some familiarity with the texture blending abstraction. MulTex is available from the Game Developer web site and is definitely useful to have by your side as you read this article. (Mfctex, a similar tool written by Microsoft, ships with the Microsoft DirectX 6 SDK.)
The Traditional Pixel Pipeline
In previous versions of DirectX, the texture mapping phase of the Direct3D pixel pipeline has only involved fetching texels from a single texture. The two gray pipeline segments in Figure 1 are the stages in the traditional pipeline that deal with determining texel color and blending that color with the color of the primitive interpolated from the vertices. These two stages of the pipeline are replaced by the new multitexturing abstraction. The rest of the pipeline remains untouched.
DirectX 6 introduces the concept of a texture operation unit. Each unit may have a single texture associated with it, and up to eight texture operation units can be cascaded together to apply multiple textures to a common primitive. Each texture operation unit has six associated render states, which control the flow of pixels through the unit, as well as additional render states associated with filtering, clamping, and so on. Figure 2 shows two texture operation units cascaded together. We'll limit our discussion here to the dual texture case to keep things simple and because most of the near-term 3D hardware will support only two textures.
Figure 2. This screenshot, taken from the MulTex utility, shows two cascaded texture operation units.
Three of the render states in each texture operation unit are associated with RGB (color), and another three are associated with alpha. For RGB color, the render states D3DTSS_COLORARG1 and D3DTSS_COLORARG2 control arguments, while D3DTSS_COLOROP controls the operation on the arguments. Likewise, D3DTSS_ALPHAARG1 and D3DTSS_ALPHAARG2 control arguments to D3DTSS_ALPHAOP. Essentially, the D3DTSS_COLORx render states control the flow of an RGB vector, while the D3DTSS_ALPHAx render states govern the flow of the scalar alpha through parallel segments of the pixel pipeline, as shown in Figure 2.
Using the argument states, you can direct input, such as interpolated diffuse color or texel color, into the texturing operations. Table 1 shows a complete list.
Table 1. DirectX 6 texturing operations.
|D3DTA_TFACTOR||Take pixel data from API-level factor. This factor is set with RENDERSTATE_TEXTUREFACTOR.|
|D3DTA_DIFFUSE||Use interpolated diffuse color (Gouraud shading).|
|D3DTA_CURRENT||Use color from previous texture operation unit.|
|D3DTA_TEXTURE||Use color from texture associated with this unit.|
Additionally, you can invert the arguments or replicate their alpha channel across the RGB channels. In the API, you can bitwise OR in the constants D3DTA_COMPLEMENT and D3DTA_ALPHAREPLICATE with any of these render states to achieve the desired effect. D3DTA_COMPLEMENT simply inverts each of the color channels, while D3DTA_ALPHAREPLICATE replicates the alpha from the argument across the R, G, and B channels. Naturally, the D3DTA_ALPHAREPLICATE flag isn't meaningful if it's used with D3DTSS_ALPHAARGx. Also, D3DTA_CURRENT doesn't make sense for the 0 texture operation unit because there is no previous texture operation unit.
The operators in each unit can operate on one or both of the corresponding arguments. The operator render states can be set to any of the values in Table 2.
This long list of operations may seem a bit daunting at first, but with some experimentation, the abstraction is actually quite approachable.
To get you started with the model, the next section illustrates some common multitexture techniques and how they can be programmed in DirectX 6. We suggest that you follow along with MulTex.
Each texture operation unit also has states for texture addressing and filtering associated with it. The application programmer can set these render states independently for each texture operation unit. A common example would be to set the base texture of an object, such as the brick texture in the following dark mapping example (Figure 3), to use D3DTADDRESS_WRAP texture addressing, while the texture operation unit for the dark map uses D3DTADDRESS_CLAMP.
Table 2. Operator render states.
|D3DTOP_DISABLE||Disable this and any later texture operation units|
|D3DTOP_SELECTARG1||Pass argument 1 untouched|
|D3DTOP_SELECTARG2||Pass argument 2 untouched|
|D3DTOP_MODULATE||Multiply both arguments together|
|D3DTOP_MODULATE2X||Multiply both arguments and shift 1 bit|
|D3DTOP_MODULATE4X||Multiply both arguments and shift 2 bits|
|D3DTOP_ADD||Add Arguments together|
|D3DTOP_ADDSIGNED||Add Arguments with -0.5 bias|
|D3DTOP_ADDSIGNED2X||Add Arguments with -0.5 bias and shift 1 bit|
|D3DTOP_SUBTRACT||Subtract Arg2 from Arg1, with no saturation|
|D3DTOP_ADDSMOOTH||Add arguments and subtract product|
|D3DTOP_BLENDDIFFUSEALPHA||Blend arguments based on interpolated alpha|
|D3DTOP_BLENDTEXTUREALPHA||Blend arguments based on texture alpha|
|D3DTOP_BLENDFACTORALPHA||Blend arguments based on factor alpha|
|D3DTOP_BLENDTEXTUREALPHAPM||Linear alpha blend with premultiplied Arg1 Arg1 + Arg2*(1-Alpha)|
|D3DTOP_BLENDCURRENTALPHA||Blend arguments based on current alpha|
|D3DTOP_PREMODULATE||Modulate with next texture before use|
|D3DTOP_MODULATEALPHA_ADDCOLOR||Arg1.RGB + Arg1.A*Arg2.RGB|
|D3DTOP_MODULATECOLOR_ADDALPHA||Arg1.RGB*Arg2.RGB + Arg1.A|
|D3DTOP_MODULATEINVALPHA_ADDCOLOR||(1-Arg1.A)*Arg2.RGB + Arg1.RGB|
|D3DTOP_MODULATEINVCOLOR_ADDALPHA||(1-Arg1.RGB)*Arg2.RGB + Arg1.A|
|D3DTOP_BUMPENVMAP||Per pixel environment map perturbation|
|D3DTOP_BUMPENVMAPLUMINANCE||Environment map perturbation w/luminance channel|
|D3DTOP_DOTPRODUCT3||A per-pixel dot product that could be used for specification of surface normal vector data in texture maps. The result is (Arg1.R*Arg2.R + Arg1.G*Arg2.G + Arg1.B*Arg2.B) where each component is scaled and offset to make it signed.|
Figure 3. Dark mapping in action.
Naturally, our first example of multiple texture mapping is the dark map described by Brian Hook in the August 1997 issue of Game Developer ("Multipass Rendering and the Magic of Alpha Rendering"). Dark mapping is commonly used in lieu of vertex lighting, where one of the two textures contains an unlit base texture and the other contains a lighting texture (the dark map). Using the new multiple texturing API, one might implement this technique as shown in Figure 2.
In the figure, the two large blue boxes represent texture operation units, and the red lines show the flow of data through the pipeline. The first texture operation unit merely passes data from texture 0 to the next stage. The second texture operation unit receives these texels via Arg2 and also fetches texels from texture 1 via Arg1. The results are modulated, giving the final texel color as shown on the right-hand side of Figure 3. Nothing interesting is being done with the alpha channel of the pipeline in this case. Code (generated by MulTex) for dark mapping is shown in Listing 1.
// Program Stage 0: lpDev->SetTexture(0, pTex0 ); lpDev->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); lpDev->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); // Program Stage 1: lpDev->SetTexture(1, pTex1 ); lpDev->SetTextureStageState(1, D3DTSS_COLORARG1, D3DTA_TEXTURE); lpDev->SetTextureStageState(1, D3DTSS_COLORARG2, D3DTA_CURRENT); lpDev->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_MODULATE);