Cesium原理篇:Batch


       通過之前的Material和Entity介紹,不知道你有沒有發現,當我們需要添加一個rectangle時,有兩種方式可供選擇,我們可以直接添加到Scene的PrimitiveCollection,也可以構造一個Entity,添加到Viewer的EntityCollection中,代碼如下:

// 直接構造Primitive,添加
rectangle = scene.primitives.add(new Cesium.Primitive({ geometryInstances : new Cesium.GeometryInstance({ geometry : new Cesium.RectangleGeometry({ rectangle : Cesium.Rectangle.fromDegrees(-120.0, 20.0, -60.0, 40.0), vertexFormat : Cesium.EllipsoidSurfaceAppearance.VERTEX_FORMAT }) }), appearance : new Cesium.EllipsoidSurfaceAppearance({ aboveGround : false
 }) })); // 間接構造一個Entity,Cesium內部將其轉化為Primitive
viewer.entities.add({ rectangle : { coordinates : Cesium.Rectangle.fromDegrees(-92.0, 20.0, -86.0, 27.0), outline : true, outlineColor : Cesium.Color.WHITE, outlineWidth : 4, stRotation : Cesium.Math.toRadians(45), material : stripeMaterial } });

       兩者有何不同,為什么還要提供Entity這種方式,繞了一大圈,最后照樣是一個primitive。當然,有一個因素是后者調用簡單,對用戶友好。但還有一個重點是內部在把Entity轉為Primitive的這條生產線上,會根據Entity的不同進行打組分類,好比快遞的分揀線會將同一個目的地的包裹分到一類,然后用一個大的箱子打包送到該目的地,目的就是優化效率,充分利用顯卡的並發能力。

       在Entity轉為Primitive的過程中,GeometryInstance是其中的過渡類,以Rectangle為例,我們看看構造GeometryInstance的過程:

RectangleGeometryUpdater.prototype.createFillGeometryInstance = function(time) { if (this._materialProperty instanceof ColorMaterialProperty) { var currentColor = Color.WHITE; if (defined(this._materialProperty.color) && (this._materialProperty.color.isConstant || isAvailable)) { currentColor = this._materialProperty.color.getValue(time); } color = ColorGeometryInstanceAttribute.fromColor(currentColor); attributes = { show : show, color : color }; } else { attributes = { show : show }; } return new GeometryInstance({ id : entity, geometry : new RectangleGeometry(this._options), attributes : attributes }); }

       首先,該材質_materialProperty就是構建Entity時傳入的材質對象,attributes則用來標識該Geometry實例化的attribute屬性,Cesium內部判斷該材質是否為color類型,進而對應不同的實例化attribute。在其他GeometryUpdater方法中都是此邏輯,因此在材質的處理上,只對color進行了實例化的處理。這里留意一下appearance參數,可以看到主要對應perInstanceColorAppearanceType和materialAppearanceType兩種,分別封裝ColorMaterialProperty和其他MaterialProperty,這個在之前的Material中已經講過。

Batch

       Cesium通過GeometryInstance作為標准,來達到物以類聚鳥以群分的結果,這個標准就是如上的介紹,有了標准還不夠,還需要為這個標准搭建一條流水線,將標准轉化為行動。這就是Batch的作用。之前在Entity中提到GeometryVisualizer提供了四種Batch,我們以StaticGeometryColorBatch和StaticGeometryPerMaterialBatch為例,對比說明一下這個流水線的流程。

       我們在此來到DataSourceDisplay類,初始化是針對每一個Updater,都封裝了一個Visualizer.我們還是以RectangleGeometry為例展開:

new GeometryVisualizer(WallGeometryUpdater, scene, entities) function GeometryVisualizer(type, scene, entityCollection) { for (var i = 0; i < numberOfShadowModes; ++i) { this._outlineBatches[i] = new StaticOutlineGeometryBatch(primitives, scene, i); this._closedColorBatches[i] = new StaticGeometryColorBatch(primitives, type.perInstanceColorAppearanceType, true, i); this._closedMaterialBatches[i] = new StaticGeometryPerMaterialBatch(primitives, type.materialAppearanceType, true, i); this._openColorBatches[i] = new StaticGeometryColorBatch(primitives, type.perInstanceColorAppearanceType, false, i); this._openMaterialBatches[i] = new StaticGeometryPerMaterialBatch(primitives, type.materialAppearanceType, false, i); } }

       而每一次添加的Entity都會以事件的方式通知到每一個Updater綁定的GeometryVisualizer,調用insertUpdaterIntoBatch方法,根據每個Entity材質的不同放到對應的Batch隊列中,其實Cesium的batch很簡單,就是看是否是color類型的材質,只有這一個邏輯判斷:

