在大规模的三维场景中,不可能一次渲染所有的三角形,而且即使能做到这点,全部渲染也是不可取的。常用的做法就是采用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所示:
图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
(二)齐次裁剪透视投影变换
真实的物体是三维的,但是计算机屏幕是二维的,必须把三维物体投影到屏幕平面上,而且还要保存深度信息,这个变换过程称为投影变换,如图4-2所示
图4-2
假设视截体Y方向的张角fov,近平面Zn,远平面的Zf,近平面的宽高比aspect,现在可以直到近平面的方程z=Zn,远平面 z=Zf。
图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).
透视投影变换矩阵=
(三)视截体平面的计算
根据模型变换矩阵和投影变换矩阵,可以计算出视截体的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
上述内容是涉及视截体计算的数学基础,下一节实战视截体编程!