Cesium原理篇:7最長的一幀之Entity(下)


       上一篇,我們介紹了當我們添加一個Entity時,通過Graphics封裝其對應參數,通過EntityCollection.Add方法,將EntityCollection的Entity傳遞到DataSourceDisplay.Visualizer中。本篇則從Visualizer開始,介紹數據的處理,並最終實現渲染的過程。

CesiumWidget.prototype.render = function() { if (this._canRender) { this._scene.initializeFrame(); var currentTime = this._clock.tick(); this._scene.render(currentTime); } else { this._clock.tick(); } };

       如上,在渲染階段,分別調用了clock.tick()和scene.render()。在這兩個階段中都有很多跟Entity相關的方面,我們分別闡述其大概過程

Viewer.prototype._onTick

       我們先溫習一下上篇的兩個知識點:DataSourceDisplay初始化的時候會調用defaultVisualizersCallback,會針對所有Geometry的Type創建對應的Visualizer;EntityCollection.Add每次添加一個Entity,會通過一系列事件傳遞,將該Entity傳遞到每一個Visualizer,保存到Visualizer中_addedObjects隊列中。

function Viewer(container, options) { eventHelper.add(clock.onTick, Viewer.prototype._onTick, this); } Viewer.prototype._onTick = function(clock) { var time = clock.currentTime; var isUpdated = this._dataSourceDisplay.update(time); } DataSourceDisplay.prototype.update = function(time) { visualizers = this._defaultDataSource._visualizers; vLength = visualizers.length; for (x = 0; x < vLength; x++) { result = visualizers[x].update(time) && result; } }

       如上,Viewer初始化時會綁定clock.onTick事件,確保每一幀都會調用。而其內部則調用DataSourceDisplay.update,進而遍歷所有的Visualizer,調用其update方法。下面,我們重點看一下Visualizer.update到底干了哪些事情。為了方便,我們還是以Rectangle為例來展開。

GeometryVisualizer.prototype.update = function(time) { // 獲取添加Entity隊列
    var addedObjects = this._addedObjects; var added = addedObjects.values; for (i = added.length - 1; i > -1; i--) { entity = added[i]; id = entity.id; // 每一個GeometryVisualizer都綁定一個具體的Updater,用來解析Entity,以Rectangle為例
        // Rectangle則由對應的RectangleGeometryUpdater來解析
        // 通過new Updater,將Entity對應的RectangleGraphics解析為RectangleGeometryUpdater的GeometryOptions
        updater = new this._type(entity, this._scene); this._updaters.set(id, updater); // 根據該RectangleGeometryUpdater的材質風格創建對應的GeometryInstance,分到對應的批次隊列中
        // 每一個批次隊列中的Geometry風格相同,因此可以通過一次DrawCommand渲染該隊列中所有Geometry
        // 目的是減少渲染次數,提高渲染效率
        insertUpdaterIntoBatch(this, time, updater); this._subscriptions.set(id, updater.geometryChanged.addEventListener(GeometryVisualizer._onGeometryChanged, this)); } // 清空添加Entity隊列,下次update中就不用重復處理
 addedObjects.removeAll(); var isUpdated = true; var batches = this._batches; var length = batches.length; for (i = 0; i < length; i++) { // 對所有批次隊列進行更新
        // 根據batch的GeometryInstance創建Primitive,並添加到Scene的PrimitiveCollection中
        isUpdated = batches[i].update(time) && isUpdated; } return isUpdated; };

       如上結合代碼和注釋,分為三步,下面我們詳細介紹一下:

  • new Updater
  • insertUpdaterIntoBatch
  • batch.update

1.new Updater

function RectangleGeometryUpdater(entity, scene) { this._options = new GeometryOptions(entity); this._onEntityPropertyChanged(entity, 'rectangle', entity.rectangle, undefined); }

       如上,簡單說,Updater的構造函數主要就做了一件事情,構建對應的GeometryOptions,並對其賦值。GeometryOptions是Cesium的一個簡單的封裝,不同的Updater對應不同的Graphics,GeometryOptions的屬性也是根據不同的Graphics量身定做,比如RectangleGeometryUpdater中對應的GeometryOptions屬性如下:

function GeometryOptions(entity) { this.id = entity; this.vertexFormat = undefined; this.rectangle = undefined; this.closeBottom = undefined; this.closeTop = undefined; this.height = undefined; this.extrudedHeight = undefined; this.granularity = undefined; this.stRotation = undefined; this.rotation = undefined; }

       大家可以看看其他的Updater,比如PolygonGeometryUpdater,EllipseGeometryUpdater等等,對應的GeometryOptions也不相同。這樣,從設計的角度,將不同Graphics中對應的不同屬性,封裝成一個標准的GeometryOptions,對外表象一致(都是Updater中的_options屬性),內部各自提供解析方法(_onEntityPropertyChanged方法),我們再看一下RectangleGeometryUpdater的_onEntityPropertyChanged實現:

