当前位置: 首页 > news >正文

Three.js 开发实战教程(四):相机系统全解析与多视角控制

在前几篇教程中,我们通过灯光与阴影提升了场景真实感,但如何 “观察” 场景同样关键 —— 这正是相机系统的核心作用。Three.js 提供了多种相机类型,不同相机决定了场景的呈现视角(如人眼透视、工程图纸正交)。本篇将系统讲解相机的工作原理、参数配置,并通过实战实现 “多视角切换”“第一人称漫游” 等常见需求,帮助开发者灵活控制场景观察方式。

一、相机系统核心知识

相机是 3D 场景的 “眼睛”,其类型和参数直接决定渲染结果。Three.js 中最常用的是透视相机正交相机,二者的核心区别在于是否遵循 “近大远小” 的透视规律。

1. 透视相机(PerspectiveCamera)

模拟人眼观察世界的方式,物体近大远小,适合创建真实感场景(如游戏、产品展示)。

核心参数
new THREE.PerspectiveCamera(fov, aspect, near, far)
  • fov(视场角):垂直方向的视野角度(单位:度),值越小视野越窄(类似望远镜),越大视野越宽(类似广角镜头),常用 60-75 度。
  • aspect(宽高比):相机视口的宽高比(通常为容器宽 / 高),比例失调会导致场景拉伸变形。
  • near(近裁剪面):相机能看到的最近距离,小于此值的物体不会被渲染。
  • far(远裁剪面):相机能看到的最远距离,大于此值的物体不会被渲染。
关键特性
  • 必须保持aspect = 容器宽/高,否则画面会拉伸(如宽屏显示正方形变成矩形)。
  • nearfar的差值不宜过大(如 1-10000),否则会导致 “深度冲突”(物体边缘出现闪烁)。

2. 正交相机(OrthographicCamera)

物体大小与距离无关,适合工程图纸、2D 游戏、UI 界面等场景(如 CAD 软件、俯视地图)。

核心参数
new THREE.OrthographicCamera(left, right, top, bottom, near, far)
  • left/right:视口左 / 右边界坐标。
  • top/bottom:视口上 / 下边界坐标。
  • near/far:近 / 远裁剪面(同透视相机)。
关键特性
  • 宽高比由(right-left)/(top-bottom)决定,需与容器比例一致。
  • 物体尺寸在任何距离下保持不变(如 10 单位的立方体,在远处仍显示 10 单位大小)。

3. 相机位置与朝向控制

无论哪种相机,都需要通过以下方法调整观察视角:

  • camera.position.set(x, y, z):设置相机位置(三维坐标)。
  • camera.lookAt(x, y, z):设置相机朝向(指向目标点)。
  • camera.up.set(x, y, z):设置相机 “上方向”(默认 (0,1,0),即 Y 轴为上)。

二、实战:多相机切换与第一人称控制

本次实战目标:创建一个包含 “产品展示台” 的场景,实现三种视角切换(透视全局、正交顶视、第一人称漫游),并支持键盘控制移动。

1. 前置准备

  • 基于前序项目,无需额外依赖。
  • 准备 1 张展示台纹理图,放入public/images/目录:platform-texture.jpg

2. 完整代码实现(创建 CameraSystemDemo.vue)

src/components/目录下新建组件,代码含详细注释:

