Cesium原理篇:3最长的一帧之地形(2:高度图)

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

       此时,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的部分取消,后面找个合适的机会在介绍。

时间: 2024-11-29 08:26:48

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

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

上一篇,我们介绍了当我们添加一个Entity时,通过Graphics封装其对应参数,通过EntityCollection.Add方法,将EntityCollection的Entity传递到DataSourceDisplay.Visualizer中.本篇则从Visualizer开始,介绍数据的处理,并最终实现渲染的过程. CesiumWidget.prototype.render = function() { if (this._canRender) { this._scene.initializ

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

有了之前高度图的基础,再介绍STK的地形相对轻松一些.STK的地形是TIN三角网的,基于特征值,坦白说,相比STK而言,高度图属于淘汰技术,但高度图对数据的要求相对简单,而且支持实时构建网格,STK具有诸多好处,但确实有一个不足,计算量比较大,所以必须预先生成.当然,Cesium也提供了一个Online的免费服务,不过因为是国外服务器,所以性能和不稳定因素都不小.好的东西自然得来不易,所以不同的层次,根据具体的情况选择不同的方案,技术并不是唯一决定因素,甚至不是主要因素. CesiumTerra

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

       地形部分的原理介绍的差不多了,但之前还有一个刻意忽略的地方,就是地形的重采样.通俗的讲,如果当前Tile没有地形数据的话,则会从他父类的地形数据中取它所对应的四分之一的地形数据.打个比方,当我们快速缩放影像的时候,下一级的影像还没来得及更新,所以会暂时把当前Level的影像数据放大显示, 一旦对应的影像数据下载到当前客户端后再更新成精细的数据.Cesium中对地形也采用了这样的思路.下面我们具体介绍其中的详细内容.        上图是一个大概流程,在创建Tile的时候(prepa

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

原计划开始着手地形系列,但发现如果想要从逻辑上彻底了解地形相关的细节,那还是需要了解Cesium的数据调度过程,这样才能更好的理解,因此,打算先整体介绍一下Cesium的渲染过程,然后在过渡到其中的两个主要模块:地形数据和影像数据. 简述 设想一下,印度洋的暖流,穿过喜马拉雅山,形成了滴一滴水,落在了青藏高原的唐古拉山,顺势而下,涌入太平洋,长江之水自此经久不息.而Cesium的一切的一切,也是从一个并不起眼的函数开始的: 通过requestAnimationFrame函数,每一帧的结束,就是下

Cesium原理篇:6 Renderer模块(2: Texture)

Texture也是WebGL中重要的概念,使用起来也很简单.但有句话叫大道至简,如果真的想要用好纹理,里面的水其实也是很深的.下面我们来一探究竟. 下面是WebGL中创建一个纹理的最简过程: var canvas = document.getElementById("canvas"); var gl = canvas.getContext("webgl"); // 创建纹理句柄 var texture = gl.createTexture(); // 填充纹理内容

Cesium原理篇:6 Render模块(3: Shader)

在介绍Renderer的第一篇,我就提到WebGL1.0对应的是OpenGL ES2.0,也就是可编程渲染管线.之所以单独强调这一点,算是为本篇埋下一个伏笔.通过前两篇,我们介绍了VBO和Texture两个比较核心的WebGL概念.假设生产一辆汽车,VBO就相当于这个车的骨架,纹理相当这个车漆,但有了骨架和车漆还不够,还需要一台机器人来加工,最终才能成产出这辆汽车.而Shader模块就是负责这个生产的过程,加工参数(VBO,Texture),执行渲染任务. 这里假设大家对Shader有一个基本的

Cesium原理篇:6 Render模块(4: FBO)

Cesium不仅仅提供了FBO,也就是Framebuffer类,而且整个渲染过程都是在FBO中进行的.FBO,中文就是帧缓冲区,通常都属于高级用法,但其实,如果你了解了它的基本原理后,用起来还是很简单的,关键在于理解.比如你盖楼,地基没打好,盖第一层楼,还可以,盖第二层楼,有点挫了,盖第三层楼,塌了.你会认为第三层楼(FBO)太难了,其实根本原因还是出在地基上. 窗口系统所管理的帧缓存有自己的缓存对象(颜色,深度和模板),它们诞生于窗口创建前,而我们自己创建的帧缓冲,这些缓存对象则需要自己来手动

Cesium原理篇:3D Tiles(1)渲染调度【转】

Cesium在2016年3月份左右推出3D Tiles数据规范,在glTF基础上提供了LOD能力,定位就是Web环境下海量三维模型数据.虽然目前3D Tiles还是Beta阶段,有不少硬伤,但3D Tiles数据规范于2016年9月30日开始了OGC标准化进程,积极成分还是很大. 之前的glTF时分享了个人对二进制格式的一些想法和谨慎的态度.3D Tiles简单说就是具备LOD能力的glTF.有了数据首先是提供API可以渲染,保证用起来,下一步就要了解该数据规范的具体特点,比如倾斜,矢量,点云,

cesium原理篇(二)--网格划分【转】

转自:http://www.cnblogs.com/fuckgiser/p/5772077.html 上一篇我们从宏观上介绍了Cesium的渲染过程,本章延续上一章的内容,详细介绍一下Cesium网格划分的一些细节,包括如下几个方面: 流程 Tile四叉树的构建 LOD 流程 首先,通过上篇的类关系描述,我们可以看到,整个调度主要是update和endFrame两个函数中,前者分工,后者干活. 另外,QuadtreePrimitive类只要来维护整个地球的四叉树,而每一个Tile对应一个Quad