RectangleGeometryUpdater.prototype._onEntityPropertyChanged = function(entity, propertyName, newValue, oldValue) { var rectangle = this._entity.rectangle; // ……
    var height = rectangle.height; // ……
    
    var options = this._options; options.vertexFormat = isColorMaterial ? PerInstanceColorAppearance.VERTEX_FORMAT : MaterialAppearance.MaterialSupport.TEXTURED.vertexFormat; options.rectangle = coordinates.getValue(Iso8601.MINIMUM_VALUE, options.rectangle); options.height = defined(height) ? height.getValue(Iso8601.MINIMUM_VALUE) : undefined; options.extrudedHeight = defined(extrudedHeight) ? extrudedHeight.getValue(Iso8601.MINIMUM_VALUE) : undefined; options.granularity = defined(granularity) ? granularity.getValue(Iso8601.MINIMUM_VALUE) : undefined; options.stRotation = defined(stRotation) ? stRotation.getValue(Iso8601.MINIMUM_VALUE) : undefined; options.rotation = defined(rotation) ? rotation.getValue(Iso8601.MINIMUM_VALUE) : undefined; options.closeBottom = defined(closeBottom) ? closeBottom.getValue(Iso8601.MINIMUM_VALUE) : undefined; options.closeTop = defined(closeTop) ? closeTop.getValue(Iso8601.MINIMUM_VALUE) : undefined; this._isClosed = defined(extrudedHeight) && defined(options.closeTop) && defined(options.closeBottom) && options.closeTop && options.closeBottom; this._outlineWidth = defined(outlineWidth) ? outlineWidth.getValue(Iso8601.MINIMUM_VALUE) : 1.0; this._dynamic = false; this._geometryChanged.raiseEvent(this); }

       如上是代碼片段,this._entity.rectangle為RectangleGraphics類,將其屬性賦給_options(GeometryOptions類),屬性賦值的過程也是一個自檢測的過程,如果存在必要屬性缺失的情況則指定一個默認值,最終完成了RectangleGraphics到GeometryOptions的轉移。

2.insertUpdaterIntoBatch

function insertUpdaterIntoBatch(that, time, updater) { if (updater.outlineEnabled) { that._outlineBatches[shadows].add(time, updater); } if (updater.fillEnabled) { if (updater.onTerrain) { that._groundColorBatch.add(time, updater); } else { if (updater.isClosed) { if (updater.fillMaterialProperty instanceof ColorMaterialProperty) { that._closedColorBatches[shadows].add(time, updater); } else { that._closedMaterialBatches[shadows].add(time, updater); } } else { if (updater.fillMaterialProperty instanceof ColorMaterialProperty) { that._openColorBatches[shadows].add(time, updater); } else { that._openMaterialBatches[shadows].add(time, updater); } } } } }

       不准確的說(但有助於理解),GeometryOptions主要對應RectangleGraphics的幾何數值,而在insertUpdaterIntoBatch中則根據RectangleGraphics的材質風格進行分組,只有材質一致的RectangleGeometryUpdater才能分到一起,進行后面的批次。比如學校分班,優等生,中等生分到不同的班級,老師根據不同班級的能力進行適當的區分,就是一種通過分組的方式來優化的思路。打組批次也是同樣一個道理。

       針對GeometryVisualizer,一共提供了四種Batch類型:

  • StaticGeometryColorBatch
  • StaticGeometryPerMaterialBatch
  • StaticGroundGeometryColorBatch
  • StaticOutlineGeometryBatch

       不同的Batch,根據材質屬性的不同,會選擇Updater的對應方法,創建GeometryInstance。比如RectangleGeometryUpdater提供了createFillGeometryInstance和createOutlineGeometryInstance兩個方法來創建其面和邊線對應的GeometryInstance。如下是RectangleGeometryUpdater對應的一種邏輯情況:

StaticOutlineGeometryBatch.prototype.add = function(time, updater) { // Key 1
    var instance = updater.createOutlineGeometryInstance(time); var width = this._scene.clampLineWidth(updater.outlineWidth); var batches; var batch; if (instance.attributes.color.value[3] === 255) { batches = this._solidBatches; batch = batches.get(width); if (!defined(batch)) { batch = new Batch(this._primitives, false, width, this._shadows); batches.set(width, batch); } // Key 2
 batch.add(updater, instance); } }; // Key 1
