控制器系统详解
控制器(Controls)是 Three.js 中用于实现相机交互的核心工具,允许用户通过鼠标、键盘、触摸等设备来控制相机的位置和方向。Three.js 提供了多种内置控制器以满足不同的交互需求。
系统架构
plaintext
┌─────────────────────────────────────────────────────────────────────────┐
│ 控制器体系结构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Controls (基类概念) │ │
│ │ │ │
│ │ 所有控制器都接收: camera, domElement │ │
│ │ 核心方法: update() / dispose() / event listeners │ │
│ └───────────────────────────┬───────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────┼───────────────────────┬ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 观察类 │ │ 导航类 │ │ 编辑类 │ │
│ ├──────────┤ ├──────────┤ ├──────────┤ │
│ │Orbit │ │Fly │ │Transform│ │
│ │Trackball │ │FirstPerson│ │Drag │ │
│ └──────────┘ │PointerLock│ └──────────┘ │
│ └──────────┘ │
│ │
│ 输入设备 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 鼠标 │ │ 键盘 │ │ 触摸 │ │ 手柄 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
工作流程:
用户输入 → 事件监听 → 控制器处理 → 更新相机 → 渲染器渲染概述
控制器的核心作用
| 作用 | 说明 |
|---|---|
| 相机操控 | 改变相机的 position 和 rotation/quaternion |
| 交互响应 | 监听鼠标/键盘/触摸事件并转换为相机运动 |
| 约束限制 | 限制旋转角度、缩放范围、平移边界等 |
| 平滑过渡 | 提供阻尼(damping)和惯性效果 |
控制器选择指南
plaintext
需要什么功能?
├─ 围绕目标点观察(产品展示、模型预览)
│ → OrbitControls ✅ 最常用
│
├─ 自由飞行浏览(场景漫游)
│ → FlyControls 或 PointerLockControls
│
├─ FPS 游戏视角
│ → PointerLockControls(沉浸式)或 FirstPersonControls
│
├─ 对象编辑操作(移动/旋转/缩放 Gizmo)
│ → TransformControls
│
├─ 拖拽对象到新位置
│ → DragControls
│
├─ 无限制自由旋转(类似 Google Earth)
│ → TrackballControls
│
└─ 自定义交互逻辑
→ 基于 EventDispatcher 自己实现基本使用模式
javascript
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
// 1. 创建控制器
const controls = new OrbitControls(camera, renderer.domElement)
// 2. 配置参数(可选)
controls.enableDamping = true
controls.dampingFactor = 0.05
// 3. 在渲染循环中调用 update()
function animate() {
requestAnimationFrame(animate)
controls.update() // 必须每帧调用
renderer.render(scene, camera)
}
animate()OrbitControls 轨道控制器
OrbitControls 是最常用的控制器,允许用户围绕一个焦点旋转、缩放和平移相机。
基础配置
javascript
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
const controls = new OrbitControls(camera, renderer.domElement)
// ==================== 目标点设置 ====================
controls.target.set(0, 1, 0) // 相机聚焦的点
controls.update() // 设置 target 后必须调用 update()
// ==================== 阻尼效果(惯性)====================
controls.enableDamping = true // 开启阻尼
controls.dampingFactor = 0.05 // 阻尼系数 (0-1),越小惯性越大
// ==================== 旋转控制 ====================
controls.enableRotate = true // 启用旋转
controls.rotateSpeed = 1.0 // 旋转速度
controls.minPolarAngle = 0 // 最小垂直角度(弧度),0 = 天顶
controls.maxPolarAngle = Math.PI // 最大垂直角度(弧度),π = 底部
controls.minAzimuthAngle = -Infinity // 最小水平角度
controls.maxAzimuthAngle = Infinity // 最大水平角度
// ==================== 缩放控制 ====================
controls.enableZoom = true // 启用缩放
controls.zoomSpeed = 1.2 // 缩放速度
controls.minDistance = 1 // 最近距离
controls.maxDistance = 100 // 最远距离
controls.enableDamping = true // 缩放也支持阻尼
// ==================== 平移控制 ====================
controls.enablePan = true // 启用平移
controls.panSpeed = 1.0 // 平移速度
controls.screenSpacePanning = true // 使用屏幕空间平移(推荐)
// ==================== 边界限制 ====================
controls.minZoom = 0.5 // 最小缩放比例
controls.maxZoom = 2 // 最大缩放比例操作方式说明
| 操作 | 鼠标 | 单指触摸 | 说明 |
|---|---|---|---|
| 旋转 | 左键拖动 | 单指拖动 | 绕目标点旋转视角 |
| 缩放 | 滚轮 / 双击 | 双指捏合 | 调整与目标的距离 |
| 平移 | 右键拖动 / Ctrl+左键 | 双指平移 | 移动相机平行于视平面 |
高级配置
javascript
const controls = new OrbitControls(camera, renderer.domElement)
// 自动旋转
controls.autoRotate = true // 开启自动旋转
controls.autoRotateSpeed = 2.0 // 旋转速度(30秒一圈 = 2.0)
// 键盘按键映射
controls.keys = {
LEFT: 'ArrowLeft', // 左键
UP: 'ArrowUp', // 上键
RIGHT: 'ArrowRight', // 右键
DOWN: 'ArrowDown', // 下键
}
// 鼠标按钮映射
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 // 双指 - 缩放+平移
}
// 边界框限制
controls.maxPolarAngle = Math.PI / 2 // 限制不能看到地面以下
controls.minAzimuthAngle = -Math.PI / 4 // 限制左右旋转范围
controls.maxAzimuthAngle = Math.PI / 4事件监听
javascript
const controls = new OrbitControls(camera, renderer.domElement)
// 开始变化时触发
controls.addEventListener('start', () => {
console.log('开始交互')
})
// 每次变化时触发
controls.addEventListener('change', () => {
// 可以在这里触发按需渲染
needsRender = true
})
// 结束变化时触发
controls.addEventListener('end', () => {
console.log('结束交互')
console.log('当前目标:', controls.target)
console.log('当前位置:', camera.position)
})典型应用:产品查看器
javascript
class ProductViewer {
constructor(container, modelUrl) {
this.container = container
this.initScene()
this.initCamera()
this.initRenderer()
this.initControls()
this.loadModel(modelUrl)
this.animate()
}
initScene() {
this.scene = new THREE.Scene()
this.scene.background = new THREE.Color(0xf0f0f0)
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)
this.scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)
directionalLight.position.set(5, 10, 7.5)
this.scene.add(directionalLight)
}
initCamera() {
const aspect = this.container.clientWidth / this.container.clientHeight
this.camera = new THREE.PerspectiveCamera(45, aspect, 0.1, 1000)
this.camera.position.set(5, 3, 5)
}
initRenderer() {
this.renderer = new THREE.WebGLRenderer({ antialias: true })
this.renderer.setSize(
this.container.clientWidth,
this.container.clientHeight
)
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
this.renderer.shadowMap.enabled = true
this.renderer.toneMapping = THREE.ACESFilmicToneMapping
this.container.appendChild(this.renderer.domElement)
}
initControls() {
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.controls.enableDamping = true
this.controls.dampingFactor = 0.08
this.controls.minDistance = 2
this.controls.maxDistance = 20
this.controls.maxPolarAngle = Math.PI / 2 + 0.1
this.controls.target.set(0, 1, 0)
this.controls.update()
window.addEventListener('resize', () => this.onResize())
}
async loadModel(url) {
const loader = new GLTFLoader()
const gltf = await loader.loadAsync(url)
this.model = gltf.scene
const box = new THREE.Box3().setFromObject(this.model)
const center = box.getCenter(new THREE.Vector3())
const size = box.getSize(new THREE.Vector3())
this.model.position.sub(center)
const maxDim = Math.max(size.x, size.y, size.z)
const scale = 3 / maxDim
this.model.scale.setScalar(scale)
this.controls.target.set(0, size.y * scale / 2, 0)
this.controls.update()
this.scene.add(this.model)
}
onResize() {
const width = this.container.clientWidth
const height = this.container.clientHeight
this.camera.aspect = width / height
this.camera.updateProjectionMatrix()
this.renderer.setSize(width, height)
}
animate() {
requestAnimationFrame(() => this.animate())
this.controls.update()
this.renderer.render(this.scene, this.camera)
}
}TransformControls 变换控制器
TransformControls 提供可视化的变换 Gizmo(移动/旋转/缩放手柄),常用于编辑器和 3D 工具中。
基本用法
javascript
import { TransformControls } from 'three/examples/jsm/controls/TransformControls'
const transformControl = new TransformControls(camera, renderer.domElement)
scene.add(transformControl.attach(targetObject))
// 切换变换模式
transformControl.setMode('translate') // 移动模式(默认)
transformControl.setMode('rotate') // 旋转模式
transformControl.setMode('scale') // 缩放模式
// 切换空间模式
transformControl.setSpace('local') // 局部坐标(默认)
transformControl.setSpace('world') // 世界坐标
// 显示/隐藏
transformControl.visible = true
transformControl.detach() // 解除绑定(隐藏 gizmo)
// 销毁
transformControl.dispose()配置参数
javascript
const tControls = new TransformControls(camera, renderer.domElement)
// 尺寸
tControls.setSize(1) // gizmo 大小 (默认 1)
// 拖拽灵敏度
tControls.translationSnap = null // 移动吸附步长(null=不吸附)
tControls.rotationSnap = null // 旋转吸附步长(弧度)
tControls.scaleSnap = null // 缩放吸附步长
// 示例:网格对齐
tControls.translationSnap = 0.5 // 每 0.5 单位吸附一次
tControls.rotationSnap = Math.PI / 4 // 每 45° 吸附一次
tControls.scaleSnap = 0.1 // 每 0.1 缩放吸附一次
// 外观
tControls.showX = true // 显示 X 轴手柄
tControls.showY = true // 显示 Y 轴手柄
tControls.showZ = true // 显示 Z 轴手柄与 OrbitControls 协同
javascript
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { TransformControls } from 'three/examples/jsm/controls/TransformControls'
const orbitControls = new OrbitControls(camera, renderer.domElement)
const transformControl = new TransformControls(camera, renderer.domElement)
scene.add(transformControl)
// 当 TransformControls 正在拖拽时,禁用 OrbitControls
transformControl.addEventListener('dragging-changed', (event) => {
orbitControls.enabled = !event.value
})
// 选择对象后绑定 TransformControls
function selectObject(object) {
transformControl.attach(object)
}
// 取消选择
function deselectObject() {
transformControl.detach()
}
// 切换模式(通过 UI 按钮)
document.getElementById('mode-translate').onclick = () => {
transformControl.setMode('translate')
}
document.getElementById('mode-rotate').onclick = () => {
transformControl.setMode('rotate')
}
document.getElementById('mode-scale').onclick = () => {
transformControl.setMode('scale')
}完整编辑器示例
javascript
class ObjectEditor {
constructor(scene, camera, renderer, domElement) {
this.scene = scene
this.camera = camera
this.renderer = renderer
this.selectedObject = null
this.orbitControls = new OrbitControls(camera, domElement)
this.transformControl = new TransformControls(camera, domElement)
scene.add(this.transformControl)
this.setupEventListeners()
}
setupEventListeners() {
// TransformControls 拖拽时禁用轨道控制
this.transformControl.addEventListener('dragging-changed', (event) => {
this.orbitControls.enabled = !event.value
})
// 对象变换完成回调
this.transformControl.addEventListener('objectChange', () => {
if (this.onObjectChange) {
this.onObjectChange(this.selectedObject)
}
})
// 鼠标点击选择对象
const raycaster = new THREE.Raycaster()
const mouse = new THREE.Vector2()
domElement.addEventListener('click', (event) => {
// 如果正在拖拽 TransformControls,不处理选择
if (this.transformControl.dragging) return
mouse.x = (event.clientX / domElement.clientWidth) * 2 - 1
mouse.y = -(event.clientY / domElement.clientHeight) * 2 + 1
raycaster.setFromCamera(mouse, this.camera)
const intersects = raycaster.intersectObjects(this.scene.children, true)
if (intersects.length > 0) {
let obj = intersects[0].object
while (obj.parent && obj.parent.type !== 'Scene') {
obj = obj.parent
}
this.select(obj)
} else {
this.deselect()
}
})
// 键盘快捷键
window.addEventListener('keydown', (e) => {
switch (e.key.toLowerCase()) {
case 'g': this.setMode('translate'); break
case 'r': this.setMode('rotate'); break
case 's': this.setMode('scale'); break
case 'escape': this.deselect(); break
case 'delete':
case 'backspace':
this.deleteSelected()
break
}
})
}
select(object) {
this.selectedObject = object
this.transformControl.attach(object)
console.log('选中:', object.name || object.type)
}
deselect() {
this.selectedObject = null
this.transformControl.detach()
}
setMode(mode) {
this.transformControl.setMode(mode)
}
deleteSelected() {
if (!this.selectedObject) return
this.transformControl.detach()
this.scene.remove(this.selectedObject)
this.selectedObject = null
}
update() {
this.orbitControls.update()
}
}DragControls 拖拽控制器
DragControls 允许通过鼠标拖拽将 3D 对象在场景中移动。
基本用法
javascript
import { DragControls } from 'three/examples/jsm/controls/DragControls'
const draggableObjects = [mesh1, mesh2, mesh3]
const dragControls = new DragControls(draggableObjects, camera, renderer.domElement)
// 事件监听
dragControls.addEventListener('hoveron', (event) => {
event.object.material.emissive.setHex(0x555555)
})
dragControls.addEventListener('hoveroff', (event) => {
event.object.material.emissive.setHex(0x000000)
})
dragControls.addEventListener('dragstart', (event) => {
event.object.material.emissive.setHex(0xff0000)
})
dragControls.addEventListener('dragend', (event) => {
event.object.material.emissive.setHex(0x000000)
console.log('新位置:', event.object.position)
})
dragControls.addEventListener('drag', (event) => {
// 拖拽过程中持续触发
})配置选项
javascript
const dragControls = new DragControls(objects, camera, domElement)
// 拖拽平面
dragControls.transformGroup = false // 是否拖拽整个组
// 启用/禁用
dragControls.enabled = true
// 销毁
dragControls.dispose()与 OrbitControls 冲突解决
javascript
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { DragControls } from 'three/examples/jsm/controls/DragControls'
const orbitControls = new OrbitControls(camera, renderer.domElement)
const dragControls = new DragControls(draggables, camera, renderer.domElement)
// 拖拽开始时禁用轨道控制
dragControls.addEventListener('dragstart', () => {
orbitControls.enabled = false
})
// 拖拽结束时恢复轨道控制
dragControls.addEventListener('dragend', () => {
orbitControls.enabled = true
})
// hover 时也可以禁用以避免误触
dragControls.addEventListener('hoveron', () => {
orbitControls.enabled = false
})
dragControls.addEventListener('hoveroff', () => {
orbitControls.enabled = true
})限制拖拽范围
javascript
dragControls.addEventListener('drag', (event) => {
const obj = event.object
// 限制 X 范围
obj.position.x = Math.max(-10, Math.min(10, obj.position.x))
// 限制 Y 范围(地面以上)
obj.position.y = Math.max(0, obj.position.y)
// 限制 Z 范围
obj.position.z = Math.max(-10, Math.min(10, obj.position.z))
})FlyControls 飞行控制器
FlyControls 实现类似飞行模拟器的控制方式,适合场景漫游。
基本用法
javascript
import { FlyControls } from 'three/examples/jsm/controls/FlyControls'
const flyControls = new FlyControls(camera, renderer.domElement)
// 基础配置
flyControls.movementSpeed = 10 // 移动速度
flyControls.rollSpeed = 0.005 // 翻滚速度
flyControls.autoForward = false // 是否自动前进
// 默认控制键
// W/S: 前进/后退
// A/D: 左右平移
// Q/E: 上下移动
// R/F: 翻滚
// ↑↓←→: 方向控制自定义配置
javascript
const flyControls = new FlyControls(camera, renderer.domElement)
flyControls.movementSpeed = 20
flyControls.rollSpeed = Math.PI / 24
flyControls.dragToLook = false // 是否拖拽来改变视角
flyControls.autoForward = false
// 在渲染循环中使用 deltaTime 保证帧率无关
const clock = new THREE.Clock()
function animate() {
requestAnimationFrame(animate)
const delta = clock.getDelta()
flyControls.update(delta)
renderer.render(scene, camera)
}FirstPersonControls 第一人称控制器
FirstPersonControls 类似于传统第一人称游戏中的视角控制。
基本用法
javascript
import { FirstPersonControls } from 'three/examples/jsm/controls/FirstPersonControls'
const fpControls = new FirstPersonControls(camera, renderer.domElement)
fpControls.movementSpeed = 10 // 移动速度
fpControls.lookSpeed = 0.002 // 视角转动速度
fpControls.lookVertical = true // 允许垂直视角
fpControls.autoForward = false // 不自动前进
fpControls.heightSpeed = false // 不根据高度调整速度
fpControls.heightCoef = 1 // 高度系数
fpControls.heightMin = 0.0 // 最低高度
fpControls.heightMax = 1.0 // 最高高度
// 控制:
// 鼠标移动:改变视角
// W/S/A/D 或 方向键:前后左右移动
// R/F:上下移动
// 鼠标点击:锁定指针使用示例
javascript
const clock = new THREE.Clock()
function animate() {
requestAnimationFrame(animate)
const delta = clock.getDelta()
fpControls.update(delta)
renderer.render(scene, camera)
}PointerLockControls 指针锁定控制器
PointerLockControls 提供 FPS 游戏风格的沉浸式体验,鼠标被锁定在窗口内。
基本用法
javascript
import { PointerLockControls } from 'three/examples/jsm/controls/PointerLockControls'
const pointerLockControls = new PointerLockControls(camera, document.body)
scene.add(pointerLockControls.getObject())
// 点击时锁定指针
document.body.addEventListener('click', () => {
pointerLockControls.lock()
})
// 锁定状态变化
pointerLockControls.addEventListener('lock', () => {
console.log('指针已锁定')
instructions.style.display = 'none'
})
pointerLockControls.addEventListener('unlock', () => {
console.log('指针已解锁')
instructions.style.display = ''
})
// 键盘控制移动
const velocity = new THREE.Vector3()
const direction = new THREE.Vector3()
document.addEventListener('keydown', (e) => {
switch (e.code) {
case 'KeyW': case 'ArrowUp': moveForward = true; break
case 'KeyS': case 'ArrowDown': moveBackward = true; break
case 'KeyA': case 'ArrowLeft': moveLeft = true; break
case 'KeyD': case 'ArrowRight': moveRight = true; break
}
})
document.addEventListener('keyup', (e) => {
switch (e.code) {
case 'KeyW': case 'ArrowUp': moveForward = false; break
case 'KeyS': case 'ArrowDown': moveBackward = false; break
case 'KeyA': case 'ArrowLeft': moveLeft = false; break
case 'KeyD': case 'ArrowRight': moveRight = false; break
}
})
// 在动画循环中更新位置
let prevTime = performance.now()
function animate() {
requestAnimationFrame(animate)
const time = performance.now()
const delta = (time - prevTime) / 1000
if (pointerLockControls.isLocked) {
velocity.x -= velocity.x * 10.0 * delta
velocity.z -= velocity.z * 10.0 * delta
direction.z = Number(moveForward) - Number(moveBackward)
direction.x = Number(moveRight) - Number(moveLeft)
direction.normalize()
if (moveForward || moveBackward) velocity.z -= direction.z * 400.0 * delta
if (moveLeft || moveRight) velocity.x -= direction.x * 400.0 * delta
pointerLockControls.moveRight(-velocity.x * delta)
pointerLockControls.moveForward(-velocity.z * delta)
}
prevTime = time
renderer.render(scene, camera)
}完整 FPS 示例
javascript
class FPSCamera {
constructor(camera, domElement) {
this.camera = camera
this.controls = new PointerLockControls(camera, domElement)
this.velocity = new THREE.Vector3()
this.direction = new THREE.Vector3()
this.moveState = { forward: false, backward: false, left: false, right: false }
this.speed = 100
this.friction = 10
this.setupInputs()
}
setupInputs() {
document.addEventListener('keydown', (e) => this.onKeyDown(e))
document.addEventListener('keyup', (e) => this.onKeyUp(e))
}
onKeyDown(event) {
switch (event.code) {
case 'KeyW': case 'ArrowUp':
this.moveState.forward = true; break
case 'KeyS': case 'ArrowDown':
this.moveState.backward = true; break
case 'KeyA': case 'ArrowLeft':
this.moveState.left = true; break
case 'KeyD': case 'ArrowRight':
this.moveState.right = true; break
case 'Space':
this.jump(); break
}
}
onKeyUp(event) {
switch (event.code) {
case 'KeyW': case 'ArrowUp':
this.moveState.forward = false; break
case 'KeyS': case 'ArrowDown':
this.moveState.backward = false; break
case 'KeyA': case 'ArrowLeft':
this.moveState.left = false; break
case 'KeyD': case 'ArrowRight':
this.moveState.right = false; break
}
}
jump() {
// 可扩展跳跃逻辑
}
update(delta) {
if (!this.controls.isLocked) return
// 应用摩擦力
this.velocity.x -= this.velocity.x * this.friction * delta
this.velocity.z -= this.velocity.z * this.friction * delta
// 计算方向
this.direction.z = Number(this.moveState.forward) - Number(this.moveState.backward)
this.direction.x = Number(this.moveState.right) - Number(this.moveState.left)
this.direction.normalize()
// 应用加速度
if (this.moveState.forward || this.moveState.backward) {
this.velocity.z -= this.direction.z * this.speed * delta
}
if (this.moveState.left || this.moveState.right) {
this.velocity.x -= this.direction.x * this.speed * delta
}
// 移动
this.controls.moveRight(-this.velocity.x * delta)
this.controls.moveForward(-this.velocity.z * delta)
}
}TrackballControls 轨迹球控制器
TrackballControls 类似于 OrbitControls 但没有极角限制,可以任意方向旋转。
基本用法
javascript
import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls'
const trackballControls = new TrackballControls(camera, renderer.domElement)
trackballControls.rotateSpeed = 4.0 // 旋转速度
trackballControls.zoomSpeed = 1.2 // 缩放速度
trackballControls.panSpeed = 0.8 // 平移速度
trackballControls.noRotate = false // 禁用旋转
trackballControls.noZoom = false // 禁用缩放
trackballControls.noPan = false // 禁用平移
trackballControls.staticMoving = false // 静态移动(无惯性)
trackballControls.dynamicDampingFactor = 0.2 // 动态阻尼因子
trackballControls.minDistance = 0 // 最小距离
trackballControls.maxDistance = Infinity // 最大距离与 OrbitControls 的区别
| 特性 | OrbitControls | TrackballControls |
|---|---|---|
| 旋转轴 | 固定上方向(有极角限制) | 无固定轴,可任意翻转 |
| 适用场景 | 产品预览、建筑展示 | 科学可视化、无限制观察 |
| 万向锁 | 通过 polarAngle 避免 | 可能发生 |
| 自动旋转 | 支持 | 不支持 |
| 阻尼 | 支持 | 支持(dynamicDampingFactor) |
自定义控制器
当内置控制器无法满足需求时,可以基于 EventDispatcher 创建自定义控制器。
基础框架
javascript
class CustomControls extends THREE.EventDispatcher {
constructor(camera, domElement) {
super()
this.camera = camera
this.domElement = domElement
this.enabled = true
// 状态
this.isDragging = false
this.previousMousePosition = { x: 0, y: 0 }
this.bindEvents()
}
bindEvents() {
this._onMouseDown = this.onMouseDown.bind(this)
this._onMouseMove = this.onMouseMove.bind(this)
this._onMouseUp = this.onMouseUp.bind(this)
this._onWheel = this.onWheel.bind(this)
this._onContextMenu = (e) => e.preventDefault()
this.domElement.addEventListener('mousedown', this._onMouseDown)
this.domElement.addEventListener('wheel', this._onWheel)
this.domElement.addEventListener('contextmenu', this._onContextMenu)
}
onMouseDown(event) {
if (!this.enabled) return
event.preventDefault()
this.isDragging = true
this.previousMousePosition = {
x: event.clientX,
y: event.clientY
}
document.addEventListener('mousemove', this._onMouseMove)
document.addEventListener('mouseup', this._onMouseUp)
this.dispatchEvent({ type: 'start' })
}
onMouseMove(event) {
if (!this.enabled || !this.isDragging) return
const deltaX = event.clientX - this.previousMousePosition.x
const deltaY = event.clientY - this.previousMousePosition.y
this.handleRotation(deltaX, deltaY)
this.previousMousePosition = {
x: event.clientX,
y: event.clientY
}
this.dispatchEvent({ type: 'change' })
}
onMouseUp(event) {
this.isDragging = false
document.removeEventListener('mousemove', this._onMouseMove)
document.removeEventListener('mouseup', this._onMouseUp)
this.dispatchEvent({ type: 'end' })
}
onWheel(event) {
if (!this.enabled) return
event.preventDefault()
this.handleZoom(event.deltaY)
this.dispatchEvent({ type: 'change' })
}
handleRotation(deltaX, deltaY) {
// 子类实现具体的旋转逻辑
}
handleZoom(delta) {
// 子类实现具体的缩放逻辑
}
update() {
// 每帧更新逻辑
}
dispose() {
this.domElement.removeEventListener('mousedown', this._onMouseDown)
this.domElement.removeEventListener('wheel', this._onWheel)
this.domElement.removeEventListener('contextmenu', this._onContextMenu)
document.removeEventListener('mousemove', this._onMouseMove)
document.removeEventListener('mouseup', this._onMouseUp)
}
}示例:限制在单一平面上旋转的控制器
javascript
class YawOnlyControls extends CustomControls {
constructor(camera, domElement, options = {}) {
super(camera, domElement)
this.sensitivity = options.sensitivity || 0.005
this.target = options.target || new THREE.Vector3(0, 0, 0)
this.radius = options.radius || 10
this.angle = 0 // 当前水平角度
}
handleRotation(deltaX, deltaY) {
// 只允许水平旋转(绕 Y 轴)
this.angle += deltaX * this.sensitivity
this.updateCameraPosition()
}
handleZoom(delta) {
this.radius += delta * 0.01
this.radius = Math.max(1, Math.min(100, this.radius))
this.updateCameraPosition()
}
updateCameraPosition() {
this.camera.position.x = this.target.x + Math.sin(this.angle) * this.radius
this.camera.position.z = this.target.z + Math.cos(this.angle) * this.radius
this.camera.position.y = this.target.y + this.radius * 0.3
this.camera.lookAt(this.target)
}
update() {
// 如果有自动旋转等逻辑在此实现
}
}多控制器协同
在实际项目中,经常需要多个控制器配合使用。
场景一:编辑器模式
javascript
class EditorControllerManager {
constructor(camera, renderer, scene) {
this.camera = camera
this.renderer = renderer
this.scene = scene
this.orbitControls = new OrbitControls(camera, renderer.domElement)
this.transformControls = new TransformControls(camera, renderer.domElement)
scene.add(this.transformControls)
this.mode = 'orbit' // 'orbit' | 'transform'
this.setupCoordination()
}
setupCoordination() {
// TransformControls 拖拽时禁用 OrbitControls
this.transformControls.addEventListener('dragging-changed', (event) => {
this.orbitControls.enabled = !event.value
})
// 快捷键切换模式
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.setMode('orbit')
this.transformControls.detach()
}
})
}
setMode(mode) {
this.mode = mode
if (mode === 'transform') {
this.orbitControls.enabled = false
} else {
this.orbitControls.enabled = true
}
}
selectObject(object) {
this.transformControls.attach(object)
this.setMode('transform')
}
deselectObject() {
this.transformControls.detach()
this.setMode('orbit')
}
update() {
this.orbitControls.update()
}
}场景二:浏览 + 拖拽混合
javascript
class HybridController {
constructor(camera, renderer) {
this.camera = camera
this.domElement = renderer.domElement
this.orbitControls = new OrbitControls(camera, this.domElement)
this.dragControls = null
this.hoveredObject = null
this.draggableObjects = []
this.raycaster = new THREE.Raycaster()
this.mouse = new THREE.Vector2()
this.setupRaycastSelection()
}
setDraggableObjects(objects) {
this.draggableObjects = objects
if (this.dragControls) {
this.dragControls.dispose()
}
this.dragControls = new DragControls(
objects,
this.camera,
this.domElement
)
this.dragControls.addEventListener('dragstart', () => {
this.orbitControls.enabled = false
})
this.dragControls.addEventListener('dragend', () => {
this.orbitControls.enabled = true
})
this.dragControls.addEventListener('hoveron', (event) => {
this.hoveredObject = event.object
this.domElement.style.cursor = 'grab'
})
this.dragControls.addEventListener('hoveroff', () => {
this.hoveredObject = null
this.domElement.style.cursor = 'default'
})
}
setupRaycastSelection() {
this.domElement.addEventListener('dblclick', (event) => {
this.mouse.x = (event.clientX / this.domElement.clientWidth) * 2 - 1
this.mouse.y = -(event.clientY / this.domElement.clientHeight) * 2 + 1
this.raycaster.setFromCamera(this.mouse, this.camera)
const intersects = this.raycaster.intersectObjects(this.draggableObjects)
if (intersects.length > 0 && this.onDoubleClick) {
this.onDoubleClick(intersects[0].object)
}
})
}
update() {
this.orbitControls.update()
}
}移动端适配
触摸事件处理
javascript
const controls = new OrbitControls(camera, renderer.domElement)
// 触摸配置
controls.touches = {
ONE: THREE.TOUCH.ROTATE, // 单指旋转
TWO: THREE.TOUCH.DOLLY_PAN // 双指缩放+平移
}
// 移动端优化
if ('ontouchstart' in window) {
controls.enablePan = false // 移动端通常禁用平移
controls.rotateSpeed = 0.5 // 降低旋转灵敏度
controls.zoomSpeed = 0.5 // 降低缩放灵敏度
controls.minDistance = 2
controls.maxDistance = 50
}手势冲突处理
javascript
// 防止页面滚动干扰 3D 交互
renderer.domElement.addEventListener('touchmove', (event) => {
if (event.touches.length === 1) {
event.preventDefault()
}
}, { passive: false })
// 双击放大(替代双指缩放)
let lastTap = 0
renderer.domElement.addEventListener('touchend', (event) => {
const currentTime = Date.now()
const tapLength = currentTime - lastTap
if (tapLength < 300 && tapLength > 0) {
// 双击重置视图
controls.reset()
}
lastTap = currentTime
})API 参考
控制器通用方法
| 方法 | 说明 | 所有控制器支持 |
|---|---|---|
update() | 每帧调用以更新状态 | ⚠️ 仅部分需要 |
dispose() | 清理事件监听和资源 | ✅ |
enabled | 启用/禁用控制器 | ✅ |
OrbitControls 完整属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
camera | Camera | 构造传入 | 关联的相机 |
domElement | HTMLElement | 构造传入 | 关联的 DOM 元素 |
target | Vector3 | (0,0,0) | 旋转中心/焦点 |
enableDamping | Boolean | false | 是否启用阻尼 |
dampingFactor | Number | 0.05 | 阻尼系数 |
autoRotate | Boolean | false | 是否自动旋转 |
autoRotateSpeed | Number | 2.0 | 自动旋转速度 |
enableRotate | Boolean | true | 是否可旋转 |
rotateSpeed | Number | 1.0 | 旋转速度 |
minPolarAngle | Number | 0 | 最小垂直角 |
maxPolarAngle | Number | π | 最大垂直角 |
minAzimuthAngle | Number | -∞ | 最小水平角 |
maxAzimuthAngle | Number | ∞ | 最大水平角 |
enableZoom | Boolean | true | 是否可缩放 |
zoomSpeed | Number | 1.2 | 缩放速度 |
minDistance | Number | 0 | 最近距离 |
maxDistance | Number | ∞ | 最远距离 |
enablePan | Boolean | true | 是否可平移 |
panSpeed | Number | 1.0 | 平移速度 |
screenSpacePanning | Boolean | false | 屏幕空间平移 |
常见问题
Q: 控制器没有反应?
检查以下几点:
javascript
// 1. 确保每帧调用了 update()
function animate() {
requestAnimationFrame(animate)
controls.update() // ← 必须!
renderer.render(scene, camera)
}
// 2. 确保 domElement 正确
new OrbitControls(camera, renderer.domElement) // ✅ 正确
new OrbitControls(camera, document.body) // ❌ 通常不对
// 3. 检查 enabled 状态
console.log(controls.enabled) // 应该是 trueQ: 如何保存和恢复相机状态?
javascript
// 保存状态
function saveCameraState(controls, camera) {
return {
target: controls.target.clone(),
position: camera.position.clone(),
zoom: camera.zoom
}
}
// 恢复状态
function restoreCameraState(state, controls, camera) {
controls.target.copy(state.target)
camera.position.copy(state.position)
camera.zoom = state.zoom
camera.updateProjectionMatrix()
controls.update()
}
// 使用
const savedState = saveCameraState(controls, camera)
// ... 用户操作 ...
restoreCameraState(savedState, controls, camera)Q: OrbitControls 的 autoRotate 怎么暂停?
javascript
controls.autoRotate = true
// 方法一:直接开关
function toggleAutoRotate() {
controls.autoRotate = !controls.autoRotate
}
// 方法二:用户交互时暂停
controls.addEventListener('start', () => {
controls.autoRotate = false
})
controls.addEventListener('end', () => {
setTimeout(() => {
controls.autoRotate = true
}, 2000) // 2秒无操作后恢复自动旋转
})Q: 如何限制相机在地面上方?
javascript
// 方式一:限制 polarAngle
controls.maxPolarAngle = Math.PI / 2 // 不能看向地面以下
// 方式二:在 update 后强制修正
const originalUpdate = controls.update.bind(controls)
controls.update = function() {
originalUpdate()
if (camera.position.y < 0.5) {
camera.position.y = 0.5
}
}Q: 多个 Canvas 共存时控制器冲突怎么办?
确保每个控制器绑定各自的 domElement:
javascript
const canvas1 = document.getElementById('canvas1')
const canvas2 = document.getElementById('canvas2')
const camera1 = new PerspectiveCamera(...)
const camera2 = new PerspectiveCamera(...)
const controls1 = new OrbitControls(camera1, canvas1)
const controls2 = new OrbitControls(camera2, canvas2)