Vue3 + Cesium 实现热气球第一人称自动飞行(支持手机端)

0 阅读4分钟

Vue3 + Cesium 实现热气球第一人称自动飞行(支持手机端)

前言

大家好,今天给大家带来一个Cesium 实现的热气球第一人称飞行的完整 Demo。

整个项目基于 Vue3 + Cesium 开发,实现了:

  • 热气球沿预定路线自动飞行

  • 第一人称视角跟随

  • 循环飞行

  • 完美支持手机端 / PC 端

本案例中Cesium版本为1.141.0

演示图示如下:

b.gif

演示地址如下:

[](web3d-demo-collection (xiazhi.tech))

完整代码

1. template 结构:

<template>
    <div class="main">
        <!-- 地图主容器 -->
        <div class="content" ref="content" id="earth"></div>
        <!-- Loading 遮罩 -->
        <div class="loading" v-if="!isLoading">Loading...</div>
    </div>
</template>

2. script 代码:

<script setup>
    import { onMounted, nextTick, ref, onUnmounted } from 'vue';
    import { token } from '../../utils/common.js';

    // 飞行路径数据(经纬度 + 高度 + 时间)
    const pathData = ref([
        { longitude: 121.168742, latitude: 31.296495, height: 3400, time: 0 },
        { longitude: 121.807188, latitude: 30.976068, height: 3400, time: 720 }
    ]);

    // 是否加载完成(控制 Loading)
    let isLoading = ref(false);


    let myMar = null;

    onMounted(() => {
        nextTick(() => {
            // DOM 准备好后再初始化地图
            initMap();
        });
    });

    onUnmounted(() => {
        // 移除相机更新监听
        window.viewer.scene.preUpdate.removeEventListener(adjustVoid);
        // 销毁 Cesium Viewer,释放 WebGL 资源
        if (window.viewer) {
            window.viewer.destroy();
            window.viewer = null;
        }
        // 清除定时器
        if (myMar) {
            clearTimeout(myMar);
            myMar = null;
        }
    });

    // 初始化地图的方法
    const initMap = () => {
        Cesium.Ion.defaultAccessToken = token;
        // 设置默认相机视角(中国区域)
        Cesium.Camera.DEFAULT_VIEW_RECTANGLE = Cesium.Rectangle.fromDegrees(89.5, 20.4, 110.4, 61.2);
        // 创建 Cesium Viewer
        window.viewer = new Cesium.Viewer('earth', {
            animation: true,     // 开启动画控件,播放/暂停(必须为true)
            timeline: true,     // 开启显示时间轴(必须为true)
            infoBox: false,     // 关闭点击信息弹窗
            geocoder: false,     // 关闭地名搜索
            homeButton: false,     // 关闭返回首页按钮
            sceneModePicker: false,     // 关闭 2D/3D 切换
            baseLayerPicker: false,     // 关闭底图选择器
            navigationHelpButton: false,     // 关闭操作帮助
            fullscreenButton: false,     // 关闭全屏按钮
            selectionIndicator: false,     // 关闭选中高亮
            shouldAnimate: true     // 开启允许时间轴自动推进(必须为true)
        });
        // 设置 Cesium 时间为 2026-05-02 15:00
        let utc = Cesium.JulianDate.fromDate(new Date('2026/05/02 15:00:00'));
        // 转为北京时间(+8 小时)
        window.viewer.clock.currentTime = Cesium.JulianDate.addHours(utc, 8, new Cesium.JulianDate());
        // / 时间流速(12 倍速),数字越大时间过的越快
        window.viewer.clock.multiplier = 12;
        // 调用添加飞机模型的方法
        addPlaneModel();
};

