这一篇,接着上一篇,内容集中在高度图方式构建地球网格的细节方面。

       此时,Globe对每一个切片(GlobeSurfaceTile)创建对应的TileTerrain类,用来维护地形切片的相关逻辑;接着,在requestTileGeometry中,TileTerrain会请求对应该切片的地形数据。如果读者对这部分有疑问的话,可以阅读《Cesium原理篇:1最长的一帧之渲染调度》;最后,如果你是采用的高度图的地形服务,地形数据对应的是HeightmapTerrainData类,最终,该TerrainData形成了一个TerrainMesh网格。下面,我们就详细的介绍一下最后一步的相关内容。

高度图

       首先,怎么理解高度图?通常一个Tile都会对应一个256*256的影像切片,代表该Tile对应的XYZ范围下对应的影像内容,高度图也是一样的思路,只是此时,前者每一个像素代表当前位置对应的颜色,而后者代表当前位置对应的高度

       一般情况下,高度图是一个缩略图,比如在Cesium中,在没有真实地形数据下,高度图的宽高是16*16大小,每个点对应的值都是0,在有真实地形数据下,高度图是65*65的大小。可见,高度图是有抽稀的,而不是一一对应,一来没必要,不然就是点云了,二来构网的计算量很大,也是效果和效率的一个折中。

Workers线程

       Cesium的调度是基于状态的变化,看似简单,但个人认为非常精髓的。相比基于事件驱动的策略,基于状态可以更好的实时的处理大数据,逻辑上也简单清晰,当然这是题外话,我们继续回到地形本身。

       有了数据,TileTerrain的状态由RECEIVING变为RECEIVED,自然也就进入了下一环节transform:将原始的地形数据(HeightmapTerrainData)转换为格网(TerrainMesh)的过程。

这个过程涉及到不小的计算量,因此,Cesium采用Promise + Workers技术,把计算量放到线程中,这样保证界面操作的流畅。对Workers感兴趣的可以参考《Cesium原理篇:4Web Workers剖析》。

       HeightmapTerrainData.prototype.createMesh方法提供了构建格网的方法,内部正是采用Workers线程的方式,下面我们进入主题,详细介绍高度图构网的细节。

HeightmapTessellator

       如上是一个流程示意图,横线以上的是主线程,调用createMesh,创建线程,把buffer(高度值数组),宽高(width&height),tile的范围(rectangle)和中心点(center)等作为createVerticesFromHeightmap函数的参数,这样,每一个Tile都会创建一个Worker线程,并在线程中实现网格的构建。

Paremeters

       网格构建的算法则封装在HeightmapTessellator.computeVertices函数中,我们先详细了解里面的参数:

  • Heightmap
    用于构建格网的高度图点串
  • Width&height
    高度图的像素宽高
  • skirtHeight
    俗称裙边,每一个Tile四周会围成一个栅栏,指定该栅栏的高度,保证和相邻的Tile拼接时没有间隙
  • nativeRectangle
    该Tile的范围,如果是WGS坐标系,单位是度,如果是墨卡托,单位是米
  • exaggeration
    地形高度的缩放系数,通常为1,现实真实的地形高度
  • rectangle
    该Tile对应的地理范围,单位是弧度,rectangle和nativeRectangle至少要有一个,如果两个参数都有,则互相是匹配的
  • isGeographic
    true则为WGS坐标,false为墨卡托
  • relativetoCenter
    该Tile对应的中心点,单位是基于球心的笛卡尔坐标,单位为米
  • ellipsoid
    椭球体类,提供一些计算和换算方法
  • structure
    高度图数据结构,后续再说,感觉有点鸡肋

       以上就是必须传入的参数(也有一些不准确,如果没有设置这些参数,则会采用默认值),当然还有一些可选参数。然后就正式开始构网了。构网的过程主要分为四个部分:

  • 构建网格
  • 计算BoundingSphere
  • 计算HorizonCulling
  • Encode

