本文主要讲的是《天龙八部》游戏中水面(TerrainLiquid)的具体实现,使用C++, Ogre1.6 。
天龙的水面做的比较简单,虽然没有倒影,但动态纹理+深度图做出的效果还行,看着不是特别假。
一般情况下,TerrainLiquid有一层动态纹理,有的还会有一层1D深度图纹理,深度图纹理用来控制不同深度水面的透明度。另外还会给出一个坐标,可以称之为种子坐标,通过这个坐标可以填充整个水面。总的来说要实现天龙的水面只要搞清楚两个问题
1.如何利用种子坐标填充整个水面
2.如何利用深度图纹理控制水面透明图
文章最后我放了TerrainLiquid的代码的链接,配合上篇随笔给的地形Demo代码再加上水面相关的资源,很容易就能在那基础上加上水面效果。
TerrainLiquid格式
<Object type="TerrainLiquid"><Property name="material" value="haihuwater"/><Property name="position" value="2500 636 -4901"/><Property name="texture scale" value="0.25"/><Property name="depth texture layer.enable" value="true"/><Property name="depth texture layer.height scale" value="0.008"/></Object>
上面是一个典型的TerrainLiquid的例子,
material,不用说了,材质
position就是我上面说的种子坐标,天龙不给出整个水面覆盖的范围,而只给出这个坐标,载入场景时实时填充
texture scale,这个值是用来确定第一层纹理坐标的,假设某个点与种子间隔(x,y)个顶点,则该点第一层动态纹理的坐标为(x*texture scale, y*texture scale)。
depth texture layer.enable,这一项如果是true的时候,说明要用深度图。
depth texture layer.height scale,是用来确定水面上某点的深度和该点的透明度间的关系,深度*这个值=透明度。
水面填充
开始要实现水面的时候,我首先想的很简单,弄四个点,一个平面,动态贴图一贴,完了。后来发现没那么简单,水面不能用一个长方形来做,多看几个场景就能发现,这个肯定是不合适的。Google了一下,看了几个大牛的博客,知道水面应该用填充算法来生成,可惜大牛们都不贴代码,估计觉得太简单了吧…… 只好自己实现一下。
我用的填充算法比较简单,递归… 没有任何优化,但很易懂,很简单。 一般如果不是大的变态的水面应该没有问题,而且这个填充的过程是在载入场景的过程中,也没有什么优化的必要,估计再快也就快个一两秒吧。
下面是核心的填充代码,很简单吧…
void TerrainLiquid::__spreed( int x, int z, int direction )
{
// 判断是否已包含该点和该点是否应该被看做水面的一部分
if( !__isGridContained( x, z ) && __isValidGrid( x,z, direction ) )
__addGrid( x, z );
else
return;
__spreed( x, z-1, UP );
__spreed( x, z+1 , DOWN );
__spreed( x-1, z, LEFT );
__spreed( x+1, z , RIGHT );
}
__isValidGrid()用来判断该点是否是水面的一部分,简单点说就是判断地形上的这一点是否高于种子的高度,实际上判断还是有点复杂的,如果单纯判断点的当前点的高度遇到复杂一点的水面情况就会出BUG,我的做法是分不同的方向分别判断。具体代码如下:
bool TerrainLiquid::__isValidGrid( int x, int z, int dir )
{
int y = mSeedPos.y;
int left = mTerrainInfo->getOffset().x;
int right = left + (mTerrainInfo->getWidth()-1)*mTerrainInfo->getScaling().x;
int top = mTerrainInfo->getOffset().z;
int bottom = top + (mTerrainInfo->getHeight()-1)*mTerrainInfo->getScaling().y;
Ogre::Vector3 leftTop = __getPos( x,z );
Ogre::Vector3 rightTop = __getPos( x+1, z );
Ogre::Vector3 leftBottom = __getPos( x,z+1);
Ogre::Vector3 rightBottom = __getPos( x+1, z+1 );
int lt = mTerrainInfo->getHeightAt( leftTop.x, leftTop.z );
int rt = mTerrainInfo->getHeightAt( rightTop.x, rightTop.z );
int lb = mTerrainInfo->getHeightAt( leftBottom.x, leftBottom.z );
int rb = mTerrainInfo->getHeightAt( rightBottom.x, rightBottom.z );
// bounding check
if( leftTop.x < left || rightTop.x > right || leftTop.z < top || leftBottom.z > bottom )
return false;
if( lt > leftTop.y && rt > rightTop.y && lb > leftBottom.y && rb > rightBottom.y )
return false;
else if( dir == LEFT )
{
if( ( lt < y || lb < y ) && ( rt >=y && rb >=y) )
return false;
}
else if( dir == RIGHT )
{
if( ( rt < y || rb < y ) && ( lt >= y && lb >= y ) )
return false;
}
else if( dir == UP )
{
if( ( rt < y || lt < y ) && ( rb >= y && lb >= y ) )
return false;
}
else if( dir == DOWN )
{
if( ( rb < y || lb < y ) && ( rt >=y && lt >= y ) )
return false;
}
return true;
}