Cesium原理篇:glTF


關鍵字:Cesium glTF WebGL技術

大綱:

1 glTF簡介,這是一個什么東西,有哪些特點

2 Cesium如何加載,渲染glTF,邏輯結構和關鍵技術

3 個人總結,從glTF學習如何設計一個二進制格式,個人想法分享

共計 4000字 | 建議閱讀時間 未知

1 glTF簡介

       之前介紹了Cesium的Property,Material,Batch,GroundPrimitive這些內容,可以說是簡單地物和風格的解決思路。當Cesium把這些技術點整合起來,我們便具備了渲染模型的威力。也就是今天要講的glTF模型渲染。

       glTF的全稱是GL傳輸格式,是一種針對GL(WebGL,OpenGL ES以及OpenGL)接口的運行時資產(asset)。在3D內容的傳輸和加載中,glTF通過提供一種高效,易擴展,可協作的格式,填補了3D建模工具和現代GL應用之間的空白。github上有對該數據規范的詳細介紹,春節期間我翻譯了其中的核心部分,有興趣的可以了解

      三維模型的格式這么多,為什么不用現成了,而是非要自己重新定義一個規范,原文中Patrick很誠實詳盡的做了解釋。簡單說,目前主流的三維模型主要的特點在於數據制作上,在Web傳輸和解析上無法滿足需求,而glTF的特點就是傳輸和解析的高效。首先,二進制的傳輸方式最為高效,也就是ArrayBuffer形式,但二進制的解析則很繁瑣,也很容易出錯。如何提高解析效率呢,就如同雲盤的秒傳功能,不解析,或減少解析是最有效的解析方式, 要做到這一點,則要求顯卡,WebGL能夠直接加載該數據結構

files

       上圖是glTF的一個大概結構,分為四大塊,最上面的json是一個表述,描述該模型的節點層級,材質,相機,動畫等相關邏輯結構,bin則對應這些對象的具體數據信息,glsl是對該模型渲染的着色器,針對該模型的數據信息,給出渲染“配方”,當然還有紋理內容。大塊內容可以以Base64的編碼內遷到文件中,方便拷貝和加載,也可以以URI的外鏈方式,側重重用性。

       下圖是json中包含的描述信息,內容詳細,比如mesh,紋理,蒙皮和動畫,定義了accessor的訪問器規則,同時還給出了相機,節點這些場景管理的信息。充分體現了glTF規范設計的強大,讓我想到了一句話:“解決問題固然重要,但通過設計避免問題則更勝一籌”。可以說,該規范是對復雜三維模型的一個很不錯的抽象,考慮的很充分,之間的接口定義也很規范,如果你也想設計一個二進制數據規范的話,這是一個很好的學習,借鑒范本。

dictionary-objects

2 glTF渲染

       東西再好,光說不練假把式。設計好了,只是一個開始而不是完結,還需要持續的推廣和應用。這年頭酒香也怕巷子深,伯牙難覓鍾子期的畫面有沒有。下面我們來看看glTF是如何渲染模型的,talk is cheap and show me the code~

加載&渲染

var entity = viewer.entities.add({
    name : url,
    position : position,
    orientation : orientation,
    model : {
        uri : url,
        minimumPixelSize : 128,
        maximumScale : 20000
    }
});

var model = scene.primitives.add(Cesium.Model.fromGltf({
    url : './duck/duck.gltf'
}));

// 內部通過該方法來解析JSON對象,獲取表述信息和具體的數據內容
function parseBinaryGltfHeader(uint8Array) {
    var json = getStringFromTypedArray(uint8Array, sceneOffset, sceneLength);
    return {
        glTF: JSON.parse(json),
        binaryOffset: binOffset
    };    
}

       如上是加載glTF的過程,也是提供兩種方式,一種是以Entity的方式,一種是以Primitive的方式,消費數碼相機(前者)和單反相機(后者)的差別。同時,Cesium對Model的渲染也是基於狀態的更新的,這個和地球,Entity的渲染思路是一致的。Model有三個狀態,加載(NEEDS_LOAD),解析(LOADING),和結束(LOADED)。在不同狀態下做該做的事,各司其職,互不干涉。