网格节点

       构建网格的代码很长,但仔细读一下其实不难理解,结合下面一张图,先跟大家解释一下思路。

       这里先把这个网格想象成正方形的(就像把地球仪平铺成一幅地图),这就是一个Tile对应的格网。通过之前传入的参数,我们已知Tile的长宽(rectangle),行列的格子数(width&height),不难计算出每一个节点的位置(经纬度),说白了,就是两个for循环嘛,伪代码如下:

       当然这是一段伪代码,如果这样写确实也很短,但Cesium认为构建网格的计算量大,也很频繁,所以在此处进行了优化,是一个简化版的cartographicToCartesian函数,这一块说复杂也复杂,需要你对椭球体能有所了解,说简单也可以,因为即使不了解,你也可以直接套公式。大致的图示和公式如下:

       其中,B是纬度,L是经度,N是长半轴,这里是地球半径6378137米,而N(1-e^2)是椭圆的短半轴,这里取值6356752.3142451793米。另外,网格中节点数和高度图的宽高是一致的,这样每一个节点都对应高度图中的一个高度heightSample,这样,套用上面的公式,对应实现代码和注释如下:

 

      这样,对应格网中每一个节点,我们可以计算出positions和heights这两个数组,同理,再次把这个Tile网格想象成平面的,每一个Tile也对应一张影像切片,假设把这两张半透明的纸叠在一起,下面那张的是网格,上面那张是影像图,就是如下的这个效果:

 

      通常影像切片是256*256像素大小,我们把[0,256]的像素范围映射到[0,1]的比例中,这样,也能够计算出每一个节点对应[0,1]的比值,也就是通常说的uv(OpenGL里面的纹理坐标,渲染纹理时需要用到该参数)。Cesium中实现uv的代码如下:

 

      此外,在计算网格节点时,还计算了每个节点距离该Tile中心点relativetoCenter的距离,这个在下面计算boundingsphere时会需要。 这需要掌握图形学和矩阵方面的一些数学基础:

       如上,通过两个遍历,我们得到了和网格节点一一对应的三个数组:positions,heights,uvs以及距离中心点的最大值maximum和最小值minimum。

Cull裁剪

       如果只是单纯的网格构建,工作已经完成,但实际中还远远不够,最直接的一个问题是你不知道是否需要显示这个Tile。

       如果做过渲染优化的人有会有这样一个共鸣吧,渲染一个物体最快的方式就是不去渲染它。看上去很正确,但做起来其实是一个很严肃的问题。把这句话转译成程序员的语言就是,判断这个物体的范围是否在当前可视范围内。在结合我们正在讨论的Cesium地形,考虑到大规模,频繁的渲染环境下,在相机的视锥体下,如何快速,简单的判断当前地形格网是否可见,这是一个严肃的问题。而Cesium在这个问题上,做到了极致,让我深为叹服。

       首先,Cesium主要采用了两种裁剪方式:

  • Frustum Cull
  • Horizon Cull

       因为里面涉及到很多算法,坦白说,每一个单独的细节,展开讲都很有学问,所以下面主要是思路和个人的理解,我也尽量把条理说的清楚一些,让大家能够一个完整的认识。由于篇幅过长,本篇主要介绍锥体裁剪部分,水平面裁剪在TIN地形的时候在涉及。

Frustum Cull

       首先,当一个物体不在视锥体范围内,自然就不需要显示了。视锥体的大小是清楚的,所以,剩下的就是如何计算该物体的Bounds。同时,从世纪角度来看,这个判断的过程一定不能超过渲染该物体的时间,否则也是没有意义的。因此,构建这个Bounds的关键就在于快速和有效之间的平衡。即能够较快的构建出一个近似准确的Bounds,同时这个Bounds也能高效的较为准确的判断是否可见。

