有些时候仅仅是静态显示三维场景是不够的,还需要能够显示交互式地实时动态观察。三维漫游就是对三维场景进行实时的浏览,即在三维场景中进行人机交互。它能够交互式地从各个不同的角度形象直观地展示三维场景,可以沿设定的路线从空中或地面动态地、多方位地展示地段的功能,使观察者具有逼真的、亲临其境的感觉,具有很大的实用价值。OpenGL所具有的平移、旋转、缩放等功能为实现三维场景的实时动态漫游提供了保证。确定飞行路线即飞行路径则是进行三维漫游的前提。
飞行路径简介
飞行路径,又称飞行路线,即在三维漫游过程中所遵循的路线。是由用户指定的一系列地采样点按照地形的起伏依次连接构成的空间曲线,为一系列三维坐标点的集合,如公式10-1所示。
                                                                     (公式10-1)
首先需要在三维地形上选取路径的采样点,然后才能在采样点之间经过插值产生反映地形起伏的飞行路径。
飞行路径设置方法
1.概念理解
确定飞行路径设置方法是设置飞行路径的前提。在选取或编辑飞行路径时,目前的三维地形显示系统多数是在二维平面图上预先选择漫游路径上的一些采样点,然后根据采样点的平面位置从数字地形图中插值确定漫游路径上其他点的平面坐标和高程值。
以下提供两种设置飞行路径的方法。
1)自定义方法
根据三维环境中三维地面坐标的获取方法,在正射投影模式或透视投影模式下直接通过鼠标在三维地形上选取一系列地面点,连接这些三维地面点并经过插值后即构成飞行漫游路径。
2)线路方案路径法
当设计好一条线路方案后,建立线路方案的三维模型,线路方案的中心线即构成三维飞行路径,沿此飞行路径可直接观察设计好的线路方案三维效果和线路周围的地形地貌,以及地理环境。
2.界面设计
首先在工程主菜单中添加一个菜单项,名称为“三维漫游(B)”,类型为“Pop_up”(弹出式)。在其下添加一个子菜单,名称为“飞行路径”,类型为“Pop_up”(弹出式),然后在“飞行路径”下添加一个子菜单,名称为“手动设置路径”,ID = ID_PATH_MANUINPUT,最后的菜单如图10-1所示。
添加菜单
 
图10-1  添加菜单
3.程序设计
1)添加变量
为CT3DSystemView.h添加私有变量。
private:
    BOOL m_ShowFlyPath;//标识是否显示飞行路径
protected:
//存储进行飞行路径选择时所选择的一系列点坐标
CArray<PCordinate,PCordinate>m_FlayPath;
//存储临时进行飞行路径所选择的一系列坐标
CArray<PCordinate,PCordinate>m_FlayPathTempPts;
修改T3DSystemView.h定义的枚举型变量:
enum{QUERY_COORDINATE,QUERY_DISTENCE,SELECTLINE};
 
为:
enum{QUERY_COORDINATE,QUERY_DISTENCE,SELECTLINE,SELECTFLYPATH};
2)函数实现
(1)为菜单ID = ID_PATH_MANUINPUT添加响应函数。
void CT3DSystemView::OnPathManuinput()//设置飞行路径
{
    m_ShowFlyPath=TRUE; //标识是否显示飞行路径
    m_QueryType=SELECTFLYPATH;//进行飞行路径选择
    m_FlayPath.RemoveAll();//存储进行飞行路径坐标数组清空
}
函数说明:
 
 
 

在OnPathManuinput()函数里,进行了信息初始化。标识开始进行飞行路径选择点状态,同时将存储进行飞行路径坐标的动态数组m_FlayPath清空。
(2)修改OnLButtonDown ()函数,加入对飞行路径设置的支持功能。
void CT3DSystemView::OnLButtonDown(UINT nFlags,CPoint point)
{
    if(m_ViewType==GIS_VIEW_ORTHO)  //如果是正射投影模式  
    {
        ……
    }
    if(m_QueryType==QUERY_COORDINATE)//空间三维坐标查询
    {
        ……
    }
    else if(m_QueryType==QUERY_DISTENCE)//空间距离查询
    {
        ……
    }  
      else if(m_QueryType==SELECTLINE)
    {
        ……
    }
    else if(m_QueryType==SELECTFLYPATH)//进行飞行路径选择
    {
             m_bmouseView=false;
             m_oldMousePos=point;
             ScreenToGL(point);
    }
    else
    {
             m_bmouseView=true;
             m_oldMousePos=point;
    }
    CView::OnLButtonDown(nFlags,point);
}

 函数说明:
 
 
 
