LOD地形设计

LOD地形设计(一) ZZ
2008-04-07 20:40
在大规模的三维场景中,不可能一次渲染所有的三角形,而且即使能做到这点,全部渲染也是不可取的。常用的做法就是采用LOD,即层次细节模型。距离视点较远的三角形可以大一些,粗糙一些,而距离视点较近的三角形则应有较为细腻地表现。常用的LOD地形的实现算法是四叉树算法,即对二维地平面进行分割时,每次把正方形分成4个等分的小正方形,直到分割的正方形尺寸达到某个阈值为止,然后对不能再分的正方形进行三角形剖分渲染。

由于正方形的右下角位于视截体内部(图1),需要把正方形进行分割,图2为第一次分割的后的状态。由于右下角正方形位于视截体内部,所以进行第二次分割,见图3。直到第3次分割(图4),才有小正方形不在视截体内,既不要细致渲染的正方形。如此不断分割,地平面会出现若干大小不等的正方形。为了记录正方形哪些是要分割的,哪些是不要分割的,需要设置一个标志,为此封装一个类来管理标志,该类记为Bit。

 

 

    为了判断一个节点区域是否在视截体内部,应该有一个管理视截体数据的类CFrustum。该类应该有视截体的六个平面方程,以及检测物体是否落在视截体内的函数。由于LOD地形是和视点相关的,距离视点进的三角形需要细腻渲染,而较远的则则可以稍微粗糙一些。因此一个正方形距离视点的距离d和自身的边长e应该是一个指标来决定改正方形是否需要细分,d/e < C1时,需要进一步细分,否则不需要进一步细分。当C1的值越大,e的值越小,节点细节较为细致;相反,C1越小,e值越大,节点细节与粗糙。正方形是否需要进一步分割,还和地形的起伏有关系,当地势平坦时,节点细节可以较少,反之节点细节应该较为丰富。r=MAX(正方形四个定点和中心)-MIN(正方形四个定点和中心)。r/e < C2时,e较大,需要分割。C2越大,e越小,层次越细腻,反之,C2越小,e越大,可以少一些细节。称C1为距离分辨率, C2为高度分辨率。

既然LOD和视点相关,就必须有一个类来记录视点,把该类记为Camera,该类应该包含视点相关数据,如视点位置,视线目标,视点如何移动,旋转等。

 


LOD地形的四叉树算法原理就是对地形进行四叉树分割,同时检查该节点是否位于视截体内部,如果在视截体内部且满足视距,周围点高程误差等条件时,则对该节点继续分割,否则不予分割。其中重点是视截体的计算,以及地形的分割及渲染。下面介绍几个系统中用到的类。
首先介绍标志节点是否分割的类Bit
类定义:

//该类根据节点的位置,为每个节点在标志段里相应位设一个标识。
/***********************************************************************
*    Copyrights Reserved by QinGeSoftware
*    Author : Qinge
*    Filename : Bit.h 1.0
*    Date: 2008-1-10
************************************************************************/
#pragma once

class Bit
{
public:
    void SetScale(int nScale);                        //伸缩系数
    void Set(int x, int y, BOOL bFlog=TRUE);          //设置标志位
    void Reset();                                     //标志清零
    BOOL CreateBits(int nXBites, int nRows);          //创建标志数组
    BOOL IsTrue(int x, int y);                        //查询该位标志
public:
    Bit();
    virtual ~Bit(void);
private:
    unsigned char *m_pBits;   //存储位标志的指针
    int m_nXBytes;            //X方向的字节数
    int m_nZRows;             //Z方向的行数
    int m_nScale;             //伸缩系数
};



//类实现文件
/***********************************************************************
*    Copyrights Reserved by QinGeSoftware
*    Author : Qinge
*    Filename : Bit.cpp 1.0
*    Date: 2008-1-10
************************************************************************/
#include "StdAfx.h"
#include "Bit.h"

Bit::Bit(void)
{
    m_pBits = NULL;                                 //指针初始化为NULL    
    m_nXBytes = 0;
    m_nZRows = 0;
    m_nScale = 1;                                        //不能初始化为0,因为是除数

}

Bit::~Bit(void)
{
    if(m_pBits != NULL)
    {
        delete [] m_pBits;                              //释放指针
        m_pBits = NULL;                                 //置为空,否则会成为野指针
    }
}

BOOL Bit::CreateBits(int nXBites, int nRows)
{
    //nXBits 必须是8的倍数
    m_nXBytes = nXBites/8+1;                            //想想为什么加1
    m_nZRows   = nRows;
    m_pBits = new unsigned char[m_nXBytes * m_nZRows]; //分配空间
    memset(m_pBits, 0, m_nZRows * m_nXBytes);         //标志段全部初始化0
    return 0;
}

