Cesium原理篇:6 Render模塊(5: VAO&RenderState&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統一管理:

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,同時也是最常用的。

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的偽代碼:

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:

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也封裝的比較好,有興趣的可以看詳細代碼,這里只講一個邏輯,太困了。。。

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用於清空緩沖區的內容,包括顏色,深度和模板。用戶在創建的時候,指定清空的顏色值等屬性:

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,而不是全盤否定重新造輪子。而且並不能因為點上的優勢而進行面上的推倒,如果對這兩個引擎都不了解,最好還是埋頭學習少一點高談闊論。基本功是頓悟不出來的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM