livox viewer多帧拼接_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队列中。

functionViewer(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;

}returnisUpdated;

};

如上结合代码和注释,分为三步,下面我们详细介绍一下:

new Updater

insertUpdaterIntoBatch

batch.update

1.new Updater

functionRectangleGeometryUpdater(entity, scene) {this._options = newGeometryOptions(entity);this._onEntityPropertyChanged(entity, 'rectangle', entity.rectangle, undefined);

}

如上,简单说,Updater的构造函数主要就做了一件事情,构建对应的GeometryOptions,并对其赋值。GeometryOptions是Cesium的一个简单的封装,不同的Updater对应不同的Graphics,GeometryOptions的属性也是根据不同的Graphics量身定做,比如RectangleGeometryUpdater中对应的GeometryOptions属性如下:

functionGeometryOptions(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

functioninsertUpdaterIntoBatch(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 instanceofColorMaterialProperty) {

that._closedColorBatches[shadows].add(time, updater);

}else{

that._closedMaterialBatches[shadows].add(time, updater);

}

}else{if (updater.fillMaterialProperty instanceofColorMaterialProperty) {

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);varbatches;varbatch;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 2batch.add(updater, instance);

}

};//Key 1

RectangleGeometryUpdater.prototype.createFillGeometryInstance = function(time) {return newGeometryInstance({

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则完成了最后,也是最关键的一步。

functionBatch(primitives, translucent, appearanceType, closed, shadows) {//每一个Batch中的GeometryInstance队列

this.geometry = newAssociativeArray();

}

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;varattributes;vari;//检测需要创建Primitive

if (this.createPrimitive) {var geometries = this.geometry.values;var geometriesLength =geometries.length;if (geometriesLength > 0) {//将队列中所有的GeometryInstances封装成一个primitive对象

primitive = newPrimitive({

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;

}returnisUpdated;

};

如上是this._clock.tick()的一个大概过程,一步一步摩擦,最终创建了Primitive,并添加到PrimitiveCollection队列中,如果以Rectangle类型的Entity为例,大概的流程如下:

DataSourceDisplay.prototype.update

GeometryVisualizer.prototype.update

updater= new this._type(entity, this._scene);newGeometryOptions(entity);

_onEntityPropertyChanged()

insertUpdaterIntoBatch

StaticGeometryColorBatch.prototype.add

RectangleGeometryUpdater.prototype.createFillGeometryInstancenewGeometryInstance()

Batch.prototype.add

batches[i].update(time)

StaticGeometryColorBatch.prototype.update

Batch.prototype.updatenewPrimitive()

primitives.add(primitive);

Primitive.prototype.update

看完了tick()后,马不停蹄的则开始了this._scene.render(currentTime),大概经过下面的几层,最终开始了Primitive.prototype.update,也就是接下来我们介绍的中重点。

this._scene.render(currentTime);

Scene.prototype.renderfunctionrender(scene, time)functionupdateAndExecuteCommands()functionexecuteCommandsInViewport()functionupdatePrimitives()

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。

functionloadAsynchronous(primitive, frameState) {varinstances;vargeometry;vari;varj;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= newArray(numberOfCreationWorkers);for (i = 0; i < numberOfCreationWorkers; i++) {

createGeometryTaskProcessors[i]= new TaskProcessor('createGeometry', Number.POSITIVE_INFINITY);

}

}//分摊任务,当前Primitive中可能需要对多个Geometry进行处理

//平坦任务,均分

varsubTask;

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);

}

}varsubTaskTransferableObjects;//将Geometry中的参数化信息保存到arraybuffer中

//方便后续传入到线程

if (packedLength > 0) {var array = newFloat64Array(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在设计上确实很优雅,但这在性能上多少也是有代价的。

终于将大概的过程写完了,总觉得欠了一些内容,有点力不从心。希望能把这个流程的大概介绍清楚,后面可以针对某一个局部细节可以细细钻研,学习里面的技巧,理解其中的设计原委。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值