用Ogre實現《天龍八部》場景中水面(TerrainLiquid)詳解

 

本文主要講的是《天龍八部》遊戲中水面(TerrainLiquid)的具體實現,使用C++, Ogre1.6 。 

天龍的水面做的比較簡單,雖然沒有倒影,但動態紋理+深度圖做出的效果還行,看着不是特別假。

一般情況下,TerrainLiquid有一層動態紋理,有的還會有一層1D深度圖紋理,深度圖紋理用來控制不同深度水面的透明度。另外還會給出一個座標,可以稱之爲種子座標,通過這個座標可以填充整個水面。總的來說要實現天龍的水面只要搞清楚兩個問題

1.如何利用種子座標填充整個水面

2.如何利用深度圖紋理控制水面透明圖

 

文章最後我放了TerrainLiquid的代碼的鏈接,配合上篇隨筆給的地形Demo代碼再加上水面相關的資源,很容易就能在那基礎上加上水面效果。

TerrainLiquid 

 

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了。

第二層紋理的形式是這樣的:(中間那一條)

 image

在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);
} 
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章