Model.prototype.update = function(frameState) {
    // Key 1 解析json對象中的各個對象
    // 比如是否有動畫,數據視圖具體情況,是否有擴展屬性等
    if ((this._state === ModelState.NEEDS_LOAD) && defined(this.gltf)) {
        parse(this);
        this._state = ModelState.LOADING;
    }

    // Key 2 解析后,對需要調用的內容賦值
    // 比如頂點數據和索引,材質,紋理等封裝,動畫,Runtime封裝到對應的RuntimeNode
    if (this._state === ModelState.LOADING) {
        createResources(this, frameState);
        this._state = ModelState.LOADED;
    }

    // Key 3 更新動態屬性,傳遞到對應的着色器參數中
    // 比如動畫,骨骼等,更新對應變量的節點矩陣,重新梳理節點層級對應的矩陣等參數
    if ((show && this._state === ModelState.LOADED) || justLoaded) {
        updateNodeHierarchyModelMatrix(this, modelTransformChanged, justLoaded, frameState.mapProjection);
    }

    // 渲染隊列
    if (show && !this._ignoreCommands) {
         var commandList = frameState.commandList;
         // ……
     }
}

       如上是Model的狀態更新函數,每一個狀態只專注於自己的業務,當處理完后完成狀態的更新。update實現實時更新和渲染。這里以讀一本書為例來描述這個過程,首先,我們先解析glTF 的頭信息,也就是json對象,了解該模型的大概結構,這就好比一本書的目錄,當我們對一本書感興趣的時候,都會先看看目錄,了解一個大概;接着,我們開始解析glTF數據,將每一個結構中的數據解析賦值,這是最復雜,也是最關鍵的過程之一,我們開始逐章節的閱讀這本書;最后,我們徹底解析完該數據,則構造對應的DrawCommand,添加到渲染隊列中;如果該數據中包含一些時態數據,比如動畫,蒙皮等,則每一幀都要動態的調整。這就是update中主要的四個狀態和邏輯,完成該模型的渲染。下面我們詳細介紹這個過程中三個重要的部分。

BufferView&Accessor

 

1

 

       如圖,紅框部分,從下往上看。Buffer緩存是一個二進制的數據塊,是幾何對象,動畫和蒙皮等數據信息的組合,在json中申明了這個數據塊的類型arraybuffer和長度。BufferView,緩存視圖,是Buffer的子集,如果Buffer是一本書的內容,那么BufferView就是一個目錄,將這本書划分成章節,並表示該章節的起始頁和長度。緩存和緩存視圖並不包含類型信息。他們只是簡單定義從文件中取出的原始數據,並不知道這些數據到底有什么涵義和結構。glTF文件中的對象(網格,蒙皮,動畫)都不會直接訪問緩存或緩存視圖,而是通過Accessor訪問器,這樣我們拿到這塊數據后,知道這塊數據是vec4,float還是其他類型。     

Mesh

 

2

 

       如上,有了訪問的規范,我們還得知道一個幾何對象的邏輯結構,就好比拼圖游戲,我們能拿到一塊塊拼圖,心中還要有一個輪廓,能把這些拼圖拼成一個完整的圖像。下面我們來看看Mesh這張圖是有哪些部分構成的,一切的一切還是從上方圖的紅框開始。

       該Mesh可以有多個Primitive組成,每個圖元有attribute頂點數據,indices頂點索引,mode類型為triangles,還有material材質,這些內容我們已經在之前的章節介紹過,不知道你還給我多少。我們再看material對象,里面用到了technique,其他的都是具體的光照模型的參數值,稍微特殊的是diffuse,是一張紋理。technique里面封裝了着色器需要的參數,包括attribute和uniform,以及GL狀態states,對應的着色器代碼program,還有shaders,texture紋理的封裝等,這些對象的值是一個accessor,進而獲取對應的值。。這些對象我們之前都詳細介紹過,我們順藤摸瓜,算是對之前內容的溫習,並串聯成一個完整體系。可以說,里面的技術點都和以前的內容一樣,glTF定義了他們之間交互的規范,將他們封裝為一個整體

Scene&Animation

 