function insertUpdaterIntoBatch(that, time, updater) { if (updater.fillMaterialProperty instanceof ColorMaterialProperty) { that._openColorBatches[shadows].add(time, updater); } else { that._openMaterialBatches[shadows].add(time, updater); } }

       我們先看ColorMaterialProperty材質的處理方式:

function StaticGeometryColorBatch(primitives, appearanceType, closed, shadows) { this._solidBatch = new Batch(primitives, false, appearanceType, closed, shadows); this._translucentBatch = new Batch(primitives, true, appearanceType, closed, shadows); } StaticGeometryColorBatch.prototype.add = function(time, updater) { var instance = updater.createFillGeometryInstance(time); if (instance.attributes.color.value[3] === 255) { this._solidBatch.add(updater, instance); } else { this._translucentBatch.add(updater, instance); } };

       可見,按照顏色是否透明分為兩類,這個不難理解,因為這兩類對應的PASS不同,渲染的優先級不一樣。同時,在添加到對應batch隊列前,會調用Updater.createFillGeometryInstance方法創建該Geometry對應的Instance。因此,這里體現了Cesium的一個規范,每一個GeometryGraphics類型都對應了一個該Geometry的Updater類,該Updater類通過一套create*geometryInstance方法,實現不同的GeometryGraphics到GeometryInstance的標准化封裝。下面是RectangleGeometryUpdater提供的三個create方法:

// 填充面的geoinstance
RectangleGeometryUpdater.prototype.createFillGeometryInstance // 邊框線的geoinstance
RectangleGeometryUpdater.prototype.createOutlineGeometryInstance // 針對動態批次
RectangleGeometryUpdater.prototype.createDynamicUpdater

       前兩個不用多說,看注釋。后一個一看dynamic,看上去牛逼,其實只是將creategeometryinstance的過程延后進行,不是在add中創建,而是在update的時候創建。接着在Batchupdate中,將batch隊列中所有的geometryinstances封裝成一個Primitve,這個流水線至此結束。因為我們在之前的Entity中介紹過這個過程,所以不在此展開。下面,我們在看看StaticGeometryPerMaterialBatch,對比一下兩者的不一樣。

StaticGeometryPerMaterialBatch.prototype.add = function(time, updater) { var items = this._items; var length = items.length; for (var i = 0; i < length; i++) { var item = items[i]; if (item.isMaterial(updater)) { item.add(time, updater); return; } } var batch = new Batch(this._primitives, this._appearanceType, updater.fillMaterialProperty, this._closed, this._shadows); batch.add(time, updater); items.push(batch); };

      可以看到,非顏色材質的batch里面還有一個items數組,會判斷當前的updater中的材質是否存在於items數組中,如果沒有,則根據該材質創建一個新的batch,並添加到items數組中。因為多了這一層,所以之前Updater.createFillGeometryInstance的調用延后到batch.add的過程中,並無其他不同。然后調用batch.update,以材質類型為標准創建對應的primitive。

BatchTable

       對於這些實例化的屬性,Primitive在update中對其進行處理,思路就是將這些值保存到一張RGBA的紋理,並根據實例化屬性的長度構建對應的VBO,從而方便Shader中的使用。下面,我們以兩個Rectangle為例,來看看詳細的過程。

createBatchTable

