Cesium 键盘控制飞行教程 | WebGL·源码三维可视化源码

键盘控制飞行

controlModel ▶ 在线运行案例 三维可视化效果------功能案例合集地址

开源github仓库地址:https://github.com/z2586300277/three-cesium-examples

你将学到什么

  • Scene / Camera / Renderer 标准渲染管线搭建
  • 案例完整源码结构与可复用初始化模板

效果说明

本案例演示 键盘控制飞行 效果:模型姿态控制对象,用于控制模型的偏航角(heading)、俯仰角(pitch)和翻滚角(roll)。建议先打开文首在线案例查看动态画面,再对照下方源码逐步理解。

核心概念

  • Viewer 聚合 Scene、Camera、Clock 与渲染循环,是 Cesium 应用入口。
  • 阅读下方完整源码时,建议从 init / load / animate 三条主线入手,再深入 shader 与工具函数。

实现步骤

  • 创建 Viewer,配置地形/影像(若案例需要)并设置初始相机
  • requestAnimationFrame 循环中更新状态并 render(Cesium 为 viewer.render 或自动渲染)

代码要点

复制代码
  import * as Cesium from "cesium";

  import { GUI } from 'dat.gui';



  // ==================== 配置区域 ====================
  /**


    
  * 模型姿态控制对象,用于控制模型的偏航角(heading)、俯仰角(pitch)和翻滚角(roll)

  * @type {Cesium.HeadingPitchRoll}

   */
  let headingPitchRoll = new Cesium.HeadingPitchRoll();





  /**


    
  * 局部变换坐标系生成器,用于创建局部坐标系到世界坐标系的变换

  * "north"表示Y轴指向北,"west"表示X轴指向西

  * @type {Function}

   */
  let fixedFrameTransform = Cesium.Transforms.localFrameToFixedFrameGenerator("north", "west");





  /**


    
  * 每次姿态变化角度(4°),将角度转换为弧度用于计算

  * @type {Number}

   */
  let deltaRadians = Cesium.Math.toRadians(4);





  /**


    
  * 速度向量,用于存储模型移动的方向和速度

  * @type {Cesium.Cartesian3}

   */
  let Vector = new Cesium.Cartesian3();





  // ==================== 状态管理 ====================
  /**


    
  * 视角控制状态,可以是"first"(第一人称)、"god"(上帝视角)或"none"(无控制)

  * @type {String}

   */
  let view = "first";





  /**


    
  * 模型实例(用于防止重复添加)

  * @type {Object}

   */
  let firstModel = "";





  /**


    
  * 模型当前位置,使用笛卡尔坐标表示

  * @type {Cesium.Cartesian3}

   */
  let position;





  /**


    
  * 模型运动速度

  * @type {Number}

   */
  let speed = 5;





  /**


    
  * 相机相对模型的位置向量,用于确定相机相对于模型的位置

  * @type {Array}

   */
  let xyz = [0, 0, 50];





  /**


    
  * 第一人称视角相机位置 [x, y, z]

  * @type {Array}

   */
  let firstRoamXYZ = [0, -50, 10];





  /**


    
  * 上帝视角相机位置 [x, y, z]

  * @type {Array}

   */
  let godRoamXYZ = [0, 0, 50];





  /**


    
  * 键盘事件处理函数引用,用于后续移除事件监听器

  * @type {Function}

   */
  let firstDown;





  /**


    
  * 场景更新前事件处理函数引用,用于后续移除事件监听器

  * @type {Function}

   */
  let preUpdate;





  // ==================== 初始化区域 ====================
  /**


    
  * 获取用于渲染Cesium场景的容器元素

  * @type {HTMLElement}

   */
  const DOM = document.getElementById('box')





  /**


    
  * 初始化Cesium Viewer

  * @type {Cesium.Viewer}

   */
  const viewer = new Cesium.Viewer(DOM, {
      animation: false,             // 是否创建动画小器件,左下角仪表    
      baseLayerPicker: false,       // 是否显示图层选择器,右上角图层选择按钮
      baseLayer: Cesium.ImageryLayer.fromProviderAsync(Cesium.ArcGisMapServerImageryProvider.fromUrl(GLOBAL_CONFIG.getLayerUrl())),
      fullscreenButton: false,      // 是否显示全屏按钮,右下角全屏选择按钮
      timeline: false,              // 是否显示时间轴    
      infoBox: false,               // 是否显示信息框   
  })





  // ==================== GUI控制 ====================



  /**


    
  * 显示操作说明

   */
  function showInstructions() {
      const instructions = `
  相机姿态控制:
    W:抬头
    S:低头
    A:左转
    D:右转
    Q:逆时针旋转
    E:顺时针旋转
  速度控制:
    1:加速
    2:减速
  `;
      alert(instructions);
  }





  /** 


    
  * 创建GUI控制面板

  * @type {dat.GUI}

   */
  const gui = new GUI();





  /** 


    
  * 定义图形绘制操作对象

  * @namespace obj

   */
  const obj = {
      '开始飞行': () => {
          startFirstRoam({
              startPosition: [116.3, 39.9, 1000],
          });
      },
      '暂停飞行': () => {
          stopFirstRoam();
      },
      '切换视角': () => {
          // 在第一人称和上帝视角之间切换
          if (view === "god") {
              changeRoamView("first");
          } else {
              changeRoamView("god");
          }
      },
      '重置': () => {
          quitFirstRoam();
      },
      '操作说明': () => {
          showInstructions();
      }
  };





  // 将操作对象添加到GUI控制面板
  for (const key in obj) gui.add(obj, key)



  // 隐藏Cesium Logo
  viewer._cesiumWidget._creditContainer.style.display = "none";



  // ==================== 功能操作区域 ====================



  /**


    
  * 第一视角漫游加载方法

  * @description 使用键盘控制第一视角漫游,模型姿态:

  * W:抬头;S:低头;A:左转;D:右转;

  * Q:逆时针旋转;E:顺时针旋转;

  * 1:加速;2:减速

  * @param {Object} parameter -第一视角漫游默认配置项

  * @param {Array} parameter.startPosition -模型初始坐标位置[经度, 纬度, 高度]

  * @param {Number} [parameter.minSize=64] -模型的最小显示像素大小

  * @param {Number} [parameter.maxSize=128] -模型的最大显示像素大小

  * @param {Number} [parameter.speed=1] -漫游速度

  * @return {Cesium.Primitive} -返回飞行对象实体

   */
  function startFirstRoam(parameter) {
      // 防止重复添加模型
      if (!firstModel) {
          // 设置模型初始位置,将经纬度坐标转换为笛卡尔坐标
          position = new Cesium.Cartesian3.fromDegrees(...parameter.startPosition);





  // 相机飞向模型初始位置
          viewer.camera.flyTo({
              destination: Cesium.Cartesian3.fromDegrees(...parameter.startPosition),
              orientation: {
                  heading: Cesium.Math.toRadians(0),  // 偏航角:正北方向
                  pitch: Cesium.Math.toRadians(-20),   // 俯仰角:向下倾斜20度
                  roll: 0.0,                           // 翻滚角:无翻滚
              },
          });
          // 使用primitive方式加载模型 - 修复 Cesium.Model.fromGltf 错误
          // 异步加载GLTF模型文件,并应用上面计算的变换矩阵
          Cesium.Model.fromGltfAsync({
              url: HOST + 'files/model/Cesium_Air.glb',
          }).then(model => {
              // 将加载完成的模型添加到场景中
              firstModel = viewer.scene.primitives.add(model);



  // 设置模型姿态矩阵,将姿态控制对象应用到模型上
              // headingPitchRollToFixedFrame创建一个从姿态角到世界坐标的变换矩阵
              firstModel.modelMatrix = Cesium.Transforms.headingPitchRollToFixedFrame(
                  position,           // 模型位置
                  headingPitchRoll,   // 姿态控制对象
                  Cesium.Ellipsoid.WGS84,  // 使用WGS84椭球体
                  fixedFrameTransform      // 局部坐标系生成器
              );
          });
      }



  // 清除已有的事件监听器,防止重复注册
      if (firstDown) {
          document.removeEventListener("keydown", firstDown, false);
      }



  // 注册键盘事件监听器
      document.addEventListener("keydown", firstDown = function (e) {
          switch (e.key.toLowerCase()) {
              // 姿态控制
              case "w":    // 抬头 - 增加俯仰角
                  headingPitchRoll.pitch += deltaRadians;
                  break;
              case "s":  // 低头 - 减少俯仰角
                  headingPitchRoll.pitch -= deltaRadians;
                  break;
              case "a":  // 左转 - 减少偏航角
                  headingPitchRoll.heading -= deltaRadians;
                  break;
              case "d": // 右转 - 增加偏航角
                  headingPitchRoll.heading += deltaRadians;
                  break;
              case "q": // 逆时针旋转 - 减少翻滚角
                  headingPitchRoll.roll -= deltaRadians;
                  break;
              case "e": // 顺时针旋转 - 增加翻滚角
                  headingPitchRoll.roll += deltaRadians;
                  break;
              // 速度控制
              case "1":          // 加速
                  speed += 10;
                  speed = Math.min(speed, 10000);
                  break;
              case "2":          // 减速
                  speed -= 10;
                  speed = Math.max(speed, 10);
                  break;
          }
      });



  if (preUpdate) {
          viewer.scene.preUpdate.removeEventListener(preUpdate);
      }



  // 注册场景更新前事件监听器,每帧执行一次
      viewer.scene.preUpdate.addEventListener(preUpdate = () => {
          // 确保模型已加载
          if (!firstModel) return;



  // 根据速度计算下一个位置
          // multiplyByScalar将单位向量乘以标量,得到实际的移动向量
          Vector = Cesium.Cartesian3.multiplyByScalar(
              Cesium.Cartesian3.UNIT_X,  // 模型的X轴正方向作为前进方向
              speed / 10,                // 速度因子
              Vector
          );



  // 计算模型新位置
          // multiplyByPoint将变换矩阵应用于点,得到变换后的新位置
          position = Cesium.Matrix4.multiplyByPoint(
              firstModel.modelMatrix,  // 当前模型的变换矩阵
              Vector,                  // 移动向量
              position                 // 当前位置,结果也存储在这里
          );



  // 更新模型姿态与位置
          // 重新计算模型的变换矩阵,应用新的位置和姿态
          Cesium.Transforms.headingPitchRollToFixedFrame(
              position,                // 新位置
              headingPitchRoll,        // 当前姿态
              Cesium.Ellipsoid.WGS84,  // 使用WGS84椭球体
              fixedFrameTransform,     // 局部坐标系生成器
              firstModel.modelMatrix   // 更新模型的变换矩阵
          );



  // 根据视角状态更新相机位置
          // lookAt使相机看向指定目标点,并保持相对位置
          if (view != "none") {
              viewer.camera.lookAt(position, new Cesium.Cartesian3(...xyz));
          }
      });
  }



  /**


    
  * 漫游视角切换方法

  * @param {String} value -视角模式 ('first'|'god'|'none')

   */
  function changeRoamView(value) {
      view = value;
      switch (value) {
          case "first":
              xyz = firstRoamXYZ;  // 第一人称视角
              break;
          case "god":
              xyz = godRoamXYZ;    // 上帝视角
              break;
      }
  }





  /**


    
  * 暂停第一视角漫游事件

   */
  function stopFirstRoam() {
      // 移除事件监听器
      document.removeEventListener("keydown", firstDown, false);
      viewer.scene.preUpdate.removeEventListener(preUpdate);
      viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
  }





  /**


    
  * 销毁第一视角漫游事件
   */
  function quitFirstRoam() {
      stopFirstRoam();
      // 移除模型
      if (firstModel) {
          viewer.scene.primitives.remove(firstModel);
          firstModel = "";
      }
      speed = 5;
      // 重置姿态控制对象
      headingPitchRoll = new Cesium.HeadingPitchRoll()
      viewer.camera.flyTo({
          duration: 1,
          destination: Cesium.Cartesian3.fromDegrees(116.3, 39.9, 1000),
          orientation: {
              heading: Cesium.Math.toRadians(0),
              pitch: Cesium.Math.toRadians(-20),
              roll: 0.0,
          },
      });
  }

完整源码:GitHub

小结