通过OnLButtonDown()函数的修改,加入对飞行路径的支持。
(3)修改ScreenToGL()函数,实现飞行路径设置的功能。
void CT3DSystemView::ScreenToGL(CPoint point)
{
    ……
    if(winZ>=0&& winZ<1.0)
    {
        if(m_QueryType==QUERY_COORDINATE)//查询三维坐标
        {
            ……
        }  
        else if(m_QueryType==QUERY_DISTENCE)//查询空间距离
        {
            ……
        }
        else if(m_QueryType==SELECTLINE)//如果是三维选线设计
        {
            …..
        }
        else if(m_QueryType==SELECTFLYPATH)//如果是设置飞行路径
        {
            PCordinate ppt=new Cordinate;  
            if(m_ViewType==GIS_VIEW_ORTHO)  //如果是正射投影模式  
            {
                double mx=wx+m_ortho_CordinateOriginX;//计算x坐标
                double my=wy+m_ortho_CordinateOriginY;//计算y坐标
                mx=theApp.m_DemLeftDown_x+mx*(theApp.m_DemRightUp_x-
theApp.m_DemLeftDown_x);//转换为大地坐标
                my=theApp.m_DemLeftDown_y+my*(theApp.m_DemRightUp_y-
theApp.m_DemLeftDown_y)/m_ortho_CordinateXYScale;//转换为大地坐标
                wz=m_demInsert.GetHeightValue(mx,my,2);//从DEM中内插出对应的高程
                mx-=theApp.m_DemLeftDown_x;//x坐标变换为绘图坐标
                my-=theApp.m_DemLeftDown_y;//y坐标变换为绘图坐标
                ppt->x=mx;ppt->y=wz;ppt->z=-my; //记录飞行路径的三维坐标
            }
            else if(m_ViewType==GIS_VIEW_PERSPECTIVE)   //如果是透视投影模式
            {
                ppt->x=wx;ppt->y=wy;ppt->z=wz;//记录飞行路径的三维坐标
            }
            m_FlayPath.Add(ppt); //将飞行路径的三维坐标存储到数组m_FlayPath
            OnDraw(GetDC()); //刷新三维场景,用来显示绘制的飞行路径
        }
    }
    else
    {
        MessageBox("鼠标选择点不够精确,请精确选择点!");
        m_bSearchDistencePtNums=0;
    }
}
函数说明:
 
 
 
