Cesium实现半球扫描效果

一、前言

         实现思路:先实现填充的半圆,在实现线框,再用Cesium.PrimitiveCollection一结合,就勉强实现,我用的是cesium1.98版本,同时实现两者并支持shader没整出来:)。

二、效果

三、关键代码

/**
 * 功能:雷达扫描特效类。
 * 作者:airduce
 * 历史:2025年6月24日16:34:52 新建
 */

import * as Cesium from 'cesium';

class RadarScanEffectFilled extends Cesium.Primitive {
    constructor(options) {
        super();
        this._options = options;
        this._ready = false;
        this._geometryInstances = null;
        this._primitive = null;
        this._appearance = null;
        this._radius = options.radius || 10000.0; // 初始化半径
        this.initializeResources();
    }

    // 初始化资源
    initializeResources() {
        // 只需要绘制上半球,垂直角度范围调整为0到90度
        var totalVAngle = 90;
        // 水平角度范围调整为360度,形成一个完整的圆
        var totalHAngle = 360;
        var step = 5; // 减小步长以提高模型精度

        // 计算顶点数量和索引数量
        var vSteps = totalVAngle / step + 1;
        var hSteps = totalHAngle / step + 1;
        var vertexCount = vSteps * hSteps;  //顶点数量
        var indexCount = (vSteps - 1) * (hSteps - 1) * 6; // 每个四边形由两个三角形组成,每个三角形3个顶点

        // 创建顶点位置数组和索引数组
        var positions = new Float64Array(vertexCount * 3);
        var indices = new Uint16Array(indexCount);

        var vertexIndex = 0;
        var indexIndex = 0;
        var radius = this._radius;

        //顶点着色器
        var vs = `attribute vec3 position3DHigh;
               attribute vec3 position3DLow;
               attribute vec4 color;
               attribute float batchId;
               varying vec4 v_color;
               varying vec3 v_position;
               void main()
               {
                   v_position = position3DHigh + position3DLow;
                   vec4 p = czm_computePosition();//返回相对eye的vec4 位置。
                   gl_Position = czm_modelViewProjectionRelativeToEye * p;

                   v_color = color;
               }`;

        //片元着色器 - 修正了扫描方向和区域判断
        var fs = `varying vec4 v_color;
               varying vec3 v_position;
               uniform float u_time;
               uniform float u_scanWidth; // 扫描扇区宽度(弧度)

               void main()
               {
                   // 计算当前点在XOY平面上的角度(相对于X轴)
                   float currentAngle = atan(v_position.y, v_position.x);

                   // 计算扫描线的角度(随时间变化,取负号使旋转方向为顺时针)
                   float scanAngle = mod(-u_time, 6.28318); // 2PI

                   // 计算当前点相对于扫描线的角度差(带方向)
                   float angleDiff = mod(currentAngle - scanAngle + 9.42477, 6.28318) - 3.14159;

                   // 判断是否在扫描扇区内(只考虑扫描线前方的区域)
                   if(angleDiff > 0.0 && angleDiff < u_scanWidth) {
                       // 计算相对距离(0-1范围),0表示扫描线位置,1表示扫描区域末尾
                       float relativeDistance = angleDiff / u_scanWidth;

                       // 透明度从1.0(扫描线位置)线性过渡到几何体的基础透明度(扫描区域末尾)
                       float alphaFactor = 1.0 - relativeDistance;

                       // 扫描线颜色(这里使用黄色)
                       vec3 scanColor = vec3(1.0, 1.0, 0.0);

                       // 最终颜色(混合原始颜色和扫描颜色)
                       gl_FragColor = vec4(mix(v_color.rgb, scanColor, alphaFactor), 
                                          mix(v_color.a, 1.0, alphaFactor));
                   } else {
                       // 不在扫描区内,保持原始颜色
                       gl_FragColor = v_color;
                   }
               }`;

        // 将角度从度转换为弧度的辅助函数
        function toRadians(degrees) {
            return degrees * Math.PI / 180;
        }

        // 生成半球的顶点
        for (var v = 0; v <= totalVAngle; v += step) {
            for (var h = 0; h <= totalHAngle; h += step) {
                var hRad = toRadians(h);
                var vRad = toRadians(v);

                // 球面坐标转笛卡尔坐标
                positions[vertexIndex++] = radius * Math.sin(vRad) * Math.cos(hRad);
                positions[vertexIndex++] = radius * Math.sin(vRad) * Math.sin(hRad);
                positions[vertexIndex++] = radius * Math.cos(vRad);
            }
        }

        // 生成索引数据,形成三角形面
        for (var vv = 0; vv < vSteps - 1; vv++) {
            for (var hh = 0; hh < hSteps - 1; hh++) {
                var idx = vv * hSteps + hh;

                // 第一个三角形
                indices[indexIndex++] = idx;
                indices[indexIndex++] = idx + hSteps;
                indices[indexIndex++] = idx + 1;

                // 第二个三角形
                indices[indexIndex++] = idx + 1;
                indices[indexIndex++] = idx + hSteps;
                indices[indexIndex++] = idx + hSteps + 1;
            }
        }

        // 创建两个几何体:一个用于填充面,一个用于线框
        var filledGeometry = new Cesium.Geometry({
            attributes: {
                position: new Cesium.GeometryAttribute({
                    componentDatatype: Cesium.ComponentDatatype.DOUBLE,
                    componentsPerAttribute: 3,
                    values: positions
                })
            },
            indices: indices,
            primitiveType: Cesium.PrimitiveType.TRIANGLES,
            boundingSphere: Cesium.BoundingSphere.fromVertices(positions)
        });

        // 创建填充面的实例
        var filledInstance = new Cesium.GeometryInstance({
            geometry: filledGeometry,
            attributes: {
                color: Cesium.ColorGeometryInstanceAttribute.fromColor(
                    Cesium.Color.AQUA.withAlpha(0.1)
                )
            },
            id: 'radarScan-filled'
        });

        // 创建外观(共享相同的着色器)
        var appearance = new Cesium.PerInstanceColorAppearance({
            flat: true,
            translucent: true,
            vertexShaderSource: vs,
            fragmentShaderSource: fs
        });

        this._geometryInstances = filledInstance;
        this._ready = true;
        this._appearance = appearance;
    }

