三维场景
three-tile 基于 threejs 开发,但只提供了地图模型,并未接管 threejs 的三维场景创建,如 scene、renderer、camera、controls 等,三维场景需要用户自己创建,这样做最大程度保持了框架的灵活性,但也增加了应用开发复杂度。为了方便初学者使用,three-tile 的插件中提供了一个GLVierwer
类用于快速创建三维场景。
1. 内置三维场景类 GLViewer
属性/方法/事件 | 类型 | 说明 |
---|---|---|
scene | Scene | threejs 场景对象 |
renderer | WebGLRenderer | threejs 渲染器对象 |
camera | PerspectiveCamera | threejs 相机对象 |
controls | MapControls | threejs 控制器对象 |
ambLight | AmbientLight | threejs 环境光对象 |
dirLight | DirectionalLight | threejs 直射光对象 |
container | HTMLElement | 场景容器 ID 或 DOM 元素 |
fogFactor (get/set) | number | 雾浓度因子 |
width (get) | number | 场景 DOM 容器的宽度 |
height (get) | number | 场景 DOM 的高度 |
controlsMode | 'MAP' 或 'ORBIT' | 控制器模式,默认 MAP |
resize() | public method | 调整大小 |
update | event | 场景更新事件,附带 delta 属性表示两帧时间间隔 |
该类封装了 threejs 的常规初始化过程,提供 Scene
、WebGLRenderer
渲染器、PerspectiveCamera
相机和 MapControls
控制器、环境光、直射光等对象。
简单应用,可以直接使用GLVierwer
类创建三维场景。https://github.com/sxguojf/three-tile/blob/master/packages/plugin/src/GLViewer/GLViewer.ts
// 初始化场景
const viewer = new plugin.GLViewer("#map");
// 地图添加到场景
viewer.scene.add(map);
2. 自定义三维场景
如果你的应用较为复杂,最好是参考下面代码自己写一个,与普通 threejs 场景初始化基本一样,简单示例:
// 创建场景
const createViewer = (container: HTMLDivElement) => {
const width = container.clientWidth;
const height = container.clientHeight;
// scene
const scene = new Scene();
// renderer
const renderer = new WebGLRenderer();
renderer.setSize(width, height);
// camera
const camera = new PerspectiveCamera(60, width / height, 10, 4e7);
camera.position.set(0, camera.far / 2, 0);
// ambient light
const ambLight = new AmbientLight(0xffffff);
scene.add(ambLight);
// directional light
const dirLight = new DirectionalLight(0xffffff);
dirLight.position.set(0, 5e6, 1e5);
dirLight.target.position.set(0, 0, -5e6);
scene.add(dirLight);
scene.add(new DirectionalLightHelper(dirLight, 2e5));
// controls
const controls = new MapControls(camera, renderer.domElement);
controls.maxDistance = 2e7;
controls.minDistance = 10;
controls.enableDamping = true;
controls.zoomToCursor = true;
return {
scene,
camera,
renderer,
controls,
ambLight,
dirLight,
};
};
3. 自定义场景说明:
上面自定义场景仅实现了最基本的功能,具体应用时需要进一步调整:
1. z-fighting 问题:
由于地图的空间尺度极大,很容易出现 z-fighting 问题,即着色器已经无法分辨模型的深度值,导致模型显示闪烁。造成 z-fighting 问题的原因主要是 camera 的 near 和 far 范围太大,超出了着色器的精度范围。解决方法一般有两种:
- 采用多段渲染,即将视锥体按深度分为多段,分别渲染,使每段的 near 和 far 在着色器能表示的精度范围内。cesium 使用这种方法。
- 根据摄像机离地图距离以及俯仰角,动态调整 near 和 far 范围,减少 near 和 far 差值。GLViewer 使用这种方法。
动态调整 near 和 far 范围不仅能解决 z-fighting 问题,还能减少视锥体内瓦片数量,提高渲染和下载效率。但存在问题是:如果在高空添加模型,可能会因为 near 值太大,导致模型被裁剪掉,比如要显示高空的卫星模型和轨迹。
在地图姿态发生变化时,动态调整 near 和 far, 使用一个经验公式(如果有同学能精确推导或证明,请联系我), 代码如下:
// Set the camera near/far based on distance and polayr angle
this.camera.far = MathUtils.clamp((dist / polar) * 8, 100, 50000 * 1000);
this.camera.near = this.camera.far / 1e3;
this.camera.updateProjectionMatrix();
2. 雾效果
三维场景中,模型深度超出 far 的部分会被剪裁,由于 three-tile 的地图并不是一个球,地图剪裁面没有过渡,造成天际线有些突兀,可通过添加雾来缓解。
threejs 内置的雾(线性雾、指数雾)浓度仅通过片元离摄像机距离来计算,而现实中雾的浓度不仅与距离有关,还与高度有关。增加根据摄像机俯仰角来调整雾浓度,也是一个经验公式:
this.scene.fog.density = (polar / (dist + 5)) * 0.25;
这种方法存在的问题是,雾浓度并不能随真实地形变化,比如山谷中的雾。更好的方法可以用体积雾实现,但体积雾计算量太大,影响渲染速度,不做更多讨论。
这里主要是解决天际线突兀的问题,其速度很快,能一定程度增加真实感。
3. 灯光
地图的材质默认使用 MeshLambertMaterial,必须要添加灯光才能渲染。建议添加一个环境光和一个直射光,环境光控制场景的整体亮度,直射光模拟太阳光,根据地形法向量调整亮度,增加地形的凹凸感。
如果要启用地图阴影,建议再增加一个灯光,并让它聚焦到需要产生阴影的模型上空,专门用来产生阴影。太阳光光源离地球太远,阴影效果不明显,并且大范围阴影会影响性能。
4. 缩放速度等参数调整
地图控制器对地图的缩放速度是固定的,但通常我们需要在地图尺度大时缩放速度快一些,地图尺度小时缩放速度慢一些,可以根据摄像机离地图距离,动态调整地图控制器的 zoomSpeed
来实现。一般缩放速度与距离为对数关系。
另外,还可根据摄像机离地图的距离,动态调整摄像机俯仰角、方位角等,当摄像机远离地球时,摆正地图,能有效提升用户体验。
5. 使用其它控制器
除了 MapControls,还可以使用 OrbitControls、PointerLockControls、FirstPersonControls、FlyControls、CameraControls 等控制器,它们的使用方法与 MapControls 类似。