3

 

       在很多應用中,只是從一個建模數據包中帶出單一對象,這並不充分。因此glTF還包含整個場景的關系,包括節點,變換矩陣,變換的層級關系,網格,材質,相機和動畫,試圖保存所有信息。這是一個場景樹的邏輯,算是glTF的一個優化。如上圖,該Scene中有三個node,其實Cesium_Air節點對應的mesh名字為Geometry-mesh090,他還有兩個子節點。

       當然,Cesium內部提供了動畫的解析(_runtime),在createRuntimeAnimations方法中實現,詳細的自己來看。其中包括TIME計時器,samplers插值方式,所對應的動畫節點和具體的屬性(比如rotation)。這樣每一幀會更新對應的值。

3 總結

       如上是glTF的一個介紹,下面來談幾點個人的想法。

必要性

       設計一個二進制文件的風險很大的,多數情況下會是一個失敗的產品。所以,當你覺得你需要一個新的數據格式時,你最好的選擇就是回家睡覺,早上起來想想是否還有這種沖動。如果時間久了,沖動還在,再理性的衡量也不遲。《Unix編程藝術》里面概括了兩個衡量點:時效性和數據量。當已有的數據格式無法滿足你對這兩點的需要時,或許你真的有充分的理由來設計一個新的數據格式了。

       設計一個新的數據格式是一件很有挑戰的事情,而且由於自身的局限性,劇情很可能是這樣的,你設計完了,很好的解決了你的問題,你覺得很棒,但不久的未來,隨着應用的推廣,需求的增加,現有的數據格式無法滿足業務的多樣性,有可能是你考慮不充分,有可能是過分需求,這些都是未知的風險,讓你陷入兩難,增加版本號,數據規范升級,版本多了會很混亂,也會出現舊版本讀取新數據這種無法解決的隱患。微調的話,則會弄臟現有的數據規范。慢慢的,時間證明了它是多么的失敗。

       所以,這個人經驗一定要豐富,謹慎,能夠做決定的人越少越好,不僅着眼於當前要解決的問題,還要綜合考慮通用性。但這又是一個困惑,也要控制它的應用范圍,隨着硬件性能的提高,避免過渡設計和優化。比如glTF提供了擴展,提供了場景樹,相機的信息,這都是出於通用性的考慮,但這個是否實用,就不好判斷了。 

Accessor&Json表述

       這是glTF數據讀取的機制,設計的很優雅,很值得我們學習。

       通常,對一個二進制文件,我們都是按照規范格式逐個字節的解析,這樣就有一個很大的風險,一步錯了,后面的都會錯。因為這太容易出錯了,萬一版本升級,多加了幾個字節,就會導致整個文件無法解析,我們增加了超級糾錯的機制。對每一塊數據前面加一個長度,或者校驗碼,檢測這塊數據是否完整,每塊讀完后,根據這塊的長度跳到下一個二進制塊重新開始(而不是循序漸進),這樣每一塊壞了不會影響下一塊。問題解決了,但並不實用,還僅僅是從技術層面的處理,所以我們需要增加規范,從設計上來解決這個問題,增加一個表述信息,根據accessor的規范來讀取二進制流。(個人猜測protocol buffer也是這樣的設計思路)

       當然,如果實現這種讀取方式,我們就需要一個“目錄”頁,glTF提供了json形式的header,這個header是json形式,也可以是xml格式,好處是靈活,兼容性強。相比xml,json是瀏覽器內部封裝成對象,效率高,缺點查詢不方便

產品化

       萬事開頭難,何況是創造一個新的東西,而且,當這個新的東西落地后,一切才剛剛開始。你需要完善的文檔和配套工具,需要和相關的廠商(數據&硬件)合作,是否開源,許可協議等,一堆事情要做,而且要做好。如果只是草草了事,只是看起來漂亮,還是無法得到別人的認可,純屬自娛自樂的行為,那就大為失色了。

       不妨低調的提供一個不是規范的規范,有了足夠的項目實踐和完善,踏踏實實的用起來,得到了用戶的認可,文檔工作也做細致了,標准化規范化自然也就提上了議程。好的技術,好的產品,好的標准,一切都是環環相扣的,所以,做好每一步,順其自然,就像那首歌一樣,Que sera sera, Whatever will be will be

       不知道有多少看到這,不容易啊,推薦有緣人看看這部推薦的電影,一部關於自閉症的動畫片,我很喜歡~


免責聲明!

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



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