通过ScreenToGL()函数的修改,实现飞行路径设置功能。将飞行路径上的三维坐标点记录到飞行路径动态数组m_FlayPath中。      
上述函数完成了飞行路径选择的功能,但为了更加清晰地看到所选择的飞行路径位置,需要在选择过程中实时绘制飞行路径,因此添加DrawFlyPath ()函数来实现此功能。
(4)绘制飞行路径。
void CT3DSystemView::DrawFlyPath()
{
    //如果显示飞行路径并且飞行路径坐标点数>1,才绘制飞行路径
    if(m_ShowFlyPath==TRUE && m_FlayPath.GetSize()>1)//进行飞行路径选择
    {
        glPushMatrix(); //压入矩阵堆栈
        glDisable(GL_TEXTURE_2D); //关闭纹理(即飞行路径不采用纹理)
        glLineWidth(2.0);//设置飞行路径线宽
        glColor3f(1,0,1);//设置飞行路径颜色
☆程序第Ⅰ部分☆《绘制正射投影模式下的飞行路径》
if(m_ViewType==GIS_VIEW_ORTHO)  //绘制正射投影模式下的飞行路径
{
    glBegin(GL_LINE_STRIP); //以折线方式绘制飞行路径
    for(int i=0;i<m_FlayPath.GetSize();i ++)
    glVertex2f(GetOrthoX(m_FlayPath.GetAt(i)->x),
        GetOrthoY(-m_FlayPath.GetAt(i)->z));
    glEnd();
    //在飞行路径每个坐标点处绘制点圆加以标识每个坐标点
    for(i=0;i<m_FlayPath.GetSize();i++)
    {
        glColor3f(0,0.5,0.5); //点的颜色
        glPointSize(4.0); //点的大小
        glBegin(GL_POINTS);
            glVertex2f(GetOrthoX(m_FlayPath.GetAt(i)->x),
                GetOrthoY(-m_FlayPath.GetAt(i)->z));
        glEnd();
    }
    glPointSize(0); //恢复点的默认大小
}
☆程序第Ⅱ部分☆《绘制透视投影模式下的飞行路径》
    else if(m_ViewType==GIS_VIEW_PERSPECTIVE)//绘制透视投影模式下的飞行路径
    {
        glBegin(GL_LINE_STRIP);
        for(int i=0;i<m_FlayPath.GetSize();i++)
            glVertex3f(m_FlayPath.GetAt(i)->x, m_FlayPath.GetAt(i)->y,
            m_FlayPath.GetAt(i)->z);
        glEnd();
    }
    glEnable(GL_TEXTURE_2D);  //开启纹理
    glLineWidth(1.0); //恢复线宽
    glColor3f(1,1,1);  //恢复颜色
    glPopMatrix(); //弹出矩阵堆栈
    }
}
函数说明:
 
 
 
DrawFlyPath()函数实现了对飞行路径的实时绘制,包括以下两个部分。
l 第1部分是绘制正射投影模式下的飞行路径。
l 第2部分是绘制透视投影模式下的飞行路径。
修改DrawScene()函数,在DrawTerrainDelaunay()后面加上DrawFlyPath(); 语句。
(5)修改CT3DSystemView的DrawScene()函数。
void CT3DSystemView::DrawScene()//场景绘制
{
……
    if ( this -> m_stereo == TRUE )
    {  
        ……
    }
    else
    {
        ……
        DrawTerrainDelaunay();
        DrawFlyPath();  //绘制飞行路径
    }
}
函数说明:
 
 
 
为DrawScene()函数添加绘制飞行路径的函数调用。
如图10-2所示为在正射投影模式下和透视模式下飞行路径设置的实现。
透视投影模式   正射投影模式
(a) 透视投影模式                                    (b) 正射投影模式
图10-2  飞行路径设置效果图
 飞行路径插值算法
1.概念理解
无论在正射投影模式下还是在透视投影模式下,当飞行路径坐标点越密时,飞行路径越精确。但是有时候我们所选择的飞行路径相临坐标点距离比较远,而地形是起伏变化的,为了保证飞行路径能够比较好地贴在地面上,这就需要在两坐标点之间插入新的坐标点,对飞行路径加密,从而保证飞行路径与其所在的地形起伏完全吻合。而在加密飞行路径之前,则需要确定飞行路径插值算法。
如图10-3所示为一个飞行路径与地形起伏不吻合的示意图。

