第三章 Vue3 + Three.js 实战:用 OrbitControls 实现相机交互与 3D 立方体展示

上一章中,介绍的是通过监听鼠标事件,控制3D立方体。本文结合官方控制器 OrbitControls,实现 "控制相机观察 3D 立方体" 的功能,涵盖场景搭建、光照配置、交互优化等核心知识点。

一、效果预览与核心功能

最终交互效果

  • 左键拖拽:旋转相机,从不同角度观察立方体
  • 鼠标滚轮:缩放相机,拉近 / 拉远与立方体的距离
  • 右键拖拽 / Shift + 左键:平移相机,改变观察位置
  • 阻尼惯性:操作结束后相机仍有轻微惯性,交互更流畅
  • 自适应窗口:窗口缩放时,3D 场景自动调整比例,无变形

技术栈选型

技术 / 工具 作用说明
Vue3(<script setup> 组件化开发,简化语法
Three.js 构建 3D 场景、物体与光照
OrbitControls Three.js 官方相机控制器,实现拖拽 / 缩放 / 平移

二、前置知识储备

在开始前,建议掌握以下基础:

  1. Vue3 核心语法:ref响应式、生命周期钩子(onMounted/onUnmounted
  2. Three.js 三要素:场景(Scene)、相机(Camera)、渲染器(Renderer)
  3. 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 资源。

六、扩展与进阶方向

掌握基础相机交互后,可尝试以下进阶功能:

  1. 限制相机距离 :通过controls.minDistancecontrols.maxDistance限制相机与立方体的最小 / 最大距离,避免过度缩放;
  2. 添加碰撞检测 :结合THREE.Raycaster实现 "相机不穿透立方体",增强真实感;
  3. 加载外部模型 :用GLTFLoader加载 Blender 等工具制作的 3D 模型(如汽车、人物),替代立方体;
  4. 添加纹理贴图 :用THREE.TextureLoader给立方体贴上图片(如木纹、金属纹理),更贴近真实场景;
  5. 多相机切换:实现 "自由视角""顶视视角""侧视视角" 一键切换,满足复杂场景需求。

七、总结

本文通过 Vue3 + Three.js + OrbitControls 实现了 "相机交互观察 3D 立方体",核心思路是:

  1. 搭建 Three.js 基础场景(场景、相机、渲染器);
  2. 用 OrbitControls 简化相机交互,配置旋转、缩放、平移等功能;
  3. 通过动画循环持续更新场景状态,确保交互流畅;
  4. 组件卸载时清理资源,避免内存泄漏。

OrbitControls 是 Three.js 官方推荐的相机控制器,相比手动写鼠标事件,它更稳定、功能更全面,是开发 Web 3D 交互场景的首选工具。建议多调整 OrbitControls 的参数(如旋转速度、阻尼系数),通过实践感受不同配置对用户体验的影响。