Cesium渲染模块之Command

发布时间 2023-03-22 19:13:54作者: 当时明月在曾照彩云归

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渲染模块的Command

2. Cesium中的Command

Cesium中的Command对象包含执行的指令参数和执行方法,比如最简单的ClearCommand

function ClearCommand(options) {
  // ...
  this.color = options.color;
  this.depth = options.depth;
  this.stencil = options.stencil;
  this.renderState = options.renderState;
  this.framebuffer = options.framebuffer;
  this.owner = options.owner;
  this.pass = options.pass;
}
 
ClearCommand.prototype.execute = function (context, passState) {
  context.clear(this, passState);
};
 

ClearCommand包含颜色、深度、通道等指令参数和执行方法context.clear(this, passState)

Command对象主要有三类:

  • ClearCommand
  • DrawCommand
  • ComputeCommand

正如其名,ClearCommand用于清除,DrawCommand用于绘制,ComputeCommand用于计算

3. ClearCommand

ClearCommand的封装很简单,如上述代码所示:

function ClearCommand(options) {
  // ...
  this.color = options.color;
  this.depth = options.depth;
  this.stencil = options.stencil;
  this.renderState = options.renderState;
  this.framebuffer = options.framebuffer;
  this.owner = options.owner;
  this.pass = options.pass;
}
 
ClearCommand.prototype.execute = function (context, passState) {
  context.clear(this, passState);
};

context.clear()会执行清除的WebGL指令:

Context.prototype.clear = function (clearCommand, passState) {
  // ...
  const c = clearCommand.color;
  const d = clearCommand.depth;
  const s = clearCommand.stencil;

  gl.clearColor(c.red, c.green, c.blue, c.alpha);
  gl.clearDepth(d);
  gl.clearStencil(s);

  bindFramebuffer(this, framebuffer);
  gl.clear(bitmask);
};

ClearCommand在Scene中的调用:

初始化Scene时初始化ClearCommand

function Scene(options) {
  // ...
  this._clearColorCommand = new ClearCommand({
    color: new Color(),
    stencil: 0,
    owner: this,
  });
  // ...
}

执行更新时调用ClearCommandexecute()方法

Scene.prototype.updateAndExecuteCommands = function (passState, backgroundColor) {
    // ...
    updateAndClearFramebuffers(this, passState, backgroundColor);
    // ...
};
function updateAndClearFramebuffers(scene, passState, clearColor) {
  // ...
  // Clear the pass state framebuffer.
  const clear = scene._clearColorCommand;
  Color.clone(clearColor, clear.color);
  clear.execute(context, passState);
  // ...
}

4. DrawCommand

DrawCommand是最常用的指令,它是绘制的主角

DrawCommand封装如下,几乎包含了绘制所需要的全部内容:

function DrawCommand(options) {
  options = defaultValue(options, defaultValue.EMPTY_OBJECT);

  this._boundingVolume = options.boundingVolume;
  this._orientedBoundingBox = options.orientedBoundingBox;
  this._modelMatrix = options.modelMatrix;
  this._primitiveType = defaultValue(
    options.primitiveType,
    PrimitiveType.TRIANGLES
  );
  this._vertexArray = options.vertexArray;
  this._count = options.count;
  this._offset = defaultValue(options.offset, 0);
  this._instanceCount = defaultValue(options.instanceCount, 0);
  this._shaderProgram = options.shaderProgram;
  this._uniformMap = options.uniformMap;
  this._renderState = options.renderState;
  this._framebuffer = options.framebuffer;
  this._pass = options.pass;
  this._owner = options.owner;
  this._debugOverlappingFrustums = 0;
  this._pickId = options.pickId;
  // ...
}

DrawCommand.prototype.execute = function (context, passState) {
  context.draw(this, passState);
};

context.draw()执行WebGL的绘制指令:

Context.prototype.draw = function (drawCommand, passState, shaderProgram, uniformMap) {
  // ...
  beginDraw(this, framebuffer, passState, shaderProgram, renderState);
  continueDraw(this, drawCommand, shaderProgram, uniformMap);
};

function continueDraw(context, drawCommand, shaderProgram, uniformMap) {
  // ...
  va._bind();
  context._gl.drawArrays(primitiveType, offset, count);
  // ...
  va._unBind();
}

DrawCommand在Scene中的调用:

初始化Scene时初始化PrimitiveCollection

