032:vue+threejs 实现物体点击后在地面上拖动平移,点击地面可旋转
作者: 还是大剑师兰特 ,曾为美国某知名大学计算机专业研究生,现为国内GIS领域高级前端工程师,CSDN知名博主,深耕openlayers、leaflet、mapbox、cesium,canvas,echarts等技术开发,欢迎加微信(gis-dajianshi),一起交流。
032个示例
文章目录
- 一、示例效果图
- 二、示例简介
- 三、配置说明
- 四、示例源代码(共 288 行)
- 五、相关文章参考
一、示例效果图

二、示例简介
本示例实现了一个基于 Vue 和 Three.js 的 实体点击拖动场景,对标购物平台移动物体的场景。
核心实现要点
- 层级结构设计
将所有可拖拽物体作为地面(ground)的子元素,利用 Three.js 的变换继承特性:当地面旋转时,子物体自动跟随旋转。 - 两种拖拽逻辑区分
拖动物体:通过射线检测到物体时,仅更新物体在地面局部坐标系中的位置(position),地面保持不动。
拖动地面:点击地面空白处时,旋转地面(rotation),由于物体是地面的子元素,会自动跟随地面变换方位。 - 坐标计算
物体拖拽时,基于地面平面(Y 轴向上)计算射线交点,确保物体始终在地面上移动。
地面旋转时,通过鼠标位移差控制地面的旋转角度(主要绕 Y 轴旋转,保持水平)。 - 交互体验优化
拖动物体时高亮显示,释放后恢复原色。
限制物体 Y 轴高度,避免拖拽时漂浮或下沉。
清晰的操作说明面板,提升用户体验。
三、配置说明
1)查看基础设置:https://dajianshi.blog.csdn.net/article/details/141936765
2)将示例源代码,粘贴到src/views/Home.vue中,npm run serve 运行即可。
四、示例源代码(共 288 行)
/*
* @Author: 大剑师兰特(xiaozhuanlan),还是大剑师兰特(CSDN)
* @此源代码版权归大剑师兰特所有,可供学习或商业项目中借鉴,未经授权,不得重复地发表到博客、论坛,问答,git等公共空间或网站中。
* @Email: 2909222303@qq.com
* @First published in xiaozhuanlan
* @Second published in CSDN
* @First published time: 2025-10-24
*/
<template><div><div id="vue-three" style="width: 100vw; height: 100vh"></div><div class="info-panel"><p>作者:还是大剑师兰特</p><p>操作说明:</p><p>• 点击物体并拖拽 → 在地面上移动物体</p><p>• 点击地面并拖拽 → 水平旋转场景</p><p>• 鼠标滚轮 → 缩放场景</p></div></div>
</template><script>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'export default {data() {return {// 核心对象scene: null,camera: null,renderer: null,controls: null,// 场景元素ground: null,draggableObjects: [],selectedObject: null,// 交互状态raycaster: new THREE.Raycaster(),mouse: new THREE.Vector2(),isDraggingObject: false,isRotatingGround: false,prevMousePos: new THREE.Vector2(),// 拖拽计算参数groundPlane: new THREE.Plane(),dragOffset: new THREE.Vector3(),intersectionPoint: new THREE.Vector3()}},mounted() {this.initScene()this.createGround()this.createDraggableObjects()this.addEventListeners()this.animate()},beforeDestroy() {// 清理资源if (this.renderer) this.renderer.dispose()window.removeEventListener('resize', this.handleResize)document.removeEventListener('mousedown', this.onMouseDown)document.removeEventListener('mousemove', this.onMouseMove)document.removeEventListener('mouseup', this.onMouseUp)document.removeEventListener('wheel', this.onWheel)},methods: {// 初始化场景initScene() {// 创建场景this.scene = new THREE.Scene()this.scene.background = new THREE.Color(0xf0f0f0)// 创建相机this.camera = new THREE.PerspectiveCamera(60,window.innerWidth / window.innerHeight,0.1,1000)this.camera.position.set(10, 8, 10)this.camera.lookAt(0, 0, 0)// 创建渲染器this.renderer = new THREE.WebGLRenderer({ antialias: true })this.renderer.setSize(window.innerWidth, window.innerHeight)document.getElementById('vue-three').appendChild(this.renderer.domElement)// 添加光源const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)this.scene.add(ambientLight)const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)directionalLight.position.set(10, 20, 15)this.scene.add(directionalLight)// 窗口大小监听window.addEventListener('resize', this.handleResize)},// 创建地面createGround() {const groundGeometry = new THREE.PlaneGeometry(20, 20)const groundMaterial = new THREE.MeshStandardMaterial({color: 0xe0e0e0,side: THREE.DoubleSide})this.ground = new THREE.Mesh(groundGeometry, groundMaterial)this.ground.rotation.x = -Math.PI / 2this.ground.name = 'ground'this.scene.add(this.ground)// 添加网格辅助线const gridHelper = new THREE.GridHelper(20, 20, 0xcccccc, 0xeeeeee)this.scene.add(gridHelper)},// 创建可拖拽物体createDraggableObjects() {// 立方体const cube = this.createObject(new THREE.BoxGeometry(1, 1, 1),0xff4444,-3, 0.5, 0)// 球体const sphere = this.createObject(new THREE.SphereGeometry(0.7, 32, 32),0x44ff44,0, 0.7, 0)// 圆柱体const cylinder = this.createObject(new THREE.CylinderGeometry(0.5, 0.5, 1.2, 32),0x4444ff,3, 0.6, 0)this.draggableObjects = [cube, sphere, cylinder]this.draggableObjects.forEach(obj => this.scene.add(obj))},// 创建物体工具函数createObject(geometry, color, x, y, z) {const material = new THREE.MeshStandardMaterial({color,metalness: 0.3,roughness: 0.7})const mesh = new THREE.Mesh(geometry, material)mesh.position.set(x, y, z)return mesh},// 添加事件监听addEventListeners() {document.addEventListener('mousedown', this.onMouseDown.bind(this))document.addEventListener('mousemove', this.onMouseMove.bind(this))document.addEventListener('mouseup', this.onMouseUp.bind(this))document.addEventListener('wheel', this.onWheel.bind(this))},// 鼠标按下事件onMouseDown(event) {event.preventDefault()this.updateMousePos(event)// 射线检测物体this.raycaster.setFromCamera(this.mouse, this.camera)const objectIntersects = this.raycaster.intersectObjects(this.draggableObjects)if (objectIntersects.length > 0) {// 选中物体,开始拖拽this.isDraggingObject = truethis.selectedObject = objectIntersects[0].object// 高亮选中物体this.selectedObject.material.color.set(0xffff00)// 初始化地面平面(Y轴向上)this.groundPlane.set(new THREE.Vector3(0, 1, 0), 0)// 计算拖拽偏移量this.raycaster.ray.intersectPlane(this.groundPlane, this.intersectionPoint)this.dragOffset.copy(this.selectedObject.position).sub(this.intersectionPoint)} else {// 检测是否点击地面const groundIntersects = this.raycaster.intersectObject(this.ground)if (groundIntersects.length > 0) {// 开始旋转地面this.isRotatingGround = truethis.prevMousePos.copy(this.mouse)}}},// 鼠标移动事件onMouseMove(event) {// event.preventDefault()this.updateMousePos(event)if (this.isDraggingObject && this.selectedObject) {// 拖动物体this.raycaster.setFromCamera(this.mouse, this.camera)if (this.raycaster.ray.intersectPlane(this.groundPlane, this.intersectionPoint)) {// 更新物体位置(保持Y轴高度)const newPos = this.intersectionPoint.clone().add(this.dragOffset)this.selectedObject.position.set(newPos.x, this.selectedObject.position.y, newPos.z)}} else if (this.isRotatingGround) {// 水平旋转场景(仅绕Y轴)const deltaX = this.mouse.x - this.prevMousePos.x// 绕场景中心Y轴旋转相机this.camera.position.applyAxisAngle(new THREE.Vector3(0, 1, 0), -deltaX * 1.5)this.camera.lookAt(0, 0, 0) // 始终看向场景中心this.prevMousePos.copy(this.mouse)}},// 鼠标释放事件onMouseUp() {// 恢复物体颜色if (this.selectedObject) {const originalColors = [0xff4444, 0x44ff44, 0x4444ff]const index = this.draggableObjects.indexOf(this.selectedObject)this.selectedObject.material.color.set(originalColors[index % originalColors.length])this.selectedObject = null}// 重置状态this.isDraggingObject = falsethis.isRotatingGround = false},// 鼠标滚轮缩放onWheel(event) {// event.preventDefault()// 缩放因子const zoomFactor = -event.deltaY * 0.001// 调整相机位置实现缩放效果this.camera.position.multiplyScalar(1 - zoomFactor)// 限制相机最小和最大距离const minDist = 3const maxDist = 20const dist = this.camera.position.length()if (dist < minDist) {this.camera.position.setLength(minDist)} else if (dist > maxDist) {this.camera.position.setLength(maxDist)}},// 更新鼠标标准化坐标updateMousePos(event) {this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1},// 窗口大小调整handleResize() {this.camera.aspect = window.innerWidth / window.innerHeightthis.camera.updateProjectionMatrix()this.renderer.setSize(window.innerWidth, window.innerHeight)},// 动画循环animate() {requestAnimationFrame(this.animate.bind(this))this.renderer.render(this.scene, this.camera)}}
}
</script><style scoped>
.info-panel {position: fixed;top: 20px;left: 20px;background: rgba(255, 255, 255, 0.9);padding: 15px;border-radius: 6px;box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);z-index: 100;font-family: sans-serif;
}.info-panel p {margin: 5px 0;font-size: 14px;color: #333;
}
</style>
五、相关文章参考
https://threejs.org/docs/index.html#api/zh/geometries/BoxGeometry
https://threejs.org/docs/index.html#api/zh/geometries/PlaneGeometry