void Bit::SetScale(int nScale)
{
    m_nScale = nScale;                                 //提供操作私有变量的接口
}

void Bit::Set(int x, int y, BOOL bFlog )
{
    x = x / m_nScale;                                 //每隔m_nScale采样
    y = y / m_nScale;
    unsigned char &c = m_pBits[y * m_nXBytes + x/8]; //获得某字符的引用,注意赋值方式,否则
    unsigned char d = 0x80;                           //后面改了白该。
    d = d >>(x%8);                                    //根据X值得不同,首位右移相应位数。移位
                                                       // 使得每个节点对应一位。 
    if(bFlog)
    {
        c|=d;                                         //把字符C与X相应的位置为1

    }
    else
    {
        d = ~d;                                       //和某节点对应的位为0,其余位为1
        c &= d;                                     //把字符C与X相应的位置为0

    }

}

void Bit::Reset()
{
    memset(m_pBits, 0, m_nXBytes * m_nZRows);

}

BOOL Bit::IsTrue(int x, int y)
{
   x = x/m_nScale;
   y = y/m_nScale;
   unsigned char c = m_pBits[y*m_nXBytes+x/8];                     //这次不是引用,想想为什么
   unsigned char d = 0x80;                                           
   c = c << (x%8);                                                   //为什么不是d移位?
   return c&d;            //把与X对应的位返回,其余位为0


}
//该函数得到字符包含包含8个节点的标志,必须根据X的值进行移位方能找到对应的节点,这次是取得标识而不是设置标识,故不用引用。c移位而不是d移位,是为了把标识移到首位。然后和0x80进行位与操作得到BOOL值。d移位操作效果是一样的,但不是左移而是右移。

 

LOD地形根据视点的变化决定是否进行网格分割,因此系统应设计一个视点类,来管理视点相关的数据。这节介绍的视点类是通用的,在很多网站都可下到这个类的代码,它可以用在OPENGL编程的各个场合,当然朋友也可根据需要自己增加相应功能!
/***********************************************************************
*    Copyrights Reserved by QinGeSoftware
*    Author : Qinge
*    Filename : Camera.h 1.0
*    Date: 2008-1-10
************************************************************************/
#pragma once
#include "Vector3.h"
class Camera
{
public:
    Camera(void);
    virtual ~Camera(void);
public:
    CVector3 GetPosition(){return m_vPosition;}                        //获得摄像机位置
    CVector3    GetView(){return m_vView;}                             //获得视线目标点
    CVector3 GetUpVector() {return m_vUpVector;}                       //获得向上方向
    CVector3 GetStrafe() {return m_vStrafe;}                           //获得平移方向的单位向量

    void PosotionCamera(float positionX, float positionY, float positionZ,
                         float viewX,     float viewY,     float viewZ,
                        float upVectorX, float upVectorY, float upVectorZ); //初始化摄像机
    void RotateView(float angle, float X, float Y, float Z);    //绕(x,y,z)旋转angle
    void SetViewByMouse();                                         //通过鼠标旋转场景
    void RotateAroundPoint(CVector3 vCenter, float X, float Y, float Z);   //绕点旋转
    void StrafeCamera(float speed);                                        //平移摄像机
    void MoveCamera(float speed);                                        //沿视线方向移动摄像机
    void Look();                                                    //设置视点相当于glLookAt()
    void Update();                                                       //更新视点位置。
    void CheckForMovement();                                           //检查是否有视点变量更新
private:
    CVector3 m_vPosition;       //摄像机视点
    CVector3 m_vView;           //摄像机视线
    CVector3 m_vUpVector;       //摄像机向上方向
    CVector3 m_vStrafe;        //摄像机平移
    const float fSpeed;        //摄像机移动速度
};


/***********************************************************************
*    Copyrights Reserved by QinGeSoftware
*    Author : Qinge
*    Filename : Camera.cpp 1.0
*    Date: 2008-1-10
************************************************************************/
#include "StdAfx.h"
#include "Camera.h"

Camera::Camera(void):fSpeed(5.0f)
{

m_vPosition = CVector3(0,0,0);
m_vView    = CVector3(0.0,1.0,0.5);
m_vUpVector = CVector3(0.0,0.0,1.0);

}

Camera::~Camera(void)
{
}


void Camera::PosotionCamera(float positionX, float positionY, float positionZ, float viewX, float viewY, float viewZ, float upVectorX, float upVectorY, float upVectorZ)
{
   m_vPosition = CVector3(positionX, positionY+200, positionZ);
   m_vView     = CVector3(viewX, viewY, viewZ);
   m_vUpVector = CVector3(upVectorX, upVectorY, upVectorZ);
}

