Cesium原理篇:6 Render模塊(6: Instance實例化)【轉】

https://www.cnblogs.com/fuckgiser/p/6027520.htmlhtml

      最近研究Cesium的實例化,儘管該技術須要在WebGL2.0,也就是OpenGL ES3.0才支持。調試源碼的時候眼前一亮,發現VAO和glDrawBuffers都不是WebGL1.0的標準函數,都是擴展功能,看來WebGL2.0標準的推廣勢在必行啊。同時發現,經過ANGLE_instanced_arrays的擴展,也能夠在WebGL1.0下實現實例化,建立實例化方法的代碼以下:web

複製代碼
var glDrawElementsInstanced; 
var glDrawArraysInstanced; 
var glVertexAttribDivisor; 
var instancedArrays; 
// WebGL2.0標準直接提供了實例化接口 
if (webgl2) { 
    glDrawElementsInstanced = function(mode, count, type, offset, instanceCount) {
         gl.drawElementsInstanced(mode, count, type, offset, instanceCount); 
    }; 
    glDrawArraysInstanced = function(mode, first, count, instanceCount) { 
        gl.drawArraysInstanced(mode, first, count, instanceCount); 
    }; 
    glVertexAttribDivisor = function(index, divisor) { 
        gl.vertexAttribDivisor(index, divisor); 
    }; 
} else { 
    // WebGL1.0下 
    // 擴展ANGLE_instanced_arrays 
    instancedArrays = getExtension(gl, ['ANGLE_instanced_arrays']); 
    if (defined(instancedArrays)) { 
        glDrawElementsInstanced = function(mode, count, type, offset, instanceCount) {
            instancedArrays.drawElementsInstancedANGLE(mode, count, type, offset, instanceCount); 
        }; 
        glDrawArraysInstanced = function(mode, first, count, instanceCount) { 
            instancedArrays.drawArraysInstancedANGLE(mode, first, count, instanceCount); 
        }; 
        glVertexAttribDivisor = function(index, divisor) {
            instancedArrays.vertexAttribDivisorANGLE(index, divisor); 
        }; 
    } 
} 
// 涉及到實例化的三個方法 
this.glDrawElementsInstanced = glDrawElementsInstanced; 
this.glDrawArraysInstanced = glDrawArraysInstanced; 
this.glVertexAttribDivisor = glVertexAttribDivisor; 
this._instancedArrays = !!instancedArrays;
複製代碼

       經過這樣的封裝,Cesium.Context提供了標準的實例化方法,不須要用戶過多的關心WebGL標準的差別。而實例化的渲染也很是簡單,核心代碼以下:閉包

複製代碼
functioncontinueDraw(context, drawCommand) { 
    // …… 
    var instanceCount = drawCommand.instanceCount; 
    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);
        }
    } else { 
        count = defaultValue(count, va.numberOfVertices); 
        if (instanceCount === 0) { 
            context._gl.drawArrays(primitiveType, offset, count); 
        } else { 
        context.glDrawArraysInstanced(primitiveType, offset, count, instanceCount); 
        } 
    } 
    // ……
}
複製代碼

       是否實例化渲染,取決於你所構造的DrawCommand是否有實例化的信息,對應代碼中的drawCommand.instanceCount,若是你的實例化數目不爲零,則進行實例化的渲染。所以,Context中對實例化進行了封裝,內部的渲染機制中,實例化和非實例化的渲染機制差異並不大。從應用的角度來看,咱們並不須要關心Context的實現,而是經過構造DrawCommand來決定是否想要實例化渲染。app

       以前咱們較詳細的介紹過Renderer.DrawCommand模塊,若是不清楚的回去再翻翻看,在VertexArray中實現了VAO中建立attr.vertexAttrib,這裏有一個instanceDivisor屬性,這就是用來表示該attribute是不是實例化的divisor:ide

