地形三维可视化系统的地形渲 -...

117
第5章 在具备地形三维可视化系统框架基础上,通过读取相应的数字高程模型 (Digital Elevation Model,DEM)和影像纹理,将影像纹理叠加在 DEM 上, 通过一定的算法就可以实现地形的三维可视化。因此,首先需要确定实现地 形三维可视化的算法。其次要确定 DEM 和纹理数据的获取方式,选择最合 适的获取方式以满足三维线路可视化设计的需要。这一章将在解决上述两个 问题基础上,实现地形三维可视化系统的三维地形渲染,实现地形的三维可 视化。以双眼视差为重要依据的立体视觉,能够对三维场景的观察和漫游具 有更真实的沉浸感,因此,如果实现对三维地形场景的双目立体视觉功能, 将大大提高三维场景的可视效果。 地形三维可视化的基本概念。 目前地形三维可视化的主要算法。 地形和影像分块原则、方法和程序设计。 地形和影像的调度。 基于 OpenGL 扩展的多重纹理程序设计。 地形节点评价系统的程序设计。 三维地形的渲染。 双目立体视觉形成的原理、算法和立体视觉 程序设计。 维可视系统的地染实现

Upload: others

Post on 18-Oct-2020

27 views

Category:

Documents


0 download

TRANSCRIPT

第 5 章

在具备地形三维可视化系统框架基础上,通过读取相应的数字高程模型(Digital Elevation Model,DEM)和影像纹理,将影像纹理叠加在 DEM 上,通过一定的算法就可以实现地形的三维可视化。因此,首先需要确定实现地形三维可视化的算法。其次要确定 DEM 和纹理数据的获取方式,选择 合适的获取方式以满足三维线路可视化设计的需要。这一章将在解决上述两个问题基础上,实现地形三维可视化系统的三维地形渲染,实现地形的三维可视化。以双眼视差为重要依据的立体视觉,能够对三维场景的观察和漫游具有更真实的沉浸感,因此,如果实现对三维地形场景的双目立体视觉功能,将大大提高三维场景的可视效果。

地形三维可视化的基本概念。

目前地形三维可视化的主要算法。

地形和影像分块原则、方法和程序设计。

地形和影像的调度。

基于 OpenGL 扩展的多重纹理程序设计。

地形节点评价系统的程序设计。

三维地形的渲染。

双目立体视觉形成的原理、算法和立体视觉 程序设计。

地形三维可视化系统的地形渲 染实现

实战 OpenGL 三维可视化系统开发与源码精解

124

在第 4 章中,我们建立了地形三维可视化系统的程序框架和 OCI 公共类。本章将在此基础

上,逐步学习三维地形渲染的程序设计和实现,这也是地形三维可视化系统的主要内容和重点。

5.1 地形三维可视化概述

地形可视化的概念是在 20 世纪 60 年代以后随着地理信息系统(Geographical Information

System,GIS)的出现而逐渐形成的,是在计算机上对数字地形模型(Digital Terrain Model,

DTM)或数字高程模型(Digital Elevation Model,DEM)三维逼真地显示、模拟仿真、简化、

多分辨率表达和网络传输等内容的一项技术。它涉及到测绘学、现代数学、计算机三维图形

学、计算几何、地理信息系统、虚拟现实、科学计算可视化等众多学科领域。地形可视化与

人类的生产生活息息相关,在城市规划、工程勘查与设计、项目选址、路径选取、资源调查

与分配、环境监测、灾害预测与预报、军事、游戏娱乐等众多领域都有广泛的应用。

地形可视化的核心问题是如何解决由海量地形数据构成的复杂地形表面模型与计算机

图形硬件有限的绘制能力之间的矛盾。过去几十年里,尽管图形硬件技术已经有了飞速发

展,但仍然不能满足大规模三维场景可视化的需要。进行地形三维可视化离不开数据准备、

数据的可视化、图形的绘制和存储,以及基于三维地形图的分析这几步。因此,地形三维

可视化的研究内容也主要针对这几个方面。

5.2 地形三维可视化的主要算法

在对地形可视化概念了解的基础上,下面将讲述大规模地形三维可视化的主要成熟算

第 5 章 | 地形三维可视化系统的地形渲染实现

125

法。在此基础上,我们选择合适的算法来开发基于 OpenGL 和 Oracle 数据库的大规模三维

地形可视化系统。

5.2.1 主要算法概述

在解决大规模地形数据三维可视化和实时绘制的问题中,国内外许多学者都进行了广

泛、深入的研究,主要集中于地形和纹理数据的分页管理、纹理数据和地形数据的 LOD

控制、可见域的裁剪等多个方面。就目前己发表的研究和应用成果来看,在提高地形场景

实时绘制方面比较有效的方法主要可归结为以下 3 类:数据、可见性预处理方法、基于图

像的绘制算法和细节层次(Level Of Detail,LOD)算法。其中,细节层次模型是一种有效

控制场景复杂度的数据简化方法。综合考虑到地形绘制的效果和实时绘制效率,目前使用

广泛的方法还是利用 LOD 算法生成地形的连续多分辨率模型,进而完成地形的实时多

分辨率绘制。从 LOD 多分辨率模型表示的结果来看,主要可归结为基于三角网格和基于

树数据结构的多分辨率层次模型。此外,随着图形处理器(Graphic Processing Unit,GPU)

性能的提高,出现了基于硬件 GPU 的粗粒度的离散层次细节方法。

从地形实时细节分层技术的发展来看, 先开发的且目前该领域研究 多的还是基于

树数据的地形 LOD 模型。在用树数据结构方式对地形进行简化时,通常使用二叉树和四

叉树两种数据结构。其中四叉树层次结构模型的运用更为广泛。第一,四叉树与地理信息

在坐标系统方面有天然的统一。第二,四叉树结构可以非常便利地把纹理镶嵌技术集成进

地形可视化系统中来。第三,采用四叉树结构,能够降低选择地形表示的时间,加速地形

实战 OpenGL 三维可视化系统开发与源码精解

126

简化算法。因此,这里也采用四叉树结构,在基于四叉树的实时连续 LOD 生成技术上,

采用地形小块(Block)来组织数据,基于数据分块、部分数据常驻内存,建立基于视点位

置、视线方向、地形起伏程度、帧频控制相关的误差评价函数,在背面剔除等处理的基础

上,实现了大规模地形的连续细节层次实时渲染方法。在运行时,根据建立的误差评价函

数,动态地生成适当的细节层次并进行渲染,实现海量地形数据的三维可视化及漫游。

5.2.2 四叉树结构的多分辨率地形模型

在上一节中,我们知道基于四叉树结构的多分辨率 LOD 地形仍是目前主要算法之一。多分

辨率地形模型的思想在不同的层次、不同的视觉条件下,采用不同精细程度的模型来表示同一

个对象,以提高场景的显示速度。多分辨率地形模型主要根据视点距地形的距离和观察的角度

等指标来确定模型的精细程度。随着距离的由远到近,地面模型的精细程度也由粗到细。

由于四叉树结构的 LOD 模型在结构上对原始模型进行了分块,因此与空间索引系统具有

一定的统一性。这种分块的块状结构用四叉树来描述,称为四叉树编码,如图 5-1 所示。这

样一旦建立了整个地形区域的四叉树模型,在结构上也相当于建立了地形范围的统一索引系

统。因此,鉴于四叉树组织方式的灵活性,通过四叉树结构来管理地形几何数据。树中的每

一个节点都覆盖地形中一矩形区域,根结点覆盖整个地形块区域,子节点所覆盖的地形区域

为父节点的四分之一,分辨率比父节点的分辨率高一倍。采用视相关的 LOD 简化方法依据视

点的位置和方向合理地选择多分辨率的地形表示,视点周围的地形用高细节的层次表示,远

离视点的区域用较粗糙的细节表示。提高绘制速率可以通过对四叉树的分割来实现,即将地

第 5 章 | 地形三维可视化系统的地形渲染实现

127

形分割成一个个大小不同的地块,近视点分割得大些,远视点分割得小些。渲染这些大小不

同的正方形地形块,从而达到 LOD 不同细节层级渲染的目的。图 5-2(a)为一种有效的四叉

树分割,图 5-2(b)是对应图 5-2(a)的四叉树结构,图 5-2(c)为四叉树单元节点图。

图 5-1 四叉树编码示意图

(a)一种有效的四叉树分割 (b)图(a)对应的四叉树结构图 (c)四叉树单元节点

图 5-2 四叉树分割与存储结构图

5.3 地形三维可视化系统的实现

在建立了三维可视化系统程序框架、实现了 OCI 公共类和确定了基于四叉树的多

分辨率 LOD 三维地形可视算法之后,就可以开始进行基于该算法的地形可视化系统

的开发了。

实战 OpenGL 三维可视化系统开发与源码精解

128

5.3.1 海量地形与影像纹理数据的获取方法

要实现地形可视化,首先获取所需要的影像纹理数据和数字高程模型数据,下面介绍

目前获取这些数据的主要方法。

大规模地形纹理影像主要采用数字正射影像图(Digital Orthophoto Map,DOM),它

利用经扫描处理的数字化的航空像片或高分辨率卫星遥感图像数据。DOM 对逐像元进行

几何纠正和镶嵌,具有精度高、信息丰富、直观真实的特点,同时是具有地图几何精度和

影像特征的图像。数字高程模型(Digital Elevation Model,DEM)是一种用有序数值阵列

x、y、z 坐标表示地面高程的一种实体地面模型,是数字地面模型(Digital Terrain Mdoel,

DTM)的一个分支。DOM 与 DEM 叠加,可以建立逼真显示的三维地形环境,因此如何快

速高效建立 DOM 和 DEM 是建立三维地形可视化系统的前提和基础。

目前建立 DEM 的方法有多种,从数据源及采集方式上主要有 3 种,如表 5-1 所示。

表 5-1 目前建立 DEM 的主要方法

序 号 方 法 名 称 说 明

1 直接地面测量法 采用 GPS、全站仪、野外测量等方法

2 地形图上采集法 从现有地形图上采集,如格网读点法、数字化仪手扶跟踪及扫描仪半自动采集,

然后通过内插生成 DEM 等方法

3 摄影测量法 根据航空或航天遥感影像,通过摄影测量途径获取,如立体坐标仪观测及空三

加密法、解析测图、数字摄影测量等

全数字摄影测量系统以遥感影像为基础,通过计算机进行影像匹配,自动进行相关运

算识别同名像点得到其像点坐标,再运用解析摄影测量的方法内定向、相对定向、绝对定

第 5 章 | 地形三维可视化系统的地形渲染实现

129

向及运用核线重排技术,由此可以测定所拍摄物体的空间三维坐标,获得 DEM 数据,具

有自动大规模生产 DEM、DOM 和自动生成等高线等功能,如图 5-3 所示。同时,全数字

摄影测量系统可以进行正射影像纠正和镶嵌、影像修补、任意影像的无缝镶嵌,是生成正

射影像图的一种快速而有效的方法,如图 5-4 所示。因此利用全数字摄影测量系统,通过

遥感影像来获取 DEM 和 DOM,成为大规模三维地形可视化系统的 DEM 与 DOM 数据获

取的首选方法。本书就是利用美国莱卡公司 Helava 全数字摄影测量系统来获取 DEM 与

DOM 数据的。

图 5-3 基于 DEM 生成的等高线

图 5-4 镶嵌好的正射影像图

5.3.2 海量地形自分块程序实现

在了解了海量地形与影像纹理数据的获取方法并获取了这些数据后,首先需要对地形数据进行分块处理,因为目前的计算机内存

图 5-5 地形数据分块

实战 OpenGL 三维可视化系统开发与源码精解

130

容量仍然有限,大规模的地形数据不可能一次性地调入内存,必须进行分块处理。其基本思路是:首先将研究区地形数据划分为大小相同的 m 行 n 列,如图 5-5 所示,每个子块边长为2n+1,命名为 i jRow Col (i、j 分别为该子块所在的行和列),左下角为第 0 行第 0 列。对于右侧和 上侧不满足大小相同要求的块,以无效数据(如-9999)填充,在图 5-5 中的灰色线填充部分。然后对每一个数据分块按照四叉树结构进行组织,以二进制格式存储到数据库中。此外,考虑到 Intel 的 CPU 内存大小是 4KB,块的大小一般取 33×33 为好,这样在一定程度上提高存储效率,降低内存缺页的次数,同时数据调度也不会太频繁。因此,这里提供了从 33×33到 1025×1025 的多个分块大小可供选择,以适应不同地形数据的需要。

系统所用 DEM 数据类型为格网 DEM,数据格式如下:

1 18312320.000 3231120.000 10 10.0 1670 1242 -9999

2 750.000 732.000 708.000 610.000 630.000 635.000 660.000 651.000 …

3 755.000 735.000 700.000 611.000 634.000 631.000 667.000 650.000… ……

第 1 行表示 DEM 的基本信息,如表 5-2 所示。

表 5-2 DEM 文件头基本信息说明

序 号 数 值 说 明

1 18312320.000 DEM 左下角 x 坐标

2 3231120.000 DEM 左下角 y 坐标

3 10 DEM 数据点在 x 方向的间距

4 10.0 DEM 数据点在 y 方向的间距

5 1670 DEM 总列数

6 1242 DEM 总行数

7 -9999 DEM 无效数据点的高程值

第 2 到第 n 行为 DEM 数据点高程值。每一高程点对应的 x、y 大地坐标可通过其

对应的行列号和 DEM 左下角 x、y 坐标计算求得。

1.界面设计

首先在工程主菜单中添加一个菜单项,名称为“数模与纹理(D)”,类型为“Pop_up”(弹

出式)。在其下添加一子菜单,名称为“数模读取与分块处理(E)”,ID=ID_MENU_DEM,

说明:

第 5 章 | 地形三维可视化系统的地形渲染实现

131

菜单如图 5-6 所示。

然后在 T3DSystem 工程中添加一个对话框(ID=IDD_DIALOG_DEMSEPERATE)和

相应控件,如图 5-7 所示,详细的信息如表 5-3 所示。

图 5-6 添加菜单 图 5-7 界面设计

表 5-3 控件的详细信息

序 号 ID 名 称 变 量

1 IDC_STATIC DEM 数据文件 ---

2 IDC_EDIT_DEMFILE --- CString m_DemFileName

3 IDC_BUTTON_BROWSE 浏览 ---

4 IDC_STATIC 分块大小 ---

5 IDC_COMBO_SUBBLOCKSIZE --- CcomboBox

m_subCombolblockSize

6 IDC_BUTTON_SEPERATE 开始分块入库处理 ---

7 IDOK 退出 ---

8 IDC_PROGRESS --- CProgressCtrl m_progress

2.程序设计

1)变量和函数添加

为该对话框添加类,名称为 CdemLoad,在头文件(DemLoad.h)中添加私有变量,代

码如下所示。

private:

float*m_pHeight; //存储 DEM 的所有点的高程 float**m_subBlockHeight; //存储每一子块的高程

int m_subBlockSize; //DEM 子块大小

CString tempDemDirctory; //存储 DEM 分块文件的临时文件夹

实战 OpenGL 三维可视化系统开发与源码精解

132

为类 CdemLoad 添加私有函数,代码如下所示。

private:

void AddDemBlockDataToDB(int RowIndex,int ColIndex,CString strfilenaem,

long ID); void Init_BlockSize();

void WriteTotalDemToBlob(CString strfile);

float HH(int mRows,int mCols); void ReadDemDataFromFiles(CString strfiles,int Index);

int GetBlcokSize(int currentSel);

void SeperateDem(CString strfilename,int mBlockSize);

并为 IDC_BUTTON_BROWSE 和 IDC_BUTTON_SEPERATE 分别添加 OnButtonBrowse()

和 OnButtonSeperate()响应函数。

2)函数实现

在 DemLoad.cpp 中 后实现的代码如下。

(1)将 DEM 子块数据写入数据库中。

void CDemLoad::AddDemBlockDataToDB(int RowIndex,int ColIndex,CString strf-

ilenaem,long ID) {

CString tt;

double centerx,centery; //DEM 子块的中心坐标

centerx=theApp.m_DemLeftDown_x+((ColIndex-1)+1.0/2)*theApp.m_Dem_Bl-

ockWidth; centery=theApp.m_DemLeftDown_y+((RowIndex-1)+1.0/2)*theApp.m_Dem_Bl-

ockWidth;

tt.Format("INSERT INTO dem_block VALUES(%d,%d,EMPTY_BLOB(),%ld, %.3f,%.3f)",RowIndex,ColIndex,ID,centerx,centery);

myOci.AddNormalDataToDB(tt); //将常规数据类型写入 Oracle 数据库中

tt.Format("SELECT DEM 数据 FROM dem_block WHERE 行号=%d AND 列号=%d AND 编 号=:%d FOR UPDATE",RowIndex,ColIndex,ID);

//将 DEM 分块文件作为 BLOB 数据类型写入 Oracle 数据库中(通过调用 OCI 公共类的

//AddBOLBDataToDB 函数实现) myOci.AddBOLBDataToDB(strfilenaem,tt,ID);

}

函数说明:

第 5 章 | 地形三维可视化系统的地形渲染实现

133

AddDemBlockDataToDB()函数通过调用OCI公共类的AddNormalDataToDB()函数

和 AddBOLBDataToDB()函数将分块后的 DEM 子块数据写入 Oracle 数据库中。

(2)初始化 DEM 分块大小。

void CDemLoad::Init_BlockSize()

{ long mvalue=1;

int m;

CString tt; m_subCombolblockSize.ResetContent(); //列表框清空

//循环加入分块大小从 33×33 到 1025×1025

for(int i=1;i<=9;i++) {

m=1;

for(int j=1;j<=i;j++) m=m*2;

mvalue=m*16+1;

tt.Format("%d",mvalue); tt=tt+"×"+tt;

m_subCombolblockSize.AddString(tt);

} m_subCombolblockSize.SetCurSel(0);

OnSelchangeComboSubblocksize();

}

Init_BlockSize()函数实现向列表框循环加入分块大小从 33×33 到 1025×1025 的选

项值。

(3)打开 DEM 文件。

void CDemLoad::OnButtonBrowse()

{ CString tt,stt;

FILE*fp;

CFileDialog FileDialog(TRUE,"DEM 数据文件",NULL,OFN_HIDEREADONLY \ |OFN_OVERWRITEPROMPT,\

"DEM 数据文件(*.dem)|*.dem|\

文本格式(*.txt)|*.txt||",NULL);

FileDialog.m_ofn.lpstrTitle="打开 DEM 数据文件";

if(FileDialog.DoModal()==IDOK) //如果 DEM 文件打开成功 m_DemFileName=FileDialog.GetPathName(); //得到 DEM 文件名

函数说明:

实战 OpenGL 三维可视化系统开发与源码精解

134

else

return; this->UpdateData(FALSE); //数据更新

if((fp=fopen(m_DemFileName,"r"))==NULL) {

MessageBox("DEM 文件不存在!","初始化 DEM",MB_ICONINFORMATION+MB_OK);

exit(-1); }

}

OnButtonBrowse()函数实现通过文件对话框选择 DEM 文件,并记录 DEM 文件名。

(4)开始分块处理 DEM 数据并写入数据库中。

void CDemLoad::OnButtonSeperate()

{ CString stt;

BeginWaitCursor();

m_progress.ShowWindow(SW_SHOW); tempDemDirctory="c:\\tempRailwayDem"; //建立临时文件夹

DWORD dwAttr=GetFileAttributes(tempDemDirctory);

if(dwAttr==0xFFFFFFFF) //如果临时文件夹不存在,则创建该临时文件夹 {

//创建该临时文件夹,用于存储分块后的临时 DEM 子块数据文件

CreateDirectory(tempDemDirctory,NULL); }

SeperateDem(m_DemFileName,m_subBlockSize); //分块处理

RemoveDirectory(tempDemDirctory); //分块完成后,删除临时文件夹

SetWindowText("数模读取与分块处理"); //设置对话框标题 m_progress.ShowWindow(SW_HIDE);

m_progress.SetPos(0);

EndWaitCursor(); MessageBox("数模读取与分块处理完成,共用时"+stt,"数模读取与分块处理",MB_

ICONINFORMATION|MB_OK);

EndDialog(IDOK); }

OnButtonSeperate()函数通过调用 SeperateDem()函数实现对打开的 DEM 数据文件

函数说明:

函数说明:

第 5 章 | 地形三维可视化系统的地形渲染实现

135

按照设置的分块大小进行分块处理。

(5)初始化信息。

BOOL CDemLoad::OnInitDialog()

{

CDialog::OnInitDialog();

m_progress.SetRange(0,100); //设置进度条控件范围 Init_BlockSize(); //初始化 DEM 分块大小

myOci.Init_OCI(); //初始化 OCI

return TRUE; }

OnInitDialog()函数通过调用 Init_BlockSize()函数、Init_OCI()函数实现对相关信息

的初始化。

(6)分块选项改变时,计算 DEM 子块分块大小。

void CDemLoad::OnSelchangeComboSubblocksize()

{

int mIndex= m_subCombolblockSize.GetCurSel();

CString tt; m_subCombolblockSize.GetLBText(mIndex,tt);

int mPos=tt.Find("×",1);

tt=tt.Left(mPos); m_subBlockSize=atoi(tt); //得到 DEM 分块大小

}

(7)根据所设置的分块大小,对 DEM 文件进行分块并写入 Oracle 数据库中。

void CDemLoad::SeperateDem(CString strfilename,int BlockSize)

{

long i,j; GLint m_DEM_X_neednumber,m_DEM_Y_neednumber;

CString*strfiles;

CStdioFile*Sfiles; float hh;

CString tt,stt,TerrainFileName;

函数说明:

实战 OpenGL 三维可视化系统开发与源码精解

136

☆程序第Ⅰ部分☆《打开 DEM 文件,读取 DEM 头信息》

FILE*fp=fopen(strfilename,"r");//打开 DEM 文件

//得到 DEM 数据的左下角大地 x 坐标 fscanf(fp,"%s",tt);theApp.m_DemLeftDown_x=atof(tt);

//得到 DEM 数据的左下角大地 y 坐标

fscanf(fp,"%s",tt);theApp.m_DemLeftDown_y=atof(tt); //得到 DEM 数据在 x 方向上数据点间距

fscanf(fp,"%s",tt);theApp.m_Cell_xwidth=atoi(tt);

//得到 DEM 数据在 y 方向上数据点间距 fscanf(fp,"%s",tt);theApp.m_Cell_ywidth=atoi(tt);

fscanf(fp,"%s",tt);theApp.m_Dem_cols=atoi(tt); //得到 DEM 数据总列数

fscanf(fp,"%s",tt);theApp.m_Dem_Rows=atoi(tt); //得到 DEM 数据总行数 //得到 DEM 数据无效数据值

fscanf(fp,"%s\n",tt);theApp.m_DEM_IvalidValue=atoi(tt);

theApp.m_Dem_BlockSize=m_subBlockSize; //DEM 分块大小

theApp.m_DemRightUp_x=theApp.m_DemLeftDown_x+theApp.m_Cell_xwidth*(theAp

p.m_Dem_cols-1); //DEM 右上角 x 坐标 theApp.m_DemRightUp_y=theApp.m_DemLeftDown_y+theApp.m_Cell_ywidth*(theAp

p.m_Dem_Rows-1); //DEM 右上角 y 坐标

//DEM 子块总宽度 theApp.m_Dem_BlockWidth=theApp.m_Cell_xwidth*(theApp.m_Dem_BlockSize-1);

☆程序第Ⅱ部分☆《对数模进行分块处理》

SetWindowText("正在进行数模分块处理....");

if(theApp.m_Dem_Rows<=BlockSize) //如果 DEM 总行数小于所设置分块大小

{ theApp.m_BlockRows=1; //则 DEM 子块总行数等于 1

//在 Y 方向(行)需要添加无效数据的行数

m_DEM_Y_neednumber=BlockSize-theApp.m_Dem_Rows; }

else//如果 DEM 总行数大于所设置的分块大小,计算 DEM 子块总行数

{ //如果 DEM 总行数是分块大小的整数倍

if((theApp.m_Dem_Rows-BlockSize)%(BlockSize-1)==0)

{ theApp.m_BlockRows=(theApp.m_Dem_Rows-BlockSize)/(BlockSize-1)+1;

m_DEM_Y_neednumber=0; //在 Y 方向上不需要添加无效数据

} else

{

//分块总行数 theApp.m_BlockRows=(theApp.m_Dem_Rows-BlockSize)/(BlockSize-1)+2;

//在 Y 方向(行)上需要添加无效数据的行数

第 5 章 | 地形三维可视化系统的地形渲染实现

137

m_DEM_Y_neednumber=theApp.m_BlockRows*(BlockSize-1)+1-theApp.

m_Dem_Rows; }

}

if(theApp.m_Dem_cols<=BlockSize) //如果 DEM 总列数小于所设置分块大小 {

theApp.m_BlockCols=1; //则 DEM 子块总列数等于 1

//需要在 x 方向(列)添加无效数据的列数 m_DEM_X_neednumber=BlockSize-theApp.m_Dem_cols;

}

else//如果 DEM 总列数大于所设置的分块大小,计算 DEM 子块总列数 {

//如果 DEM 总列数是分块大小的整数倍

if((theApp.m_Dem_cols-BlockSize)%(BlockSize-1)==0) {

theApp.m_BlockCols=(theApp.m_Dem_cols-BlockSize)/(BlockSize-1)+1;

m_DEM_X_neednumber=0; //在 X 方向上不需要添加无效数据 }

else

{ //分块总列数

theApp.m_BlockCols=(theApp.m_Dem_cols-BlockSize)/(BlockSize-1)+2;

//在 X 方向(列)上需要添加无效数据的列数 m_DEM_X_neednumber=theApp.m_BlockCols*(BlockSize-1)+1-theApp.m_Dem_

cols;

} }

//重新定义 m_pHeight 数组大小 m_pHeight=new float[BlockSize*BlockSize];

//根据分块的总行数和总列数重新定义 strfiles和 Sfiles大小,用来存储每个子块数据的文件名

strfiles=new CString[theApp.m_BlockRows*theApp.m_BlockCols]; Sfiles=new CStdioFile[theApp.m_BlockRows*theApp.m_BlockCols];

