Cesium 高性能扩展之DrawCommand(三):显隐和点选

5 篇文章 3 订阅
3 篇文章 9 订阅

DrawCommand 是 Cesium 渲染器的核心类,常用的接口 EntityPrimitiveCesium3DTileSet,以及地形和影像的渲染等等,底层都是一个个 DrawCommand 完成的。在进行扩展开发、视觉特效提升、性能优化、渲染到纹理(RTT),甚至基于 Cesium 封装自己的开发框架,定义独家数据格式等等,都需要开发人员对 DrawCommand 熟练掌握。而这部分接口,Cesium 官方文档没有公开,网上的相关资料也比较少,学习起来比较困难,所以接下来我们用几期文章,由浅入深,实用为主,力求全面地介绍 DrawCommand 及相关类的运用。

第一次被关注我们的小伙伴催更,还是挺开心的😂,公司事务略忙,只有深夜码字来赶稿了~那么本篇我们就按照计划往下写,介绍自定义Primitive的显隐控制,以及如何支持scene.pick,实现要素拾取查询。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v4zA2IeF-1642559060254)(./drawCommand3.png)]

1、显隐控制

从前面两篇的代码看,显隐控制是非常简单的,我们只需要在update方法的开头加入显隐判断,如果隐藏则直接退出,否则继续。

//...
update(frameState) {
    if (!this.show) return;
    //...
}
//...

考虑一下,如果我们将多个要素的几何体合并成一个几何体,以达到减少绘图批次(DrawCommand的数量)提升性能的目的,这种情况下,上述方法将会同时显示或者隐藏这些要素,那么该如何控制其中任一要素的显隐呢?

参考Cesium的其他Primitive类,会发现他们为每一个顶点创建了一个batchId或者a_batchId属性,这个属性非常重要,除了可以用于控制显隐外,在3D Tiles样式引擎、要素拾取查询(pick)等都起到关键作用。

这里简单介绍基于batchId的显隐控制实现思路:

  • 为每一个要素创建batchId,从0开始编号;
  • 为要素几何体创建batchId顶点属性,并将batchId值逐个写入顶点属性数组;
  • 合并所有要素几何体;
  • 创建一个二维数组类型的纹理贴图,宽度等于要素数量,高度为1,用01表示要素隐藏或者显示,写入纹理数据;
  • shader中根据batchId并计算uv,从显隐纹理获取要素的显隐标记,如果小于1discard
  • 当某个要素显隐发生变化时,更新显隐纹理。

目前我们先不要想那么复杂,等掌握了更多技能,需要设计这么复杂的自定义Primitve的时候,自然能理解并参考Cesium的代码来实现这一显隐控制方法。

2、支持pick实现点选

要素拾取查询(Pick),是3D交互的重要基础,想象一下一个3D应用,所有图形要素怎么点击都没有反应,那它跟一个视频播放器就没有什么两样了~

Cesium提供了统一的要素拾取查询机制,拾取API封装在Cesium.Scene中,前面我们介绍3D描边等后期处理时就曾使用过,非常简单。

 viewer.screenSpaceEventHandler.setInputAction(e => {
    var picked = viewer.scene.pick(e.position)
    if(picked){
        //...
    }
 })

那么如果让我们的自定义Primitive能够被scene.pick点击拾取到呢?

2.1、context创建pickId
/**
 * @param {Cesium.Context} context 
 */
createCommand(context) {
    /**
     * @type {Cesium.PickId}
     */
    var pickId = this.pickId;
    if (!pickId) {
        pickId = context.createPickId({
            primitive: this,
            description: '要素描述内容',
            //[key:string]:any
        });
        this.pickId = pickId
    }

    //
    var strPickId = 'czm_pickColor';
    var czm_pickColor = pickId.color;
    

    //...
}
2.2、shader声明pickId变量