RectangleGeometryUpdater.prototype.createFillGeometryInstance = function(time) { return new GeometryInstance({ id : entity, geometry : new RectangleGeometry(this._options), attributes : attributes }); }; // Key 2
Batch.prototype.add = function(updater, instance) { var id = updater.entity.id; this.createPrimitive = true; this.geometry.set(id, instance); this.updaters.set(id, updater); };

       第一是構建GeometryInstance,這里有一個新的對象RectangleGeometry,之前我們對於Rectangle的理解,都是在Graphics這樣的一個概念,可以認為這是一個參數化的對象,對用戶而言容易理解。比如一個圓對應的參數化信息就是圓心+半徑,我們很好理解,但對計算機,或者WebGL則不能理解,WebGL能理解的是三角形,所以我們就需要把這個圓分解成三角形的拼接,比如切成一塊塊的西瓜狀。將參數化的圖形分解成非參數的簡單三角形。這個過程是在Primitive.update中完成的,但最終是由RectangleGeometry提供的算法來實現。其他Geometry也是同樣的一個邏輯。第二,把創建的GeometryInstance放到Batch隊列中。

3.batch.update

       之前的步驟1和步驟2,我們對當前這一幀中新增的Entity進行解析,構造成對應的GeometryInstance,放到對應的Batch隊列中。比如有兩個Rectangle類型的Entity,假設他們的風格一樣,都是純色的,當然顏色可能不相同,但最終都是在一個批次隊列(StaticOutlineGeometryBatch)。接下來,1每一個批次隊列會構建一個Primitive,包括該隊列中所有的GeometryInstances,因為顯卡強大的並行能力,繪制一個三角面和繪制N個三角面的所需的時間是一樣的(N取決於頂點數),2所以盡可能的將多個Geometry封裝成一個VBO是提高渲染性能的一個關鍵思路(批次&實例化)。而這個batch.update完成的前半部分,而Primitive.update則完成了最后,也是最關鍵的一步。

function Batch(primitives, translucent, appearanceType, closed, shadows) { // 每一個Batch中的GeometryInstance隊列
    this.geometry = new AssociativeArray(); } Batch.prototype.add = function(updater, instance) { var id = updater.entity.id; // 添加新的GeometryInstance,並標識,此時需要創建Primitvie
    this.createPrimitive = true; this.geometry.set(id, instance); }; Batch.prototype.update = function(time) { var isUpdated = true; var removedCount = 0; var primitive = this.primitive; var primitives = this.primitives; var attributes; var i; // 檢測需要創建Primitive
    if (this.createPrimitive) { var geometries = this.geometry.values; var geometriesLength = geometries.length; if (geometriesLength > 0) { // 將隊列中所有的GeometryInstances封裝成一個primitive對象
            primitive = new Primitive({ asynchronous : true, geometryInstances : geometries, appearance : new this.appearanceType({ translucent : this.translucent, closed : this.closed }), shadows : this.shadows }); // 將primitive添加到Scene.primitives中
            // 既然已經綁定到Scene,接下來就要准備渲染
 primitives.add(primitive); isUpdated = false; } this.attributes.removeAll(); this.primitive = primitive; this.createPrimitive = false; this.waitingOnCreate = true; } return isUpdated; };

       如上是this._clock.tick()的一個大概過程,一步一步摩擦,最終創建了Primitive,並添加到PrimitiveCollection隊列中,如果以Rectangle類型的Entity為例,大概的流程如下:

DataSourceDisplay.prototype.update GeometryVisualizer.prototype.update updater = new this._type(entity, this._scene); new GeometryOptions(entity); _onEntityPropertyChanged() insertUpdaterIntoBatch StaticGeometryColorBatch.prototype.add RectangleGeometryUpdater.prototype.createFillGeometryInstance new GeometryInstance() Batch.prototype.add batches[i].update(time) StaticGeometryColorBatch.prototype.update Batch.prototype.update new Primitive() primitives.add(primitive);

Primitive.prototype.update

       看完了tick()后,馬不停蹄的則開始了this._scene.render(currentTime),大概經過下面的幾層,最終開始了Primitive.prototype.update,也就是接下來我們介紹的中重點。

this._scene.render(currentTime); Scene.prototype.render function render(scene, time) function updateAndExecuteCommands() function executeCommandsInViewport() function updatePrimitives() PrimitiveCollection.prototype.update() for (var i = 0; i < primitives.length; ++i) { primitives[i].update(frameState); }

       如上的鋪墊工作結束,進入正文。Primitive.prototype.update究竟做了哪些重要的事情.

