地形渲染的动态LOD四叉树算法详细实现

时间:2022-09-10 17:00:16

地形渲染的动态LOD四叉树算法详细实现

                                                                                    作者:野草 email:JerryLi@sina.com

声明:请将本文档和程序配合使用,旨在使读者费很少的时间和精力就能理解此算法。读者应该熟悉递归程序设计,以及基本的VC OpenGL编程.

渲染地形时,如果采用固定地形精度等级,则会随着地形面积的增大,渲染的三角形数量会随着面积的增大而增长。采用动态精度等级,越远处的地形画的三角形面积越大(精度越低),越近的地形面积越小(精度越高),坡度高的地方精度高,平坦的地方精度低,这样能有效地减少画三角形的数量,提高游戏引擎的速度.

采用四叉树算法,可有效把地形不在视野内的网格去掉,降低程序的时间复杂度

其基本思路是:先把地形一分为四,用递归的方法对每个网格渲染。对每个网格,如果达到最高精度,则退出;如果不在视野内,也退出。再对符合条件的网格递归下去。

渲染网格的方法是用GL_TRIANGLE_FAN方法,如下图

地形渲染的动态LOD四叉树算法详细实现

但是这样在相邻精度等级相差1倍或1倍以上时,渲染的地形会出现裂缝。

地形渲染的动态LOD四叉树算法详细实现

 

解决方法见下图:

 

地形渲染的动态LOD四叉树算法详细实现

这样相邻的网格可以相差任意级精度

参见下面的截图:看中间的那个大正方形。

 

地形渲染的动态LOD四叉树算法详细实现

 

 

                  

 

程序如下:

 

 

 

void CTerrain::Render()

{

    ………

//细分标记数组清0(这里为了节约内存,一个网格用一个bit表示)

m_cFlag.Reset();  

//在细分标记数组记录需要细分的网格

Update((m_nMapSize-1)/2,(m_nMapSize-1)/2,(m_nMapSize-1)/2,1);

        //根据细分标记数组递归渲染网格

        RenderQuad((m_nMapSize-1)/2,(m_nMapSize-1)/2,(m_nMapSize-1)/2);

        …….    

}

 

//渲染网格的递归程序

//参数:

//                 nXCenter: 网格中心点坐的X

//                 nZCenter: 网格中心点坐的Y

//                 nSize:网格大小

//返回:渲染的三角形数量

//使用前提:m_cFlag需要细分数组要先调用Update函数记录好.

int CTerrain::RenderQuad(int nXCenter,int nZCenter,int nSize)

