VAO(Vertext Array Object),中文是頂點數組對象。以前在《Buffer》一文中,咱們介紹了Cesium如何建立VBO的過程,而VAO能夠簡單的認爲是基於VBO的一個封裝,爲頂點屬性數組和VBO中的頂點數據之間創建了關聯。咱們來看一下使用示例:前端
var indexBuffer = Buffer.createIndexBuffer({ context : context, typedArray : indices, usage : BufferUsage.STATIC_DRAW, indexDatatype : indexDatatype }); var buffer = Buffer.createVertexBuffer({ context : context, typedArray : typedArray, usage : BufferUsage.STATIC_DRAW }); // 屬性數組,當前是頂點數據z // 所以,該屬性有3個份量XYZ // 值類型爲float,4個字節 // 所以總共佔3 *4= 12字節 attributes.push({ index : 0, vertexBuffer : buffer, componentsPerAttribute : 3, componentDatatype : ComponentDatatype.FLOAT, offsetInBytes : 0, strideInBytes : 3 * 4, normalize : false }); // 根據屬性數組和頂點索引構建VAO var va = new VertexArray({ context : context, attributes : attributes, indexBuffer : indexBuffer });
如同,建立頂點數據和頂點索引的部分以前已經講過,而後將頂點數據添加到屬性數組中,並最終構建成VAO,使用方式很簡單。數組
function VertexArray(options) { var vao; // 建立VAO if (context.vertexArrayObject) { vao = context.glCreateVertexArray(); context.glBindVertexArray(vao); bind(gl, vaAttributes, indexBuffer); context.glBindVertexArray(null); } } function bind(gl, attributes, indexBuffer) { for ( var i = 0; i < attributes.length; ++i) { var attribute = attributes[i]; if (attribute.enabled) { // 綁定頂點屬性 attribute.vertexAttrib(gl); } } if (defined(indexBuffer)) { // 綁定頂點索引 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer._getBuffer()); } } attr.vertexAttrib = function(gl) { var index = this.index; // 以前經過Buffer建立的頂點數據_getBuffer gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer._getBuffer()); // 根據Attribute中的屬性值來設置以下參數 gl.vertexAttribPointer(index, this.componentsPerAttribute, this.componentDatatype, this.normalize, this.strideInBytes, this.offsetInBytes); gl.enableVertexAttribArray(index); if (this.instanceDivisor > 0) { context.glVertexAttribDivisor(index, this.instanceDivisor); context._vertexAttribDivisors[index] = this.instanceDivisor; context._previousDrawInstanced = true; } };
指定DrawCommand的渲染狀態,好比剔除,多邊形偏移,深度檢測等,經過RenderState統一管理:app
function RenderState(renderState) { var rs = defaultValue(renderState, {}); var cull = defaultValue(rs.cull, {}); var polygonOffset = defaultValue(rs.polygonOffset, {}); var scissorTest = defaultValue(rs.scissorTest, {}); var scissorTestRectangle = defaultValue(scissorTest.rectangle, {}); var depthRange = defaultValue(rs.depthRange, {}); var depthTest = defaultValue(rs.depthTest, {}); var colorMask = defaultValue(rs.colorMask, {}); var blending = defaultValue(rs.blending, {}); var blendingColor = defaultValue(blending.color, {}); var stencilTest = defaultValue(rs.stencilTest, {}); var stencilTestFrontOperation = defaultValue(stencilTest.frontOperation, {}); var stencilTestBackOperation = defaultValue(stencilTest.backOperation, {}); var sampleCoverage = defaultValue(rs.sampleCoverage, {}); }
前面咱們講了VBO/VAO,Texture,Shader以及FBO,終於萬事俱備只欠東風了,當咱們一切準備就緒,剩下的就是一個字:幹。Cesium中提供了三類Command:DrawCommand、ClearCommand以及ComputeCommand。咱們先詳細的講DrawCommand,同時也是最經常使用的。ide
var colorCommand = new DrawCommand({ owner : primitive, // TRIANGLES primitiveType : primitive._primitiveType }); colorCommand.vertexArray = primitive._va; colorCommand.renderState = primitive._rs; colorCommand.shaderProgram = primitive._sp; colorCommand.uniformMap = primitive._uniformMap; colorCommand.pass = pass;
如上是DrawCommand的建立方式,這裏只有兩個新的知識點,一個是owner屬性,記錄該DrawCommand是誰的菜,另一個是pass屬性。這是渲染隊列的優先級控制。目前,Pass的枚舉以下,具體內容下面後涉及:函數
var Pass = { ENVIRONMENT : 0, COMPUTE : 1, GLOBE : 2, GROUND : 3, OPAQUE : 4, TRANSLUCENT : 5, OVERLAY : 6, NUMBER_OF_PASSES : 7 };
建立完的DrawCommand會經過update函數,加載到frameState的commandlist隊列中,好比Primitive中update加載drawcommand的僞代碼:post
Primitive.prototype.update = function(frameState) { var commandList = frameState.commandList; var passes = frameState.passes; if (passes.render) { var colorCommand = colorCommands[j]; commandList.push(colorCommand); } if (passes.pick) { var pickLength = pickCommands.length; var pickCommand = pickCommands[k]; commandList.push(pickCommand); } }
進入隊列後就開始遵從安排,隨時準備上前線(渲染)。Scene會先對全部的commandlist會排序,Pass值越小優先渲染,經過Pass的枚舉能夠看到最後渲染的是透明的和overlay:學習
function createPotentiallyVisibleSet(scene) { for (var i = 0; i < length; ++i) { var command = commandList[i]; var pass = command.pass; // 優先computecommand,經過GPU計算 if (pass === Pass.COMPUTE) { computeList.push(command); } // overlay最後渲染 else if (pass === Pass.OVERLAY) { overlayList.push(command); } // 其餘command else { var frustumCommandsList = scene._frustumCommandsList; var length = frustumCommandsList.length; for (var i = 0; i < length; ++i) { var frustumCommands = frustumCommandsList[i]; frustumCommands.commands[pass][index] = command; } } } }
根據渲染優先級排序後,會先渲染環境相關的command,好比skybox,大氣層等,接着,開始渲染其餘command:this
function executeCommands(scene, passState) { // 地球 var commands = frustumCommands.commands[Pass.GLOBE]; var length = frustumCommands.indices[Pass.GLOBE]; for (var j = 0; j < length; ++j) { executeCommand(commands[j], scene, context, passState); } // 球面 us.updatePass(Pass.GROUND); commands = frustumCommands.commands[Pass.GROUND]; length = frustumCommands.indices[Pass.GROUND]; for (j = 0; j < length; ++j) { executeCommand(commands[j], scene, context, passState); } // 其餘非透明的 var startPass = Pass.GROUND + 1; var endPass = Pass.TRANSLUCENT; for (var pass = startPass; pass < endPass; ++pass) { us.updatePass(pass); commands = frustumCommands.commands[pass]; length = frustumCommands.indices[pass]; for (j = 0; j < length; ++j) { executeCommand(commands[j], scene, context, passState); } } // 透明的 us.updatePass(Pass.TRANSLUCENT); commands = frustumCommands.commands[Pass.TRANSLUCENT]; commands.length = frustumCommands.indices[Pass.TRANSLUCENT]; executeTranslucentCommands(scene, executeCommand, passState, commands); // 後面在渲染Overlay }
接着,就是對每個DrawCommand的渲染,也就是把以前VAO,Texture等等渲染到FBO的過程,這一塊Cesium也封裝的比較好,有興趣的能夠看詳細代碼,這裏只講一個邏輯,太困了。。。spa
DrawCommand.prototype.execute = function(context, passState) { // Contex開始渲染 context.draw(this, passState); }; Context.prototype.draw = function(drawCommand, passState) { passState = defaultValue(passState, this._defaultPassState); var framebuffer = defaultValue(drawCommand._framebuffer, passState.framebuffer); // 準備工做 beginDraw(this, framebuffer, drawCommand, passState); // 開始渲染 continueDraw(this, drawCommand); }; function beginDraw(context, framebuffer, drawCommand, passState) { var rs = defaultValue(drawCommand._renderState, context._defaultRenderState); // 綁定FBO bindFramebuffer(context, framebuffer); // 設置渲染狀態 applyRenderState(context, rs, passState, false); // 設置ShaderProgram var sp = drawCommand._shaderProgram; sp._bind(); } function continueDraw(context, drawCommand) { // 渲染參數 var primitiveType = drawCommand._primitiveType; var va = drawCommand._vertexArray; var offset = drawCommand._offset; var count = drawCommand._count; var instanceCount = drawCommand.instanceCount; // 設置Shader中的參數 drawCommand._shaderProgram._setUniforms(drawCommand._uniformMap, context._us, context.validateShaderProgram); // 綁定VAO數據 va._bind(); var indexBuffer = va.indexBuffer; // 渲染 if (defined(indexBuffer)) { offset = offset * indexBuffer.bytesPerIndex; // offset in vertices to offset in bytes count = defaultValue(count, indexBuffer.numberOfIndices); if (instanceCount === 0) { context._gl.drawElements(primitiveType, count, indexBuffer.indexDatatype, offset); } else { context.glDrawElementsInstanced(primitiveType, count, indexBuffer.indexDatatype, offset, instanceCount); } } va._unBind(); }
ClearCommand用於清空緩衝區的內容,包括顏色,深度和模板。用戶在建立的時候,指定清空的顏色值等屬性:prototype
function Scene(options) { // Scene在構造函數中建立了clearCommand this._clearColorCommand = new ClearCommand({ color : new Color(), stencil : 0, owner : this }); }
而後在渲染中更新隊列執行清空指令:
function updateAndClearFramebuffers(scene, passState, clearColor, picking) { var clear = scene._clearColorCommand; // 設置想要清空的顏色值,默認爲(1,0,0,0,) Color.clone(clearColor, clear.color); // 經過execute方法,清空當前FBO對應的幀緩衝區 clear.execute(context, passState); }
而後,會根據你設置的顏色,深度,模板值來清空對應的幀緩衝區,代碼好多啊,但很容易理解:
Context.prototype.clear = function(clearCommand, passState) { clearCommand = defaultValue(clearCommand, defaultClearCommand); passState = defaultValue(passState, this._defaultPassState); var gl = this._gl; var bitmask = 0; var c = clearCommand.color; var d = clearCommand.depth; var s = clearCommand.stencil; if (defined(c)) { if (!Color.equals(this._clearColor, c)) { Color.clone(c, this._clearColor); gl.clearColor(c.red, c.green, c.blue, c.alpha); } bitmask |= gl.COLOR_BUFFER_BIT; } if (defined(d)) { if (d !== this._clearDepth) { this._clearDepth = d; gl.clearDepth(d); } bitmask |= gl.DEPTH_BUFFER_BIT; } if (defined(s)) { if (s !== this._clearStencil) { this._clearStencil = s; gl.clearStencil(s); } bitmask |= gl.STENCIL_BUFFER_BIT; } var rs = defaultValue(clearCommand.renderState, this._defaultRenderState); applyRenderState(this, rs, passState, true); var framebuffer = defaultValue(clearCommand.framebuffer, passState.framebuffer); bindFramebuffer(this, framebuffer); gl.clear(bitmask); };
ComputeCommand須要配合ComputeEngine一塊兒使用,能夠認爲是一個特殊的DrawCommand,它不是爲了渲染,而是經過渲染機制,實現GPU的計算,經過Shader計算結果保存到紋理傳出的一個過程,實如今Web前端高效的處理大量的數值計算,下面,咱們經過學習以前ImageryLayer中對墨卡託影像切片動態投影的過程來了解該過程。
首先,建立一個ComputeCommand,定義這個計算過程前須要準備的內容,以及計算後對計算結果如何處理:
var computeCommand = new ComputeCommand({ persists : true, owner : this, // 執行前計算一下當前網格中插值點經緯度和墨卡託 // 並構建相關的參數,好比GLSL中的計算邏輯 // 傳入的參數,包括attribute和uniform等 preExecute : function(command) { reprojectToGeographic(command, context, texture, imagery.rectangle); }, // 執行後的結果保存在outputTexture postExecute : function(outputTexture) { texture.destroy(); imagery.texture = outputTexture; finalizeReprojectTexture(that, context, imagery, outputTexture); imagery.releaseReference(); } });
還記得Pass中的Compute枚舉吧,放在第一位,每次Scene.update時,發現有ComputeCommand都會優先計算,這個邏輯和DrawCommand同樣,都會在update中push到commandlist中,好比在ImageryLayer中,則是在
queueReprojectionCommands方法完成的,而具體的執行也和DrawCommand比較類似,稍微有一些特殊和針對的部分,具體代碼以下:
ComputeCommand.prototype.execute = function(computeEngine) { computeEngine.execute(this); }; ComputeEngine.prototype.execute = function(computeCommand) { if (defined(computeCommand.preExecute)) { // Ready? computeCommand.preExecute(computeCommand); } var outputTexture = computeCommand.outputTexture; var width = outputTexture.width; var height = outputTexture.height; // ComputeEngine是一個全局類,在Scene中能夠獲取 // 內部有一個Drawcommand // 把ComputeCommand中的參數賦給DrawCommand var drawCommand = drawCommandScratch; drawCommand.vertexArray = vertexArray; drawCommand.renderState = renderState; drawCommand.shaderProgram = shaderProgram; drawCommand.uniformMap = uniformMap; drawCommand.framebuffer = framebuffer; // Go! drawCommand.execute(context); if (defined(computeCommand.postExecute)) { // Over~ computeCommand.postExecute(outputTexture); } };
Renderer系列告一段落,並無涉及不少WebGL的語法層面,主要但願你們能對各個模塊的做用有一個瞭解,並在這個瞭解的基礎上,學習一下Cesium對WebGL渲染引擎的封裝技巧。經過這一系列,我的很佩服Cesium的開發人員對OpenGL渲染引擎的理解,在完成這一系列的過程當中,我的受益不淺,也但願能對各位起到一個分享和幫助。
基於功能的面向函數的接口,封裝成基於狀態管理的面向對象的封裝,方便了咱們的使用和管理。但從中咱們仍是能夠看到,WebGL在某些方面的薄弱,好比實例化和FBO的部分功能須要在WebGL2.0的規範下才支持,固然對此,我表示樂觀,我感覺到了WebGL標準化的快速發展。
另外,我也想到了用Three.js封裝Cesium渲染引擎的可能,固然我對Three.js不瞭解,但隨着不斷學習Cesium。Renderer,我我的並不喜歡這個想法。我以爲在設計和封裝上,Renderer已經很不錯了,咱們能夠借鑑Three.js在功能和易用性上的特色,強化Cesium,而不是全盤否認從新造輪子。並且並不能由於點上的優點而進行面上的推倒,若是對這兩個引擎都不瞭解,最好仍是埋頭學習少一點高談闊論。基本功是頓悟不出來的。