剛剛結束完地球切片的渲染調度后,打算介紹一下目前大家都很關注的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。