{

                   int nCount = 0;

                   int nH0 = Height(nXCenter,nZCenter);

                   int nH1 = Height(nXCenter-nSize,nZCenter-nSize);

                   int nH2 = Height(nXCenter+nSize,nZCenter-nSize);

                   int nH3 = Height(nXCenter+nSize,nZCenter+nSize);

                   int nH4 = Height(nXCenter-nSize,nZCenter+nSize);

                   //求网格5个点中高度最大和最小值

                   int nMax = nH0,nMin = nH0;

                   if(nMax<nH1)nMax = nH1;

                   if(nMax<nH2)nMax = nH2;

                   if(nMax<nH3)nMax = nH3;

                   if(nMax<nH4)nMax = nH4;

                   if(nMin>nH1)nMin = nH1;

                   if(nMin>nH2)nMin = nH2;

                   if(nMin>nH3)nMin = nH3;

                   if(nMin>nH4)nMin = nH4;

                   SIZE size;

                   size.cx = nSize;size.cy=(nMax-nMin)/2;

                   //如果此网格不在视野内,返回

                   if(!m_pFrustum->CubeInFrustum(nXCenter,(nMax+nMin)/2,nZCenter,max(size.cx,size.cy)))

                     return 0;

                   if(m_cFlag.IsTrue(nXCenter,nZCenter))//需要细分,递归渲染4个子网格

                   {

                     nCount += RenderQuad(nXCenter-nSize/2,nZCenter-nSize/2,nSize/2);

                     nCount += RenderQuad(nXCenter+nSize/2,nZCenter-nSize/2,nSize/2);

                     nCount += RenderQuad(nXCenter+nSize/2,nZCenter+nSize/2,nSize/2);

                     nCount += RenderQuad(nXCenter-nSize/2,nZCenter+nSize/2,nSize/2);

                   }

                   else//渲染此网格

                   {

                     glBegin( GL_TRIANGLE_FAN );

                     SetTextureCoord(nXCenter,nZCenter);  // center

                     glVertex3i(nXCenter, nH0, nZCenter);

 

                     SetTextureCoord(nXCenter-nSize,nZCenter-nSize); //left top

                     glVertex3i(nXCenter-nSize,nH1 , nZCenter-nSize);

                     //为了避免出现裂缝而多画三角形扇(就是多加那些红线啦)

                     RemedyTop(nXCenter,nZCenter,nSize);

                    

                     SetTextureCoord(nXCenter+nSize,nZCenter-nSize); //right top

                     glVertex3i(nXCenter+nSize, nH2, nZCenter-nSize);

                     //为了避免出现裂缝而多画三角形扇(就是多加那些红线啦)

                     RemedyRight(nXCenter,nZCenter,nSize);

 

                     SetTextureCoord(nXCenter+nSize,nZCenter+nSize); //right bottom

                     glVertex3i(nXCenter+nSize, nH3, nZCenter+nSize);

                     //为了避免出现裂缝而多画三角形扇(就是多加那些红线啦)

                     RemedyBottom(nXCenter,nZCenter,nSize);

 

                     SetTextureCoord(nXCenter-nSize,nZCenter+nSize);//left bottom

                     glVertex3i(nXCenter-nSize, nH4, nZCenter+nSize);

                     //为了避免出现裂缝而多画三角形扇(就是多加那些红线啦)

                      RemedyLeft(nXCenter,nZCenter,nSize);

 

                     SetTextureCoord(nXCenter-nSize,nZCenter-nSize); //left top

                     glVertex3i(nXCenter-nSize, nH1, nZCenter-nSize);

                     glEnd();

                     nCount += 4;//这里为求简单,计算渲染的三角形数目并不是十分准确,比实际要小些

                   }

                   return nCount;

}

 

 

 

 

//更新细分记录数组的递归函数

//参数:

//                 nXCenter: 网格中心点坐的X

//                 nZCenter: 网格中心点坐的Y

//                 nSize:网格大小

//  nLevel:当前的细分等级(其实可由nSize计算出来)    

void CTerrain::Update(int nXCenter,int nZCenter,int nSize,int nLevel)

{

                   CVector3 vPos = m_pCamera->GetPos();

//                 int nSize = (m_nMapSize-1)/pow(2,nLevel);//由细节级度得到当前渲染矩形的大小

                   //求网格5个点中高度最大和最小值

                   int nMax = Height(nXCenter,nZCenter);

                   int nMin = nMax;

                   int nH1 = Height(nXCenter-nSize,nZCenter-nSize);

                   int nH2 = Height(nXCenter+nSize,nZCenter-nSize);

                   int nH3 = Height(nXCenter+nSize,nZCenter+nSize);

                   int nH4 = Height(nXCenter-nSize,nZCenter+nSize);

                   if(nMax<nH1)nMax = nH1;

                   if(nMax<nH2)nMax = nH2;

                   if(nMax<nH3)nMax = nH3;

                   if(nMax<nH4)nMax = nH4;

                   if(nMin>nH1)nMin = nH1;

                   if(nMin>nH2)nMin = nH2;

                   if(nMin>nH3)nMin = nH3;

                   if(nMin>nH4)nMin = nH4;

                   //网格的中心点坐标

                   CVector3 vDst(nXCenter,(nMax+nMin)/2,nZCenter);

                   SIZE size;

                   size.cx = nSize;size.cy=(nMax-nMin)/2;

                   //如果此网格不在视野内,返回

                   if(!m_pFrustum->CubeInFrustum(nXCenter,(nMax+nMin)/2,nZCenter,max(size.cx,size.cy)))

                     return ;

                   //视点到网格中心点的距离

                   int nDist = Dist(vPos,vDst);

                   if(nDist>2000)//距离大于2000时才将距离和误差(坡度)2个因素考虑

                   {

                     //与距离成正比,与误差(坡度)成反比,还与当前细分等级有关

                     if(70.0*Dist(vPos,vDst)/nSize/CalcError(nXCenter,nZCenter,nSize)>nLevel)

                            return;//此网格不需要细分

                   }

                   else//近距离(<2000)时只考虑误差(坡度)因素(因为动态LOD会造成地表呼吸现象,为减弱此现象不考虑距离因素)

                   {

                     if(CalcError(nXCenter,nZCenter,nSize)<30)

                            return;//此网格不需要细分

                   }

                   if(nSize!=MIN_GRID)

                   { ////此网格需要细分

                     m_cFlag.Set(nXCenter,nZCenter,true);//把细分数组相应位设为true

                     //递归4个子网格

                     Update(nXCenter-nSize/2,nZCenter-nSize/2,nSize/2,nLevel+1);

                     Update(nXCenter+nSize/2,nZCenter-nSize/2,nSize/2,nLevel+1);

                     Update(nXCenter+nSize/2,nZCenter+nSize/2,nSize/2,nLevel+1);

                     Update(nXCenter-nSize/2,nZCenter+nSize/2,nSize/2,nLevel+1);

                   }

                   //此网格不需要细分

}

 

