不用游戏引擎,Three.js + CesiumJS 在浏览器里跑出了一个 F-15 战斗机模拟器

有人用纯 Web 技术做了一个 F-15 飞行模拟器,在真实的地球地形上飞行,带完整 HUD、加农炮、导弹和曳光弹,不用 Unity 也不用 Unreal,浏览器直接跑。

技术栈是 Three.js + CesiumJS + Vite。这个组合本身就是个有意思的问题——两个库各自有独立的 WebGL 渲染器,怎么让它们在同一个画面里共存?

GitHub:https://github.com/dimartarmizi/web-flight-simulator (opens in a new tab)

两个渲染器叠在一起

这是整个项目最核心的技术问题。

CesiumJS 负责地球——真实卫星影像、3D 地形、动态 LOD。Three.js 负责飞机——模型、尾焰、粒子特效。但两个渲染器各自维护一套深度缓冲,直接叠加的话,飞机和地形的遮挡关系会完全乱掉。

解决思路是在每帧的渲染循环里做三件事:

// 1. CesiumJS 先把地球画完
cesiumViewer.render();
 
// 2. 告诉 Three.js:别清屏,Cesium 的画面要保留
threeRenderer.autoClear = false;
 
// 3. 只清深度缓冲,让 Three.js 的深度测试从零开始
threeRenderer.clearDepth();
 
// 4. Three.js 叠上去,只画飞机
threeRenderer.render(threeScene, threeCamera);

clearDepth() 是关键——清掉深度信息但保留颜色缓冲,相当于告诉 Three.js"地球的画面你不要动,但深度关系你自己重新算"。飞机就这样干净地叠在地球上面,遮挡正确。

相机也要同步:每帧从 CesiumJS 相机里取出当前 FOV,更新 Three.js 相机的投影矩阵,两边的透视才能对齐。

项目实际上维护了三个 Cesium Viewer 实例:mainViewer(主视图,完整地形和光照)、miniviewer(战术小地图,禁用光照提升性能)、pauseMiniviewer(暂停状态冻结视角)。主视图做了分辨率缩放(resolutionScale: 0.75)和 LOD 跳过优化,在保证视觉效果的前提下控制 GPU 压力。

另外,在 Vite 里集成 CesiumJS 需要用 vite-plugin-cesium,CesiumJS 的静态资源(Worker、图片、着色器)需要插件统一处理,直接用会有路径问题。

飞行手感怎么做的

飞行物理在 planePhysics.js 里,用四元数处理姿态,而不是欧拉角。原因很实际:战斗机会做大角度机动,欧拉角在某些极端姿态下会出现万向锁,四元数可以避免这个问题。

控制输入在送进物理计算之前,planeController.js 先做了一层 lerp 平滑:

this.input.pitch = lerp(this.input.pitch, pitchTarget, 0.1)

插值系数 0.1 意味着每帧只走 10% 的距离,按键按下和松开都有缓冲,避免操控抖动。

三个轴的响应速率不一样——横滚 2.5,俯仰 1.2,偏航 0.5。横滚最灵敏,偏航最迟钝,符合真实战斗机的操控特性。速度低的时候操控会变迟钝("控制有效性"随速度缩放),模拟低速时气流对舵面作用变弱的效果。后燃器开启后速度上限临时提升到 1.5 倍,持续 2.5 秒。

尾焰是用 Shader 写的

尾焰效果在 jetFlame.js 里,用自定义片元着色器实现,不是粒子系统。几何体是一个锥形圆柱,贴在飞机尾部,Shader 里叠了五层效果:

  1. 颜色渐变:正常状态橙色→黄色,加力状态蓝色→青色
  2. 径向光晕:从中心向边缘指数衰减
  3. 冲击波条纹:正弦函数生成水平扫描条纹,模拟气流冲击
  4. 菱形网纹:沿长度方向滚动的网格纹理,模拟超音速菱形激波
  5. 闪烁噪声:程序化扰动,防止火焰看起来太规则

加力状态下火焰长度从 1.0 扩展到 1.3,亮度提升,同时触发一个点光源照亮周围几何体。

武器系统:点积做目标锁定

武器系统里最有意思的是导弹的目标锁定逻辑。判断"飞机有没有对准目标"用的是点积:飞机朝向向量与飞机到目标方向的向量做点积,结果越接近 1 说明越对准。目标在 10km 内、持续对准超过 2 秒,才进入锁定状态,同时触发音效提示。

加农炮有过热机制:每开一发热量 +0.02,散热速率 0.2×delta,打太快超过 1.0 强制停火,冷却到 0.3 以下才能重新开火。曳光弹一次齐射 6 枚,每枚间隔 0.15 秒弹出。

爆炸效果是四层粒子叠加:闪光(快速膨胀消退)→ 火焰粒子(橙色→黄色→透明)→ 火花(高速四散)→ 烟雾(缓慢上升消散)。残骸用随机几何体生成,带旋转翻滚动画和重力轨迹。

HUD 没有用 WebGL

HUD 全部用 DOM + CSS transform 实现,不是 Canvas 也不是 WebGL。

俯仰梯跟着飞机的 roll 角旋转、随 pitch 值上下平移,刻度从 -90° 到 +90°,10° 一格;罗盘带是一个横向的 DOM 元素,用 CSS translateX 滑动到当前航向;小地图是个旋转的 div,NPC 白色三角实时更新。每帧飞行状态变化,把对应的 CSS 值更新一遍。

HUD 本身也会跟着飞机姿态做透视变换——激烈机动时整个界面跟着倾斜,增强沉浸感。后燃器开启触发屏幕震动 + 边缘暗角,也是纯 CSS 动画。击杀提示做了 glitch text 效果——每 40ms 随机替换一次字符,像在解码一样逐渐显示出目标名称。

跑起来

git clone https://github.com/dimartarmizi/web-flight-simulator.git
npm install
npm run dev

需要一个 Cesium Ion Token 才能加载真实地形,官网免费注册可以拿到。

写在最后

这个项目技术上最值得参考的有两点:一是双渲染器的叠加方案(autoClear = false + clearDepth()),在 Web 端把地理引擎和 3D 图形引擎组合在一起的场景都可以用这套思路;二是尾焰 Shader 的分层设计——把冲击波、菱形激波、闪烁分成独立的视觉层叠加,每层逻辑简单,合在一起效果复杂,这个思路在做其他复杂特效时也适用。