<template><div class="container"><div class="three-container" ref="container"></div><!-- 视角控制按钮 --><div class="control-panel"><button @click="switchCamera('perspective')">透视全局</button><button @click="switchCamera('orthographic')">正交顶视</button><button @click="switchCamera('firstPerson')">第一人称</button><p>第一人称控制:WASD移动,鼠标拖动旋转视角</p></div></div>
</template><script setup>
import { ref, onMounted, onUnmounted, reactive } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js' // 第一人称控制器// 核心变量
const container = ref(null)
const state = reactive({currentCamera: 'perspective', // 当前相机类型cameras: {}, // 存储所有相机velocity: new THREE.Vector3(), // 第一人称移动速度direction: new THREE.Vector3(), // 第一人称移动方向moveForward: false,moveBackward: false,moveLeft: false,moveRight: false,canJump: false // 跳跃开关(本例暂不实现跳跃)
})let scene, renderer, controls, firstPersonControls, animationId
let platform, box, sphere // 场景物体// 初始化场景
const initScene = () => {// --------------------------// 1. 基础配置(场景、渲染器)// --------------------------scene = new THREE.Scene()scene.background = new THREE.Color(0xf0f0f0)const width = container.value.clientWidthconst height = container.value.clientHeight// 渲染器renderer = new THREE.WebGLRenderer({ antialias: true })renderer.setSize(width, height)container.value.appendChild(renderer.domElement)// --------------------------// 2. 创建相机(三种类型)// --------------------------// 2.1 透视相机(全局视角)state.cameras.perspective = new THREE.PerspectiveCamera(60, // fovwidth / height, // aspect0.1, // near1000 // far)state.cameras.perspective.position.set(8, 8, 8) // 斜上方俯视state.cameras.perspective.lookAt(0, 0, 0)// 2.2 正交相机(顶视视角)const aspect = width / heightconst orthoSize = 10 // 正交相机视口大小state.cameras.orthographic = new THREE.OrthographicCamera(-orthoSize * aspect, // leftorthoSize * aspect, // rightorthoSize, // top-orthoSize, // bottom0.1,1000)state.cameras.orthographic.position.set(0, 20, 0) // 正上方state.cameras.orthographic.lookAt(0, 0, 0)// 2.3 第一人称相机(漫游视角)state.cameras.firstPerson = new THREE.PerspectiveCamera(75,width / height,0.1,1000)state.cameras.firstPerson.position.set(0, 1.6, 5) // 模拟人眼高度(y=1.6)// --------------------------// 3. 创建场景物体// --------------------------const textureLoader = new THREE.TextureLoader()// 3.1 展示台(地面)const platformGeometry = new THREE.PlaneGeometry(15, 15)const platformMaterial = new THREE.MeshLambertMaterial({map: textureLoader.load('/images/platform-texture.jpg')})platform = new THREE.Mesh(platformGeometry, platformMaterial)platform.rotation.x = -Math.PI / 2platform.receiveShadow = truescene.add(platform)// 3.2 立方体const boxGeometry = new THREE.BoxGeometry(2, 2, 2)const boxMaterial = new THREE.MeshPhongMaterial({ color: 0x409eff })box = new THREE.Mesh(boxGeometry, boxMaterial)box.position.set(-3, 1, 0)box.castShadow = truescene.add(box)// 3.3 球体const sphereGeometry = new THREE.SphereGeometry(1.2, 32, 32)const sphereMaterial = new THREE.MeshPhongMaterial({ color: 0xff7d00 })sphere = new THREE.Mesh(sphereGeometry, sphereMaterial)sphere.position.set(3, 1.2, 0)sphere.castShadow = truescene.add(sphere)// --------------------------// 4. 灯光(复用前序配置)// --------------------------const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)scene.add(ambientLight)const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)directionalLight.position.set(10, 10, 10)directionalLight.castShadow = truescene.add(directionalLight)// --------------------------// 5. 控制器配置// --------------------------// 5.1 轨道控制器(用于透视/正交相机)controls = new OrbitControls(state.cameras.perspective,renderer.domElement)controls.enableDamping = true// 5.2 第一人称控制器(基于PointerLockControls)firstPersonControls = new PointerLockControls(state.cameras.firstPerson,document.body)// 点击场景时进入第一人称锁定状态container.value.addEventListener('click', () => {if (state.currentCamera === 'firstPerson') {firstPersonControls.lock()}})// --------------------------// 6. 键盘事件监听(第一人称移动)// --------------------------const onKeyDown = (event) => {switch (event.code) {case 'KeyW': state.moveForward = true; breakcase 'KeyA': state.moveLeft = true; breakcase 'KeyS': state.moveBackward = true; breakcase 'KeyD': state.moveRight = true; break}}const onKeyUp = (event) => {switch (event.code) {case 'KeyW': state.moveForward = false; breakcase 'KeyA': state.moveLeft = false; breakcase 'KeyS': state.moveBackward = false; breakcase 'KeyD': state.moveRight = false; break}}document.addEventListener('keydown', onKeyDown)document.addEventListener('keyup', onKeyUp)// --------------------------// 7. 动画循环// --------------------------const animate = () => {animationId = requestAnimationFrame(animate)// 第一人称移动逻辑if (state.currentCamera === 'firstPerson' && firstPersonControls.isLocked) {// 重置速度state.velocity.x -= state.velocity.x * 10.0 * 0.01state.velocity.z -= state.velocity.z * 10.0 * 0.01// 计算方向(基于相机朝向)state.direction.z = Number(state.moveForward) - Number(state.moveBackward)state.direction.x = Number(state.moveRight) - Number(state.moveLeft)state.direction.normalize() // 确保斜向移动速度与轴向一致// 应用速度if (state.moveForward || state.moveBackward) {state.velocity.z -= state.direction.z * 20.0 * 0.01}if (state.moveLeft || state.moveRight) {state.velocity.x -= state.direction.x * 20.0 * 0.01}// 更新位置(限制Y轴防止飞行)firstPersonControls.moveRight(-state.velocity.x * 0.1)firstPersonControls.moveForward(-state.velocity.z * 0.1)state.cameras.firstPerson.position.y = 1.6 // 固定高度} else {// 非第一人称时更新轨道控制器controls.update()}// 渲染当前激活的相机renderer.render(scene, state.cameras[state.currentCamera])}animate()
}// 切换相机
const switchCamera = (type) => {state.currentCamera = type// 切换控制器目标相机if (type !== 'firstPerson') {firstPersonControls.unlock() // 退出第一人称锁定controls.object = state.cameras[type] // 轨道控制器绑定到当前相机controls.enableDamping = true}
}// 窗口自适应
const handleResize = () => {if (!container.value || !renderer) returnconst width = container.value.clientWidthconst height = container.value.clientHeight// 更新透视相机state.cameras.perspective.aspect = width / heightstate.cameras.perspective.updateProjectionMatrix()// 更新第一人称相机state.cameras.firstPerson.aspect = width / heightstate.cameras.firstPerson.updateProjectionMatrix()// 更新正交相机(保持宽高比)const orthoSize = 10const aspect = width / heightstate.cameras.orthographic.left = -orthoSize * aspectstate.cameras.orthographic.right = orthoSize * aspectstate.cameras.orthographic.top = orthoSizestate.cameras.orthographic.bottom = -orthoSizestate.cameras.orthographic.updateProjectionMatrix()// 更新渲染器renderer.setSize(width, height)
}// 生命周期
onMounted(() => {initScene()window.addEventListener('resize', handleResize)
})onUnmounted(() => {window.removeEventListener('resize', handleResize)cancelAnimationFrame(animationId)renderer.dispose()controls.dispose()firstPersonControls.dispose()document.removeEventListener('keydown', () => {})document.removeEventListener('keyup', () => {})
})
</script><style scoped>
.container {position: relative;
}.three-container {width: 100vw;height: 80vh;margin-top: 20px;
}.control-panel {position: absolute;top: 20px;left: 20px;display: flex;gap: 10px;flex-direction: column;z-index: 100;
}button {padding: 8px 12px;background: #409eff;color: white;border: none;border-radius: 4px;cursor: pointer;
}button:hover {background: #66b1ff;
}p {margin: 0;font-size: 14px;color: #333;
}
</style>