var PrimitiveState = { READY : 0, CREATING : 1, CREATED : 2, COMBINING : 3, COMBINED : 4, COMPLETE : 5, FAILED : 6
};

       似曾相識的感覺有沒有。在設計上,Primitive和Globe相似,也是基於狀態的管理:每個狀態都有專門的模塊來負責,而每一幀主要用來維護和更新狀態,並根據當前的狀態來調用對應的模塊。我們看看PrimitiveState,里面主要有三類:CREATE,COMBINE,COMPLETE。心里大概有個一知半解,下面來解惑。

Primitive.prototype.update = function(frameState) { if (this._batchTable.attributes.length > 0) { this._batchTable.update(frameState); } if (this._state !== PrimitiveState.COMPLETE && this._state !== PrimitiveState.COMBINED) { if (this.asynchronous) { loadAsynchronous(this, frameState); } else { loadSynchronous(this, frameState); } } if (this._state === PrimitiveState.COMBINED) { createVertexArray(this, frameState); } if (!this.show || this._state !== PrimitiveState.COMPLETE) { return; } if (createRS) { var rsFunc = defaultValue(this._createRenderStatesFunction, createRenderStates); rsFunc(this, context, appearance, twoPasses); } if (createSP) { var spFunc = defaultValue(this._createShaderProgramFunction, createShaderProgram); spFunc(this, frameState, appearance); } if (createRS || createSP) { var commandFunc = defaultValue(this._createCommandsFunction, createCommands); commandFunc(this, appearance, material, translucent, twoPasses, this._colorCommands, this._pickCommands, frameState); } updateAndQueueCommandsFunc(); }

       如上是Primitive.update的主要過程,我們以狀態的變化為序,介紹一下loadAsynchronous,createVertexArray以及create*這幾個內容。

loadAsynchronous

       Primitive初始化時,默認為READY狀態。Update方法中,首先會進入loadAsynchronous方法。這里主要做了兩個事情:Create&Combine。