複製代碼
attr.vertexAttrib = function(gl) { 
    var index = this.index; 
    gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer._getBuffer()); 
    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;
    }
};
複製代碼

       根據OpenGL的定義,glVertexAttribDivisor在設置多實例渲染時,位於index位置的頂點着色器中頂點屬性是如何分配值到每一個實例的。instanceDivisor若是是0,那該屬性的多實例特性將被禁用,其餘值則表示頂點着色器中,每instanceDivisor個實力會分配一個新的屬性值。函數

       可見,對於一個DrawCommand,實例化有三處特別的地方,一個是attribute的instanceDivisor屬性,用來肯定實例化的頻率,一個是instanceCount,實例化的個數,最後一個,固然是頂點着色器了,attribute屬性傳到頂點着色器了,你得用纔有效果啊。由於實例化的自由度很高,因此多數狀況下須要你本身寫。性能

       固然,目前Cesium用實例化的地方很少,只有BillboardCollection和3D Tiles中用到了,提供了完整的實現方法,不妨看看Cesium本身的調用方式,學習一下Cesium中如何使用實例化的。學習

ModelInstanceCollection

      咱們先看一下3D Tiles中實例化的實現方式,這個較爲簡單,由於3D Tiles中的數據都是預處理的,能夠直接加載,另外由於模型是gltf,自帶Shader,不須要過多的邏輯判斷。優化

QQ截圖20161104141959

 

       上面是一個3D Tiles實例化的效果圖,可見除了位置不一樣,其餘的都一致。實例化正是避免相同屬性之間的內存和顯存的調度,同時對不一樣的屬性的調度進行優化,從而提升渲染效率。咱們先看一下數據處理的代碼:webgl

複製代碼
functiongetVertexBufferData(collection, context, result) { 
    var instances = collection._instances; 
    var instancesLength = collection.length; 
    var collectionCenter = collection._center; 
    var vertexSizeInFloats = 12; 
    if (!defined(result)) { 
        result = new Float32Array(instancesLength * vertexSizeInFloats); 
    } 
    for (var i = 0; i < instancesLength; ++i) { 
        var modelMatrix = instances[i].modelMatrix; 

        // Instance matrix is relative to center 
        var instanceMatrix = Matrix4.clone(modelMatrix, scratchMatrix); 
        instanceMatrix[12] -= collectionCenter.x; 
        instanceMatrix[13] -= collectionCenter.y; 
        instanceMatrix[14] -= collectionCenter.z; 
        var offset = i * vertexSizeInFloats; 

        // First three rows of the model matrix 
        result[offset + 0] = instanceMatrix[0]; 
        result[offset + 1] = instanceMatrix[4]; 
        result[offset + 2] = instanceMatrix[8]; 
        result[offset + 3] = instanceMatrix[12]; 
        result[offset + 4] = instanceMatrix[1]; 
        result[offset + 5] = instanceMatrix[5]; 
        result[offset + 6] = instanceMatrix[9]; 
        result[offset + 7] = instanceMatrix[13]; 
        result[offset + 8] = instanceMatrix[2]; 
        result[offset + 9] = instanceMatrix[6]; 
        result[offset + 10] = instanceMatrix[10]; 
        result[offset + 11] = instanceMatrix[14]; 
    }

    return result;
}
複製代碼

       代碼有點長,但不難理解,instancesLength是要進行實例化的實例個數,collectionCenter則是這些實例Collection的中心點,之前這些實例中都保存的是相對球心的模型矩陣,這樣構建instancesLength個DrawCommand,最終渲染到FBO中。但發現,這些實例基本同樣啊,之前是一筆一劃的渲染出來,否則先弄一個印章,而後啪啪啪的蓋在不一樣的位置就能夠了,這樣多快啊。因此如今要對他進行實例化的改造。這樣,當我在collectionCenter位置構造了一個實例(印章),大家告訴我距離中心點的偏移量,我就知道在哪裏直接「蓋」這個實例了。因此,數據上,咱們須要把這個矩陣改成相對collectionCenter的,getVertexBufferData就是作這個事情。接着在createVertexBuffer中咱們將這個矩陣數據構建成一個VertexBuffer:

複製代碼
function createVertexBuffer(collection, context) { 
    var vertexBufferData = getVertexBufferData(collection, context); 
    collection._vertexBuffer = Buffer.createVertexBuffer({ 
        context : context, 
        typedArray : vertexBufferData, usage : dynamic ?  BufferUsage.STREAM_DRAW : BufferUsage.STATIC_DRAW
    });
}
複製代碼

        這樣,當咱們建立好適合實例化的VertexBuffer後,就能夠封裝實例化的屬性:

複製代碼
function createModel(collection, context) { 
    var instancingSupported = collection._instancingSupported; 
    var modelOptions; 
    if (instancingSupported) { 
        createVertexBuffer(collection, context); 
        var instancedAttributes = { 
            czm_modelMatrixRow0 : { 
                index : 0, // updated in Model 
                vertexBuffer : collection._vertexBuffer, 
                componentsPerAttribute : 4, 
                componentDatatype : ComponentDatatype.FLOAT, 
                normalize : false, 
                offsetInBytes : 0, 
                strideInBytes : componentSizeInBytes * vertexSizeInFloats, 
                instanceDivisor : 1 
               },
            czm_modelMatrixRow1 : {
                index : 0, // updated in Model 
                vertexBuffer : collection._vertexBuffer, 
                componentsPerAttribute : 4, 
                componentDatatype : ComponentDatatype.FLOAT, 
                normalize : false, 
                offsetInBytes : componentSizeInBytes * 4, 
                strideInBytes : componentSizeInBytes * vertexSizeInFloats, 
                instanceDivisor : 1
               },    
            czm_modelMatrixRow2 : {
                index : 0, // updated in Model 
                vertexBuffer : collection._vertexBuffer, 
                componentsPerAttribute : 4, 
                componentDatatype : ComponentDatatype.FLOAT, 
                normalize : false, 
                offsetInBytes : componentSizeInBytes * 8, 
                strideInBytes : componentSizeInBytes * vertexSizeInFloats, 
                instanceDivisor : 1 
            }
           };
        modelOptions.precreatedAttributes = instancedAttributes; 
    } 

    collection._model = new Model(modelOptions);
}
複製代碼

       這裏稍微有一些麻煩,將矩陣分解爲3個vec4的attribute,分別對應czm_modelMatrixRow0~2,這裏能夠看到,每個實例化的屬性中,instanceDivisor值爲1,也就是一個實例更新一次,正好對應每個實例的偏移量。 最後構形成Model(_precreatedAttributes)。

      該Model實際上就是一個實例集合,在Model.update()中調用createVertexArrays方法建立VAO。這樣完成了一個arraybuffer(內存)->VertexBuffer(顯存)->Attributes->VertexArray的整個過程。最後綁定到DrawCommand中進行渲染。整個流程大概以下:

複製代碼
ModelInstanceCollection.prototype.update = function(frameState) { 
    if (this._state === LoadState.NEEDS_LOAD) { 
        this._state = LoadState.LOADING; 
        this._instancingSupported = context.instancedArrays; 
        // 數據處理,符合實例化的須要 
        createModel(this, context); 
    } 

    var model = this._model; 
    // 建立VAO 
    model.update(frameState); 
    if (instancingSupported) { 
        // 構造最終的DrawCommand 
        // 指定instanceCount 
        // 綁定VertexArray 
        createCommands(this, modelCommands.draw, modelCommands.pick);
    }
}
複製代碼

       這樣,一個實例化的DrawCommand完成,之前須要Count個DrawCommand渲染的過程,只須要一個DrawCommand一次性渲染instanceCount個實例便可。固然,這裏沒有給出3D Instance Tiles的頂點着色器代碼,只好本身想象這樣一個轉換代碼:a_position爲原點,經過czm_modelMatrixRow0,czm_modelMatrixRow1,czm_modelMatrixRow2三個相對原點的偏移矩陣構造出czm_instanced_modelView模型試圖矩陣,最終結合投影矩陣計算出gl_Position。

