Three.js 相机完整指南 - 修正版
1. 相机基础概念
相机决定了场景如何被渲染到屏幕上,相当于观察者的"眼睛"。在 Three.js 中,相机主要分为两种类型:透视相机和正交相机。
2. 两种主要相机类型
2.1 透视相机 (PerspectiveCamera)
const camera = new THREE.PerspectiveCamera(
75, // 视野角度 (FOV)
window.innerWidth / window.innerHeight, // 宽高比
0.1, // 近平面
100 // 远平面
);
特点:
- 模拟人眼视觉,有"近大远小"的效果
- 参数直观,适合大多数3D场景
- 更符合真实世界的视觉感受
2.2 正交相机 (OrthographicCamera)
const aspect = window.innerWidth / window.innerHeight;
const d = 3;
const camera = new THREE.OrthographicCamera(
-d * aspect, // 左边界
d * aspect, // 右边界
d, // 上边界
-d, // 下边界
0.1, // 近平面
100 // 远平面
);
特点:
- 无透视效果,物体大小不随距离改变
- 保持物体的实际尺寸比例
- 常用于工程制图、2.5D游戏、UI界面等
3. 相机的重要属性和方法
3.1 基本属性设置
// 设置相机位置
camera.position.set(4, 2, 5);
// 让相机看向某个点
camera.lookAt(0, 0, 0);
// 相机的缩放(两种相机都有效)
camera.zoom = 1;
3.2 投影矩阵更新
// 当修改相机参数后必须调用
camera.updateProjectionMatrix();
3.3 窗口大小调整处理
window.addEventListener("resize", () => {
// 透视相机的更新方式
camera.aspect = window.innerWidth / window.innerHeight;
// 或者正交相机的更新方式
const aspect = window.innerWidth / window.innerHeight;
const d = 3;
camera.left = -d * aspect;
camera.right = d * aspect;
camera.top = d;
camera.bottom = -d;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
4. camera.zoom 详解
4.1 基本概念
camera.zoom 是一个数值,用于控制相机的缩放级别:
- 默认值:
1(无缩放) - 大于1:放大效果(如
2表示2倍放大) - 小于1:缩小效果(如
0.5表示缩小到一半)
重要更正:camera.zoom 在两种相机中都有效!
4.2 在不同相机类型中的行为
正交相机 (OrthographicCamera):
camera.zoom = 2; // 2倍放大
camera.updateProjectionMatrix(); // 必须调用!
效果:
zoom = 2:视锥体缩小一半,场景看起来放大2倍zoom = 0.5:视锥体扩大一倍,场景看起来缩小一半
透视相机 (PerspectiveCamera):
camera.zoom = 1.5; // 1.5倍放大
camera.updateProjectionMatrix(); // 必须调用!
效果:
- 通过调整投影矩阵实现缩放效果
- 保持透视关系不变
- 相当于改变视野范围,但不改变实际FOV值
4.3 重要注意事项
必须调用 updateProjectionMatrix():
// ❌ 错误:修改zoom后不更新
camera.zoom = 2;
// ✅ 正确:修改zoom后必须更新投影矩阵
camera.zoom = 2;
camera.updateProjectionMatrix();
4.4 与 OrbitControls 的配合
OrbitControls 会自动管理相机的 zoom:
orbitControls = new OrbitControls(camera, renderer.domElement);
// 可以设置zoom的限制
orbitControls.minZoom = 0.5; // 最小缩放
orbitControls.maxZoom = 3; // 最大缩放
// 启用或禁用缩放
orbitControls.enableZoom = true; // 默认true
5. 相机动画
5.1 基础动画原理
相机动画通过不断更新相机的位置、旋转或其他属性来实现:
function animate() {
const time = clock.getElapsedTime();
// 相机绕Y轴旋转
camera.position.x = Math.sin(time) * 5;
camera.position.z = Math.cos(time) * 5;
camera.lookAt(0, 0, 0);
// 更新投影矩阵(如果修改了zoom等属性)
camera.updateProjectionMatrix();
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
5.2 平滑动画技术
使用 GSAP 库实现平滑动画:
import gsap from 'gsap';
// 相机移动到新位置
function moveCameraTo(position, target, duration = 1) {
gsap.to(camera.position, {
x: position.x,
y: position.y,
z: position.z,
duration: duration,
ease: "power2.inOut"
});
gsap.to(camera, {
lookAt: target,
duration: duration,
ease: "power2.inOut",
onUpdate: () => {
camera.lookAt(target.x, target.y, target.z);
}
});
}
// 使用示例
moveCameraTo(
{ x: 10, y: 5, z: 8 },
{ x: 0, y: 0, z: 0 }
);
程序化缩放动画:
function zoomIn() {
gsap.to(camera, {
zoom: 2,
duration: 1,
onUpdate: () => camera.updateProjectionMatrix()
});
}
function zoomOut() {
gsap.to(camera, {
zoom: 1,
duration: 1,
onUpdate: () => camera.updateProjectionMatrix()
});
}
5.3 相机路径动画
使用曲线定义相机路径:
// 创建相机运动路径
const curve = new THREE.CatmullRomCurve3([
new THREE.Vector3(10, 5, 10),
new THREE.Vector3(5, 8, -5),
new THREE.Vector3(-5, 3, 0),
new THREE.Vector3(0, 5, 10)
]);
function animateCameraAlongPath() {
const time = clock.getElapsedTime();
const t = (time * 0.1) % 1; // 循环动画
// 获取路径上的点
const position = curve.getPoint(t);
camera.position.copy(position);
// 让相机看向路径上的下一个点,创造平滑的朝向
const lookAtT = (t + 0.01) % 1;
const lookAtPoint = curve.getPoint(lookAtT);
camera.lookAt(lookAtPoint);
}
5.4 重要注意事项:与 OrbitControls 的冲突
当使用程序化相机动画时,需要特别注意与 OrbitControls 的冲突:
// ❌ 错误做法:同时使用程序化动画和OrbitControls
function animate() {
// 程序化改变相机lookAt
camera.lookAt(movingTarget);
// OrbitControls也在控制相机
orbitControls.update();
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
// ✅ 正确做法1:在程序化动画期间禁用OrbitControls
function animateCameraToTarget(target) {
// 禁用控制器
orbitControls.enabled = false;
// 执行相机动画
gsap.to(camera.position, {
x: target.x,
y: target.y,
z: target.z,
duration: 1,
onComplete: () => {
// 动画完成后重新启用控制器
orbitControls.enabled = true;
}
});
}
// ✅ 正确做法2:修改OrbitControls的target而不是直接修改相机
function changeOrbitControlsTarget(newTarget) {
orbitControls.target.set(newTarget.x, newTarget.y, newTarget.z);
orbitControls.update();
}
// ✅ 正确做法3:完全分离控制模式
let cameraMode = 'auto'; // 'auto' 或 'manual'
function toggleCameraMode() {
if (cameraMode === 'auto') {
// 自动模式:禁用OrbitControls,启用程序化动画
orbitControls.enabled = false;
startCameraAnimation();
} else {
// 手动模式:启用OrbitControls,停止程序化动画
orbitControls.enabled = true;
stopCameraAnimation();
}
}
关键点:当 lookAt 的值改变时,应该临时禁用 OrbitControls!
6. Z-Fighting 问题详解
6.1 什么是 Z-Fighting?
Z-Fighting(深度冲突)是指当两个或多个表面在深度缓冲区(Z-Buffer)中具有相同或非常接近的深度值时,出现的闪烁或交替显示的现象。
6.2 产生原因
- 深度缓冲区精度有限:深度缓冲区使用有限精度(通常是24位或32位浮点数)
- 共面或近平面的几何体:当两个面距离非常近时,深度值可能无法区分
6.3 解决方案
1. 增加距离(推荐):
// 将第二个平面抬高足够距离
plane2.position.y = 0.1; // 增加到0.1,避免深度冲突
2. 使用多边形偏移(Polygon Offset):
const material = new THREE.MeshStandardMaterial({
color: 0xff0000,
polygonOffset: true, // 启用多边形偏移
polygonOffsetFactor: 1, // 偏移因子
polygonOffsetUnits: 1 // 偏移单位
});
3. 调整渲染顺序:
// 强制后渲染的平面在深度测试中胜出
plane2.renderOrder = 1;
4. 修改深度测试函数:
// 较少使用,但也是解决方案之一
material.depthFunc = THREE.LessEqualDepth;
6.4 最佳实践
- 为不同物体预留足够的间距(建议至少0.1个单位)
- 保持场景在合理的尺度范围内(0.1-1000单位)
- 避免使用极小的数值
7. 相机选择建议
7.1 使用透视相机当:
- 需要真实的3D视觉效果
- 制作第一人称/第三人称游戏
- 产品展示、建筑可视化
7.2 使用正交相机当:
- 需要保持物体尺寸一致
- 制作2D游戏或UI界面
- 工程制图、CAD应用
- 等距视角游戏
8. 与 OrbitControls 的配合
orbitControls = new OrbitControls(camera, containerRef.value);
orbitControls.enableDamping = true; // 平滑阻尼效果
orbitControls.dampingFactor = 0.05; // 阻尼系数
// 限制控制范围
orbitControls.minDistance = 2;
orbitControls.maxDistance = 10;
orbitControls.maxPolarAngle = Math.PI / 2; // 限制垂直旋转角度
9. 实际应用示例
9.1 完整的相机设置
<template>
<div class="container" ref="containerRef"></div>
</template>
<script setup>
import * as THREE from "three";
import { onMounted, ref } from "vue";
// Controls鼠标交互
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import Stats from "three/examples/jsm/libs/stats.module";
import * as dat from "dat.gui";
const containerRef = ref(null);
const stat = new Stats();
// 场景、相机、渲染器
const scene = new THREE.Scene();
// 环境光
scene.add(new THREE.AmbientLight(0xffffff, 0.2));
//方向光
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
directionalLight.castShadow = true; // 开启阴影投射
//阴影更光滑优化
directionalLight.shadow.mapSize.set(2048, 2048);
scene.add(directionalLight);
// gui
const gui = new dat.GUI();
gui.add(directionalLight.position, "x", -5, 5, 0.01).name("X灯光的位置");
gui.add(directionalLight.position, "y", -5, 5, 0.01).name("Y灯光的位置");
gui.add(directionalLight.position, "z", -5, 5, 0.01).name("z灯光的位置");
// 只有三种光能产生阴影 DirectionalLight PointLight SpotLight
// Plane
const planeG = new THREE.PlaneGeometry(4, 4,10,10);
const planeM = new THREE.MeshStandardMaterial({
color: 0xcccccc,
side: THREE.DoubleSide,
// wireframe: true,这个看近大远小用的
});
const plane = new THREE.Mesh(planeG, planeM);
plane.receiveShadow = true; // 接收阴影
plane.rotation.x = -Math.PI / 2;
scene.add(plane);
//再加一个平面
const planeG2 = new THREE.PlaneGeometry(1,4);
const planeM2 = new THREE.MeshStandardMaterial({
color: 0xff0000, // 添加颜色以便区分
polygonOffset: true, // 启用多边形偏移
polygonOffsetFactor: 1, // 偏移因子
polygonOffsetUnits: 1 // 偏移单位
});
const plane2 = new THREE.Mesh(planeG2, planeM2);
scene.add(plane2);
// z-flighting的问题
plane2.position.y = 0.01; // 在 Y 轴方向偏移,而不是 Z 轴
plane2.rotation.x = -0.5 * Math.PI;
// 添加一个球体作为示例
const sphereGeometry = new THREE.SphereGeometry(0.5, 32, 32);
const sphereMaterial = new THREE.MeshStandardMaterial({
color: 0xffff00,
});
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.position.set(0, 0.5, 0);
// sphere.position.y = 1; // 将球体稍微抬高,避免与平面重合
sphere.castShadow = true; // 投射阴影
scene.add(sphere);
// 第一种相机,透视相机
// const camera = new THREE.PerspectiveCamera(
// 75,
// window.innerWidth / window.innerHeight,
// 0.1,
// 100
// );
// 第二种相机,正交相机
const aspect = window.innerWidth / window.innerHeight;
const d = 3;
const camera = new THREE.OrthographicCamera(
-d * aspect,
d * aspect,
d,
-d,
0.1,
100
);
camera.position.set(4, 2, 5);
camera.lookAt(0, 0, 0);
camera.zoom = 1;
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true; // 启用阴影映射
// 坐标轴
const axes = new THREE.AxesHelper(50);
scene.add(axes);
// 控制器和动画
let orbitControls;
const clock = new THREE.Clock();
function animate() {
const time = clock.getElapsedTime();
sphere.position.y = Math.abs(Math.sin(time)) + 0.5;
// camera.position.x = Math.sin(time) * 5;
// camera.position.z = Math.cos(time) * 5;
//和上边的camera.zoom 配合使用
camera.updateProjectionMatrix();
// 更新控制器
if (orbitControls) {
orbitControls.update();
}
// 渲染
renderer.render(scene, camera);
stat.update();
requestAnimationFrame(animate);
}
// 挂载
onMounted(() => {
// 初始化轨道控制器
orbitControls = new OrbitControls(camera, containerRef.value);
orbitControls.enableDamping = true;
// 添加到DOM
containerRef.value.appendChild(renderer.domElement);
containerRef.value.appendChild(stat.domElement);
// 开始动画
animate();
});
// 窗口大小调整
window.addEventListener("resize", () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
<style>
.container {
width: 100%;
height: 100vh;
overflow: hidden;
}
</style>
10. 总结与最佳实践
- 相机类型选择:根据项目需求选择合适的相机类型
- 参数更新:修改相机参数后一定要调用
updateProjectionMatrix() - zoom 属性:在两种相机中都有效,但行为略有不同
- 控制器冲突:程序化相机动画与 OrbitControls 会冲突,需要适当处理
- 深度冲突:注意 Z-Fighting 问题,合理设置物体间距或使用多边形偏移
- 性能优化:合理设置近平面和远平面,避免过大比例影响深度精度
- 响应式设计:正确处理窗口大小变化,保持正确的宽高比
特别提醒:
camera.zoom在透视相机和正交相机中都有效- 当使用程序化
lookAt()时,应该临时禁用 OrbitControls 避免冲突 - 修改任何相机参数后都必须调用
updateProjectionMatrix()
掌握 Three.js 相机的使用是创建优秀3D应用的关键。理解不同类型相机的特点、学会处理常见问题、掌握相机动画技术,能够让你更好地控制3D场景的视觉表现。
966

被折叠的 条评论
为什么被折叠?



