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)详解
- fov(Field of View) 垂直视野角度(度)。常用值 45°-75°。fov 越大视野越广,但边缘畸变越明显;越小越像长焦镜头,压缩空间感。
-
aspect(宽高比)
通常是
canvas.width / canvas.height。窗口大小改变时必须更新,并调用updateProjectionMatrix()。 - near / far(裁剪面) 只有 near 到 far 之间的物体才会渲染。near 不要太小(会导致深度精度问题),far 不要太大(near/far 比值影响 Z-fighting)。
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 交互的基础。