BoundingSphere

       Cesium最先提供的是BoundingSphere,如下图,就是一个模型的BoundingSphere,也就是一个物体的外接圆。

       现在,我们理解BoundingSphere的概念,那么我们有一堆点串,如何实现BoundingSphere.fromPoints这个函数呢?在阅读下面的内容前,希望大家也琢磨一下这个问题。

       BoundingSphere就是一个球,所以这个问题就是获取球的球心和半径。之前我遇到过类似的问题,有一堆点串,已知中心点的情况下,计算其半径。我的思路是遍历所有点,计算每个点和中心点距离,取最大值作为半径。

       现在,这个中心点是未知的,所以我们需要先遍历所有点,找到XYZ三个方向的Max和Min,即X(Min,Max),Y(Min,Max),Z(Min,Max),然后计算Min和Max的均值,作为中心点,即:P = (Min+Max) / 2。这样有了中心点,前面也给出了计算半径的思路,我们就实现了BoundingSphere.fromPoints。

这个算法不难理解,也是最简单最快速的方式,在Cesium中,称这个算法为Naïve Method,看到Naïve,不知道有几个人会会脱口而出“图样图森破”?但这个算法也有一个缺点,这个球通常都不是最优的,就像你穿了一件大一号的衣服,略微不太优雅。

       接着,Cesium对比了Jack Ritter算法。这个算法和Naïve算法相似,也是需要遍历两遍,第一次遍历后,估算出一个初始的球,然后再次遍历,如果点在这个球内则什么也不做,如果点在球外,则调整中心点和半径,确保该点在球内。调整算法如下:

       Naïve和Jack Ritter相比,第一次遍历过程基本一致,但第二次遍历时,Naïve只修改半径,而后者对中心点和半径都会调整。Jack Ritter自己测试,两者在计算量上相当,但后者要多5%的准确性。但Cesium自己测试发现,19%的情况下,效果会比前者差,而11%的情况下,效果会比前者好,说明第一次估算的球和添加点的顺序也会影响Jack Ritter算法的结果。

       如上是一个测试数据对比(参考),最后在Cesium里面会同时执行Naïve和Ritter算法,以半径最大的值作为最后的结果,这个思路是可取的,两次遍历的计算内容99%都一样,就像拼车一样,举手之劳,受益良多。有了BoundingSphere,如何判断是否在视锥体范围内呢,在《Cesium原理篇:2最长的一帧之网格划分》里面有详细解释,这里就不赘述。