//根据分块的总行数和总列数,建立临时文件,用来存储子块数据 for(i=0;i<theApp.m_BlockRows;i++)

{

for(j=0;j<theApp.m_BlockCols;j++) {

//获得文件名

tt.Format("%d.txt",j+i*theApp.m_BlockCols+1); strfiles[j+i*theApp.m_BlockCols]=tempDemDirctory+"\\block_"+tt;

//创建文件

Sfiles[j+i*theApp.m_BlockCols].Open(strfiles[j+i*theApp.m_Block- Cols],CFile::modeCreate|CFile::modeWrite);

Sfiles[j+i*theApp.m_BlockCols].Close(); //关闭文件

实战 OpenGL 三维可视化系统开发与源码精解

138

}

}

long mto=theApp.m_Dem_Rows*theApp.m_Dem_cols;

int mColsdatanum=0; int mCurrentRow,mCurrentCol;

int m_oldcurrentRow=-1;

CString*strSaveUpText; strSaveUpText=new CString[theApp.m_BlockCols];

theApp.m_DemHeight=new float[theApp.m_Dem_Rows*theApp.m_Dem_cols];

//根据分块的总行数/总列数和分块大小,对 DEM 数据进行了分块处理,将数据写入对应的 DEM 子

块数据文件中

for(i=1;i<=theApp.m_Dem_Rows;i++)

{ m_progress.SetPos(i*(100.0/theApp.m_Dem_Rows));

if(i<=BlockSize)

mCurrentRow=1; else

{

if((i-1)%(BlockSize-1)==0) mCurrentRow=(i-1)/(BlockSize-1);

else

mCurrentRow=(i-1)/(BlockSize-1)+1; }

if(m_oldcurrentRow!=mCurrentRow)

{ if(m_oldcurrentRow>0)

{

for(int k=0;k<theApp.m_BlockCols;k++) {

Sfiles[k+(m_oldcurrentRow-1)*theApp.m_BlockCols].Close();

} }

for(int k=0;k<theApp.m_BlockCols;k++)

{ Sfiles[k+(mCurrentRow-1)*theApp.m_BlockCols].Open(strfiles[k+

(mCurr entRow-1)*theApp.m_BlockCols],CFile::modeCreate|CFile::

modeWrite); }

}

stt=""; mColsdatanum=0;

for(j=1;j<=theApp.m_Dem_cols;j++)

{ if(j<=BlockSize)

mCurrentCol=1;

第 5 章 | 地形三维可视化系统的地形渲染实现

139

else

{ if((j-1)%(BlockSize-1)==0)

mCurrentCol=(j-1)/(BlockSize-1);

else mCurrentCol=(j-1)/(BlockSize-1)+1;

}

fscanf(fp,"%f ",&hh); //读取高程

tt.Format("%.3f",hh); //高程精度取小数点后 3 位

theApp.m_DemHeight[(i-1)*theApp.m_Dem_cols+(j-1)]=hh; stt=stt+tt;

mColsdatanum++;

if(mColsdatanum%BlockSize==0||j==theApp.m_Dem_cols) {

if(j==theApp.m_Dem_cols)

{ //对于不足分块大小的子块数据,以无效数据补充

int pos=BlockSize-mColsdatanum;

for(int k=1;k<=pos;k++) {

tt.Format("%d",theApp.m_DEM_IvalidValue);

stt=stt+tt; }

}

stt=stt+"\n"; if(mCurrentRow>1&&m_oldcurrentRow!=mCurrentRow)

{

Sfiles[(mCurrentRow-1)*theApp.m_BlockCols+mCurrentCol-1]. WriteString(strSaveUpText[mCurrentCol-1]);

}

Sfiles[(mCurrentRow-1)*theApp.m_BlockCols+mCurrentCol-1]. WriteString(stt);

if((i-1)%(BlockSize-1)==0 &&(i>1&&i<theApp.m_Dem_Rows))

{ int ms=(i-1)/(BlockSize-1)*theApp.m_BlockCols+mCurrentCol-1;

Sfiles[ms].Open(strfiles[ms],CFile::modeCreate|CFile::

modeWrite); Sfiles[ms].WriteString(stt); //将数据写入文件

Sfiles[ms].Close();//关闭文件

strSaveUpText[mCurrentCol-1]=stt; }

if(mColsdatanum%BlockSize==0)

{ stt=tt; mColsdatanum=1;

}

实战 OpenGL 三维可视化系统开发与源码精解

140

else

{ stt=""; if(j<theApp.m_Dem_cols)mColsdatanum=0;

}

if(i>=theApp.m_Dem_Rows&&j>=theApp.m_Dem_cols) {

int pos=m_DEM_Y_neednumber;

for(int p=1;p<=theApp.m_BlockCols;p++) {

for(int k=1;k<=pos;k++)

{ stt="";

//对于不足分块大小的子块数据,以无效数据补充

for(int m=1;m<=BlockSize+1;m++) { tt.Format(" %d",theApp.m_DEM_IvalidValue);

stt=stt+tt;

} stt=stt+"\n";

Sfiles[theApp.m_BlockCols*theApp.m_BlockRows-

(theApp.m_BlockCols-p+1)].WriteString(stt); }

}

} }

}

fscanf(fp,"\n"); m_oldcurrentRow=mCurrentRow;

}

//关闭所打开的临时文件

for(int k=0;k<theApp.m_BlockCols;k++)

{ Sfiles[k+(theApp.m_BlockRows-1)*theApp.m_BlockCols].Close();

}

☆程序第Ⅲ部分☆《将分块后的 DEM 数据写入数据库中》

//写入数据库之前,先删除 dem_block(DEM 数据表中的原来所有数据)

//是为了防止对同一数据文件多次写入数据库中造成的重复写入错误 CString strSql;

strSql="DELETE FROM dem_block";

HRESULT hr=theApp.m_pConnection->Execute(_bstr_t(strSql),NULL,adCmdText); if(!SUCCEEDED(hr))

{

MessageBox("删除失败!","写入 DEM 数据到数据库",MB_ICONSTOP);

第 5 章 | 地形三维可视化系统的地形渲染实现

141

return;

} //依次将分块捕捞 DEM 子块数据文件写入 Oracle 数据库中

SetWindowText("将数模分块数据写入数据库...");

for(i=0;i<theApp.m_BlockRows;i++) {

for(j=0;j<theApp.m_BlockCols;j++)

{ m_progress.SetPos((i*theApp.m_BlockCols+j+1)*(100.0/

(theApp.m_BlockRows*theApp.m_BlockCols)));

AddDemBlockDataToDB(i+1,j+1,strfiles[i*theApp.m_BlockCols+j], i*theApp.m_BlockCols+j+1);

}

} fclose(fp); //关闭文件

WriteTotalDemToBlob(strfilename); //将 DEM 数据写入 Oracle 数据库中

}

SeperateDem()函数根据所设置的分块大小,对 DEM 文件进行分块并写入 Oracle数据库中,包括以下 3 个部分。

第 1 部分是打开 DEM 文件,读取 DEM 头信息。 第 2 部分是对数模进行分块处理。 第 3 部分是将分块后的 DEM 数据写入数据库中。

(8)得到分块大小。

int CDemLoad::GetBlcokSize(int currentSel)

{

int m=1; for(int i=0;i<currentSel;i++)

m=m*2;

return m*32+1; }

GetBlcokSize()函数根据选择项计算分块大小。

(9)从 DEM 分块文件中读取数据。

void CDemLoad::ReadDemDataFromFiles(CString strfiles,int Index)

{

函数说明:

函数说明:

实战 OpenGL 三维可视化系统开发与源码精解

142

float hh;

int i,j; int mCount=theApp.m_Dem_BlockSize;

FILE*fp=fopen(strfiles,"r");//打开文件

for(i=0;i< mCount;i++) //DEM 子块的行、列数是相同的(如 33×33、65×65 等)

{

for(j=0;j<mCount;j++) {

fscanf(fp,"%f ",&hh);

m_pHeight[i*mCount+j]=hh; }

}

fclose(fp); //关闭文件 DeleteFile(strfiles); //删除临时分块文件

}

ReadDemDataFromFiles()函数从每个 DEM 分块文件中读取高程数据,并存储在高

程数组中。

(10)根据行、列索引值计算对应 DEM 数据点的高程。

float CDemLoad::HH(int mRows,int mCols)

{ return m_pHeight[mRows*theApp.m_Dem_BlockSize+mCols];//计算高程值

}

(11)将 DEM 数据写入 Oracle 数据库中。

void CDemLoad::WriteTotalDemToBlob(CString strfile)

{ CString tt;

tt.Format("INSERT INTO DEM_INFO VALUES(%.3f,%.3f,%d,%d,%d,%d,%d, %d,%d,EMPTY_BLOB(),%d,%d)",\

theApp.m_DemLeftDown_x,theApp.m_DemLeftDown_y,theApp.m_Cell_xwidth,\

theApp.m_Cell_ywidth,theApp.m_Dem_BlockSize,theApp.m_BlockRows,\ theApp.m_BlockCols,theApp.m_Dem_Rows,theApp.m_Dem_cols,1,theApp.

m_DEM_IvalidValue);

myOci.AddNormalDataToDB(tt); //将常规数据类型写入 Oracle 数据库中 tt.Format("SELECT DEM 数据 FROM DEM_INFO WHERE 编号=:%d FOR UPDATE",1);

myOci.AddBOLBDataToDB(strfile,tt,1); //将 DEM分块文件作为 BLOB数据类型写

入数据库中 }

函数说明:

第 5 章 | 地形三维可视化系统的地形渲染实现

143

WriteTotalDemToBlob()函数通过调用OCI公共类的相应函数将DEM头文件信息、

分块信息和分块数据写入 Oracle 数据库中。

后程序运行界面如图 5-8 所示。单击“浏览”按钮选择数据文件,然后单击“开始分

块入库处理”按钮,将原始的 DEM 数据文件按照所设置的分块大小写入 Oracle 数据库

中。

完成后提示读取和写入 DEM 时间,如图 5-9 所示。

图 5-8 数模读取与分块处理运行效果图 图 5-9 数模读取完成提示信息

5.3.3 大影像的自分块及程序实现

在对地形数据分块处理后,接下来就是对影像纹理进行分块处理。因为影像数据占用

较大的存储空间,而且一般计算机的图形渲染设备限制了单次装载影像的大小,如目前的

OpenGL 渲染设备支持的单张影像的 大范围为 2048×2048,而在多数实际情况中地形影

像的范围远远大于这个限制数量的大小。因此需要对大的影像纹理进行分块处理,即在平

面空间上将影像分割成一系列规则的影像块。

影像分块分为规则分块(一个影像块对应一个或多个 DEM 子块)和不规则分块(一

个影像块并不对应整数个的 DEM 子块)。在实际应用效率上,不规则分块将造成大量纹理

函数说明:

实战 OpenGL 三维可视化系统开发与源码精解

144

数据冗余,而且在纹理影像块调度时计算烦琐,因此,这里

采用规则分块中的一个影像块对应一个 DEM 子块,漫游时可

立即确定所应调入的纹理影像块,如图 5-10 所示。

对于分块后的影像纹理,为了方便以后进行纹理映射和

保证在三维交互显示时纹理数据调度的高效性,我们设计了

如下的数据结构用于管理这些纹理数据,如表 5-4 所示。

表 5-4 数据库存储的纹理模型数据结构

序 号 空间位置(角点坐标) 影 像 大 小 影 像 数 据

1 x1,y1,x2,y2 W1×h1 01010101…

2 x1,y1,x2,y2 W2×h2 01010101…

… … … …

n x1,y1,x2,y2 Wn×hn 01010101…

从表 5-4 中可以看出,纹理数据是由一些子块影像数据构成的,其中每一子块影像具

有一定的属性特征。空间位置表示该影像被映射的区域范围,影像的大小表示该子块影像

的高和宽,用于计算纹理坐标。影像数据用于存储该子块影像数据,以 BLOB 数据类型存

储在 Oracle 数据库中。

程序设计实现如下所述。

1.界面设计

首先在工程“数模与纹理(D)”菜单下添加子菜单,名称为“纹理影像入库(T)”,

ID=ID_MENU_TEXTUREIMAGE,菜单如图 5-11 所示。

图 5-10 纹理分块示意图

第 5 章 | 地形三维可视化系统的地形渲染实现

145

然后在 T3DSystem 工程中添加一个对话框和相应控件,如图 5-12 所示,详细的信息

如表 5-5 所示。

图 5-11 添加纹理入库菜单 图 5-12 界面设计

表 5-5 控件详细的信息

序 号 ID 名 称 变 量

1 IDC_STATIC 纹理影像 ---

2 IDC_BUTTON_BROWSE 浏览 ---

3 IDC_LIST_FILES --- CListBox m_listfiles

4 IDC_PROGRESS --- CProgressCtrl m_progress

5 IDOK 确定 CcomboBox m_subCombolblockSize

6 IDCANCEL 取消 ---

2.程序设计

1)添加变量和函数

为该对话框添加类,名称为 CTextureLoad,在头文件(TextureLoad.h)中添加对

"ImageObject.h"的引用和私有变量:

#include "ImageObject.h" //影像分块处理头文件

private: CImageObject*m_pImageObject; //CImageObject 类型变量,用于实现对影像纹理

的分块处理

int m_totalRows,m_totalCols; //存储纹理影像分块的总行数和总列数

为类 CdemLoad 添加私有函数,代码如下。

实战 OpenGL 三维可视化系统开发与源码精解

146

private:

BOOL GetTextureRange(CString tcrPathname) BOOL WriteImageToDB(CString strFile,int m_RowIndex,int m_ColIndex,int

m_height,int m_width,int m_phramidLayer,int m_fg_width,int m_fg_height);

void SeperateImage(CString mfilename,int m_phramidLayer,CString tempDi- rctory);

并为 IDC_BUTTON_BROWSE 和 IDOK 分别添加 OnButtonBrowse()和 OnOK()响应函数。

2)函数实现

在 TextureLoad.cpp 中 后实现的代码如下。

(1)为 IDC_BUTTON_BROWSE 按钮添加响应函数。

void CTextureLoad::OnButtonBrowse() //浏览外部纹理文件

{ CString strFile,strFilter="Tif files(*.Tif)|*.Tif|\

BMP files(*.BMP)|*.BMP|\

Jpeg files(*.JPG)|*.JPG|\ GIF files(*.GIF)|*.GIF|\

PCX files(*.PCX)|*.PCX|\

Targa files(*.TGA)|*.TGA||"; //定制打开文件对话框,使支持多项选择(OFN_ALLOWMULTISELECT)

CFileDialog fdlg(TRUE,NULL,NULL,OFN_HIDEREADONLY|OFN_ALLOWMULTISELECT,

strFilter); char *pBuf=new char[BUFFERLEN]; //申请缓冲区

fdlg.m_ofn.lpstrFile=pBuf; //让 pBuf 代替 CFileDialog 缓冲区

fdlg.m_ofn.lpstrFile[0]='\0'; fdlg.m_ofn.nMaxFile=BUFFERLEN;

int nCount=0; //初始选择的文件数为 0

//如果成功,则获取对话框多项选择的文件,并依次添加到列表框里 if(fdlg.DoModal()==IDOK)

{ //GetStartPosition():返回指示遍历映射起始位置的 POSITION 位置,如果映射为

//空则返回 NULL

POSITION pos=fdlg.GetStartPosition(); m_listfiles.ResetContent(); //清空列表框

while(pos) //如果映射不为空

{ nCount++;

strFile=fdlg.GetNextPathName(pos); //得到文件名

m_listfiles.AddString(strFile); //将文件名加入到列表框

第 5 章 | 地形三维可视化系统的地形渲染实现

147

}

} delete[] pBuf; //回收缓冲区

}

OnButtonBrowse()函数通过设置打开文件对话框的多项选择功能,实现从外部一

次性选择多个影像纹理文件,并将影像纹理文件名填写到纹理列表框中。

(2)初始化对话框。

BOOL CTextureLoad::OnInitDialog()

{ CDialog::OnInitDialog();

myOci.Init_OCI(); //初始化 OCI

return TRUE; }

(3)为 IDOK 按钮添加响应函数。

void CTextureLoad::OnOK()//对各级纹理影像逐个分块处理,并写入 Oracle 数据库中

{

CString mfilename,stt,tt;

BeginWaitCursor(); CString tempDirctory="c:\\tempRailwayBmp";//存放纹理影像子块的临时文件夹

DWORD dwAttr=GetFileAttributes(tempDirctory); //获取文件夹属性

if(dwAttr==0xFFFFFFFF) //如果该文件夹不存在则创建 CreateDirectory(tempDirctory,NULL); //创建临时文件夹

☆程序第Ⅰ部分☆《对各级纹理影像逐个分块处理,并写入 Oracle 数据库中》

//对各级纹理影像逐个分块处理,并写入 Oracle 数据库中

for(int i=0;i<m_listfiles.GetCount();i++)

{ m_listfiles.SetCurSel(i); //设置列表框当前索引 //得到当前包含全路径的影像文件名

m_listfiles.GetText(m_listfiles.GetCurSel(),mfilename); int nPos=mfilename.ReverseFind('\\');

tt=mfilename.Mid(nPos+1,mfilename.GetLength()-nPos);//得到影像文件名

stt.Format("正在处理:%s",tt); this->SetWindowText(stt); //设置对话框标题

//对影像纹理进行分块处理,并写入 Oracle 数据库中

SeperateImage(mfilename,i,tempDirctory);

函数说明:

实战 OpenGL 三维可视化系统开发与源码精解

148

}

☆程序第Ⅱ部分☆《将影像纹理总体信息写入影像信息表中》

//将影像纹理总体信息写入影像信息表中 tt.Format("INSERT INTO IMAGERECT_INFO VALUES(%.3f,%.3f,%.3f,%.3f,%d,\

%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f)",theApp.m_TexturLeftDown_x,theApp.m_

TexturLeftDown_y,theApp.m_TexturRightUp_x,theApp.m_TexturRightUp_y,m_listfiles.GetCount(),theApp.m_ImageResolution[1],theApp.m_ImageResolution[2],theA

pp.m_ImageResolution[3],theApp.m_ImageResolution[4],theApp.m_ImageResolutio

n[5],theApp.m_ImageResolution[6],theApp.m_ImageResolution[7]); theApp.m_pConnection->Execute(_bstr_t(tt),NULL,adCmdText);//执行 SQL 语句

☆程序第Ⅲ部分☆《删除临时文件夹,恢复进度条初始设置》

EndWaitCursor(); m_progress.ShowWindow(SW_HIDE); //隐藏进度条

m_progress.SetPos(0); //恢复初始位置 0 值

MessageBox("影像纹理文件写入完成!","影像纹理入库",MB_OK); RemoveDirectory(tempDirctory); //删除临时文件夹

theApp.bLoadImage=TRUE; //加载影像成功

CDialog::OnOK(); }

OnOK()函数实现对各级纹理影像逐个分块处理,然后写入 Oracle 数据库中,主要

由 3 部分组成。 第 1 部分是对各级纹理影像逐个分块处理,并写入 Oracle 数据库中。 第 2 部分是将影像纹理总体信息写入影像信息表中。 第 3 部分是删除临时文件夹,恢复进度条初始设置,并给出影像纹理文件写入

完成提示信息。

(4)将纹理影像进行分块处理,并写入 Oracle 数据库中。

void CTextureLoad::SeperateImage(CString mfilename,int m_phramidLayer,

CString tempDirctory) { CString stt,strfile;

☆程序第Ⅰ部分☆《加载影像纹理,计算纹理高度、宽度等信息,为分块做准备》

m_pImageObject->Load(mfilename,NULL,-1,-1); //加载影像纹理

long m_height=m_pImageObject->GetHeight(); //得到纹理高度 long m_width=m_pImageObject->GetWidth(); //得到纹理宽度

函数说明:

第 5 章 | 地形三维可视化系统的地形渲染实现

149

//当前 LOD 级别的纹理分辨率

theApp.m_ImageResolution[m_phramidLayer]=(theApp.m_TexturRightUp_x- theApp.m_TexturLeftDown_x)/m_width;

//纹理影像子块的宽度

int m_fg_width=theApp.m_Dem_BlockWidth/theApp.m_ImageResolution [m_phramidLayer];

//纹理影像子块的高度

int m_fg_height=theApp.m_Dem_BlockWidth/theApp.m_ImageResolution [m_phramidLayer];

//计算当前 LOD 级的纹理影像分块的总行数

if(m_height%m_fg_height==0) m_totalRows=m_height/m_fg_height;

else

{ m_totalRows=m_height/m_fg_height+1;

}

//计算当前 LOD 级的纹理影像分块的总列数

if(m_width%m_fg_width==0)

m_totalCols=m_width/m_fg_width; else

{

m_totalCols=m_width/m_fg_width+1; }

☆程序第Ⅱ部分☆《根据计算的影像纹理信息,对影像纹理进行分块处理》

int nPos=mfilename.ReverseFind('\\');

strfile=mfilename.Mid(nPos+1,mfilename.GetLength()-nPos-5); for(int i=0;i<m_totalRows;i++)

{

for(int j=0;j<m_totalCols;j++) {

//设置进度条值

m_progress.SetPos((i*m_totalCols+j+1)*100.0/(m_totalRows*m_totalCols)); int mleftx=(j-1)*m_fg_width; //影像子块左下角 x 坐标

int mlefty=(m_totalRows-i)*m_fg_height; //影像子块左下角 y 坐标

int mrightx=mleftx+m_fg_width-1; //影像子块右上角 x 坐标 int mrighty=mlefty+m_fg_height-1; //影像子块右上角 y 坐标

//读取由 mleftx、mlefty、mrightx 和 mrighty 所确定的影像子块 m_pImageObject->Crop(mleftx,mlefty,mrightx,mrigty);

stt.Format("%s\\%s@%d_%d.bmp",tempDirctory,strfile,i,j);

m_pImageObject->Save(stt,1);//将读取的影像子块存储到临时文件中 int m_subImageWidth=m_pImageObject->GetWidth(); //得到影像子块的宽度

int m_subImageHeight=m_pImageObject->GetHeight(); //得到影像子块的高度

实战 OpenGL 三维可视化系统开发与源码精解

150

//将影像子块以 BLOB 数据类型写入 Oracle 数据库中 WriteImageToDB(stt,i+1,j+1,m_subImageHeight,m_subImageWidth,m_phrami- dLayer,m_fg_width,m_fg_height);

m_pImageObject->Load(mfilename,NULL,-1,-1);//重新加载原始影像 }

}

}

SeperateImage()函数实现对加载的纹理影像进行分块处理,主要由以下两部分组成。 第 1 部分是加载影像纹理,计算纹理高度、宽度,计算当前 LOD 级别的纹理

影像分块的总行数、总行数,以及纹理分辨率等信息,为分块做准备。 第 2 部分是根据计算的影像纹理信息,对影像纹理进行分块处理,并将分块后

的影像子块以 BLOB 数据类型写入 Oracle 数据库中。

(5)将影像子块以 BLOB 数据类型写入 Oracle 数据库中。

BOOL CTextureLoad::WriteImageToDB(CString strFile,int m_RowIndex,int m_Col- Index,int m_height,int m_width,int m_phramidLayer,int m_fg_width,int m_fg_height)

{

☆程序第Ⅰ部分☆《将影像子块常规数据类型的数据写入 Oracle 数据库中》

//计算影像子块的左下角和左上角 x、y 大地坐标

double m_leftdownx=(m_ColIndex-1)*m_fg_width*theApp.m_ImageResolution [m_phramidLayer]+theApp.m_TexturLeftDown_x;

double m_leftdowny=(m_RowIndex-1)*m_fg_height*theApp.m_ImageResolution

[m_phramidLayer]+theApp.m_TexturLeftDown_y; double m_rightUpx=m_leftdownx+m_width*theApp.m_ImageResolution

[m_phramidLayer];

double m_rightUpy=m_leftdowny+m_height*theApp.m_ImageResolution [m_phramidLayer];

int m_ID=(m_RowIndex-1)*m_totalCols+m_ColIndex;//影像子块的 ID 号

CString tt;

tt.Format("INSERT INTO texture VALUES(%d,%d,%d,%d,%d,EMPTY_BLOB(),\

%ld,%.3f,%.3f,%.3f,%.3f)",\ m_RowIndex,m_ColIndex,m_height,m_width,m_phramidLayer,m_ID,\

m_leftdownx,m_leftdowny,m_rightUpx,m_rightUpy);

//调用 OCI 公共类的 AddNormalDataToDB 函数,将常规数据类型的数据写入 Oracle 数据库中 myOci.AddNormalDataToDB(tt);

函数说明:

第 5 章 | 地形三维可视化系统的地形渲染实现

151

☆程序第Ⅱ部分☆《将影像子块的影像数据以 BLOB 数据类型写入 Oracle 数据库中》

//调用 OCI 公共类的 AddBOLBDataToDB 函数,将 BLOB 类型的数据写入 Oracle 数据库中

tt.Format("SELECT 纹理数据 FROM texture WHERE 行号=%d AND 列号=%d AND 纹理 金子塔层号=%d AND 编号=:%d FOR UPDATE",m_RowIndex,m_ColIndex,

m_phramidLayer,m_ID);

myOci.AddBOLBDataToDB(strFile,tt,m_ID); return TRUE;

}

WriteImageToDB()函数实现将分块后的纹理子块存储到 Oracle 数据库中,主要由

以下两部分组成。 第 1 部分是调用 OCI 公共类的 AddNormalDataToDB 函数,将常规数据类型的

数据(如影像子块的左下角和左上角 x、y 大地坐标、ID 号等)写入 Oracle数据库中。

第 2 部分是调用 OCI 公共类的 AddBOLBDataToDB 函数,将影像子块的影像

数据以 BLOB 数据类型写入 Oracle 数据库中。

(6)得到影像纹理的左下角和右上角 x、y 大地坐标。

BOOL CTextureLoad::GetTextureRange(CString tcrPathname)