    // 更新半径的方法
    updateRadius(newRadius) {
        this._radius = newRadius;
        this._ready = false;
        if (this._primitive) {
            this._primitive = this._primitive.destroy();
            this._primitive = null;
        }
        this.initializeResources();
    }

    // 更新方法,每帧调用
    update(frameState) {
        if (!this._ready) {
            return;
        }

        // 如果还没有创建内部Primitive,则创建它
        if (!this._primitive) {
            this._primitive = new Cesium.Primitive({
                geometryInstances: this._geometryInstances,
                appearance: this._appearance,
                asynchronous: false
            });

            var material = {
                isTranslucent: function () { return true; },
                update: function () { },
                _uniforms: {
                    u_time: function () { return Cesium.getTimestamp() / 1500 },
                    u_scanWidth: function () { return 0.9 }
                }
            };
            this._appearance.material = material;
        }

        // 更新内部Primitive
        this._primitive.update(frameState);
    }

    // 销毁资源
    destroy() {
        if (this._primitive) {
            this._primitive = this._primitive.destroy();
        }
        return Cesium.destroyObject(this);
    }
}

export default RadarScanEffectFilled;

四、顶点着色器解析

在官网例子中,看到如上图给gl_Position赋值,关于czm_computePosition(),我翻了一下旧版本的源码只知道了如下的说明,没找到具体的函数定义。

心有些不甘呀,我继续找czm_modelViewProjectionRelativeToEye的源码,在AutomaticUniforms.js的定义如下:

继续找uniformState.modelViewProjectionRelativeToEye,如下:

继续找_modelViewProjectionRelativeToEye的赋值代码:

到这里,终于看出点意思了,_modelViewProjectionRelativeToEye等于模型视图矩阵*投影矩阵,这很标准,主要关注一下modelViewRelativeToEye:

这里所谓的_modelViewRelativeToEye是在标准的modelView的基础上少赋值了12,13,14这三个分量,而这三个分量所代表的是位移,也就是说_modelViewRelativeToEye撇去了modelView的位移部分,将物体转换到以相机为原点的坐标系(Eye Space),但忽略物体与相机的相对位置,使物体始终固定在相机原点(位置 (0,0,0),到这里就清楚了czm_modelViewProjectionRelativeToEye的含义:是以相机位置为原点的mvp矩阵。
       上面提到既然找不到czm_computePosition()的定义,那么,能不能不使用czm_modelViewProjectionRelativeToEye,而是直接使用czm_modelViewProjection呢,果不其然,找到了以下方法:

 v_position = position3DHigh + position3DLow;
 gl_Position = czm_modelViewProjection * vec4(v_position,1.0);

其中position3DHigh和position3DLow就是咋们定义几何体的这些点:

因为float表达精度范围的问题,cesium采用分开存储的方式。

这里我又在想,既然czm_modelViewProjectionRelativeToEye撇掉了相对于相机位置点的位移,那么我在几何体位置点加上这段偏移行不行,于是又有以下方式:

vec4 p = czm_translateRelativeToEye(position3DHigh, position3DLow);
gl_Position = czm_modelViewProjectionRelativeToEye * p;

其中czm_translateRelativeToEye的定义如下:

这里减去了相机的位置,这不正好把撇掉的补回来了吗。

好了,来个总结:

        在顶点着色器中给gl_Position赋值的方法有以下三种:

其中方式1是官方例子中的,感觉方式2最直接,为什么官网不采用呢,先将这个疑问留在这。

原创不易,记得点赞加关注哦,我会持续分享实用的功能(:-

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一梦、んんん

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值