1. 引言

Cesium是一款三维地球和地图可视化开源JavaScript库,使用WebGL来进行硬件加速图形,使用时不需要任何插件支持,基于Apache2.0许可的开源程序,可以免费用于商业和非商业用途

Cesium官网:Cesium: The Platform for 3D Geospatial

Cesium GitHub站点:CesiumGS/cesium: An open-source JavaScript library for world-class 3D globes and maps (github.com)

API文档:Index - Cesium Documentation

通过阅读源码,理清代码逻辑,有助于扩展与开发,笔者主要参考了以下两个系列的文章

本文描述Cesium的渲染流程

2. 概述

Cesium渲染流程大致如图:

image-20230210152429959

  • endFrame()准备需要渲染的数据
  • updateAndExecuteCommands()更新和调度渲染指令(图元对象含有渲染指令)
  • Scene.render()进行绘制渲染
  • CesiumWidget展示Scene.render()的内容并让其不断绘制(RenderLoop)

3. 渲染流程

Cesium的使用,往往是从创建Viewer开始的:

// ...
<div id="cesiumContainer"></div>
<script>
    const viewer = new Cesium.Viewer('cesiumContainer')
</script>
// ...

new一个Viewer就可以创建一个地球,这其中经历了什么样的过程呢?

Viewer构造函数大致如下:

function Viewer(container, options) {
  // ...
  container = getElement(container);
  options = defaultValue(options, defaultValue.EMPTY_OBJECT);

  // Cesium widget container
  const cesiumWidgetContainer = document.createElement("div");

  // Cesium widget
  const cesiumWidget = new CesiumWidget(cesiumWidgetContainer);
  // ...

}

Viewer主要由CesiumWidget构成,其构造函数大致如下:

function CesiumWidget(container, options) {
  // ...
  container = getElement(container);
  options = defaultValue(options, defaultValue.EMPTY_OBJECT);
  // ...
  const canvas = document.createElement("canvas");
  const scene = new Scene({
        canvas: canvas,
        // ...
  });

    //Set the base imagery layer
    let imageryProvider =
        options.globe === false ? false : options.imageryProvider;
    if (!defined(imageryProvider)) {
        imageryProvider = createWorldImagery();
    }

    //Set the terrain provider if one is provided.
    if (defined(options.terrainProvider) && options.globe !== false) {
        scene.terrainProvider = options.terrainProvider;
    }
    
    // 设置循环渲染
    this._useDefaultRenderLoop = undefined;
    this.useDefaultRenderLoop = defaultValue(
      options.useDefaultRenderLoop,
      true
    );
  // ...
}

CesiumWidget中默认渲染函数useDefaultRenderLoop指向startRenderLoop:

useDefaultRenderLoop: {
  // ...
  startRenderLoop(this);
},

渲染函数startRenderLoop为:

function startRenderLoop(widget) {
  // ...
  let lastFrameTime = 0;
  function render(frameTime) {
  // ....
          widget.render();
          requestAnimationFrame(render);
  // ...
  }
  requestAnimationFrame(render);
}

widget的render函数指向scene的render函数:

CesiumWidget.prototype.render = function () {
     // ...
	this._scene.render(currentTime);
};

scene的render函数:

function render(scene) {
  // ...
  if (defined(scene.globe)) {
    scene.globe.beginFrame(frameState);
  }

  scene.updateEnvironment();
  scene.updateAndExecuteCommands(passState, backgroundColor);
  scene.resolveFramebuffers(passState);

  if (defined(scene.globe)) {
    scene.globe.endFrame(frameState);
  }
  // ...
}

updateAndExecuteCommands()负责管理,创建执行Commands的Tasks,自己并不负责Tasks内容的实现

scene.globe.endFrame()中,会对该帧所涉及的GlobeTile的下载,解析等进行处理

先看看scene.globe.endFrame()的数据处理:

scene.globe.endFrame()指向surface.endFrame()

Globe.prototype.endFrame = function (frameState) {
	this._surface.endFrame(frameState);
};

surface是图元QuadtreePrimitive:

 this._surface = new QuadtreePrimitive({
    tileProvider: new GlobeSurfaceTileProvider({
      terrainProvider: terrainProvider,
      imageryLayers: imageryLayerCollection,
      surfaceShaderSet: this._surfaceShaderSet,
    }),
  });

QuadtreePrimitive的endFrame()方法进行影像和高程的处理:

QuadtreePrimitive.prototype.endFrame = function (frameState) {
  // ...
  // Load/create resources for terrain and imagery. Prepare texture re-projections for the next frame.
  processTileLoadQueue(this, frameState);
  updateHeights(this, frameState);
  updateTileLoadProgress(this, frameState);
};

再来看看updateAndExecuteCommands()方法:

Scene.prototype.updateAndExecuteCommands = function (passState, backgroundColor) {
    // ...
    executeCommandsInViewport(true, this, passState, backgroundColor);
    // ...
};

updateAndExecuteCommands()方法主要进行更新执行各种Commands:

function executeCommandsInViewport(firstViewport, scene, passState, backgroundColor) {
  // ...
  updateAndRenderPrimitives(scene);
  executeCommands(scene, passState);
}

updateAndRenderPrimitives()进行更新:

this._primitives = new PrimitiveCollection();

function updateAndRenderPrimitives(scene) {
  // ...
  scene._groundPrimitives.update(frameState);
  scene._primitives.update(frameState);
  // ...
}
PrimitiveCollection.prototype.update = function (frameState) {
  // ...
  for (let i = 0; i < primitives.length; ++i) {
    primitives[i].update(frameState);
  }
};

进行纹理、外观和材质的更新:

Primitive.prototype.update = function (frameState) {
    // ...
   this._batchTable.update(frameState);

   // ...
   // Create or recreate render state and shader program if appearance/material changed
   commandFunc(this, appearance,material,translucent,twoPasses,this._colorCommands,this._pickCommands,frameState);
};

BatchTable.prototype.update = function (frameState) {
  // ...
  updateTexture(this);
};
function updateTexture(batchTable) {
  const dimensions = batchTable._textureDimensions;
  batchTable._texture.copyFrom({
    source: {
      width: dimensions.x,
      height: dimensions.y,
      arrayBufferView: batchTable._batchValues,
    },
  });
}

executeCommands()调度执行命令

function executeCommands(scene, passState) {
    // ...
    // Draw terrain classification
    executeCommand(commands[j], scene, context, passState);

    // Draw 3D Tiles
    executeCommand(commands[j], scene, context, passState)

    // Draw classifications. Modifies 3D Tiles color.
    executeCommand(commands[j], scene, context, passState);
    // ...
}
function executeCommand(command, scene, context, passState, debugFramebuffer) {
  // ...
  if (command instanceof ClearCommand) {
    command.execute(context, passState);
    return;
  }

  // ...
  command.execute(context, passState);
  // ...

}

比如ClearCommand,它会执行clear命令:

ClearCommand.prototype.execute = function (context, passState) {
  context.clear(this, passState);
};

最终命令变成GL指令:

Context.prototype.clear = function (clearCommand, passState) {
  // ...
  const c = clearCommand.color;
  gl.clearColor(c.red, c.green, c.blue, c.alpha);
  // ...
};

4. 参考资料

  1. Cesium原理篇:1最长的一帧之渲染调度 - fu*k - 博客园 (cnblogs.com)
  2. CesiumJS 2022^ 源码解读1 使用 requestAnimationFrame 循环触发帧动画 - 四季留歌 - 博客园 (cnblogs.com)
  3. GitHub - CesiumGS/cesium: An open-source JavaScript library for world-class 3D globes and maps