{

/* 影像纹理坐标文件是以项目名称同名的,是以扩展名为.tod 的文件存储的,其格式为:

lb:781395.000 1869975.000

rt:797995.000 1876275.000 其中 第 1 行的 lb:表示影像纹理的左下角 x、y 坐标

第 2 行的 rt:表示影像纹理的右上角 x、y 坐标

*/ CString tt,strpath;

int pos=tcrPathname.ReverseFind('\\');

strpath=tcrPathname.Left(pos); pos=strpath.ReverseFind('\\');

tt=strpath.Right(strpath.GetLength()-pos-1);

FILE*fp; tt=strpath+"\\"+tt+".tod";//得到影像范围文件名

if((fp=fopen(tt,"r"))==NULL)//如果文件打开失败

{ MessageBox("影像范围文件"+tt+"不存在!","读取影像范围文件",

MB_ICONINFORMATION);

fclose(fp); //关闭文件 return FALSE;//返回 False

函数说明:

实战 OpenGL 三维可视化系统开发与源码精解

152

}

else {

fscanf(fp,"%s",tt);//得到 lb 字符串

//纹理的左下角 x 坐标 fscanf(fp,"%s",tt);theApp.m_TexturLeftDown_x=atof(tt);

//纹理的左下角 y 坐标

fscanf(fp,"%s\n",tt);theApp.m_TexturLeftDown_y=atof(tt); fscanf(fp,"%s",tt);//得到 rt 字符串

//纹理的右上角 x 坐标

fscanf(fp,"%s",tt);theApp.m_TexturRightUp_x=atof(tt); //纹理的右上角 y 坐标

fscanf(fp,"%s\n",tt);theApp.m_TexturRightUp_y=atof(tt);

fclose(fp);//关闭文件 return TRUE;//返回 True

}

}

GetTextureRange()函数计算影像纹理的左下角和右上角 x,y 大地坐标,并存储到

全局变量中。

(7)响应销毁对话框消息。

void CTextureLoad::OnDestroy()

{ CDialog::OnDestroy();

m_pImageObject=NULL;

delete m_pImageObject; //删除 pImageObject,释放内存 }

后程序运行界面如图 5-13 所示。单击“浏览”按钮同时选择各 LOD 级影像纹理文

件,然后单击“确定”按钮,将各级原始大的影像纹理根据 DEM 的分块大小进行分块处

理并写入 Oracle 数据库中,如图 5-14 所示。

函数说明:

第 5 章 | 地形三维可视化系统的地形渲染实现

153

图 5-13 纹理影像分块入库运行效果图 图 5-14 纹理影像分块入库过程与完成后的提示信息

5.3.4 状态栏指示器的实现

当 DEM 和影像纹理分块完成后,需要从 Oracle 数据库中读取视野范围内的 DEM 数

据子块和对应的影像纹理子块,参与三维地形的绘制。在读取的过程中,需要一定的时间,

如果能够给予读取进度信息将会使程序更具友好性。同时为了能够更好地显示视点坐标、

观察点坐标、内存/渲染块数、刷新频率、当前绘制的地形三角形总数等信息,在这一节中,

我们将这些信息在状态栏上给予提示或显示,使我们能够实时地了解读取进度和三维场景

相关的信息,下面详细说明实现这一功能的程序设计。

1.程序设计

在 CmainFrame 框架创建以下指示器,用来显示上述信息,实现过程如下。

(1)将 MainFrm.cpp 中原来的代码:

static UINT indicators[]= {

ID_SEPARATOR, //status line indicator

ID_INDICATOR_CAPS, ID_INDICATOR_NUM,

ID_INDICATOR_SCRL,

};

修改为:

实战 OpenGL 三维可视化系统开发与源码精解

154

static UINT indicators[]=

{ ID_INDICATOR_LOADRENDERDEM, //【内存/渲染块数】=

ID_INDICATOR_FPS, //刷新频率

ID_INDICATOR_TRINUMBER, //三角形数 ID_INDICATOR_FSANGLE, //【俯视角】=

ID_INDICATOR_VIEWPOS, //视点坐标

ID_INDICATOR_PROGRESSBAR, //进度条 };

然后在 VC 左侧的 Resource 视窗中,单击“String Table”添加 ID_INDICATOR_FPS 等指

示器资源,效果如图 5-15 所示。

图 5-15 在 String Table 中添加指示器资源

(2)添加变量

在 MainFrm.h 头文件中添加 CTextProgressCtrl 类引用和如下变量:

#include "TextProgressCtrl.h"

CTextProgressCtrl 类是通过对 ProgressCtrl 类的扩展,用于实现在进度条上显示文

本。在这里用于显示地形和影像子块加载的百分比进度值(如 62%),详细代码请参

考光盘附件的程序源代码。

通 过 选 择 Visual C++6.0 菜 单 中 “ Project ”→“Add to Project ”→“ Files ” 命 令 将

TextProgressCtrl.cpp 和 TextProgressCtrl.h 文件添加到 T3DSystem 工程中。

protected: CStatusBar m_wndStatusBar; //状态栏变量

CTextProgressCtrl m_progress; //进度条变量

说明:

第 5 章 | 地形三维可视化系统的地形渲染实现

155

为 CmainFrame 类添加公共函数和 OnPaint()函数:

public:

void Set_BarText(int index,CString strText,int nPos);

protected: afx_msg void OnPaint();

(3)函数实现

① 修改 CmainFrame 类的 OnCreate()函数。

//在 m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY)前面添加下面代码 int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)

{

…… //设置各指示器的索引、宽度等信息

m_wndStatusBar.SetPaneInfo(0,ID_INDICATOR_LOADRENDERDEM, SBPS_NORMAL,155);

m_wndStatusBar.SetPaneInfo(1,ID_INDICATOR_FPS,SBPS_NORMAL,85); m_wndStatusBar.SetPaneInfo(2,ID_INDICATOR_TRINUMBER,SBPS_NORMAL,90);

m_wndStatusBar.SetPaneInfo(3,ID_INDICATOR_FSANGLE,SBPS_NORMAL,120);

m_wndStatusBar.SetPaneInfo(4,ID_INDICATOR_VIEWPOS,SBPS_NORMAL,300); m_wndStatusBar.SetPaneInfo(5,ID_INDICATOR_PROGRESSBAR,SBPS_STRETCH, 120);

m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);

EnableDocking(CBRS_ALIGN_ANY);//工具栏 DockControlBar(&m_wndToolBar);

……

}

② 根据索引在状态栏上显示相关信息。

void CMainFrame::Set_BarText(int index,CString strText,int nPos)

{ switch(index)

{

case 0://【内存/渲染块数】 m_wndStatusBar.SetPaneText(index,strText);

break;

case 1://刷新频率 m_wndStatusBar.SetPaneText(index,strText);

break;

case 2://三角形数 m_wndStatusBar.SetPaneText(index,strText);

break;

实战 OpenGL 三维可视化系统开发与源码精解

156

case 3://【俯视角】

m_wndStatusBar.SetPaneText(index,strText); break;

case 4://视点坐标

m_wndStatusBar.SetPaneText(index,strText); break;

case 5://进度条

if(nPos<=0) { m_progress.ShowWindow(SW_HIDE); //隐藏进度条

m_wndStatusBar.SetPaneText(index,strText);

} else

{ m_progress.ShowWindow(SW_SHOW); //显示进度条

m_progress.SetPos(nPos); }

break;

} }

Set_BarText()函数根据传递的索引号设置状态栏指示器的相关信息(如刷新频率、

视点坐标等)。

③ 刷新状态栏指示器上的进度条。

void CMainFrame::OnPaint()

{

CPaintDC dc(this); //device context for painting

CRect rect; //得到状态栏指示器上存放进度条的客户区大小

m_wndStatusBar.GetItemRect(m_wndStatusBar.CommandToIndex(ID_INDICATOR_

PROGRESSBAR),&rect); if(!m_progress.m_hWnd) //如果没有创建进度条控件,则创建

{

m_progress.Create(WS_CHILD|PBS_SMOOTH,rect,&m_wndStatusBar,123); }

else

{ m_progress.MoveWindow(rect); //将进度条移动到状态栏指示器上对应的位置

}

}

程序实现效果如图 5-16 所示。

函数说明:

第 5 章 | 地形三维可视化系统的地形渲染实现

157

图 5-16 状态栏指示器的实现

5.3.5 地形与影像子块的调度

当 DEM 和影像纹理分块完成并存储到数据库中后,需要从数据库中读取相应的地形

子块和影像纹理子块,通过对这些数据块的调度,来实现每个地形块的渲染, 终完成整

体三维地形渲染的实现。在这一节中我们将讲解地形与影像子块从 Oracle 数据库中调度的

程序设计和实现过程。

1.程序设计

1)T3DSystemView 类的程序实现

(1)在 T3DSystemView.cpp 实现文件中添加对 MainFrm 的引用:

#include"MainFrm.h"

(2)在 T3DSystemView.h 头文件中添加对 AllocUnAlloc2D3D 公共类的的引用:

#include"AllocUnAlloc2D3D.h" #include"Vector.h"

#include"Texture.h"

AllocUnAlloc2D3D 是新添加的类,用于实现对一维、二维和三维数组分配和销毁

内存的公共类。Vector 也是新添加的类,用于实现三维坐标相关计算。Texture 是用来

说明:

实战 OpenGL 三维可视化系统开发与源码精解

158

实现读取影像纹理的公共类,详细代码请参考光盘的程序源代码。

选 择 “ Project ” →“ Add to Project ” →“ Files ” 命 令 将 AllocUnAlloc2D3D.cpp 和

AllocUnAlloc2D3D.h 文件添加到 T3DSystem 工程中。

2)添加变量

在 T3DSystemView.h 文件中添加如下常量定义:

#define MAX_TEXTURENUM 500 //定义 多可同时加载的地形块数量

在 T3DSystemView.h 文件中添加如下变量:

//在正射投影模式下地形的 x、y 中心坐标和 x、y 方向的比例 float m_ortho_CordinateOriginX; //在正射投影模式下地形的 x 中心坐标

float m_ortho_CordinateOriginY; //在正射投影模式下地形的 y 中心坐标

float m_ortho_CordinateXYScale; //在正射投影模式下地形的 x、y 方向的比例 //定义用于实现对一维、二维和三维数组分配和销毁内存的类变量

AllocUnAlloc2D3D m_AllocUnAlloc2D3D;

BOOL m_BhasInitOCI;//标识是否初始化 OCI int m_nMapSize; //记录地形子块大小,渲染地形使用

long m_Radius; //包围球半径,用来实现对地形剪裁(位于包围球外的地形块不需绘制)

float m_lodScreenError; //设定的屏幕误差

//地形参数变量

int m_loddem_StartRow; //存储调入的地形块的起始行 int m_loddem_StartCol; //存储调入的地形块的起始列

int m_loddem_EndRow; //存储调入的地形块的结束行

int m_loddem_EndCol; //存储调入的地形块的结束列 float**m_pHeight_My; //存储调入地形子块的高程点

int m_LodDemblockNumber; //存储所加载的地形块数量

double m_DemBlockCenterCoord[MAX_TEXTURENUM][2];//存储所加载地形块中心大地坐标 int m_lodDemBlock[MAX_TEXTURENUM][4];//存储调入地形块的行号、列号、调入地形块数量

bool*m_pbm_DemLod; //标识地形块是否被调入

int**m_DemLod_My; //存储调入地形子块的行号、列号等信息 int m_DemLodIndex[MAX_TEXTURENUM]; //存储调入的地形子块的索引号

int*m_tempDemLodIndex; //存储前一 DEM 子块的索引

/*标识所调入的地形块是否参与绘制(=1,表示调入的地形子块参与绘制;=0,表示未参与绘 制,位于视景体外被剔除掉了)*/

int*m_bsign;

bool**m_pbQuadMat; //标识地形子块的节点是否还需要继续分割 float m_maxHeight,m_minHeight; //DEM 数据点的 大、 小高程值

float m_heighScale;//DEM数据点高程式缩放比例(<1:高程减小;=1:高程不变;>1:高程增大)

第 5 章 | 地形三维可视化系统的地形渲染实现

159

bool m_bLoadInitDemData; //标识加载初始化地形和影像纹理是否成功

//纹理参数变量 double m_Texturexy[MAX_TEXTURENUM][4];//存储调入影像纹理子块左下角和左上角 x、

y 坐标

int m_demTextureID[MAX_TEXTURENUM];//存储调入影像纹理子块的纹理 ID,用于绑定纹理 int m_currebtPhramid; //存储当前影像纹理的 LOD 级别

CTexture m_texturesName; //定义 CTexture 类的变量,用于实现影像纹理的加载

//相机参数变量 BOOL m_bCamraInit;//标识相机是否初始化

CVector3 m_vPosition; //相机视点坐标

CVector3 m_vView; //相机观察点坐标 CVector3 m_vUpVector; //相机中的三维矢量

CVector3 m_oldvPosition; //相机前一视点坐标

float m_viewHeight; //视点高度 float m_camraDistence; //相机距离

CVector3 m_originView; //相机初始视点坐标

CVector3 m_originPosition; //相机初始观察点坐标

3)函数实现

在 T3DSystemView.cpp 实现文件中添加如下程序。

(1)地形子块和影像子块的调入。

BOOL CT3DSystemView::LoadInitDemData() {

☆程序第Ⅰ部分☆《确定是否能够调入地形数据和影像数据》

if(theApp.bLoadImage==FALSE) //如果影像加载失败,则返回

return FALSE;

if(theApp.bLoginSucceed==FALSE) //如果数据库连接失败,则返回 return FALSE;

if(m_bLoadInitDemData==TRUE) //如果地形和影像子块已调入成功,则返回

return TRUE; //如果地形块的总行数或总列数≤0,则返回

if(theApp.m_BlockCols<=0||theApp.m_BlockRows<=0)

return FALSE; else //重新定义二维数组 m_DemLod_My 的大小,并为其分配内存

m_AllocUnAlloc2D3D.Alloc2D_int(m_DemLod_My,theApp.m_BlockCols*theApp.

m_BlockRows+1,3);

☆程序第Ⅱ部分☆《计算在正射投影模式下地形的 x、y 中心坐标和 x、y 方向的比例》

//在正射投影模式下地形的 x、y 中心坐标和 x、y 方向的比例

实战 OpenGL 三维可视化系统开发与源码精解

160

m_ortho_CordinateOriginX=0.5;

m_ortho_CordinateXYScale=(theApp.m_DemRightUp_y-theApp.m_DemLeftDown_y)/(theApp.m_DemRightUp_x-theApp.m_DemLeftDown_x);

m_ortho_CordinateOriginY=m_ortho_CordinateOriginX*m_ortho_CordinateXYScale;

//包围球中心 x 坐标 double m_Sphere_x=(theApp.m_DemRightUp_x+theApp.m_DemLeftDown_x)/2.0;

//包围球中心 y 坐标

double m_Sphere_y=(theApp.m_DemRightUp_y+theApp.m_DemLeftDown_y)/2.0;

☆程序第Ⅲ部分☆《打开数据库,准备读取数据》

CString strsql;

int mRowId,mColID;

strsql="select*from dem_block order by 行号,列号"; if(m_Recordset->State) //如果 m_Recordset 已打开

m_Recordset->Close(); //关闭

try {

m_Recordset->Open(_bstr_t(strsql),(IDispatch*)(theApp.m_pConnection),

adOpenDynamic,adLockOptimistic,adCmdText); }

catch(_com_error& e) //错误处理

{ CString errormessage;

errormessage.Format("打开数据库表失败!\r\n错误信息:%s",e.ErrorMessage());

AfxMessageBox(errormessage,MB_ICONSTOP,0); m_Recordset->Close();

return FALSE;

}

☆程序第Ⅳ部分☆《根据 DEM 子块中心与包围球中心距离,读取位于包围球内的数据》

long mcount;

m_LodDemblockNumber=0; //加载地形块数量 while(!m_Recordset->adoEOF) //如果没有到数据库尾

{

Thevalue=m_Recordset->GetCollect("中心坐标 X"); double m_demblock_centerx=(double)Thevalue; //得到 DEM 子块中心 x 坐标

Thevalue=m_Recordset->GetCollect("中心坐标 Y");

double m_demblock_centery=(double)Thevalue; //得到 DEM 子块中心 y 坐标 //计算 DEM 子块中心与包围球中心距离

double distence=sqrt((m_Sphere_x-m_demblock_centerx)*(m_Sphere_x-m_

demblock_centerx)+(m_Sphere_y-m_demblock_centery)* (m_Sphere_y-m_demblock_centery));

//如果 DEM 子块中心与包围球中心距离<设置的包围球半径,则加载该地形子块

第 5 章 | 地形三维可视化系统的地形渲染实现

161

if(distence<m_Radius)

{ Thevalue=m_Recordset->GetCollect("行号");

mRowId=(long)Thevalue;

Thevalue=m_Recordset->GetCollect("列号"); mColID=(long)Thevalue;

//存储所加载的地形块中心大地 x、y 坐标

m_DemBlockCenterCoord[m_LodDemblockNumber][0]=m_demblock_ centerx-theApp.m_DemLeftDown_x;

m_DemBlockCenterCoord[m_LodDemblockNumber][1]=

-(m_demblock_centery-theApp.m_DemLeftDown_y); m_lodDemBlock[m_LodDemblockNumber][0]=mRowId;//调入的地形块的行号

m_lodDemBlock[m_LodDemblockNumber][1]=mColID;//调入的地形块的列号

//调入的地形块数量 m_lodDemBlock[m_LodDemblockNumber][2]=m_LodDemblockNumber;

//计算调入的地形块在所有 DEM 数据块中的索引

mcount=(mRowId-1)*theApp.m_BlockCols+mColID; //记录调入的第 m_LodDemblockNumber 地形子块的索引

m_DemLodIndex[m_LodDemblockNumber]=mcount;

m_DemLod_My[mcount][0]=mRowId; //调入的地形块的行号 m_DemLod_My[mcount][1]=mColID; //调入的地形块的列号

m_DemLod_My[mcount][2]=m_LodDemblockNumber; //调入的地形块数量

m_pbm_DemLod[mcount]=true; if(m_LodDemblockNumber==0) //如果是第一次调入

{

m_loddem_StartRow=mRowId; //存储调入的地形块的起始行号 m_loddem_StartCol=mColID; //存储调入的地形块的起始列号

}

m_LodDemblockNumber++; //调入地形块数量+1 }

if(mRowId>(theApp.m_BlockRows/2.0+m_Radius/theApp.m_Dem_BlockWidth)&&

mColID>(theApp.m_BlockCols/2.0+m_Radius/theApp.m_Dem_BlockWidth)&& distence>m_Radius)

break;

m_Recordset->MoveNext(); }

☆程序第Ⅴ部分☆《从数据库中读取 DEM 数据和影像数据》

m_Recordset->Close(); //关闭数据库表

m_loddem_EndRow=mRowId; //存储调入的地形块的结束行号 m_loddem_EndCol=mColID; //存储调入的地形块的结束列号

if(m_LodDemblockNumber<=0) //如果加载地形块数量≤0,则表示调入失败,返回

return FALSE; CString strtempfile="c:\\tempdem.txt";//临时 ASCII 文件,用于存储从数据库中读取

DEM 子块数据

实战 OpenGL 三维可视化系统开发与源码精解

162

ExporttotalDemToFIle(strtempfile);//从数据库中读取 DEM 子块数据到临时文件中

ReadHdata(strtempfile);//读取 DEM 所有高程点到全局数组 theApp.m_DemHeight[]中 CMainFrame*pMainFrame=(CMainFrame*)AfxGetApp()->m_pMainWnd;

float iPer=100.0/m_LodDemblockNumber;

for(int i=0;i<m_LodDemblockNumber;i++) {

//将第 m_lodDemBlock[i][0]行和第 m_lodDemBlock[i][1]列的二进制(BLOB)

//DEM 数据从数据库中读取并写入临时 ASCII 文件中 ExportBlobDemToFIle(strtempfile,m_lodDemBlock[i][0],m_lodDemBlock[i][1]);

//从临时的 ASCII 文件读取 DEM 数据点到高程

ReadDataFromFiles(strtempfile,i); //读取并加载对应 DEM 地形子块的影像纹理

getDemBlockTexture(m_lodDemBlock[i][0],m_lodDemBlock[i][1],i);

pMainFrame->Set_BarText(4,"正在加载影像纹理数据,请稍候... ",0); pMainFrame->Set_BarText(5,"",(i+1)*iPer);//在状态栏上显示加载进度信息

}

pMainFrame->Set_BarText(5,"",0); //加载完成,清空进度信息 m_bLoadInitDemData=TRUE; //数据加载成功

☆程序第Ⅵ部分☆《根据读取的地形数据设置相机初始参数》

double mCx=(theApp.m_DemRightUp_x-theApp.m_DemLeftDown_x)/2.0;//初始相机视

点 x 坐标 double mCz=0; //初始相机视点 z 坐标

if(m_bCamraInit==FALSE) //如果相机没有初始化

{ m_bCamraInit=TRUE; //标识相机初始化

float mDis=100; //视点与观察点初始距离

//初始化相机,并记录其各参数 PositionCamera(mCx,m_viewHeight+m_maxHeight*m_heighScale*1.0,mCz,mCx,

m_viewHeight+m_maxHeight*m_heighScale*1.0-mDis*tan(m_viewdegree*PAI_

D180), mCz-mDis,0,1,0);

//相机初始视点坐标

m_originPosition=CVector3(mCx,m_viewHeight+m_maxHeight*m_heighScale* 1.0,mCz);

//相机初始观察点坐标

m_originView=CVector3(mCx,m_viewHeight+m_maxHeight*m_heighScale*1.0- mDis*tan(m_viewdegree*PAI_D180),mCz-mDis);

}

return TRUE; }

LoadInitDemData()函数完成地形子块和影像子块的调入,主要由以下 6 部分组成。 函数说明:

第 5 章 | 地形三维可视化系统的地形渲染实现

163

第 1 部分是确定是否能够调入地形数据和影像数据。 第 2 部分是计算在正射投影模式下地形的 x、y 中心坐标和 x、y 方向的比例,

用于正射投影模式的三维地形渲染。 第 3 部分是打开数据库,准备读取地形数据和影像数据。

第 4 部分是根据 DEM 子块中心与包围球中心距离,读取位于包围球内的数据。 第 5 部分是从数据库中读取 DEM 数据和影像数据。 第 6 部分是根据读取的地形数据设置相机初始参数。

(2)初始化相机,并记录其各参数。

void CT3DSystemView::PositionCamera(double positionX,double positionY, double positionZ,double viewX,double viewY,double viewZ,double upVectorX, double

upVectorY,double upVectorZ)

{ CVector3 vPosition =CVector3(positionX,positionY,positionZ);

CVector3 vView =CVector3(viewX,viewY,viewZ);

CVector3 vUpVector =CVector3(upVectorX,upVectorY,upVectorZ); m_vPosition=vPosition; //视点坐标

m_vView =vView; //观察点坐标

m_vUpVector=vUpVector; //向上矢量坐标 m_oldvPosition=m_vPosition; //保存当前视点坐标

}

PositionCamera()函数完成相机参数初始化,并记录各参数。

(3)从 DEM 数据表中读取二进制 DEM 数据,并写入临时文件中,实现 DEM 数据的

读取。

void CT3DSystemView::ExportBlobDemToFIle(CString strFilename,int RowIndex,

int ColIndex)

