此处,我们先抛出两个重要的问题:
1、当用户双击数字地球放大、鼠标滚轮滚动缩放、鼠标左键按下漫游时,程序是如何根据当前视高、视场角和视点中心加载分层分块的地形和纹理数据的?
2、当用户漫游时,QuadTileSet类的Update(DrawArgs drawArgs)方法中会调用的是qt = new QuadTile(south, north, west, east, 0, this)语句构建窗口需要的但未在缓存(Hashtable缓存和文件缓存中)中的新的瓦片数据。此处每一次构建新瓦片对象的初始等级Level=0,那么当需要更精细的高级别纹理的时候,层级是在哪里增加了呢?
具体剖析如下:
1、 主窗体的构造方法MainApplication()的最后位置调用了worldWindow.Render()方法,后者首先启动一个工作线程(绑定的入口方法为WorkerThreadFunc),WorkerThreadFunc()方法内部内部包含一个while(true){}的一直在执行的循环,它调用m_World.Update(this.drawArgs)方法用于更新高程和影像数据,通过远程下载或者检索本地缓存获得数据,然后完成初始化和更新工作。worldWindow.Render()方法发起工作线程之后,调用this.drawArgs.WorldCamera.Update(m_Device3d)方法来更新相机参数,调用m_World.Render(this.drawArgs)方法来渲染当前的行星,当然了worldWindow.Render()方法内部同时完成了其它的相关处理。总结一句话:即漫游过程中,数据的下载、更新Update(drawArgs)和初始化Initialize(drawArgs)都是在工作线程WorkerThreadFunc()中完成的,且所有的需要渲染的对象在Update()之前都会调用Initialize()方法来完成数据的初始化工作;瓦片数据的真正的渲染工作是由主线程调用worldWindow.Render()方法直接进行绘制。记住:所有的可渲染对象都需要经过先初始化,再更新,最后渲染的流程。
2、 当用户双击数字地球放大、鼠标滚轮滚动缩放、鼠标左键按下漫游时,都会调用worldWindow的重载方法OnPaint(PaintEventArgs e),内部会调用worldWindow.Render()方法,改方法经过层层调用最终会调用QuadTileSet类的Update(DrawArgs drawArgs)方法,其内部又会调用QuadTileSet类的Initialize(DrawArgs drawArgs) 和Update (DrawArgs drawArgs)方法;后两个方法内部又会调用QuadTile类的Initialize()和Update (DrawArgs drawArgs)方法。QuadTileSet类的Update(DrawArgs drawArgs)方法按照以下步骤工作:
(1)进行如下判断:先判断LZTS<180是否成立,再判断相机是否对准地球观看,或者说地球是否在相机的视场角范围之内,如果否,则不进行数据下载和更新,直接退出;
(2)再判断当瓦片大小的3.5倍是否能覆盖当前窗口视角的一半,如果否,则也不进行数据下载和更新,直接退出;
(3)移除当前视景体内不可见的瓦片数据;
(4)如果上述判断都通过,则计算当前相机观察的目标点(即窗口中心)所在的瓦片的行、列号(默认从第0层级开始计算和加载);
(5)计算当前相机观察的目标点(即窗口中心)所在的瓦片的四周的经纬度范围;
(6)计算当前相机观察的目标点(即窗口中心)所在的瓦片的中心经纬度坐标;
(7)更新以当前相机观察的目标点(即窗口中心)为中心周围9*9范围的不在缓存中但是确实当前窗口需要的瓦片影像数据;
(8)在更新某一瓦片之前,先要判断它是否已经存在于哈希表中了。如果是,则说明该瓦片已经被请求过,不再需要重新下载,直接取出并更新即可;如果否,则用相关参数(默认层级Level=0)构造新瓦片对象,然后判断该瓦片是否与当前窗口视景体相交,如果相交,则将其加入哈希表中,并调用QuadTile类的Update (DrawArgs drawArgs)方法进行更新,如果不相交,则什么也不用做。
(9)在QuadTile类的Update (DrawArgs drawArgs)方法内部,会先判断当前瓦片对象是否已经被初始化,如果否,则再判断当瓦片大小的3.5倍是否能覆盖当前窗口视角的一半,且当前瓦片中心到窗口中心的角度是否小于瓦片大小的3.5*1.25倍,且当前瓦片的外包围盒是否与当前场景的视景体相交,如果上面三个条件都满足则,调用QuadTile类的Initialize()方法进行瓦片的初始化工作。所谓的瓦片的初始化工作是指:判断本地缓存中是否已经存在所需要的瓦片数据,如果是则加载本地缓存数据,如果否则针对每一个瓦片数据发起网络下载请求,从远程地理数据服务器下载所需要的数据。
(10)如果当前瓦片对象已经初始化,则调用CreateTileMesh()方法创建瓦片格网,其内部又会根据条件调用CreateElevatedMesh()或CreateFlatMesh()来创建高程格网或者平面格网。
(11)如果当前瓦片对象已经初始化,则再判断当瓦片大小的3.5倍是否能覆盖当前窗口视角,且当前瓦片中心到窗口中心的角度是否小于瓦片大小的3.5倍,且当前瓦片的外包围盒是否与当前场景的视景体相交,如果上面三个条件都满足则,调用QuadTile类的ComputeChildren(DrawArgs drawArgs)计算当前瓦片对象的孩子节点,它会用瓦片层级和范围等相关参数构建下一级四个孩子节点对象。
(12)构建完毕孩子节点对象后,则判断每个孩子节点对象是否为空,如果否,则用用孩子节点对象调用Update(drawArgs)方法来更新孩子节点的数据。因为孩子节点对象也属于QuadTile类型,所以调用又回归到步骤(9)。
3、 World对象中的可渲染对象初始状态如下图所示。
4、 当用户通过漫游操作需要更精细的模型时,所有获取远程下载或检索本地缓存中的高程数据和影像数据的工作都是由工作线程WorkerThreadFunc()方法完成的。其内部调用了m_World.Update(this.drawArgs)方法,后者先后调用如下方法:
this.Initialize(drawArgs)、 this.RenderableObjects.Update(drawArgs)、 this.m_WorldSurfaceRenderer.Update(drawArgs)、 this.m_projectedVectorRenderer.Update(drawArgs)、 drawArgs.WorldCamera.TerrainElevation=(short)this.TerrainAccessor.GetElevationAt(//参数略)、 m_outerSphere.Update(drawArgs)等方法。 this.RenderableObjects.Update(drawArgs)内部先后调用this.Initialize(drawArgs)和ro.Update(drawArgs) 方法,此处的Initialize(drawArgs)和Update(drawArgs)两个方法会多次嵌套调用。最终调用ImageLayer.Update(drawArgs)。 其中,this.TerrainAccessor.GetElevationAt(//参数略)方法根据多态性实际上调用的是NLTTerrainAccessor重载的GetElevationAt(//参数略)方法,其内部有调用下列方法: TerrainTile tt = m_terrainTileService.GetTerrainTile(latitude, longitude, targetSamplesPerDegree); ttce.TerrainTile.GetElevationAt(latitude, longitude);//内含一个Elevation[,]二维数组
当用户点击WorldWind中的地球时,首先响应的是WorldWindow.OnPaint()函数,后续程序的调用流程如下图所示。
零散知识点:
1、 地形瓦片类TerrainTile引用了地形瓦片服务类TerrainTileService,在TerrainTile的Initialize()函数中实例化并发起了地形下载请求类TerrainDownloadRequest对象。而在TerrainTileService类中也引用了TerrainTile类,通过GetTerrainTile()函数返回一个TerrainTile类对象。
2、 地形访问器类TerrainAccessor或其子类NltTerrainAccessor的构造函数中也引用了TerrainTileService。其中使用Hashtable对象m_tileCache存储了当前需要加载或下载的地形瓦片缓冲实体类TerrainTileCacheEntry对象,用于建立和维护一个访问下载请求队列。ConfigurationLoader类的私有成员方法private static TerrainAccessor[] getTerrainAccessorsFromXPathNodeIterator(XPathNodeIterator iter, string cacheDirectory)中采用了如下图所示代码先构造TerrainTileService类对象tts,然后再将tts作为参数之一构造TerrainAccessor类对象newTerrainAccessor。
3、 TileSizeDegree:每个瓦片覆盖的度数大小;
SamplesPerTile: 每个瓦片的高程采样点数;
4、 星球表面渲染类WorldSurfaceRenderer和表面瓦片类SurfaceTile之间相互引用,且SurfaceTile类中定义了存储高程数据的二维数组float[,] m_HeightData = null,如果该数组为空则当层级大于2,则将TerrainTileService类二维数组public float[,] ElevationData赋值给m_HeightData,如果当前层级小于或等于2或ElevationData为空,则为m_HeightData重新分配空间。SurfaceTile.Initialize(DrawArgs drawArgs)函数调用了TerrainAccessor类的public virtual TerrainTile GetElevationArray(double north, double south, double west, double east, int samples)和SurfaceTile.buildTerrainMesh()。
5、 四叉树瓦片类QuadTile中引用了地形瓦片类TerrainTile,返回tile,而tile包含高程数组ElevationData。
6、 配置加载器类ConfigurationLoader的Load()函数从XML文件Earth.xml中解析出地形访问器类对象,并且该对象作为参数构造World对象newWorld。
7、 m_World. Update(DrawArgs drawArgs)函数内部当相机高度小于30000米时,才调用this. TerrainAccessor. GetElevationArray(B,L,S)计算出用户鼠标点击处的地形高程,具体内容为:(1)判断地形瓦片服务是否为空;(2) 判断更高分辨率数据集是否为空;(3)通过调用TerrainTileService. GetTerrainFile () 函数获取鼠标点击区域的TerrainTile 对象,然后将其加入到访问下载请求Hashtable对象m_tileCache 中;(4)判断所请求的瓦片缓存实体对象(是对TerrainTile类的进一步封装)是否已经初始化,如否则完成地形瓦片的初始化工作。所谓初始化实质上是解析扩展名为.bil的二进制文件中的数据,放入二维数组ElevationData[x,y]。其中,x=y= 150,代表150X150个格网点的高程数据;(5)调用ttce.TerrainTile. GetElevationAt(B,L)获取用户点击处经纬度的高程值,采用双线性内插方法插值得到并返回。在WorldWind V1.4.0.1版本中,上述功能代码被移入WorldWindow.cs文件的WorldWindow.Render()函数中。