文章来源:http://www.gamedev.net/reference/articles/article2238.asp
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失礼,他所写的并不是一篇条理清晰的文章,存在许多的令人迷惑的地方。使用它的大部分人少有对其进行充分的解释。我希望自己能揭开围绕它的迷雾,并阐述如何在你自己的地形引擎中实现这个技术。
The Basics
What is texture splatting? In its simplest form, it is a way to blend textures together on a surface using alphamaps.
那什么是splatting呢? 最简单的解释如下:它是一种使用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.
在地形里,纹理可能是草、泥土、岩石、雪或者你能想到的其他类型。Bloom将纹理和其对应的alphamap值统称为一个splat。类似于在帆布上涂鸦的效果。splat就是无处不在的小水滴,通过在绘制层混合这些水滴创建最好的图像。
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.
块大小的选择有一定的技巧。太小的话,你将涉及过多的状态改变和绘制调用。太到的话,alphamaps有可能包含过多的空区域。例如:如果你打算创建一个一单位一个texel的128×128的块,但alphamap仅仅在一个4×4的小区域有非零的值,124×124的内存空间都被浪费了。如果块是32×32,那么浪费的空间仅仅是28×28。这是一个需注意的地方:如果给的纹理并不覆盖块的所有区域,那么不必为块提供
与纹理相关的alphamap。
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.
为什么地形需要分为块的原因逐渐明朗。首先,分块能节省大量显存空间。再次,它降低了速度上的消耗。使用较小纹理时,如果纹理不出现在块中,显卡的采样处理将减小。第三,这种方法适用于geomipmapping等需要地形切分的LOD地形技术。
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.
获得平滑混合效果的关键是对alphamaps进行线性插值。假设a1在a0的右边。当alphamap延展到整个地形,Direct3D在两个值之间创建平滑的混合。延展的alphamap用纹理连接起来促使了纹理自身的混合。
遍历每个块并在其上绘制splat是一个简单的小问题。通常,第一个splat将是完全不透明的,接下来的splats的值根据alphamaps将有变换。让我们用一些图进行论证。第一个splat为泥土层。因为泥土更像是在块中的最底层显示的东西,它的alphamap应该是完全不透明的。
*
=
After the first splat is rendered, the chunk is covered with dirt. Then a grass layer is added on top of that:
第一个splat绘制后,块被泥土覆盖。结着草层将被加在上边。
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:
剩下的splats对于块的操作都是类似的重复。为每个块绘制东西的顺序是十分重要的。Splat混合的顺序不可交换。不进行splats混合倒不会有什么问题,但如果你改变了周围块的绘制顺序,另一个块的绘制结果如下:
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:
因为泥土splat完全不透明,并且是第二次绘制的,致使草层被掩盖。
你可能对第一个splat要保持不透明有所疑问。让我们将其设为透明试试,替代图的一部分草层splat被清空。结果如下:
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.
很明显,混合后的效果并不是很好。但如果将第一个splat设置为完全透明的话,效果就不一样了。(这里不知如何翻译合适)
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.
现在我们了解了什么是texture splatting,我们需要创建alphamaps来描述涂鸦用的帆布。但我们该往alphamaps上填充什么值呢?
一些人创建alphamaps时脱离了地形高度值,但我建议读者最好有控制alphamaps的能力。这是你将任何纹理置于你所希望的地方。在你的程序中加入绘制通道。如果你能创建一个简单的世界编辑器允许美工看到alphamap,并能够将它融入实际的世界中。
Implementation
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的纹理单位,一个用于纹理的纹理单位以及正确的混合状态设置。
// 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.
g_Direct3DDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
g_Direct3DDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
g_Direct3DDevice->SetRenderState(D3DRS_DESTBLEND, 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,然后进行绘制。
ps_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.
如果你想绘制多于4个splats的时候,就需要多passes的操作。假设需要渲染7个splat。你先绘制前4个。第二个alphamap未用到的alpha通道将被填充为0, 使第四个纹理从方程中剔除。你仅仅需要设置alphamap和剩余的三个纹理进行混合和渲染。前面介绍的D3DRS_BLEND和D3DRS_SRCBLEND渲染状态设置可实现同样的效果,将第二个pass与前面的pass混合。
The Demo
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.
演示程序使用了两种技术描述了渲染splatted四边形的方法。为了简化splatting操作,我没有引入一个完整的高度图。也正因为如此,演示程序的功能有一定的限制。
Sources
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 nglasser@charter.net or private message Raloth on the forums!
----------------------------------------------------------------------------------------
译注:文中用的是比较早的shader版本,用汇编写的,我改成了通俗的版本:
float4 main_ps(float2 iTexCoord0: TEXCOORD0) : COLOR
{
float3 cov1 = tex2D(SplatMap0, iTexCoord0).rgb;
float3 cov2 = tex2D(SplatMap1, iTexCoord0).rgb;
float4 oColor;
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;
return oColor;
}
另外,如显存及带宽足够大的话,用于Splatting的地形纹理可以组合在一张纹理中,减少创建纹理的开销,类似这样:
对应的shader代码可以改成:
float4 main_ps(float2 iTexCoord0: TEXCOORD0) : COLOR
{
float4 oColor;
float3 cov1 = tex2D(SplatMap0, iTexCoord0).rgb;
float3 cov2 = tex2D(SplatMap1, iTexCoord0).rgb;
float2 splatTexCoord0;
float2 splatTexCoord1;
float2 splatTexCoord2;
float2 splatTexCoord3;
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;
return oColor;
}
----------------------------------------------------------------------------------------