感谢汉化组heloc精确细致的译文!
很多人问我地形贴图进行的怎么样了,所以我就写了这个专门的开发日志来介绍一下。
Sub-tiling (注释1)
所有的技术都是建立在sub-tiling的基础上的。首先做一个包括材质包(也可以叫层或者小图形),草地,岩石,雪地等等。
我们假设每种图案占512x512像素,一张2048x2048的图就可以包含4x4=16种图案。以下就是一张例图,这张图包含了13个小图形(右上角三块没用,用全黑表示):
对材质包作MipMap处理
每张小图形(平铺)原来都是无缝的,也就是说可以上下左右完美的拼接在一起。但要生成MipMap的话这就必须要做些处理。普通的MipMap方法(下采样外加一个Box滤波器)行不通,所以我们必须做自己的MipMap,并复制所有的小图形边缘,这样就可以在各层面上完全无缝的拼接了。
在生成MipMap的过程中,会有这么一步:所有的小图形只有1x1像素,也就是说整个材质包为4x4。这样也就没办法连贯地完成整个MipMap过程了。不过这只是个小问题,因为在像素渲染引擎(Pixel Shader)里,你可以在采样的时候设一个最大层次细节度。然后你就可以下采样并通过一个Box滤波器来完成它,或者随便加点材料。反正这不是个大问题。
材质ID查寻
每个地形的顶点都会有一个倾斜度和高度值。倾斜度是一个0-1之间的小数,表示顶点和垂直向量的点积。高度也是由一个0-1之间的数值表示的。
程序运行时会在内核里生成一张查询表。每种层(layer)/小图形(tile)都有自己的限制条件。比如说,草只会长在倾斜度小于20°且高度在50-3000米的地方。有很多种方法来生成这张表,但在本文中就不多说了。反正这张查询表是用来表示水平轴(U轴)上的倾斜度和垂直轴(V轴)上的高度。
这个查询表是RGBA(红,绿,蓝,Alpha)模式的(不过现在我只用了红色波段)。它包含了层/小图形的ID和相应的倾斜度、高度值。下面举个例子:
一旦材质包和查询表被载入GPU(图形处理器)中,渲染器就可以开始它的工作了。第一步很简单:
vec4 terrainType = texture2D(terrainLUT, vec2(slope, altitude));
当前像素要用的小图形ID(0-15)就被赋值到变量地面类型中了。
以下就是3D中的效果。因为这个ID太小了(0-15),我把它乘了16,这样就可以在这个灰度图片中看的更清楚:
计算MipMap水平
现在我们有UV两个坐标来对小图形进行采样,但问题是你不能直接对整个材质包采样,因为它包含了很多小图形。那在做MipMap和包裹(Wrap)的同时,你怎么才能对包里的小图形采样呢?
一般人都会想在渲染器里运行以下的代码:
u = fract(u)
v = fract(v)
u = tile_offset.x + u * 0.25
v = tile_offset.y + v * 0.25
(我们前面说过一个材质包里有4x4个小图形,另外U和V坐标都是在0-1内的,所以坐标都被乘了0.25。)
但做MipMap时不是这么算的,因为硬件用2x2个相邻像素来决定MipMap水平。fract()指令破坏了小图形间的一致性,而且会出现一像素宽的接缝。(这个瑕疵很惹眼,令人十分讨厌)
解决的方法只能是自己设计段代码来计算MipMap水平。以下就是那个函数:
/// This function evaluates the mipmap LOD level for a 2D texture using the given texture coordinates
/// and texture size (in pixels)
float mipmapLevel(vec2 uv, vec2 textureSize)
{
vec2 dx = dFdx(uv * textureSize.x);
vec2 dy = dFdy(uv * textureSize.y);
float d = max(dot(dx, dx), dot(dy, dy));
return 0.5 * log2(d);
}
请注意,这里用到了dFdx/dFdy指令来计算输入函数的导数,这大大增加了显卡配置要求。
这个函数只能用在纹理大小和小图形大小等同的情况下。所以说如果包的大小是2048x2048且每个小图形是512x512,纹理大小(textureSize)就必须为512。
一旦你得到了层次细节度,就把它设成最大MipMap水平,比如说4x4。
对包裹(wrapping)的sub-tile进行的采样
然后又有个问题,层次细节度不是整数,而是个浮点数。这就意味着当前在处理的那个像素有可能跨越了两块MipMap。所以在计算材质包内各个图形的坐标来对像素采样时必须考虑到这一点。我在不断的尝试中找到了一个可行方案。以下就是对包内某个小图形的像素进行采样的代码:
/// This function samples a texture with tiling and mipmapping from within a texture pack of the given
/// attributes
/// - tex is the texture pack from which to sample a tile
/// - uv are the texture coordinates of the pixel *inside the tile*
/// - tile are the coordinates of the tile within the pack (ex.: 2, 1)
/// - packTexFactors are some constants to perform the mipmapping and tiling
/// Texture pack factors:
/// - inverse of the number of horizontal tiles (ex.: 4 tiles -> 0.25)
/// - inverse of the number of vertical tiles (ex.: 2 tiles -> 0.5)
/// - size of a tile in pixels (ex.: 1024)
/// - amount of bits representing the power-of-2 of the size of a tile (ex.: a 1024 tile is 10 bits).
vec4 sampleTexturePackMipWrapped(const in sampler2D tex, in vec2 uv, const in vec2 tile,
const in vec4 packTexFactors)
{
/// estimate mipmap/LOD level
float lod = mipmapLevel(uv, vec2(packTexFactors.z));
lod = clamp(lod, 0.0, packTexFactors.w);
/// get width/height of the whole pack texture for the current lod level
float size = pow(2.0, packTexFactors.w - lod);
float sizex = size / packTexFactors.x; // width in pixels
float sizey = size / packTexFactors.y; // height in pixels
/// perform tiling
uv = fract(uv);
/// tweak pixels for correct bilinear filtering, and add offset for the wanted tile
uv.x = uv.x * ((sizex * packTexFactors.x - 1.0) / sizex) + 0.5 / sizex + packTexFactors.x * tile.x;
uv.y = uv.y * ((sizey * packTexFactors.y - 1.0) / sizey) + 0.5 / sizey + packTexFactors.y * tile.y;
return(texture2DLod(tex, uv, lod));
}
这个函数大约用到了25个算术指令。
最终结果
最终在着色代码如下:
const int nbTiles = int(1.0 / diffPackFactors.x);
vec3 uvw0 = calculateTexturePackMipWrapped(uv, diffPackFactors);
vec4 terrainType = texture2D(terrainLUT, vec2(slope, altitude));
int id0 = int(terrainType.x * 256.0);
vec2 offset0 = vec2(mod(id0, nbTiles), id0 / nbTiles);
diffuse = texture2DLod(diffusePack, uvw0.xy + diffPackFactors.xy * offset0, uvw0.z);
以下就是完成后的图像:
再加上光影等效果:
噪声的重要性
倾斜度和高度需要用一些(八个一组的)2D噪声来优化,让最终的图像看上去更真实。我用的是FbM 2D纹理,并采样了十次。十次听起来很多,但这是要来做整个星球的工程,我们必须让它不管在高处,低处还是地面上都看上去一样完美。十已经是让我觉得能看上去满意的最小值了。
没有噪声的话,不同高度和倾斜度的分界面上就会很难看:
http://www.infinity-universe.com/InfinityForums/viewtopic.php?t=7598
注释1:Sub-tiling一种过程生成的贴图技术,相关资料:
http://lists.openstreetmap.org/pipermail/dev/2008-January/008581.html
http://projects.blender.org/tracker/index.php?func=detail&aid=6842&group_id=9&atid=127