Cesium渲染模組之Command

當時明月在曾照彩雲歸發表於2023-03-14

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)

相關文章