//计算误差(坡度)的函数

//参数:

//                 nXCenter: 网格中心点坐的X

//                 nZCenter: 网格中心点坐的Y

//                 nSize:网格大小

//返回: 误差大小

//说明:只是计算某些点细分与不细分的高度值差的和

inline int CTerrain::CalcError(int nXCenter,int nZCenter,int nSize)

{

                   int nError = 0;

                   int nH0 = Height(nXCenter,nZCenter);

                   int nH1 = Height(nXCenter-nSize,nZCenter-nSize);

                   int nH2 = Height(nXCenter+nSize,nZCenter-nSize);

                   int nH3 = Height(nXCenter+nSize,nZCenter+nSize);

                   int nH4 = Height(nXCenter-nSize,nZCenter+nSize);

                   nError += abs(Height(nXCenter,nZCenter-nSize)-(nH1+nH2)/2);

                   nError += abs(Height(nXCenter+nSize,nZCenter)-(nH2+nH3)/2);

                   nError += abs(Height(nXCenter,nZCenter+nSize)-(nH3+nH4)/2);

                   nError += abs(Height(nXCenter-nSize,nZCenter)-(nH4+nH1)/2);

 

                   nError +=abs(Height(nXCenter-nSize/2,nZCenter-nSize/2)-(nH0+nH1)/2);

                   nError +=abs(Height(nXCenter+nSize/2,nZCenter-nSize/2)-(nH0+nH2)/2);

                   nError +=abs(Height(nXCenter+nSize/2,nZCenter+nSize/2)-(nH0+nH3)/2);

                   nError +=abs(Height(nXCenter-nSize/2,nZCenter+nSize/2)-(nH0+nH4)/2);

                   return nError;

}

如下图:

地形渲染的动态LOD四叉树算法详细实现

当不细分时红点的高度值是它所在线段的两个端点的平均值,细分时就是它实际的高度值,2个值是不一样的. 两者这间的高度差就是误差,把所有红点的误差相加就是此网格的误差

 

//修补网格顶部裂缝的函数

//参数:

//                 nXCenter: 网格中心点坐的X

//                 nZCenter: 网格中心点坐的Y

//                 nSize:网格大小

void CTerrain::RemedyTop(int nXCenter,int nZCenter,int nSize)

{

                   if(nZCenter-2*nSize>=0)//保证寻址不越界

                   {

                     if(!m_cFlag.IsTrue(nXCenter,nZCenter-2*nSize))//此网格相邻的顶部网格(同级的)没有细分

                            return;

                   }

                   else

                     return;

                   //此网格相邻的顶部网格(同级的)已经细分

                   //对顶部一分为二递归(跨级精度的情况)

                   RemedyTop(nXCenter-nSize/2,nZCenter-nSize/2,nSize/2);

                   SetTextureCoord(nXCenter,nZCenter-nSize);

                   glVertex3i(nXCenter,Height(nXCenter,nZCenter-nSize),nZCenter-nSize);

                   RemedyTop(nXCenter+nSize/2,nZCenter-nSize/2,nSize/2);

}

其它的修补裂缝的函数也是一样的流程.

 

 

 

近处没有考虑距离因素,只考虑误差(坡度)因素

地形渲染的动态LOD四叉树算法详细实现

 

距离越运,精度越低

地形渲染的动态LOD四叉树算法详细实现

 

感谢你的阅读,如果你有什么更好的思路或更好的算法(比如能消除地表呼吸现象),请和我联系:JerryLi@sina.com