第三章 Vue3 + Three.js 实战:用 OrbitControls 实现相机交互与 3D 立方体展示
上一章中,介绍的是通过监听鼠标事件,控制3D立方体。本文结合官方控制器 OrbitControls,实现 “控制相机观察 3D 立方体” 的功能,涵盖场景搭建、光照配置、交互优化等核心知识点。
一、效果预览与核心功能
最终交互效果
- 左键拖拽:旋转相机,从不同角度观察立方体
- 鼠标滚轮:缩放相机,拉近 / 拉远与立方体的距离
- 右键拖拽 / Shift + 左键:平移相机,改变观察位置
- 阻尼惯性:操作结束后相机仍有轻微惯性,交互更流畅
- 自适应窗口:窗口缩放时,3D 场景自动调整比例,无变形
技术栈选型
技术 / 工具 | 作用说明 |
---|---|
Vue3(<script setup> ) | 组件化开发,简化语法 |
Three.js | 构建 3D 场景、物体与光照 |
OrbitControls | Three.js 官方相机控制器,实现拖拽 / 缩放 / 平移 |
二、前置知识储备
在开始前,建议掌握以下基础:
- Vue3 核心语法:
ref
响应式、生命周期钩子(onMounted
/onUnmounted
) - Three.js 三要素:场景(Scene)、相机(Camera)、渲染器(Renderer)
- OrbitControls 基础概念:官方为简化相机交互开发的控制器,无需手动写鼠标事件
若对 Three.js 基础不熟悉,可先理解核心逻辑:场景是 “舞台”,相机是 “观众视角”,渲染器是 “画布”,三者结合才能显示 3D 内容。
三、完整实现步骤
1. 项目初始化与依赖安装
首先确保 Vue3 项目已创建(若未创建,执行npm create vue@latest
初始化,选择<script setup>
语法),然后安装 Three.js:
npm install three
OrbitControls 无需额外安装,Three.js 已内置在three/addons/controls/
目录中,直接导入即可。
2. 组件完整代码(带详细注释)
创建CameraControlCube.vue
组件,代码如下,每一步都附带核心逻辑说明:
<template><!-- 3D场景容器:通过ref获取DOM,用于挂载Three.js渲染器 --><div class="three-container" ref="container"></div>
</template><script setup>
// 1. 导入依赖:Vue生命周期、Three.js核心、OrbitControls
import { onMounted, ref, onUnmounted } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';// 2. 响应式引用:获取3D容器DOM元素
const container = ref(null);// 3. 声明全局变量:存储Three.js核心对象(避免函数内重复创建)
let scene, camera, renderer, cube, controls, animationId;// 4. 组件挂载时初始化3D场景(页面加载完成后执行)
onMounted(() => {initScene(); // 初始化场景(舞台)initCamera(); // 初始化相机(视角)initRenderer(); // 初始化渲染器(画布)initObject(); // 初始化3D物体(立方体)initLight(); // 初始化光照(避免物体漆黑)initControls(); // 初始化OrbitControls(相机交互)startAnimation(); // 启动动画循环(持续渲染)// 监听窗口缩放,适配场景window.addEventListener('resize', handleResize);
});// 5. 组件卸载时清理资源(防止内存泄漏)
onUnmounted(() => {cancelAnimationFrame(animationId); // 取消动画循环window.removeEventListener('resize', handleResize); // 移除窗口监听controls.dispose(); // 释放控制器资源renderer.dispose(); // 释放渲染器资源
});/*** 6. 初始化场景:创建3D“舞台”,承载所有元素*/
const initScene = () => {scene = new THREE.Scene();// 设置场景背景色(浅灰色,十六进制表示,0xf0f0f0对应RGB(240,240,240))scene.background = new THREE.Color(0xf0f0f0);
};/*** 7. 初始化相机:定义“观众视角”,决定能看到场景的哪些部分*/
const initCamera = () => {// 获取容器宽高(确保相机比例与容器一致,避免物体变形)const { clientWidth, clientHeight } = container.value;// 创建透视相机(模拟人眼视角,近大远小,适合3D场景)camera = new THREE.PerspectiveCamera(75, // 视野角度(FOV):单位度,值越小视角越窄clientWidth / clientHeight, // 宽高比:必须与容器一致0.1, // 近裁剪面:距离相机小于此值的物体不渲染1000 // 远裁剪面:距离相机大于此值的物体不渲染);// 设置相机初始位置(x:5, y:5, z:5):从斜上方观察立方体camera.position.set(5, 5, 5);// 让相机始终“看向”立方体中心(默认原点(0,0,0))camera.lookAt(0, 0, 0);
};/*** 8. 初始化渲染器:将3D场景“画”到浏览器画布上*/
const initRenderer = () => {const { clientWidth, clientHeight } = container.value;// 创建WebGL渲染器,开启抗锯齿(让立方体边缘更平滑,避免锯齿感)renderer = new THREE.WebGLRenderer({ antialias: true });// 设置渲染器尺寸(与容器宽高一致,全屏显示)renderer.setSize(clientWidth, clientHeight);// 将渲染器生成的Canvas元素添加到Vue容器中(否则场景无法显示)container.value.appendChild(renderer.domElement);
};/*** 9. 初始化3D物体:创建可观察的立方体*/
const initObject = () => {// ① 几何体:定义立方体的“形状”(参数:宽、高、深,均为2)const geometry = new THREE.BoxGeometry(2, 2, 2);// ② 材质:定义立方体的“外观”(Phong材质,支持高光效果,更有3D质感)const material = new THREE.MeshPhongMaterial({color: 0xff0000, // 物体颜色(红色,十六进制0xff0000)shininess: 100, // 高光强度:值越大,高光区域越亮、范围越小});// ③ 网格:结合几何体和材质,生成可渲染的3D物体cube = new THREE.Mesh(geometry, material);// ④ 将立方体添加到场景中(否则不显示)scene.add(cube);
};/*** 10. 初始化光照:Three.js中物体默认不发光,需手动添加光源*/
const initLight = () => {// ① 环境光:均匀照亮整个场景,避免局部过暗(柔和补光,无方向)const ambientLight = new THREE.AmbientLight(0xffffff, 0.3);scene.add(ambientLight);// ② 平行光:模拟太阳光,有方向,产生明暗对比(增强立方体立体感)const directionalLight = new THREE.DirectionalLight(0xffffff, 1);directionalLight.position.set(10, 10, 10); // 光源位置(斜上方)scene.add(directionalLight);// ③ 点光源:模拟灯泡,向四周发散光(增加立方体局部高光,更真实)const pointLight = new THREE.PointLight(0xffff00, 0.5);pointLight.position.set(-5, 5, -5); // 光源位置(左上方)scene.add(pointLight);
};/*** 11. 初始化OrbitControls:实现相机交互的核心(重点配置)*/
const initControls = () => {// 创建控制器实例:参数1=要控制的相机,参数2=渲染器的Canvas元素(用于监听鼠标事件)controls = new OrbitControls(camera, renderer.domElement);// -------------------------- 核心交互配置 --------------------------controls.enableRotate = true; // 允许旋转(左键拖拽)controls.enableZoom = true; // 允许缩放(鼠标滚轮)controls.enablePan = true; // 允许平移(右键拖拽/Shift+左键)// -------------------------- 速度配置 --------------------------controls.rotateSpeed = 0.5; // 旋转速度:值越大,拖拽时旋转越快controls.zoomSpeed = 0.7; // 缩放速度:值越大,滚轮缩放越快controls.panSpeed = 0.5; // 平移速度:值越大,拖拽平移越快// -------------------------- 阻尼与边界配置 --------------------------controls.enableDamping = true; // 启用阻尼效果(操作结束后有惯性,更流畅)controls.dampingFactor = 0.05; // 阻尼系数:值越小,惯性越明显(0~1)// 限制垂直旋转范围(防止相机“翻转”到立方体下方,视角更合理)controls.minPolarAngle = 0; // 最小垂直角度(0弧度,水平视角)controls.maxPolarAngle = Math.PI; // 最大垂直角度(π弧度,俯视视角)// 限制水平旋转范围(默认无限制,可根据需求调整)controls.minAzimuthAngle = -Infinity; // 最小水平角度controls.maxAzimuthAngle = Infinity; // 最大水平角度// -------------------------- 事件监听 --------------------------// 监听控制器变化(调试用,可查看相机实时位置)controls.addEventListener('change', () => {console.log('相机位置:', {x: camera.position.x.toFixed(2), // 保留2位小数y: camera.position.y.toFixed(2),z: camera.position.z.toFixed(2),});});
};/*** 12. 启动动画循环:持续渲染场景(Three.js动效的核心)*/
const startAnimation = () => {// 递归调用:浏览器刷新一帧就执行一次(默认60帧/秒)const animate = () => {animationId = requestAnimationFrame(animate); // 记录动画ID,用于后续取消// 让立方体缓慢自转(增强3D效果感知,可根据需求删除)cube.rotation.x += 0.005; // 绕X轴旋转(上下翻转方向)cube.rotation.y += 0.005; // 绕Y轴旋转(左右旋转方向)// 关键:更新控制器状态(启用阻尼后必须调用,否则惯性效果无效)controls.update();// 渲染场景:将“舞台”(scene)通过“视角”(camera)画到“画布”(renderer)上renderer.render(scene, camera);};animate(); // 启动循环
};/*** 13. 窗口缩放处理:适配场景尺寸,避免变形*/
const handleResize = () => {const { clientWidth, clientHeight } = container.value;// ① 更新相机宽高比(确保与容器一致)camera.aspect = clientWidth / clientHeight;camera.updateProjectionMatrix(); // 必须更新相机投影矩阵,否则配置不生效// ② 更新渲染器尺寸(与容器一致)renderer.setSize(clientWidth, clientHeight);
};
</script><style scoped>
/* 14. 容器样式:全屏显示,隐藏滚动条 */
.three-container {width: 100vw; /* 占满屏幕宽度 */height: 100vh; /* 占满屏幕高度 */overflow: hidden; /* 隐藏溢出内容,避免滚动条 */
}
</style>
四、核心逻辑深度解析
1. OrbitControls 配置详解(交互核心)
OrbitControls 的配置直接决定用户体验,重点参数说明:
参数 | 作用与取值建议 |
---|---|
enableRotate | 是否允许旋转(默认 true),关闭后左键拖拽无效 |
enableZoom | 是否允许缩放(默认 true),关闭后滚轮无效 |
enablePan | 是否允许平移(默认 true),关闭后右键拖拽无效 |
rotateSpeed | 旋转速度(0.1~2),建议 0.5~1,过快易眩晕 |
enableDamping | 阻尼效果(默认 false),开启后操作更流畅 |
dampingFactor | 阻尼系数(0.01~0.1),建议 0.05,平衡流畅度与响应速度 |
minPolarAngle /maxPolarAngle | 垂直旋转范围(0~π 弧度),限制为 0~π 可避免视角翻转 |
2. 动画循环的必要性
代码中startAnimation
函数通过requestAnimationFrame
实现递归调用,核心作用:
- 持续渲染场景:即使物体不自动旋转,OrbitControls 的阻尼效果也需要每帧更新状态;
- 处理动态效果:如立方体自转、相机位置变化等,确保动效流畅;
- 同步浏览器刷新:
requestAnimationFrame
会与浏览器刷新率同步(默认 60 帧 / 秒),避免卡顿。
3. 资源清理的重要性
在onUnmounted
中清理资源,避免内存泄漏:
cancelAnimationFrame(animationId)
:停止动画循环,避免组件卸载后仍占用 CPU;controls.dispose()
:释放控制器监听的鼠标事件,避免事件冲突;renderer.dispose()
:释放 WebGL 渲染器占用的 GPU 资源,尤其在多 3D 组件场景中关键。
五、常见问题与解决方案
1. 问题 1:OrbitControls 拖拽无反应?
- 原因 1:未调用
controls.update()
,阻尼效果启用后必须在动画循环中更新控制器; - 原因 2:渲染器 Canvas 元素未正确挂载到 DOM,导致控制器无法监听鼠标事件;
- 解决:确保
controls.update()
在animate
函数中调用,且renderer.domElement
已添加到 Vue 容器。
2. 问题 2:窗口缩放后立方体变形?
- 原因:未更新相机宽高比和渲染器尺寸;
- 解决:在
handleResize
中调用camera.updateProjectionMatrix()
(更新相机投影)和renderer.setSize()
(更新渲染器尺寸)。
3. 问题 3:立方体是黑色的?
- 原因:未添加光源,或光源强度不足;
- 解决:确保
initLight()
函数被调用,且至少添加AmbientLight
(环境光)和DirectionalLight
(平行光),可适当提高光源强度(如ambientLight
的强度设为 0.5)。
4. 问题 4:组件卸载后控制台报错?
- 原因:未清理动画循环或事件监听,导致组件卸载后仍执行渲染;
- 解决:在
onUnmounted
中完整清理animationId
、事件监听和 Three.js 资源。
六、扩展与进阶方向
掌握基础相机交互后,可尝试以下进阶功能:
- 限制相机距离:通过
controls.minDistance
和controls.maxDistance
限制相机与立方体的最小 / 最大距离,避免过度缩放; - 添加碰撞检测:结合
THREE.Raycaster
实现 “相机不穿透立方体”,增强真实感; - 加载外部模型:用
GLTFLoader
加载 Blender 等工具制作的 3D 模型(如汽车、人物),替代立方体; - 添加纹理贴图:用
THREE.TextureLoader
给立方体贴上图片(如木纹、金属纹理),更贴近真实场景; - 多相机切换:实现 “自由视角”“顶视视角”“侧视视角” 一键切换,满足复杂场景需求。
七、总结
本文通过 Vue3 + Three.js + OrbitControls 实现了 “相机交互观察 3D 立方体”,核心思路是:
- 搭建 Three.js 基础场景(场景、相机、渲染器);
- 用 OrbitControls 简化相机交互,配置旋转、缩放、平移等功能;
- 通过动画循环持续更新场景状态,确保交互流畅;
- 组件卸载时清理资源,避免内存泄漏。
OrbitControls 是 Three.js 官方推荐的相机控制器,相比手动写鼠标事件,它更稳定、功能更全面,是开发 Web 3D 交互场景的首选工具。建议多调整 OrbitControls 的参数(如旋转速度、阻尼系数),通过实践感受不同配置对用户体验的影响。