{

CString tt; int idcol=(RowIndex-1)*theApp.m_BlockCols+ColIndex;

tt.Format("SELECT DEM数据 FROM dem_block WHERE行号=%d AND列号=%d AND编号

=:%d FOR UPDATE",RowIndex,ColIndex,idcol); myOci.ReadBOLBDataFromDB(strFilename,tt,idcol);//从数据库中读取 BLOB数据

}

(4)从 ASCII 格式的文件中读取 DEM 高程点数据,并写入高程数组中。

函数说明:

实战 OpenGL 三维可视化系统开发与源码精解

164

void CT3DSystemView::ReadDataFromFiles(CString strfiles,int Index)

{ float hh;

//地形子块大小(如分块大小为 33×33,则地形子块大小=33,以此类推)

int mCount=theApp.m_Dem_BlockSize;/ FILE*fp=fopen(strfiles,"r"); //打开文件

//循环读取 DEM 数据 for(int i=0;i<mCount;i++)

{

for(int j=0;j<mCount;j++) {

fscanf(fp,"%f ",&hh);

m_pHeight_My[Index][i*mCount+j]=hh; //将 DEM 高程值写入数组 if(m_maxHeight<hh)m_maxHeight=hh; //计算 大高程值

if(m_minHeight>hh&&hh!=-9999)m_minHeight=hh;//计算 小高程值

} }

fclose(fp); //关闭文件

DeleteFile(strfiles); //删除临时 ASCII 文件 }

ReadDataFromFiles()函数根据 DEM 子块的索引号,读取该子块的所有高程数据并

存储到全局数组中。

(5)读取并加载对应 DEM 地形子块的影像纹理。

void CT3DSystemView::getDemBlockTexture(int RowIndex,int ColIndex,int Index)

{

☆程序第Ⅰ部分☆《打开数据库表》

CString strsql;

int mID;

_RecordsetPtr mRst; mRst.CreateInstance(_uuidof(Recordset));

strsql.Format("select*from TEXTURE WHERE 行号=%d AND 列号=%d AND 纹理金

子塔层号=%d",RowIndex,ColIndex,m_currebtPhramid); try//打开数据库表

{

mRst->Open(_bstr_t(strsql),(IDispatch*)(theApp.m_pConnection),adOpenDynamic,adLockOptimistic,adCmdText);

}

函数说明:

第 5 章 | 地形三维可视化系统的地形渲染实现

165

catch(_com_error& e) //出错处理

{ CString errormessage;

errormessage.Format("打开数据库表失败!\r\n错误信息:%s",e.ErrorMessage());

MessageBox(errormessage,"读取影像纹理",MB_ICONSTOP); mRst->Close(); //关闭数据库表

return;

} if(mRst->adoEOF)

{

mRst->Close(); //关闭数据库表 return;

}

☆程序第Ⅱ部分☆《读取影像纹理子块的左下角、右上角坐标和 ID 号信息》

Thevalue=mRst->GetCollect("左下角坐标 X");

m_Texturexy[Index][0]=(double)Thevalue; Thevalue=mRst->GetCollect("左下角坐标 Y");

m_Texturexy[Index][1]=(double)Thevalue;

Thevalue=mRst->GetCollect("右上角坐标 X"); m_Texturexy[Index][2]=(double)Thevalue;

Thevalue=mRst->GetCollect("右上角坐标 Y");

m_Texturexy[Index][3]=(double)Thevalue; Thevalue=mRst->GetCollect("编号");

mID=(long)Thevalue;

mRst->Close(); //关闭数据库表

☆程序第Ⅲ部分☆《根据传入的行、列值获得影像纹理子块名称,并加载该纹理子块》

strsql.Format("C:\\%d_%d.bmp",RowIndex,ColIndex); //存储调入的影像纹理子块的纹理 ID,用于绑定纹理

m_demTextureID[Index]=m_texturesName.LoadGLTextures(_bstr_t(strsql));

DeleteFile(strsql); //删除临时文件 }

getDemBlockTexture()函数完成影像子块信息读取和纹理加载,主要由以下3部分组成。 第 1 部分是打开数据库表,准备读取影像纹理子块的左下角、右上角坐标和 ID号信息。

第 2 部分是读取影像纹理子块的左下角、右上角坐标和 ID 号信息并存储到数组中。 第 3 部分是根据传入的行、列值获得纹理子块文件名称,并加载该纹理子块。

(6)初始化数组和 OCI。

函数说明:

实战 OpenGL 三维可视化系统开发与源码精解

166

void CT3DSystemView::Init_ArrayData()

{ if(theApp.bLoadImage==FALSE)

return;

if(theApp.bLoginSucceed==TRUE&&m_BhasInitOCI==FALSE) {

m_nMapSize=theApp.m_Dem_BlockSize-1;

int blocks=m_nMapSize/2; m_AllocUnAlloc2D3D.Alloc2D_float(m_pHeight_My,blocks*blocks,

(m_nMapSize+1)*(m_nMapSize+1)); //存储调入地形子块的高程点 m_AllocUnAlloc2D3D.Alloc2D_bool(m_pbQuadMat,blocks*blocks, (m_nMapSize+1)*(m_nMapSize+1)); //标识地形子块的节点是否还需要继续分割

m_AllocUnAlloc2D3D.Alloc1D_bool(m_pbm_DemLod,theApp.m_BlockCols*

theApp.m_BlockRows); //标识地形块是否被调入 m_AllocUnAlloc2D3D.Alloc2D_int(m_DemLod_My,theApp.m_BlockCols*

theApp.m_BlockRows+1,3); //存储调入地形子块的行号,列号等信息

if(m_BhasInitOCI==FALSE) //如果 OCI 还未被初始化 {

myOci.Init_OCI(); //初始化 OCI

m_BhasInitOCI=TRUE; //标识为 True }

}

}

Init_ArrayData()函数根据地形信息,为相关数组分配内存和初始化 OCI。

(7)修改 Init_data()函数,添加如下代码,完成 currebtPhramid 变量初始化。

void CT3DSystemView::Init_data()

{

……

m_currebtPhramid=1;//当前影像纹理的 LOD 级别 }

(8)读取 DEM 所有高程点到全局数组 theApp.m_DemHeight[]中。

void CT3DSystemView::ReadHdata(CString strfilename)

{

CMainFrame*pMainFrame=(CMainFrame*)AfxGetApp()->m_pMainWnd;

long i,j; CString tt;

函数说明:

第 5 章 | 地形三维可视化系统的地形渲染实现

167

☆程序第Ⅰ部分☆《打开 DEM 文件,读取 DEM 文件头信息》

float hh;

FILE*fp=fopen(strfilename,"r");//打开 DEM 文件 //读取 DEM 文件头信息

fscanf(fp,"%s",tt);

fscanf(fp,"%s",tt); fscanf(fp,"%s",tt);

fscanf(fp,"%s",tt);

fscanf(fp,"%s",tt); fscanf(fp,"%s",tt);

fscanf(fp,"%s\n",tt);

☆程序第Ⅱ部分☆《读取 DEM 数据,将高程存储到全局数组中》

//重新定义全局数组 theApp.m_DemHeight[]的大小,并分配内存

theApp.m_DemHeight= new float[theApp.m_Dem_Rows*theApp.m_Dem_cols];

float mPer=100.0/(theApp.m_Dem_Rows*theApp.m_Dem_cols); for(i=0;i<theApp.m_Dem_Rows;i++)

{

for(j=0;j<theApp.m_Dem_cols;j++) {

pMainFrame->Set_BarText(5,"",(i*theApp.m_Dem_cols+j+1)*mPer);

fscanf(fp,"%f ",&hh); //读取高程 theApp.m_DemHeight[i*theApp.m_Dem_cols+j]=hh;

}

fscanf(fp,"\n"); }

fclose(fp); //关闭文件

DeleteFile(strfilename); //删除临时文件 }

ReadHdata()函数完成从 DEM 文件中读取高程数据,并存储到全局数组中,主要

包括以下两个部分。 第 1 部分是打开 DEM 文件,读取 DEM 文件头信息。 第 2 部分是读取 DEM 数据,将高程存储到全局数组中。

(9)从数据库中读取所有高程数据到数组中,是为了用来内插高程时用的。

void CT3DSystemView::ExporttotalDemToFile(CString strFilename)

{

CString tt;

函数说明:

实战 OpenGL 三维可视化系统开发与源码精解

168

int idcol=1;

tt.Format("SELECT DEM 数据 FROM DEM_INFO WHERE 编号=:%d FOR UPDATE",1); myOci.ReadBOLBDataFromDB(strFilename,tt,1);//从数据库中读取 DEM 数据

}

ExporttotalDemToFile()函数通过调用 OCI 公共类的接口函数,将以 BLOB 数据类

型存储在数据库中的 DEM 数据读取出来,并存储到临时文件中。

5.3.6 三维地形纹理映射

在三维图形中,纹理映射(Texture Mapping)的方法运用得 广泛,尤其描述具有真

实感的物体。我们通过加载遥感正射影像图作为三维地形的纹理,大大增加了三维地形的

真实性。OpenGL 提供的纹理映射机制,可以非常方便地实现影像纹理到三维地形的映射,

如图 5-17 所示。

纹理映射只能在 RGBA 模式下执行,不能运用于颜色索引模式。

OpenGL 支持一维、二维和三维纹理,但使用 多 广泛的为二维纹理,下面以二维

纹理为例讲解 OpenGL 纹理映射。

纹理映射是一个相当复杂的过程, 基本的执行纹理映射所需的步骤如图 5-18 所示。

函数说明:

注意:

第 5 章 | 地形三维可视化系统的地形渲染实现

169

图 5-17 地形的纹理映射 图 5-18 执行纹理映射所需的基本步骤

1.纹理定义函数

在 OpenGL 中二维纹理定义函数主要有 glTexImage2D 和 gluBuild2Dmipmaps 两个函

数。但 glTexImage2D 函数要求纹理影像的宽度必须是 2n+2(有边界时,n 是某个整数)、

高度必须是 2m+2(有边界时,m 是某个整数),而 gluBuild2Dmipmaps 可以载入任意大小

的影像纹理,这两个函数的定义如下:

void glTexImage2D(GLenum target,GLint level,GLint components,GLsizei width,

glsizei height,GLint border,Lenum format,GLenum type,const GLvoid*pixels); void gluBuild2DMipmaps(GLenum target,GLint internalFormat,GLsizei width,

glsizei height,GLenum type,const void*data)

(1)glTexImage2D()的用法举例:

glTexImage2D(GL_TEXTURE_2D, //此纹理是一个二维纹理 0, //代表图像的详细程度,默认为 0 即可 3, //颜色成分 R(红色分量)、G(绿色分量)、B(蓝色分量)3 部分

PImage->sizeX, //纹理的宽度

PImage->sizeY, //纹理的高度

实战 OpenGL 三维可视化系统开发与源码精解

170

0, //边框的值

GL_RGB, //告诉 OpenGL 图像数据由红、绿、蓝三色数据组成 GL_UNSIGNED_BYTE, //组成图像的数据是无符号字节类型

PImage->data); //告诉 OpenGL 纹理数据的来源,此例中指向存放在 PImage 记录中的数据

(2)gluBuild2DMipmaps()的用法举例:

gluBuild2DMipmaps(GL_TEXTURE_2D, //此纹理是一个二维纹理

3,//颜色成分 R(红色分量)、G(绿色分量)、B(蓝色分量)3 部分 PImage->sizeX, //纹理的宽度

PImage->sizeY, //纹理的高度

GL_RGB, //告诉 OpenGL 图像数据由红、绿、蓝三色数据组成 GL_UNSIGNED_BYTE, //组成图像的数据是无符号字节类型

PImage->data);//告诉 OpenGL纹理数据的来源,此例中指向存放在 PImage记录中的数据

2.OpenGL 中的纹理控制

一般来说,纹理图像为正方形或长方形。但当它映射到一个多边形或曲面上并变换到

屏幕坐标时,纹理的单个纹素很少对应于屏幕图像上的像素。根据所用变换和所用纹理映

射,屏幕上单个像素可以对应于一个纹素的一小部分(即放大)或一大批纹素(即缩小)。

此外,在进行纹理帖图时, 图像会出现在物体表面的(u,v)位置上,而这些值在(0.0,1.0)

范围内。但是,如果超出这个值域,会发生什么情况呢?这由纹理的映射函数来决定。在

OpenGL 中,这类映射函数称为“Texture Wrapping Mode”。上述这些可通过 OpenGL 中的纹

理控制函数 glTexParameter 来实现,代码如下:

void glTexParameter{if}[v](GLenum targe,GLenum pname,TYPE param);

第 1 个参数 target 可以是 GL_TEXTURE_1D 或 GL_TEXTURE_2D,它指出是为一维

或二维纹理说明参数;后两个参数的取值如表 5-6 所示。

表 5-6 OpenGL 纹理控制函数 pname 和 param 参数说明

第 5 章 | 地形三维可视化系统的地形渲染实现

171

Pname 含 义 param 常用值

GL_TEXTURE_ MIN_FILTER

一个像素对应多个纹素

缩小滤波

GL_NEAREST 使用坐标离像素中心 近的纹理像素, 近点采样,不进行任何过滤操作(这种纹理过滤方法的效果 差,在屏幕显示的图像会显得十分模糊)。

GL_LINEAR 离像素坐标中心 2×2 纹理像素,线性插值(线性过滤)。

GL_NEAREST_MIPMAP_NEAREST 使用 1 个 mipmap 近点采样。

GL_LINEAR_MIPMAP_NEAREST 使用 1 个 mipmap 取线性插值。

GL_NEAREST_MIPMAP_LINEAR 从两个 mipmap 近点采样,进行线性插值(双线性过滤)。

GL_LINEAR_MIPMAP_LINEAR 从两个 mipmap 线性插值取得两个点后,然后在两个值之间进行线性插值(三线性过滤)

(续表)

Pname 含 义 param 常用值

GL_TEXTURE_ MIN_FILTER

一个像素对应多个纹素

缩小滤波

GL_TEXTURE_MAX_ANISOTROPY_EXT 在取样时候,会取 8 个甚至更多的像素来加以处理,所得到的质量 好,不能够单独使用,需用与其他过滤方法结合一起使用(各异向性过滤)

GL_NEAREST 采用坐标 靠近像素中心的纹素,有可能使图像走样。

GL_TEXTURE_ MAG_FILTER

多个像素对应一个纹素

放大滤波

GL_LINEAR 采 用 靠 近 像 素 中 心 的 4 个 像 素 的 加 权 平 均 值 , 所 需 计 算 比GL_LINEAR 要多,因而执行比 GL_NEAREST 要慢一些,但提供了比较光滑的效果

GL_TEXTURE_ WRAP_S

设置纹理在s 方向上的纹理环绕方式

GL_TEXTURE_ WRAP_T

设置纹理在t 方向上的纹理环绕方式

GL_CLAMP GL_REPEAT GL_MIRRORED_REPEAT_ARB CLAMP_TO_BORDER_ARB GL_CLAMP_TO_EDGE (详细说明见表 5-7)

GL_TEXTURE_ BORDER_COLOR

设 置 边 界颜色

表 5-7 OpenGL 纹理映射函数说明

序 号 类 型 特 性

1 GL_REPEAT(重复)

图像在表面上重复出现。在算法上,忽略纹理坐标的整数部分,并将纹理图的副本粘贴在物体表面上。对于大多数复制纹理的使用,在纹理顶部的纹理单元应与底部的纹理单元相匹配,在纹理左侧的纹理单元也应与右侧的纹理单元相匹配,这样才能做到无缝连接

2 GL_CLAMP(截取) 将大于 1.0 的数值设置为 1.0,将小于 0.0 的数值设置为 0.0,即将超出(0.0,1.0)

范围的数值截取到(0.0,1.0)范围内,这样会导致纹理边缘的重复

3 GL_MIRRORED_REPEAT_ARB (镜像重复)

图像在物体表面上不断重复,但是每次重复的时候对图像进行镜像或者反转,这样在纹理边缘处比较连贯。在 OpenGL 中通过 ARB_texture_mirrored_ repeat 扩展来实现

4 CLAMP_TO_BORDER_ARB (边界截取)

在(0.0,1.0)范围外的参数值用单独定义的边界颜色或纹理边缘进行绘制。适合于绘制物体表面的贴花纸。在 OpenGL 中通过 ARB_texture_border_clamp 扩展来实现,CLAMP_TO_BORDER_ARB 在所有 mipmap 层次上对纹理坐标进行截取,使

实战 OpenGL 三维可视化系统开发与源码精解

172

nearest 和 linear 过滤只返回边界纹理单元的颜色

5 GL_CLAMP_TO_EDGE (边缘截取)

总是忽略边界。处于纹理边缘或者靠近纹理边缘的纹理单元都用做纹理计算,但是不包括边界上的纹理单元

1)Bilinear Interpolation(双线性过滤:GL_NEAREST_MIPMAP_LINEAR)

这是一种较好的材质影像插补的处理方式,会先找出 接近的 4 个像素,然后在它们

之间做差补效果, 后产生的结果才会被贴到像素的位置上,这样不会看到“马赛克”现象。

虽然无法提供 佳品质,但具有较好的贴图效果和较少的计算时间。

2)Trilinear Interpolation(三线性过滤)

三线性是一种更复杂材质影像插补处理方式,它只能用于纹理被缩小的情况,需要

先对 初的纹理图像构造一系列分辨率减少并且预先过滤的纹理图(mipmap)。例如对

于一个 8×8 的纹理来说,需要为它构造 4×4、2×2、1×1 这 3 个 mipmap。如果正方形被

缩小到在屏幕上占 6×6 的像素矩阵,一个像素的采样过程就变成这样,首先是到 8×8

的纹理图中进行对 接近它 2×2 的纹理单元矩阵进行采样(也就是上面的线性过滤)。

其次是到 4×4 的纹理图中重复上面的过程。接着把上面两次采样的结果进行加权平均,

得到 后的采样数据。可以看出整个过程一共进行了 3 次的线性过滤,所以这种方法叫

做三线性过滤,它的效果是 3 种线性纹理过滤方法里面 好的,会去除材质的“闪烁”效

果,能提供高品质的贴图效果。一个“双线性过滤”需要 3 次混合,而“三线性过滤”就得

做 7 次混合处理,还需要两倍大的存储器时钟带宽,对于计算机制图的硬件要求较高。

3)Anisotropic Interpolation(各异向性过滤)

假设 Px 为纹理在 x 坐标方向上的缩放的比例因子,Py 为纹理在 y 坐标方向上的缩放

的比例因子,Pmax 为 Px 和 Py 中的 大值,Pmin 为 Px 和 Py 中的 小值。当 Pmax/Pmin

第 5 章 | 地形三维可视化系统的地形渲染实现

173

等于 1 时,也就是说 Px 等于 Py,纹理的缩放是各同向的;但是如果 Pmax/Pmin 不等于 1

而是大于 1,Px 不等于 Py,也就是说纹理在 x 坐标方向和在 y 坐标方向缩放的比例不一样,

纹理的缩放是各异向的,Pmax/Pmin 代表了各异向的程度。

举个例子来说,64×64 的纹理贴到一个开始平行于 xy 平面的正方形上,但是正方形绕

y 轴旋转 60°, 后投影到屏幕上占了 16×32 的像素矩阵。纹理在 x 坐标方向上缩放的比

例因子为 64/16 等于 4,在 y 坐标方向缩放的比例因子为 64/32 等于 2,Pmax 等于 4,Pmin

等于 2。缩放的各异向程度为 2。当把各异向性过滤和线性过滤结合起来的时候,应该是对

接近像素的 4×2 的纹理单元矩阵采样才合理,因为一个像素在 x 坐标方向上对应了更多

的纹理单元(Px > Py)。即使是纹理在一个轴方向上缩小而在另一个轴方向上放大,处理

的过程也是一样的。

如果纹理在一个轴方向上缩小而在另一个轴方向上放大,OpenGL 仍然把它当做

是纹理被缩小的情况,将采用为纹理缩小情况设置的过滤方法为基本过滤方法,然后

再加上各异向性过滤。

OpenGL 里面的各异向性纹理过滤的参数设置是独立于纹理缩小和放大这两种情况

的,也就是说不需要为这两种情况进行分别设置。参数设置十分简单,只有一个参数就是

大各异向程度(TEXTURE_MAX_ANISOTROPY_EXT)。因为纹理缩放的各异向程度越

大,就需要对更多的纹理单元进行采样,这样在处理速度上是不能接受的,所以必须设置

一个 大各异向程度,当 OpenGL 进行各异向性过滤的时候,采用的各异向程度参数为纹

理缩放的各异向程度和 大各异向程度之间的 小值,也就是说当纹理缩放的各异向程度

注意:

实战 OpenGL 三维可视化系统开发与源码精解

174

大于设置的 大各异向程度时,将使用设置的 大各异向程度作为过滤使用的参数。由此

可见,当该参数设置为 1 的时候就是不进行各异向性过滤,1 也是 OpenGL 为这个参数设

定的默认设置。另外还可以通过查询 MAX_TEXTURE_MAX_ANISOTROPY_EXT 获得该

OpenGL 实现支持的 大各异向程度。示例程序如下:

//假设这是一个二维纹理并且已经设置了 mipmap

//获得运行的 OpenGL 实现支持的 大各异向程度

GLfloat largest_supported_anisotropy; glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT,

&largest_supported_anisotropy);

//设置纹理缩小时采用的过滤方法,这里设置的是三线性过滤 glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

//设置纹理放大时采用的过滤方法,这里设置的是线性过滤

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); //用 OpenGL 实现支持的 大各异向程度设置 大各异向程度参数

glTexParameterf(GL_TEXTURE_MAX_ANISOTROPY_EXT, largest_supported_anisotropy);

在 OpenGL 里面使用各异向性纹理过滤首先要求系统运行的 OpenGL 实现支持

EXT_texture_filter_anisotropic 这个OpenGL扩展(图 5-19即为通过OpenGL Extensions Viewer 软件查询的结果)。各异向性纹理过滤不是单独使用的而是和其线性、双线性、

三线性等过滤方法结合一起使用的。

图 5-19 OpenGL Extensions Viewer 软件查询的扩展结果

注意:

第 5 章 | 地形三维可视化系统的地形渲染实现

175

3.映射方式

纹理图像可直接作为画到多边形上的颜色,也可以用纹理中的值来调整多边形(曲面)

原来的颜色,或用纹理图像中的颜色与多边形(曲面)原来的颜色进行混合。因此,OpenGL

提供了 3 种纹理映射的方式,这个函数为:

void glTexEnv{if}[v](GLenum target,GLenum pname,TYPE param);

参数详细说明如表 5-8 所示。

表 5-8 glTexEnv 函数参数说明

target pname param

GL_TEXTURE_ENV_MODE GL_DECAL GL_MODULATEGL_BLEND

说明纹理值与原来颜色不同的处理方式 GL_TEXTURE_ENV

GL_TEXTURE_ENV_COLOR

包含 4 个浮点数(分别是 R、G、B、A 分量)的数组,只在采用 GL_BLEND 纹理函数时才有用

4.纹理坐标

在绘制纹理映射场景时,不仅要给每个顶点定义几何坐标,而且也要定义纹理坐标。

经过多种变换后,几何坐标决定顶点在屏幕上绘制的位置,而纹理坐标决定纹理图像中的

哪一个纹素赋予该顶点。OpenGL 纹理坐标定义的函数为:

void gltexCoord{1234}{sifd}[v](TYPE coords);

设置当前纹理坐标,此后调用 glVertex*()所产生的顶点都赋予当前的纹理坐标。

对于 gltexCoord1*(), s 坐标被设置成给定值, t 和 r 设置为 0,q 设置为 1;用

gltexCoord2*()可以设置 s 和 t 坐标值,r 设置为 0,q 设置为 1;对于 gltexCoord3*(),

q 设置为 1,其他坐标按给定值设置;用 gltexCoord4*()可以给定所有的坐标。使用适

实战 OpenGL 三维可视化系统开发与源码精解

176

当的后缀(s、 i、f 或 d)和 TYPE 的相应值(GLshort、GLint、glfloat 或 Gldouble)

来说明坐标的类型。

整型纹理坐标可以直接应用,而不是像普通坐标那样被映射到(-1,1)之间。

5.命名纹理对象

载入一幅纹理所需要的时间是比较多的,因此应该尽量减少载入纹理的次数。如

果只有一幅纹理,则应该在第一次绘制前就载入它,以后就不需要再次载入了,纹理

对象正是这样一种机制。我们可以把每一幅纹理(包括纹理的像素数据、纹理大小等

信息,也包括了前面所讲的纹理参数)放到一个纹理对象中,通过创建多个纹理对象

来达到同时保存多幅纹理的目的。这样一来,在第一次使用纹理前,就把所有的纹理

都载入,然后在绘制时只需要指明究竟使用哪一个纹理对象就可以了。这样能够大幅

度地提高应用程序的性能,因为绑定(复用)一个原有的纹理对象要比重新加载一幅

纹理图像快得多。

使用纹理对象和使用显示列表有相似之处,即使用一个正整数来作为纹理对象的编号。

在使用前,可以调用 glGenTextures()函数来分配纹理对象。任何非 0 的无符号整数都可以

用来表示纹理对象的名称。为了避免意外的重名,应该坚持 glGenTextures()函数来提供未

使用的纹理对象名称。

void glGenTextures(GLsizei n,GLuint *textures)

在数组 textures 中返回 n 个当前未使用值,表示纹理对象的名称。textures 中所返回的

注意:

第 5 章 | 地形三维可视化系统的地形渲染实现

177

值并不一定是连续的整数。

textures 中的名称被标记为已使用但它们只有在第一次被绑定时才获得纹理状态和维

属性(1D、2D、3D)。

0 作为一个被保留的纹理对象名,它不会被 glGenTextures()函数当做纹理名称而返回。

glGenTextures()函数根据纹理参数返回 n 个纹理名称。纹理名称集合不必是一

个连续的整数集合。因此,可能在先前调用的 glGenTextures()的时候没有名称集合

被返回。 先前调用 glGentTextures()产生的纹理名称集不会由后面调用的 glGenTextures()得

到,除非它们首先被 glDeleteTextures()删除。

不可以在显示列表中包含 glGenTextures()函数。

glIsTexture()函数用于判断一个纹理名称是否处于实际使用中。如果一个纹理名称是由

glGentTextures()函数返回的,但它还没有被绑定,那么 glIsTexture()函数返回 GL_FALSE。

GLboolean glIsTexture(GLuint texture)

如果 texture 已经是个已绑定的纹理对象名称,并且还没有被删除,那么这个函数就返回

GL_TURE。如果 texture 不是一个现有的纹理对象名称,那么这个函数就返回 GL_FALSE。

6.创建和使用纹理对象

glBindTexture()函数可以创建和使用纹理对象。当一个纹理对象名称初次被绑定时(使用

glBindTexture()函数),OpenGL 就会创建一个新的纹理对象,并把纹理对象和纹理属性设置

为默认值。glTexImage*()、glTexSubImage*()、glCopyTexImage()、glCopyTexSubImage*()、

说明:

注意:

实战 OpenGL 三维可视化系统开发与源码精解

178

glTexParameter*()、glPrioritizeTextures()函数的后续调用将把数据存储在这个纹理对象中。纹

理对象可以包含一幅纹理图像,以及相关的 mipmap 图像(如果有的话),并包括相关的数据,

例如纹理宽度、高度、边框宽度、内部格式、成分的分辨率和纹理属性等。被保存的纹理属

性包括缩小和放大过滤器、环绕模式、边框颜色和纹理优先级。

当一个纹理对象再次被绑定时,它的数据就成为当前的纹理状态(以前绑定的纹理对

象的状态被替换)。

void glBindTexture(GLenum target,GLuint textureName)

glBindTexture()函数可以完成 3 个不同的任务,如表 5-9 所示。

表 5-9 glBindTexture()函数可以完成的任务

序 号 参数 textureName 值 功 能

1 当 textureName 是一个非 0 的无符号整数,并且应用程序

是第一次在这个函数中使用这个值时 将创建一个新的纹理对象,把这个名称分

配给它

2 当 textureName 是一个以前已经创建的纹理对象时 这个纹理对象将成为当前活动纹理对象,

可以编辑这个被绑定的纹理对象的内容

3 如果 textureName=0 OpenGL 就停止使用纹理对象,并返回到无

名称的默认纹理

当一个纹理对象被初次绑定(即被创建)时,它的维数由 target 参数所指定,也就是

GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D 或 GL_TEXTURE_CUBE_MAP。

在初次绑定之后,这个纹理对象状态就立即等于 OpenGL 在初始化 target 时的默认状态。

在处于初始状态时,像缩小和放大过滤器、环绕模式、边框颜色和纹理优先级这样的纹理

属性都被设置为它们的默认值。

当想改变纹理时,应该绑定新的纹理。不能在 glBegin()和 glEnd()之间绑定纹理,

说明:

第 5 章 | 地形三维可视化系统的地形渲染实现

179

必须在 glBegin()之前或 glEnd()之后绑定。

7.删除纹理对象

绑定纹理对象和解除纹理对象时,它们的数据仍然保存在纹理资源的某个位置中。如

果纹理资源有限,删除纹理显然是释放纹理资源的有效方法之一。

void glDeleteTextures(GLsizei n,GLuint *textureNames)

删除 n 个纹理对象,它们的名称由 textureNames 数组提供。被释放的纹理对象名称可

以被重新使用(例如,调用 glGenTextures()函数可能会重新返回这些名字)。

