聚焦3D地形编程第三章纹理化地形
聚焦3D地形编程第三章纹理化地形
翻译: 神杀中龙 邵小宁 microsoftxiao@163.com
原著: 《Focus on 3D Terrain Programming》
翻译的烂请见谅
现在你已经可以制作简单的地形网格, 你需要知道怎样如何使用纹理贴图来为那令人讨厌的网格添加细节。我将继续讨论简单的纹理并且一直到我们可以使用真正有趣的片段(地形算法)作为开始。我将停止浪费空间现在,并且仅仅告诉你将在本章学习什么:
n 怎样应用巨大的单模式纹理映射到地形网格上。
n 怎样以程序式手段来产生复杂的纹理地图使用各种各种的地形 tiles
n 怎样为地形添加纹理细节甚至以前产生的纹理的更多细节。
Simple Texture Mapping 简单纹理映射
我们将使用简单纹理映射。 你将学习怎样延长一个纹理到整个地形网格。大部分时间, 如果这个技术看起来是糟糕的,当然,你已经真的制作好了纹理映射,我们将在下节继续工作。立刻计算出你学习的怎样延长纹理最终结果看起来像什么。
延长单张纹理越过整个地形, 我们将为地形上的每个顶点带上范围在0.0f-1.0f(标准的纹理坐标范围)的分量。这么做比听起来容易。出发了, 看图3.1。
像图3.1展示的, 左下角地形网格(, 我们将选择256x256的高度图), (0, 0)纹理坐标为(0.0f, 0.0f), 而左上角地形(255, 255),纹理坐标为(1.0f, 1.0f)。 基本上,所有我们需要的是找到顶点,包括当前渲染的和划分它使用高度图。(这么做是可以产生我们期望范围内的值日, 0.0f-1.0f, 没有超过我们的范围。注意这是重要的,因为我们在后续章节将超过这个范围。)在渲染每个顶点前, 我们需要计算三个东西: 纹理值的x, z和z+1, 我们将叫做fTexLeft, fTexBottom, 和fTexTop, 。 这里我们该如何计算这些值呢:
fTexLeft = (float)x/m_iSize;
fTexBottom = (float)z/m_iSize;
fTexTop = (float)(z+1)/m_iSize;
那么你可以思考下这将是困难的!无论如何, 我们需要在每个顶点被渲染前计算他们内并且然后将纹理坐标传送给我们的渲染API。当我们渲染顶点(x, z)时, 我们发送(fTexLeft, fTexBottom)作为我们的纹理坐标, 并且当我们渲染(x, z+1)时, 我们发送(fTexLeft, fTexTop)作为我们的纹理坐标。看图3.2和在CD上Code"Chapter3"demo3_1上的demo3_1将看到你劳动的成果。
屏幕截图后了比我们第二章地形更多的细节, “地形 101”(然而注意我移除了渐变), 但是目睹了实际形式的地形它是困难的。如图3.3延长单一纹理, 甚至如果纹理在demo3_1内使用纹理被高分辨率, 在我们的纹理地形上我们可以捕捉到一切细节。
我们需要更多细节。 我们想要纹理地图像图3.4一样,通过使用一系列程序式方法产生 tiles(泥土, 草地, 岩石还有雪地等)。
如图3.4有多少细节?这个纹理帮助我们区别高山区域到低矮的平原,如图3.3。 你需要知道怎样产生真正酷的纹理像下面展示的。继续阅读!
Procedural Texture Generation 程序式产生纹理
Procedural texture generation是很酷并且很有用的技术对于任何地形引擎。之后我们完成我们的程序化纹理产生器,我们将允许用户加载一系列2到4个它可选择的tiles。然后我们将调用我们的纹理生成函数。(所有用户都需要知道他想要创建的纹理的大小。) 那时!我们怎样着手创建我们的纹理生成函数呢?首先,你需要知道我们这里实际的目标。我们将让它和产生的高度图表现一致(如高山,那就用高山纹理,如平原,就产生平原纹理)。我们将遍历我们纹理地图的每个像素,找到高度符合该像素的并计算出每个纹理tile的像素。(每个tile有一个规定好的区域。)非常罕见的tile将被100%的可见,所有我们需要合并tile到其他tile(在RGB颜色上进行插值)。这个结果将如图3.5,你可以看到插值是在草地和岩石tile间进行的。
The Region System 系统区域
开始编码前,我们需要创建一个表示每个tile信息的结构。一个region, 被使用,是一系列定义好高度值范围的三个值。这个结果我们创建出来:
struct STRN_TEXTURE_REGIONS
{
int m_iLowHeight; // lowest possible height(0%)
int m_iOptimalHeight; // optimal height(100%)
int m_iHighHeight; // highest possible height(0%)
};
解释每个值意思将由图3.6来完成。
根据解释, 我们将让m_iLowHeight等于63并且m_iOptimalHeight等于128。 计算m_iHighHeight的值需要些简单的数学。我们将让m_iOptimalHeight减去m_iLowHeight。 然后我们将加上m_iOptimalHeight和前一个处理的值。我们有我们的范围集(low:63, optimal:128, high:193), 所以如图3.6那样替换那些值。 现在想象一下怎样描绘当前tile的高度值, 说, 150。 想象一这个值如图3.6在线上所示, 计算出我们的范围。To save you the trouble of trying to figure it out, 如图3.7。
正如你看到的图像, 纹理presence在高度(150)时为70%。 现在我们知道许多信息, 那我们怎么做呢,我们计算出RGB三个一组从纹理图内并且乘以0.7f。 这个结果正似乎我们当前像素想要的值。
我们需要创建一个函数将计算出提供给我们的百分区域。 这个函数相当简单。它需要两个微不足道的测试看这个高度是否真的在这个范围内;如果不在这个范围内, 退出函数。 接着,我们需要标记出高度的位于哪个区域。它在最佳值之下还是之上,或者等于最佳值?微不足道的情况是高度等于最佳值;如果是这样, 那么当前tile纹理presence为当前像素的100%, 而且我们不需要担心插值过程。
如果高度在最佳值之下, 我们需要减少这些片段的值。 之后,我们用这个高度值减去这个区域较低的值。然后我们最理想的范围值是减去低范围值。我们然后从第一个计算到最后一个计算值划分这个结果, 恩迷糊! 我们有了我们的百分比presence。
这是我刚刚讨论的代码:
// 高度是位于最佳值之下
if(ucHeight<m_tiles.m_regions[tileType].m_iOptimalHeight)
{
// calculate the texture percentage for the given tile’s region
fTemp1 = (float)m_tiles.m_regions[tileType].m_iLowHeight – ucHeight;
fTemp2 = (float)m_tiles.m_regions[tileType].m_iOptimalHeight – m_tiles.m_regions[tileType].m_iLowHeight;
return (fTemp1/fTemp2);
}
最后的情况是如果这个高度值在最佳范围之上。计算就要考虑的复杂些比当高度在范围下复杂,但是他们仍然不是非常难的。代码形式比文字更容易理解:
// height is above the optimal height
else if(ucHeight>m_tiles.m_regions[tileType].m_iOptimalHeight)
{
// calculate the texture percentage for the given tile’s region
fTemp1 = (float)m_tiles.m_regions[tileType].m_iHighHeight – m_tiles.m_regions[tileType].m_iOptimalHeight;
return ((fTemp1 – (ucHeight – m_tiles.m_regions[tileType].m_iOptimalHeight))/fTemp1);
}
这个计算,理论上,是基于同样的低比最佳,高好的情况,除了我们必须获取场景中100%时的小部分值,它低于某高度,取代的值高过某高度!
The Tile System Tile系统
好的,现在你知道如何从一个纹理tile内获得纹理的presence了和纹理像素内。现在你需要应用你刚刚学到的来为每个纹理tiles加上这个计算并创建出整个纹理地图。这虽然比听起来容易,所以别受打击!
首先,我们需要创建纹理tile结构以管理所有的纹理tiles。每个tile我们不需要更多的信息;所有我们需要替换纹理信息值且每个纹理的结构区域。我们将也想明白tiles的数量,有多少个被加载。根据这些需求,我创建了STR_TEXTURE_TILES结构体,它是这样的:
struct STRN_TEXTURE_TILES
{
STRN_TEXTURE_REGIONS m_regions[TRN_NUM_TILES]; // texture regions
CIMAGE textureTiles[TRN_NUM_TILES]; // texture tiles
Int iNumTiles;
};
其次,你需要一些管理纹理tile的函数。我需要加载和卸载单个tile的函数,还有卸载所有tiles。这些函数的实现不值一提,所以我在这里不展示它们。如果你感兴趣可以去看代码。除此之外,你要阅读地形生成函数!
开始生成函数,我们需要计算出实际加载多少纹理。(我们想要用户可以没有纹理时产生纹理。)之后要完成它,我们需要重新循环的计算出区域范围内的每个tile。(我们想要tile区域被0-255的范围内隔开)。这现在我将这么做:
iLastHeight = -1;
for(i = 0; i<TRN_NUM_TILES; i++)
{
// we only want to perform these calculations if we
// actually have a tile loaded
if(m_tiles.textureTiles[i].IsLoaded())
{
// calculate the three height boundaries
m_tiles.m_regions[i].m_iLowHeight = iLastHeight+1;
iLastHeight+=255/m_tiles.iNumTiles;
m_tiles.m_regions[i].m_iOptimalHeight = iLastHeight;
m_tiles.m_regions[i].m_iHeighHeight = (iLastHeight – m_tiles.m_regions[i].m_iLowHeight) + iLastHeight;
}
}
The only thing that should look remotely odd here is the last segment where we calculate m_iHighHeight, 甚至它看上去不是个单数。(如果它是单数,就涉及到开始我解释的区域范围了。)
Creating the Texture Data 创建纹理数据
现在到了创建纹理数据的时候了。要做这个,我们需要创建三个不同的循环: 一个是纹理地图的Z轴,一个是X轴,还有每个tile。 (在函数内tile将是第三个循环。)我们也许哟啊创建三个保存运行时我们计算出的每像素的当前RGB分量。实际的生成纹理函数像这样:
for( z = 0; z<uSize; z++)
{
for(x = 0; x<uiSize; x++)
{
// set out total color counters to 0.0f
fTotalRed = 0.0f;
fTotalGreen = 0.0f;
fTotalBlue = 0.0f;
// loop through the tiles
// for the third time in this function
for(i=0; i<TRN_NUM_TILES;i++)
{
// if the tile is loaded, we can perform the calculations
if( m_tiles.textureTiles[i].IsLoaded())
{
}
}
}
}
下面,我们需要从纹理中提取出RGB值的分量(当前像素)到我们的临时RGB unsigned char变量。一旦完成,我们
需要标记出当前tile的presence在当前像素上(使用我们前面创建的函数), 临时RGB变量乘以结果,并加上我们的总RGB数量。现在我们需要设置前面解释的代码:
// get the current color in the texture at the coordinates that we
// got in GetTexCoords
m_Tiles.textureTiles[i].GetColor(uiTexX, uiTexZ, &ucRed, &ucGreen, &ucBlue);
// get the current coordinate’s blending percentage for this tile
fBlend[i] = RegionPercent( I, InterpolateHeight(x, z, fMapRatio ) );
// calculate the RGB values that will be used
fTotalRed += ucRed*fBlend[i];
fTotalGreen += ucGreen*fBlend[i];
fTotalBlue += ucBlue*fBlend[i];
之后我们需要循环处理四个tiles 我们然后设置纹理上的像素颜色,然后对下一像素重复这一过程。当我们完全完成产生纹理完成值时, 我们使用我们的图形API来设置!
Improving the Texture Generator 改进纹理生成器
好的,我要倒下了。我们还没有准备就绪。我们的纹理生成函数还有一些纹理。这些问题是:
n 我们仅仅可以使用一种分辨率或在这之下,或与我们高度图相同的方式来创建纹理。
n 如果解决这个问题,那么我们仅可以创建在我们的纹理tiles之下分辨率的tiles。
然而这两个问题是关联在一起的。让我们从高度图的分辨率开始。
Getting Rid of the Heightmap Resolution Dependency 摆脱高度图的分辨率依赖性
我需要让用户选择任何他想要的纹理大小。(好,几乎任何纹理大小。我想应该是2的N次方)。前面的纹理生成函数中,我们输入巨大的一系列循环,我们需要计算出高度图内纹理地图像素的比率,可以这样完成:
fMapRatio = (float)m_iSize/uiSize;
然后我们需要创建一个函数将这个值和我们展开的高度图进行插值。我们将插值划分成两个部分: 一部分是X轴到Z轴。我们将获取两个部分结果的平均值,作为我们插值的高度。最后也许最好的方式是这个东西,但是它可以工作,工作的很快!
这个函数,我们需要三个参数。前两个为X, Z坐标,我们获取它们。这非常high, 像这样,超过高度图范围。第三个是我们计算出的纹理图像素比率(fMapRatio)。函数内,我们将缩放(x,z)坐标使用比率。计算出X轴的插值像这样:
// set the middle boundary
ucLow = GetTrueHeightAtPoint( ( int )fScaledX, (int)fScaledZ );
// set the high boundary
if( (fScaledX+1) > m_iSize )
return ucLow;
else
ucHighX = GetTrueHeightAtPoint( (int)fScaledX+1, (int)fScaledZ);
// calculate the interpolation (For the X axis)
fInterpolation = (fScaledX – (int)fScaledX);
ucX = ((ucHighX-ucLow)*fInterpolation)+ucLow;
正如你看到的,我们这么做获取了低高度的值给第一个东西。然后我们核对看它是否超过高度图下一个高度。如果没有,那么我们必须满足低的值。如果下个高度在高度图上,那么我们可以获取它并准备为两个值插值。我们获取不同的浮点值X和unsigned char X值。(它将低值的精确到度,将定义出总的插值。)下个计算,我们计算沿着X轴的插值。然后同样为Z轴做一次,将两个结果合并,并除以2。这样就完成了。
Getting Rid of the Tile Resolution Dependency 摆脱Tile的分辨率依赖
好的,我们差不多了。我们仅仅才完成一个计算,我们还需要排除tile大小的限制。解决方法也许会让你惊讶,”为什么我这么说? “ 好,相信我,这需要很长时间的一个计算来解决,所以感觉很坏。我们需要重复tile!我创建了简单的函数来新建纹理坐标并重复到我们的纹理。它是这样的:
void CTERRAIN::GetTexCoords(CIMAGE texture, unsigned int* x, unsigned int* y)
{
unsigned int uiWidth = texture.GetWidth();
unsigned int uiHeight = texture.GetHeight();
int iRepeatX = -1;
int iRepeatY = -1;
int i = 0;
// loop until we figure out how many time the tile
// has repeated (on the X axis)
while(iRepeatX == -1)
{
i++;
//if x is less than the total width,
// then we found a winner!
if(*x<(uiWidth*i))
iRepeatX = i-1;
}
// prepare to figure out the repetition on the Y axis
i = 0;
// loop until we figure out how many times the tile has repeated
// (on the Y axis)
while(iRepeatY == -1)
{
i++;
// if y is less than the total height, then we have a bingo!
if( *y<(uiHeight*i))
iRepeatY = i-1;
}
// update the given texture coordinates
*x = *x-(uiWidth*iRepeatX);
*y = *y-(uiHeight*iRepeatY);
}
这个函数的大部分由两个while循环组成主要目的是为计算出需要重复多少个纹理,以在它到达纹理坐标时传递参数。之后计算,我们缩放他们使用纹理的范围值。(我们不想试着开出纹理范围外的信息。这将导致一个错误,错误是不好的。)
那么!我们的纹理产生函数现在完成了!如图3.8。这个demo, 你将注意到菜单里的新选项叫做纹理映射。这个选项,你可以产生高分辨率的纹理并保存纹理到当前目录。谈到这个demo, 你可以看到你努力工作的成果在CD内的Code"Chapter3"demo3_2. 打开工作区用Microsoft Visual C++,开始有趣的把玩吧。
Using Detail Maps 使用细节地图
1024x1024是相当大的数据量,我想在我们的纹理上达到这个细节。必须通过另外一种方式来达到我们不浪费资源又兼有这些细节愿望。好的,不要多想!一种酷的方式可以使用细节地图来为你的地形添加这些细节。细节地图是如图3.9那样的灰度图重复多次加到地形上并添加上细微差别,像裂缝,凹陷,岩石和其他有趣的东西。
为你的地形引擎添加细节地图支持是个简单的过程。添加一些加载和卸载细节图的管理函数,函数将允许用户决定在地形上重复填充多少次,然后你必须编辑你的渲染代码。最难的决定是是否使用硬件多纹理或仅仅使用两个分离的渲染过程。因为地形网格变得相当大,你最好坚持赌上硬件多纹理。实现硬件多纹理在这本书之外了,但是如果你不知道怎样利用你的图形API做的话,它的实现也是相当简单的,你将学习它。这种情况是你不知道一种好的地方去学习API,拿出OpenGL Game Programming(Astle/Hawkins)或Special Effect Game Programming with DirectX 8.0)McCuskey), 都是Premier Press出版的——一个伟大的出版商,所以我这么说!
编辑你的渲染代码,仅仅设置基本纹理颜色(例如,我们前面产生的) 作为第一个纹理单位,然后设置你的细节纹理到第二个纹理单位。记得怎样为纹理坐标添加上颜色是这样计算的?
fTexLeft = ( float )x/m_iSize;
fTexBottom = ( float)z/m_iSize;
fTexTop = ( float)(z+1)/m_iSize;
好的,我仅仅必须做下轻微的修改这些计算获得我们细节纹理的纹理坐标:
fTexLeft = (float)(x/m_iSize)*m_iRepeatDetailMap);
fTexBottom = (float)(z/m_iSize)*m_iRepeatDetailMap);
fTexTop = (float)((z+1)/m_iSize)*m_iRepeatDetailMap);
m_iRepeatDetailMap是用户希望重复填充地形的次数。最难的部分是使用你的图形API设置多纹理,大拿市如果你使用OpenGL, 我将都给你设置好。 (Yeah, 我知道我是个和蔼的家伙,并且如果你感激要报答我的话,我的生日在3月11日!)看你的地形上的细节地图,使用256x256的程序式纹理没有使用细节图(如图3.10左边) 而使用了256x256细节图的在3.10右边。看这么多的细节看来是正确的?最好的部分是添加这些昂贵的细节是简单的。拿出CD上Code"Chapter3"demo3_3下的demo3_3, 在运动中看新的细节图。仅仅是改变了休息T关闭细节图, D开起细节。(默认是开启的。)
摘要 Summary
我们学习了一些关于纹理化地形的东西。我们从简单的纹理出发(延长但个纹理覆盖整个地形),然后我们踢开它调整到程序式纹理生成。我们最后使用了简单而酷的技术叫做细节贴图。在下一章,我们将学习提高我们地形更多真实感的步骤: 光照化。如果喜欢纹理技术,你也许想看到Tobias Franke的文章标题”Terrain Textuere Generation” 或者Yordan Gyurchev’s 的文章标题”Generating Terrain Textures.”你也许对Jeff Lander的文章标题”Terrain Texturing,”感兴趣,描述了动态纹理tiling的解决方法。
参考 Reference
1 Franke, Tobias. “Terrain Texture Generation. “ 2001
http://www.flipcode.com/tutorials/tut_proctext.shtml
2. Gyurchev, Yordan, “Generaing Terrain Textures. “ 2001.
http://www.flipcode.com/tutorials/tut_terrtex.shtml
3. Lander, Jeff. “Terrain Texturing. “ Delphi3D-Rapid OpenGL
Development. 2002. http://www.delphi3d.net/articles/viewarticle.php?articel=terrainex.htm