function loadAsynchronous(primitive, frameState) { var instances; var geometry; var i; var j; var instanceIds = primitive._instanceIds; //開始進入createGeometry
    if (primitive._state === PrimitiveState.READY) { instances = (isArray(primitive.geometryInstances)) ? primitive.geometryInstances : [primitive.geometryInstances]; var length = primitive._numberOfInstances = instances.length; var promises = []; var subTasks = []; for (i = 0; i < length; ++i) { geometry = instances[i].geometry; instanceIds.push(instances[i].id); // 用於處理數據的線程名稱
            // 需要進行數據處理的geometry對象
 subTasks.push({ moduleName : geometry._workerName, geometry : geometry }); } // 根據當前瀏覽器允許的最大線程數,創建N個createGeometry線程,方便后續通過Workers線程處理
        if (!defined(createGeometryTaskProcessors)) { createGeometryTaskProcessors = new Array(numberOfCreationWorkers); for (i = 0; i < numberOfCreationWorkers; i++) { createGeometryTaskProcessors[i] = new TaskProcessor('createGeometry', Number.POSITIVE_INFINITY); } } // 分攤任務,當前Primitive中可能需要對多個Geometry進行處理
        // 平坦任務,均分
        var subTask; subTasks = subdivideArray(subTasks, numberOfCreationWorkers); for (i = 0; i < subTasks.length; i++) { var packedLength = 0; var workerSubTasks = subTasks[i]; var workerSubTasksLength = workerSubTasks.length; for (j = 0; j < workerSubTasksLength; ++j) { subTask = workerSubTasks[j]; geometry = subTask.geometry; if (defined(geometry.constructor.pack)) { subTask.offset = packedLength; packedLength += defaultValue(geometry.constructor.packedLength, geometry.packedLength); } } var subTaskTransferableObjects; // 將Geometry中的參數化信息保存到arraybuffer中
            // 方便后續傳入到線程
            if (packedLength > 0) { var array = new Float64Array(packedLength); subTaskTransferableObjects = [array.buffer]; for (j = 0; j < workerSubTasksLength; ++j) { subTask = workerSubTasks[j]; geometry = subTask.geometry; if (defined(geometry.constructor.pack)) { geometry.constructor.pack(geometry, array, subTask.offset); subTask.geometry = array; } } } // 調用線程,傳入參數subTask,subTaskTransferableObjects中以引用方式,非復制
 promises.push(createGeometryTaskProcessors[i].scheduleTask({ subTasks : subTasks[i] }, subTaskTransferableObjects)); } // creating狀態,線程中處理
        primitive._state = PrimitiveState.CREATING; when.all(promises, function(results) { // 成功后更新狀態,已經創建成功,返回值results
            primitive._createGeometryResults = results; primitive._state = PrimitiveState.CREATED; }).otherwise(function(error) { setReady(primitive, frameState, PrimitiveState.FAILED, error); }); } else if (primitive._state === PrimitiveState.CREATED) { // 如下,同上面的思路一致,通過combine線程,將多個geometry的返回值合並成一個vbo
        var transferableObjects = []; instances = (isArray(primitive.geometryInstances)) ? primitive.geometryInstances : [primitive.geometryInstances]; var scene3DOnly = frameState.scene3DOnly; var projection = frameState.mapProjection; var promise = combineGeometryTaskProcessor.scheduleTask(PrimitivePipeline.packCombineGeometryParameters({ createGeometryResults : primitive._createGeometryResults, instances : instances, ellipsoid : projection.ellipsoid, projection : projection, elementIndexUintSupported : frameState.context.elementIndexUint, scene3DOnly : scene3DOnly, vertexCacheOptimize : primitive.vertexCacheOptimize, compressVertices : primitive.compressVertices, modelMatrix : primitive.modelMatrix, createPickOffsets : primitive._createPickOffsets }, transferableObjects), transferableObjects); primitive._createGeometryResults = undefined; primitive._state = PrimitiveState.COMBINING; when(promise, function(packedResult) { var result = PrimitivePipeline.unpackCombineGeometryResults(packedResult); primitive._geometries = result.geometries; primitive._attributeLocations = result.attributeLocations; primitive.modelMatrix = Matrix4.clone(result.modelMatrix, primitive.modelMatrix); primitive._pickOffsets = result.pickOffsets; primitive._instanceBoundingSpheres = result.boundingSpheres; primitive._instanceBoundingSpheresCV = result.boundingSpheresCV; if (defined(primitive._geometries) && primitive._geometries.length > 0) { primitive._state = PrimitiveState.COMBINED; } else { setReady(primitive, frameState, PrimitiveState.FAILED, undefined); } }).otherwise(function(error) { setReady(primitive, frameState, PrimitiveState.FAILED, error); }); } }

       如上是一段代碼示意,從中可見,Create和Combine這類計算量比較大的操作,都是放在線程中進行的,避免阻塞主線程。這樣,通過loadAsynchronous函數,將參數化的geometry轉化為三角形,同時對同類的geometry合並成一個渲染批次,進而優化了渲染效率。可以說,這一塊是Cesium對Geometry處理的核心。此時,狀態已經更新為PrimitiveState.COMBINED。      

       備注:如果對Workers不太了解,可以參考之前寫的《Cesium原理篇:4Web Workers剖析

create*

  • createVertexArray
    上面的Geometry已經將數據處理為indexBuffer和vertexBuffer,下面則需要將該數據結合attributes創建為vbo&vao,這個過程就是通過createVertexArray完成
  • createRS
    創建RenderState
  • createSP
    創建ShaderProgram

       很明顯,渲染主要是數據+風格,當我們滿足了geometry的數據部分已經符合WebGL渲染的格式后,結合appearance封裝的材質,設置對應的RenderState以及Shader和所需要的參數。最后,我們構造出最終的DrawCommand,添加到DrawCommandList中,完成最終的渲染。這塊就不對細節展開了,涉及到Renderer模塊的,在之前的Renderer系列都有詳細介紹,這里主要介紹了大概的流程。

總結

       Entity牽扯到的內容很多,從方便用戶使用,到Geometry類型以及風格的多樣性,到最終構造出DrawCommand,以及渲染Pass的優先級,里面牽扯的內容非常多,同時出於渲染性能的優化,還要打組批次,搞多線程,里面隨便一個點都有很多值得學習,借鑒的地方。

       自問如果要自己來做這一套Geometry渲染,首先多線程是必須要設計的,不然性能上負擔不起,打組也是一個技術要點,但優先級不是最高。個人可能不會把Add,Updater以及Primitive分的這么細,材質上壓根就想不出來該如何做。Cesium在設計上確實很優雅,但這在性能上多少也是有代價的。

       終於將大概的過程寫完了,總覺得欠了一些內容,有點力不從心。希望能把這個流程的大概介紹清楚,后面可以針對某一個局部細節可以細細鑽研,學習里面的技巧,理解其中的設計原委。


免責聲明!

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



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