如果当前一个绑定的纹理对象被删除,这个绑定就会恢复到默认的纹理,就像 0 为

textureName 参数调用了 glBindTexture()函数一样。

如果试图删除不存在的纹理对象或名称为 0 的纹理对象时,这个函数将忽略该类操作,

并不会产生错误。

下面给出系统中应用纹理映射的程序。

(1)从 BMP 文件创建纹理,并返回纹理 ID 号。

int CTexture::LoadGLTextures(char*Filename)

{ AUX_RGBImageRec*pImage=NULL;

FILE*pFile=NULL;//文件句柄

☆程序第Ⅰ部分☆《打开并加载纹理位图文件》

if(!Filename)//确保文件名已提供

{ return false;//如果没提供,则返回 False

}

if((pFile=fopen(Filename,"rb"))==NULL)//尝试打开文件

{

实战 OpenGL 三维可视化系统开发与源码精解

180

MessageBox(NULL,"不能够打开 BMP 纹理文件!","打开 BMP 纹理文件失败",MB_OK);

MessageBox(NULL,Filename,"Error",MB_OK); return NULL;//如果打开文件失败,则返回 False

}

//读取图像数据并将其返回(使用 glaux 辅助库函数 auxDIBImageLoad 来载入位图) pImage=auxDIBImageLoad(Filename);

if(pImage==NULL) //如果读取失败,则返回

return false;

☆程序第Ⅱ部分☆《创建纹理,并设置纹理控制参数》

//创建纹理,告诉 OpenGL 我们想生成一个纹理名字(如果你想载入多个纹理,就加大数字)

glGenTextures(1,&m_nTxt); //分配一个纹理对象的编号

glPixelStorei(GL_UNPACK_ALIGNMENT,1);//设定像素存储模式 //使用来自位图数据生成的典型纹理,告诉 OpenGL 将纹理名字 m_nTxt 绑定到纹理目标上

glBindTexture(GL_TEXTURE_2D,m_nTxt);

gluBuild2DMipmaps(GL_TEXTURE_2D,3,pImage->sizeX,pImage->sizeY,GL_RGB,GL_UNSIGNED_BYTE,pImage->data);

//pImage->data:告诉 OpenGL 纹理数据的来源。这里指向存放在 pImage→data 中的数据

//pImage->sizeX:纹理的宽度; pImage->sizeY:纹理的高度

//设置纹理模式

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_NEAREST);//双线过滤

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR_MIPMAP_NEA

REST);//双线过滤 //边缘截取

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_EDGE);

//边缘截取 glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGE);

glTexEnvf(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,GL_DECAL);

☆程序第Ⅲ部分☆《释放纹理图像占用的内存和图像结构》

if(pImage) //纹理是否存在

{ if(pImage->data) //纹理图像是否存在

{

free(pImage->data);//释放纹理图像占用的内存 }

free(pImage); //释放图像结构

} fclose(pFile); //关闭文件

return m_nTxt; //返回纹理 ID 号

}

第 5 章 | 地形三维可视化系统的地形渲染实现

181

LoadGLTextures()函数主要由以下 3 部分组成。 第 1 部分是打开并加载纹理位图文件,确保纹理图像能够成功加载。 第 2 部分是由加载的纹理图像创建纹理,并设置纹理控制参数。 第 3 部分是创建纹理,纹理创建成功后,释放纹理图像占用的内存和图像结构,

并返回纹理 ID 号。

(2)设置纹理坐标。

void CT3DSystemView::SetTextureCoord(float x,float z,int mRowIndex,int

mColIndex)

{ double X=x*theApp.m_Cell_xwidth;

double Y=-z*theApp.m_Cell_xwidth;

//计算纹理坐标

float u=(X)/(m_Texturexy[mCurrentTextID][2]-m_Texturexy

[mCurrentTextID][0]); float v=-(Y)/(m_Texturexy[mCurrentTextID][3]-m_Texturexy

[mCurrentTextID][1]);

glMultiTexCoord2fARB(GL_TEXTURE0_ARB,u,v);//指定多重纹理单元TEXTURE0纹理坐标 glMultiTexCoord2fARB(GL_TEXTURE1_ARB,u,v);//指定多重纹理单元TEXTURE1纹理坐标

}

SetTextureCoord()函数根据传入的 x、y 坐标和行列值计算并设置多重纹理单元的

纹理坐标。

5.3.7 地形节点评价系统

在加载 DEM 数据和影像纹理数据后,通过 OpenGL 的纹理映射,就可以初步实现三

维地形的绘制。但作为动态 LOD 三维地形,不同的节点可能需要不同的细节来绘制,这

就需要节点评价系统来实现。节点评价系统即度量地形区域在实时绘制过程中应采用适宜

分辨率的标尺。我们通过在地形四叉树中选取适当的节点集合来完成。对地形某一区域,

集合中覆盖该区域的节点越大,表示该区域的分辨率越低。为了判断一个节点是否需要划

函数说明:

函数说明:

实战 OpenGL 三维可视化系统开发与源码精解

182

分,需要建立一个节点评价系统,决定节点是否继续进行分割。评价系统由两个部分组

成:一是视点相关,二是视点无关,这两部分由节点的误差来确定。节点的误差分为静

态误差(也称视点无关误差)和动态误差(也称视点相关误差),视点相关误差由静态误

差根据视点位置计算,它是简化的决定因素。模型的简化就是以节点的误差为依据,在

分割过程中,当一个节点的误差大于要求的 小误差阈值时,需要对其进行分割;否则,

不必对其进行分割。

1.视点无关标准—静态误差计算

静态误差的计算方法多种多样,但以 大高程差方法为主:设实际的地形曲面为

( , )f x y , 简 化 曲 面 为 ( , )f x y′ , 则 在 某 一 节 点 控 制 区 域 S , 节 点 的 静 态 误 差 为

max | ( , ) ( , ) |f x y f x y′− 。考虑到一个节点包含 9 个顶点,中心点高度值为 0h ,4 个边点的高

度值为 ( 1,2,3,4)ih i = ,4 个角点的高度值为 ( 5,6,7,8)ih i = ,如图 5-20 所示。其中中心点、4

个边点在被分割和未被分割时候会引起一定的误差,其误差值越大,表示这个节点所表示

的地形越粗略。除此之外,为了保持节点粗糙度的单调性,即父节点的粗糙度大于等于子

节点的粗糙度,还要考虑到这个节点的 4 个子节点的粗糙程度,即要考虑 4 个子节点中心

所产生的高度变化。如果取这 9 个点的 大高度值 0 8max( , , )h hL 和 小值 0 8min( , , )h hL ,

计算两个值的差 0 8 0 8max( , , ) min( , , )h h h h hΔ = −L L ,然后构造 /r e h= Δ , r 为节点的静态误

差, e 为节点边长。由此可见,粗糙度的计算是一个递归的过程。

第 5 章 | 地形三维可视化系统的地形渲染实现

183

图 5-20 节点静态误差

2. 视点相关标准—视点相关误差计算

视点相关误差存在各种度量方法, 合理、 直接的方法是计算静态误差在屏幕上引

起的像素差,即以屏幕误差作为评价准则。如图 5-21 所示,设视点为 E,视线方向为 L,

节点位置为 V,视点与屏幕的距离为 d,节点高程差为 Δh,则节点在屏幕上引起的误差为

/{( ) }d V E L h− ⋅ × Δ 。把该投影误差的大小作为衡量不同分辨率 LOD 模型图像质量的差别。

对于特定的情况下,如果投影面与屏幕的比例的是 1:1,那么该误差的大小即是屏幕空间

像素的误差。而该误差的大小是由构造 LOD 模型过程中顶点的合并或分裂所引起的。因

此,可以通过控制屏幕投影误差的大小来控制 LOD 模型的简化,如选取一个像素或多个

像素作为投影误差阈值的大小,从而得到不同精度的 LOD 模型。设屏幕上单位长度对应

的像素个数为 λ ,则节点在屏幕上引起的像素差为 /{( ) }d V E L h λ− ⋅ × Δ × 。

如图 5-21 所示,Lindstrom 屏幕误差算法引入一个顶点带来的误差 h 投影到屏幕上,

用得到的屏幕误差 δ 与阈值参数τ 进行比较,控制该节点被引入或忽略。

实战 OpenGL 三维可视化系统开发与源码精解

184

图 5-21 屏幕像素误差

Lindstrom 给出的顶点判断公式如公式 5-1 所示。

2 2 2 2 22

2 2 2

(( ) ( ) )(( ) ( ) ( )

x x y y

x x y y z z

d h e v e ve v e v e v

λτ

Δ − + −≤

− + − + − (公式 5-1)

在公式中, λ 为单位长度对应的像素个数,τ 为误差像素阈值。

采用屏幕投影误差来衡量一个 LOD 层次是否继续向下划分,而精确计算屏幕误差的公

式比较复杂,时间开销较大,根据公式 5-1 可得到简化误差判定公式。

首先将公式 5-1 进行简化,变为公式 5-2 的形式:

1

2

cos( ) ( )

2 tan( )2

ss

e

h C Nodeerror ieC d

θτ

α⋅ ⋅

= • ≤⋅

(公式 5-2)

在公式中, se 为屏幕空间投影误差估算值, sh 为屏幕高度,θ 为视点的下视角,α 为

视点的垂向张角, Nodeerror( )i 为对应节点的空间误差(即静态误差), 1C 是一个可调的常

数,称为高度分辨率,表示节点静态误差对 LOD 层次分割的影响程度。常数 1C 取值越大,

在其他参数一定的情况下,节点细节较为精致。 ed 为视点到节点的距离,由公式 5-3 计算:

2 2 2( ) ( ) ( )e x x y y z zd e v e v e v= − + − + − (公式 5-3)

常数 2C 也是细节调节因子,称为距离分辨率,表示视点与节点的距离对 LOD 层次分

第 5 章 | 地形三维可视化系统的地形渲染实现

185

割的影响程度,在其他参数一定的情况下, 2C 越大,影响越小,节点细节就越粗糙。

于是,当 se τ> 时,节点设置分割标识为 True,需要进一步分割,否则设置分割标识

为 False,不需要再进行分割。

从公式 5-1 到公式 5-2 的简化在于将点 V 到视点 E 的夹角用屏幕中心点到视点的 E 夹

角代替。该简化会造成部分屏幕边角处误差精度的改变,其大小与视线方向及镜头视角相

关。但优点是使各个点的 cos( )θ 值固定,这大大降低了计算量。实验证明,该简化并没有

对视觉效果产生很大影响。

在公式 5-2 中的前半部分仅视参数(视线方向、视景体参数)发生变化时改变。由公

式 5-2 可知,屏幕投影误差与视点的下视角θ 成反比,与视点距误差计算点的距离 ed 成反

比。为简化计算,可采用 大屏幕空间误差法,令θ =0(即视线总是平行于 x z− 平面),

由此得到的是一个保守的投影误差值,见公式 5-4:

1

2

( )

2 tan( )2

ss

e

h C Nodeerror ieC d

τα

⋅= • ≤

⋅ (公式 5-4)

由公式 5-4 可知,Nodeerror( )i 一定时, ed 越大, se 也就越小,如用户指定的阈值为 δ ,

那么当 ed 充分大时,其在屏幕上的投影误差 se 可以小于 δ ,从而可以将其忽略。

对于 ed 的计算,由于在实际的视觉效果中,模型的详细程度不仅与视点位置有关,还

跟视线方向有关。即使两个节点与视点距离 ed 相等,但视线方向的偏离程度不同,节点的

精度也不同,正对视线方向往往比偏离视线方向的节点需要较高的精度。如图 5-22 所示,

节点 A 的精度应该比节点 B 的精度高一些。所以在考虑视点距离因素之外,对于距离 ed 表

实战 OpenGL 三维可视化系统开发与源码精解

186

达式加入了一个视点方向相关的因子 mDirectionfactor ,令 (1 mDirectionfactor)e ed d′ = + 。

图 5-22 计算节点与视点间的夹角

mDirectionfactor 因子的计算如下。将视线方向投影在上图的 xz 平面上,计算节点与视

点构成的向量与视线方向向量间的夹角α ,取公式 5-5 的因子:

mDirectionfactor | sin |K α= × (公式 5-5)

其中 K 为方向调节因子。综合公式 5-4 和公式 5-5,得到公式 5-6:

2 2 2(1 sin ) ( ) ( ) ( )e x x y y z zd K e v e v e vα′ = + × − + − + − (公式 5-6)

则 终的节点评价公式为公式 5-7:

1'

2

( )

2 tan( )2

ss

e

h C Nodeerror ieC d

τα

⋅= • ≤

⋅ (公式 5-7)

公式 5-7 是综合考虑了节点的视点无关误差和视点相关误差,并 终转换为屏幕误差

来确定节点的分割的。

3.误差阈值负反馈控制

在实时渲染过程中,可以通过设定节点重要性来限制一帧当中三角形的绘制总数,从

而使前后帧保持一致,以达到稳定帧频的效果,实现帧频控制。由于算法每帧的计算量和

三角形数量是正相关的。同时,在地形不同区域粗糙度相差较大时,三角形数量及帧速率

第 5 章 | 地形三维可视化系统的地形渲染实现

187

相差也较大。为此,引入负反馈控制的思想,通过三角形数量对误差阈值τ 进行动态调整:

当场景三角形数量增加过多,就加大τ ,反之亦然。虽然对于不同地形区域来说,该反比

的系数是不同的,但由于视点是连续变化的,绘制的地形区域也是连续变化的。又由于只

需要控制三角形数在一个范围内,因此,对τ 进行一阶负反馈即可,伪代码如下:

if(结果面片数>面片数上限 maxTriNum)

τ =τ *K1;

else if(结果面片数<面片数下限 minTriNum) τ =τ/K2;

只要设定合适的参数 maxTriNum、minTriNum、K1、K2,反馈就可以很好地跟上地形

的变化,同时又不会产生反馈振荡。

4.裂缝的修正处理

在运用树结构对地形进行多分辨率表达时,不同分辨率节点间必然会产生裂缝。裂缝

是由于相邻子块的分辨率不一致所引起的。由于分辨率不同,相邻子块在构建三角形网格

时,因为在相接的边界上所用的点不完全一致而出现裂缝,从而产生空洞的“错误”结果,

如图 5-23(a)所示,这是地形显示必须解决的问题,以保证在三维地形漫游或可视化过

程中不会产生“漏洞”,保证 LOD 模型结构的有效性。

解决的方法之一是不使相邻的网格有相差一倍精度以上的情况出现,这显然容易实

现,但是并不一定能符合实际的地形特征。解决方法之二是将高分辨率模型顶点位置移

动到相邻模型的边界点上,如图 5-23(b)所示。这种方法的优点是其三角形的数目在

裂缝弥合过程中不会增多,但它至少会带来下列问题。

实战 OpenGL 三维可视化系统开发与源码精解

188

需要进行插值产生新的数据点来弥合裂缝。

由于产生了新的数据点,原来的误差计算准则可能被破坏。

解决方法之三是根据“限定四叉树”的结构采用一定算法对缝隙进行缝合,在进行三

维图形可视化时进行判断,如果有“缝隙”则进行一定的处理。根据地形块的特点,通过

改变高分辨率与中低分辨率连接处的网格结构来消除裂缝,改变 T 型节点的连接方式,

把 T 顶点去掉,改变与 相近的顶点连接,它就不需要进行 LOD 层次限制,从而采取

低分辨率,使地形块保持不变。采用强拉顶点的方式来消除裂缝,方法简单,容易实现,

如图 5-23(c)所示。

(a) (b) (c)

图 5-23 不同分辨率节点间的裂缝修正

5.3.8 系统优化算法

由于大规模三维地形的范围大,而人眼的视觉范围有限,因此可设定一定的视野范围,

在实时漫游时只将视野范围内的地形块调入内存绘制。这样,必须有一套高效的场景管理

机制来调度绘制数据,降低无用数据页面载入数目并相应减少无效场景部分的绘制从而消

除迟滞,保证漫游的流畅性。

第 5 章 | 地形三维可视化系统的地形渲染实现

189

1.视景体裁剪

剪裁是地形简化的重要一环,由于在特定视点下,地形大部分区域都位于视锥体之外,

不需要绘制,应在预处理阶段剔除掉,因此对视锥体外的数据进行剪裁是提高显示效率的

关键之一。

如图 5-24 所示,视域通常表示为一个世界坐标系中的截锥体,可以用 6 个形如

0Ax By Cz D+ + + = 的方程组表示。对于在视域外的节点的判断和剔除,传统的裁剪算法

是解一个方程组,把每个三角形与视景体的 6 个裁剪面进行比较,判断是否可见,设三角

形顶点的坐标为( , ,i i ix y z ),如果每个 0i i i iA x B y C z D+ + + > ( 0, ,6i = L ),则该顶点在视

域内,为可见。

图 5-24 世界坐标系中的截锥体

如果处理的数据量很大就会消耗大量的处理时间,导致渲染的速度非常慢。按照上述

算法,剪裁计算量大,影响处理速度。为了加快处理速度,这里采用以地形块的节点为

小单元进行裁剪,并忽略上、下剪裁面和近剪裁面,将视景体投影到 xz 平面上,并加以改

进,建立扇形剪裁区域,把节点作为裁剪的 小单元进行裁剪,对数据块的节点进行可见

性测试。

实战 OpenGL 三维可视化系统开发与源码精解

190

首先以每个地形块的几何中心为圆心,半径为 r,为每个地形块建立球形包围区域,

以该球表示当前的地形块来完成各种裁减操作。选择球形是因为它比传统的包围盒计算更

简单,当前的视野范围也定义为一个以视点为圆心半径为 R 的球体。对地形块的裁减分为

两步,首先遍历所有的地形块,将位于当前视野范围内的地形块全部调入内存,但并不绘

制;然后根据当前的视锥体选择内存中可见的地形块进行绘制。这样,只有在视点移动时

才需要从数据库中读取数据,当视点旋转时则不需要读取数据,所有操作都在内存中完成,

速度更快。

对完成第 1 步需要计算每个包围球的球心到视点的距离,如果小于 r+R 则位于视野内。

对第 2 步的裁减,将视锥体投影到 xy 平面,形成扇形的视野范围。判断每个包围球的球心

在 xy 平面上的投影是否落于扇形内,如果是则绘制对应的地形块,如果不是则分别计算球

心到扇形的两条边的距离。如果 小值小于 r,则绘制对应的地形块,如图 5-25 所示,对

于扇形的圆弧部分,则以圆来判断。

图 5-25 地形块数据剪裁示意图

判断规则如下(点对地形块而言是指地形块中心,对于地形块节点而言是指节点的中心)。

第 5 章 | 地形三维可视化系统的地形渲染实现

191

设点与扇形圆心(视点坐标)的所确定的直线与 X 轴夹角为 rAngle。

扇形起始角和终止角为 m_sectorStartAngle 和 m_sectorEndAngle。

点到扇形圆心(视点坐标)的距离为 mDis。

扇形半径(R)、DEM 子块包围球半径(r)。

点到扇形两直线的距离:d1 和 d2。

点到扇形圆心的距离:d3。

如果 mDis≤R,则分为以下两种情况。

(1)m_sectorStartAngle≤rAngle≤m_sectorEndAngle,则该地形块(或节点)位于扇

形内,否则判断第(2)步。

(2)若 d1≤r 或 d2≤r,则该地形块(或节点)认为位于扇形内,否则位于扇形之外。

如 果 mDis>R( 判 断 点 是 否 位 于 扇 形 圆 弧 外 侧 ) 、 m_sectorStartAngle≤ rAngle≤

m_sectorEndAngle,则判断地形块节点与扇形圆心距离为 d3,若 d3≤R+r,则该认为地形

块(或节点)位于扇形内,否则位于扇形之外。

2.背面剔除

背面剔除是在地形绘制前,剔除掉与视线方向背离的多边形表面,只绘制面向视点的多

边形,从而减少绘制的多边形数目。通常方法是通过计算视方向和多边形表面法向量的夹角

来删减多边形,当夹角大于 90°,则剔除掉该多边形,否则绘制该多边形,如图 5-26 所示。

这一点是基于视角不可能大于 180°的估计,与视线方向夹角为 90α < o(绘制), 90β > o

(剔除掉)。但这对于动态 LOD 地形而言,为了提高渲染速度,地形是采用更高效的三角形条带绘制的,而不是逐个绘制每个三角形,采用计算视线方向和多边形表面法向量的夹

实战 OpenGL 三维可视化系统开发与源码精解

192

角计算量大,而且效率也低。

视线方向和多边形表面法向量的夹角计算可以转换为向量间夹角的计算。设向量u 和向量

v 的夹角为α (较小的那个夹角作为夹角),为此,构造角α 的对边向量 u - v (向量的箭头指

向u ,起始位置为向量 v 的终点),从而u 、v 和u - v 向量构成了一个三角形,如图 5-27 所示。

图 5-26 背面裁剪示意图 图 5-27 计算向量的夹角

假设 ( , , )x y zu u u u= , ( , , )x y zv v v v= ,从三角形的边角关系可以推导出公式 5-8:

cos|| || ||

x x y y z zu v u v u vu v

α+ +

= (公式 5-8)

这样就可以根据向量 u 和 v 的坐标值计算出它们的夹角。

引入向量 u 和 v 的点积运算: x x y y z zu v u v u v u v⋅ = + + ,则公式 5-8 可简单的写成公式 5-9:

cos|| || ||

u vu v

α ⋅= (公式 5-9)

可见,当 u v⋅ =0 时,向量 u 和 v 垂直( u v⊥ );当 u v⋅ >0 时,u 和 v 的夹角为锐角(不

剔除); u v⋅ <0 时,向量 u 和 v 的夹角为钝角(剔除)。

这里所述的背面剔除算法并不同于一般的消隐算法,因为消隐算法需要绘制被遮挡的

物体(即需要执行绘图语句),但是不显示这部分被遮挡的物体,而该背面剔除算法则不绘

制(不执行绘图语句)被剔除的面,减少了渲染三角形的数量,提高渲染速度。

第 5 章 | 地形三维可视化系统的地形渲染实现

193

5.3.9 三维地形的渲染

通过上述地形、影像加载与调度、纹理生成与控制、节点评价系统和系统优化算法等

就可以实现三维地形的渲染,具体实现程序如下。

1.程序设计

1)在 T3DSystemView.h 文件中添加如下变量

private:

float mTimeElaps; //用于计算刷新频率的时间值 //在正射投影模式下地形的 x、y 中心坐标和 x、y 方向的比例

float m_ortho_CordinateOriginX; //在正射投影模式下地形的 x 中心坐标

float m_ortho_CordinateOriginY; //在正射投影模式下地形的 y 中心坐标 float m_ortho_CordinateXYScale; //在正射投影模式下地形的 x、y 方向的比例

//定义用于实现对一维、二维和三维数组分配,并销毁内存的类变量

AllocUnAlloc2D3D m_AllocUnAlloc2D3D;/ BOOL m_BhasInitOCI; //标识是否初始化 OCI

int m_nMapSize; //记录地形子块大小,渲染地形使用

long m_Radius;//包围球半径,用来实现对地形剪裁(位于包围球半径一边的地形块不需绘制) float m_lodScreenError; //设定的屏幕误差

//视景体剪裁半径 long m_R;

long m_r;

//地形参数变量 int m_loddem_StartRow; //存储调入的地形块的起始行

int m_loddem_StartCol; //存储调入的地形块的起始列

int m_loddem_EndRow; //存储调入的地形块的结束行 int m_loddem_EndCol; //存储调入的地形块的结束列

float**m_pHeight_My; //存储调入地形子块的高程点

int m_LodDemblockNumber; //存储所加载的地形块数量 //存储所加载地形块中心大地坐标

double m_DemBlockCenterCoord[MAX_TEXTURENUM][2];

//存储调入地形块的行号、列号、调入的地形块数量 int m_lodDemBlock[MAX_TEXTURENUM][4];

bool*m_pbm_DemLod; //标识地形块是否被调入

int**m_DemLod_My; //存储调入地形子块的行号、列号等信息 int m_DemLodIndex[MAX_TEXTURENUM]; //存储调入的地形子块的索引号

int*m_tempDemLodIndex; //存储前一 DEM 子块的索引

//bsign 标识所调入的地形块是否参与绘制(若值为 1,则表示调入的地形子块参与绘制;

实战 OpenGL 三维可视化系统开发与源码精解

194

若值为 0,则表示未参与绘制,位于视景体外被剔除掉了)

int*m_bsign; bool**m_pbQuadMat;//标识地形子块的节点是否还需要继续分割

float m_maxHeight,m_minHeight;//DEM 数据点的 大、 小高程值

float m_heighScale;//DEM 数据点高程式缩放比例(<1:高程减小;=1:高程不变;>1: 高程增大)

bool m_bLoadInitDemData;//标识加载初始化地形和影像纹理是否成功

//纹理参数变量 //存储调入的影像纹理子块左下角和左上角 x、y 坐标

double m_Texturexy[MAX_TEXTURENUM][4];

//存储调入的影像纹理子块的纹理 ID,用于绑定纹理 int m_demTextureID[MAX_TEXTURENUM];

int m_currebtPhramid; //存储当前影像纹理的 LOD 级别

CTexture m_texturesName; //定义 CTexture 类的变量,用于实现影像纹理的加载 //相机参数变量

BOOL m_bCamraInit; //标识相机是否初始化

CVector3 m_vPosition; //相机视点坐标 CVector3 m_vView; //相机观察点坐标

CVector3 m_vUpVector; //相机中的三维矢量

CVector3 m_oldvPosition; //相机前一视点坐标 float m_viewHeight; //视点高度

float m_oldviewHeight; //前一视点高度

float m_camraDistence; //相机距离 CVector3 m_originView; //相机初始视点坐标

CVector3 m_originPosition; //相机初始观察点坐标

//用于计算相机参数的 CVector3 类型变量 CVector3 m_vStrafe;

CVector3 View;

//刷新频率参数 float mTimeElaps; //用于计算刷新频率的时间值

int nCountFrame; //绘制的帧数

//屏幕误差负反馈参数 BOOL m_checkt; //标识是否打开屏幕误差负反馈功能

long m_maxTrinum; // 大三角形数量

long m_minTrinum; // 小三角形数量 float m_k1; //放大参数

float m_k2; //缩小参数