void Camera::SetViewByMouse()
{
    CPoint m_CurPt,m_PrePt;
    HDC hDC = ::GetDC(NULL);
    float angleY, angleZ;
    CVector3 m_uAixs, m_vViewDire;
    unsigned long WIDTH, HEIGHT;

    WIDTH =::GetDeviceCaps(hDC,HORZRES);                //获得屏幕分辨率
    HEIGHT =::GetDeviceCaps(hDC,VERTRES);                //
    
    ::GetCursorPos(&m_CurPt);
    m_PrePt.x = WIDTH >>1;                                //分辨率/2
    m_PrePt.y = HEIGHT >> 1;
    ::SetCursorPos(m_PrePt.x, m_PrePt.y);                //固定光标在屏幕中心

    angleY = (m_CurPt.x - m_PrePt.x    )/1000.0;            //根据鼠标移动距离确定旋转角度
    angleZ = (m_CurPt.y - m_PrePt.y )/1000.0;            //

    m_vViewDire = m_vView - m_vPosition;
    m_uAixs = m_vViewDire.CrossProduct(m_vViewDire,m_vUpVector);   //得到平移向量
    m_uAixs = m_uAixs.Normalize(m_uAixs);

    RotateView(angleZ, m_uAixs.x, m_uAixs.y, m_uAixs.z);           //绕任意轴旋转
    RotateView(angleY,0,1,0);                                       //绕y轴旋转



}

void Camera::RotateView(float angle, float x, float y, float z)
{
    CVector3 vNewView;

    
    CVector3 vView = m_vView - m_vPosition;    //视线方向
    
    float cosTheta = (float)cos(angle);
    float sinTheta = (float)sin(angle);
    //下面就是一个数学公式
    vNewView.x = (cosTheta + (1 - cosTheta) * x * x)        * vView.x;
    vNewView.x += ((1 - cosTheta) * x * y - z * sinTheta)    * vView.y;
    vNewView.x += ((1 - cosTheta) * x * z + y * sinTheta)    * vView.z;

    vNewView.y = ((1 - cosTheta) * x * y + z * sinTheta)    * vView.x;
    vNewView.y += (cosTheta + (1 - cosTheta) * y * y)        * vView.y;
    vNewView.y += ((1 - cosTheta) * y * z - x * sinTheta)    * vView.z;

    vNewView.z = ((1 - cosTheta) * x * z - y * sinTheta)    * vView.x;
    vNewView.z += ((1 - cosTheta) * y * z + x * sinTheta)    * vView.y;
    vNewView.z += (cosTheta + (1 - cosTheta) * z * z)        * vView.z;
        
    m_vView = m_vPosition + vNewView;           //视点+新向量=新视线目标点
}

void Camera::StrafeCamera(float speed)
{
    
                                                //给视线目标点,视点增加一个增量
    m_vPosition.x += m_vStrafe.x * speed;
    m_vPosition.z += m_vStrafe.z * speed;
    
    m_vView.x += m_vStrafe.x * speed;
    m_vView.z += m_vStrafe.z * speed;
}

void Camera::MoveCamera(float speed)
{
    CVector3 vVector = m_vView - m_vPosition;
    vVector = vVector.Normalize(vVector);
    
    m_vPosition.x += vVector.x * speed;        //沿视线方向移动 
    m_vPosition.z += vVector.z * speed;        // 
    m_vView.x += vVector.x * speed;            // 
    m_vView.z += vVector.z * speed;            // 
}

void Camera::Update() 
{
    
    CVector3 vCross =m_vView.CrossProduct(m_vView - m_vPosition, m_vUpVector);
    m_vStrafe = vCross.Normalize(vCross);

    SetViewByMouse();
    CheckForMovement();

}
void Camera::CheckForMovement()                           // 上下左右移动视点
{
    if(GetKeyState(VK_UP) & 0x80 || GetKeyState('W') & 0x80)
    {                

        MoveCamera(fSpeed);                
    }

    if(GetKeyState(VK_DOWN) & 0x80 || GetKeyState('S') & 0x80)
    {            

        MoveCamera(-fSpeed);                
    }

    if(GetKeyState(VK_LEFT) & 0x80 || GetKeyState('A') & 0x80) 
    {            

        StrafeCamera(-fSpeed);
    }

    if(GetKeyState(VK_RIGHT) & 0x80 || GetKeyState('D') & 0x80)
    {            

        StrafeCamera(fSpeed);
    }    
}
void Camera::Look()                            //等于gluLookAt()
{
    gluLookAt(m_vPosition.x, m_vPosition.y, m_vPosition.z,    
            m_vView.x,    m_vView.y,     m_vView.z,    
            m_vUpVector.x, m_vUpVector.y, m_vUpVector.z);
}

 

