在上一篇Cesium 源码分析入门(一)一文中,我们简单解读了Viewer.js脚本,初步了解到如何初始化地球和小部件功能模块,勉强算是推开了cesium源码世界的大门的一条缝儿,此文涉及的内容会更多,层次会更深,争取带大家推开大门,一起看看大门的另一边是什么!
一、CesiumWidget
之前已经介绍过,CesiumWidget功能模块是加载地球的关键脚本,用一行代码即可实现加载地球。
const cesiumWidget = new CesiumWidget(document.getElementById("cesiumContainer"));
可以找到CesiumWidget模块代码位于Source\Widgets\CesiumWidget\CesiumWidget.js,相较于Viewer.js的两千多行代码而言,CesiumWidget.js只有八百行左右代码,老规矩,直接Ctrl+F搜索function CesiumWidget 进行定位。
在此先简单解读一下CesiumWidget.js脚本,仔细研读之后,发现大致过程就是:
- 获取dom容器
- 判断options参数,若不存在则 freeze 冻结赋值空对象
- 创建div容器与canvas画布,用于显示地球,创建一些用于显示版权部件的div容器,并处理一些兼容性功能细节,设置画布尺寸、分辨率等
- 将一些属性赋值为CesiumWidget示例的私有属性
- 创建Scene三维场景,完成了Scene、Globe、SkyBox、SkyAtmosphere模块的实例化
- 配置一些其他辅助属性为私有属性,并编写错误事件处理函数
- 最后就是为CesiumWidget实例注册一些公共函数API和私有函数API,以及一些属性等,开放的一些方法和属性都可以在API开发文档中可以找到,并可直接调取使用。附上cesiumAPI地址
function CesiumWidget(container, options) {
// 判断是否传入domid,如果没有传值,则抛出错误,defined方法:判断参数是否为undifined或null,如果不是则返回true。
if (!defined(container)) {
throw new DeveloperError("container is required.");
}
// 根据domid获取DOM元素,使用getElement方法返回dom元素【document.getElementById()】。
container = getElement(container);
// 判断是否传入options,如果为空,则使用预设值:defaultValue.EMPTY_OBJECT:{}
// defaultValue方法:判断第一个参数如果不存在,则把第二个参数作为它的值返回,如果存在,那就返回它本身。
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
// 创建DOM元素存放画布
const element = document.createElement("div");
element.className = "cesium-widget";
container.appendChild(element);
// 创建canvas画布
const canvas = document.createElement("canvas");
// 判断浏览器是否支持渲染图像像素相关,在不受支持的浏览器上,canvas.style.imageRendering将为未定义、空或空字符串。
const supportsImageRenderingPixelated = FeatureDetection.supportsImageRenderingPixelated();
this._supportsImageRenderingPixelated = supportsImageRenderingPixelated;
if (supportsImageRenderingPixelated) {
canvas.style.imageRendering = FeatureDetection.imageRenderingValue();
}
// 屏蔽在鼠标右键在canvas画布弹出菜单
canvas.oncontextmenu = function () {
return false;
};
// 避免鼠标连续快速点击画布导致选中整个canvas变蓝的情况
canvas.onselectstart = function () {
return false;
};
// 与画布交互之后,上一个元素的焦点不会自动失焦,如果上一个dom元素是输入框,则会导致意外交互。
// 例如,在画布上点击滚轮按键可能会导致输入框的值改变
// 解决方案是一旦在画布上开始交互,就将当前focus的元素进行失焦
function blurActiveElement() {
if (canvas !== canvas.ownerDocument.activeElement) {
canvas.ownerDocument.activeElement.blur();
}
}
// 监听鼠标与画布的交互,进行元素失焦
canvas.addEventListener("mousedown", blurActiveElement);
// 监听指针设备与画布的交互,进行元素失焦
canvas.addEventListener("pointerdown", blurActiveElement);
// 将canvas画布添加到dom容器中
element.appendChild(canvas);
// 一些其他的小部件(例如商标版权等)以及分辨率等设置
const innerCreditContainer = document.createElement("div");
innerCreditContainer.className = "cesium-widget-credits";
const creditContainer = defined(options.creditContainer)
? getElement(options.creditContainer)
: element;
creditContainer.appendChild(innerCreditContainer);
const creditViewport = defined(options.creditViewport)
? getElement(options.creditViewport)
: element;
// 设置属性,如果渲染出现错误,是否需要弹出错误提示窗口
const showRenderLoopErrors = defaultValue(options.showRenderLoopErrors, true);
// 设置属性,是否需要使用浏览器推荐的分辨率
const useBrowserRecommendedResolution = defaultValue(
options.useBrowserRecommendedResolution,
true
);
// 注册为CesiumWidget实例的属性
this._element = element;
this._container = container;
this._canvas = canvas;
this._canvasClientWidth = 0;
this._canvasClientHeight = 0;
this._lastDevicePixelRatio = 0;
this._creditViewport = creditViewport;
this._creditContainer = creditContainer;
this._innerCreditContainer = innerCreditContainer;
this._canRender = false;
this._renderLoopRunning = false;
this._showRenderLoopErrors = showRenderLoopErrors;
this._resolutionScale = 1.0;
this._useBrowserRecommendedResolution = useBrowserRecommendedResolution;
this._forceResize = false;
this._clock = defined(options.clock) ? options.clock : new Clock();
// 设置canvas尺寸显示比例,并把canvas画布的尺寸_canvasClientWidth、_canvasClientHeight等属性注册为CesiumWidget实例的属性
configureCanvasSize(this);
// 最关键的部分,创建Scene三维场景。
//是一个大大的try/catch块,C这部分代码,完成了Scene、Globe、SkyBox、SkyAtmosphere模块的实例化。
try {
const scene = new Scene({
canvas: canvas,
contextOptions: options.contextOptions,
creditContainer: innerCreditContainer,
creditViewport: creditViewport,
mapProjection: options.mapProjection,
orderIndependentTranslucency: options.orderIndependentTranslucency,
scene3DOnly: defaultValue(options.scene3DOnly, false),
shadows: options.shadows,
mapMode2D: options.mapMode2D,
requestRenderMode: options.requestRenderMode,
maximumRenderTimeChange: options.maximumRenderTimeChange,
depthPlaneEllipsoidOffset: options.depthPlaneEllipsoidOffset,
msaaSamples: options.msaaSamples,
});
this._scene = scene;
// 指定摄像机的约束轴为Z轴
scene.camera.constrainedAxis = Cartesian3.UNIT_Z;
// 调整配置像素比例
configurePixelRatio(this);
// 调整配置摄像机视锥体
configureCameraFrustum(this);
// 创建ellipsoid和globe,并传递给scene.
const ellipsoid = defaultValue(
scene.mapProjection.ellipsoid,
Ellipsoid.WGS84
);
let 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
);
}
// 创建环境因素,主要是天空盒和太阳、月亮、大气环境。
let 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.
let skyAtmosphere = options.skyAtmosphere;
if (!defined(skyAtmosphere)) {
skyAtmosphere = new SkyAtmosphere(ellipsoid);
}
if (skyAtmosphere !== false) {
scene.skyAtmosphere = skyAtmosphere;
}
// 创建影像数据源(若无,则调用createWorldImagery模块创建世界影像,和CesiumION的token有关)和地形数据源,并传递给scene。
// 影像数据源和地形数据源均可以从options中获取,若options没有,则使用Cesium官方给的,需要注意token问题。
let 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;
}
//
this._screenSpaceEventHandler = new ScreenSpaceEventHandler(canvas);
// 确定scene对象的视图模式是二维的、三维的还是哥伦布的(2.5D)
if (defined(options.sceneMode)) {
if (options.sceneMode === SceneMode.SCENE2D) {
this._scene.morphTo2D(0);
}
if (options.sceneMode === SceneMode.COLUMBUS_VIEW) {
this._scene.morphToColumbusView(0);
}
}
// 确定了是否使用默认的循环渲染机制(useDefaultRenderLoop属性),这个属性若为false,则需手动调用CesiumWidget.render()渲染
this._useDefaultRenderLoop = undefined;
this.useDefaultRenderLoop = defaultValue(
options.useDefaultRenderLoop,
true
);
// 确定在默认循环渲染机制时,目标帧速率(targetFrameRate属性)
this._targetFrameRate = undefined;
this.targetFrameRate = options.targetFrameRate;
// 给scene绑定了渲染错误事件处理函数
const that = this;
this._onRenderError = function (scene, error) {
that._useDefaultRenderLoop = false;
that._renderLoopRunning = false;
if (that._showRenderLoopErrors) {
const title =
"An error occurred while rendering. Rendering has stopped.";
that.showErrorPanel(title, undefined, error);
}
};
scene.renderError.addEventListener(this._onRenderError);
} catch (error) {
if (showRenderLoopErrors) {
const title = "Error constructing CesiumWidget.";
const message =
'Visit <a href="http://get.webgl.org">http://get.webgl.org</a> to verify that your web browser and hardware support WebGL. Consider trying a different web browser or updating your video drivers. Detailed error information is below:';
this.showErrorPanel(title, message, error);
}
throw error;
}
}
二、webGL入口
从上文代码中可以看到,创建Scene三维场景尤为关键,接下来简单解读一下Scene.js脚本,简单解读后发现,是通过调用Context模块获取的webGL上下文对象
Scene.js
function Scene(options) {
// ....省略一些代码
if (!defined(canvas)) {
throw new DeveloperError("options and options.canvas are required.");
}
const context = new Context(canvas, contextOptions);
// ...... 省略一些代码
在Context.js脚本中,首先就是判断浏览器是否支持使用webGL,之后进行了一系列的重要参数配置的获取与赋值,最终使用我们最熟悉的哪一行代码 canvas.getContext() 获取画笔,开启接下来的画作。
Context.js
function Context(canvas, options) {
// this check must use typeof, not defined, because defined doesn't work with undeclared variables.
if (typeof WebGLRenderingContext === "undefined") {
throw new RuntimeError(
"The browser does not support WebGL. Visit http://get.webgl.org."
);
}
//>>includeStart('debug', pragmas.debug);
Check.defined("canvas", canvas);
//>>includeEnd('debug');
this._canvas = canvas;
options = clone(options, true);
// Don't use defaultValue.EMPTY_OBJECT here because the options object gets modified in the next line.
options = defaultValue(options, {});
options.allowTextureFilterAnisotropic = defaultValue(
options.allowTextureFilterAnisotropic,
true
);
const webglOptions = defaultValue(options.webgl, {});
// Override select WebGL defaults
webglOptions.alpha = defaultValue(webglOptions.alpha, false); // WebGL default is true
webglOptions.stencil = defaultValue(webglOptions.stencil, true); // WebGL default is false
const requestWebgl2 =
defaultValue(options.requestWebgl2, false) &&
typeof WebGL2RenderingContext !== "undefined";
let webgl2 = false;
let glContext;
const getWebGLStub = options.getWebGLStub;
if (!defined(getWebGLStub)) {
if (requestWebgl2) {
glContext =
canvas.getContext("webgl2", webglOptions) ||
canvas.getContext("experimental-webgl2", webglOptions) ||
undefined;
if (defined(glContext)) {
webgl2 = true;
}
}
if (!defined(glContext)) {
glContext =
canvas.getContext("webgl", webglOptions) ||
canvas.getContext("experimental-webgl", webglOptions) ||
undefined;
}
if (!defined(glContext)) {
throw new RuntimeError(
"The browser supports WebGL, but initialization failed."
);
}
} else {
// Use WebGL stub when requested for unit tests
glContext = getWebGLStub(canvas, webglOptions);
}
// .......省略一些代码
}
三、总结
深深的话,浅浅的说。cesium源码就像套娃一样,一层又一层,最终呈现给我们使用的是,仅仅用一行代码就可以加载一个地球,但这行代码背后的逻辑判断错综复杂 却又有迹可循,都知道cesium底层就是基于webGL开发的,探寻了Viewer.js、CesiumWidget.js、Scene.js、Context.js,终于嗅到了WebGL的一点点踪迹,接下来的行程可能会布满荆棘,将会涉及大量的数学、地理、图形、着色器等等一系列的难题,但是希望我们都能从荆棘中找到一条通往成功的路!