这里可以pickId变量类型有两种:uniformvarying

  • uinform:如果DrawCommand绘制的是单一要素,所绘制的内容就是一个整体,则使用uniform,所有顶点的pickId颜色是一样的,通过uniformMap来传递;
  • varying:如果绘制的是多个要素,绘制内容可能分为多个部件,只是合并为一个批次来渲染,可能每个部件需要能够被单独点击拾取,则使用varying,需要将每一个部件要素的pickId颜色写入对应的几何顶点属性,顶点着色器接收颜色值,并通过varying传递到偏远着色器。
2.2.1、uniform类pickId
var fs = `
uniform vec3 color; 
//
uniform vec4 ${strPickId}; 
void main(){
    gl_FragColor=vec4( color , 1. );
}
`;
var shaderProgram = Cesium.ShaderProgram.fromCache({
    context: context,
    vertexShaderSource: vs,
    fragmentShaderSource: fs,
    attributeLocations: attributeLocations
})
2.2.2、 varying类pickId
var vs = `
attribute vec3 position;
//声明pickColor属性,接收顶点pickId颜色
attribute vec4 pickColor;
//
varying vec4 ${strPickId}; 
void main(){
    ${strPickId}=pickColor; 
    gl_Position = czm_projection  * czm_modelView * vec4( position , 1. );
}
`;
var fs = `
uniform vec3 color; 
//
varying vec4 ${strPickId}; 
void main(){
    gl_FragColor=vec4( color , 1. );
}
`;
var shaderProgram = Cesium.ShaderProgram.fromCache({
    context: context,
    vertexShaderSource: vs,
    fragmentShaderSource: fs,
    attributeLocations: attributeLocations
})
2.2.3、Cesium对DrawCommand的后续加工

大家可能很好奇,为什么pickId变量,只是声明,却没有见到具体的使用,怎么能实现点击拾取呢?

这个问题,大家不妨在绘制出结果后(延迟一段时间)打印出DrawCommand对象看看:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IIvbu2v9-1642559060255)(./derivedCommands.png)]

我们接着展开看看pickCommand._shaderProgram._vertexShaderText

#ifdef GL_FRAGMENT_PRECISION_HIGH
    precision highp float;
#else
    precision mediump float;
    #define highp mediump
#endif

#define OES_texture_float_linear
#define OES_texture_float

#line 0
#line 0

uniform vec3 color; 
uniform vec4 czm_pickColor; 
void czm_non_pick_main(){
    gl_FragColor=vec4( color , 1. );
}
        
#line 0
void main() 
{ 
    czm_non_pick_main(); 
    if (gl_FragColor.a == 0.0) { 
        discard; 
    } 
    gl_FragColor = czm_pickColor; 
}

Cesiumpick操作创建了一个新的DrawCommand副本pickCommand,并且基于我们的shader进行加工处理,在新的main函数使用了我们声明的pickId变量。

同时大家可以看到,不止多了一个pickCommand,还增加了好几个其他DrawCommand副本,说明我们创建好的DrawCommand并没有直接进入渲染阶段,而是要经过Cesium渲染器进行加工处理,才最终交付执行渲染。

想一下,如果我们开启了阴影那么会不会又增加一个副本呢?这个问题我们留到下一篇来探讨。

2.3、传递pickId颜色

2.2节相对应,如果pickId变量为uniform类,则通过unformMap传递其颜色,如果为varying则需要修改几何体,增加pickId颜色相关的顶点属性。

2.3.1、uniformMap传递
 var uniformMap = {
    color() {
        return Cesium.Color.GRAY
    },
    //如果pickId为uniform类,则这里必须传递pickId颜色;为varying则可以去掉,不去掉也不影响使用
    [strPickId]() {
        return czm_pickColor;
    }
}
2.3.2、顶点属性传递

这种情况下,2.1创建的pickId就不够用了,我们需要为每一个要素的几何体创建一个pickId。我们定一个方法,来处理每一个要素的几何体。

/**
 * @param {object} feature 
 * @param {Cesium.Geometry} geometry
 * @param {Cesium.Context} context 
 */
