Chapter 05

相机与控制

透视相机与正交相机的选择、OrbitControls 交互旋转、FPS 视角、Raycaster 点击拾取 3D 对象,构建可交互 3D 场景

1. PerspectiveCamera vs OrthographicCamera

Three.js 最常用的两种相机模型,对应两种根本不同的投影方式:

透视相机(Perspective)

近大远小,模拟人眼/真实相机,最常用。

new THREE.PerspectiveCamera(
  75,   // fov(°)
  w/h,  // aspect
  0.1, // near
  1000 // far
);

正交相机(Orthographic)

无透视失真,远近物体同样大小,用于 2D 游戏、UI、等距视图。

new THREE.OrthographicCamera(
  -w/2, w/2, // left/right
  h/2, -h/2, // top/bottom
  0.1, 1000 // near/far
);

视锥体(Frustum)详解

2. OrbitControls 交互旋转缩放

OrbitControls 是 Three.js 最常用的相机控制器,提供鼠标拖拽旋转、滚轮缩放、右键平移等交互:

import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

const controls = new OrbitControls(camera, renderer.domElement);

// 阻尼(惯性),让旋转更丝滑
controls.enableDamping = true;
controls.dampingFactor = 0.05;

// 限制旋转范围
controls.minPolarAngle = 0;           // 最小仰角(0 = 正上方)
controls.maxPolarAngle = Math.PI / 2; // 最大俯角(90° = 水平)
controls.minAzimuthAngle = -Math.PI / 4; // 最小水平旋转
controls.maxAzimuthAngle =  Math.PI / 4; // 最大水平旋转

// 限制缩放范围
controls.minDistance = 2;
controls.maxDistance = 20;

// 禁用某些交互
controls.enablePan    = false; // 禁止平移
controls.enableZoom   = true;
controls.enableRotate = true;

// 自动旋转(展示用途)
controls.autoRotate      = true;
controls.autoRotateSpeed = 2.0; // 转速(RPM)

// ⚠️ 必须在动画循环中调用 update(尤其是启用了 damping 或 autoRotate)
function animate() {
  requestAnimationFrame(animate);
  controls.update(); // 不能省略!
  renderer.render(scene, camera);
}

// 监听控制器事件
controls.addEventListener('change', () => {
  console.log('相机位置变化了');
});

3. PointerLockControls — FPS 视角

PointerLockControls 锁定鼠标指针,实现第一人称视角(FPS)的鼠标转向控制:

import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';

const controls = new PointerLockControls(camera, renderer.domElement);

// 点击锁定鼠标
document.addEventListener('click', () => controls.lock());

controls.addEventListener('lock', () => {
  document.getElementById('instructions').style.display = 'none';
});
controls.addEventListener('unlock', () => {
  document.getElementById('instructions').style.display = 'flex';
});

// WASD 移动
const keys = {};
document.addEventListener('keydown', e => keys[e.code] = true);
document.addEventListener('keyup',   e => keys[e.code] = false);

const clock = new THREE.Clock();
const SPEED = 5;

function animate() {
  requestAnimationFrame(animate);
  const delta = clock.getDelta();

  if (controls.isLocked) {
    if (keys['KeyW']) controls.moveForward(SPEED * delta);
    if (keys['KeyS']) controls.moveForward(-SPEED * delta);
    if (keys['KeyA']) controls.moveRight(-SPEED * delta);
    if (keys['KeyD']) controls.moveRight(SPEED * delta);
  }

  renderer.render(scene, camera);
}
animate();

4. 相机动画 lerp

平滑过渡相机位置,使用线性插值(lerp):

const targetPos = new THREE.Vector3(5, 3, 0);
const targetLook = new THREE.Vector3(0, 0, 0);

function animate() {
  requestAnimationFrame(animate);

  // 缓慢移向目标位置(0.05 = 缓动系数,越小越慢)
  camera.position.lerp(targetPos, 0.05);

  // lookAt 也可以插值
  const currentLook = new THREE.Vector3();
  camera.getWorldDirection(currentLook);
  // 使用 slerp 对四元数插值实现平滑旋转
  const targetQuat = new THREE.Quaternion().setFromRotationMatrix(
    new THREE.Matrix4().lookAt(camera.position, targetLook, camera.up)
  );
  camera.quaternion.slerp(targetQuat, 0.05);

  renderer.render(scene, camera);
}

5. Raycaster 鼠标拾取(点击 3D 对象)

Raycaster 从相机出发,沿鼠标方向发射一条射线,检测它与场景中哪些物体相交,从而实现点击 3D 对象的交互:

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

// 将屏幕坐标转换为 NDC 坐标(-1 到 1)
function getMouseNDC(event) {
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
}

// 点击检测
const interactObjects = [cube, sphere, torus]; // 可交互对象列表

window.addEventListener('click', (event) => {
  getMouseNDC(event);

  // 从相机发射射线
  raycaster.setFromCamera(mouse, camera);

  // 检测与指定对象的交叉(第二个参数 true = 包含后代)
  const intersects = raycaster.intersectObjects(interactObjects, true);

  if (intersects.length > 0) {
    const hit = intersects[0]; // 最近的交点(数组按距离排序)
    console.log('点击了:', hit.object.name);
    console.log('交点位置:', hit.point);      // Vector3 世界坐标
    console.log('距离:',    hit.distance);   // 相机到交点距离
    console.log('面索引:',  hit.faceIndex);  // 被击中的三角面

    // 高亮被点击的对象
    hit.object.material.color.set(0xff4444);
  }
});

// Hover 悬浮效果
window.addEventListener('mousemove', (event) => {
  getMouseNDC(event);
  raycaster.setFromCamera(mouse, camera);
  const intersects = raycaster.intersectObjects(interactObjects);

  // 改变鼠标样式
  document.body.style.cursor = intersects.length > 0 ? 'pointer' : 'default';
});
ℹ️

Raycaster 性能优化intersectObjects 对每个对象进行 AABB 包围盒检测再做精确测试,当场景中有大量对象时,可以手动设置 raycaster.params.Points.threshold 调整精度,或使用八叉树(Octree)加速空间查询。

本章小结:透视相机用于绝大多数 3D 场景;正交相机用于 2D/UI/等距视图。OrbitControls 提供免费的交互控制,记得在动画循环中调用 controls.update()。Raycaster 是所有 3D 交互的基础。