Cesium原理篇:6 Renderer模塊(1: Buffer)


       剛剛結束完地球切片的渲染調度后,打算介紹一下目前大家都很關注的3D Tiles方面的內容,但發現要講3D Tiles,或者充分理解它,需要對DataSource,Primitive要有基礎,而這要求對最底層的渲染模塊,也就是Render有一個了解,真的是書到用時方恨少的代入感,索性從頭到尾的說清楚。所以,下面幾篇(估算四篇)先把Render模塊介紹清楚。

       Render,顧名思義就是渲染模塊,在Cesium中,所有的對象,都是通過Render模塊完成最終調用WebGL的渲染過程。換句話說,Render是Cesium的渲染引擎模塊,在Cesium的Geometry和WebGL之間搭建了一個橋梁。至於為什么不直接調用WebGL接口,還需要封裝一套,這樣做有何意義呢,這就是一個比較發散的問題了。首先WebGL提供的是函數,而在真正的渲染中,會有很多狀態的切換,而且隨着邏輯的負責而難以維護,而面向對象的思想的精髓就在於對對象狀態的管理,這也是為什么很多三維應用都提供了自己的渲染引擎模塊,個人覺得主要的價值在於易用性和狀態的管理。

       首先,目前主流的瀏覽器基本支持WebGL 1.0的標准,對應的是OpenGL ES2.0,也就是可編程渲染管線,采用GPU渲染,性能上有保證。據我了解,只有FireFox的一些實驗版本支持WebGL2.0,也就是OpenGL ES3.0的標准。如果對WebGL的基本接口還不熟悉,推薦這本《WebGL編程指南》,除了感覺價格略貴外,還是一本很不錯的入門書。我們的重點是學習Cesium如何設計並封裝WebGL接口,並不會過多糾結在WebGL本身的理解,所以假設你對WebGL有一個基本的了解。

Buffer

       任意一個非參數化的幾何對象(參數化的可以通過差值轉為非參數化,比如一個參數化的圓,對應的是圓心和半徑,我們通過差值,將該圓轉為一個多邊形,圓周對應的點越密,則效果越好,而代價也是點數據越大),對應的就是N個頂點(Node)之間的空間關系。當我們想要通過WebGL渲染該幾何對象時,首先就是要講該幾何對象轉化為WebGL可以識別的數據格式:1構建該對象的頂點數組,里面包括每一個點的XYZ位置(必須),該點的顏色,紋理坐標,法線等信息;2構建該對象對應的索引信息,也就是點之間的先后順序。

       上述說的就是一個VBO(頂點緩存)的概念,WebGL提供了bufferData接口,我們對其中的參數做一個簡單介紹:

void gl.bufferData(target, size, usage);
void gl.bufferData(target, data, usage);
        
  • target
    gl.ARRAY_BUFFER 該數據為頂點數據,比如位置信息,顏色,法線,紋理坐標或自定義的一些屬性信息
    gl.ELEMENT_ARRAY_BUFFER 該數據為頂點索引
  • size
    數據長度,此時只是預留了對應長度的空間,里面的數據為空
  • data
    數據內容,此時WebGL內部可以判斷該數據內容對應的長度
  • usage
    gl.STATIC_DRAW 靜態數據,數據保存在GPU中,適合一次寫入不再更改,多次使用情況
    gl.DYNAMIC_DRAW 動態數據,保存在內存中,適合多次寫入,多次使用下調用情況
    gl.STREAM_DRAW 流數據,保存在GPU中,適合一次寫入一次使用的情況

       如下是直接調用WebGL提供的方法創建頂點屬性的代碼:

// 獲取WebGL對象
var canvas = document.getElementById("canvas");
var gl = canvas.getContext("webgl");
// 創建頂點緩存
var buffer = gl.createBuffer();
// 綁定該頂點緩存類型為頂點屬性數據
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
// 指定其對應的數據長度及方式
gl.bufferData(gl.ARRAY_BUFFER, 1024, gl.STATIC_DRAW);
// 解除綁定
gl.bindBuffer(bufferTarget, null);

      通過上面的介紹,可見頂點屬性和頂點索引的調用方法完全相同,邏輯上的創建過程也如出一轍,只是具體的參數稍有不同,因此,在Cesium中把創建VBO的過程化的函數封裝為一個抽象的Buffer類,偽代碼如下:

function Buffer(options) {
    var gl = options.context._gl;
    var bufferTarget = options.bufferTarget;
    var typedArray = options.typedArray;
    var sizeInBytes = options.sizeInBytes;
    var usage = options.usage;
    var hasArray = defined(typedArray);

    if (hasArray) {
        sizeInBytes = typedArray.byteLength;
    }
    // ……
    var buffer = gl.createBuffer();
    gl.bindBuffer(bufferTarget, buffer);
    gl.bufferData(bufferTarget, hasArray ? typedArray : sizeInBytes, usage);
    gl.bindBuffer(bufferTarget, null);
    // ……
    this._gl = gl;
    this._bufferTarget = bufferTarget;
    this._sizeInBytes = sizeInBytes;
    this._usage = usage;
    this._buffer = buffer;
    this.vertexArrayDestroyable = true;
}

       可見,這個過程和剛才WebGL調用的方式幾乎一模一樣,只是把所需要的參數都封裝了一下,並將調用函數的返回值,作為屬性保存在該Buffer對象中。於是,一個過程式的函數調用封裝成了一個Ojbect,即可以重用,也方便內部細節的管理。

       當然,如果只有如上的一個方法也能用,但輸入的參數不太少,只是一個很簡單的函數封裝而已,使用上並沒有太多簡化,而且也不方便理解,只能算是一個半成品。接着Cesium在Buffer類中提供了Buffer.createVertexBuffer和Buffer.createIndexBuffer方法,算是基於Buffer的一個二次加工:

Buffer.createVertexBuffer = function(options) {
    return new Buffer({
        context: options.context,
        bufferTarget: WebGLConstants.ARRAY_BUFFER,
        typedArray: options.typedArray,
        sizeInBytes: options.sizeInBytes,
        usage: options.usage
    });
};
    
Buffer.createIndexBuffer = function(options) {
    return new Buffer({
        context: options.context,
        bufferTarget : WebGLConstants.ELEMENT_ARRAY_BUFFER,
        typedArray : options.typedArray,
        sizeInBytes : options.sizeInBytes,
        usage : options.usage
    });
};

       這下用起來就so easy了,用戶創建頂點屬性和頂點索引的范例如下:

var buffer = Buffer.createVertexBuffer({
  context : context,
  sizeInBytes : 16,
  usage : BufferUsage.DYNAMIC_DRAW
});
     
var buffer = Buffer.createIndexBuffer({
  context : context,
  typedArray : new Uint16Array([0, 1, 2]),
  usage : BufferUsage.STATIC_DRAW,
  indexDatatype : IndexDatatype.UNSIGNED_SHORT
});

       當然,如果你此時是用的DYNAMIC_DRAW的方式,並沒有指定bufferdata,則創建該頂點緩存對象,但數據還是空的,在獲取數據后可以調用Buffer.prototype.copyFromArrayView方法更新一下該數據。可見,Buffer類的設計還是比較周全的,考慮到數據更新可能存在這種不確定的邏輯:

Buffer.prototype.copyFromArrayView = function(arrayView, offsetInBytes) {
    var gl = this._gl;
    var target = this._bufferTarget;
    gl.bindBuffer(target, this._buffer);
    gl.bufferSubData(target, offsetInBytes, arrayView);
    gl.bindBuffer(target, null);
};

       如上就是創建VBO的一個完整過程,可以仔細對比一下兩者之間的不同。因為VBO是渲染中最基本的,也是最重要的概念和渲染對象,通過Buffer類對這個過程進行分裝和管理,雖然難度不大,但意義卻很重要。

       通過Buffer,我們可以將Primitive(圖元)中的幾何數據轉化為VBO,這相當於創建了該幾何對象的骨架。通常我們還需要紋理信息,貼在這個骨架的表面,讓它看上去有血有肉,惟妙惟肖。下一篇我們介紹Cesium是如何封裝Texture。


免責聲明!

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



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