function Scene(options) {
  // ...
  this._primitives = new PrimitiveCollection();
  this._groundPrimitives = new PrimitiveCollection();
  // ...
}

执行更新时调用DrawCommandprimitives.update(frameState)()方法

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

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

function updateAndRenderPrimitives(scene) {
  // ...
  scene._groundPrimitives.update(frameState);
  scene._primitives.update(frameState);
  // ...
}

再来看看primitives.update(frameState)方法:

PrimitiveCollection.prototype.update = function (frameState) {
  const primitives = this._primitives;
  for (let i = 0; i < primitives.length; ++i) {
    primitives[i].update(frameState);
  }
};

Primitive.prototype.update = function (frameState) {
  // ...
  const updateAndQueueCommandsFunc = updateAndQueueCommands
  updateAndQueueCommandsFunc(...);
};

function updateAndQueueCommands(...) {
  // ...
  const commandList = frameState.commandList;
  const passes = frameState.passes;
    
  if (passes.render || passes.pick) {
    const colorLength = colorCommands.length;
    for (let j = 0; j < colorLength; ++j) {
      const colorCommand = colorCommands[j];
      // ...
      commandList.push(colorCommand);
    }
  }
}

primitives.update(frameState)方法会将Command推入CommandList,然后在Scene中执行execute()方法:

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) {
  // ...
  command.execute(context, passState);
  // ...
}

5. ComputeCommand

ComputeCommand需要配合ComputeEngine一起使用,可以将它认为是一个特殊的DrawCommand,通过渲染机制实现GPU的计算,通过Shader计算结果保存到纹理传出,实现在Web前端高效的处理大量的数值计算

ComputeCommand的构造函数如下:

function ComputeCommand(options) {
  options = defaultValue(options, defaultValue.EMPTY_OBJECT);

  this.vertexArray = options.vertexArray;
  this.fragmentShaderSource = options.fragmentShaderSource;
  this.shaderProgram = options.shaderProgram;
  this.uniformMap = options.uniformMap;
  this.outputTexture = options.outputTexture;
  this.preExecute = options.preExecute;
  this.postExecute = options.postExecute;
  this.canceled = options.canceled;
  this.persists = defaultValue(options.persists, false);
  this.pass = Pass.COMPUTE;
  this.owner = options.owner;
}

ComputeCommand.prototype.execute = function (computeEngine) {
  computeEngine.execute(this);
};

computeEngine.execute()方法使用DrawCommandClearCommand执行计算:

ComputeEngine.prototype.execute = function (computeCommand) {
  // ...
  computeCommand.preExecute(computeCommand);
  const outputTexture = computeCommand.outputTexture;
  const framebuffer = createFramebuffer(context, outputTexture);
  // ...
  clearCommand.execute(context);
  drawCommand.framebuffer = framebuffer;
  drawCommand.execute(context);
  framebuffer.destroy();
  computeCommand.postExecute(outputTexture);
};

ImageryLayer.js中重投影就使用了ComputeCommand

ImageryLayer.prototype._reprojectTexture = function (frameState, imagery, needGeographicProjection) {
    // ...
    const computeCommand = new ComputeCommand({
        persists: true,
        owner: this,
        // Update render resources right before execution instead of now.
        // This allows different ImageryLayers to share the same vao and buffers.
        preExecute: function (command) {
            reprojectToGeographic(command, context, texture, imagery.rectangle);
        },
        postExecute: function (outputTexture) {
            imagery.texture = outputTexture;
            that._finalizeReprojectTexture(context, outputTexture);
            imagery.state = ImageryState.READY;
            imagery.releaseReference();
        },
        canceled: function () {
            imagery.state = ImageryState.TEXTURE_LOADED;
            imagery.releaseReference();
        },
    });
    this._reprojectComputeCommands.push(computeCommand);
    // ...
};

6. 参考资料

[1] Cesium原理篇:6 Render模块(5: VAO&RenderState&Command) - fu*k - 博客园 (cnblogs.com)

[2] Cesium渲染模块之概述 - 当时明月在曾照彩云归 - 博客园 (cnblogs.com)

[3] Cesium渲染调度 - 当时明月在曾照彩云归 - 博客园 (cnblogs.com)

[4] CesiumJS 2022^ 源码解读 5 - 着色器相关的封装设计 - 岭南灯火 - 博客园 (cnblogs.com)

[5] Cesium教程系列汇总 - fu*k - 博客园 (cnblogs.com)