3. 运行效果

启动项目后,可体验:

  • 三种视角切换
    • 透视全局:斜上方视角,支持轨道控制器旋转 / 缩放。
    • 正交顶视:正上方俯视,物体大小与距离无关(类似 2D 俯视图)。
    • 第一人称:点击场景进入鼠标锁定模式,WASD 键控制移动,鼠标拖动旋转视角(模拟人在场景中漫游)。
  • 自适应窗口:调整浏览器窗口大小,三种相机均能保持画面比例正确。

三、相机使用常见问题与解决方案

1. 画面拉伸变形?

  • 原因:相机aspect参数与容器宽高比不一致。
  • 解决:在窗口resize事件中同步更新aspect并调用camera.updateProjectionMatrix()

2. 物体突然消失?

  • 原因:物体位置超出相机near-far范围,或相机lookAt指向错误。
  • 解决:
    • 检查camera.nearfar是否覆盖物体坐标。
    • 确保camera.lookAt()指向场景内物体(而非空坐标)。

3. 第一人称控制器视角抖动?

  • 原因:移动速度过快或未限制 Y 轴位置。
  • 解决:
    • 降低移动速度(如*0.1缩放)。
    • 固定 Y 轴高度(模拟地面行走,防止上下浮动)。

4. 正交相机物体显示不全?

  • 原因:left/right/top/bottom范围未覆盖场景物体。
  • 解决:根据场景大小调整正交相机边界(如orthoSize值),确保包含所有物体。