int m_Drawmode; //绘制模式(线框模式;渲晕模式;纹理模式) BOOL m_stereo; //是否立体模式(True:立体;False;非立体)

BOOL m_bShowbreviary;//是否显示缩略视图

//视景体参数 float m_sectorStartAngle,m_sectorEndAngle;

double m_triPtA[2],m_triPtB[2],m_triPtC[2];

Point m_Viewpolygon[3]; //存储视口三角扇形的 3 个坐标点 //存储视口三角扇形的 3 个坐标点中的 大、 小坐标

double m_Viewpolygon_Minx,m_Viewpolygon_Miny;

第 5 章 | 地形三维可视化系统的地形渲染实现

195

double m_Viewpolygon_Maxx,m_Viewpolygon_Maxy;

//渲染参数 int m_RenderDemblockNumber; //渲染的地形块数量

int mCurrentTextID; //当前渲染地形块的纹理 ID 号

int m_CurrentDemArrayIndex; //当前渲染地形块的数组索引 GLuint m_TerrainList; //地形显示列表

//存储 大、 小高程对应的红、绿、蓝三色的颜色值

float minZ_color_R,minZ_color_G,minZ_color_B; float maxZ_color_R,maxZ_color_G,maxZ_color_B;

CalCulateF mCalF; //数学计算公共类变量

CalCulateF 是新添加的类,用于实现相关图形学计算的公共类(如点是否在封闭

的多边形内)。在光盘附录 A:相关图形学、数学程序模块文件夹下有 CalCulateF 类

实现文件 alCulateF.cpp 和头文件 alCulateF.h 的源代码,这里就不再列出。

通过选择“Project”→“Add to Project”→“Files”命令将 alCulateF.cpp 和 alCulateF.h 文

件添加到 T3DSystem 工程中。然后在 stdafx.h 文件中添加对 CalCulateF 类的引用:

#include"CalCulateF.h"

在 T3DSystemView.h 文件中添加以下的函数。

private:

BOOL ExportBlobTextureToFIle(CString strFilename,int RowIndex,int ColIndex,int mID);

void DrawScene();

void SetTextureCoord(float x,float z,int mRowIndex,int mColIndex); int RenderQuad(int nXCenter,int nZCenter,int nSize,int mRowIndex,int

mColIndex);

void CracksPatchTop(int nXCenter,int nZCenter,int nSize,int mRowIndex,int mColIndex);

void CracksPatchRight(int nXCenter,int nZCenter,int nSize,int

mRowIndex,int mColIndex); void CracksPatchLeft(int nXCenter,int nZCenter,int nSize,int

mRowIndex,int mColIndex);

void CracksPatchBottom(int nXCenter,int nZCenter,int nSize,int mRowIndex,int mColIndex);

void SetTextureCoordZoomRoad(double x,double z,int mRowIndex,int

mColIndex); BOOL GetIfINView(int mBlockRow,int mBlockCol);

float GetNodeError(int nXCenter,int nZCenter,int nSize,int

说明:

实战 OpenGL 三维可视化系统开发与源码精解

196

mRowIndex,int mColIndex);

double maxValue(double dx,double dy,double dz); float GetHeightValue(int X,int Y,int mRowIndex,int mColIndex);

void UpdateQuad(int nXCenter,int nZCenter,int nSize,int nLevel,int

mRowIndex,int mColIndex); void CalculateViewPortTriCord(double View_x,double View_z,double

look_x,double look_z);

BOOL bnTriangle(double cneterx,double cnetery,double x2,double y2,double x3,double y3,double x,double y);

void DrawTerrainZoomonRoad();

void InitList(); void SetCamra(int mStyle);

void CalcFPS();

void DrawTerrain(); void InitTerr();

void Init_ArrayData();

void getDemBlockTexture(int RowIndex,int ColIndex,int Index); void ReadDataFromFiles(CString strfiles,int Index);

void ExportBlobDemToFIle(CString strFilename,int RowIndex,int ColIndex);

void PositionCamera(double positionX,double positionY,double positionZ, double viewX,double viewY,double viewZ,double upVectorX,double

upVectorY, double upVectorZ);

void Init_data(); CVector3 Strafe();

CVector3 UpVector();

CVector3 GetView(); CVector3 GetPos();

2)函数实现

在 T3DSystemView.cpp 文件中添加如下函数。

(1)绘制三维场景。

void CT3DSystemView::OnDraw(CDC*pDC)

{ CT3DSystemDoc*pDoc=GetDocument();

ASSERT_VALID(pDoc);

wglMakeCurrent(pDC->m_hDC,m_hRC);//使 RC 与当前 DC 相关联 DrawScene();//场景绘制

glFlush(); //强制绘图

//交换前后缓存,把 RC 中所绘的传到当前的 DC 上,从而在屏幕上显示 SwapBuffers(m_pDC->GetSafeHdc());

wglMakeCurrent(m_pDC,NULL);//释放 RC,以便其他 DC 进行绘图

}

第 5 章 | 地形三维可视化系统的地形渲染实现

197

在视图类的 OnDraw()函数中,调用场景绘制函数 DrawScene()实现整个三维场景

的绘制。

程序函数讲解

① glFlush()/ glFinish()

功能:将 OpenGL 命令队列中的命令发送给显卡并清空命令队列。

定义:

void glFlush(void);

void glFinish(void);

glFlush()函数将 OpenGL 命令队列中的命令发送给显卡并清空命令队列,发送

完立即返回。 glFinish()函数将 OpenGL 命令队列中的命令发送给显卡并清空命令队列,显卡

完成这些命令后返回。 因此,在绘图命令比较冗长的情况下,可以分段调用 glFlush()以清空命令队列并

让显卡开始先执行这些命令,最后调用 glFinish()来同步。

② SwapBuffers()

功能:交换前后缓存,把 RC 中所绘传到当前的 DC 上,从而在屏幕上显示。

定义:

BOOL SwapBuffers(HDC hdc);

其中参数 hdc 为设备上下文(DC)句柄。

对于双缓冲的程序来说,SwapBuffers()命令只是把前台和后台的缓冲区指针交换

一下而已,也就是把前台的内容变成后台缓冲的内容,把后台的缓冲内容换到了前台。

SwapBuffers 函数本身并不对换过来的成为了后台的 Buffer 做清理工作。因此,绘制

是在原来前台的内容上进行的,当再次使用 SwapBuffers()的时候,原来前台的内容和

函数说明:

说明:

注意:

实战 OpenGL 三维可视化系统开发与源码精解

198

所新绘制的内容一起又回到了前台。

(2)场景绘制。

void CT3DSystemView::DrawScene() {

InitTerr(); //信息初始化和加载地形,影像数据

glDrawBuffer(GL_BACK); //选择后色彩缓冲区 glClearColor(1.0f,1.0f,1.0f,0.0f); //以白色清除背景

glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);//清除颜色缓存和深度缓存

DrawTerrain();//三维地形绘制 }

DrawScene()函数实现三维场景绘制。

程序函数讲解 glDrawBuffer()

功能:选择用于写入或清除的色彩缓冲区,并禁用 glDrawBuffer()调用所启用的缓

冲区。这个函数可以同时启用超过 1 个以上的缓冲区。

定义:

void glDrawBuffer(GLenum mode);

void glDrawBuffers(GLsizei n, const GLenum *buffers);

其中参数 mode 为缓冲区模式,可以是下列值之一。

GL_FRONT GL_FRONT_LEFT GL_FRONT_AND_BACK GL_BACK GL_FRONT_RIGHT GL_AUXi GL_LEFT GL_BACK_LEFT GL_NONE GL_RIGHT GL_BACK_RIGHT

其中,省略了 LEFT 和 RIGHT 的参数表示同时包括左、右立体缓冲区。类似,省略了

FRONT 和 BACK 的参数表示同时包括前后缓冲区。GL_AUXi 中的 i 用于标识某个特定的

函数说明:

第 5 章 | 地形三维可视化系统的地形渲染实现

199

辅助缓冲区。

在 默 认 情 况 下 , mode 值 在 单 缓 冲 环 境 下 为 GL_FRONT, 在 双 缓 冲 环 境 下 为

GL_BACK。

(3)信息初始化和加载地形、影像数据。

void CT3DSystemView::InitTerr()

{

Init_ArrayData(); //初始化数组和 OCI if(LoadInitDemData()==FALSE) //如果数据加载失败,则返回

return; //返回

}

(4)初始化数组和 OCI。

void CT3DSystemView::Init_ArrayData()

{ if(theApp.bLoadImage==FALSE) //如果影像纹理加载失败,则返回

return;

☆程序第Ⅰ部分☆《初始化数组》

//如果数据加载成功,但 OCI 未初始化

if(theApp.bLoginSucceed==TRUE&&m_BhasInitOCI==FALSE) {

m_nMapSize=theApp.m_Dem_BlockSize-1;

int blocks=m_nMapSize/2; //分配数组内存

m_AllocUnAlloc2D3D.Alloc2D_float(m_pHeight_My,blocks*blocks,

(m_nMapSize+1)*(m_nMapSize+1));//存储调入地形子块的高程点 m_AllocUnAlloc2D3D.Alloc2D_bool(m_pbQuadMat,blocks*blocks,

(m_nMapSize+1)*(m_nMapSize+1));//标识地形子块的节点是否还需要继续分割

m_AllocUnAlloc2D3D.Alloc1D_bool(m_pbm_DemLod,theApp.m_BlockCols *theApp.m_BlockRows); //标识地形块是否被调入

m_AllocUnAlloc2D3D.Alloc2D_int(m_DemLod_My,theApp.m_BlockCols

*theApp.m_BlockRows+1,3); //存储调入地形子块的行号、列号等信息

☆程序第Ⅱ部分☆《初始化 OCI》

if(m_BhasInitOCI==FALSE) //如果 OCI 还未被初始化

实战 OpenGL 三维可视化系统开发与源码精解

200

{

myOci.Init_OCI(); //初始化 OCI m_BhasInitOCI=TRUE; //标识为 True } }

}

Init_ArrayData()函数主要由两部分组成。 第 1 部分是根据读取的地形数据,对相应的数组进行初始化(重新定义大小和

分配内存)。 第 2 部分是判断 OCI 的初始化情况,若未初始化,则进行 OCI 的初始化。

(5)三维地形绘制(三维地形渲染)。

void CT3DSystemView::DrawTerrain()

{ if(theApp.bLoginSucceed==FALSE||m_bLoadInitDemData==FALSE)

return;

☆程序第Ⅰ部分☆《绘图参数和多重纹理设置》

//glShadeModel 函数用来设置阴影的效果,主要有 GL_SMOOTH 和 GL_FLAT 两种效果

//其中 GL_SMOOTH 为默认值,表示平滑阴影效果 glShadeModel(GL_SMOOTH);

glDisable(GL_TEXTURE_2D); //关闭二维纹理映射功能

glActiveTextureARB(GL_TEXTURE0_ARB); //选择 TEXTURE0 为设置目标 glEnable(GL_TEXTURE_2D); //激活 TEXTURE0 单元

glActiveTextureARB(GL_TEXTURE1_ARB); //选择 TEXTURE1 为设置目标

glEnable(GL_TEXTURE_2D); //激活 TEXTURE1 单元,启用二维纹理映射 glTexEnvi(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,GL_COMBINE_ARB);

glTexEnvi(GL_TEXTURE_ENV,GL_RGB_SCALE_ARB,2);

glMatrixMode(GL_TEXTURE); //定义矩阵为模型纹理矩阵 glLoadIdentity(); //将当前矩阵置换为单位矩阵

glDisable(GL_TEXTURE_2D); //关闭纹理功能

glActiveTextureARB(GL_TEXTURE0_ARB); //选择 TEXTURE0 为设置目标 glEnable(GL_TEXTURE_2D); //开启 TEXTURE0纹理功能,启用二维纹理映射

☆程序第Ⅱ部分☆《设置视景体和深度测试参数》

glMatrixMode(GL_PROJECTION);

glLoadIdentity();

gluPerspective(40.0+m_ViewWideNarrow,(float)WinViewX/(float)WinViewY,m_ near,m_far);

函数说明:

第 5 章 | 地形三维可视化系统的地形渲染实现

201

glMatrixMode(GL_MODELVIEW); //定义矩阵为模型矩阵

glLoadIdentity(); //将当前矩阵置换为单位矩阵

/*glClearDepth 函数设置深度缓冲区的,它的含义就是在 OpenGL 窗口绘制的图形深入到屏幕

中的程度,深度的意义就是在三维空间中 z 坐标的数值,z 取 0 时表示在平面上,就看不到窗口中的图

形,所以 z 值越小,越远离窗口平面向里,说明窗口中的图形离我们观察者的距离变远了*/

glClearDepth(1.0f); //设置初始化深度缓存值

glEnable(GL_DEPTH_TEST); //启用深度测试 /*在调用 glEnable(GL_DEPTH_TEST);开启这个功能以后,当深度变化小于当前深度值时,更

新深度值*/

glDepthFunc(GL_LESS);

☆程序第Ⅲ部分☆《设置相机参数》

int nCount=0;

if(this->m_stereo==TRUE) //如果是双目立体模式

{ SetCamra(1); //设置相机

}

else //非立体 {

SetCamra(0); //设置相机

}

☆程序第Ⅳ部分☆《绘制三维地形,计算帧率和三角形总数》

//绘制地形 CMainFrame*pMainFrame=(CMainFrame*)AfxGetApp()->m_pMainWnd;

if(theApp.bLoginSucceed==TRUE&&m_bLoadInitDemData==TRUE)

{ float currentTime1=timeGetTime()*0.001f;

nCountFrame++;

CalculateViewPortTriCord(m_vPosition.x,m_vPosition.z,m_vView.x,m_vVi ew.z);

//为数组 m_pbQuadMat 分配内存(LOD 节点分割的标识)

for(int j=0;j<m_LodDemblockNumber;j++) {

memset(m_pbQuadMat[j],0,m_nMapSize*m_nMapSize);

}

CalcFPS();//计算帧率

//创建地形显示列表

glNewList(m_TerrainList,GL_COMPILE_AND_EXECUTE);

m_RenderDemblockNumber=0; //渲染的地形块数量 View=m_vView-m_vPosition;

实战 OpenGL 三维可视化系统开发与源码精解

202

for(int i=0;i<m_LodDemblockNumber;i++)

{ mCurrentTextID=i;

m_CurrentDemArrayIndex=i;

//绑定第 i 地形子块的纹理 glBindTexture(GL_TEXTURE_2D,m_demTextureID[i]);

m_lodDemBlock[i][3]=0;//初始值为未参与渲染

//如果当前地形块不在视景体内 if(bnTriangle(m_triPtA[0],m_triPtA[1],m_triPtB[0],

m_triPtB[1],m_triPtC[0],m_triPtC[1],m_DemBlockCenterCoord

[i][0],m_DemBlockCenterCoord[i][1])==FALSE) continue;

m_RenderDemblockNumber++;//渲染的地形块数量+1

m_lodDemBlock[i][3]=1;//当前地形块参与渲染 //对当前地形块进行 LOD 四叉树分割

UpdateQuad(m_nMapSize/2,m_nMapSize/2,m_nMapSize/2,1,

m_lodDemBlock[i][0],m_lodDemBlock[i][1]); //渲染 LOD 四叉树分割后的当前地形块,并计算出当前所绘制的三角形总数量

nCount+=RenderQuad(m_nMapSize/2,m_nMapSize/2,m_nMapSize/2,

m_lodDemBlock[i][0],m_lodDemBlock[i][1]); }

if(m_checkt==TRUE)//如果进行错误负反馈控制

{ if(nCount>=m_maxTrinum)//如果三角形数量超过 大三角形总数

m_lodScreenError=m_lodScreenError*m_k1;//屏幕误差τ增大

if(nCount<=m_minTrinum)//如果三角形数量小于 小三角形总数 m_lodScreenError=m_lodScreenError/m_k2;//屏幕误差τ减小

}

glEndList();//结束显示列表 CString strText;

strText.Format("【内存/渲染块数】=%d/%d",m_LodDemblockNumber,

m_RenderDemblockNumber); pMainFrame->Set_BarText(0,strText,0);//显示内存中和渲染的地形块数量

float currentTime2=timeGetTime()*0.001f;

mTimeElaps+=currentTime2-currentTime1; if(mTimeElaps>=1.0)//如果两次时间间隔≥1 秒

{

strText.Format("频率%d FPS",nCountFrame); pMainFrame->Set_BarText(1,strText,0);//在状态栏指示器上显示绘制帧率

mTimeElaps=nCountFrame=0;

} strText.Format("三角形%d ",nCount);

//在状态栏指示器上显示当前帧所绘制的地形三角形总数

pMainFrame->Set_BarText(2,strText,0); }

第 5 章 | 地形三维可视化系统的地形渲染实现

203

☆程序第Ⅵ部分☆《重新设置多重纹理参数》

glActiveTextureARB(GL_TEXTURE1_ARB); //选择 TEXTURE1 为设置目标

glDisable(GL_TEXTURE_2D); //关闭 TEXTURE1 纹理功能 glActiveTextureARB(GL_TEXTURE0_ARB); //选择 TEXTURE0 为设置目标 glDisable(GL_TEXTURE_2D); //关闭 TEXTURE0 纹理功能

glDisable(GL_DEPTH_TEST); //关闭深度缓冲区测试功能 }

DrawTerrain()函数实现三维地形的绘制,主要由以下 5 部分组成。 第 1 部分是绘图参数和多重纹理设置。 第 2 部分是设置视景体和深度测试参数。 第 3 部分是设置相机参数(双目立体和非立体模式)。 第 4 部分是绘制三维地形,计算帧率和三角形总数。 第 5 部分是重新设置多重纹理参数。

(6)设置相机。

void CT3DSystemView::SetCamra(int mStyle)

{

☆程序第Ⅰ部分☆《根据立体/非立体显示模式设置相机》

switch(mStyle)

{ case 0://非双目立体模式

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);

break;

case 1://双目立体模式,左相机 gluLookAt(m_vPosition.x-m_camraDistence/2.0,m_vPosition.y,m_vPosition.z,

m_vView.x,m_vView.y,m_vView.z,

m_vUpVector.x,m_vUpVector.y,m_vUpVector.z); break;

case 2://双目立体模式,右相机

gluLookAt(m_vPosition.x+m_camraDistence/2.0,m_vPosition.y,m_vPosition.z, m_vView.x,m_vView.y,m_vView.z,

m_vUpVector.x,m_vUpVector.y,m_vUpVector.z);

}

函数说明:

实战 OpenGL 三维可视化系统开发与源码精解

204

☆程序第Ⅱ部分☆《计算相机俯视角》

CMainFrame*pMainFrame=(CMainFrame*)AfxGetApp()->m_pMainWnd;

CString strText; float dy=m_vPosition.y-m_vView.y;

float dz=fabs(m_vPosition.z-m_vView.z);

if(theApp.bLoginSucceed==TRUE) {

if(fabs(dz)<=0.000001)

m_viewdegree=0; else

m_viewdegree=HDANGLE*atan(dy/dz);

}

☆程序第Ⅲ部分☆《在状态栏指示器上显示相机相关信息》

//在状态栏指示器上显示相关信息 strText.Format("【俯视角】A=%.2f ",m_viewdegree);

pMainFrame->Set_BarText(3,strText,0);

if(m_ifZoomonRoad==FALSE)//非沿线路方案漫游,否则,显示当前里程 {

strText.Format(" 视点坐标:x=%.3f,y=%.3f,z=%.3f ",m_vPosition.x,

m_vPosition.y,fabs(m_vPosition.z)); pMainFrame->Set_BarText(4,strText,0);

}

if(m_ifZoomonRoad==FALSE)//非沿线路方案漫游,否则显示当前里程

{

strText.Format(" 观察点坐标:x=%.3f,y=%.3f,z=%.3f ",m_vView.x, m_vView.y,

fabs(m_vView.z));

pMainFrame->Set_BarText(5,strText,0); }

}

SetCamra()函数主要由以下 3 部分组成。 第 1 部分是根据立体/非立体显示模式设置相机。 第 2 部分是计算相机俯视角。 第 3 部分是在状态栏指示器上显示相机相关信息。

程序函数讲解 gluLookAt()

函数说明:

第 5 章 | 地形三维可视化系统的地形渲染实现

205

功能:相当于设定平移、旋转、倾斜 3 个基本的矩阵,gluLookAt()函数可以变换视

点的位置,使可以在不同角度观察三维场景。在实际的编程应用中,用户在完成场

景的建模后,往往需要选择一个合适的视角或者不停地变换视角,以对场景进行观

察。实用库函数 gluLookAt()就提供了这样的一个功能。

定义:

gluLookAt(GLdouble eyex,GLdouble eyey,GLdouble eyez,GLdouble centerx,GLdouble

centery,GLdouble centerz,GLdouble upx,GLdouble upy,GLdouble upz);

参数说明如下。

eyex、eyey、eyez:指定视点的位置,即眼睛的位置。

centerx、entery、centerz:表示眼睛看的位置,即你观察三维场景(scene)的位置。

upx、upy、upz:指定视点向上的方向,用来定义眼睛所在面的其中一个矢量(可

以理解为从脚到头的矢量方向),多数情况下设置为 0、1、0。

(7)计算简化扇形视景体内的 3 个顶点坐标。

void CT3DSystemView::CalculateViewPortTriCord(double View_x,double View_z,

double look_x,double look_z)

{ double m_derx=look_x-View_x;

double m_derz=look_z-View_z;

float angle_arefa; m_triPtA[0]=View_x;m_triPtA[1]=View_z;

if(m_derx<=0&&m_derz>0)

{ if(m_derx==0)

angle_arefa=270;

else angle_arefa=180+atan(fabs(m_derz/m_derx))*HDANGLE;

}

实战 OpenGL 三维可视化系统开发与源码精解

206

else if(m_derx>=0&&m_derz>0)

{ if(m_derx==0)

angle_arefa=0;

else angle_arefa=360-atan(fabs(m_derz/m_derx))*HDANGLE;

}

else if(m_derx>=0&&m_derz<0) {

if(m_derx==0)

angle_arefa=0; else

angle_arefa=atan(fabs(m_derz/m_derx))*HDANGLE;

} else if(m_derx<=0&&m_derz<0)

{

if(m_derx==0) angle_arefa=90;

else

angle_arefa=180-atan(fabs(m_derz/m_derx))*HDANGLE; }

m_sectorStartAngle=angle_arefa-m_FrustumAngle/2;

m_triPtB[0]=m_R*cos(m_sectorStartAngle*PAI_D180)+m_triPtA[0]; m_triPtB[1]=-m_R*sin(m_sectorStartAngle*PAI_D180)+m_triPtA[1]; m_sectorEndAngle=angle_arefa+m_FrustumAngle/2; m_triPtC[0]=m_R*cos(m_sectorEndAngle*PAI_D180)+m_triPtA[0];

m_triPtC[1]=-m_R*sin(m_sectorEndAngle*PAI_D180)+m_triPtA[1];

m_Viewpolygon[0].x=m_triPtA[0]; m_Viewpolygon[0].y=-m_triPtA[1]; m_Viewpolygon[1].x=m_triPtB[0]; m_Viewpolygon[1].y=-m_triPtB[1];

m_Viewpolygon[2].x=m_triPtC[0]; m_Viewpolygon[2].y=-m_triPtC[1];

//存储视口三角扇形的 3 个坐标点中 大、 小坐标

m_Viewpolygon_Minx=m_Viewpolygon[0].x;

m_Viewpolygon_Miny=m_Viewpolygon[0].y; m_Viewpolygon_Maxx=m_Viewpolygon[0].x;

m_Viewpolygon_Maxy=m_Viewpolygon[0].y;

if(m_Viewpolygon_Minx>m_Viewpolygon[1].x) m_Viewpolygon_Minx=m_Viewpolygon[1].x;

if(m_Viewpolygon_Maxx<m_Viewpolygon[1].x)

m_Viewpolygon_Maxx=m_Viewpolygon[1].x; if(m_Viewpolygon_Miny>m_Viewpolygon[1].y)

m_Viewpolygon_Miny=m_Viewpolygon[1].y;

if(m_Viewpolygon_Maxy<m_Viewpolygon[1].y) m_Viewpolygon_Maxy=m_Viewpolygon[1].y;

if(m_Viewpolygon_Minx>m_Viewpolygon[2].x)

m_Viewpolygon_Minx=m_Viewpolygon[2].x;

第 5 章 | 地形三维可视化系统的地形渲染实现

207

if(m_Viewpolygon_Maxx<m_Viewpolygon[2].x)

m_Viewpolygon_Maxx=m_Viewpolygon[2].x; if(m_Viewpolygon_Miny>m_Viewpolygon[2].y)

m_Viewpolygon_Miny=m_Viewpolygon[2].y;

if(m_Viewpolygon_Maxy<m_Viewpolygon[2].y) m_Viewpolygon_Maxy=m_Viewpolygon[2].y;

}

(8)计算帧率。

void CT3DSystemView::CalcFPS()

{ static DWORD dwStart=0;

static nCount=0;

nCount++; DWORD dwNow=::GetTickCount();

if(dwNow-dwStart>=1000) //每秒计算一次帧率 {

CMainFrame*pMainFrame=(CMainFrame*)AfxGetApp()->m_pMainWnd;

CString strText; strText.Format("帧率%d FPS ",nCount,0);

pMainFrame->Set_BarText(1,strText,0); //在状态栏上指示器显示帧率值

dwStart=dwNow; nCount=0;

}

}

CalcFPS()函数计算三维场景帧率,并将帧率值在状态栏指示器上显示。

(9)初始化显示列表。

void CT3DSystemView::InitList()

{ m_TerrainList=glGenLists(2);//创建两个显示列表的名称

}

InitList()函数初始化地形显示列表 m_TerrainList。

函数说明:

函数说明:

实战 OpenGL 三维可视化系统开发与源码精解

208

程序函数讲解 glGenLists()

功能:分配 n 个连续的未使用的显示列表编号,并返回分配的若干连续编号中 小

的一个。如果函数返回零,则表示分配失败。

定义。

GLuint glGenLists(GLsizei range);

其中参数 range 为分配连续的未使用的显示列表编号的数量。

OpenGL 允许多个显示列表同时存在,就好像 C 语言允许程序中有多个函数同