图10-3  飞行路径与地形起伏不吻合示意图
2.界面设计
在菜单“三维漫游(B)”→“飞行路径”下添加子菜单,名称为“飞行路径插值”,ID = ID_FLPPATH_INTERPOLATION,最后的菜单如图10-4所示。
添加菜单
图10-4  添加菜单
3.程序设计
为菜单ID = ID_FLPPATH_INTERPOLATION添加响应函数。
void CT3DSystemView::OnFlppathInterpolation()//路径坐标插值
{
    float m_InsertDdis=5; //插值间距
    double x1,y1,z1,x2,y2,z2;
    PCordinate ppt;  
    m_FlayPathTempPts.RemoveAll();//临时存储飞行路径
    for(int i=0;i<m_FlayPath.GetSize()-1;i++)
    {
☆程序第Ⅰ部分☆《获取飞行路径当前点和下一点的三维坐标,为插值做数据准备》
x1=m_FlayPath.GetAt(i)->x;  //获取飞行路径当前点的x坐标
y1=m_FlayPath.GetAt(i)->y;  //获取飞行路径当前点的y坐标
z1=m_FlayPath.GetAt(i)->z;  //获取飞行路径当前点的z坐标
x2=m_FlayPath.GetAt(i+1)->x;  //获取飞行路径下一点的x坐标
y2=m_FlayPath.GetAt(i+1)->y; //获取飞行路径下一点的y坐标
z2=m_FlayPath.GetAt(i+1)->z; //获取飞行路径下一点的z坐标
☆程序第Ⅱ部分☆《如果是飞行路径的起始点,则直接作为插值点》
if(i==0)//如果是飞行路径的起始点,则记录
{
    ppt=new Cordinate;
    ppt->x=x1;
    ppt->y=y1;
    ppt->z=z1;
    m_FlayPathTempPts.Add(ppt);
}
☆程序第Ⅲ部分☆《根据飞行路径相临两点的距离和内插间距,线性内
插出应增加的飞行路径内插点的三维坐标》
//计算飞行路径当前点与下一点的距离
double L=myDesingScheme.GetDistenceXYZ(x1,y1,z1,x2,y2,z2);
int M=L/m_InsertDdis; //计算应内插的坐标点数
for(int j=1;j<=M;j++)
{
    //线性内插计算出新的内插点的三维坐标
    ppt=new Cordinate;
    ppt->x=x1+j*m_InsertDdis/L*(x2-x1);
    ppt->z=z1+j*m_InsertDdis/L*(z2-z1);
    ppt->y=m_demInsert.GetHeightValue(ppt->x+theApp.m_DemLeftDown_x,
        -ppt->z+theApp.m_DemLeftDown_y,2);
    m_FlayPathTempPts.Add(ppt);  //记录内插点坐标
}
☆程序第Ⅳ部分☆《将飞行路径下一点也作为内插点,但不遗漏原飞行路径已有坐标点》
    ppt=new Cordinate;
    ppt->x=x2;
    ppt->y=y2;
    ppt->z=z2;
    m_FlayPathTempPts.Add(ppt); //将飞行路径下一点的坐标也记录在内
}
☆程序第Ⅴ部分☆《将所有内插点写入动态数组,作为新的飞行路径》
    m_FlayPath.RemoveAll();  //飞行路径数组清空
    for(i=0;i<m_FlayPathTempPts.GetSize();i++)
    {
        ppt=new Cordinate ;
        ppt->x=m_FlayPathTempPts.GetAt(i)->x;
        ppt->y=m_FlayPathTempPts.GetAt(i)->y;
        ppt->z=m_FlayPathTempPts.GetAt(i)->z;
        m_FlayPath.Add(ppt); //重新填充飞行路径数组
    }
    OnDraw(GetDC()); //刷新三维场景
    MessageBox("路径坐标插值完成!","路径坐标插值",MB_ICONINFORMATION);
}
函数说明:
 
 
 
在OnFlppathInterpolation ()函数中实现对飞行路径的插值计算,包括以下5个部分。
l 第1部分是获取飞行路径当前点和下一点的三维坐标,为插值做数据准备。
l 第2部分是如果是飞行路径的起始点,则直接作为插值点。
l 第3部分是根据飞行路径相临两点的距离和内插间距,线性内插出应增加的飞行路径内插点的三维坐标。
l 第4部分是将飞行路径下一点也作为内插点,但不遗漏原飞行路径已有坐标点。
l 第5部分是将所有内插点写入动态数组,作为新的飞行路径。
如图10-5和图10-6所示为插值前后飞行路径的对比图。
插值前的飞行路径  插值后的飞行路径
图10-5  插值前的飞行路径(正射)            图10-6  插值后的飞行路径(透视)
 
飞行路径的保存
1.概念理解
在加密飞行路径后,需要保存飞行路径,这里采用将插值后的飞行路径坐标点写入到文件中进行保存。
2.界面设计
在菜单“三维漫游(B)”→“飞行路径”下添加一个子菜单,名称为“保存飞行路径”,ID = ID_FLYPATH_SAVE,最后的菜单如图10-7所示。
 
添加菜单
 
