Texture Splatting in Direct3D Introduction
If you've been looking into terrain texturing techniques, you've probably heard about texture splatting. The term was coined by Charles Bloom, who discussed it at http://www.cbloom.com/3d/techdocs/splatting.txt. With no disrespect to Charles Bloom, it was not the most clear or concise of articles, and has left many confused in its wake. While many use and recommend it, few have taken the time to adequately explain it. I hope to clear up the mystery surrounding it and demonstrate how to implement it in your own terrain engine.
如果有已经学习过地形纹理技术，你很可能已经听说过纹理splatting技术。这个术语是Charles Bloom创造的，他在http://www.cbloom.com/3d/techdocs/splatting.txt里对这个技术进行了阐述。并不是想对Charles Bloom失礼，他所写的并不是一篇条理清晰的文章，存在许多的令人迷惑的地方。使用它的大部分人少有对其进行充分的解释。我希望自己能揭开围绕它的迷雾，并阐述如何在你自己的地形引擎中实现这个技术。
What is texture splatting? In its simplest form, it is a way to blend textures together on a surface using alphamaps.
I will use the term alphamap to refer to a grayscale image residing in a single channel of a texture. It can be any channel alpha, red, green, blue, or even luminance. In texture splatting, it will be used to control how much of a texture appears at a given location. This is done by a simple multiplication, alphamap * texture. If the value of a pexel in the alphamap is 1, that texture will appear there in full value; if the value of a pexel in the alphamap is 0, that texture will not appear there at all.
我将使用术语alphamap来关联属于纹理中某通道的颜色。一个纹理中通常有多个通道：红、绿、蓝、或者是亮度。在纹理的splatting中，alphamap将用于控制该纹理在当前位置显示颜色的多少。通过一个简单的乘法很容易做到：alphamap * texture（texture指代当前位置纹理的颜色值）。如果某像素的alphamap是1，则纹理显示全值，如果某像素的alphamap是0，则该纹理完全不显示。
For terrain, the texture might be grass, dirt, rock, snow, or any other type of terrain you can think of. Bloom refers to a texture and its corresponding alphamap as a splat. An analogy would be throwing a glob of paint at a canvas. The splat is wherever you see that glob. Multiple globs of paint layered on top of each other create the final picture.
Let's say you have a 128x128 heightmap terrain, divided into 32x32 chunks. Each chunk would then be made up of 33x33 vertices. Each chunk has the base textures repeated several times over it ?but the alphamap is stretched over the entire area. (0, 0) of the chunk would have alphamap coordinates of (0, 0) and texture coordinates of (0, 0). (33, 33) of the chunk would have alphamap coordinates of (1, 1) and texture coordinates of (x, x), where x is the number of times you want the textures to be repeated. x will depend on the resolution of the textures. Try to make sure they repeat enough to make detail up close, but not so much that the repetition is obvious from far away.
假设你有一个128×128的地形高度图，将其分割为32×32的块。每块由33×33个顶点构成。每块通过平铺贴图的形式贴上了基本的纹理。但alphamap贴图覆盖了整个块的区域，块(0, 0)的位置对应alphamap坐标和纹理坐标(0, 0)的位置，第(33, 33)个顶点alphamap坐标为（1，1），而纹理坐标为 (x, x)，x是你想让纹理重复的次数。x视纹理的分辨率而定。应该通过平铺纹理确保纹理有足够的细节，但从远处看的话，细节就不重要了。
The resolution of your alphamap per chunk is something you need to decide for yourself, but I recommend powers of two. For a 32×32 chunk, you could have a 32×32 alphamap (one texel per unit), a 64×64 alphamap (two texels per unit), or even a 128×128 alphamap (four texels per unit). When deciding what resolution to use, remember that you need an alphamap for every texture that appears on a given chunk. The higher the resolution is, the more control over the blending you have, at the cost of memory.
每个块的alphamap的分辨率需要你去制定，但我建议是2的次方。如果是32×32的块，你可以创建一个32×32 alphamap（每个单位一个texel），64×64 alphamap或者是128×128 alphamap。在选择使用哪个分辨率的时候，记住你需要为每个呈现于所给块上的纹理一个alphamap（译者：如果在一个块上你打算使用4种纹理的元素，那么这个块将由4个alphamap进行混合而得到）。分辨率越高，你需要做的混合就越多，内存消耗也越大。
The size of the chunk is a little trickier to decide. Too small and you will have too many state changes and draw calls, too large and the alphamaps may contain mostly empty space. For example, if you decided to create an alphamap with 1 texel per unit with a chunk size of 128x128, but the alphamap only has non-zero values in one small 4x4 region, 124x124 of your alphamap is wasted memory. If your chunk size was 32x32, only 28x28 would be wasted. This brings up a point: if a given texture does not appear at all over a given chunk, don't give that chunk an alphamap for that texture.
The reason the terrain is divided into chunks is now apparent. Firstly, and most importantly, it can save video memory, and lots of it. Secondly, it can reduce fillrate consumption. By using smaller textures, the video card has less sampling to do if the texture does not appear on every chunk. Thirdly, it fits into common level of detail techniques such as geomipmapping that require the terrain to be divided into chunks anyway.
Creating the Blends
The key to getting smooth blending is linear interpolation of the alphamaps. Suppose there is a1 right next to a0. When the alphamap is stretched out over the terrain, Direct3D creates an even blend between the two values. The stretched alphamap is then combined with terrain texture, causing the texture itself to blend.
Rendering then becomes the simple matter of going through each chunk and rendering the splats on it. Generally, the first splat will be completely opaque, with the following splats having varying values in their alphamaps. Let me demonstrate with some graphics. Let's say the first splat is dirt. Because it is the first that appears on that chunk, it will have an alphamap that is completely solid.
After the first splat is rendered, the chunk is covered with dirt. Then a grass layer is added on top of that:
The process is repeated for the rest of the splats for the chunk.
It is important that you render everything in the same order for each chunk. Splat addition is not commutative. Skipping splats won't cause any harm, but if you change the order around, another chunk could end up looking like this:
The grass splat is covered up because the dirt splat is completely opaque and was rendered second.
You may be wondering why the first splat should be opaque. Let's say it wasn't, and instead was only solid where the grass splat was clear. Here's what happens:
It's obvious this does not look right when compared with the blend from before. By having the first splat be completely opaque, you prevent any roles?from appearing like in the picture above.
Creating the Alphamaps
Now that we know what texture splatting is, we need to create the alphamaps to describe our canvas. But how do we decide what values to give the alphamaps?
Some people base it off of terrain height, but I recommend giving the ability to make the alphamaps whatever you want them to be. This gives you the flexibility to put any texture wherever you want with no limitations. It's as simple as drawing out the channels in your paint program of choice. Even better, you could create a simple world editor that allows the artist to see their alphamap and interact with it in the actual world.
Let's take a step back and look at what we have: Some sort of terrain representation, such as a heightmap A set of textures to be rendered on the terrain. An alphamap for each texture
Look at #3. We know that each alphamap has to be in a texture. Does this mean that every alphamap needs its own texture? Thankfully, the answer is no. Because the alphamap only has to be in a single channel of a texture, we can pack up to four alphamaps into a single texture ?one in red, one in green, one in blue, and one in alpha. In order to access these individual channels we will need to use a pixel shader, and because we need five textures (one with the alphamaps and four to blend), PS 1.4 is required. Unfortunately this is a still stiff requirement, so I will show how to use texture splatting with the fixed function pipeline as well as with a pixel shader.
退一步说，看看我们将实现什么：某种地面的表示，例如贴了多种纹理的高度图A需要绘制到地形。每个纹理的alphamap如 #3。我们知道每个alphamap都呈现一个纹理。这是否意味着每个alphamap需要其自身的纹理呢？谢天谢地，答案是否定的。一个alphamap只需占用纹理的一个通道。我们可以将四个alphamap打包进一张纹理中，r、g、b、a四个通道。我们需要使用 5张纹理，一张用于alphamap，四张用于混合。PS shader1.4的支持是必须的。不幸的是，硬件的要求比较高，所以我将介绍一种使用固定管线的绘制方法实现纹理的splatting。
Splatting with the Fixed Function Pipeline
Using the fixed function pipeline has one benefit that the pixel shader technique lacks: it will run on virtually any video card. All it requires is one texture unit for the alphamap, one texture unit for the texture, and the correct blending states.
I chose to put the alphamap in stage 0 and the texture in stage 1. This was to stay consistent with the pixel shader, which makes most sense with the alphamap in stage 0. The texture stage states are relatively straightforward from there. Stage 0 passes its alpha value up to stage 1. Stage 1 uses that alpha value as its own and pairs it with its own color value.
// Alphamap: take the alpha from the alphamap, we don't care about the color
g_Direct3DDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
g_Direct3DDevice->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);
// Texture: take the color from the texture, take the alpha from the previous stage
g_Direct3DDevice->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
g_Direct3DDevice->SetTextureStageState(1, D3DTSS_COLORARG1, D3DTA_TEXTURE);
g_Direct3DDevice->SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
g_Direct3DDevice->SetTextureStageState(1, D3DTSS_ALPHAARG1, D3DTA_CURRENT);
We have to set the blending render states as well in order to get the multiple splats to combine together correctly. D3DRS_SRCBLEND is the alpha coming from the splat being rendered, so we set it to D3DBLEND_SRCALPHA. The final equation we want is FinalColor
= Alpha * Texture + (1 ?Alpha) * PreviousColor. This is done by setting D3DRS_DESTBLEND to D3DBLEND_INVSRCALPHA.
我们必须正确地设置绘制状态使得splats被正确地连接起来。D3DRS_SRCBLEND是splat被绘制所需的alpha状态，所以我们设置D3DBLEND_SRCALPHA。混合公式如下：FinalColor = Alpha * Texture + (1-Alpha) * PreviousColor。通过设置D3DRS_DESTBLEND to D3DBLEND_INVSRCALPHA，可以达到这种混合效果。
Splatting with a Pixel Shader
Why even bother with the pixel shader? Using all the channels available in a texture instead of only one saves memory. It also allows us to render four splats in a single pass, reducing the number of vertices that need to be transformed. Because all of the texture combining takes place in the shader, there are no texture stage states to worry about. We just load the texture with an alphamap in each channel into stage 0, the textures into stages 1 through 4, and render.
为什么恰好要涉及pixel shader呢？使用纹理中的多个通道来替换传统的仅仅是节省内存的技术。它容许我们在一个绘制过程里渲染四个splats，降低了需要变换的顶点数。因为所有的纹理混合发生在shader操作中，所以无需关心纹理的stage状态。我们正好将每个通道的alphamap放在stage 0，纹理放在stage 1~4，然后进行绘制。
// r0: alphamaps
// r1 - r4: textures
// Sample textures
texld r0, t0 //t0 放在 r0存储器中
texld r1, t1 //
texld r2, t1
texld r3, t1
texld r4, t1
// Combine the textures together based off of their alphamaps
mul r1, r1, r0.x //r1 = r0.x * r1
lrp r2, r0.y, r2, r1 //以r0.y为参数在r2与r1之间做插值，结果放在r2中
lrp r3, r0.z, r3, r2 //以r0.z为参数在r3与r2之间做插值，结果放在r3中
lrp r0, r0.w, r4, r3 //以r0.w为参数在r4与r3之间做插值，结果放在r0中
The mul instruction multiplies the first texture by its alphamap, which is stored in the red channel of the texture in sampler 0. The lrp instruction does the following arithmetic: dest = src0 * src1 + (1 - src0) * src2. Let's say r0.x is the alphamap for a dirt texture stored in r1, and r0.y is the alphamap for a grass texture stored in r2. r2 contains the following after the first lrp: GrassAlpha * GrassTexture + (1-GrassAlpha) * DirtBlended, where DirtBlended is DirtAlpha * DirtTexture. As you can see, lrp does the same thing as the render states and texture stage states we set before. The final lrp uses r0 as the destination register, which is the register used as the final pixel color. This eliminates the need for a final mov instruction.
乘法指令将sampler0纹理的x值与sampler1纹理的值相乘. lrp指令做了一下的计算：dest = src0 * src1 + (1 - src0) * src2.
我们可以这样理解：r0.x是存于r1的灰土纹理的alphamap分量，r0.y是存于r2的草纹理的alphamap分量，依此类推。经过第一个lrp操作后r2的值将变为：GrassAlpha * GrassTexture + (1-GrassAlpha) * DirtBlended。正如你所看到的，lrp所做的与之前提到的固定管线绘制操作一样。最后的lrp操作将r0作为目标寄存器，该寄存器的值便是最后的像素颜色。这省去(eliminate)了一个mov指令。
What if you only need to render two or three splats for a chunk? If you want to reuse the pixel shader, simply have the remaining channels be filled with 0. That way they will have no influence on the final result. You could also create another pixel shader for two splats and a third for three splats, but the additional overhead of more SetPixelShader calls may be less efficient than using an extra instruction or two.
如果你仅仅要为一个chunk绘制2到3个splats，那什么是你需要的呢？如果你想重复使用 pixel shader ，要实现的功能仅仅是将剩余没有到的通道置0。这种方式将不会对最后的结果产生影响。你也可以针对2个splat的操作创建另一个pixel shader，也可以创建第三个pixel shader用于3个splat的操作，但是SetPixelShader带来的额外的开销比使用一个额外的指令效率低。
Multiple passes are required if you need to render more than four splats for a chunk. Let's say you have to render seven splats. You first render the first four, leaving three left. The alpha channel of your second alphamap texture would be filled with 0, causing the fourth texture to cancel out in the equation. You simply set the alphamap texture and the three textures to be blended and render. The D3DRS_BLEND and D3DRS_SRCBLEND stages from before perform the same thing as the lrp in the pixel shader, allowing the second pass to combine seamlessly with the first.
The demo application uses the two techniques described here to render a texture splatted quad. I decided not to go for a full heightmap to make it as easy as possible to find the key parts in texture splatting. Because of this, the demo is completely fillrate limited. The initial overhead of the pixel shader may cause some video cards to perform worse with it than with its fixed function equivalent, so take the frame rates with a grain of salt. The pixel shader will almost always come out ahead in a more complex scene.
You can toggle between the fixed function pipeline and the pixel shader through the option in the View menu.
The textures used are property of nVidia? and are available in their full resolution at http://developer.nvidia.com/object/IO_TTVol_01.html.
Terrain Texture Compositing by Blending in the Frame-Buffer by Charles Bloom, http://www.cbloom.com/3d/techdocs/splatting.txt
And, of course, the helpful people at http://www.gamedev.net
Feel free to send any questions or comments to firstname.lastname@example.org or private message Raloth on the forums!
float4 main_ps(float2 iTexCoord0: TEXCOORD0) : COLOR
float3 cov1 = tex2D(SplatMap0, iTexCoord0).rgb;
float3 cov2 = tex2D(SplatMap1, iTexCoord0).rgb;
oColor = tex2D(TexSplat0, iTexCoord0) * cov1.x
+ tex2D(TexSplat1, iTexCoord0) * cov1.y
+ tex2D(TexSplat2, iTexCoord0) * cov1.z
+ tex2D(TexSplat3, iTexCoord0) * cov2.x
+ tex2D(TexSplat4, iTexCoord0) * cov2.y
+ tex2D(TexSplat5, iTexCoord0) * cov2.z;
float4 main_ps(float2 iTexCoord0: TEXCOORD0) : COLOR
float3 cov1 = tex2D(SplatMap0, iTexCoord0).rgb;
float3 cov2 = tex2D(SplatMap1, iTexCoord0).rgb;
iTexCoord0.x = fmod( iTexCoord0.x,1.0 );
iTexCoord0.y = fmod( iTexCoord0.y,1.0 );
splatTexCoord0 = iTexCoord0/2.0;
splatTexCoord1 = splatTexCoord0 + float2( 0.5,0.0 );
splatTexCoord2 = splatTexCoord0 + float2( 0.0,0.5 );
splatTexCoord3 = splatTexCoord0 + float2( 0.5,0.5 );
oColor = tex2D(TexSplat0, splatTexCoord0) * cov1.x
+ tex2D(TexSplat0, splatTexCoord1) * cov1.y
+ tex2D(TexSplat0, splatTexCoord2) * cov1.z
+ tex2D(TexSplat0, splatTexCoord3) * cov2.x
+ tex2D(TexSplat1, splatTexCoord0) * cov2.y
+ tex2D(TexSplat1, splatTexCoord1) * cov2.z;