本文主要講的是《天龍八部》遊戲中水面(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; }
首先判斷四個點對應的地形的高度是否都大於種子高度,若大於則返回false
然後判斷是否超出地圖邊界,超出則返回false
再分四個方向判斷前兩點和後兩點的高度,若前兩點有一點或兩點地形高度小於種子高度,且後兩點地形高度都大於種子高度,則返回false
具體爲什麼這麼判斷比較難描述… 反正在邊界比較窄的情況下,若不這樣判斷就會檢測不到水面的邊界。
水面透明度處理
當水面填充做好以後,這個就不難處理了,就是在每個頂點生成的時候設置紋理座標。第一層紋理是動態紋理,第二層是一維的深度圖紋理。第一層紋理座標的設定要根據 TerrainLiquid 中 texture_scale值來確定。由於Ogre默認的紋理映射方式是wrap,就是說Any value beyond 1.0 wraps back to 0.0. Texture is repeated (引用自Ogre官網的Manual). 我們不需要考慮紋理座標大於1或者小於0的狀況,它自己會映射到正確的位置,所以我們只要將(x,y)處點的紋理座標設爲( x*texture_scale, y*texture_scale )就OK了。
第二層紋理的形式是這樣的:(中間那一條)
在ps裏面看一下可以發現,這是一張寬度爲256,高度爲1的一維紋理,有4通道,每個通道的值都是從0-255遞增。不難推測,我們需要用水面的深度情況來取一個值作爲水面的透明度。
從水面的紋理的材質可以看出,第二層紋理的映射方式爲clamp,所以紋理座標在在大於1.0時,會映射到1.0.所以( depth texture layer.height scale*水面深度)求出的就是第二層的紋理座標。水面深度=種子座標高度-當前點的地形高度。
要注意的是第二層紋理一定要聲明爲1D的。
下面是頂點格式的聲明,有FLOAT3的頂點座標,FLOAT3的法線方向(統一向上),FLOAT2的一層紋理,FLOAT1的二層紋理。
decl->addElement( MAIN_BINDING, offset, VET_FLOAT3, VES_POSITION ); offset+= Ogre::VertexElement::getTypeSize( VET_FLOAT3 ); decl->addElement( MAIN_BINDING, offset, VET_FLOAT3, VES_NORMAL ); offset+= Ogre::VertexElement::getTypeSize( VET_FLOAT3 ); decl->addElement( MAIN_BINDING, offset, VET_FLOAT2, VES_TEXTURE_COORDINATES, 0 ); offset+= Ogre::VertexElement::getTypeSize( VET_FLOAT2 ); if( m_bDepthEnable ) decl->addElement( MAIN_BINDING, offset, VET_FLOAT1, VES_TEXTURE_COORDINATES, 1 );
將TerrainLiquid添加到地形中
在原來地形Demo中載入場景的函數中適當位置添加這一段應該就沒問題了。
else if( IsStrEqual( "TerrainLiquid", strTemp ) ) { TerrainLiquid* pTerrainLiquid = new TerrainLiquid; SceneNode* pSsceneNode = m_pSceneManager->getRootSceneNode()->createChildSceneNode( "terrain_liquid" + StringConverter::toString( staticIndex++ ) ); TiXmlElement* propriety = element->FirstChildElement( "Property" ); float x,y,z; float texture_scale = 0.0f; float depth_scale = 0.0f; bool depth_enable = false; while( propriety ) { strTemp = propriety->Attribute( "name" ); sValue = propriety->Attribute( "value" ); if( IsStrEqual("material", strTemp ) ) { sValue = UTF8ToANSI(sValue); pTerrainLiquid->setMaterial( sValue ); delete[] sValue; } else if( IsStrEqual( "position", strTemp ) ) { sscanf( sValue, "%f %f %f", &x, &y, &z ); pSsceneNode->setPosition( x, y, z ); } else if( IsStrEqual( "texture scale", strTemp ) ) { sscanf( sValue, "%f", &texture_scale ); } else if( IsStrEqual( "depth texture layer.enable", strTemp ) ) { if( IsStrEqual( sValue, "true")) depth_enable = true; else depth_enable = false; } else if( IsStrEqual( "depth texture layer.height scale", strTemp ) ) { sscanf( sValue, "%f", &depth_scale ); } else ThrowException( "TerrainLiquid", strTemp/Files/syqking/TerrainLiquid_src.rar ); propriety = propriety->NextSiblingElement(); } pTerrainLiquid->createTerrainLiquid( Ogre::Vector3( x,y,z ), texture_scale, depth_enable, depth_scale, mTerrainMgr->getTerrainInfo() ); m_pSceneManager->getRootSceneNode()->createChildSceneNode()->attachObject(pTerrainLiquid); }