原计划开始着手地形系列,但发现如果想要从逻辑上彻底了解地形相关的细节,那还是需要了解Cesium的数据调度过程,这样才能更好的理解,因此,打算先整体介绍一下Cesium的渲染过程,然后在过渡到其中的两个主要模块:地形数据和影像数据。
简述
设想一下,印度洋的暖流,穿过喜马拉雅山,形成了滴一滴水,落在了青藏高原的唐古拉山,顺势而下,涌入太平洋,长江之水自此经久不息。而Cesium的一切的一切,也是从一个并不起眼的函数开始的:
通过requestAnimationFrame函数,每一帧的结束,就是下一帧的开始,从Cesium启动之时,直至其消亡,不曾停歇。
现在,我们就开始万里长征第一步,最终,该函数会调用Scene的Render方法:
虽然render(scene, time)的方法很长,但对于地球的主要要素(地形&影像),都是在Globe的beginFrame和endFrame之间完成的:
其中红色高亮的两个函数用来维护两个重要的逻辑,updateAndExecuteCommands负责数据的调度,比如哪些Tile需要创建,这些Tile相关的地形数据,以及涉及到的影像数据之间的调度,都是在该函数中维护。但是updateAndExecuteCommands函数只负责管理,也就是他所创建的Tile类只是一个个的Tasks,自己并不负责Tasks内容的实现(数据的加载和维护)。这样,导致架构会有些复杂,因为不是一个流水线的作业方式,所以就需要有状态的维护。但这也是必然的,不仅保证该函数高效,不拖累每一帧的时间消耗,同时因为实时性满足人的要求(帧数能够达到或接近60),每一帧需要的Tile队列可以实时计算,不用考虑时间和状态之间的关系。
可以这样理解,updateAndExecuteCommands是一个管理者,只说不做,与之相反,scene.globe.endFrame中,会对该帧所涉及的GlobeTile的下载,解析等进行处理。
如上图,简单而言,updateAndExecuteCommands负责上面看得见的事情,根据当前这一帧相机的状态,先打一个黄色的网格,然后检查当前状态下,这些网格里面的地形和影像数据是否都已经准备好了,如果准备好,每一个Tile就创建一个DrawCommand来渲染,把这个球画出来。如果没准备好也不管,反正有endFrame函数负责,各司其职,每个人做好本职工作,不是很好吗。
scene.globe.endFrame就按照吩咐来准备每一个Tile的数据,全是看不见的工作,默默的奉献自己的青春。这里,Promise,Worker等技术都会使用,通过异步和多线程的方式,来缓解每一帧的负担,而在主线程下,主要是数据状态的维护。如果一个Tile的数据解析完成,则该GlobeTile的状态更新为renderable(可渲染),done(已完成)。
类关系
在这个过程中,类的关系大致如下:
网格按照经纬度来划分的,每一个网格对应一个QuadtreeTile四叉树类,其中有一个GlobeSurfaceTile,里面保存地形数据TileTerrain和影像数据TileImagery。当前相机下需要多个网格,就需要多个QuadtreeTile,该队列保存在QuadtreePrimitive,QuadtreePrimitive统一维护所有Tile的管理,具体是通过GlobeSurfaceTileProvider来实现逻辑操作,而QuadtreePrimitive上面包了一个Globe和Scene。
渲染过程
updateAndExecuteCommands的关系过程如下:
在QuadtreePrimitive的update中,调用selectTilesForRendering来或许当前需要创建的网格,其中,在第一帧会把全球分为两块Tile:
而在queueChildrenLoadAndDetermineIfChildrenAreAllRenderable函数中,基于这两个Tile来做四叉树剖分,也就是所有的后来的QuadtreeTile都能够上溯到这两个Tile之一:
创建的GlobeSurfaceTile都会存储到更新队列中,等待数据下载和解析的过程,而如果当前帧中,存在准备好的,已经可以渲染的GlobeSurfaceTile,则调用addDrawCommandsForTile,构造成VBO,最终通过WebGL实现渲染。
更新过程
更新队列的逻辑如上,稍显复杂,还是着重看框选的部分:
最初,在processTileLoadQueue中,遍历更新队列中所有的GlobeSurfaceTile,如果发现里面没有数据,状态是QuadtreeTileLoadState. START,于是乎,大声喊道:“姐妹们,开始接客了”。
青楼就在GlobeSurfaceTile.processStateMachine。GlobeSurfaceTile就是这里的顾客,尽管人潮涌动,但来此处的人,不外乎三种:第一次来的,您就到prepareNewTile函数来吧,先帮你疏通疏通筋骨:new TileTerrain(),新建地形数据类,在帮你润润肌肤:_createTileImagerySkeletons,创建影像数据类;原来是老司机了,那就老规矩,来个三温暖吧:processTerrainStateMachine来负责地形数据加载,createWaterMaskTextureIfNeeded负责水面(如果该Tile是海洋区域的话),还有TileImagery.prototype.processStateMachine负责影像数据的加载;服务满意吗,来结账买单了:renderable = true/state = QuadtreeTileLoadState.DONE。
通过上面的描述,一个地球的渲染过程主要四个环节:
l 网格划分
l Terrain数据
l Imagery数据
l 渲染
而这四个环节互相交错,虽然看上去有些混乱,函数众多,但不知道你是否注意到,无论是渲染过程还是更新过程,都是针对当前状态的响应和状态的更新。
从流程上讲了一下大致的过程,不知道大家是否还有疑惑,或者文中有不对的地方,也希望多提意见。当然这个颗粒度还有点大,后面,在和大家分享一下每一个环节中具体的技术和优化策略。