时存在一样。在 C 语言中,不同的函数用不同的名字来区分,而在 OpenGL 中,不

同的显示列表用不同的正整数来区分。可以自己指定一些各不相同的正整数来表示

不同的显示列表,但是如果不够小心,可能出现一个显示列表将另一个显示列表覆

盖的情况。为了避免这个问题,使用 glGenLists()函数来自动分配一个没有使用的

显示列表编号。

(10)判断地形节点是否在简化扇形视景体内。

BOOL CT3DSystemView::bnTriangle(double cneterx,double cnetery,double x2,

double y2,double x3,double y3,double x,double y)

{

double m_dx,m_dz,d1,d2; float remianAngle; double mDis=(cneterx-x)*(cneterx-x)+(cnetery-y)*(cnetery-y);

mDis=sqrt(mDis); if(mDis>=m_R)

return FALSE;

else if(mDis<=m_r) return TRUE;

m_dx=cneterx-x;

m_dz=cnetery-y; if(m_dx<=0&&m_dz>=0)

{

if(m_dx==0) remianAngle=90;

说明:

第 5 章 | 地形三维可视化系统的地形渲染实现

209

else

remianAngle=atan(fabs(m_dz/m_dx))*HDANGLE; }

else if(m_dx>=0&&m_dz>=0)

{ if(m_dx==0)

remianAngle=90;

else remianAngle=180-atan(fabs(m_dz/m_dx))*HDANGLE;

}

else if(m_dx>=0&&m_dz<=0) {

if(m_dx==0)

remianAngle=270; else

remianAngle=180+atan(fabs(m_dz/m_dx))*HDANGLE;

} else if(m_dx<=0&&m_dz<=0)

{

if(m_dx==0) remianAngle=270;

else

remianAngle=360-atan(fabs(m_dz/m_dx))*HDANGLE; }

if(mDis<=m_R)

{ if(remianAngle>=m_sectorStartAngle&&remianAngle<=m_sectorEndAngle)

return TRUE;

else {

//计算点到直线的距离

d1=mCalF.GetPointToLineDistence(x,y,cneterx,cnetery,x2,y2); //计算点到直线的距离

d2=mCalF.GetPointToLineDistence(x,y,cneterx,cnetery,x3,y3);

if(d1<=m_r||d2<=m_r) return TRUE;

}

} else

{

if(remianAngle>=m_sectorStartAngle&&remianAngle<=m_sectorEndAngle) {

if(mDis<=m_R+m_r)

return TRUE; }

}

实战 OpenGL 三维可视化系统开发与源码精解

210

return FALSE;

}

bnTriangle()函数判断地形节点是否在简化扇形视景体内,只有位于视景体内的节

点才绘制。

(11)对地形块进行四叉树 LOD 分割。

void CT3DSystemView::UpdateQuad(int nXCenter,int nZCenter,int nSize,int

nLevel,int mRowIndex,int mColIndex) {

double mx=(mColIndex-1)*theApp.m_Dem_BlockWidth;

double mz=(mRowIndex-1)*theApp.m_Dem_BlockWidth; //如果地形节点不在视景体内,则返回

if(bnTriangle(m_triPtA[0],m_triPtA[1],m_triPtB[0],m_triPtB[1],m_trip

tC[0],m_triPtC[1],nXCenter*theApp.m_Cell_xwidth+mx,-nZCenter*theApp. m_Cell_ywidth-mz)==FALSE)

return;

if(m_ifZoomonRoad==FALSE)//是否沿线路方案漫游

{

CVector3 vPos=GetPos(); CVector3 vDst(nXCenter*theApp.m_Cell_xwidth+(mColIndex-1)*

theApp.m_Dem_BlockWidth,GetHeightValue(nXCenter,nZCenter,

mRowIndex,mColIndex), -nZCenter*theApp.m_Cell_ywidth-(mRowIndex-1)*theApp.

m_Dem_BlockWidth);

float nDist=maxValue(fabs(vPos.x-vDst.x),fabs(vPos.y-vDst.y), fabs(vPos.z-vDst.z));

float es,em;

em=GetNodeError(nXCenter,nZCenter,nSize,mRowIndex,mColIndex); //计算节点误差 es=m_es*(em/nDist);

if(es<m_lodScreenError)//如果误差小于屏幕误差τ,不绘制 return;

}

if(nSize>1)//表示地形块节点还需要继续分割 {

m_pbQuadMat[m_CurrentDemArrayIndex ][nXCenter+nZCenter

*m_nMapSize]=true; //分割左下角子节点

UpdateQuad(nXCenter-nSize/2,nZCenter-nSize/2,nSize/2,nLevel+1,

mRowIndex,mColIndex);

函数说明:

第 5 章 | 地形三维可视化系统的地形渲染实现

211

//分割右下角子节点

UpdateQuad(nXCenter+nSize/2,nZCenter-nSize/2,nSize/2,nLevel+1, mRowIndex,mColIndex);

//分割右上角子节点

UpdateQuad(nXCenter+nSize/2,nZCenter+nSize/2,nSize/2,nLevel+1, mRowIndex,mColIndex);

//分割左上角子节点

UpdateQuad(nXCenter-nSize/2,nZCenter+nSize/2,nSize/2,nLevel+1, mRowIndex,mColIndex);

}

}

UpdateQuad()函数实现对地形块进行四叉树 LOD 分割

(12)渲染地形节点。

int CT3DSystemView::RenderQuad(int nXCenter,int nZCenter,int nSize,int

mRowIndex,int mColIndex)

{

float hjh; CVector3 pos,VPos;

int kk,nH0,nH1,nH2,nH3,nH4;

int nCount=0;

☆程序第Ⅰ部分☆《地形块节点尺寸判断》

if(nSize>=1.0f)//地形块节点尺寸≥1.0,表示该节点还有子节点 {

//节点中心点和 4 个角点的高程

nH0=GetHeightValue(nXCenter,nZCenter,mRowIndex,mColIndex); nH1=GetHeightValue(nXCenter-nSize,nZCenter-nSize,mRowIndex,mColIndex);

nH2=GetHeightValue(nXCenter+nSize,nZCenter-nSize,mRowIndex,mColIndex);

nH3=GetHeightValue(nXCenter+nSize,nZCenter+nSize,mRowIndex,mColIndex); nH4=GetHeightValue(nXCenter-nSize,nZCenter+nSize,mRowIndex,mColIndex);

}

int mnum=0;

☆程序第Ⅱ部分☆《判断地形节点是否在视景体内》

double mx=(mColIndex-1)*theApp.m_Dem_BlockWidth; double mz=(mRowIndex-1)*theApp.m_Dem_BlockWidth;

//如果地形节点不在视景体内,则返回

if(bnTriangle(m_triPtA[0],m_triPtA[1],m_triPtB[0],m_triPtB[1],m_triPtC [0],m_triPtC[1],nXCenter*theApp.m_Cell_xwidth+mx,-nZCenter*theApp.m_Cell_yw

函数说明:

实战 OpenGL 三维可视化系统开发与源码精解

212

idth-mz)==FALSE)

return 0;

☆程序第Ⅲ部分☆《根据节点分割标识,判断是否进行子节点分割》

float dz=m_maxHeight-m_minHeight;

double x,z;

double xcenter,zcenter; //如果节点标识分割=True,则依次渲染该节点的 4 个子节点

if(m_pbQuadMat[m_CurrentDemArrayIndex][nXCenter+nZCenter*m_nMapSize]==true)

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

mRowIndex,mColIndex);

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

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

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

mRowIndex,mColIndex);

}

☆程序第Ⅳ部分☆《对地形节点进行绘制,并计算绘制的三角形总数量》

else

{ //以扇形绘制地形块节点

glBegin(GL_TRIANGLE_FAN);

x=nXCenter*theApp.m_Cell_xwidth+mx; z=-nZCenter*theApp.m_Cell_ywidth-mz;

VPos.x=x-m_vPosition.x;VPos.y=nH0-m_vPosition.y;VPos.

z=z-m_vPosition.z; xcenter=x; zcenter=-z;

if(DotProduct(View,VPos)>0)//如果在视点范围内

{ if(nH0!=theApp.m_DEM_IvalidValue) //如果高程有效

{ //设置颜色 glColor3f(

(maxZ_color_R-minZ_color_R)*(nH0-m_minHeight)

/dz+minZ_color_R, (maxZ_color_G-minZ_color_G)*(nH0-m_minHeight)

/dz+minZ_color_G,

(maxZ_color_B-minZ_color_B)*(nH0-m_minHeight) /dz+minZ_color_B);

glNormal3f(x,nH0,z);//设置法线

第 5 章 | 地形三维可视化系统的地形渲染实现

213

SetTextureCoord(nXCenter,nZCenter,mRowIndex,

mColIndex); glVertex3i(x,nH0,z);//写入中心点坐标

}

} x=(nXCenter-nSize)*theApp.m_Cell_xwidth+mx;

z=-(nZCenter-nSize)*theApp.m_Cell_ywidth-mz;

VPos.x=x-m_vPosition.x; VPos.y=nH1-m_vPosition.y;

VPos.z=z-m_vPosition.z;

if(DotProduct(View,VPos)>0) {

if(nH1!=theApp.m_DEM_IvalidValue) //如果高程有效

{ //设置颜色

glColor3f(

(maxZ_color_R-minZ_color_R)*(nH1-m_minHeight) /dz+minZ_color_R,

(maxZ_color_G-minZ_color_G)*(nH1-m_minHeight)

/dz+minZ_color_G, (maxZ_color_B-minZ_color_B)*(nH1-m_minHeight)

/dz+minZ_color_B);

glNormal3f(x,nH1,x);//设置法线 SetTextureCoord(nXCenter-nSize,nZCenter-nSize,

mRowIndex,mColIndex);//设置纹理坐标

glVertex3i(x,nH1,z);//左下角点 //节点裂缝修补 CracksPatchTop(nXCenter,nZCenter,nSize,mRowIndex,mColIndex)

nCount++;//三角形数+1

} }

if(nZCenter-nSize==0)

{ for(kk=1;kk<=2*nSize-1;kk++)

{

hjh=GetHeightValue(nXCenter-nSize+kk,nZCenter-nSize, mRowIndex,mColIndex);

x=(nXCenter-nSize+kk)*theApp.m_Cell_xwidth+mx;

z=-(nZCenter-nSize)*theApp.m_Cell_ywidth-mz; VPos.x=x-m_vPosition.x;

VPos.y=hjh-m_vPosition.y;

VPos.z=z-m_vPosition.z; if(DotProduct(View,VPos)>0)//如果节点在视景体内

{

if(hjh!=theApp.m_DEM_IvalidValue) //如果高程有效 {

//设置颜色

实战 OpenGL 三维可视化系统开发与源码精解

214

glColor3f(

(maxZ_color_R-minZ_color_R)*(hjh-m_minHeight) /dz+minZ_color_R,

(maxZ_color_G-minZ_color_G)*(hjh-m_minHeight)

/dz+minZ_color_G, (maxZ_color_B-minZ_color_B)*(hjh-m_minHeight)

/dz+minZ_color_B);

glNormal3f(x,hjh,z);//设置法线 SetTextureCoord(nXCenter-nSize+kk,nZCenter

-nSize,

mRowIndex,mColIndex);//设置纹理坐标 glVertex3i(x,hjh,z);

mnum++;//三角形数+1

} }

}

} x=(nXCenter+nSize)*theApp.m_Cell_xwidth+mx;

z=-(nZCenter-nSize)*theApp.m_Cell_ywidth-mz;

VPos.x=x-m_vPosition.x; VPos.y=nH2-m_vPosition.y;

VPos.z=z-m_vPosition.z;

if(DotProduct(View,VPos)>0)//如果节点在视景体内 {

if(nH2!=theApp.m_DEM_IvalidValue) //如果高程有效

{ //设置颜色

glColor3f(

(maxZ_color_R-minZ_color_R)*(nH2-m_minHeight) /dz+minZ_color_R,

(maxZ_color_G-minZ_color_G)*(nH2-m_minHeight)

/dz+minZ_color_G, (maxZ_color_B-minZ_color_B)*(nH2-m_minHeight)

/dz+inZ_color_B);

glNormal3f(x,nH2,z);//设置法线 SetTextureCoord(nXCenter+nSize,nZCenter-nSize,

mRowIndex,mColIndex);//设置纹理坐标

glVertex3i(x,nH2,z);//左上角点 //节点裂缝修补

CracksPatchRight(nXCenter,nZCenter,nSize,

mRowIndex,mColIndex); nCount++;//三角形数+1

}

} if(nXCenter+nSize>=m_nMapSize)

{

第 5 章 | 地形三维可视化系统的地形渲染实现

215

for(kk=1;kk<=2*nSize-1;kk++)

{ hjh=GetHeightValue(nXCenter+nSize,kk+

(nZCenter-nSize),mRowIndex,mColIndex);

x=(nXCenter+nSize)*theApp.m_Cell_xwidth+mx; z=-(kk+(nZCenter-nSize))*theApp.m_Cell_ywidth-mz;

VPos.x=x-m_vPosition.x;

VPos.y=hjh-m_vPosition.y; VPos.z=z-m_vPosition.z;

if(DotProduct(View,VPos)>0)//如果节点在视景体内

{ if(hjh!=theApp.m_DEM_IvalidValue) //如果高程有效

{

//设置颜色 glColor3f(

(maxZ_color_R-minZ_color_R)*(hjh-m_minHeight)

/dz+minZ_color_R, (maxZ_color_G-minZ_color_G)*(hjh-m_minHeight)

/dz+minZ_color_G,

(maxZ_color_B-minZ_color_B)*(hjh-m_minHeight) /dz+minZ_color_B);

glNormal3f(x,hjh,z);//设置法线

SetTextureCoord(nXCenter+nSize,kk+ (nZCenter-nSize),mRowIndex,mColIndex);

//设置纹理坐标

glVertex3i(x,hjh,z); mnum++;//三角形数+1

}

} }

}

x=(nXCenter+nSize)*theApp.m_Cell_xwidth+mx; z=-(nZCenter+nSize)*theApp.m_Cell_ywidth-mz;

VPos.x=x-m_vPosition.x;

VPos.y=nH3-m_vPosition.y; VPos.z=z-m_vPosition.z;

if(DotProduct(View,VPos)>0)//如果节点在视景体内

{ if(nH3!=theApp.m_DEM_IvalidValue) //如果高程有效

{

//设置颜色 glColor3f(

(maxZ_color_R-minZ_color_R)*(nH3-m_minHeight)

/dz+minZ_color_R, (maxZ_color_G-minZ_color_G)*(nH3-m_minHeight)

/dz+minZ_color_G,

实战 OpenGL 三维可视化系统开发与源码精解

216

(maxZ_color_B-minZ_color_B)*(nH3-m_minHeight)

/dz+minZ_color_B); glNormal3f(x,nH3,z);//设置法线

SetTextureCoord(nXCenter+nSize,nZCenter+nSize,

mRowIndex,mColIndex);//设置纹理坐标 glVertex3i(x,nH3,z);//右上角点

//节点裂缝修补 CracksPatchBottom(nXCenter,nZCenter,

nSize,mRowIndex,mColIndex); nCount++;//三角形数+1

}

} if(nZCenter+nSize>=m_nMapSize)

{

for(kk=1;kk<=2*nSize-1;kk++) {

hjh=GetHeightValue(nXCenter+nSize-kk,nZCenter+nSize,

mRowIndex,mColIndex); x=(nXCenter+nSize-kk)*theApp.m_Cell_xwidth+mx;

z=-(nZCenter+nSize)*theApp.m_Cell_ywidth-mz;

VPos.x=x-m_vPosition.x; VPos.y=hjh-m_vPosition.y;

VPos.z=z-m_vPosition.z;

if(DotProduct(View,VPos)>0)//如果节点在视景体内 {

if(hjh!=theApp.m_DEM_IvalidValue) //如果高程有效

{ //设置颜色

glColor3f(

(maxZ_color_R-minZ_color_R)*(hjh-m_minHeight) /dz+minZ_color_R,

(maxZ_color_G-minZ_color_G)*(hjh-m_minHeight)

/dz+minZ_color_G, (maxZ_color_B-minZ_color_B)*(hjh-m_minHeight)

/dz+minZ_color_B);

glNormal3f(x,hjh,z);//设置法线 SetTextureCoord(nXCenter+nSize-kk,

nZCenter+nSize,mRowIndex,mColIndex);

//设置纹理坐标 glVertex3i(x,hjh,z);

mnum++;//三角形数+1

} }

}

}

x=(nXCenter-nSize)*theApp.m_Cell_xwidth+mx;

第 5 章 | 地形三维可视化系统的地形渲染实现

217

z=-(nZCenter+nSize)*theApp.m_Cell_ywidth-mz;

VPos.x=x-m_vPosition.x; VPos.y=nH4-m_vPosition.y;

VPos.z=z-m_vPosition.z;

if(DotProduct(View,VPos)>0)//如果节点在视景体内 {

if(nH4!=theApp.m_DEM_IvalidValue) //如果高程有效

{ //设置颜色

glColor3f(

(maxZ_color_R-minZ_color_R)*(nH4-m_minHeight) /dz+minZ_color_R,

(maxZ_color_G-minZ_color_G)*(nH4-m_minHeight)

/dz+minZ_color_G, (maxZ_color_B-minZ_color_B)*(nH4-m_minHeight)

/dz+minZ_color_B);

glNormal3f(x,nH4,z);//设置法线 SetTextureCoord(nXCenter-nSize,nZCenter+nSize,

mRowIndex,mColIndex);//设置纹理坐标

glVertex3i(x,nH4,z);//左上角点 //节点裂缝修补 CracksPatchLeft(nXCenter,nZCenter,nSize,mRowIndex,mColIndex)

nCount++;//三角形数+1

} }

if(nXCenter-nSize<=0)

{ for(kk=2*nSize-1;kk>0;kk--)

{

hjh=GetHeightValue(nXCenter-nSize,kk+(nZCenter- nSize),mRowIndex,mColIndex);

x=(nXCenter-nSize)*theApp.m_Cell_xwidth+mx;

z=-(kk+(nZCenter-nSize))*theApp.m_Cell_ywidth-mz; VPos.x=x-m_vPosition.x;

VPos.y=hjh-m_vPosition.y;

VPos.z=z-m_vPosition.z; if(DotProduct(View,VPos)>0)//如果节点在视景体内

{

if(hjh!=theApp.m_DEM_IvalidValue) //如果高程有效 {

//设置颜色

glColor3f( (maxZ_color_R-minZ_color_R)*(hjh-m_minHeight)

/dz+minZ_color_R,

(maxZ_color_G-minZ_color_G)*(hjh-m_minHeight) /dz+minZ_color_G,

(maxZ_color_B-minZ_color_B)*(hjh-m_minHeight)

实战 OpenGL 三维可视化系统开发与源码精解

218

/dz+minZ_color_B);

glNormal3f(x,hjh,z);//设置法线 SetTextureCoord(nXCenter-nSize,kk+

(nZCenter-nSize),mRowIndex,mColIndex);

//设置纹理坐标 glVertex3i(mx,hjh,z);

mnum++;//三角形数+1

} }

}

} x=(nXCenter-nSize)*theApp.m_Cell_xwidth+mx;

z=-(nZCenter-nSize)*theApp.m_Cell_ywidth-mz;

VPos.x=x-m_vPosition.x; VPos.y=nH1-m_vPosition.y;

VPos.z=z-m_vPosition.z;

if(DotProduct(View,VPos)>0) //如果节点在视景体内 {

if(nH1!=theApp.m_DEM_IvalidValue) //如果高程有效

{ //设置颜色

glColor3f(

(maxZ_color_R-minZ_color_R)*(nH1-m_minHeight) /dz+minZ_color_R,

(maxZ_color_G-minZ_color_G)*(nH1-m_minHeight)

/dz+minZ_color_G, (maxZ_color_B-minZ_color_B)*(nH1-m_minHeight)

/dz+minZ_color_B);

glNormal3f(x,nH1,z); //设置法线 //设置纹理坐标

SetTextureCoord(nXCenter-nSize,nZCenter-nSize,

mRowIndex,mColIndex); glVertex3i(x,nH1,z); //重写左下角点

nCount++; //三角形数+1

} }

glEnd(); //渲染地形节点完成

nCount=nCount+mnum; //三角形总数量 mnum=0;

}

☆程序第Ⅴ部分☆《返回三角形数量》

return nCount;//返回三角形数量 }

第 5 章 | 地形三维可视化系统的地形渲染实现

219

RenderQuad()函数实现对地形的渲染,主要由以下 5 部分组成。 第 1 部分是地形块节点尺寸判断。 第 2 部分是判断地形节点是否在视景体内。 第 3 部分是根据节点分割标识,判断是否进行子节点分割。 第 4 部分是对地形节点进行绘制,并计算绘制的三角形总数量。 第 5 部分是返回所绘制地形的三角形总数量。

(13)根据节点的 X、Y 和地形子块的行号和列号从高程数组中得到对应的节点高程值。

float CT3DSystemView::GetHeightValue(int X,int Y,int mRowIndex,int mColIndex) {

return m_pHeight_My[m_CurrentDemArrayIndex][X+Y*(m_nMapSize+1)]

*m_heighScale; }

(14)设置纹理坐标。

void CT3DSystemView::SetTextureCoord(float x,float z,int mRowIndex,

int mColIndex)

{

double X=x*theApp.m_Cell_xwidth; double Y=-z*theApp.m_Cell_xwidth;

float u=(X)/(m_Texturexy[mCurrentTextID][2]-m_Texturexy

[mCurrentTextID][0]); float v=-(Y)/(m_Texturexy[mCurrentTextID][3]-m_Texturexy

[mCurrentTextID][1]);

//指定多重纹理单元 TEXTURE0 的纹理 u、v 坐标 glMultiTexCoord2fARB(GL_TEXTURE0_ARB,u,v);

//指定多重纹理单元 TEXTURE1 的纹理 u、v 坐标

glMultiTexCoord2fARB(GL_TEXTURE1_ARB,u,v); }

(15)节点裂缝修补(底部)。

void CT3DSystemView::CracksPatchBottom(int nXCenter,int nZCenter,int nSize,

int mRowIndex,int mColIndex)

{ if(nSize<=0)return;//如果地形节点尺寸≤0,则返回

if(m_ifZoomonRoad==FALSE)return;

if(nZCenter+2*nSize<m_nMapSize)

函数说明:

实战 OpenGL 三维可视化系统开发与源码精解

220

{

if(!m_pbQuadMat[m_CurrentDemArrayIndex][nXCenter+(nZCenter+2*nSize) *m_nMapSize])

return;

} else

return;

//节点裂缝修补 CracksPatchBottom(nXCenter+nSize/2,nZCenter+nSize/2,nSize/2,mRowIndex,

mColIndex);

SetTextureCoord(nXCenter,nZCenter+nSize,mRowIndex,mColIndex); //设置纹理坐标

glVertex3i(nXCenter*theApp.m_Cell_xwidth+(mColIndex-1)*theApp.m_Dem_

BlockWidth, GetHeightValue(nXCenter,nZCenter+nSize,mRowIndex,mColIndex),-(nZCenter

+nSize)*theApp.m_Cell_ywidth-((mRowIndex-1)*theApp.m_Dem_BlockWidth));

CracksPatchBottom(nXCenter-nSize/2,nZCenter+nSize/2,nSize/2,mRowIndex, mColIndex);

}

(16)节点裂缝修补(左侧)。

void CT3DSystemView::CracksPatchLeft(int nXCenter,int nZCenter,int nSize,

int mRowIndex,int mColIndex) {

if(nSize<=0)return;//如果地形节点尺寸≤0,则返回

if(m_ifZoomonRoad==FALSE)return; if(nXCenter-2*nSize>=0)

{

if(!m_pbQuadMat[m_CurrentDemArrayIndex][(nXCenter-2*nSize)+nZCenter* m_nMapSize])

return;

} else

return;

CracksPatchLeft(nXCenter-nSize/2,nZCenter+nSize/2,nSize/2,mRowIndex, mColIndex);

SetTextureCoord(nXCenter-nSize,nZCenter,mRowIndex,mColIndex);

//设置纹理坐标 glVertex3i((nXCenter-nSize)*theApp.m_Cell_xwidth+(mColIndex-1)*theAp p.

m_Dem_BlockWidth,GetHeightValue(nXCenter-nSize,nZCenter,mRowIndex,

mColIndex),-nZCenter*theApp.m_Cell_ywidth-((mRowIndex-1)*theApp. m_De m_BlockWidth));

CracksPatchLeft(nXCenter-nSize/2,nZCenter-nSize/2,nSize/2,mRowIndex,

mColIndex);

第 5 章 | 地形三维可视化系统的地形渲染实现

221

}

(17)节点裂缝修补(右侧)。

void CT3DSystemView::CracksPatchRight(int nXCenter,int nZCenter,int nSize,

int mRowIndex,int mColIndex) {

if(nSize<=0)return;//如果地形节点尺寸≤0,则返回

if(m_ifZoomonRoad==FALSE)return; if(nXCenter+2*nSize<m_nMapSize)

{

if(!m_pbQuadMat[m_CurrentDemArrayIndex][(nXCenter+2*nSize)+nZCenter* m_nMapSize])

return;

} else

return;

CracksPatchRight(nXCenter+nSize/2,nZCenter-nSize/2,nSize/2,mRowIndex, mColIndex);

SetTextureCoord(nXCenter+nSize,nZCenter,mRowIndex,mColIndex);

//设置纹理坐标 glVertex3i((nXCenter+nSize)*theApp.m_Cell_xwidth+(mColIndex-1)*theApp.

m_Dem_BlockWidth,GetHeightValue(nXCenter+nSize,nZCenter,mRowIndex,

mColIndex),-nZCenter*theApp.m_Cell_ywidth-((mRowIndex-1)*theApp. m_Dem_BlockWidth));

CracksPatchRight(nXCenter+nSize/2,nZCenter+nSize/2,nSize/2,mRowIndex,

mColIndex); }

(18)节点裂缝修补(顶部)。

void CT3DSystemView::CracksPatchTop(int nXCenter,int nZCenter,int nSize, int mRowIndex,int mColIndex)

