共计 19505 个字符,预计需要花费 49 分钟才能阅读完成。
API 回顾
在创立 Viewer
时能够间接指定 影像供应器 (ImageryProvider
),官网提供了一个非常简单的例子,即离屏例子(搜 offline):
new Cesium.Viewer("cesiumContainer", {
imageryProvider: new Cesium.TileMapServiceImageryProvider({url: Cesium.buildModuleUrl("Assets/Textures/NaturalEarthII"),
})
})
这个例子的影像供应器是 TMS
瓦片服务,也就是预制瓦片地图,资源位于 Source/Assets/Textures/NaturalEarthII
文件夹下。
若没有指定 地形供应器(TerrainProvider),Cesium 也有默认策略:
// Globe.js 构造函数内
const terrainProvider = new EllipsoidTerrainProvider({ellipsoid: ellipsoid,})
这就是说,应用 EllipsoidTerrainProvider
来作为默认地形,也就是把椭球面当作地形(因为没有)。
小提示:
TileMapServiceImageryProvider
其实是UrlTemplateImageryProvider
的一个子类,在文章最初一节申请瓦片时会再次提及。
本篇要解决的两大疑难:
- 椭球体是如何形成的
- 瓦片是如何从创立到申请,最终到渲染的
这篇比拟长,不计代码也有 8000 多字,而且波及的数据类比拟多,然而我感觉能实现上述两个过程的大抵解说,就能沿着思路细化钻研上来了。
1. 对象层级关系
其下层上层的次要类从属关系(从 Scene
开始算起)大抵是:
Scene
┖ Globe
┠ Ellipsoid
┖ QuatreePrimitive
┠ GlobeSurfaceTileProvider
┖ QuadtreeTile
┖ GlobeSurfaceTile
┠ TileImagery[]
┃ ┖ Imagery
┖ TerrainData
我简化了 ImageryLayer
、ImageryProvider
、ImageryLayerCollection
、TerrainProvider
与下面这些类的关系,也就没让他们呈现在图中。
1.1. Scene 中非凡的物体 – Globe
Scene
下辖的次要三维容器是 PrimitiveCollection
,能持续往里套 PrimitiveCollection
,也能够独自放类 Primitive
。
然而 Scene
中有一个三维物体是独立于 Scene
构造函数之外才创立的对象,也就是地球对象 Globe
(精确的说是地表),这意味着 Scene
能够脱离 Globe
,独自作一个一般三维场景容器应用也没有问题。事实上 Scene
渲染单帧的流程中,也的确如此,会判断 scene.globe
是否存在才持续 Globe
的更新、渲染。
Globe
是随同着 CesiumWidget
的创立而创立的,优先级仅次于 Scene
、Ellipsoid
。
Globe
被 Scene
对象治理,随同着 Scene
的单帧渲染而渲染、申请。本文中最关怀的,就是地球表面的影像 + 地形的瓦片,它是一种略微做了批改的四叉树数据结构。
这棵四叉树是本文的简直全部内容,在下下大节会讲。
不过也不能忘了 Globe
的其它作用,这里提一下便带过:
- 管制地表水面成果;
- 领有影像图层容器(
ImageryLayerCollection
),进而领有各个影像图层(ImageryLayer
),每个图层又收纳着影像供应器(ImageryProvider
); - 领有地形供应器(
TerrainProvider
); - 管制地球椭球体的裸色(根底色);
- 能显示暗藏;
- 管制瓦片显示精度;
- 管制是否接管光照;
- 管制局部大气着色成果;
- 管制深度检测 ;
- 管制暗影成果;
- 管制地形夸大成果;
- 管制瓦片缓存数量;
- 最重要的一个: 管制瓦片四叉树
1.2. 地球 Globe 与椭球 Ellipsoid
Globe
既然作为地表,撑持起地表的骨架则由 Ellipsoid
定义。
Ellipsoid
定义了地球的形态,也就是数学外表 —— 旋转椭球面,是一个纯正的数学定义,默认值是 WGS84 椭球。创立一个椭球也很简略:
// WGS84 椭球,Cesium 的默认值
new Ellipsoid(6378137.0, 6378137.0, 6356752.3142451793)
你甚至都能够传递一个其它的星球的参数(如果你有),譬如月球。
CGCS2000 椭球与 WGS84 椭球在参数上极其类似,个别无需批改椭球体定义。
1.3. 瓦片四叉树 – QuadtreePrimitive 及其成员
Globe
非凡就非凡在它保护着一棵状态极其简单的瓦片四叉树 QuadtreePrimitive
,每一帧,这个四叉树对象都要决定:
- 瓦片下载好了吗?
- 下载好的瓦片解析了吗?解析时要不要重投影?
- 瓦片是否以后摄像机可见?解析的瓦片能渲染了吗?
- 不能渲染的瓦片做好回退计划了吗?
多个异步同步的状态混合起来判断,就显得比繁多三维物体的 Primitive、Entity 简单得多了。
这个瓦片四叉树,严格来说可能有一棵,也可能有两棵,取决于瓦片的细化规定。
如果用的是 Web 墨卡托投影来做四叉树递归划分,那么只需一棵,因为 Web 墨卡托投影的坐标范畴是一个正方形:
如果 间接应用经纬度范畴 作为坐标值域来做四叉树递归划分瓦片,那么就须要左右两棵。
如何对立示意这两个状态呢?只需在 QuadtreePrimitive
上用个数组存 根瓦片 就好了。它的公有属性 _levelZeroTiles
就是这么一个数组,这个数组只有 0、1、2 三个长度,即 0 代表以后没有根瓦片,1 代表应用 Web 墨卡托投影的范畴做四叉树,2 代表应用天文经纬度来做四叉树。
QuadtreeTile
┖ GlobeSurfaceTile
┠ TileImagery[]
┃ ┖ Imagery
┖ *TerrainData
根瓦片乃至任意瓦片都是 QuadtreeTile
类型的。
每个 QuadtreeTile
都有一个 data
成员,代表这棵形象空间四叉树的任意瓦片上的 数据 ,类型为 GlobeSurfaceTile
;作为一个瓦片的数据,必然有多层影像和单个地形数据形成,瓦片的多层影像数据交由 TileImagery
和 Imagery
实现治理,地形数据则由 HeatmapTerrainData
实现治理。
当然,地形供应器有多种类型,天然就还有其它的地形数据类,譬如 QuantizedMeshTerrainData
、GoogleEarthEnterpriseTerrainData
等。
GlobeSurfaceTileProvider
则是形象瓦片对象 QuadtreeTile
和具体瓦片数据,或者叫数据瓦片 GlobeSurfaceTile
的中间人,负责一系列计算。
这些对象的创立,扩散在第 2、3、4、5 节中。
2. 瓦片四叉树单帧四个流程
在 Scene
原型链上的 render
函数中不难找到 Globe
在一帧内的更新、渲染步骤:
// 步骤:update ~ beginFrame ~ render ~ endFrame
[Module Scene.js]
Scene.prototype.render()
fn prePassesUpdate()
[Module Globe.js]
Globe.prototype.update() // ①
fn render()
Globe.prototype.beginFrame() // ②
fn updateAndExecuteCommands()
fn executeCommandsInViewport()
fn updateAndRenderPrimitives()
[Module Globe.js]
Globe.prototype.render() // ③
[Module Globe.js]
Globe.prototype.endFrame() // ④
Globe 的这 4 个步骤,实际上都是由 QuadtreePrimitive
同名的办法实现的:
Globe.prototype.update()
[Module QuadtreePrimitive.js]
QuadtreePrimitive.prototype.update()
Globe.prototype.beginFrame()
[Module QuadtreePrimitive.js]
QuadtreePrimitive.prototype.beginFrame()
Globe.prototype.render()
[Module QuadtreePrimitive.js]
QuadtreePrimitive.prototype.render()
Globe.prototype.endFrame()
[Module QuadtreePrimitive.js]
QuadtreePrimitive.prototype.endFrame()
接下来就是对这 4 个步骤分步解析。
3. 更新与起帧
更新和起帧比较简单,次要是管制图层可见、初始化各种对象的状态的,没什么简单的行为,所以在第 3 节中一起讲了。
3.1. 更新过程 – Globe 的 update
从 Globe
原型链上的 update
函数开始看,它是 Scene
渲染单帧时,对 Globe
作的第一个大操作。
Globe.prototype.update()
[Module QuadtreePrimitive.js]
QuadtreePrimitive.prototype.update()
[Module GlobeSurfaceTileProvider.js]
GlobeSurfaceTileProvider.prototype.update()
[Module ImageryLayerCollection.js]
ImageryLayerCollection.prototype._update()
抽离骨干,发现次要线索指向的是 ImageryLayerCollection
原型链上的公有办法 _update
,这个瓦片图层容器,就是创立 Globe
时实例化的那一个:
function Globe(ellipsoid) {
// ...
const imageryLayerCollection = new ImageryLayerCollection();
this._imageryLayerCollection = imageryLayerCollection;
// ...
this._surface = new QuadtreePrimitive({
tileProvider: new GlobeSurfaceTileProvider({
terrainProvider: terrainProvider,
imageryLayers: imageryLayerCollection,
surfaceShaderSet: this._surfaceShaderSet,
}),
});
}
它会间接传递给 GlobeSurfaceTileProvider
,并在其被 QuadtreePrimitive
更新时,一起更新,也就是间接执行 ImageryLayerCollection
原型链上的公有更新函数:
GlobeSurfaceTileProvider.prototype.update = function (frameState) {this._imageryLayers._update();
};
影像图层容器的公有更新函数 _update
做了什么呢?这个函数只有三十多行,更新容器内所有 ImageryLayer
的可见状态,顺便触发相干事件,就这么简略。
所以说,第一道过程“更新”,实际上只是:
<div style=”display: grid; place-items: center;”>
<p style=”font-size: 2rem;”>
① update - 更新影像图层的 <code style="font-size: 2rem !important;">show</code> 状态
</p>
</div>
3.2. 起帧过程 – Globe 的 beginFrame
起帧的大抵流程,简直就是产生在 QuadtreePrimitive.js
模块内的:
Globe.prototype.beginFrame()
[Module QuadtreePrimitive.js]
QuadtreePrimitive.prototype.beginFrame()
fn invalidateAllTiles()
[Module GlobeSurfaceTileProvider.js]
GlobeSurfaceTileProvider.prototype.initialize()
fn clearTileLoadQueue()
解读下面这个流程。
起帧是由 Globe
原型链上的 beginFrame
办法登程,会先判断是否有水面成果,有的话持续判断是否有水面法线贴图的相干资源,没有则会创立纹理对象。判断水面这里不波及太多简单的作用域跳转,不过多介绍了。该办法会作通道判断,若为渲染通道,才设置 GlobeSurfaceTileProvider
的一系列状态。
接下来才是起帧的重点:
- 有效化全副瓦片(跟重置状态一个意思)
- 从新初始化
GlobeSurfaceTileProvider
- 革除瓦片四叉树内的加载队列
这 3 个分步骤,由 QuadtreePrimitive.js
模块内的两个函数 invalidateAllTiles()
、clearTileLoadQueue()
以及 GlobeSurfaceTileProvider
原型链上的 initialize
办法按上述流程中的程序顺次执行。
上面是文字版解析。
-
函数
invalidateAllTiles
调用条件及作用- 条件:当
GlobeSurfaceTileProvider
扭转了它的TerrainProvider
时,会要求下一次起帧时QuadtreePrimitive
重设全副的瓦片 - 作用:先调用
clearTileLoadQueue
函数(QuadtreePrimitive.js
模块内函数),革除瓦片加载队列;随后,若存在零级根瓦片(数组成员_levelZeroTiles
),那么就调用它们的freeResources
办法(QuadtreeTile
类型),开释掉所有瓦片上的数据以及子瓦片递归开释
- 条件:当
-
办法
GlobeSurfaceTileProvider.prototype.initialize
的作用:- 作用①是判断影像图层是否程序有变动,有则对瓦片四叉树的每个
QuadtreeTile
的 data 成员上的数据瓦片重排列 - 作用②是开释掉
GlobeSurfaceTileProvider
上上一帧遗留下来待销毁的VertexArray
- 作用①是判断影像图层是否程序有变动,有则对瓦片四叉树的每个
- 函数
clearTileLoadQueue
作用更简略,清空了QuadtreePrimitive
对象上三个公有数组成员,即在第 5 局部要介绍的三个优先级瓦片加载队列,并把一部分调试状态重置。
简略的说,起帧之前:
<div style=”display: grid; place-items: center;”>
<p style=”font-size: 2rem;”>② beginFrame – 清扫洁净屋子好请客 </p>
</div>
4. 瓦片的渲染 – Globe 的 render
这个阶段做两件事:
- 抉择要渲染的瓦片
- 创立绘制指令
瓦片四叉树的类名是 QuadtreePrimitive
,其实它也有一般 Primitive
相似的性能。
Primitive
在它原型链的 update
办法中创立了绘图指令(DrawCommand
),增加到帧状态对象中。
QuadtreePrimitive
则把创立指令并增加到帧状态对象的过程拉得很长,而且作为一个在场景中十分非凡、简单的对象,这么做是正当的;不过,它创立绘图指令的过程不是 update
办法了,而是源自下层 Globe
对象的 render
办法。
大抵流程:
Globe.prototype.render()
[Module QuadtreePrimitive.js]
QuadtreePrimitive.prototype.render()
[Module GlobeSurfaceTileProvider.js]
GlobeSurfaceTileProvider.prototype.beginUpdate()
fn selectTilesForRendering()
fn visitIfVisible()
[Module GlobeSurfaceTileProvider.js]
GlobeSurfaceTileProvider.prototype.computeTileVisibility()
fn visitTile()
fn createRenderCommandsForSelectedTiles()
[Module GlobeSurfaceTileProvider.js]
GlobeSurfaceTileProvider.prototype.showTileThisFrame()
[Module GlobeSurfaceTileProvider.js]
GlobeSurfaceTileProvider.prototype.endUpdate()
fn addDrawCommandsForTile()
在 Chrome 开发者工具中也能截到相似的过程(断点设在 GlobeSurfaceTileProvider.js
模块的 addDrawCommandsForTile
函数中):
比拟长,有两个 QuadtreePrimitive.js
模块内的函数比拟重要:
selectTilesForRendering()
createRenderCommandsForSelectedTiles()
对应就是刚刚提到的两件事:抉择瓦片、创立指令。
这两个函数是夹在 GlobeSurfaceTileProvider
原型链上 beginUpdate()
和 endUpdate()
办法之间的,其中,endUpdate()
办法将创立好的 绘图指令 增加到帧状态对象(FrameState
)中。
4.1. 抉择要被渲染的瓦片 – selectTilesForRendering
由 QuadtreePrimitive
原型链上的 render
办法开始,咱们间接进入渲染通道的分支(拾取通道给须要调试学习的人钻研吧):
// QuadtreePrimitive.prototype.render 中
if (passes.render) {tileProvider.beginUpdate(frameState);
selectTilesForRendering(this, frameState);
createRenderCommandsForSelectedTiles(this, frameState);
tileProvider.endUpdate(frameState); // 4.2 大节介绍
}
首先是 GlobeSurfaceTileProvider
对象的 beginUpdate
办法被调用,它会清空这个对象上的曾经被渲染过的 QuadtreeTile
数组 _tilesToRenderByTextureCount
,并更新裁剪立体(_clippingPlanes
),而后才是重中之重的瓦片对象选择函数 selectTilesForRendering()
。
一进入 selectTilesForRendering()
函数,简单且漫长的瓦片可见性、是否被抉择的计算就开始了。这些瓦片就像是养殖场待选的鱼一样,浮进去的,兴许就被捞走了。
上面用三个大节简略介绍这个选择函数的步骤,不波及具体算法实现。
步骤① 革除待渲染瓦片的数组容器 – _tilesToRender
selectTilesForRendering()
函数会立刻革除瓦片四叉树类(QuadtreePrimitive
)上的 待渲染瓦片数组 _tileToRender
(每个元素是 QuadtreeTile
):
const tilesToRender = primitive._tilesToRender;
tilesToRender.length = 0;
这一步不难理解。它这个行为,侧面反映出 Scene 渲染一帧会齐全清空上一帧要渲染的四叉树瓦片。
步骤② 判断零级瓦片的状态 – _levelZeroTiles
上一步完结后立即会判断瓦片四叉树上的零级瓦片是否存在,不存在则要创立进去。零级瓦片在上文 1.3 大节提过,是一个数组对象 _levelZeroTiles
。
若 GlobeSurfaceTileProvider
不存在是无奈创立零级瓦片的。
const tileProvider = primitive._tileProvider;
if (!defined(primitive._levelZeroTiles)) {if (tileProvider.ready) {
const tilingScheme = tileProvider.tilingScheme;
primitive._levelZeroTiles = QuadtreeTile.createLevelZeroTiles(tilingScheme);
// ...
} else {return;}
}
QuadtreeTile
的静态方法 createLevelZeroTiles()
应用瓦片四叉树上的瓦片宰割模式(tilingScheme
)来创立零级瓦片。其实就是判断是 WebMercator
的正方形区域还是经纬度长方形区域,用一个简略的两层循环创立 QuadtreeTile
。
步骤③ 递归遍历零级瓦片 – visitTile
上一步若能进一步向下执行,那零级瓦片数组必然存在零级瓦片,在 selectTilesForRender()
函数中的最初应用一个 for 循环来遍历它们,会执行深度优先遍历。
这个循环之前还有一些简略的相机运算,状态、数据运算,比较简单,就不开展了
for (i = 0, len = levelZeroTiles.length; i < len; ++i) {
// ... 疏忽分支逻辑层级
visitIfVisible(/* ... */);
// ...
}
循环内先判断瓦片对象是否能够渲染,不能则代表此四叉树瓦片还没下载完数据,将它放入高优先加载数组等第 5 节的终帧过程下载;
循环这一步,还能向下延长两层函数,第一个就是 visitIfVisible()
函数,第二个是 visitTile()
函数:
function visitIfVisible(/* ... */) {
if (tileProvider.computeTileVisibility(tile, frameState, occluders) !==
Visibility.NONE
) {
return visitTile(
primitive,
frameState,
tile,
ancestorMeetsSse,
traversalDetails
);
}
// ...
}
GlobeSurfaceTileProvider
原型链上的 computeTileVisibility
办法会计算瓦片的可见性(Visibility
),对于不是不可见的瓦片,立刻进入递归拜访瓦片的函数 visitTile()
。
visitTile
函数的计算量比拟大,靠近 300 行的数学计算量,这就是 CesiumJS 剔除瓦片,甚至是瓦片调度的外围算法。
算法当前有趣味能够开展细讲,然而这篇文章介绍的并不是算法,就省略这些算法实现了。
既然是四叉树结构,本级瓦片与子一级的四个瓦片的判断就须要谨慎设计。因而,在 visitTile
函数内有一个比拟长的分支,是判断到本级瓦片可被细分的状态时要进行的:
if (tileProvider.canRefine(tile)) {// ... 140+ 行}
在 visitTile
函数中,波及对本级、子一级瓦片各种状态(屏幕空间误差、瓦片数据加载状况、父子替代性优化等)的判断,剩下的活儿就是把适合的瓦片增加至瓦片四叉树上的 _tilesToRender
数组,并再次发动加载级别高优先的瓦片数组的加载行为:
addTileToRenderList(primitive, tile);
queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState);
queueTileLoad
函数比较简单,省略细节;那么 addTileToRenderList
函数就是下一节要重点介绍的了,它把通过是否可见、数据是否加载结束、父子判断后还存活的 QuadtreeTile
生成 DrawCommand
,相当于 Primitive
中的 update
办法,会向帧状态增加绘图指令(也叫绘制指令)。
有人可能会好奇,数据都没通过 HTTP 申请下载下来,这怎么就到生成
DrawCommand
了呢?是这样的,CesiumJS 是一个 WebGL 可视化运行时,渲染当然是第一工作。所以在 4.1 这一节中会有大量的“瓦片是否加载好”的判断,能拿去创立绘制指令的瓦片,必须数据是曾经筹备好的,而没筹备好的,在第 5 大节会申请、下载、创立瓦片等动作。
4.2. 创立指令前的筹备操作 – showTileThisFrame
瓦片通过简单的抉择后,QuadtreePrimitive
类就开始为这些摆放到 _tilesToRender
数组中的 QuadtreeTile
生成以后帧的 绘图指令(DrawCommand)。不过,在创立绘图指令之前,还须要对数组内的 QuadtreeTile
对象们做一下是否真的能被填充到瓦片上的判断,也就是 createRenderCommandsForSelectedTiles
函数的调用:
// QuadtreePrimitive.js 中
function createRenderCommandsForSelectedTiles(primitive, frameState) {
const tileProvider = primitive._tileProvider;
const tilesToRender = primitive._tilesToRender;
for (let i = 0, len = tilesToRender.length; i < len; ++i) {const tile = tilesToRender[i];
tileProvider.showTileThisFrame(tile, frameState);
}
}
这个函数比拟短。它遍历的是瓦片四叉树对象上的 _tilesToRender
数组,这个数组是什么?上一大节第 ③ 步的 visitTile
函数最初会把选到的瓦片通过 addTileToRenderList
函数,把选出来的瓦片增加到这个数组中。
遍历这个数组干嘛呢?做创立 DrawCommand
前的最初一道判断,调用 GlobeSurfaceTileProvider
原型链上的 showTileThisFrame
办法。
这个 showTileThisFrame
办法会统计传进来的 QuadtreeTile
的 data
成员(TileImagery
类型)上的 imagery
成员(Imagery[]
类型)有多少个是筹备好的,条件有二:
Imagery
数据对象是筹备好的Imagery
对象对应的ImageryLayer
不是全透明的
而后,应用这个“筹备好的瓦片的个数”作为键,在 GlobeSurfaceTileProvider
上从新初始化待渲染瓦片的数组:
let tileSet = this._tilesToRenderByTextureCount[readyTextureCount];
if (!defined(tileSet)) {tileSet = [];
this._tilesToRenderByTextureCount[readyTextureCount] = tileSet;
}
并将这个 QuadtreeTile 增加到这个 tileSet
:
tileSet.push(tile);
这个 showTileThisFrame
办法还要判断一下 GlobeSurfaceTile
对象上的 VertexArray
是否筹备好了,如果筹备好了,那么就标记 GlobeSurfaceTileProvider
的 _hasFillTilesThisFrame
为 true,即以后帧已被填充数据;否则就标记 _hasLoadedTilesThisFrame
为 true,即以后帧已加载数据但未生成 VertexArray
。
事已至此,终于实现了一个瓦片的判断,上战场的时刻到了。
4.3. 为抉择的瓦片创立绘制指令 – addDrawCommandsForTile
最初的 GlobeSurfaceTileProvider
对象的 endUpdate
办法才会真正实现指令的创立。
GlobeSurfaceTileProvider.prototype.endUpdate
办法有三个行为:
- 混合可填充瓦片和已加载但未填充的瓦片,应用
TerrainFillMesh.updateFillTiles
静态方法 - 更新地形夸张成果
- 应用双层循环遍历上一步判断已筹备好的
QuadtreeTile
,调用addDrawCommandsForTile
创立DrawCommand
重点也就是最初一个行为,创立绘图指令才是真正的起点,也就是 addDrawCommandsForTile
函数的调用。
它综合了 Globe
上所有的行为、数据对象的成果,次要责任就是把 QuadtreeTile 上的各种材料转换为 DrawCommand
,细分一下责任:
- 判断
VertexArray
- 判断 TerrainData
- 判断水面纹理
- 创立
DrawCommand
所需的各种资源(ShaderProgram
、UniformMap、RenderState
等),并最终创立DrawCommand
这个函数相当长,靠近 700 行,然而创立指令的代码(new DrawCommand
)在这个模块文件中也只有一处,不过如此:
// GlobeSurfaceTileProvider.js 模块内函数
function addDrawCommandsForTile(tileProvider, tile, frameState) {
// ... 省略层级
if (tileProvider._drawCommands.length <= tileProvider._usedDrawCommands) {command = new DrawCommand()
command.owner = tile
command.cull = false
command.boundingVolume = new BoundingSphere()
command.orientedBoundingBox = undefined
} else {/* ... */}
// ...
pushCommand(command, frameState) // 将指令对象增加到帧状态上
// ...
}
其中,pushCommand
这个模块内的函数就是把指令通过简略判断后就增加到帧状态对象中,功败垂成。
绘图指令创立结束,并移交给帧状态对象后,地球渲染地表瓦片的全程,就完结了。然而你肯定会有一个问题:
QuadtreeTile
上的数据哪来的?
这就不得不说到第四个过程了,也就是后置在渲染过程后的终帧过程,它就负责把待加载(下载、解析)的瓦片实现网络数据申请、解析。
劳烦看下一节:
5. 瓦片数据的申请与解决 – Globe 的 endFrame
终帧其实产生了很多事件,包含数据的下载、解析成纹理等对象,甚至瓦片的重投影。
Globe.prototype.endFrame()
[Module QuadtreePrimitive.js]
QuadtreePrimitive.prototype.endFrame()
fn processTileLoadQueue()
fn updateHeights()
fn updateTileLoadProgress()
这一道流程做了优化,在相机航行等过程中是不会进行的,以保障动画性能。
5.1. 回顾瓦片对象层级关系
QuadtreeTile
┖ GlobeSurfaceTile
┠ TileImagery[]
┃ ┖ Imagery → ImageryLayer → *ImageryProvider
┖ (Heightmap|QuantizedMesh|GoogleEarthEnterprise)TerrainData
瓦片四叉树,从形象的角度来看,必然有一个四叉树对象,也就是 QuadtreePrimitive
,它每一个节点即 QuadtreeTile
,也就是树结构上的一个元素。
QuadtreePrimitive
和 QuadtreeTile
并不负责数据管理,它们的作用是数据结构方面的调度,比方依据四叉树瓦片的索引计算其空间范畴、可见性、渲染状态等,脚踏实地地提供着四叉树这种数据结构带来的索引性能晋升。
四叉树瓦片对象有一个 data
成员属性,类型是 GlobeSurfaceTile
,这个才是瓦片的数据自身。GlobeSurfaceTile
对象收纳着影像服务在该四叉树瓦片地位上的影像,以及地形数据。GlobeSurfaceTile
对象有一个 imagery
成员,它是 TileImagery
类型的数组,每一个 TileImagery
就代表一个影像图层在该瓦片处的瓦片图像。
因为 TileImagery
是与瓦片四叉树这一脉相关联的,属于数据模型一层,而真正对服务端的影像服务发动申请的是 ImageryLayer
领有的各种 ImageryProvider
,所以 TileImagery
就用 readyImagery
和 loadingImagery
两个类型均为 Imagery
的成员负责与 ImageryLayer
相关联。这个 Imagery
就是由 ImageryLayer
中的某种 ImageryProvider
在下载数据之后创立的 单个影像瓦片 ,在 Imagery
上就有用瓦片图像生成的 Texture
对象。
对于 GlobeSurfaceTile
的形态,也就是地形数据,在 CesiumJS 中有多种地形数据可供选择,这里不细细开展了,罕用的有高度图(HeightmapTerrainData
)、STK(QuantizedMeshTerrainData
)等,取决于应用的地形提供器(如 EllipsoidTerrainProvider
)。有趣味的能够去学习一下 fuckgiser 的相干博客。
5.2. 地形瓦片(TerrainData)的下载
瓦片的外观是由影像局部负责的,瓦片的形态则由地形服务负责。在本节最开始的代码繁难流程中,QuadtreePrimitive
的 endFrame
函数首先会执行 processTileLoadQueue
函数,这个函数实际上就是取 QuadtreePrimitive
这棵四叉树对象上的三个瓦片加载队列,按程序进行瓦片加载:
// QuadtreePrimitive.js 中
function processTileLoadQueue(primitive, frameState) {
const tileLoadQueueHigh = primitive._tileLoadQueueHigh;
const tileLoadQueueMedium = primitive._tileLoadQueueMedium;
const tileLoadQueueLow = primitive._tileLoadQueueLow;
// ...
let didSomeLoading = processSinglePriorityLoadQueue(/* ... */);
didSomeLoading = processSinglePriorityLoadQueue(/* ... */);
processSinglePriorityLoadQueue(/* ... */);
}
processSinglePriorityLoadQueue
这个模块内的函数会解决单个加载队列,一个一个来,高优先级的 _tileLoadQueueHigh
数组先被这个函数解决,而后是中优先级、低优先级,按程序。它的代码次要就是一个 for 循环,应用 tileProvider
这个传入的参数(GlobeSurfaceTileProvider
类型)的 loadTile
办法,加载每个被遍历到的 QuadtreeTile
:
GlobeSurfaceTileProvider.prototype.loadTile
GlobeSurfaceTile.processStateMachine
fn processTerrainStateMachine
GlobeSurfaceTile 地形状态判断
这个被遍历到的 QuadtreeTile
通过层层传递,到 GlobeSurfaceTile
的静态方法 processStateMachine
之后,交由模块内函数 processTerrainStateMachine
先进行了地形数据的解决(这个函数先解决地形数据,而后才解决影像数据)。
你能够在这个 processTerrainStateMachine
函数内看到并列的几个 if
分支,它们对这个层层传下来的 QuadtreeTile
的数据本体,也就是它的 data
成员(GlobeSurfaceTile
类型)的状态进行判断,满足哪个状态,就进行哪一种解决:
function processTerrainStateMachine(/* 参数 */) {
const surfaceTile = tile.data;
// ...
if (
surfaceTile.terrainState === TerrainState.FAILED &&
parent !== undefined
) {/* ... */}
if (surfaceTile.terrainState === TerrainState.FAILED) {/* ... */}
if (surfaceTile.terrainState === TerrainState.UNLOADED) {/* ... */}
if (surfaceTile.terrainState === TerrainState.RECEIVED) {/* ... */}
if (surfaceTile.terrainState === TerrainState.TRANSFORMED) {/* ... */}
if (
surfaceTile.terrainState >= TerrainState.RECEIVED &&
surfaceTile.waterMaskTexture === undefined &&
terrainProvider.hasWaterMask
) {/* ... */}
}
从这 6 个状态判断分支中,能够看到 CesiumJS 是如何设计瓦片地形数据加载的优先级的:
- 若没加载胜利以后瓦片的地形且下级瓦片存在,则判断父级瓦片是否筹备好,没筹备好则让它持续走
GlobeSurfaceTile.processStateMachine
这个动态函数; - 紧随上一步,用父级瓦片向上采样(以后 Tile 没筹备好,就用父级的地形)
- 紧随上一步,若
GlobeSurfaceTile
的地形状态是未加载,那么调用requestTileGeometry
这个模块内函数,应用对应的地形供应器发动网络数据申请; - 若在以后帧中曾经接管到了网络申请下来的数据,那么第 4 个分支就去创立网格对象;
- 若曾经解决成网格对象,那么第 5 个分支就会创立 WebGL 所需的资源,即顶点缓冲,这一步会应用
GlobeSurfaceTile
更新瓦片的地形夸大成果状态; - 最初一个分支,解决水面成果。
processTerrainStateMachine
函数执行结束后,紧接着流程作用域会返回到 GlobeSurfaceTile.processStateMachine
动态函数,持续下载影像瓦片。
5.3. 影像瓦片(Imagery)的下载
上一大节(5.2)完结了地形数据的战斗,又立马开始了影像的运作。
这一个过程是由 GlobeSurfaceTile
对象的 processImagery
办法执行的,大抵流程如下:
// 下级作用链是 QuadtreePrimitive 对象的 endFrame 办法,始终到 GlobeSurfaceTile 类的 processStateMachine 静态方法
GlobeSurfaceTile.prototype.processImagery
ImageryLayer.prototype._createTileImagerySkeletons
new Imagery
new TileImagery
TileImagery.prototype.processStateMachine
Imagery.prototype.processStateMachine
| ImageryLayer.prototype._requestImagery
*ImageryProvider.prototype.requestImage
| ImageryLayer.prototype._createTexture
| ImageryLayer.prototype._reprojectTexture
首先,先由 ImageryLayer
给 GlobeSurfaceTile
创立 TileImagery & Imagery
,并将 Imagery
送入缓存池,这一步参考 ImageryLayer
原型链上的 _createTileImagerySkeletons
办法,这个办法比拟长,你能够间接拉到办法开端找到 new TileImagery
,简略的说,就是先要确定装数据篮子存在,没有就创立进去。
待确定篮子存在后,才调用 TileImagery
对象的 processStateMachine
办法,进而调用 Imagery
对象的 processStateMachine
办法,去依据 Imagery
的状态抉择不同的解决办法:
Imagery.prototype.processStateMachine = function (
frameState,
needGeographicProjection,
skipLoading
) {if (this.state === ImageryState.UNLOADED && !skipLoading) {
this.state = ImageryState.TRANSITIONING;
this.imageryLayer._requestImagery(this);
}
if (this.state === ImageryState.RECEIVED) {
this.state = ImageryState.TRANSITIONING;
this.imageryLayer._createTexture(frameState.context, this);
}
const needsReprojection =
this.state === ImageryState.READY &&
needGeographicProjection &&
!this.texture;
if (this.state === ImageryState.TEXTURE_LOADED || needsReprojection) {
this.state = ImageryState.TRANSITIONING;
this.imageryLayer._reprojectTexture(
frameState,
this,
needGeographicProjection
);
}
};
其实,就三个状态:
- 没加载且不疏忽加载时,由
ImageryLayer
对象发动网络申请 - 数据接管后,由
ImageryLayer
对象创立Texture
对象 - 纹理创立好后,由
ImageryLayer
进行重投影
咱们这篇文章就不开展纹理对象的介绍和重投影的介绍了,重点还是影像瓦片的下载:调用 ImageryLayer
的数据申请办法 _requestImagery
,进而调用 ImageryProvider
的 requestImage
办法申请瓦片。
ImageryLayer
来自ImageryLayerCollection
,这个容器对象由Globe
对外裸露以供开发者增加图层,对内则从GlobeSurfaceTileProvider
始终向下传递到须要的类上
那么,瓦片的影像局部就实现了下载、生成纹理。
5.4. 小结
第四道过程,也就是终帧过程完结后,Scene
中渲染地球对象的全副工作才算实现。
在这一道过程中,次要还是为下一帧筹备好地形和影像瓦片数据,期间会应用 WebWorker 技术进行地形数据的解决,还会发动影像瓦片的网络申请。
我集体认为,这一步理清各种对象之间的关系十分重要。前期思考画一下对象关系图,Globe
这一支上的类还是蛮多蛮杂的。
数据最终都会记录在 QuadtreeTile
的 data
字段(GlobeSurfaceTile
类型)上,期待 Globe
下一帧渲染时(也就是回到本文第 3 节)取用。
6. 总结
我预料到地球的渲染会比较复杂,然而没想到这个会比 Primitive API
、比 Entity API
更简单,所以花了较长时间去钻研源码,是 Entity API
耗时的两倍多。
瞎话说,我写这篇很毛糙,甚至有可能呈现前后表述不相接,还请读者体谅。
Globe
作为 Scene
中较为非凡的一个三维物体,不像 Entity API
那样用事件机制实现渲染循环的挂载、Primitive
的生成,最重要的就是它保护的瓦片四叉树对象,负责渲染(间接创立 DrawCommand
、ComputeCommand
等)、网络申请瓦片数据并解析,计算瓦片可见、可渲染、多成果叠加(也就是所谓的瓦片调度),这就比 Entity API
、Primitive API
要简单得多。
我本认为,一个四叉树对象,每个节点对象在渲染时用相机视锥体判断一下可不可见,数据有没有,就算全副了,没想到真正由 CesiumJS 实现起来居然有这么简单,数据模型、数据容器、网络申请等均被各种类合成了,并没有糅杂在一起
CesiumJS 在瓦片的可见、父子替换计算、地表成果叠加等方面做了很多功夫,因为 3D 的瓦片并不是 2D 瓦片多了一个高度那么简略的。基于各种对象的状态设计,随同着每一个申请帧流逝,真正做到了“处于什么状态就做什么事件”。
6.1. 好基友 QuadtreePrimitive 和 GlobeSurfaceTileProvider
这俩都有本人的小弟,前者是 QuadtreeTile
,后者是 GlobeSurfaceTile
,一度让我很好奇为什么要在数据模型和数据处理上做这两个类。
起初我想了想,用这两个角色思考就很容易了解了:项目经理和技术经理。
QuadtreePrimitive
大多数时候负责 QuadtreeTile
的空间算法调度,是一种“调度角色”,而 GlobeSurfaceTileProvider
则负责与各种数据发生器交换,具备创立数据对象的能力,它须要来自 QuadtreePrimitive
的抉择后果,最初交给 GlobeSurfaceTile
实现每个瓦片的数据生成工作。
这两个好基友就这么一左一右搭配,扛起了地球的绝大多数职责,Globe
更多时候是对外的一个状态窗口,也就是“大老板”。
6.2. 不能顾及的其它细节
Globe
除了瓦片四叉树这一脉之外,还有用于成果方面的对象,譬如淡水动静法线纹理、地球拾取、深度问题、切片规定、裁剪和限定显示、大气层成果、特定材质等,不能一一列举,然而这些都会随着 GlobeSurfaceTileProvider
的 addDrawCommandsForTile
函数一并创立出绘图指令,并交给帧状态的,而且绝对这棵四叉树来说没那么简单,所以倡议有余力的读者深入研究。
对于地形瓦片,CesiumJS 应用 ①高度值瓦片、②STK 瓦片 两种格局来表白瓦片的形态;对于影像瓦片,CesiumJS 则应用应用 TileImagery
治理起多个影像图层的瓦片。这两处数据的差别、生成过程,我并没有介绍,fuckgiser 的博客曾经介绍得很具体了,数据格式这方面这几年来很稳固,没怎么变动,当前有机会的话也能够写一写。
影像瓦片的重投影,我也没有深刻,当前或者思考独自写一个系列,对于影像瓦片的坐标纠正之类的吧。
着色器方面,整套源码中着色器代码大小最大的就是 GlobeVS
和 GlobeFS
这一对了,精力有限,当前持续探讨(实际上,CesiumJS 的着色器是一套整体,可能专门找工夫学习效果会好些)。