第二章 Vue + Three.js 实现鼠标拖拽旋转 3D 立方体交互实践
在 Web 3D 开发中,鼠标与 3D 物体的交互是最基础也最核心的需求之一。本文将基于 Vue 3 + Three.js,手把手教你实现 “鼠标拖拽控制 3D 立方体旋转” 的功能,从环境搭建到交互逻辑,每一步都附带完整代码和详细解析,即使是 Three.js 新手也能轻松上手。
一、效果预览与核心需求
最终交互效果
- 鼠标未拖拽时:立方体静止,鼠标显示 “抓取” 图标(
grab
) - 鼠标按住拖拽时:立方体跟随鼠标移动方向旋转,鼠标显示 “正在抓取” 图标(
grabbing
) - 窗口大小变化时:3D 场景自动适配,保持显示比例不变
核心技术栈
技术 | 版本 / 作用 |
---|---|
Vue 3 | 采用<script setup> 语法糖 |
Three.js | 构建 3D 场景与物体 |
原生鼠标事件 | 监听mousedown/mousemove 等 |
二、前置知识准备
在开始前,需确保掌握以下基础:
- Vue 3 基础语法(尤其是
<script setup>
和ref
响应式) - Three.js 核心概念:场景(Scene)、相机(Camera)、渲染器(Renderer)、物体(Mesh)
- 原生 JS 鼠标事件机制(事件监听、坐标计算)
若对 Three.js 核心概念不熟悉,可先了解 “Three.js 三要素”:场景是容器,相机是视角,渲染器是画布,三者共同构成 3D 显示的基础。
三、完整实现步骤
1. 项目初始化与依赖安装
首先确保 Vue 项目已创建(若未创建,执行npm create vue@latest
初始化),然后安装 Three.js:
npm install three
Three.js 无需额外配置,安装后即可在组件中直接导入使用。
2. 组件完整代码
创建DragRotateCube.vue
组件,代码如下(已添加详细注释):
<template><!-- 3D场景容器:通过ref获取DOM元素 --><div class="three-container" ref="container"></div>
</template><script setup>
// 1. 导入Vue和Three.js核心模块
import { onMounted, ref, onUnmounted } from 'vue';
import * as THREE from 'three';// 2. 响应式引用:获取3D容器DOM
const container = ref(null);// 3. 声明Three.js核心对象(全局作用域,避免函数内重复创建)
let scene, camera, renderer, cube;// 4. 鼠标状态管理:控制拖拽逻辑
let isDragging = false; // 是否处于拖拽中
let previousMousePosition = { x: 0, y: 0 }; // 上一帧鼠标位置// 5. 组件挂载时初始化3D场景
onMounted(() => {initScene(); // 初始化场景initCamera(); // 初始化相机initRenderer(); // 初始化渲染器initObject(); // 初始化3D物体(立方体)initLight(); // 初始化光照(否则物体是黑色)initEventListeners(); // 初始化鼠标事件监听render(); // 首次渲染场景
});// 6. 组件卸载时清理资源(防止内存泄漏)
onUnmounted(() => {const canvas = renderer.domElement;// 移除鼠标事件监听canvas.removeEventListener('mousedown', handleMouseDown);canvas.removeEventListener('mousemove', handleMouseMove);canvas.removeEventListener('mouseup', handleMouseUp);canvas.removeEventListener('mouseleave', handleMouseUp);// 移除窗口 resize 监听window.removeEventListener('resize', handleResize);// 释放渲染器资源renderer.dispose();
});/*** 7. 初始化场景:3D物体的“容器”*/
const initScene = () => {scene = new THREE.Scene();// 设置场景背景色(浅灰色,十六进制)scene.background = new THREE.Color(0xf0f0f0);
};/*** 8. 初始化相机:控制“视角”,决定能看到什么*/
const initCamera = () => {// 获取容器宽高(确保相机比例与容器一致)const { clientWidth, clientHeight } = container.value;// 透视相机(模拟人眼视角,近大远小)camera = new THREE.PerspectiveCamera(75, // 视野角度(FOV):单位度,值越小视角越窄clientWidth / clientHeight, // 宽高比:必须与容器一致,否则物体变形0.1, // 近裁剪面:距离相机小于此值的物体不渲染1000 // 远裁剪面:距离相机大于此值的物体不渲染);// 设置相机位置(Z轴正向远离物体,避免“穿模”)camera.position.z = 8;
};/*** 9. 初始化渲染器:将3D场景“画”到浏览器画布上*/
const initRenderer = () => {const { clientWidth, clientHeight } = container.value;// 创建WebGL渲染器,开启抗锯齿(让物体边缘更平滑)renderer = new THREE.WebGLRenderer({ antialias: true });// 设置渲染器尺寸(与容器一致)renderer.setSize(clientWidth, clientHeight);// 将渲染器生成的Canvas元素添加到容器中container.value.appendChild(renderer.domElement);
};/*** 10. 初始化3D物体:创建可旋转的立方体*/
const initObject = () => {// ① 几何体:定义物体的“形状”(长方体,参数:宽、高、深)const geometry = new THREE.BoxGeometry(3, 3, 3);// ② 材质:定义物体的“外观”(Phong材质,支持高光效果)const material = new THREE.MeshPhongMaterial({color: 0x42b983, // 物体颜色(Vue绿,十六进制)shininess: 100, // 高光强度:值越大高光越明显wireframe: false // 是否显示线框(false为实心)});// ③ 网格:结合几何体和材质,生成可渲染的3D物体cube = new THREE.Mesh(geometry, material);// ④ 将物体添加到场景中(否则不显示)scene.add(cube);
};/*** 11. 初始化光照:Three.js中物体默认不发光,需手动添加光源*/
const initLight = () => {// ① 环境光:均匀照亮所有物体,避免局部过暗(柔和补光)const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);scene.add(ambientLight);// ② 平行光:模拟太阳光,有方向,产生明暗对比(增强立体感)const directionalLight = new THREE.DirectionalLight(0xffffff, 1);directionalLight.position.set(5, 5, 5); // 光源位置(斜上方)scene.add(directionalLight);
};/*** 12. 初始化鼠标事件监听:实现拖拽逻辑的核心*/
const initEventListeners = () => {const canvas = renderer.domElement;// 鼠标按下:开始拖拽canvas.addEventListener('mousedown', handleMouseDown);// 鼠标移动:更新物体旋转canvas.addEventListener('mousemove', handleMouseMove);// 鼠标释放:结束拖拽canvas.addEventListener('mouseup', handleMouseUp);// 鼠标离开画布:强制结束拖拽canvas.addEventListener('mouseleave', handleMouseUp);// 窗口大小变化:适配场景window.addEventListener('resize', handleResize);
};/*** 13. 鼠标按下事件:标记拖拽状态,记录初始位置*/
const handleMouseDown = (event) => {isDragging = true;// 记录鼠标按下时的屏幕坐标previousMousePosition = {x: event.clientX,y: event.clientY};
};/*** 14. 鼠标移动事件:计算移动距离,控制物体旋转*/
const handleMouseMove = (event) => {// 仅在拖拽状态下处理(避免无意义计算)if (!isDragging) return;// ① 计算鼠标移动的距离(当前位置 - 上一帧位置)const deltaX = event.clientX - previousMousePosition.x; // 水平移动距离const deltaY = event.clientY - previousMousePosition.y; // 垂直移动距离// ② 根据移动距离旋转立方体(系数0.005控制旋转速度)cube.rotation.y += deltaX * 0.005; // 水平移动 → 绕Y轴旋转(左右转)cube.rotation.x += deltaY * 0.005; // 垂直移动 → 绕X轴旋转(上下转)// ③ 更新上一帧鼠标位置(为下一帧计算做准备)previousMousePosition = {x: event.clientX,y: event.clientY};// ④ 重新渲染场景(否则物体不更新)render();
};/*** 15. 鼠标释放事件:结束拖拽状态*/
const handleMouseUp = () => {isDragging = false;
};/*** 16. 窗口大小变化:适配场景尺寸,避免变形*/
const handleResize = () => {const { clientWidth, clientHeight } = container.value;// ① 更新相机宽高比camera.aspect = clientWidth / clientHeight;camera.updateProjectionMatrix(); // 必须更新相机投影矩阵,否则不生效// ② 更新渲染器尺寸renderer.setSize(clientWidth, clientHeight);// ③ 重新渲染render();
};/*** 17. 渲染函数:将场景和相机的内容画到画布上*/
const render = () => {renderer.render(scene, camera);
};
</script><style scoped>
/* 18. 容器样式:全屏显示,隐藏滚动条 */
.three-container {width: 100vw; /* 占满屏幕宽度 */height: 100vh; /* 占满屏幕高度 */overflow: hidden; /* 隐藏溢出内容 */cursor: grab; /* 鼠标默认显示“抓取”图标 */
}/* 鼠标按下时显示“正在抓取”图标 */
.three-container:active {cursor: grabbing;
}
</style>
3. 核心逻辑解析
(1)Three.js 三要素的协作
- 场景(Scene):作为 “容器”,承载相机、立方体、光源等所有 3D 元素。
- 相机(PerspectiveCamera):设置在
(0,0,8)
位置,从 Z 轴正向 “看向” 立方体(默认看向原点(0,0,0)
)。 - 渲染器(WebGLRenderer):生成 Canvas 元素并插入到 Vue 容器中,将场景内容渲染到 Canvas 上。
(2)鼠标拖拽的核心逻辑
拖拽功能通过 “状态标记 + 坐标计算” 实现,关键步骤:
mousedown
:标记isDragging = true
,记录初始鼠标位置。mousemove
:- 若未拖拽,直接返回;
- 计算鼠标移动距离(
deltaX
/deltaY
); - 根据移动距离旋转立方体(
cube.rotation.y
/x
); - 重新渲染场景。
mouseup
/mouseleave
:标记isDragging = false
,结束拖拽。
(3)旋转速度控制
代码中deltaX * 0.005
的系数0.005
是旋转速度的关键:
- 系数越大,鼠标移动相同距离时物体旋转越快;
- 系数越小,旋转越平缓;
- 可根据需求调整(如改为
0.003
减慢速度,0.008
加快速度)。
四、常见问题与优化方案
1. 问题 1:立方体是黑色的?
- 原因:未添加光源,Three.js 中
MeshPhongMaterial
等材质需要光照才能显示颜色。 - 解决:确保
initLight()
函数被调用,且添加了AmbientLight
(环境光)和DirectionalLight
(平行光)。
2. 问题 2:窗口缩放后物体变形?
- 原因:相机宽高比未更新,导致渲染比例与容器比例不一致。
- 解决:
handleResize
函数中必须调用camera.updateProjectionMatrix()
,更新相机投影矩阵。
3. 问题 3:组件卸载后内存泄漏?
- 原因:未移除事件监听或释放渲染器资源。
- 解决:在
onUnmounted
中移除所有事件监听,并调用renderer.dispose()
释放 WebGL 资源。
4. 优化方案:添加旋转边界限制
若不想让立方体无限制旋转(如绕 X 轴旋转不超过 90 度),可在handleMouseMove
中添加边界判断:
// 限制绕X轴旋转在 [-π/2, π/2] 范围内(避免立方体翻转)
cube.rotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, cube.rotation.x + deltaY * 0.005));
五、扩展与进阶方向
掌握基础拖拽旋转后,可尝试以下进阶功能:
- 多物体交互:通过射线检测(
THREE.Raycaster
)实现 “点击选中物体后拖拽”。 - 添加惯性旋转:拖拽结束后,物体继续旋转一段时间(通过
requestAnimationFrame
实现)。 - 纹理贴图:用
THREE.TextureLoader
给立方体贴上图文(如木纹、图片)。 - 组合控制器:集成 Three.js 官方控制器
OrbitControls
,支持缩放、平移等更多交互。
六、总结
本文通过 Vue 3 + Three.js 实现了 “鼠标拖拽旋转 3D 立方体”,核心是:
- 搭建 Three.js 基础场景(场景、相机、渲染器);
- 通过鼠标事件监听控制拖拽状态;
- 计算鼠标移动距离,映射为物体旋转角度;
- 组件卸载时清理资源,避免内存泄漏。
Three.js 的交互逻辑本质是 “事件监听 + 状态更新 + 重新渲染”,掌握这个核心思路后,无论是拖拽、点击还是缩放,都能举一反三。建议多尝试修改参数(如旋转速度、物体颜色、相机位置),通过实践加深理解。