{

if(nSize<=0)return;//如果地形节点尺寸≤0,则返回 if(m_ifZoomonRoad==FALSE)return;

if(nZCenter-2*nSize>=0) {

if(!m_pbQuadMat[m_CurrentDemArrayIndex][nXCenter+(nZCenter-2*nSize)

*m_nMapSize]) return;

}

else

实战 OpenGL 三维可视化系统开发与源码精解

222

return;

CracksPatchTop(nXCenter-nSize/2,nZCenter-nSize/2,nSize/2,mRowIndex, mColIndex);

SetTextureCoord(nXCenter,nZCenter-nSize,mRowIndex,mColIndex);

//设置纹理坐标 glVertex3i(nXCenter*theApp.m_Cell_xwidth+(mColIndex-1)*theApp.

m_Dem_BlockWidth,GetHeightValue(nXCenter,nZCenter-nSize,mRowIndex,

mColIndex),-(nZCenter-nSize)*theApp.m_Cell_ywidth-((mRowIndex-1)* theApp.m_Dem_BlockWidth));

CracksPatchTop(nXCenter+nSize/2,nZCenter-nSize/2,nSize/2,mRowIndex,

mColIndex); }

(19)将 Init_data()函数中添加下面的代码,完成相关变量的初始化。

void CT3DSystemView::Init_data() //初始化相关变量

{

m_currebtPhramid=0; //当前影像纹理的 LOD 级别 mTimeElaps=0; //用于计算帧率的时间值

m_Drawmode=3; //绘制模式(线框模式;渲晕模式;纹理模式)

m_bCamraInit=FALSE; //相机未初始化 m_bLoadInitDemData=FALSE; //地形和影像数据未调入

m_LodDemblockNumber=0; //加载地形块数量初始为 0

m_RenderDemblockNumber=0; //渲染地形块数量初始为 0 //相机参数向上矢量

m_vUpVector.x=0;

m_vUpVector.y=1; m_vUpVector.z=0;

m_viewdegree=0; //初始视角增量

m_viewHeight=m_oldviewHeight=10; //相机初始高度 m_camraDistence=4; //双目立体模式下的左右相机初始间距

m_heighScale=1.0; //高程缩放系数

m_Radius=30000; //包围球半径 m_R=3500;

m_r=1500;

m_BhasInitOCI=FALSE; //初始 OCI 未初始化 mTimeElaps=0; //用于计算帧率的时间值

m_maxHeight=-9999; //初始 大高程

m_minHeight=9999; //初始 小高程 //DEM 地形 大高程和 小高程对应的颜色初始值

minZ_color_R=0;minZ_color_G=1;minZ_color_B=0; //绿色

maxZ_color_R=1;maxZ_color_G=0;maxZ_color_B=0; //红色 }

第 5 章 | 地形三维可视化系统的地形渲染实现

223

如图 5-28 所示为三维地形渲染效果图。

在三维地形渲染中,我们多次使用了显示列表,下面对显示列表进行介绍。

在编写程序时如果遇到重复的工作,我们往往将重复的工作编写为函数,在需要的地

方调用它。类似地,在编写 OpenGL 程序时,如果遇到重复的工作,也可以创建一个显示

列表,把重复的工作装入其中,并在需要的地方调用这个显示列表。

显示列表可以提高性能,因为可以用它来存储 OpenGL 函数,供以后执行。如果需要

多次绘制同一个几何图形,或者需要多次调用用于更改状态的函数,就可以把这些函数存

储在显示列表中。使用显示列表,可以一次定义几何图形(或状态更改),并在以后多次调

用它们。

使用显示列表一般有 4 个步骤:分配显示列表编号、创建显示列表、调用显示列表、

销毁显示列表,如图 5-29 所示。

图 5-28 三维地形渲染效果图 图 5-29 使用显示列表的 4 个步骤

下面将详细讲述这 4 个步骤。

实战 OpenGL 三维可视化系统开发与源码精解

224

(1)分配显示列表编号。

OpenGL 允许多个显示列表同时存在,就好像 C 语言允许程序中有多个函数同时存在

一样。在 C 语言中,不同的函数用不同的名字来区分,而在 OpenGL 中,不同的显示列表

用不同的正整数来区分。

你可以自己指定一些各不相同的正整数来表示不同的显示列表。但是如果不够小心,

可能出现一个显示列表将另一个显示列表覆盖的情况。为了避免这一问题,使用 glGenLists

函数来自动分配一个没有使用的显示列表编号,定义如下:

GLuint glGenLists(GLsizei range);

glGenLists()函数分配 range 个相邻的未被占用的显示列表索引。这个函数返回的是一

个正整数索引值,它是一组连续空索引的第一个值,即返回的是分配的若干连续编号中

小的一个。返回的索引都标识为空且已被占用,以后再调用这个函数时不再返回这些索引。

若申请索引的指定数目不能满足或 range 为 0,则函数返回 0。

例如,glGenLists(3),如果返回 20,则表示分配了 20、21、22 这 3 个连续的编号。如

果函数返回 0,则表示分配失败。

可以使用 glIsList()函数判断一个编号是否已经被用做显示列表,定义如下:

GLboolean glIsList(GLuint list)

该函数询问显示列表是否已被占用,若索引 list 已被占用,则函数返回 GL_TURE;反

之则返回 GL_ FALSE。

注意:

第 5 章 | 地形三维可视化系统的地形渲染实现

225

glGenLists()函数不在 glBegin()和 glEnd()函数之间调用。在还没有建立 OpenGL 的

Context 的时候,也不能调用 glGenLists()函数。否则可能会产生错误 GL_INVALID_ OPERATION,返回值 0。

(2)创建显示列表。

创建显示列表实际上就是把各种 OpenGL 函数的调用装入到显示列表中。使用

glNewList 开始装入,使用 glEndList 结束装入。glNewList()函数定义如下:

void glNewList(GLuint list,GLenum mode);

参数如下所示。

list:是一个正整数,表示要装入到哪个显示列表。

mode:创建的显示列表模式,有以下两种取值。

GL_COMPILE 表示以下的内容只是装入到显示列表,但现在不执行它们。

GL_COMPILE_AND_EXECUTE 表示在装入显示列表的同时,把装入的内容

执行一遍。

例如,需要把“设置颜色为红色,并且指定一个坐标为(0,0)的顶点“这两条命令装入

到编号为 list 的显示列表中,并且在装入的时候不执行,则可以用下面的代码:

glNewList(list,GL_COMPILE); glColor3f(1.0f,0.0f,0.0f);

glVertex2f(0.0f,0.0f);

glEnd();

显示列表只能装入 OpenGL 函数,而不能装入其他内容。

例如:

注意:

实战 OpenGL 三维可视化系统开发与源码精解

226

int i=3;

glNewList(list,GL_COMPILE); if(i > 20)

glColor3f(1.0f,0.0f,0.0f);

glVertex2f(0.0f,0.0f); glEnd();

其中 if 这个判断就没有被装入到显示列表中。以后即使修改 i 的值,使 i>20 的条件成

立,则 glColor3f 这个函数也不会被执行,因为它根本就不存在于显示列表中。

一次只能创建一个显示列表。换句话说,必须使用 glNewList()和 glEndList()函数

结束一个显示列表的定义之后才能定义下一个显示列表。在没有开始定义显示列表的

情况下,直接调用 glEndList()函数将会产生 GL_INVALID_OPERATION 错误。

并非所有的 OpenGL 函数都可以装入到显示列表中。表 5-10 列出了无法在显示列

表中存储的 OpenGL 函数。

表 5-10 无法在显示列表中存储的 OpenGL 函数

glAreTexturesResident glAttachShader glBindAttribLocation glBindBuffer glBufferData glBufferSubData glClientActiveTextur glColorPointer glCompileShader glCreateProgram glCreateShader glDeleteBuffers glDeleteLists glDeleteProgram glDeleteQueries glDeleteTextures glDetachShader glDisableClientState

glDisableVertexAttribArray glEdgeFlagPointer glEnableClientState glEnableVertexAttribArray glFeedbackBuffer glFinish glFlush glFogCoordPointer glGenBuffers glGenLists glGenQueries glGenTextures glGet* glIndexMask glInterleavedArrays glIsEnable glIsList glIsProgram

glIsQuery glIsShader glIsTexture glMapBuffer glNormalPointer glPixelStore glPixelStore glPopClientAttrib glPushClientAttrib glReadPixels glRenderMode glSelectBuffer glShaderSource glTexCoordPointer glUnmapBuffer glValidateProgram glVertexAttribPointer glVertexPointer

(3)调用显示列表。

注意:

说明:

第 5 章 | 地形三维可视化系统的地形渲染实现

227

使用 glCallList 函数可以调用一个显示列表。定义如下:

void glCallList(GLuint list);

该函数有一个参数,表示要调用的显示列表的编号。例如,要调用编号为 10 的显示列

表,直接使用 glCallLists(10)就可以了。

使用 glCallLists 函数还可以调用一系列的显示列表。定义如下:

void glCallLists(GLsizei n,GLenum type,const GLvoid *lists);

该函数有以下 3 个参数。

n:表示要调用多少个显示列表。

type:表示这些显示列表的编号的存储格式,可以取以下几个值。

GL_BYTE。

GL_UNSIGNED_BYTE。

GL_SHORT。

GL_UNSIGNED_SHORT。

GL_UNSIGNED_INT。

GL_FLOAT。

*lists : 表 示 这 些 显 示 列 表 的 编 号 所 在 的 位 置 。 在 使 用 该 函 数 前 , 需 要 用

glListBase 函数设置一个偏移量。假设偏移量为 k,且 glCallLists 中要求调用的

显示列表编号依次为 L1、L2、L3…,则实际调用的显示列表为 L1+k、L2+k、

实战 OpenGL 三维可视化系统开发与源码精解

228

L3+k…。

例如下面的代码:

GLuint lists[]={1,3,4,8};

glListBase(10);

glCallLists(4,GL_UNSIGNED_INT,lists);

则实际上调用的是编号为 11、13、14、18 的 4 个显示列表。

“调用显示列表”这个动作本身也可以被装在另一个显示列表中。

(4)销毁显示列表。

销毁显示列表可以回收资源,使用 glDeleteLists 来销毁一串编号连续的显示列表。定

义如下:

void glDeleteLists(GLuint list,GLsizei range)

该函数删除一组连续的显示列表,即从参数 list 所指示的显示列表开始,删除 range

个显示列表,并且删除后的这些索引重新有效。

例如,使用 glDeleteLists(20,4),将销毁 20、21、22、23 这 4 个显示列表。

5.3.10 真三维立体的实现

现在我们已经实现了三维地形的渲染,但此时的三维地形还不是真正意义的真三维立

体,在绘制三维场景时也只是将场景单独绘制到后缓存中(glDrawBuffer(GL_BACK))。

但 OpenGL 立体显示技术能够帮助我们实现真正的立体三维,在这一节中将详细讲解基于

OpenGL 真三维立体的实现的原理、算法和程序设计。

说明:

第 5 章 | 地形三维可视化系统的地形渲染实现

229

1.立体显示图像对生成算法

人双眼的平均瞳距约为 65mm,基本保持平行。当两眼从稍有不同的两个角度去观察客

观三维世界的景物时,由于几何光学的投影,离观察者不同距离的像点在左右两眼视网膜上

就不在相同的位置上。这种两眼视网膜上位置的差就称为双眼视差,它反映了客观景物的深

度。人眼的深度感即立体感就是因为有了这个视差,再经大脑加工而形成的。立体视觉的形

成就是以双眼视差为重要依据的。 立体视觉的形成主要有两类因素:一类为客观因素,一

类为生理因素。由前者引起的立体视觉,称为单眼立体视觉;由后者引起的立体视觉,称为

双眼立体视觉。因此立体图对算法主要分为两类:单投影中心成像法和双投影中心成像法。

单投影中心成像法由于是从一幅图像中生成立体图像对,由于原始信息有限,所以立

体图像对也不可能提供更多的信息,因此立体感和视觉精度都受到影响,只适合用于一些

对三维信息要求不很严格的场合,如立体画,或不要求实时交互的立体场景等。

在双投影中心成像法中,左眼和右眼图像分别有一个透视投影中心,将三维场景分别

沿这两个投影中心投影到各自的像平面。需要设置两个虚拟摄像机,一个摄像机用来获取

左眼的图像,一个摄像机用来获取右眼的图像,并采用立体眼镜等辅助设备,分别将左右

眼图像传送给相应的眼睛,经过大脑的融合产生物体的立体深度信息,因此,我们这里采

用双投影中心成像法,图 5-30 为其垂直视图。

实战 OpenGL 三维可视化系统开发与源码精解

230

图 5-30 双中心投影成像法模型

点 P(xp, yp, zp)和 Rcop 的投影线的参数方程如公式 5-10 所示。

( / 2 ) (0 ) (0 )p p p p p px x t e x y y t y z z t z= + − = + − = + − (公式 5-10)

在三维空间中设立两个视点(投影中心),一个为左视点 Lcop,一个为右视点 Rcop,

两个视点位于 X 轴上,且两视点的距离是 e,两个视点连线的中点为坐标原点(0,0,0),则

左视点坐标为(-e/2,0,0),右视点坐标为(+e/2,0,0)。投影平面平行于 XY 平面,其左、右

视点的距离是 d。三维空间中的一点 P(xp, yp, zp)在左视点投影中的坐标为(xl, yl, zl),在

右视点投影中的坐标为(xr, yr, zr),可知 l rz z d= = 。

在投影平面 z d= 上,可得出公式 5-11:

( ) /p pt z d z= − (公式 5-11)

将 t 值代入公式 5-10,就能得到点 P 在投影平面中的坐标(xr, yr),结果如公式 5-12

所示:

( / 2)2

p pr r

p p

x e d y dex yz z

−= + = (公式 5-12)

同理,点 P(xp, yp, zp)和 Lcop 的投影线的参数方程如公式 5-13 所示:

( / 2)2

p pl l

p p

x e d y dex yz z

+= − = (公式 5-13)

第 5 章 | 地形三维可视化系统的地形渲染实现

231

可得水平视差,如公式 5-14 所示:

( )r lp

edH x x ez

= − = − + (公式 5-14)

可见,当 pz d> 时,0 H e< < ,此时视差为正视差;当 pz d< 时, 0H < ,此时视差为负视差;当 pz d= 时, 0H = ,此时视差为零视差。公式 5-14 很好地避免了发散视差的出现。这样采用双中心投影算法设置双视点,可以获取三维空间中物体的左、右眼图像,从而生成立体图像对。它没有垂直视差,立体窗口是共面的不会发生扭曲,而且 大正水平视差是有界的,得到的立体效果 为理想,适合于虚拟操作等这种需要精确空间感的立体显示系统。

2.立体显示的双目数学模型

立体显示基于人眼的双目视差原理,即通过模拟人眼生成体视图对,再分别传送至左、

右眼,因此这种技术的显示效果取决于双目系统数学模型构造的准确性。模拟人的双眼、

生成符合深度感要求的体视对是体视显示的先决条件,关键在于建立准确的数学模型。按

照投影方式不同,可分为汇聚双目投影模型和平行双目投影模型,如图 5-31 和图 5-32 所

示,它们的视轴分别为交于一点和相互平行两种情况。

图 5-31 汇聚双目投影模型 图 5-32 平行双目投影模型

汇聚式投影的模拟摄像机位置如图 5-33 所示,视轴交汇于焦点处,一定意义上这种方

式更贴近于人眼的视觉习惯,满足了双眼辐合的仿真。但视轴不平行产生的模型投影形变

会引入垂直视差,垂直视差将导致观察者的不适度加剧(如双眼疲劳、眩晕感),并引起几

何畸变削弱立体效果。垂直视差从投影面中心逐渐增大,当视场孔径变大时,垂直视差带

实战 OpenGL 三维可视化系统开发与源码精解

232

来的负面效应尤为严重。

相反地,平行双目系统避免了汇聚式投影的缺陷,完全没有引入垂直视差,使得左、

右眼图像更易融合。在透视视景体(图 5-33)的定义上二者也有不同,汇聚投影的棱锥

台是对称式的,平行投影是非对称的。对称与否是以视锥台的左右、上下边是否相等来

衡量的。

图 5-33 视锥台(Frustum)

3.立体透视投影原理的数学描述

将左、右图像分别放置入左、右眼缓冲区,经透视

投影,将产生一个无论在几何外观上还是感觉上都非常

好的立体效果。一个高质量的立体图像,包括两个立体

部分,这两部分都是透视投影的,且它们的投影中心(摄

像机)在位置上平行对称,如图 5-34 所示。

假设摄像机位于 z 轴的正半轴上的点(0,0,d0)处,d0 表示摄像机距 xy 平面的距离,

点(x,y,z)投影到 xy 平面的位置用以下公式计算。

图 5-34 立体图像的基本几何模型

第 5 章 | 地形三维可视化系统的地形渲染实现

233

对左眼,用公式 5-15 计算:

0 0

0 0

( / 2)2

c cx t d t y dd z d z

⎡ ⎤+ × ×−⎢ ⎥− −⎣ ⎦

(公式 5-15)

对右眼,用公式 5-16 计算:

0 0

0 0

( / 2)2

c cx t d t y dd z d z

⎡ ⎤− × ×+⎢ ⎥− −⎣ ⎦

(公式 5-16)

设有一点(8,-5,-3),摄像机距 xy 平面的距离 d0=9(0,0,9),也就是摄像机 z 坐标的

绝对值,经左摄像机投影到点(5.875,-3.75),经右摄像机投影到点(6.125,-3.75),z 值都

为-3,该点被移到了正视差区,看上去好像出现在显示器的表面后部。

利用公式 5-15 和公式 5-16 计算出来一对立体投影时,所有可能的三维点将投影到正

视差区,左、右摄像机的透视投影能很好地平衡视差效果,使合成结果更添立体感。

4.OpenGL 立体显示技术

OpenGL 立体显示技术多用于三维图形的显示,它提供两个附加的颜色缓存区来生成

左右屏幕图像,通过正确选择每只眼睛的观察位置就可以生成真实的三维图像。眼睛的观

察位置对应于 OpenGL 中的投影矩阵,我们可以先选取一个初始的投影位置(一般选取在

两眼间连线的中点位置),然后再通过分别进行正向和负向的平移而产生左、右眼位置的投

影矩阵。

立体显示的步骤如下。

(1)首先确认显示卡支持立体显示模式,然后在初始化每个显示窗口时加载立体缓存

实战 OpenGL 三维可视化系统开发与源码精解

234

支持(两个前台缓存和两个后台缓存)。

(2)接下来分别设置左、右眼的投影矩阵,并在后台缓存中绘制场景。

(3) 后,将后台与前台缓存互换,这样后台缓存上的图像就被显示出来了,而原来

的前台缓存变为后台缓存用于新的绘图,整个绘图过程就是这样的循环往复过程。

程序设计

在系统中实现代码如下。

(1)设置相机参数。

void CT3DSystemView::SetCamra(int mStyle)

{

switch(mStyle) {

case 0://非双目立体模式

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);

break; case 1://双目立体模式,左相机

gluLookAt(m_vPosition.x-m_camraDistence/2.0,m_vPosition.y,

m_vPosition.z, m_vView.x,m_vView.y,m_vView.z,

m_vUpVector.x,m_vUpVector.y,m_vUpVector.z);

break; case 2://双目立体模式,右相机

gluLookAt(m_vPosition.x+m_camraDistence/2.0,m_vPosition.y,

m_vPosition.z, m_vView.x,m_vView.y,m_vView.z,

m_vUpVector.x,m_vUpVector.y,m_vUpVector.z);

} …… }

SetCamra()函数通过传入不同的参数值来设置相应的场景相机。

函数说明:

第 5 章 | 地形三维可视化系统的地形渲染实现

235

参数 m_camraDistence 值就是图 5-34 中所示的 ct 值,通过调整 m_camraDistence就可以达到不同的立体显示效果。如果 m_camraDistence=0,则表示左、右眼图像重

合,不再有立体几何效果;如果 m_camraDistence 过大,左、右眼图像重合较少,则

立体效果不好,立体感弱,图像效果不好。

(2)将 CT3DsystemView 类中的 DrawScene()函数修改为以下代码。

void CT3DSystemView::DrawScene() //场景绘制

{ InitTerr();

SetDrawMode(); //设置绘图模式

if(this->m_stereo==TRUE) {

SetCamra(1); //设置左眼相机

glDrawBuffer(GL_BACK_LEFT); //将左眼图像写入左后缓存 glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);

DrawSky(); //绘制背景天空

DrawTerrain(); //三维地形绘制 glDrawBuffer(GL_BACK_RIGHT); //将右眼图像写入右后缓存

glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);

SetCamra(2); //设置右眼相机 glCallList(m_TerrainList); //调用地形显示列表

}

else {

SetCamra(0); //设置单眼相机

glDrawBuffer(GL_BACK); //将图像直接写入后缓存 glClearColor(1.0f,1.0f,1.0f,0.0f);

glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);

DrawSky(); //绘制背景天空 DrawTerrain(); //三维地形绘制

}

}

观察者需要有辅助地观看设备,如佩戴立体眼镜(场顺序眼镜,如液晶光阀

眼镜—双目分时观看左、右图像,即某一时刻只有一只眼睛观察图像。戴上这

种眼镜的观察者观看时间一长,就会产生头晕的感觉,因此并不适合长时间观看;

偏振光眼镜—双目分别观看不同偏振的左、右图像,不会产生液晶光阀眼镜那

种头晕的感觉,可以长时间观看)才能观察到立体显示效果。当立体显示模式运

作起来后,显示卡驱动就向立体眼镜发出指令,在左、右眼镜片上加载同步信号,

也就是当显示器上显示左眼图像时,右眼镜片生成屏蔽信号,右眼也就看不到显

说明:

注意:

实战 OpenGL 三维可视化系统开发与源码精解

236

示器上的图像;在显示右眼图像时,过程类似。这样,无论何时,观察者的左、

右眼都只能看到左、右眼对应的视频图像,带视差的左、右眼图像经大脑加工合

成就产生了立体视觉。

图 5-35 给出了几种立体眼镜实例图。

图 5-35 立体眼镜

目前还有一种不需要辅助观看设备(如场顺序或偏振眼镜)的自动立体显示,其

观看区域或观看体积的大小有所不同,但可由多人观看。飞利浦公司推出了名为

3D-WOW-Display 的自动立体显示器,用户能够从多个视角观赏到三维效果,而不用

戴上特制眼镜。苹果公司也正研究三维立体显示器,不需要使用者在头部佩戴任何特

制眼镜之类的装备,而是应用当前计算机的计算能力回放或实时渲染显示三维影像。

并希望可以跟踪观众位置,即无论观众位置如何,这套系统都能确保正确地进行三维

图像渲染。还可以为多个用户同时提供不同三维体验,既可以投射相同的图像,也可

以投射不同的图像。

5.4 本章涉及到的 OpenGL 函数与知识点

本章涉及到的 OpenGL 函数主要如表 5-11 所示。

表 5-11 本章涉及到的 OpenGL 函数

OpenGL 核心函数 功 能

glBegin

glEnd 定义一个或一组原始的顶点

glBindTexture 允许建立一个绑定到目标纹理的有名称的纹理

glClear 用当前值清除缓冲区

glClearColor 为色彩缓冲区指定用于清除的值

glClearDepth 为深度缓冲区指定用于清除的值

glColor3f 设置当前色彩

说明:

第 5 章 | 地形三维可视化系统的地形渲染实现

237

(续表)

OpenGL 核心函数 功 能

glDepthFunc 定义用于深度缓冲区对照的数据

glDrawBuffer 定义选择哪个色彩缓冲区被绘制

glEnable

glDisable 打开或关闭 OpenGL 的特殊功能

glFinish 等待直到 OpenGL 执行结束

glFlush 在有限的时间里强制 OpenGL 的执行

glGenTextures 生成纹理名称

glGetDoublev

glGetIntegerv

glGetBooleanv

glGetFloatv

返回值或所选参数值

glLineWidth 设定光栅线段的宽

glLoadIdentity 用单位矩阵替换当前矩阵

glMatrixMode 定义哪一个矩阵是当前矩阵

glNewList

glEndList 创建或替换一个显示列表

glGenLists 生成一组空的连续的显示列表

glCallList 执行一个显示列表

glNormal 设定当前顶点法向

glOrtho 用垂直矩阵与当前矩阵相乘

glPixelStorei 设定像素存储模式

glPolygonMode 选择一个多边形的光栅模式

glPushMatrix

glPopMatrix 矩阵堆栈的压入和弹出操作

glShadeModel 选择平面明暗模式或光滑明暗模式

glTexEnvf

glTexEnvi

glTexEnvfv

glTexEnviv

设定纹理坐标环境参数

glTexParameteri 设置纹理参数

glVertex 定义一个顶点

glViewport 设置视窗

(续表)

实战 OpenGL 三维可视化系统开发与源码精解

238

OpenGL 应用库函数 功 能

gluBuild2DMipmaps 建立二维多重映射

gluLookAt 设定一个变换视点

gluPerspective 设置一个透视投影矩阵

gluProject 将对象坐标映射为窗口坐标

gluUnProject 将窗口坐标映射为对象坐标

OpenGL 扩展库函数 功 能

glMultiTexCoord2fARB 设置多重纹理单元的纹理坐标

glActiveTextureARB 指定当前操作的是哪一个纹理单元

在上述函数中,应重点掌握以下几个。

设定视点变换函数 gluLookAt 函数。

OpenGL 扩 展 函 数 glActiveTextureARB 和 指 定 多 重 纹 理 单 元 、 纹 理 坐 标 的

glMultiTexCoord2fARB()函数。

创建和调用显示列表 glNewList、glEndList、glGenLists 和 glCallList 函数。

设定纹理坐标环境参数 glTexEnvf 和设置纹理参数 glTexParameter 函数。

5.5 本章小结

本章对地形三维可视化进行了基本概述,介绍了目前地形三维可视化的主要算法。主

要介绍了海量地形与影像纹理数据的常用获取方法,给出了海量地形自分块与影像纹理分

块原则和程序实现,以及地形与影像子块调度的程序实现,并在此基础上实现了地形的三

维可视化。介绍了真三维立体的实现算法和数学模型,在此基础上给出了基于 OpenGL 的

真三维立体的程序实现。

第 5 章 | 地形三维可视化系统的地形渲染实现

239