setPickId(feature, geometry, context) {
    //创建pickId
    var pickId = context.createPickId({
        feature: feature,
        primitive: this
    })
    //保存pickId,用于后期处理、释放等
    this.pickIds.push(pickId);

    //增加几何顶点属性pickColor,这里属性名称和顶点着色器里面的对应attribute变量名称一致

    var ptCount = geometry.attributes.position.values.length / 3
    var pickColors = [], { red, green, blue, alpha } = pickId.color;
    for (let i = 0; i < ptCount; i++) {
        pickColors.push(red, green, blue, alpha)
    }
    pickColors = new Float32Array(pickColors);

    geometry.attributes.pickColor = new GeometryAttribute({
        componentDatatype: ComponentDatatype.FLOAT,
        componentsPerAttribute: 4,
        normalize: false,
        values: pickColors
    })
}
2.4、drawCommand指定pickId变量名
this.drawCommand = new Cesium.DrawCommand({
    modelMatrix: modelMatrix,
    vertexArray: va,
    shaderProgram: shaderProgram,
    uniformMap: uniformMap,
    renderState: renderState,
    pass: Cesium.Pass.OPAQUE,
    //
    pickId: strPickId
})
2.5、释放pickId

需要注意,当我们不需要pickId或者drawCommand释放或者整个Primitive释放时,一定要释放pickId,否则将会出现内存泄漏,因为我们会将primitive对象绑定到pickId,这个pickId不释放的话,会一直保存在context的缓存里,直到整个程序消亡。

下面是完整的drawCommandd释放方法,包含了pickId的释放。

destroy(){
    var drawCommand = this.drawCommand;
    if (drawCommand) {
        var va = drawCommand.vertexArray, sp = drawCommand.shaderProgram;
        if (!va.isDestroyed()) va.destroy();
        if (!sp.isDestroyed || !sp.isDestroyed()) {
            sp.destroy();
        }
        drawCommand.isDestroyed = function returnTrue() {
            return true
        };
        drawCommand.uniformMap = undefined;
        drawCommand.renderState = Cesium.RenderState.removeFromCache(drawCommand.renderState)
        this.drawCommand=null
    }
    //单个要素的pickId
    if(this.pickId){
        this.pickId.destroy()
        this.pickId=null
    }
    //多个要素的pickId
    if(this.pickIds){
        this.pickIds.forEach(pickId=>{
            pickId.destroy()
        })
        this.pickIds.length=0;
    }
}

3、点选示例及效果视频

var origin = Cesium.Cartesian3.fromDegrees(106, 26, 250000)
var modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(origin)
var primitive = new MyPrimitive(modelMatrix);
viewer.scene.primitives.add(primitive)

//拾取结果高亮
var edgedStage = createEdgeStage()
edgedStage.uniforms.useSingleColor = true;
edgedStage.visibleEdgeColor = Cesium.Color.DARKORANGE
viewer.postProcessStages.add(edgedStage)

//测试pick
viewer.screenSpaceEventHandler.setInputAction(e => {
    var picked = viewer.scene.pick(e.position)

    viewer.selectedEntity = undefined
    edgedStage.selected = [];

    if (picked && picked.primitive && picked.primitive instanceof MyPrimitive) {
        viewer.selectedEntity = new Cesium.Entity({
            name: "拾取结果",
            description: picked.description
        })
        edgedStage.selected = [picked.primitive]
    }

}, Cesium.ScreenSpaceEventType.LEFT_CLICK)

有没有发现,当我们加入pickId之后,不但点击拾取查询功能实现了,包括后期处理高亮也自然支持了,这是因为Cesium后期处理的要素选择也是基于pickId实现的。

本篇到此结束,如果觉得有用,不妨点赞+分享,让更多小伙伴一起来交流学习吧!
下一篇我们将介绍阴影(shadows)和实例化(instancing),敬请期待!

欢迎关注微信公众号【三维网格3D】,第一时间获取最新文章

在这里插入图片描述

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值