cesium 原理 之 command拼接

VAO

       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;
    }
};
複製代碼

RenderState

       指定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, {});
}
複製代碼

Drawcommand

       前面咱們講了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

       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

       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,而不是全盤否認從新造輪子。並且並不能由於點上的優點而進行面上的推倒,若是對這兩個引擎都不瞭解,最好仍是埋頭學習少一點高談闊論。基本功是頓悟不出來的。

相關文章
相關標籤/搜索