一、前言
实现思路:先实现填充的半圆,在实现线框,再用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最直接,为什么官网不采用呢,先将这个疑问留在这。
原创不易,记得点赞加关注哦,我会持续分享实用的功能(:-