相机与控制器
相机决定看到什么,控制器决定怎么移动。三维 GIS 场景里, 这两者的默认值几乎都要改:俯仰角、缩放范围、拾取射线都依赖它们。
四个参数决定画面
| 参数 | 含义 | 常见取值 | 影响 |
|---|---|---|---|
fov | 垂直视野角度 | 45–60 | 越大透视变形越明显 |
aspect | 画布宽高比 | 宽 / 高 | 窗口变化时必须更新 |
near | 最近可见距离 | 0.1–1 米 | 过小会损失深度精度 |
far | 最远可见距离 | 按场景尺度,如 20000 | 过大会导致远处闪烁 |
near 与 far 的比值越大,深度缓冲的有效精度越低。
城市级场景常见的做法是把 far 压到刚好覆盖可视范围,
而不是直接写 1e9。
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const camera = new THREE.PerspectiveCamera(
50, // fov
window.innerWidth / window.innerHeight, // aspect
0.5, // near
20000 // far
);
camera.position.set(320, 180, 420);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(320, 20, 300);
controls.enableDamping = true;
controls.dampingFactor = 0.08;
controls.minDistance = 40;
controls.maxDistance = 6000;
controls.maxPolarAngle = THREE.MathUtils.degToRad(85); // 不允许转到地下
controls.update();
enableDamping 开启后必须每帧调用 controls.update(),
否则阻尼不生效,画面在松手后立即停住。
OrbitControls 与 MapControls
两者内部是同一个类,只是默认手势映射不同。GIS 场景通常更接近
MapControls:左键平移、右键旋转。
import { MapControls } from 'three/addons/controls/MapControls.js';
const mapControls = new MapControls(camera, renderer.domElement);
mapControls.enableRotate = true;
mapControls.screenSpacePanning = false; // 平移贴着地面,而不是屏幕平面
mapControls.zoomSpeed = 0.8;
mapControls.rotateSpeed = 0.5;
mapControls.keyPanSpeed = 12;
// 限制在某个投影范围内
mapControls.minTargetRadius = 0;
mapControls.maxTargetRadius = 2500;
平移方向反了先看 screenSpacePanning。设为 true
时平移在屏幕平面内进行,设为 false 时沿地面移动。
地图类场景一般用后者。
屏幕点击转成三维坐标
Raycaster 用归一化设备坐标构造射线,与场景中的对象求交。
拿到的 point 是世界坐标,可以直接在上面放标记。
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
renderer.domElement.addEventListener('pointerdown', function (event) {
const rect = renderer.domElement.getBoundingClientRect();
pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(pointer, camera);
const hits = raycaster.intersectObjects(pickables, false);
if (hits.length === 0) {
// 没打中模型时,与地面平面求交
const ground = new THREE.Plane(new THREE.Vector3(0, 1, 0), -40);
const point = new THREE.Vector3();
if (raycaster.ray.intersectPlane(ground, point)) {
console.log('地面点', point);
}
return;
}
console.log('命中', hits[0].object.name, hits[0].point);
});
射线求交依赖对象的 matrixWorld 与几何的包围体。
刚创建还没渲染的对象要先调用 scene.updateMatrixWorld(),
否则射线会按旧位置计算。
相机补间
没有内置的相机动画类,最直接的做法是按帧做线性插值或缓动, 在接近目标时停止。
const camTween = {
active: false,
fromPos: new THREE.Vector3(),
toPos: new THREE.Vector3(),
fromTarget: new THREE.Vector3(),
toTarget: new THREE.Vector3(),
t: 0,
duration: 1.2
};
function flyTo(position, target) {
camTween.fromPos.copy(camera.position);
camTween.toPos.copy(position);
camTween.fromTarget.copy(controls.target);
camTween.toTarget.copy(target);
camTween.t = 0;
camTween.active = true;
}
function tickTween(dt) {
if (!camTween.active) return;
camTween.t = Math.min(camTween.t + dt / camTween.duration, 1);
const k = 1 - Math.pow(1 - camTween.t, 3); // easeOutCubic
camera.position.lerpVectors(camTween.fromPos, camTween.toPos, k);
controls.target.lerpVectors(camTween.fromTarget, camTween.toTarget, k);
if (camTween.t >= 1) camTween.active = false;
}
注意事项
- 控制器接管后会修改相机位置,手动设置
camera.position前先关掉或暂停控制器。 - 窗口尺寸变化时同时更新
camera.aspect与camera.updateProjectionMatrix()。 raycaster.intersectObjects的第二个参数为true时会递归子对象,性能开销更大。- 拾取对象过多时,先用包围盒粗筛,或把不参与拾取的对象放进独立的组并跳过。
- 正交相机用于二维视图或示意图,不参与透视变形,尺寸用
left/right/top/bottom控制。
经纬志(2026)。《相机与控制器》。https://www.jingweizhi.com/three/tutorial/camera-controls.html(访问日期 2026-11-03)。