实现抛物线+流动线效果
通过本文,你将学习如何在 Cesium 中实现抛物线轨迹以及在轨迹上添加流动线动态效果。
效果预览
先上效果动图,大家可以直观感受一下抛物线流动的炫酷效果。
1. 初始化 Cesium Viewer
// 引入 Cesium 相关包
import * as Cesium from "cesium";
import "cesium/Build/Cesium/Widgets/widgets.css";
// 设置 Cesium Ion 令牌
Cesium.Ion.defaultAccessToken = import.meta.env.VITE_CESIUM_ION_TOKEN || "";
// 初始化 Cesium Viewer
const viewer = new Cesium.Viewer("cesiumContainer", {});
- 首先,我们从
cesium
包中导入所有对象,并且引入默认的 CSS 样式。 Cesium.Ion.defaultAccessToken
用来设置你的 Ion 令牌,没有的话默认会报错或出现水印。Cesium.Viewer("cesiumContainer", {})
用来在一个id
为cesiumContainer
的 DOM 容器中初始化 Viewer。此处未做特殊配置,使用 Cesium 自带默认配置即可。
2. 实现抛物线绘制
在 Cesium 中绘制两点之间的直线非常简单,我们可以直接使用 PolylineGeometry
并给定两个端点即可。然而,很多场景下,我们会希望线条不是“贴着地面”或简单地“穿过地球”,而是呈现一种拱起或弹道的效果,这在演示“导弹轨迹”“飞行航线”“数据流可视化”等情景中非常常见。
基本思路
- 我们将起点和终点之间在“经度纬度”空间做一个线性插值,也就是把两点的经纬度分割成若干份(比如 100 份),让第 i 个点的经纬度从起点平滑过渡到终点。
- 在插值得到的经纬度基础上,再为每个点赋予一个“高度值”,这个高度值随插值比例(
ratio
)变化,形成抛物线形状。
定义 setPathData
函数
/**
* 生成抛物线路径数据
*/
const setPathData = (pointStart, pointEnd, options = {}) => {
// 合并默认选项
const height = options.height || 50000;
const pointsCount = options.pointsCount || 100;
// 提取起点和终点的经纬度
const startLon = pointStart[0];
const startLat = pointStart[1];
const endLon = pointEnd[0];
const endLat = pointEnd[1];
// 预分配数组(每个点有经度、纬度、高度共 3 个值)
const positionsArray = new Array(pointsCount * 3);
// 生成抛物线点集
for (let i = 0; i < pointsCount; i++) {
// 计算当前点在起点到终点连线上的比例
const ratio = i / (pointsCount - 1);
// 线性插值计算当前经纬度
const lon = startLon + ratio * (endLon - startLon);
const lat = startLat + ratio * (endLat - startLat);
// 计算抛物线高度 - 利用公式 y = 4 * h * x * (1 - x)
const curHeight = 4 * height * ratio * (1 - ratio);
// 写入数组
const idx = i * 3;
positionsArray[idx] = lon;
positionsArray[idx + 1] = lat;
positionsArray[idx + 2] = curHeight;
}
// 返回 Cesium 坐标数组
return Cesium.Cartesian3.fromDegreesArrayHeights(positionsArray);
};
为什么选择 y = 4 _ height _ x * (1 - x)
假设我们定义 x
在 [0, 1]
区间(对应起点到终点的进度),那么 x*(1-x)
在 0
和 1
时都是 0
,在 0.5
时是最大值 0.25
。
因此:
就能让抛物线在 x=0.5
的时候达到 height
。从数值上讲,这是一个简单而优雅的抛物线,方便我们在 0~1 的插值区间使用。如果你需要让抛物线偏前或偏后拱起,也可以自行改变这部分公式。
从度数转为 Cartesian3
Cesium 在世界坐标中使用Cartesian3
(笛卡尔坐标),而我们更常用经纬度(度数)+ 高度来表示地理位置。因此需要在最后一步使用:
Cesium.Cartesian3.fromDegreesArrayHeights(positionsArray);
positionsArray
的结构必须满足[lon1, lat1, alt1, lon2, lat2, alt2, ...]
这样顺序才正确。fromDegreesArrayHeights
会帮我们自动做坐标系变换、球面投影计算,得到 3D 场景中的坐标点。
3. 流动线材质
有了抛物线路径,我们还需要实现“流动”的视觉效果。Cesium 默认提供了多种材质,但是无法直接满足“流动”的需求,因此需要自定义 GLSL 片元着色器。
创建自定义材质
/**
* 创建流动线材质
*/
const createFlowingLineMaterial = () => {
// GLSL 代码
const flowingLineGLSL = `
float SPEED_STEP = 0.01; // 增加速度步长,使光线移动更快
vec4 drawLight(float xPos, vec2 st, float headOffset, float tailOffset, float widthOffset) {
float lineLength = smoothstep(xPos + headOffset, xPos, st.x) - smoothstep(xPos, xPos - tailOffset, st.x);
float lineWidth = smoothstep(widthOffset, 0.5, st.y) - smoothstep(0.5, 1.0 - widthOffset, st.y);
return vec4(lineLength * lineWidth);
}
czm_material czm_getMaterial(czm_materialInput materialInput) {
// 获取基础材质
czm_material m = czm_getDefaultMaterial(materialInput);
// sinTime 用来让光线往复运动
float sinTime = sin(czm_frameNumber * SPEED_STEP * speed);
float xPos = 0.0;
if (sinTime < 0.0) {
xPos = cos(czm_frameNumber * SPEED_STEP * speed) + 1.0 - tailsize;
} else {
xPos = -cos(czm_frameNumber * SPEED_STEP * speed) + 1.0 - tailsize;
}
// 计算光带范围
vec4 v4_color = drawLight(xPos, materialInput.st, headsize, tailsize, widthoffset);
// 光带的“核心”更加亮,宽度更小
vec4 v4_core = drawLight(xPos, materialInput.st, coresize, coresize * 2.0, widthoffset * 2.0);
// 叠加核心与整体颜色
m.diffuse = color.xyz + v4_core.xyz * v4_core.w * 0.8;
m.alpha = pow(v4_color.w, 3.0);
return m;
}
`;
// 创建 Cesium 材质对象
return new Cesium.Material({
fabric: {
type: "FlowingLineMaterial",
uniforms: {
color: new Cesium.Color(0.0, 1.0, 0.0, 0.5), // 线条的整体颜色(绿色+透明度)
speed: 2, // 流动速度
headsize: 0.05, // 光带头部尺寸
tailsize: 0.5, // 光带尾部尺寸
widthoffset: 0.1, // 光带在宽度方向的占比
coresize: 0.05, // 核心光带
},
source: flowingLineGLSL,
},
});
};
关键点解析:
flowingLineGLSL
: 这是自定义的片元着色器程序,利用smoothstep
函数绘制渐变边缘,使光带看起来柔和。czm_frameNumber
:Cesium 在每一帧渲染时都会自增,可以视为“时间”的离散变量。xPos = ... + 1.0 - tailsize
:这是一个偏移,用来调整光带的初始位置。cos(...)
、sin(...)
等:利用三角函数使 xPos 在区间内往复运动,从而产生流动效果。
4. 将一切添加到场景中
现在我们已经有了抛物线坐标和流动线材质。接下来就是把它们“合并”在 Cesium 场景里。
/**
* 添加抛物线到场景
*/
const addParabolaToScene = (viewer, startPoint, endPoint, options = {}) => {
// 获取路径坐标
const cartesianPositions = setPathData(startPoint, endPoint, options);
const pointsCount = options.pointsCount || 100;
// 创建颜色数组,用于给普通线条渐变着色
const colors = new Array(pointsCount);
for (let i = 0; i < pointsCount; i++) {
const ratio = i / (pointsCount - 1);
// 蓝色逐渐过渡到红色
colors[i] = Cesium.Color.lerp(
new Cesium.Color(0.0, 0.0, 1.0, 1.0), // 蓝色
new Cesium.Color(1.0, 0.0, 0.0, 1.0), // 红色
ratio,
new Cesium.Color()
);
}
// 1. 添加普通线条(渐变色)
viewer.scene.primitives.add(
new Cesium.Primitive({
geometryInstances: new Cesium.GeometryInstance({
geometry: new Cesium.PolylineGeometry({
positions: cartesianPositions,
width: 2.0,
vertexFormat: Cesium.PolylineColorAppearance.VERTEX_FORMAT,
colors: colors,
colorsPerVertex: true,
}),
}),
appearance: new Cesium.PolylineColorAppearance(),
})
);
// 2. 创建流动线材质
const flowingMaterial = createFlowingLineMaterial();
// 3. 添加流动线
viewer.scene.primitives.add(
new Cesium.Primitive({
geometryInstances: new Cesium.GeometryInstance({
geometry: new Cesium.PolylineGeometry({
positions: cartesianPositions,
width: 20.0,
vertexFormat: Cesium.VertexFormat.ALL,
}),
}),
appearance: new Cesium.PolylineMaterialAppearance({
material: flowingMaterial,
}),
})
);
};
关键点解析:
- 这里我们分别添加了两条线:
- 一条普通的多彩渐变线(利于观察抛物线形状)。
- 一条带有流动材质的“粗线”(
width: 20.0
),用于呈现流动效果。
viewer.scene.primitives.add(...)
用于添加Primitive
到场景。PolylineGeometry
用来绘制线条几何体,并通过appearance
指定呈现方式。colorsPerVertex: true
说明颜色是根据顶点渐变插值的。
代码
https://github.com/calmound/gis-demo/tree/main/src/components/parabola (opens in a new tab)
参考
https://juejin.cn/post/7268896098827468837 (opens in a new tab)