Three.js相机类型与应用详解( 这介详解是根据我自己的代码来的)

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 产生原因

  1. 深度缓冲区精度有限:深度缓冲区使用有限精度(通常是24位或32位浮点数)
  2. 共面或近平面的几何体:当两个面距离非常近时,深度值可能无法区分

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. 总结与最佳实践

  1. 相机类型选择:根据项目需求选择合适的相机类型
  2. 参数更新:修改相机参数后一定要调用 updateProjectionMatrix()
  3. zoom 属性:在两种相机中都有效,但行为略有不同
  4. 控制器冲突:程序化相机动画与 OrbitControls 会冲突,需要适当处理
  5. 深度冲突:注意 Z-Fighting 问题,合理设置物体间距或使用多边形偏移
  6. 性能优化:合理设置近平面和远平面,避免过大比例影响深度精度
  7. 响应式设计:正确处理窗口大小变化,保持正确的宽高比

特别提醒:

  • camera.zoom 在透视相机和正交相机中都有效
  • 当使用程序化 lookAt() 时,应该临时禁用 OrbitControls 避免冲突
  • 修改任何相机参数后都必须调用 updateProjectionMatrix()

掌握 Three.js 相机的使用是创建优秀3D应用的关键。理解不同类型相机的特点、学会处理常见问题、掌握相机动画技术,能够让你更好地控制3D场景的视觉表现。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值