Cesium源码跟读之CesiumWidget的实现

前言

没错,我也来读源码了,因为之前的文章中简单提到了一下Cesium的渲染机制,所以我索性一咬牙将这块的源码通读一遍,自己也能更深入的了解Cesium,这篇文章也就作为我的一个记录。准备工作只有一步:从GitHub上拉取一份Cesium最新的代码,找到Source\Widgets\CesiumWidget\CesiumWidget.js文件。

文章中代码仅为涉及到讲解部分的代码,未全部放出

用法

CesiumWidget的用法和Viewer的用法类似,都是通过一个id来生成容器。

1
2
const widget = new Cesium.CesiumWidget('id');
const viewer = new Cesium.Viewer('id');

两者的区别是,只要实例化了Viewer则必定会实例化一个CesiumWidget。Viewer除了可视区域外还囊括了各种控件,比如时间轴、时间拨盘、搜索框等。

Cesium中使用WebGL进行绘图的是实例化的Scene模块。CesiumWidget更像一个桥梁,把构造时传递的DOM元素(或ID选择器)再内嵌了一个canvas元素,再将此canvas元素传递给Scene让其绘图。

内部方法

  • [24行]getDefaultSkyBoxUrl:获取默认天空盒资源地址
  • [30行]startRenderLoop:开启渲染循环,这也是我们上一篇中提到的Cesium的渲染机制。利用requestAnimationFrame来不停的刷新渲染页面。
  • [71行]configurePixelRatio:配置像素比。
  • [86行]configureCanvasSize:配置画布大小。
  • [105行]configureCameraFrustum:配置相机视锥。

构造

DOM构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function CesiumWidget(container, options) {
if (!defined(container)) {
throw new DeveloperError("container is required.");
}

container = getElement(container);
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
//Configure the widget DOM elements
var element = document.createElement("div");
element.className = "cesium-widget";
container.appendChild(element);
var canvas = document.createElement("canvas");

// ……

element.appendChild(canvas);
}

这部分代码完成了Viewer实例的DOM元素判断、本级DOM元素创建和下级DOM(canvas)的创建,以及传入的options是否为空。

如此DOM的层级便生成完毕了,下方代码(227~263行)主要是像商标这种部分以及分辨率等设置、将CesiumWidget的私有变量赋值并调用configureCanvasSize来修改canvas的大小。

场景构造

从265行开始我们会看到一个巨大的try/catch模块。这部分代码完成了SceneGlobeSkyBoxSkyAtmosphere四个模块的实例化。

这里我们会发现一个问题,在使用CesiumWidget的时候,明明有像CameraImageryLayers之类的API,是从哪来的呢。这里先卖个关子,我们先专注于Scene

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var scene = new Scene({
canvas: canvas,
contextOptions: options.contextOptions,
creditContainer: innerCreditContainer,
creditViewport: creditViewport,
mapProjection: options.mapProjection,
orderIndependentTranslucency: options.orderIndependentTranslucency,
scene3DOnly: defaultValue(options.scene3DOnly, false),
terrainExaggeration: options.terrainExaggeration,
shadows: options.shadows,
mapMode2D: options.mapMode2D,
requestRenderMode: options.requestRenderMode,
maximumRenderTimeChange: options.maximumRenderTimeChange,
});
this._scene = scene;

实例化Scene对象,传递构造的参数(绝大部分来自于options)。

1
2
3
4
scene.camera.constrainedAxis = Cartesian3.UNIT_Z;

configurePixelRatio(this);
configureCameraFrustum(this);

指定camera的约束轴为Z轴,调用函数来配置像素比、相机视锥体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var ellipsoid = defaultValue(
scene.mapProjection.ellipsoid,
Ellipsoid.WGS84
);
var globe = options.globe;
if (!defined(globe)) {
globe = new Globe(ellipsoid);
}
if (globe !== false) {
scene.globe = globe;
scene.globe.shadows = defaultValue(
options.terrainShadows,
ShadowMode.RECEIVE_ONLY
);
}

创建ellipsoidglobe并传递给scene

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var skyBox = options.skyBox;
if (!defined(skyBox)) {
skyBox = new SkyBox({
sources: {
positiveX: getDefaultSkyBoxUrl("px"),
negativeX: getDefaultSkyBoxUrl("mx"),
positiveY: getDefaultSkyBoxUrl("py"),
negativeY: getDefaultSkyBoxUrl("my"),
positiveZ: getDefaultSkyBoxUrl("pz"),
negativeZ: getDefaultSkyBoxUrl("mz"),
},
});
}
if (skyBox !== false) {
scene.skyBox = skyBox;
scene.sun = new Sun();
scene.moon = new Moon();
}