// // 计算飞行路径(时间 → 位置),把路径点变成Cesium识别的路线。
const computePath = (source) => {
        // 创建时间 → 位置 采样器
        let property = new Cesium.SampledPositionProperty();
        for (let i = 0; i < source.length; i++) {
            // 根据时间偏移量计算 JulianDate
            let time = Cesium.JulianDate.addSeconds(window.start, source[i].time, new Cesium.JulianDate);
            // 经纬度转笛卡尔坐标
            let position = Cesium.Cartesian3.fromDegrees(source[i].longitude, source[i].latitude, source[i].height);
            // 添加时间-位置采样
            property.addSample(time, position);
        }
        return property;
    };

    // 添加飞机实体
    const addPlaneModel = () => {
      // 飞行开始时间
      window.start = Cesium.JulianDate.fromDate(new Date('2026/05/02 15:00:00'));
      // 飞行结束时间
      window.stop = Cesium.JulianDate.addSeconds(window.start, 720, new Cesium.JulianDate());
     // 设置时钟
      window.viewer.clock.startTime = window.start.clone();
      window.viewer.clock.currentTime = window.start.clone();
      window.viewer.clock.stopTime = window.stop.clone();
      // 时间轴缩放到飞行区间
      window.viewer.timeline.zoomTo(window.start, window.stop);
      // 时间结束后循环
      window.viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP;
      // 计算飞行轨迹
      let property = computePath(pathData.value);
      // 创建飞机实体
      window.plane = {
        availability: new Cesium.TimeIntervalCollection([new Cesium.TimeInterval({
          start: window.start,
          stop: window.stop
        })]),
        position: property,
        orientation: new Cesium.VelocityOrientationProperty(property),
        model: {
          uri: 'models/airBalloon.glb',
          minimumPixelSize: 1,
          scale: 2.2
        }
      };
      // 添加到场景中
            window.viewer.entities.add(window.plane);
            // 6 秒后关闭 Loading,启动第一人称
            myMar = setTimeout(() => {
                    isLoading.value = true;
                    window.viewer.scene.preUpdate.addEventListener(adjustVoid);
            }, 6000);

    };

    // 第一人称视角跟随
    const adjustVoid = () => {
            // 只有在播放状态且有飞机实体时才执行
      if (window.viewer.clock.shouldAnimate === true && window.plane) {
                    // 获取当前时刻飞机位置
        let center = window.plane.position.getValue(
          window.viewer.clock.currentTime
        );
                    // 获取当前时刻飞机朝向
        let orientation = window.plane.orientation.getValue(
          window.viewer.clock.currentTime
        );
        // 四元数 → 旋转矩阵
        var mtx3 = Cesium.Matrix3.fromQuaternion(orientation);
        // 旋转 + 平移 → 模型矩阵
        var mtx4 = Cesium.Matrix4.fromRotationTranslation(mtx3, center);
        // 提取航向、俯仰、滚转
        var hpr = Cesium.Transforms.fixedFrameToHeadingPitchRoll(mtx4);
        // 当前航向角
        const headingTemp = hpr.heading;
                    // 当前俯仰角
        const pitchTemp = hpr.pitch;
        // 调整视角(右偏 90°,下压 12°)
        const heading = Cesium.Math.toRadians(Cesium.Math.toDegrees(headingTemp) + 90);
        const pitch = Cesium.Math.toRadians(Cesium.Math.toDegrees(pitchTemp) - 12);
        // 相机距离
        const range = 200.0;
        // 锁定相机到飞机第一人称视角
        window.viewer.camera.lookAt(center, new Cesium.HeadingPitchRange(heading, pitch, range));
      }
    };
</script>

3.css样式代码:

* {
	margin: 0;
	padding: 0;
}

.main {
	width: 100%;
	height: 100vh;
	position: relative;
}

.content {
	width: 100%;
	height: 100%;
	position: relative;
	z-index: 1;
}

.loading {
	width: 100%;
	height: 100%;
	position: absolute;
	left: 0;
	top: 0;
	z-index: 2;
	display: flex;
	justify-content: center;
	align-items: center;
	font-size: 34px;
	display: flex;
	justify-content: center;
	align-items: center;
	font-size: 50px;
	color: #000000;
}

核心功能总结:

1. 飞行路线

通过 SampledPositionProperty 将经纬度 + 时间绑定,实现自动沿路线飞行。

2. 第一人称跟随

本例代码中的核心方法:adjustVoid

  • 每帧获取热气球位置与朝向
  • 计算旋转矩阵
  • 提取航向 / 俯仰
  • lookAt 锁定相机

3. 自动播放

  • shouldAnimate: true
  • clockRange: LOOP_STOP 循环播放
  • multiplier = 12 控制飞行速度

模型版权声明

本文演示所用热气球 3D 模型来源于 Sketchfab:
Hot Air Balloon by Anton Krupnov
许可协议:CC BY 4.0 International
模型原始链接:sketchfab.com/3d-models/h…
说明:本人仅用于技术演示,未对模型本身做任何修改