上一篇,我们介绍了当我们添加一个 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 相关的方面,我们分别阐述其大概过程
我们先温习一下上篇的两个知识点: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;
- };
如上结合代码和注释,分为三步,下面我们详细介绍一下:
- 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 的转移。
- 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 类型:
不同的 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 队列中。
之前的步骤 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);
看完了 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 * 这几个内容。
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 不太了解,可以参考之前写的《》
很明显,渲染主要是数据 + 风格,当我们满足了 geometry 的数据部分已经符合 WebGL 渲染的格式后,结合 appearance 封装的材质,设置对应的 RenderState 以及 Shader 和所需要的参数。最后,我们构造出最终的 DrawCommand,添加到 DrawCommandList 中,完成最终的渲染。这块就不对细节展开了,涉及到 Renderer 模块的,在之前的 Renderer 系列都有详细介绍,这里主要介绍了大概的流程。
Entity 牵扯到的内容很多,从方便用户使用,到 Geometry 类型以及风格的多样性,到最终构造出 DrawCommand,以及渲染 Pass 的优先级,里面牵扯的内容非常多,同时出于渲染性能的优化,还要打组批次,搞多线程,里面随便一个点都有很多值得学习,借鉴的地方。
自问如果要自己来做这一套 Geometry 渲染,首先多线程是必须要设计的,不然性能上负担不起,打组也是一个技术要点,但优先级不是最高。个人可能不会把 Add,Updater 以及 Primitive 分的这么细,材质上压根就想不出来该如何做。Cesium 在设计上确实很优雅,但这在性能上多少也是有代价的。
终于将大概的过程写完了,总觉得欠了一些内容,有点力不从心。希望能把这个流程的大概介绍清楚,后面可以针对某一个局部细节可以细细钻研,学习里面的技巧,理解其中的设计原委。
来源: http://www.cnblogs.com/fuckgiser/p/6115421.html