// Blue sky, and the glow around the Earth's limb.
var skyAtmosphere = options.skyAtmosphere;
if (!defined(skyAtmosphere)) {
skyAtmosphere = new SkyAtmosphere(ellipsoid);
}
if (skyAtmosphere !== false) {
scene.skyAtmosphere = skyAtmosphere;
}

创建天空盒、大气环境等环境因素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Set the base imagery layer
var imageryProvider =
options.globe === false ? false : options.imageryProvider;
if (!defined(imageryProvider)) {
imageryProvider = createWorldImagery();
}

if (imageryProvider !== false) {
scene.imageryLayers.addImageryProvider(imageryProvider);
}

//Set the terrain provider if one is provided.
if (defined(options.terrainProvider) && options.globe !== false) {
scene.terrainProvider = options.terrainProvider;
}

设置影像数据源,如果没有配置,则调用createWorldImagery模块创建世界影像并传递给scene,如果配置中有地形数据源,则将其传递给scene

1
this._screenSpaceEventHandler = new ScreenSpaceEventHandler(canvas);

创建事件处理器,用于下方暴露在原型中。

1
if (defined(options.sceneMode)) {    if (options.sceneMode === SceneMode.SCENE2D) {        this._scene.morphTo2D(0);    }    if (options.sceneMode === SceneMode.COLUMBUS_VIEW) {        this._scene.morphToColumbusView(0);    }}

判断scene的视图模式是二维、三维还是2.5的。

1
var that = this;this._onRenderError = function (scene, error) {    that._useDefaultRenderLoop = false;    that._renderLoopRunning = false;    if (that._showRenderLoopErrors) {        var title =            "An error occurred while rendering.  Rendering has stopped.";        that.showErrorPanel(title, undefined, error);    }};scene.renderError.addEventListener(this._onRenderError);

scene绑定了渲染错误事件的处理函数。

1
this._useDefaultRenderLoop = undefined;this.useDefaultRenderLoop = defaultValue(    options.useDefaultRenderLoop,    true);this._targetFrameRate = undefined;this.targetFrameRate = options.targetFrameRate;

确认是否使用默认循环机制,如果配置为false则需要手动调用render()方法,否则使用目标帧速率进行渲染。

原型定义

我们在上边提过,CesiumWidget那么多的API都是从哪来的,代码读到这里我们就会发现,从390行向下,都是利用Object.defineProperties()在CesiumWidget的原型上添加属性以及方法。

属性定义
1
Object.defineProperties(CesiumWidget.prototype, {    container: {        get: function () {            return this._container;        },    },    canvas: {        get: function () {            return this._canvas;        },    },    // ……}
方法定义
  • [640行] CesiumWidget.prototype.showErrorPanel:向用户显示错误面板,其中包含标题和较长的错误消息,可以使用’确定’按钮将其关闭。该面板自动显示发生渲染循环错误时,如果showRenderLoopErrors不为false,则当小部件已构建。
  • [740行]CesiumWidget.prototype.isDestroyed:返回CesiumWidget是否被销毁。
  • [748行]CesiumWidget.prototype.destroy:销毁视图。
  • [763行]CesiumWidget.prototype.resize:更新画布大小、相机纵横比和视口大小。这个函数默认会自动调用,除非useDefaultRenderLoop设置为false
  • [785行]CesiumWidget.prototype.render:渲染场景,同样的它也会自动调用,除非useDefaultRenderLoop设置为false

导出

1
export default CesiumWidget;

最后对整个模块进行导出。

结构图

image-20210531155553870.png

最后

我们只是针对这个文件中的私有函数以及构造函数进行阅读和分析,并未对其引用的模块比如SceneGlobe等进行阅读。感兴趣的小伙伴可以针对其中一个引入的模块进行更深入的阅读。如果是想在Cesium上深入研究的话我认为有些功能和实现的源码大概通读一下,了解一下实现原理是有必要的。


Cesium源码跟读之CesiumWidget的实现
https://moewang0321.github.io/2021/06/02/Cesium源码跟读之CesiumWidget的实现/
作者
Moe Wang
发布于
2021年6月2日
许可协议