四、专栏预告

下一篇将讲解 Three.js 的外部模型加载与优化,内容包括:

  • 常见 3D 模型格式(glTF、OBJ、FBX)的加载方法。
  • 模型压缩、纹理优化、层级管理等实战技巧。
  • 实战:加载并控制一个带骨骼动画的人物模型。
http://www.dtcms.com/a/395663.html

相关文章:

  • 介绍一下SQLite的基本语法和常用命令
  • 台式电脑如何恢复出厂设置?Win10 强制重置详细教程
  • 李宏毅2023机器学习作业 HW02实操
  • 【C++实战㉜】深入C++动态内存分配:从理论到实战的进阶之路
  • 小鼠抗新冠病毒N蛋白IgG亚型抗体ELISA检测试剂盒
  • 安防监控中常见的报警类型有哪些?国标GB28181平台EasyGBS的报警能力解析
  • C++ 中 size_t 的用(用于跨平台编译)
  • C++ 拷贝构造函数调用时机
  • 手机镜头参数介绍
  • 区块链技术之《(1)—概述》
  • 复盘与导出工具最新版V31.0版本更新---彻底修复卡死闪退bug,盘中实时丝滑
  • 深入理解JVM类加载与垃圾回收机制
  • Ethernet/IP转ProfiNet网关选型指南:欧姆龙PLC对接研祥工控机最佳实践
  • Java 面试高频手撕题清单
  • 【论文阅读】Long-VLA:释放视觉语言动作模型在机器人操作中的长时程能力
  • Python poplib 库全解析:POP3 邮件收取的完整指南
  • DanceTrack数据集介绍
  • 【无标题】话题学习笔记1,话题基本了解
  • 【论文阅读】OpenVLA:一个开源的视觉-语言-动作模型
  • 科技信息差(9.22)
  • Zotero中进行文献翻译【Windows11】【新版,目前没发现bug】
  • 单细胞数据分析:单细胞计数矩阵(Seurat)
  • Hyperf使用视图
  • React何时用函数组件(Hooks),何时用类组件?(错误边界用类组件Error Boundary)
  • VMware虚拟机ubuntu20.04共享文件夹突然无法使用
  • 流行AI工具的分类与比较
  • 哪些行业需要使用时序数据库?
  • PyTorch 神经网络工具箱简明笔记
  • Pytorch目录细查
  • VMware的Ubuntu与windows共享文件夹