Cesium 源码分析入门(二)

在上一篇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的一点点踪迹,接下来的行程可能会布满荆棘,将会涉及大量的数学、地理、图形、着色器等等一系列的难题,但是希望我们都能从荆棘中找到一条通往成功的路!

  • 12
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值