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没同步。
总结一下
这一章我们聊了三件事:
- 透视相机 vs 正交相机——选哪个?看你的场景需不需要“近大远小”。做3D游戏用透视,做2.5D界面用正交。
- OrbitControls——三行代码搞定相机交互,记得开damping,记得调update。
- 相机动画——手动更新位置适合简单场景,复杂缓动建议上GSAP。
下一章,咱们聊聊光照。没有光,再好的模型也是一片黑。到时候我会分享一些布光的“野路子”技巧,都是我实际项目中试出来的。