{T}

控制器系统详解

控制器(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 的区别

特性OrbitControlsTrackballControls
旋转轴固定上方向(有极角限制)无固定轴,可任意翻转
适用场景产品预览、建筑展示科学可视化、无限制观察
万向锁通过 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 完整属性

属性类型默认值说明
cameraCamera构造传入关联的相机
domElementHTMLElement构造传入关联的 DOM 元素
targetVector3(0,0,0)旋转中心/焦点
enableDampingBooleanfalse是否启用阻尼
dampingFactorNumber0.05阻尼系数
autoRotateBooleanfalse是否自动旋转
autoRotateSpeedNumber2.0自动旋转速度
enableRotateBooleantrue是否可旋转
rotateSpeedNumber1.0旋转速度
minPolarAngleNumber0最小垂直角
maxPolarAngleNumberπ最大垂直角
minAzimuthAngleNumber-∞最小水平角
maxAzimuthAngleNumber最大水平角
enableZoomBooleantrue是否可缩放
zoomSpeedNumber1.2缩放速度
minDistanceNumber0最近距离
maxDistanceNumber最远距离
enablePanBooleantrue是否可平移
panSpeedNumber1.0平移速度
screenSpacePanningBooleanfalse屏幕空间平移

常见问题

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)  // 应该是 true

Q: 如何保存和恢复相机状态?

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)