Billboard:

       這裏主要介紹Billboard中實例化的設計和封裝,至於Billboard的整個過程,咱們後續在介紹DataSource模塊時再詳細介紹。首先,咱們要本身明白,對Billboard進行實例化渲染的意義在哪裏,在目標明確的基礎下,咱們才能總結這些實例之間的共同出和不一樣點,方能更好的設計:哪些屬性須要實例化,哪些屬性不須要。

       你們能夠本身思考一下,再往下看。由於這個涉及到對Billboard的理解,本篇主要集中在實例化上面,因此,直接給出Cesium的設計。

複製代碼
var attributeLocationsInstanced = { 
    direction : 0, 
    positionHighAndScale : 1, 
    positionLowAndRotation : 2, 
    // texture offset in w 
    compressedAttribute0 : 3, 
    compressedAttribute1 : 4, 
    compressedAttribute2 : 5, 
    eyeOffset : 6, 
    // texture range in w 
    scaleByDistance : 7, 
    pixelOffsetScaleByDistance : 8 
};
複製代碼

       如上是Cesium中Billboard須要的attribute屬性,對每個實例而言,direction都是同樣的,而其餘八個屬性則不一樣。direction是公告板的四個頂點的相對位置(比例),對於全部公告板,這四個頂點之間的相對位置是同樣的,就比如一個印章,你只須要縮放一下相對位置就能夠改變總體大小,移動一下位置就能夠改變總體位置,旋轉也是如此。不管怎麼變,Billboard的樣式都不會走樣。所以, 在BillboardCollection中,會默認建立惟一的公告板的direction:

複製代碼
functiongetIndexBufferInstanced(context) { 
    var indexBuffer = context.cache.billboardCollection_indexBufferInstanced; 
    if (defined(indexBuffer)) { 
        return indexBuffer; 
    } 
    indexBuffer = Buffer.createIndexBuffer({ 
        context : context, 
        typedArray : new Uint16Array([0, 1, 2, 0, 2, 3]), 
        usage : BufferUsage.STATIC_DRAW, 
        indexDatatype : IndexDatatype.UNSIGNED_SHORT 
    }); 
    indexBuffer.vertexArrayDestroyable = false; 
    context.cache.billboardCollection_indexBufferInstanced = indexBuffer; 
    return indexBuffer; 
} 

function getVertexBufferInstanced(context) { 
    var vertexBuffer = context.cache.billboardCollection_vertexBufferInstanced; 
    if (defined(vertexBuffer)) { 
        return vertexBuffer; 
    } 

    vertexBuffer = Buffer.createVertexBuffer({ 
        context : context, 
        typedArray : new Float32Array([0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0]), 
        usage : BufferUsage.STATIC_DRAW 
    }); 

    vertexBuffer.vertexArrayDestroyable = false; 
    context.cache.billboardCollection_vertexBufferInstanced = vertexBuffer; 
    return vertexBuffer;
}
複製代碼

       如上,你們能夠想象一個矩形(Billboard),中間畫一條對角線分紅了兩個相接的三角形,小學幾何裏面說過三角形的穩定性,所以,該矩形經過兩個三角形確保了樣式不變。咱們先看看VertexBuffer,頂點數據爲:[0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0],也就是Billboard的四個頂點,頂點索引爲[0, 1, 2, 0, 2, 3],把四個點分紅了兩個三角形(0, 1, 2,)和(0, 2, 3)。這樣,咱們經過indexBuffer和vertexBuffer,構建了一個Billboard樣式,並將它保存在context.cache下,分別是billboardCollection_indexBufferInstanced和billboardCollection_vertexBufferInstanced,做爲一個全局的單例。

       就比如百米決賽,每一個運動員都在高速奔跑,而攝像機也須要實時調整位置,保持一個最佳角度捕捉運動員的動做。一個公告板的的樣式肯定了,但在不一樣的位置,角度以及公告報的大小,每一個Billboard在不一樣的位置,這些屬性都會不一樣。所以這些屬性就是須要實例化的部分,而且這些屬性值(Buffer)須要實時的更新。