OrientedBoundingBox

       就这样相安无事了不久,人们对性能的追求始终没有停止。之前的BoundingSphere发现还是不够精确,你也看到了,这个球里面有不少的空白区域,造成了过度的渲染,那有没有更精准的Bound呢,这个就是OrientedBoundingBox了。

       BoundingBox是指包围盒,再加上Oriented,顾名思义就是有朝向的包围盒了。上一个对比效果图,可以看到这个其实就是在本地坐标系下的一个包围盒,如下是一个OrientedBoundingBox和对比图,可见,确实范围要小很多。

       同样,我们要考虑两个问题,获取这个Bounds的成本,以及判断Bounds是否可见的成本。

       首先,对应一个地形Tile,总会有一个中心点,也就是参数relativetoCenter,该点对应球面的切面+法线,构成了这个local coordination(NEU:north east up)的XYZ轴,这样一个相对NEU坐标的正交geometry,相对于球心的笛卡尔坐标系就是一个斜geometry了,这就不像BoundingSphere那样具有更好的对称性,可以很直白的用参数化的方式来构造了。OrientedBoundingBox默认是一个2*2*2的正方体,center是该包围盒的中心点,而还有一个矩阵halfAxes用来记录包围盒按照中心点旋转和缩放信息。

       下面简单介绍一下如何获取一个地形Tile对应的OrientedBoundingBox,也就是Cesium.OrientedBoundingBox.fromRectangle函数。

       首先,我们知道该Tile对应的relativetoCenter,然后构造出EllipsoidTangentPlane对象,也就是该点对应的椭球切面,这个过程其实就是从笛卡尔坐标转为NEU坐标的过程,进而获取该点对应椭球的法线方向,点+法线 = 切面。如下图,红线是一个二维椭圆切线示意图,对应三维椭球就是一个切面:

       此时,我们只是获取了OrientedBoundingBox的XYZ的三个朝向,并在XYZ三个方向上无限延伸,但无法确定具体的范围。如何计算这个范围呢?

       先举个简单的例子,上面在一个黑暗的屋子里,你手拿这个足球正对着一面墙,在球心放一盏灯,。假设黑色的部分是透光的,而白色的不透光,这样墙上会有黑色切面的影子。我们把球不断的靠近这面墙,直到刚刚好贴在墙面上,这时每一个切面对应的影子是最小的。

       很显然,这个切面是一个弧面,而墙是一个平面,刚才的过程其实就是把XYZ的三维体投影到XY平面的过程。Cesium也是用同样的思路,通过EllipsoidTangentPlane.prototype.projectPointsToNearestOnPlane方法计算每一个Tile地形在XY上的范围(此时并不是计算所有点,而是类似九宫格,计算Tile对应左中右上中下这九个点来配准),而Z的范围可以简单的理解为当前地形数据对应的最高点和最低点,从而得到该Tile对应的minX, maxX, minY, maxY, minZ, maxZ(该值是以relativetoCenter为原点,米单位)。

       最后通过如上获取该包围盒准确的中心点以及相对偏移量和缩放比,结合NEU矩阵最终构造出halfAxes矩阵。

       这样,我们就找到了地形Tile对应的OrientedBoundingBox,坦白说,理解上要比BoundingSphere复杂很多,但在计算量上,因为只针对特征点来计算,其实性能要好,当然,个人没有测试,只是推测。

       如何判断包围盒和视锥体的位置关系,这个和BoundingSphere的算法非常相似。那这种方式效果是否有改进,还得看疗效,如下是Cesium自己提供的对比效果(参考):

       可见,改善效果还是很不错的,而判断是否相交的性能上差别不大:

       实际上,两种Bounds方式在Cesium中都在使用,而且,计算格网positions和两个bounds(BoundingSphere,OrientedBoundingBox)中,有一些重复计算的部分,所以还是有一定的优化空间。但这主要是在编程技巧上的对比,从逻辑和算法上,Cesium已经非常专业了,我个人觉得在研究源码时,在方方面面,都受益匪浅。

       如上,我们计算了positions,heights,uvs以及bounds后,我们基本完成了HeightmapTerrainData.CreateMesh的过程,也是地形中最关键的环节,下一步,就是开始加载到显卡中,通过shader渲染了,我们在后续会介绍。同时,由于篇幅的问题,临时决定把水平裁剪和Encode的部分取消,后面找个合适的机会在介绍。

Cesium原理篇:3最长的一帧之地形(2:高度图)的更多相关文章

  1. Cesium原理篇:5最长的一帧之影像

    如果把地球比做一个人,地形就相当于这个人的骨骼,而影像就相当于这个人的外表了.之前的几个系列,我们全面的介绍了Cesium的地形内容,详见: Cesium原理篇:1最长的一帧之渲染调度 Cesium原 ...

  2. Cesium原理篇:7最长的一帧之Entity(下)

    上一篇,我们介绍了当我们添加一个Entity时,通过Graphics封装其对应参数,通过EntityCollection.Add方法,将EntityCollection的Entity传递到DataSo ...

  3. Cesium原理篇:3最长的一帧之地形(3:STK)

    有了之前高度图的基础,再介绍STK的地形相对轻松一些.STK的地形是TIN三角网的,基于特征值,坦白说,相比STK而言,高度图属于淘汰技术,但高度图对数据的要求相对简单,而且支持实时构建网格,STK具 ...

  4. Cesium原理篇:7最长的一帧之Entity(上)

    之前的最长的一帧系列,我们主要集中在地形和影像服务方面.简单说,之前我们都集中在地球是怎么造出来的,从这一系列开始,我们的目光从GLOBE上解放出来,看看球面上的地物是如何渲染的.本篇也是先开一个头, ...

  5. Cesium原理篇:3最长的一帧之地形(1)

    前面我们从宏观上分析了Cesium的整体调度以及网格方面的内容,通过前两篇,读者应该可以比较清楚的明白一个Tile是怎么来的吧(如果还不明白全是我的错).接下来,在前两篇的基础上,我们着重讨论一下地形 ...

  6. Cesium原理篇:3最长的一帧之地形(4:重采样)

           地形部分的原理介绍的差不多了,但之前还有一个刻意忽略的地方,就是地形的重采样.通俗的讲,如果当前Tile没有地形数据的话,则会从他父类的地形数据中取它所对应的四分之一的地形数据.打个比方 ...

  7. cesium原理篇(三)--地形(1)【转】

    转自:http://www.cnblogs.com/fuckgiser/p/5824743.html 简述 前面我们从宏观上分析了Cesium的整体调度以及网格方面的内容,通过前两篇,读者应该可以比较 ...

  8. Cesium原理篇:1最长的一帧之渲染调度

    原计划开始着手地形系列,但发现如果想要从逻辑上彻底了解地形相关的细节,那还是需要了解Cesium的数据调度过程,这样才能更好的理解,因此,打算先整体介绍一下Cesium的渲染过程,然后在过渡到其中的两 ...

  9. Cesium原理篇:2最长的一帧之网格划分

    上一篇我们从宏观上介绍了Cesium的渲染过程,本章延续上一章的内容,详细介绍一下Cesium网格划分的一些细节,包括如下几个方面: 流程 Tile四叉树的构建 LOD 流程 首先,通过上篇的类关系描 ...

随机推荐

  1. python 之 Django 基础篇

    1,Django流程介绍 MTV模式 著名的MVC模式:所谓MVC就是把web应用分为模型(M),控制器(C),视图(V)三层:他们之间以一种插件似的,松耦合的方式连接在一起. 模型负责业务对象与数据 ...

  2. spark - tasks is bigger than spark.driver.maxResultSize

    Error ERROR TaskSetManager: Total size of serialized results of 8113 tasks (1131.0 MB) is bigger tha ...

  3. Daily Scrum02 12.17

    软件发布到了最后的阶段,大家都在抓紧时间DEBUG,美化界面,做各种测试…… 大家抓紧最后一把劲,一起努力冲最后一下,努力吧! Member 任务进度 下一步工作 吴文会 会议组织 会议总结,发表博客 ...

  4. ubuntu 下emacs 配置

    (set-language-environment 'Chinese-GB) (set-keyboard-coding-system 'utf-8) (set-clipboard-coding-sys ...

  5. 一鼓作气 博客--第五篇 note5

    一.迭代器 二.装饰器 三.生成器 1.生成列表的方式有几种 2.把列表每个数都加1 2.1 data =[1,2,3] for i in map(lambda x:x+1,data):print(i ...

  6. VS2012 2013 显示查找功能 无法具体定位 解决方法

    问题的现象:通过使用 Ctrl + Shift + F 也就是Find In Files功能,使用之后只能显示统计结果,不显示具体行.如下图     regedit 中在注册表中查找:HKEY_CLA ...

  7. 关于c++的 vector 容器的使用及创建方法

    1.vector向量容器的使用,vector具有自动管理的功能,可以进行元素的查找删除 创建方法: (1)  vector<int > v; 创建了一个v的容器,没指定容量: (2)  v ...

  8. 简单的ATM取款过程

    一个简单的ATM的取款过程是这样的:首先提示用户输入密码(pwd),最多只能输3次,超过三次则提示用户“密码已输入三次错误,请取卡.“结束交易.如果用户密码正确,在提示用户输入金额(money),AT ...

  9. c#之结构体

       struct是一种复合值类型,通常用来封装小型变量组,其中每个变量为结构的成员. C#中结构类型和类类型在语法上非常相似,他们都是一种数据结构,都可以包括数据成员和方法成员. 结构和类的区别: ...

  10. 清晰易懂TCP通信原理解析(附demo、简易TCP通信库源码、解决沾包问题等)C#版

    目录 说明 TCP与UDP通信的特点 TCP中的沾包现象 自定义应用层协议 TCPLibrary通信库介绍 Demo演示 未完成功能 源码下载 说明 我前面博客中有多篇文章讲到了.NET中的网络编程, ...