function createBatchTable(primitive, context) { // 0 獲取instance
    // 獲取該Primitive中instance數組
    var geometryInstances = primitive.geometryInstances; var instances = (isArray(geometryInstances)) ? geometryInstances : [geometryInstances]; var attributeIndices = {}; // 獲取這些instances中相同的attribute屬性字段的名稱
    var names = getCommonPerInstanceAttributeNames(instances); // 1創建attribute
    // 創建這些屬性字段的attribute,包括對應的變量名,字段類型等
    // indices是他們的索引
    for (i = 0; i < length; ++i) { name = names[i]; attribute = instanceAttributes[name]; attributeIndices[name] = i; attributes[i] = { functionName : 'czm_batchTable_' + name, componentDatatype : attribute.componentDatatype, componentsPerAttribute : attribute.componentsPerAttribute, normalize : attribute.normalize }; } // 2為attributes賦值
    // 遍歷所有的instance,取出每一個instance對應的attribute值
    // 將這些值按照其字段類型保存到batchTable中
    for (i = 0; i < numberOfInstances; ++i) { var instance = instances[i]; instanceAttributes = instance.attributes; for (var j = 0; j < length; ++j) { name = names[j]; attribute = instanceAttributes[name]; var value = getAttributeValue(attribute.value); var attributeIndex = attributeIndices[name]; batchTable.setBatchedAttribute(i, attributeIndex, value); } } // 4 將batch的結果保存在primitive中,方便下面的處理
    primitive._batchTable = batchTable; primitive._batchTableAttributeIndices = attributeIndices; }

       如上,可以看到BatchTable可以認為是這些instance的一個實例化屬性表,屬性也是按照VBO的結構來設計的,就是為了后續shader中使用的方便。

BatchTable.prototype.update

BatchTable.prototype.update = function(frameState) { createTexture(this, context); updateTexture(this); }

       創建完BatchTable,則調用update,將該Table的屬性值以RGBA的方式保存到一張Texture中,這類似於一個float紋理,但通用性更強一些,畢竟有一些瀏覽器竟然不支持float紋理。比如ColorMaterialProperty,里面有color,show,distanceDisplayCondition三個實例化屬性,分別控制顏色,是否可見以及可見范圍的控制。實際上,還包括pickColor,boundingSphereRadius,boundingSphereCenter3DHigh,boundingSphereCenter3DLow,boundingSphereCenter2DHigh,boundingSphereCenter2DLow共計9個屬性,其中Center占了四個屬性,我們后續有機會在詳細說一下Cesium的算法細節,目的是為了避免近距離觀察物體時因為精度導致的抖動問題,用兩個float來表示一個double的思路來解決。這樣,假如我們有兩個instance對象,則該紋理x維度是9*2 = 18,而大多數情況下,y維度則始終為1(只要x維度的長度不超過顯卡最大紋理長度的限制),相當於一個一維紋理,其實就是一個hashtable。

       createTexture后就是updateTexture就是把我們之前set的attribute屬性值保存到該紋理中。

updateBatchTableBoundingSpheres

       這個不多講了,就是剛才說的,把centre的xyz每一個屬性保存為兩個float的過程。

createShaderProgram

  • BatchTable.prototype.getVertexShaderCallback
  • Primitive._appendShowToShader
  • Primitive._appendDistanceDisplayConditionToShader
  • Primitive._updateColorAttribute

       通過如上幾個函數,添加處理BatchTable部分的片源着色器代碼。

createCommands

BatchTable.prototype.getUniformMapCallback = function() { var that = this; return function(uniformMap) { var batchUniformMap = { batchTexture : function() { return that._texture; }, batchTextureDimensions : function() { return that._textureDimensions; }, batchTextureStep : function() { return that._textureStep; } }; return combine(uniformMap, batchUniformMap); }; };

       如上,在創建command時,創建對應的uniformMap,傳入uniform變量。

總結

       如上就是Cesium批次流程的大概流程,着重介紹了BatchTable這種思想,如果我們在實際設計中這也是值得我們學習借鑒的地方。打個比方,如果你對相機不熟悉,就用自動對焦,則基本就是傻瓜操作,而如果你比較熟悉,則可以用單反,自己來設置參數。Batch流程也是如此,只是解決了最常見,最基本的分類,如果你足夠熟悉,可以基於Primitive自己來手動完成分組的過程,多快好省,可以不同類型的geometry來做一個批次,always try, never die。

       好了,這里忽略了兩個地方,一個是這個面如何做到貼地,二是Geometry在渲染時抖動有是怎么一回事。前者用到了模版緩沖的方法,后者則是float精度問題導致的,我們下一節在說一下第一個問題。


免責聲明!

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



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