图10-7  添加菜单
3.程序设计
(1)为菜单ID = ID_FLYPATH_SAVE添加响应函数。
void CT3DSystemView::OnFlypathSave()//保存飞行路径
{
    Cstring  NeededFile;
    char  FileFilter[]="飞行路径(*.pth)|*.pth||";
    //设置对话框属性
    DWORD   FileDialogFlag=OFN_EXPLORER|OFN_PATHMUSTEXIST |
OFN_HIDEREADONLY|OFN_OVERWRITEPROMPT;
    CFileDialog FileDialogBoxFile(FALSE,NULL,0,FileDialogFlag,FileFilter,this);
    FileDialogBoxFile.m_ofn.lpstrTitle="保存飞行路径文件";
    char    FileName[200];
   
    CString tt[3] ;
    if(FileDialogBoxFile.DoModal()==IDOK)//如果对话框成功打开
    {  
        NeededFile=FileDialogBoxFile.GetPathName(); //得到文件名
        sprintf(FileName,"%s",NeededFile);
        if(strcmp(FileDialogBoxFile.GetFileExt(),"pth")!=0)
            strcat(FileName,".pth"); //添加飞行路径文件扩展名
        if(FlyPathSave(FileName))//如果飞行路径保存成功
            MessageBox("路径点保存完毕","保存飞行路径",MB_ICONWARNING);
    }
}

 函数说明:
 
 
 
添加OnFlyPathSave()函数,并调用FlyPathSave()函数实现飞行路径数据点的保存。
(2)将飞行路径坐标点写入文件中。
int CT3DSystemView::FlyPathSave(char*pathfile)
{
    FILE    *fpw;
    char    message[200];
    if((fpw=fopen(pathfile,"w"))==NULL)//如果写入文件失败
    {
        sprintf(message,"文件%s创建无效",pathfile);
        MessageBox(message,"保存飞行路径坐标到文件",MB_ICONWARNING);
        return 0; //返回失败
    }
    fprintf(fpw,"%d\n",m_FlayPath.GetSize());//写入飞行路径坐标点总数
    for(int i=0;i<m_FlayPath.GetSize();i++)
    {
        //向文件fpw写入飞行路径坐标点的三维坐标
         fprintf(fpw,"%lf,%lf,%lf\n",m_FlayPath.GetAt(i)->x,m_FlayPath.GetAt(i)->y,
                m_FlayPath.GetAt(i)->z);
    }
    fclose(fpw); //关闭文件
    return1; //返回成功
}
函数说明:
 
 
FlyPathSave()函数实现保存飞行路径数据点到外部文件。
如图10-8所示为程序运行结果。
飞行路径保存运行界面 p_w_picpath357.jpg
图10-8  飞行路径保存运行界面
如图10-9所示为用记事本打开保存的飞行路径文件的结果。
打开飞行路径文件
图10-9  打开飞行路径文件
 
打开飞行路径
1.概念理解
打开飞行路径即把已经保存的飞行路径文件打开,从该文件中读取飞行路径坐标用于三维漫游。
2.界面设计
在菜单“三维漫游(B)”→“飞行路径”下添加一个子菜单,名称为“打开飞行路径”,ID = ID_FLY_OPENPATH,最后的菜单如图10-10所示。
添加菜单
图10-10  添加菜单
3.程序设计
1)添加变量
private:
    BOOL m_PathFlag; //是否成功打开飞行路径
2)函数实现
(1)修改Init_data()函数,在最后添加m_PathFlag初始化代码。
void CT3DSystemView::Init_data()//初始化相关变量
{  
    ……
    m_PathFlag=FALSE;//是否成功打开飞行路径
}
(2)为菜单ID = ID_FLY_OPENPATH添加响应函数。
void CT3DSystemView::OnFlyOpenpath()//打开飞行路径
{
    CString     NeededFile;
    char        FileFilter[]="飞行路径(*.pth)|*.pth||";
    //设置文件对话框属性
    DWORD   FileDialogFlag=OFN_EXPLORER|OFN_PATHMUSTEXIST|
            OFN_HIDEREADONLY|OFN_OVERWRITEPROMPT;
    CFileDialog FileDialogBoxFile(TRUE,NULL,0,FileDialogFlag,FileFilter,this);
    FileDialogBoxFile.m_ofn.lpstrTitle="打开飞行路径文件";
    char        FileName[200] ;
   
    CString tt[3];
    if(FileDialogBoxFile.DoModal()==IDOK)//如果对话框成功打开
    {  
        NeededFile=FileDialogBoxFile.GetPathName();//得到文件名
        sprintf(FileName,"%s",NeededFile);
        if(strcmp(FileDialogBoxFile.GetFileExt(),"pth")!=0)
            strcat(FileName,".pth");//添加飞行路径文件扩展名
       
        if(FlyPathRead(FileName))//读取飞行路径文件数据动态数组中
            MessageBox("打开路径点完毕","提示",MB_ICONWARNING ) ;
        if(m_FlayPath.GetSize()>1)//如果飞行路径数据坐标点数>1
        {
            m_ShowFlyPath=TRUE; //显示飞行路径
            m_PathFlag=TRUE; //标识飞行路径打开成功
        }
        else
            m_PathFlag=FALSE;//标识飞行路径打开失败
    }          
}
函数说明:
 
 
 

