4、相机控制:透视相机与正交相机的区别,OrbitControls实现交互控制,相机动画

好,咱们今天聊聊相机。在3D世界里,相机就是你的眼睛。你想想看,没有相机,你拿什么看场景?

Three.js里有两类最常用的相机:透视相机正交相机。我刚开始学的时候,总觉得它们差不多,不就是个相机嘛。结果第一次做项目就踩了坑——我用正交相机去渲染一个建筑模型,出来的效果怎么看怎么别扭,像个平面图。后来才明白,这两兄弟的“脾气”完全不同。

透视相机 vs 正交相机

说白了,透视相机模拟的是人眼的真实视觉。近大远小,有纵深感。正交相机则像是工程制图,物体大小不随距离变化。

特性 透视相机 (PerspectiveCamera) 正交相机 (OrthographicCamera)
视觉效果 近大远小,有透视感 大小一致,无透视变形
适用场景 游戏、建筑漫游、VR 2.5D游戏、UI、数据可视化
参数特点 需要视场角(fov) 需要左右上下边界
性能开销 稍高(需要透视计算) 较低

核心区别一句话:透视相机有“消失点”,正交相机没有。你离物体越近,透视效果越明显。

来看代码。创建一个透视相机很简单:

// 透视相机:参数依次是视场角、宽高比、近裁面、远裁面
const camera = new THREE.PerspectiveCamera(
  75,                    // fov,我习惯用45-75之间
  window.innerWidth / window.innerHeight, // 宽高比
  0.1,                   // 近裁面,别设成0,会出问题
  1000                   // 远裁面
)
camera.position.set(5, 5, 5)
camera.lookAt(0, 0, 0)

正交相机呢,参数稍微绕一点:

// 正交相机:左右上下近远
const camera = new THREE.OrthographicCamera(
  -5, 5,   // left, right
  5, -5,   // top, bottom(注意:top > bottom)
  0.1,      // near
  1000      // far
)
camera.position.set(5, 5, 5)
camera.lookAt(0, 0, 0)

我的小经验:正交相机的top和bottom参数,我经常写反。记住一个口诀:top值大于bottom值。因为Y轴向上,所以上面的数值大。

OrbitControls:让相机“活”起来

光有相机还不够,你得能控制它。手动写鼠标拖拽、缩放?太累了。Three.js官方提供了OrbitControls,开箱即用。

我记得第一次用OrbitControls时,就一行代码,鼠标就能旋转、平移、缩放场景了。当时觉得:嗯,真香。

import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'

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

// 常用配置
controls.enableDamping = true    // 开启惯性效果,手感更顺滑
controls.dampingFactor = 0.05   // 惯性系数,越小越滑
controls.autoRotate = false     // 是否自动旋转
controls.enableZoom = true      // 允许缩放
controls.zoomSpeed = 1.0        // 缩放速度
controls.target.set(0, 0, 0)    // 围绕哪个点旋转

注意:如果你开启了enableDamping,必须在动画循环里调用controls.update(),否则惯性效果不生效。我曾经因为这个排查了半天,结果就是少写了一行update。

OrbitControls还有一些很实用的属性:

  • minDistance / maxDistance:限制缩放范围,防止相机穿模
  • minPolarAngle / maxPolarAngle:限制上下旋转角度,比如不让相机转到地下
  • enablePan:是否允许平移,有些场景需要固定视角

相机动画:让视角“讲故事”

静态的相机太死板了。你想让用户一进入页面,相机自动绕场景转一圈?或者点击按钮,相机平滑地飞到某个物体面前?这就是相机动画。

实现方式有很多,我推荐两种:

方式一:手动更新position

在动画循环里,每帧修改相机的位置。简单直接,适合做匀速运动。

function animate() {
  requestAnimationFrame(animate)

  // 让相机绕Y轴旋转
  const time = Date.now() * 0.001
  camera.position.x = 10 * Math.sin(time * 0.5)
  camera.position.z = 10 * Math.cos(time * 0.5)
  camera.lookAt(0, 0, 0)

  controls.update()
  renderer.render(scene, camera)
}

方式二:使用TWEEN.js或GSAP

如果你需要缓动效果(比如先快后慢),建议用动画库。我个人偏爱GSAP,API很直观。

// 假设你已经引入了GSAP
// 点击按钮,相机飞到物体位置
function flyToObject(objectPosition) {
  gsap.to(camera.position, {
    x: objectPosition.x + 3,
    y: objectPosition.y + 3,
    z: objectPosition.z + 3,
    duration: 1.5,
    ease: 'power2.inOut',
    onUpdate: () => {
      camera.lookAt(objectPosition)
      controls.update()
    }
  })
}

避坑指南:做相机动画时,别忘了同时更新controls.target。否则OrbitControls会跟你的动画“打架”,相机位置变了,但旋转中心还是原来的点,画面会突然跳一下。我曾经就被这个坑过,用户反馈说“画面闪了一下”,排查了好久才发现是target没同步。

总结一下

这一章我们聊了三件事:

  1. 透视相机 vs 正交相机——选哪个?看你的场景需不需要“近大远小”。做3D游戏用透视,做2.5D界面用正交。
  2. OrbitControls——三行代码搞定相机交互,记得开damping,记得调update。
  3. 相机动画——手动更新位置适合简单场景,复杂缓动建议上GSAP。

下一章,咱们聊聊光照。没有光,再好的模型也是一片黑。到时候我会分享一些布光的“野路子”技巧,都是我实际项目中试出来的。