(一)世界坐标系向观察坐标系的转换

假如任何形体都放在世界坐标系中,那么计算是相当复杂的,为了简化计算,我们需要把形体从世界坐标系转到观察坐标系中。观察坐标系的原点在是世界坐标系的位置为Eye,Z轴与观察方向一致(从Eye出发到At点的向量)如图4-1所示:

                                                                        image  

 

                                                                            图4-1

 

假设观察坐标系的坐标轴分别以单位向量xaxis,yaxis,zaxis,则:

                                                               xaxis= normal (At-Eye);

                                                               yaxis= normal (cross(Up,zaxis));

                                                               zaxis= normal (zaxis,xaxis);

假设世界坐标系中任意一点P的坐标(x,y,z),在观察坐标系中的坐标(x',y',z')。

x' = (P-Eye)* xaxis = x*xaxis.x + y* xaxis.y + z * xaxis.z - xaxis*Eye

y' = (P- Eye)*yaxis = x*yaxis.x + y* yaxis.y + z * yaxis.z - yaxis*Eye

z'= (P- Eye)*zaxis = x*zaxis.x + y* zaxis.y + z * zaxis.z - zaxis*Eye

 

                                    (x',y',z',1) = (x,y,z,1)*2

所以从世界坐标系向观察坐标系变换的矩阵为2

 

(二)齐次裁剪透视投影变换

真实的物体是三维的,但是计算机屏幕是二维的,必须把三维物体投影到屏幕平面上,而且还要保存深度信息,这个变换过程称为投影变换,如图4-2所示

                                                                 3

                                                                                                    图4-2

假设视截体Y方向的张角fov,近平面Zn,远平面的Zf,近平面的宽高比aspect,现在可以直到近平面的方程z=Zn,远平面 z=Zf。

                                                                    4

                                                                                                      图4-3

由图4-3可以看出,视截体的顶面方程为y=z*tan(fov/2);底面方程=-z*tan(fov/2);视截体的右侧面x=cot(fov/2)*aspect*z.

左侧面方程x=-cot(fov/2)*aspect*z.

首先寻求把顶面y = z*tan(fov/2) 转换为y'=1,y'=k*y ,k=cot(fov/2)*y/z就是满足条件的变换,底面变换也是这个表达式。

右侧面x = cot(fov/2)*aspect*z,转换为x'=1, x'=p*x, 从而p=(tan(fov/2)/aspect)/z(左侧面表达式相同).

最后寻求把近平面Zn转换为z'=0;Zf转换为z'=1.   z'= r*z + s.于是r* Zn + s =0,r*Zf + s =1,由此求出 r= /(Zf-Zn), s= -Zn/(Af-Zn).

透视投影变换矩阵=

5

 

(三)视截体平面的计算

根据模型变换矩阵和投影变换矩阵,可以计算出视截体的6个平面。世界坐标系中的视截体在模型变换和透视投影变换后,成为观察坐标系中的[-1,1]*[-1,1]*[0,1]。设模型变换A,投影变换B,M=A*B,视截体的方程:ax+by+cz+d=0。该平面在观察坐标系中的形式为a'x'+b'y'+c'z'+d'=0.

(x',y',z',1) = (x,y,z,1)M

(x,y,z,1)(a,b,c,d)(转置)=0

(x',y',z',1)(a',b',c',d')(转置) = 0

可得:(x,y,z,1)M(a',b',c',d')(转置) = 0

(a,b,c,d)(转置)= M (a',b',c',d')(转置)

a=M11a'+M12b'+M13c'+M14d'

b=M21a'+M22b'+M23c'+M24d'

c=M31a'+M32b'+M33c'+M34d'

d=M41a'+M42b'+M43c'+M44d'

视截体的6个平面的法向量均指向视截体内部,视截体的左侧面leftplane   观察坐标系中的左侧面x+1=0 ,代入上式可得视截体左侧面的系数

a=M11+M14

b=M21+M24

c=M31+M34

d=M41+M44

右侧面的方程1-x=0;系数

a=M14-M11

b=M24-M21

c=M34-M31

d=M44-M41

同理:顶面系数

a=M14-M12

b=M24-M22

c=M24-M32

d=M44-M42

底面系数

a=M12+M14

b=M22+M24

c=M32+M34

d=M42+M44

近平面系数:

a=M13

b=M23

c=M33

d=M43

远平面系数:

a=M14-M13

b=M24-M23

c=M34-M33

d=M44-M43

 

上述内容是涉及视截体计算的数学基础,下一节实战视截体编程!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值