第二章 Vue + Three.js 实现鼠标拖拽旋转 3D 立方体交互实践

发布于:2025-08-30 ⋅ 阅读:(22) ⋅ 点赞:(0)

在 Web 3D 开发中,鼠标与 3D 物体的交互是最基础也最核心的需求之一。本文将基于 Vue 3 + Three.js,手把手教你实现 “鼠标拖拽控制 3D 立方体旋转” 的功能,从环境搭建到交互逻辑,每一步都附带完整代码和详细解析,即使是 Three.js 新手也能轻松上手。

一、效果预览与核心需求

最终交互效果

  • 鼠标未拖拽时:立方体静止,鼠标显示 “抓取” 图标(grab
  • 鼠标按住拖拽时:立方体跟随鼠标移动方向旋转,鼠标显示 “正在抓取” 图标(grabbing
  • 窗口大小变化时:3D 场景自动适配,保持显示比例不变

核心技术栈

技术 版本 / 作用
Vue 3 采用<script setup>语法糖
Three.js 构建 3D 场景与物体
原生鼠标事件 监听mousedown/mousemove

二、前置知识准备

在开始前,需确保掌握以下基础:

  1. Vue 3 基础语法(尤其是<script setup>ref响应式)
  2. Three.js 核心概念:场景(Scene)、相机(Camera)、渲染器(Renderer)、物体(Mesh)
  3. 原生 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)鼠标拖拽的核心逻辑

拖拽功能通过 “状态标记 + 坐标计算” 实现,关键步骤:

  1. mousedown:标记isDragging = true,记录初始鼠标位置。
  2. mousemove
    • 若未拖拽,直接返回;
    • 计算鼠标移动距离(deltaX/deltaY);
    • 根据移动距离旋转立方体(cube.rotation.y/x);
    • 重新渲染场景。
  3. 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));

五、扩展与进阶方向

掌握基础拖拽旋转后,可尝试以下进阶功能:

  1. 多物体交互:通过射线检测(THREE.Raycaster)实现 “点击选中物体后拖拽”。
  2. 添加惯性旋转:拖拽结束后,物体继续旋转一段时间(通过requestAnimationFrame实现)。
  3. 纹理贴图:用THREE.TextureLoader给立方体贴上图文(如木纹、图片)。
  4. 组合控制器:集成 Three.js 官方控制器OrbitControls,支持缩放、平移等更多交互。

六、总结

本文通过 Vue 3 + Three.js 实现了 “鼠标拖拽旋转 3D 立方体”,核心是:

  1. 搭建 Three.js 基础场景(场景、相机、渲染器);
  2. 通过鼠标事件监听控制拖拽状态;
  3. 计算鼠标移动距离,映射为物体旋转角度;
  4. 组件卸载时清理资源,避免内存泄漏。

Three.js 的交互逻辑本质是 “事件监听 + 状态更新 + 重新渲染”,掌握这个核心思路后,无论是拖拽、点击还是缩放,都能举一反三。建议多尝试修改参数(如旋转速度、物体颜色、相机位置),通过实践加深理解。


网站公告

今日签到

点亮在社区的每一天
去签到