Three.js教程
cesium
实现抛物线+流动线

实现抛物线+流动线效果

通过本文,你将学习如何在 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", {}) 用来在一个 idcesiumContainer 的 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)01 时都是 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,
      }),
    })
  );
};

关键点解析:

  • 这里我们分别添加了两条线
    1. 一条普通的多彩渐变线(利于观察抛物线形状)。
    2. 一条带有流动材质的“粗线”(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)