複製代碼
createVAF(context, numberOfBillboards, buffersUsage, instanced) { 
    // 須要實例化的屬性 
    var attributes = [
    { 
        index : attributeLocations.positionHighAndScale, 
        componentsPerAttribute : 4, 
        componentDatatype : ComponentDatatype.FLOAT, 
        usage : buffersUsage[POSITION_INDEX] 
    }, 
    { 
        index : attributeLocations.positionLowAndRotation, 
        componentsPerAttribute : 4, 
        componentDatatype : ComponentDatatype.FLOAT, 
        usage : buffersUsage[POSITION_INDEX] 
    }, 
    // …… 
    { 
        index : attributeLocations.pixelOffsetScaleByDistance, 
        componentsPerAttribute : 4, 
        componentDatatype : ComponentDatatype.FLOAT, 
        usage : buffersUsage[PIXEL_OFFSET_SCALE_BY_DISTANCE_INDEX] 
    }]; 
    // direction不須要實例化 
    if (instanced) { 
        attributes.push({ 
            index : attributeLocations.direction, 
            componentsPerAttribute : 2, 
            componentDatatype : ComponentDatatype.FLOAT, 
            vertexBuffer : getVertexBufferInstanced(context) 
        }); 
    } 
    // 計算須要實例化的個數 
    // 也就是Billboard的個數 
    var sizeInVertices = instanced ? numberOfBillboards : 4 * numberOfBillboards; 
    return new VertexArrayFacade(context, attributes, sizeInVertices, instanced);
}
複製代碼

       createVAF建立告終構體attributeLocationsInstanced所須要的全部屬性,也是渲染每個Billboard實例時,在頂點着色器中須要的attribute屬性,這裏主要有三個關鍵點:(1)只有direction屬性建立了vertexBuffer,而其餘八個屬性是空的,須要實時的更新屬性值,也是須要實例化的屬性;(2)肯定了instanceCount,也就是sizeInVertices;(3)最終Billboard全部attribute屬性(實例化和不須要實例化的direction)都交給了VertexArrayFacade。前兩點都很明確,如今就看VertexArrayFacade到底幹了什麼。

       仍是先思考一下,attribute都已經準備好了,下來應該是CreateVertexArray的過程了,而這裏有兩處不一樣,第一,實例化所須要的八個屬性並無VertexBuffer,須要一個機制:(1)對全部實例更新這八個屬性值(2)屬性中有須要實例化的,須要在attribute中標識instanceDivisor屬性爲true,而direction則不須要實例化。所以不難理解,VertexArrayFacade就是BillboardCollection和VertexArray之間的一個過渡,用來解決上面的兩個問題。

複製代碼
functionVertexArrayFacade(context, attributes, sizeInVertices, instanced) { 
    var attrs = VertexArrayFacade._verifyAttributes(attributes); 
    var length = attrs.length; 
    for (var i = 0; i < length; ++i) { 
        var attribute = attrs[i]; 
        // 若是存在vertexBuffer,好比direction屬性 
        // 則不須要實時更新屬性值 
        // 放到precreatedAttributes,能夠直接用 
        if (attribute.vertexBuffer) { 
            precreatedAttributes.push(attribute); continue; 
        } 

        // 沒有vertexBuffer的 
        // 則放到attributesForUsage 
        // 後面對這些屬性進行賦值 
        usage = attribute.usage; 
        attributesForUsage = attributesByUsage[usage]; 
        if (!defined(attributesForUsage)) { 
            attributesForUsage = attributesByUsage[usage] = [];
        }

        attributesForUsage.push(attribute);
    }
}
複製代碼

       如上對attribute根據是否須要實例化,進行了區分。而後在渲染時,在更新隊列中更新數據:

複製代碼
BillboardCollection.prototype.update = function(frameState) { 
    if (billboardsLength > 0) { 
        // 建立Attribute屬性 
        this._vaf = createVAF(context, billboardsLength, this._buffersUsage, this._instanced); 
        vafWriters = this._vaf.writers; 
        // 數據有更新時,須要重寫實例化的屬性值 
        for (var i = 0; i < billboardsLength; ++i) { 
            var billboard = this._billboards[i]; 
            billboard._dirty = false; 
            writeBillboard(this, context, textureAtlasCoordinates, vafWriters, billboard); 
        } 
        // 建立實例化的VAO,這裏使用同一個頂點索引,也就是用一個相同的樣式 
        this._vaf.commit(getIndexBuffer(context));
    }
}
複製代碼

       這裏,writeBillboard經過vafWriters方法,將實例化的屬性值寫入到arraybuffer中,這裏就不詳細介紹過程了。簡單說就三個過程:建立,寫,提交。 首先在VertexArrayFacade初始化中,最終會調用_resize,這裏雖然並不知道實例化attribute屬性的值,但所佔內存的大小是明確的,因此會在內存中建立一個屬性值均爲0的arraybuffer。而後在createWriters中實現了寫的方法,VertexArrayFacade經過閉包的方式綁定到writers屬性中,BillboardCollection中對應:vafWriters = this._vaf.writers,實現屬性值的寫操做。最後,經過commit提交,建立VAO,將內存中的Buffer傳遞到顯存中。

複製代碼
VertexArrayFacade.prototype.commit = function(indexBuffer){ 
    for (i = 0, length = allBuffers.length; i < length; ++i) { 
        buffer = allBuffers[i]; 
        // 建立VertexBuffer 
        // 將寫到arraybuffer中的屬性值綁定到顯存中 
        recreateVA = commit(this, buffer) || recreateVA; 
    } 
    // 建立attribute,指定實例化屬性 
    // instanceDivisor : instanced ? 1 : 0 
    VertexArrayFacade._appendAttributes(attributes, buffer, offset, this._instanced); 

    // 添加以前已經建立好的非實例化的attribute 
    attributes = attributes.concat(this._precreated); 

    // 建立VAO 
    va.push({ 
        va : new VertexArray({ 
            context : this._context, 
            attributes : attributes, indexBuffer : indexBuffer 
        }), 
        indicesCount : 1.5 * ((k !== (numberOfVertexArrays - 1)) ? (CesiumMath.SIXTY_FOUR_KILOBYTES - 1) : (this._size % (CesiumMath.SIXTY_FOUR_KILOBYTES - 1)))
    });
}
複製代碼

       如上,完成了BillboardCollection中VAO的建立。最後一步,就是頂點着色器中如何使用這些屬性,這裏主要看一個思路,看一下實例化和非實例化之間的區別,以及如何配合:

複製代碼
vec4 computePositionWindowCoordinates(vec4 positionEC, vec2 imageSize, float scale, vec2 direction, vec2 origin, vec2 translate, vec2 pixelOffset, vec3 alignedAxis, bool validAlignedAxis, float rotation, bool sizeInMeters) 
{ 
    vec2 halfSize = imageSize * scale * czm_resolutionScale; 
    // 經過direction,判斷當前的頂點位於四個頂點中的哪個 
    // 左上,左下,右下,右上? 
    // 全部實例的direction都是一致的,所以該屬性不須要實例化 

    halfSize *= ((direction * 2.0) - 1.0); 

    // 下面根據實例化的屬性來計算該點的真實位置
    ……
}
複製代碼

總結

       實例化是一個強大功能,但性能的提高每每須要跟數據緊密聯繫,須要有一個數據規範的前提,因此Cesium目前對實例化應用的地方並很少,但即便這樣,Cesium也意識到必要性,即便WebGL1.0規範並不支持的狀況下,也經過擴展的方式來支持。固然,最重要的是可以學習到Cesium對實例化的封裝和應用,以及如何理解,哪些不一樣的attribute須要實例化。

相關文章
相關標籤/搜索