OnFlyOpenpath()函数实现打开飞行路径,并将飞行路径数据读取到动态数组中。
(3)读取飞行路径文件于数据动态数组中。
int CT3DSystemView::FlyPathRead(char*pathfile)
{
    CString tt,m_strszLine;
    PCordinate ppt=new Cordinate;  
    m_FlayPath.RemoveAll (); //飞行路径数组清空
    CStdioFile m_inFile;   
    if(m_inFile.Open(pathfile,CFile::modeRead)==FALSE) //打开文件
    {
        return 0; //返回失败标志
    }
    m_inFile.ReadString(m_strszLine); //读取飞行路径坐标点总数
    while(m_inFile.ReadString(m_strszLine))
    {
        ppt=newCordinate;
        m_strszLine.TrimLeft("");
        m_strszLine.TrimRight("");
        int nPos=m_strszLine.Find(",");
        tt=m_strszLine.Left(nPos);
        ppt->x=atof(tt);
        m_strszLine=m_strszLine.Right(m_strszLine.GetLength()-nPos-1);
        nPos=m_strszLine.Find(",");
        tt=m_strszLine.Left(nPos);
        ppt->y=atof(tt); 
        m_strszLine=m_strszLine.Right( m_strszLine.GetLength()-nPos-1);
        ppt->z=atof(m_strszLine);
        m_FlayPath.Add(ppt);  //记录飞行路径坐标点
    }
    m_inFile.Close(); //关闭文件
    return1; //返回成功标志
}
函数说明:
 
 
FlyPathRead()函数实现读取飞行路径于文件数据动态数组中。
如图10-11所示为程序运行结果。
飞行路径打开运行界面 p_w_picpath362.jpg
图10-11  飞行路径打开运行界面
如图10-12所示为打开飞行路径在三维场景中的绘制结果。
飞行路径打开后显示结果
图10-12  飞行路径打开后显示结果
显示/关闭飞行路径
1.概念理解
显示/关闭飞行路径即控制飞行路径在三维场景中是否显示。
2.界面设计
在菜单“三维漫游(B)”→“飞行路径”下添加一个子菜单,名称为“显示飞行路径”,ID = ID_FLY_ONOFFPATH,最后的菜单如图10-13所示。
p_w_picpath364.jpg
图10-13  添加菜单
3.程序设计
(1)显示或关闭飞行路径。
void CT3DSystemView::OnFlyOnoffpath()
{
    if(m_ShowFlyPath==TRUE) //如果当前是显示飞行路径
        m_ShowFlyPath=FALSE;  //标识设置为False
    else
        m_ShowFlyPath=TRUE;  //反之,设置为True  
    OnDraw(GetDC());   //刷新三维场景
}
(2)根据m_ShowFlyPath值修改菜单文本。
void CT3DSystemView::OnUpdateFlyOnoffpath(CCmdUI*pCmdUI )
{
    if(m_ShowFlyPath==TRUE)//如果当前是显示飞行路径
        pCmdUI->SetText("关闭飞行路径"); //将菜单名称设置为“关闭飞行路径”
    else//如果当前是关闭飞行路径
        pCmdUI->SetText("显示飞行路径");//将菜单名称设置为“显示飞行路径”
}

本文出自 《实战OpenGL三维可视化系统开发与源码精解》一书