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精度问题导致的,我们下一节在说一下第一个问题。

时间: 2024-12-19 16:24:28

Cesium原理篇:Batch的相关文章

Cesium原理篇:7最长的一帧之Entity(下)

上一篇,我们介绍了当我们添加一个Entity时,通过Graphics封装其对应参数,通过EntityCollection.Add方法,将EntityCollection的Entity传递到DataSourceDisplay.Visualizer中.本篇则从Visualizer开始,介绍数据的处理,并最终实现渲染的过程. CesiumWidget.prototype.render = function() { if (this._canRender) { this._scene.initializ

Cesium原理篇:3最长的一帧之地形(2:高度图)

       这一篇,接着上一篇,内容集中在高度图方式构建地球网格的细节方面.        此时,Globe对每一个切片(GlobeSurfaceTile)创建对应的TileTerrain类,用来维护地形切片的相关逻辑:接着,在requestTileGeometry中,TileTerrain会请求对应该切片的地形数据.如果读者对这部分有疑问的话,可以阅读<Cesium原理篇:1最长的一帧之渲染调度>:最后,如果你是采用的高度图的地形服务,地形数据对应的是HeightmapTerrainDat

Cesium原理篇:3最长的一帧之地形(3:STK)

有了之前高度图的基础,再介绍STK的地形相对轻松一些.STK的地形是TIN三角网的,基于特征值,坦白说,相比STK而言,高度图属于淘汰技术,但高度图对数据的要求相对简单,而且支持实时构建网格,STK具有诸多好处,但确实有一个不足,计算量比较大,所以必须预先生成.当然,Cesium也提供了一个Online的免费服务,不过因为是国外服务器,所以性能和不稳定因素都不小.好的东西自然得来不易,所以不同的层次,根据具体的情况选择不同的方案,技术并不是唯一决定因素,甚至不是主要因素. CesiumTerra

Cesium原理篇:6 Renderer模块(2: Texture)

Texture也是WebGL中重要的概念,使用起来也很简单.但有句话叫大道至简,如果真的想要用好纹理,里面的水其实也是很深的.下面我们来一探究竟. 下面是WebGL中创建一个纹理的最简过程: var canvas = document.getElementById("canvas"); var gl = canvas.getContext("webgl"); // 创建纹理句柄 var texture = gl.createTexture(); // 填充纹理内容

Cesium原理篇:glTF

关键字:Cesium glTF WebGL技术 大纲: 1 glTF简介,这是一个什么东西,有哪些特点 2 Cesium如何加载,渲染glTF,逻辑结构和关键技术 3 个人总结,从glTF学习如何设计一个二进制格式,个人想法分享 共计 4000字 | 建议阅读时间 未知 1 glTF简介 之前介绍了Cesium的Property,Material,Batch,GroundPrimitive这些内容,可以说是简单地物和风格的解决思路.当Cesium把这些技术点整合起来,我们便具备了渲染模型的威力.

Cesium原理篇:Material

Shader 首先,在本文开始前,我们先普及一下材质的概念,这里推荐材质,普及材质的内容都是截取自该网站,我觉得他写的已经够好了.在开始普及概念前,推荐一首我此刻想到的歌<光---陈粒>. 在真实世界里,每个物体会对光产生不同的反应.钢看起来比陶瓷花瓶更闪闪发光,一个木头箱子不会像钢箱子一样对光产生很强的反射.每个物体对镜面高光也有不同的反应.有些物体不会散射(Scatter)很多光却会反射(Reflect)很多光,结果看起来就有一个较小的高光点(Highlight),有些物体散射了很多,它们

Cesium原理篇:6 Render模块(3: Shader)

在介绍Renderer的第一篇,我就提到WebGL1.0对应的是OpenGL ES2.0,也就是可编程渲染管线.之所以单独强调这一点,算是为本篇埋下一个伏笔.通过前两篇,我们介绍了VBO和Texture两个比较核心的WebGL概念.假设生产一辆汽车,VBO就相当于这个车的骨架,纹理相当这个车漆,但有了骨架和车漆还不够,还需要一台机器人来加工,最终才能成产出这辆汽车.而Shader模块就是负责这个生产的过程,加工参数(VBO,Texture),执行渲染任务. 这里假设大家对Shader有一个基本的

Cesium原理篇:6 Render模块(4: FBO)

Cesium不仅仅提供了FBO,也就是Framebuffer类,而且整个渲染过程都是在FBO中进行的.FBO,中文就是帧缓冲区,通常都属于高级用法,但其实,如果你了解了它的基本原理后,用起来还是很简单的,关键在于理解.比如你盖楼,地基没打好,盖第一层楼,还可以,盖第二层楼,有点挫了,盖第三层楼,塌了.你会认为第三层楼(FBO)太难了,其实根本原因还是出在地基上. 窗口系统所管理的帧缓存有自己的缓存对象(颜色,深度和模板),它们诞生于窗口创建前,而我们自己创建的帧缓冲,这些缓存对象则需要自己来手动

Cesium原理篇:3最长的一帧之地形(4:重采样)

       地形部分的原理介绍的差不多了,但之前还有一个刻意忽略的地方,就是地形的重采样.通俗的讲,如果当前Tile没有地形数据的话,则会从他父类的地形数据中取它所对应的四分之一的地形数据.打个比方,当我们快速缩放影像的时候,下一级的影像还没来得及更新,所以会暂时把当前Level的影像数据放大显示, 一旦对应的影像数据下载到当前客户端后再更新成精细的数据.Cesium中对地形也采用了这样的思路.下面我们具体介绍其中的详细内容.        上图是一个大概流程,在创建Tile的时候(prepa