相机控制器
相机控制器是 Three.js 中用于简化相机交互的重要工具。通过控制器,用户可以用鼠标、触摸或键盘来旋转、缩放、平移视角,而无需手动计算相机位置和朝向。本章将介绍 Three.js 提供的各种控制器及其使用方法。
控制器概述
Three.js 在 examples/jsm/controls 目录下提供了多种控制器,每种控制器适用于不同的交互场景:
| 控制器 | 用途 | 典型场景 |
|---|---|---|
| OrbitControls | 围绕目标旋转 | 产品展示、模型查看器 |
| MapControls | 类似地图操作 | 2D/2.5D 地图应用 |
| TrackballControls | 自由旋转 | 3D 模型编辑器 |
| FlyControls | 飞行模拟 | 飞行游戏、空间探索 |
| FirstPersonControls | 第一人称视角 | 漫游应用 |
| PointerLockControls | 鼠标锁定 | FPS 游戏 |
| TransformControls | 物体变换 | 编辑器工具 |
OrbitControls
OrbitControls 是最常用的控制器,它让相机围绕一个目标点旋转。就像你围绕着一个物体观察,可以放大缩小,从各个角度查看。
基本使用
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// 创建控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 启用阻尼(惯性),让交互更平滑
controls.enableDamping = true;
controls.dampingFactor = 0.05;
// 渲染循环中更新
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
OrbitControls 构造函数接收两个参数:相机对象和监听鼠标事件的 DOM 元素(通常是渲染器的 canvas)。
核心概念
OrbitControls 的核心是"目标点"(target)。相机围绕这个目标点旋转,目标点的默认位置是世界原点 (0, 0, 0)。
// 设置目标点
controls.target.set(0, 1, 0);
controls.update(); // 设置目标点后需要调用 update
// 或者
controls.target = new THREE.Vector3(0, 1, 0);
相机的位置到目标点的距离决定了缩放级别,方向决定了观察角度。
常用配置
const controls = new OrbitControls(camera, renderer.domElement);
// ===== 阻尼与惯性 =====
controls.enableDamping = true; // 启用阻尼
controls.dampingFactor = 0.05; // 阻尼系数,越小越滑
controls.rotateSpeed = 1.0; // 旋转速度
controls.zoomSpeed = 1.0; // 缩放速度
controls.panSpeed = 1.0; // 平移速度
// ===== 自动旋转 =====
controls.autoRotate = true; // 自动旋转
controls.autoRotateSpeed = 2.0; // 每秒旋转的角度(弧度)
// ===== 缩放限制 =====
controls.minDistance = 2; // 最近距离
controls.maxDistance = 20; // 最远距离
controls.minZoom = 0.5; // 最小缩放(正交相机)
controls.maxZoom = 4; // 最大缩放(正交相机)
// ===== 旋转角度限制 =====
// 极角(垂直方向),0 是顶部,PI 是底部
controls.minPolarAngle = 0; // 最小极角
controls.maxPolarAngle = Math.PI / 2; // 最大极角(限制在地平线以上)
// 方位角(水平方向)
controls.minAzimuthAngle = -Math.PI / 4; // 最小方位角
controls.maxAzimuthAngle = Math.PI / 4; // 最大方位角
// ===== 功能开关 =====
controls.enablePan = true; // 启用平移
controls.enableZoom = true; // 启用缩放
controls.enableRotate = true; // 启用旋转
// ===== 平移限制 =====
controls.panBounds = {
min: new THREE.Vector2(-10, -10),
max: new THREE.Vector2(10, 10)
};
// ===== 其他选项 =====
controls.screenSpacePanning = true; // 在屏幕空间平移(而非沿相机方向)
controls.verticalDragToForward = false; // 垂直拖动是否影响前进方向
鼠标按钮配置
// 自定义鼠标按钮
controls.mouseButtons = {
LEFT: THREE.MOUSE.ROTATE, // 左键旋转
MIDDLE: THREE.MOUSE.DOLLY, // 中键缩放
RIGHT: THREE.MOUSE.PAN // 右键平移
};
// 触摸配置
controls.touches = {
ONE: THREE.TOUCH.ROTATE, // 单指旋转
TWO: THREE.TOUCH.DOLLY_PAN // 双指缩放/平移
};
事件监听
OrbitControls 继承自 EventDispatcher,可以监听各种事件:
// 控制器变化时触发
controls.addEventListener('change', () => {
console.log('相机位置变化');
});
// 开始交互时触发
controls.addEventListener('start', () => {
console.log('开始交互');
});
// 结束交互时触发
controls.addEventListener('end', () => {
console.log('结束交互');
});
手动控制
除了用户交互,也可以通过代码控制相机:
// 使用球坐标设置相机位置
controls.spherical.set(radius, phi, theta);
controls.update();
// 保存当前状态
const state = {
target: controls.target.clone(),
position: camera.position.clone(),
zoom: camera.zoom
};
// 恢复状态
controls.target.copy(state.target);
camera.position.copy(state.position);
camera.zoom = state.zoom;
controls.update();
// 重置到初始状态
controls.reset();
MapControls
MapControls 是 OrbitControls 的子类,针对地图浏览做了优化:
import { MapControls } from 'three/addons/controls/MapControls.js';
const controls = new MapControls(camera, renderer.domElement);
// 默认配置已针对地图优化
// 主要区别:
// - screenSpacePanning 默认为 true(屏幕空间平移)
// - mouseButtons.LEFT 默认为 PAN(左键平移而非旋转)
// - zoom 以鼠标位置为中心
MapControls 适合用于地图、平面设计等场景,用户的操作习惯更接近 Google Maps 等地图应用。
TrackballControls
TrackballControls 提供更自由的旋转方式,没有"向上"的概念限制:
import { TrackballControls } from 'three/addons/controls/TrackballControls.js';
const controls = new TrackballControls(camera, renderer.domElement);
controls.rotateSpeed = 1.0;
controls.zoomSpeed = 1.2;
controls.panSpeed = 0.8;
controls.noRotate = false;
controls.noZoom = false;
controls.noPan = false;
controls.staticMoving = true; // 是否有惯性
controls.dynamicDampingFactor = 0.2; // 阻尼系数
与 OrbitControls 的区别在于,TrackballControls 允许相机翻转(可以"倒过来看"),这在某些 3D 建模软件中常见。
FlyControls
FlyControls 模拟飞行器的控制方式:
import { FlyControls } from 'three/addons/controls/FlyControls.js';
const controls = new FlyControls(camera, renderer.domElement);
controls.movementSpeed = 1.0; // 移动速度
controls.rollSpeed = 0.005; // 翻滚速度
controls.autoForward = false; // 自动前进
controls.dragToLook = false; // 拖动查看模式
操作方式:
- W/S:前进/后退
- A/D:左移/右移
- R/F:上升/下降
- 鼠标移动:调整朝向
FirstPersonControls
FirstPersonControls 提供简单的第一人称控制:
import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js';
const controls = new FirstPersonControls(camera, renderer.domElement);
controls.movementSpeed = 1.0; // 移动速度
controls.lookSpeed = 0.005; // 查看速度
controls.lookVertical = true; // 是否允许垂直查看
controls.constrainVertical = true; // 是否限制垂直角度
controls.verticalMin = 0; // 最小垂直角度
controls.verticalMax = Math.PI; // 最大垂直角度
与 FlyControls 不同,FirstPersonControls 假设用户站在地面上,没有上下移动(除非启用垂直查看)。
PointerLockControls
PointerLockControls 将鼠标指针锁定在画布中,隐藏鼠标光标,适合 FPS 游戏:
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
const controls = new PointerLockControls(camera, document.body);
// 点击锁定指针
document.addEventListener('click', () => {
controls.lock();
});
// 监听锁定状态
controls.addEventListener('lock', () => {
console.log('指针已锁定');
// 开始游戏逻辑
});
controls.addEventListener('unlock', () => {
console.log('指针已解锁');
// 暂停游戏逻辑
});
// 移动控制
const velocity = new THREE.Vector3();
const direction = new THREE.Vector3();
const keys = { w: false, a: false, s: false, d: false };
document.addEventListener('keydown', (e) => {
switch (e.code) {
case 'KeyW': keys.w = true; break;
case 'KeyA': keys.a = true; break;
case 'KeyS': keys.s = true; break;
case 'KeyD': keys.d = true; break;
}
});
document.addEventListener('keyup', (e) => {
switch (e.code) {
case 'KeyW': keys.w = false; break;
case 'KeyA': keys.a = false; break;
case 'KeyS': keys.s = false; break;
case 'KeyD': keys.d = false; break;
}
});
function animate() {
requestAnimationFrame(animate);
if (controls.isLocked) {
const delta = clock.getDelta();
direction.z = Number(keys.w) - Number(keys.s);
direction.x = Number(keys.d) - Number(keys.a);
direction.normalize();
velocity.z -= velocity.z * 10.0 * delta;
velocity.x -= velocity.x * 10.0 * delta;
if (keys.w || keys.s) velocity.z -= direction.z * 25.0 * delta;
if (keys.a || keys.d) velocity.x -= direction.x * 25.0 * delta;
controls.moveRight(-velocity.x * delta);
controls.moveForward(-velocity.z * delta);
}
renderer.render(scene, camera);
}
PointerLockControls 方法
// 锁定指针
controls.lock();
// 解锁指针
controls.unlock();
// 向前移动
controls.moveForward(distance);
// 向右移动
controls.moveRight(distance);
// 获取当前朝向
const direction = new THREE.Vector3();
camera.getWorldDirection(direction);
TransformControls
TransformControls 用于在场景中直接操作物体,常见于编辑器:
import { TransformControls } from 'three/addons/controls/TransformControls.js';
const transformControls = new TransformControls(camera, renderer.domElement);
scene.add(transformControls);
// 附加到物体
transformControls.attach(mesh);
// 切换变换模式
transformControls.setMode('translate'); // 平移
transformControls.setMode('rotate'); // 旋转
transformControls.setMode('scale'); // 缩放
// 监听变化
transformControls.addEventListener('change', () => {
console.log('物体已变换');
});
// 拖拽时禁用 OrbitControls
transformControls.addEventListener('dragging-changed', (event) => {
orbitControls.enabled = !event.value;
});
// 键盘切换模式
window.addEventListener('keydown', (e) => {
switch (e.key.toLowerCase()) {
case 'w': transformControls.setMode('translate'); break;
case 'e': transformControls.setMode('rotate'); break;
case 'r': transformControls.setMode('scale'); break;
}
});
TransformControls 配置
// 设置变换空间
transformControls.setSpace('world'); // 世界坐标
transformControls.setSpace('local'); // 局部坐标
// 设置大小
transformControls.setSize(1);
// 显示/隐藏特定轴
transformControls.showX = true;
transformControls.showY = true;
transformControls.showZ = true;
// 启用/禁用特定变换
transformControls.enableTranslate = true;
transformControls.enableRotate = true;
transformControls.enableScale = true;
// 取消附着
transformControls.detach();
DragControls
DragControls 允许直接拖拽场景中的物体:
import { DragControls } from 'three/addons/controls/DragControls.js';
const objects = [mesh1, mesh2, mesh3];
const dragControls = new DragControls(objects, camera, renderer.domElement);
// 事件监听
dragControls.addEventListener('dragstart', (event) => {
orbitControls.enabled = false; // 拖拽时禁用轨道控制
event.object.material.emissive.set(0x333333);
});
dragControls.addEventListener('drag', (event) => {
// 可以在这里限制拖拽范围
event.object.position.y = Math.max(0, event.object.position.y);
});
dragControls.addEventListener('dragend', (event) => {
orbitControls.enabled = true;
event.object.material.emissive.set(0x000000);
});
dragControls.addEventListener('hoveron', (event) => {
document.body.style.cursor = 'grab';
});
dragControls.addEventListener('hoveroff', (event) => {
document.body.style.cursor = 'auto';
});
ArcballControls
ArcballControls 提供类似专业 3D 软件的相机控制:
import { ArcballControls } from 'three/addons/controls/ArcballControls.js';
const controls = new ArcballControls(camera, renderer.domElement, scene);
controls.enableAnimations = true; // 启用动画过渡
controls.enableRotations = true; // 启用旋转
controls.enablePan = true; // 启用平移
controls.enableZoom = true; // 启用缩放
controls.focusDistance = 20; // 聚焦距离
// 聚焦到物体
controls.focus(mesh);
// 重置
controls.reset();
控制器最佳实践
按需渲染
使用控制器时,可以只在相机移动时渲染:
let needsRender = true;
controls.addEventListener('change', () => {
needsRender = true;
});
function animate() {
requestAnimationFrame(animate);
controls.update();
if (needsRender) {
renderer.render(scene, camera);
needsRender = false;
}
}
平滑过渡
使用 GSAP 或 Tween.js 实现相机平滑过渡:
import * as TWEEN from '@tweenjs/tween.js';
function animateCamera(targetPosition, targetLookAt, duration = 1000) {
const startPos = camera.position.clone();
const startTarget = controls.target.clone();
new TWEEN.Tween({ t: 0 })
.to({ t: 1 }, duration)
.easing(TWEEN.Easing.Cubic.InOut)
.onUpdate(({ t }) => {
camera.position.lerpVectors(startPos, targetPosition, t);
controls.target.lerpVectors(startTarget, targetLookAt, t);
controls.update();
})
.start();
}
function animate() {
requestAnimationFrame(animate);
TWEEN.update();
renderer.render(scene, camera);
}
多控制器切换
可以在不同场景下切换控制器:
let currentControls;
function setupControls(type) {
// 禁用当前控制器
if (currentControls) {
currentControls.enabled = false;
}
// 创建新控制器
switch (type) {
case 'orbit':
currentControls = new OrbitControls(camera, renderer.domElement);
break;
case 'fly':
currentControls = new FlyControls(camera, renderer.domElement);
break;
case 'firstPerson':
currentControls = new FirstPersonControls(camera, renderer.domElement);
break;
}
currentControls.enabled = true;
}
function animate() {
requestAnimationFrame(animate);
currentControls.update();
renderer.render(scene, camera);
}
移动端适配
移动端需要注意触摸事件的处理:
const controls = new OrbitControls(camera, renderer.domElement);
// 触摸配置
controls.touches = {
ONE: THREE.TOUCH.ROTATE,
TWO: THREE.TOUCH.DOLLY_PAN
};
// 防止触摸穿透
renderer.domElement.addEventListener('touchstart', (e) => {
e.preventDefault();
}, { passive: false });
// 防止双击缩放(移动端)
renderer.domElement.addEventListener('dblclick', (e) => {
e.preventDefault();
});
完整示例
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { TransformControls } from 'three/addons/controls/TransformControls.js';
// 场景设置
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);
const camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
100
);
camera.position.set(5, 5, 5);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
// OrbitControls
const orbitControls = new OrbitControls(camera, renderer.domElement);
orbitControls.enableDamping = true;
orbitControls.dampingFactor = 0.05;
orbitControls.minDistance = 2;
orbitControls.maxDistance = 20;
orbitControls.maxPolarAngle = Math.PI / 2;
// TransformControls
const transformControls = new TransformControls(camera, renderer.domElement);
scene.add(transformControls);
transformControls.addEventListener('dragging-changed', (event) => {
orbitControls.enabled = !event.value;
});
transformControls.addEventListener('change', () => {
console.log('物体变换:', transformControls.object);
});
// 创建物体
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({
color: 0x00ff88,
metalness: 0.3,
roughness: 0.4
});
const cube = new THREE.Mesh(geometry, material);
cube.castShadow = true;
cube.receiveShadow = true;
scene.add(cube);
// 赋予变换控制
transformControls.attach(cube);
// 地面
const groundGeometry = new THREE.PlaneGeometry(20, 20);
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x333344,
roughness: 0.8
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -0.5;
ground.receiveShadow = true;
scene.add(ground);
// 光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 10, 5);
directionalLight.castShadow = true;
scene.add(directionalLight);
// 键盘控制变换模式
window.addEventListener('keydown', (e) => {
switch (e.key.toLowerCase()) {
case 'w':
transformControls.setMode('translate');
break;
case 'e':
transformControls.setMode('rotate');
break;
case 'r':
transformControls.setMode('scale');
break;
case 'escape':
transformControls.detach();
break;
}
});
// 点击选择物体
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
renderer.domElement.addEventListener('click', (event) => {
// 检查是否点击了变换控制器
if (transformControls.dragging) return;
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children, false);
for (const intersect of intersects) {
if (intersect.object.isMesh) {
transformControls.attach(intersect.object);
break;
}
}
});
// 渲染循环
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
orbitControls.update();
renderer.render(scene, camera);
}
animate();
// 响应窗口变化
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
小结
相机控制器让 3D 场景的交互变得简单:
- OrbitControls 是最常用的控制器,适合产品展示和模型查看
- MapControls 针对地图浏览优化,操作方式类似 Google Maps
- PointerLockControls 适合 FPS 游戏,隐藏鼠标指针
- TransformControls 用于编辑器场景,直接操作物体
- FlyControls 和 FirstPersonControls 用于漫游类应用
选择合适的控制器可以大大提升用户体验,记得在渲染循环中调用 controls.update() 来更新控制器状态。