别再用原生的 Cesium 了?Three.js + 3DTiles 丝滑加载城市级海量数据,带你起飞!

88 阅读4分钟

前言:当 Three.js 遇上 3D Tiles

在 WebGL 的世界里,Three.js 是浪漫的代名词,负责处理精致的渲染、酷炫的特效;而 Cesium 则是不折不扣的硬核派,统治着地理空间数据的天下。

但是,如果你想在 Three.js 的极致视觉体验中加载高精度的城市 3D 瓦片(3D Tiles),同时还要保持大地图操作的“丝滑感”,该怎么办?

今天我就来分享一下我在开发 Meteor3D 时的核心实现方案:如何利用 Three.js 高效加载 Cesium Ion 的 3D Tiles 数据,并实现一套近乎完美的相机控制系统。


1. 核心选型:为什么是 3d-tiles-renderer?

在 Three.js 中加载 3D Tiles,首选当然是 3d-tiles-renderer。它支持:

  • LOD(细节分级渲染)
  • 视锥剪裁
  • 完整的 Cesium Ion 插件支持
  • Draco 压缩解码

安装依赖

npm install 3d-tiles-renderer three

"3d-tiles-renderer": "^0.4.19"

"three": "^0.181.2"


2. 核心代码:加载 Cesium Ion 瓦片

加载 Cesium 数据最麻烦的是权限校验和坐标系转换。在 Meteor3D 中,我们采用了以下的高效配置:

2.1 初始化加载器

import { TilesRenderer } from '3d-tiles-renderer/three';
import { CesiumIonAuthPlugin, GLTFExtensionsPlugin } from '3d-tiles-renderer/three/plugins';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';

const tiles = new TilesRenderer();

// 1. 注册 Cesium Ion 认证插件
tiles.registerPlugin(
  new CesiumIonAuthPlugin({
    apiToken: 'YOUR_ION_TOKEN_HERE',
    assetId: '2275207', // 你的 Asset ID
  }),
);

// 2. 注册 Draco 解码(大场景必备性能优化)
tiles.registerPlugin(
  new GLTFExtensionsPlugin({
    dracoLoader: new DRACOLoader().setDecoderPath(
      'https://unpkg.com/three@0.153.0/examples/jsm/libs/draco/gltf/',
    ),
  }),
);

scene.add(tiles.group);

2.2 坐标系大挪移(重点!)

Cesium 使用的是 ECEF(以地球中心为原点)坐标系,且 Z 轴向上。而 Three.js 通常是 Y 轴向上。如果不进行处理,你会发现模型在遥远的天边或者是倒过来的。

Meteor3D 中,我们监听了 load-root-tileset 事件,自动将瓦片对齐到 Three.js 的中心:

tiles.addEventListener('load-root-tileset', () => {
  const sphere = new Sphere();
  tiles.getBoundingSphere(sphere);
  const position = sphere.center.clone();
  const distanceToEllipsoidCenter = position.length();

  // 计算将表面法线对齐到 Three.js Y 轴的旋转
  const surfaceDirection = position.normalize();
  const up = new Vector3(0, 1, 0);
  const rotationToNorthPole = rotationBetweenDirections(surfaceDirection, up);

  // 应用旋转并将模型中心重置到 Y=0
  tiles.group.quaternion.copy(rotationToNorthPole);
  tiles.group.position.y = -distanceToEllipsoidCenter;
});

image.png

3. 参数大字典:如何调教出丝滑的交互?

光加载出数据是不够的,在大场景下,默认的 OrbitControls 往往力不从心。我们使用了专门设计的 EnvironmentControls,并开放了一系列核心参数供用户调节。

参数名称意义与物理作用调优建议
dampingFactor阻尼系数。决定了你松开鼠标后,场景缓慢滑行多久。建议设置在 0.05 - 0.2 之间,让惯性感觉更自然。
adjustHeight自动贴地。相机会根据下方的瓦片高度自动调整位置。在地形起伏较大的漫游模式下开启,防止视角钻入地下。
minAltitude最小俯仰角。限制相机不能低于地平线。设置为 0 度,防止看穿大地底部的背面。
maxAltitude最大俯仰角。限制相机最高俯仰程度。通常限制在 85 度左右,防止翻转导致的眩晕感。
cameraRadius碰撞碰撞半径。相机与物体的最小物理间距。设置为 2-5 左右,防止镜头直接撞进楼体内部。
useFallbackPlane兜底平面。当瓦片还在加载中(空洞)时的物理平面。开启它,防止相机在数据未加载时坠入“虚空”。
autoAdjustUp自动回正。确保视角的 Up 方向始终垂直于地面。开启后,即便在复杂的旋转操作下,天空也永远在上方。

4. 性能压榨:如何保持 60 帧?

  1. LogarithmicDepthBuffer:在大地图场景下,Z-Fighting(深度冲突导致的闪烁)非常严重。初始化 WebGLRenderer 时务必开启 logarithmicDepthBuffer: true
  2. Resolution Scaling
    tiles.setResolutionFromRenderer(camera, renderer); // 自动根据屏幕分辨率调整渲染精度
    
  3. 分帧更新:不要在每一帧都强行刷新所有 Tile 状态,利用 tiles.update() 内部的调度机制。

结语

Three.js + 3D Tiles 为地理空间可视化打开了新世界的大门。如果你正在寻找一个开箱即用的解决方案,欢迎尝试我的项目 Meteor3D。它已经把这些坑都踩平了,并封装成了易用的编辑器。

如果你在实现过程中遇到任何问题,欢迎在评论区讨论!


关注作者,解锁更多 3D 硬核技术分享 🧊