当前位置: 首页 > news >正文

第八篇:交互入门:鼠标拾取物体

第八篇:交互入门:鼠标拾取物体

引言

交互是3D应用的核心灵魂,它让用户从旁观者变为参与者。Three.js提供了强大的射线检测(Raycaster)功能,可实现物体拾取、拖拽等交互效果。本文将深入解析交互技术原理,并通过Vue3实现一个交互式3D展厅,让你掌握用户与3D世界沟通的桥梁技术。


在这里插入图片描述

1. 射线检测(Raycaster)原理
1.1 射线检测流程
鼠标点击屏幕
标准化设备坐标
相机发射射线
检测与物体交点
返回最近交点
1.2 核心代码实现
<script setup>
import { ref, onMounted } from 'vue';
import * as THREE from 'three';const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const intersectedObjects = ref([]);// 初始化事件监听
onMounted(() => {const canvas = renderer.domElement;canvas.addEventListener('mousemove', onMouseMove);canvas.addEventListener('click', onClick);
});// 更新鼠标位置
function onMouseMove(event) {// 将鼠标位置归一化为设备坐标(-1到+1)mouse.x = (event.clientX / window.innerWidth) * 2 - 1;mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;// 更新射线raycaster.setFromCamera(mouse, camera);// 检测相交物体const intersects = raycaster.intersectObjects(scene.children);// 更新响应式数据intersectedObjects.value = intersects.map(i => i.object);
}
</script>
1.3 性能优化策略
// 只检测特定物体
const interactiveObjects = [obj1, obj2, obj3];
const intersects = raycaster.intersectObjects(interactiveObjects);// 节流检测频率
let lastCheck = 0;
function onMouseMove(event) {const now = Date.now();if (now - lastCheck < 50) return; // 20FPS检测lastCheck = now;// 执行检测...
}

2. 基础交互实现
2.1 悬停高亮效果
<script setup>
// 当前悬停的物体
const hoverObject = ref(null);// 高亮材质
const highlightMaterial = new THREE.MeshBasicMaterial({color: 0xffff00,wireframe: true
});watch(intersectedObjects, (intersects) => {const newHover = intersects.length > 0 ? intersects[0] : null;// 移除旧高亮if (hoverObject.value) {hoverObject.value.material = hoverObject.value.userData.originalMaterial;}// 应用新高亮if (newHover) {newHover.userData.originalMaterial = newHover.material;newHover.material = highlightMaterial;hoverObject.value = newHover;} else {hoverObject.value = null;}
});
</script>
2.2 点击选择物体
<template><div v-if="selectedObject" class="info-panel"><h3>{{ selectedObject.name }}</h3><p>位置: {{ selectedObject.position.toArray() }}</p></div>
</template><script setup>
const selectedObject = ref(null);function onClick() {if (intersectedObjects.value.length > 0) {selectedObject.value = intersectedObjects.value[0];} else {selectedObject.value = null;}
}
</script>
2.3 拖拽物体
let dragObject = null;
let dragOffset = new THREE.Vector3();function onMouseDown(event) {raycaster.setFromCamera(mouse, camera);const intersects = raycaster.intersectObjects(scene.children);if (intersects.length > 0) {dragObject = intersects[0].object;// 计算物体中心到交点的偏移dragOffset.copy(intersects[0].point).sub(dragObject.position);// 添加移动和释放事件canvas.addEventListener('mousemove', onDragMove);canvas.addEventListener('mouseup', onDragEnd);}
}function onDragMove(event) {if (!dragObject) return;// 更新射线raycaster.setFromCamera(mouse, camera);// 创建拖拽平面(与相机视线垂直)const dragPlane = new THREE.Plane();dragPlane.setFromNormalAndCoplanarPoint(camera.getWorldDirection(new THREE.Vector3()),dragObject.position);// 计算交点const intersectPoint = new THREE.Vector3();raycaster.ray.intersectPlane(dragPlane, intersectPoint);// 应用位置(考虑偏移)dragObject.position.copy(intersectPoint.sub(dragOffset));
}function onDragEnd() {dragObject = null;canvas.removeEventListener('mousemove', onDragMove);canvas.removeEventListener('mouseup', onDragEnd);
}

3. 高级交互技术
3.1 变换控制器(TransformControls)
<script setup>
import { TransformControls } from 'three/addons/controls/TransformControls.js';const transformControls = ref(null);onMounted(() => {// 创建变换控制器transformControls.value = new TransformControls(camera, renderer.domElement);// 监听变换事件transformControls.value.addEventListener('dragging-changed', (event) => {orbitControls.enabled = !event.value;});scene.add(transformControls.value);
});// 绑定到选中物体
watch(selectedObject, (obj) => {if (obj) {transformControls.value.attach(obj);} else {transformControls.value.detach();}
});
</script>
3.2 碰撞检测
// 使用Cannon.js进行物理碰撞检测
const physicsWorld = new CANNON.World();// 创建物理体
const physicsBody = new CANNON.Body({mass: 0, // 静态物体shape: new CANNON.Box(new CANNON.Vec3(1, 1, 1))
});// 在拖拽中检测碰撞
function onDragMove() {// 更新物理体位置physicsBody.position.copy(dragObject.position);// 检测碰撞physicsWorld.step(1/60);const collisions = physicsWorld.contacts;if (collisions.length > 0) {// 处理碰撞反馈(如震动、变色)gsap.to(dragObject.material.color, {r: 1, g: 0, b: 0,duration: 0.2,yoyo: true,repeat: 1});}
}
3.3 多物体选择
const selectedObjects = ref([]);function onClick(event) {raycaster.setFromCamera(mouse, camera);const intersects = raycaster.intersectObjects(scene.children);if (intersects.length > 0) {const object = intersects[0].object;// Ctrl多选if (event.ctrlKey) {const index = selectedObjects.value.indexOf(object);if (index === -1) {selectedObjects.value.push(object);} else {selectedObjects.value.splice(index, 1);}} else {selectedObjects.value = [object];}} else {selectedObjects.value = [];}
}

4. Vue3实战:交互式3D展厅
4.1 项目结构
src/├── components/│    ├── ExhibitionViewer.vue   // 3D展厅主组件│    ├── ExhibitInfo.vue        // 展品信息面板│    ├── Toolbar.vue            // 操作工具栏│    └── ExhibitThumbnails.vue  // 展品缩略图列表└── App.vue
4.2 展厅主组件
<!-- ExhibitionViewer.vue -->
<template><div class="exhibition-viewer"><canvas ref="canvasRef"></canvas><ExhibitInfo :exhibit="selectedExhibit" /><Toolbar @mode-change="setMode" /><ExhibitThumbnails :exhibits="exhibits" @select="selectExhibit" /></div>
</template><script setup>
import { ref, reactive } from 'vue';
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { TransformControls } from 'three/addons/controls/TransformControls.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';// 展品数据
const exhibits = reactive([{ id: 1, name: '雕塑', model: 'sculpture.gltf', position: [0, 0, 0] },{ id: 2, name: '花瓶', model: 'vase.gltf', position: [2, 0, -1] },// 更多展品...
]);const selectedExhibit = ref(null);
const interactionMode = ref('view'); // 'view' or 'edit'// 初始化展厅
const initExhibition = async () => {const loader = new GLTFLoader();// 加载所有展品for (const exhibit of exhibits) {const gltf = await loader.loadAsync(`models/${exhibit.model}`);const model = gltf.scene;model.position.set(...exhibit.position);model.userData = { exhibitId: exhibit.id };scene.add(model);}
};// 选择展品
const selectExhibit = (exhibit) => {// 通过射线检测或缩略图点击选择selectedExhibit.value = exhibit;// 定位相机到展品if (exhibit) {const model = scene.children.find(m => m.userData.exhibitId === exhibit.id);cameraControls.value.fitToObject(model, true);}
};// 设置交互模式
const setMode = (mode) => {interactionMode.value = mode;if (mode === 'edit') {transformControls.visible = true;} else {transformControls.visible = false;}
};// 保存展品位置
const saveExhibitPositions = () => {exhibits.forEach(exhibit => {const model = scene.children.find(m => m.userData.exhibitId === exhibit.id);if (model) {exhibit.position = [model.position.x, model.position.y, model.position.z];}});
};
</script>
4.3 展品信息面板
<!-- ExhibitInfo.vue -->
<template><div v-if="exhibit" class="info-panel"><h2>{{ exhibit.name }}</h2><p>{{ exhibit.description }}</p><button v-if="editMode" @click="removeExhibit">移除</button></div>
</template><script setup>
defineProps(['exhibit']);
const emit = defineEmits(['remove']);const removeExhibit = () => {emit('remove', exhibit.id);
};
</script>
4.4 工具栏组件
<!-- Toolbar.vue -->
<template><div class="toolbar"><button :class="{ active: mode === 'view' }" @click="setMode('view')">查看</button><button :class="{ active: mode === 'edit' }" @click="setMode('edit')">编辑</button><button @click="addExhibit">添加展品</button><button @click="saveLayout">保存布局</button></div>
</template><script setup>
const emit = defineEmits(['mode-change', 'add-exhibit', 'save-layout']);const mode = ref('view');const setMode = (newMode) => {mode.value = newMode;emit('mode-change', newMode);
};const addExhibit = () => {emit('add-exhibit');
};const saveLayout = () => {emit('save-layout');
};
</script>
4.5 展品缩略图列表
<!-- ExhibitThumbnails.vue -->
<template><div class="thumbnails"><div v-for="exhibit in exhibits" :key="exhibit.id"class="thumbnail":class="{ active: exhibit === selected }"@click="select(exhibit)"><img :src="exhibit.thumbnail" :alt="exhibit.name"><span>{{ exhibit.name }}</span></div></div>
</template><script setup>
defineProps({exhibits: Array,selected: Object
});const emit = defineEmits(['select']);const select = (exhibit) => {emit('select', exhibit);
};
</script>

5. 触摸屏适配
5.1 触摸事件处理
// 添加触摸事件
canvas.addEventListener('touchstart', onTouchStart);
canvas.addEventListener('touchmove', onTouchMove);
canvas.addEventListener('touchend', onTouchEnd);function onTouchStart(event) {event.preventDefault();// 获取第一个触摸点const touch = event.touches[0];// 模拟鼠标事件const mouseEvent = new MouseEvent('mousedown', {clientX: touch.clientX,clientY: touch.clientY});onMouseDown(mouseEvent);
}function onTouchMove(event) {event.preventDefault();const touch = event.touches[0];const mouseEvent = new MouseEvent('mousemove', {clientX: touch.clientX,clientY: touch.clientY});onMouseMove(mouseEvent);
}function onTouchEnd(event) {event.preventDefault();const mouseEvent = new MouseEvent('mouseup');onMouseUp(mouseEvent);
}
5.2 手势识别
// 双指缩放
let initialDistance = 0;function handlePinch(event) {if (event.touches.length === 2) {const dx = event.touches[0].clientX - event.touches[1].clientX;const dy = event.touches[0].clientY - event.touches[1].clientY;const distance = Math.sqrt(dx * dx + dy * dy);if (initialDistance === 0) {initialDistance = distance;} else {const zoomFactor = distance / initialDistance;camera.zoom = Math.max(0.1, Math.min(5, initialZoom * zoomFactor));camera.updateProjectionMatrix();}} else {initialDistance = 0;}
}

6. 性能优化
6.1 交互物体分组
// 创建交互组
const interactiveGroup = new THREE.Group();
scene.add(interactiveGroup);// 添加可交互物体
exhibits.forEach(exhibit => {exhibit.model.userData.interactive = true;interactiveGroup.add(exhibit.model);
});// 检测时只检查该组
raycaster.intersectObjects(interactiveGroup.children);
6.2 空间分割优化
// 使用八叉树加速检测
import { Octree } from 'three/addons/math/Octree.js';const octree = new Octree();
octree.fromGraphNode(scene);function raycast() {// 使用八叉树检测const intersects = octree.raycast(raycaster);// ...
}
6.3 GPU拾取技术
// 创建离屏渲染目标
const pickingTexture = new THREE.WebGLRenderTarget(1, 1);// 给每个物体分配唯一ID
let objectId = 1;
scene.traverse(obj => {if (obj.isMesh) {obj.userData.id = objectId++;}
});// 渲染ID到纹理
function renderPicking() {const material = new THREE.MeshBasicMaterial({color: new THREE.Color().setHex(objectId)});renderer.setRenderTarget(pickingTexture);scene.overrideMaterial = material;renderer.render(scene, camera);scene.overrideMaterial = null;renderer.setRenderTarget(null);
}// 读取像素获取ID
function getObjectId(x, y) {const pixelBuffer = new Uint8Array(4);renderer.readRenderTargetPixels(pickingTexture,x, y, 1, 1,pixelBuffer);// 将RGB转换为IDreturn (pixelBuffer[0] << 16) | (pixelBuffer[1] << 8) | pixelBuffer[2];
}

7. 常见问题解答

Q1:射线检测不到物体怎么办?

  1. 确认物体在相机视锥内
  2. 检查物体是否被其他物体遮挡
  3. 确认物体已添加到检测数组中
  4. 增加raycaster的far参数

Q2:拖拽时物体跳动?

  • 使用offset补偿交点与物体中心的偏移
  • 确保在同一个平面上移动
  • 使用物理引擎稳定位置

Q3:移动端如何优化交互?

  1. 增加触摸区域
  2. 使用防抖减少事件频率
  3. 提供视觉反馈(如按钮高亮)
  4. 简化复杂交互

8. 总结

通过本文,你已掌握:

  1. 射线检测原理与实现
  2. 基础交互:悬停、点击、拖拽
  3. 高级交互:变换控制、碰撞检测
  4. Vue3集成3D交互的完整流程
  5. 触摸屏适配与手势识别
  6. 交互性能优化技术
  7. 交互式3D展厅的实现

核心原理:Three.js的交互系统基于射线检测技术,通过从相机发射射线并计算与物体的交点,实现精确的3D拾取操作。


下一篇预告

第九篇:调试工具:Three.js Inspector使用
你将学习:

  • 浏览器控制台调试技巧
  • Three.js Inspector安装与使用
  • 场景结构可视化分析
  • 性能指标监控
  • 实时属性调整
  • Vue3集成调试工具

准备好成为Three.js调试大师了吗?让我们揭开场景优化的秘密!

http://www.dtcms.com/a/325456.html

相关文章:

  • TRS(总收益互换)系统架构设计:多市场交易的技术实现分析
  • 网络编程~
  • 套接字技术、视频加载技术、断点续传技术
  • 前端执行上下文(简版)
  • 2025.8.6 图论(1)Solution
  • 大模型提示词工程实践:聊天机器人定制与实践-打造个性化任务助手
  • Oracle 19C 配置TAF
  • 投资光伏怕成本超标?鹧鸪云系统配置最优方案
  • 微算法科技(NASDAQ:MLGO)通过蚁群算法求解资源分配的全局最优解,实现低能耗的区块链资源分配
  • 【深度学习计算性能】03:自动并行
  • Apache Ignite 生产级的线程池关闭工具方法揭秘
  • 【C++】封装哈希表模拟实现unordered_set和unordered_map
  • 【10】微网优联——微网优联 嵌入式技术一面,校招,面试问答记录
  • 【Linux让旧电脑重获新生的奇妙魔法】
  • 【k8s】k8s安装与集群部署脚本
  • Godot ------ 平滑拖动03
  • 量子神经网络:从NISQ困境到逻辑比特革命的破局之路
  • Day 37:早停策略和模型权重的保存
  • C语言指针(四):字符指针、数组指针与函数指针的实战指南
  • Unity大型场景性能优化全攻略:PC与安卓端深度实践 - 场景管理、渲染优化、资源调度 C#
  • 在Mac 上生成GitLab 的SSH 密钥并将其添加到GitLab
  • 在 .NET Core 5.0 中启用 Gzip 压缩
  • AI时代基于云原生的 CI/CD 基础设施 Tekton
  • Redis Sentinel 中 `sentinel resolve-hostnames yes` 的必要性解析
  • C#图形库SciChart与ScottPlot及LiveCharts2对比
  • Linux客户端利用MinIO对服务器数据进行同步
  • 掌握while循环:C语言编程基础
  • Unity跨平台性能优化全攻略:PC与安卓端深度优化指南 - CPU、GPU、内存优化 实战案例C#
  • 《番外:Veda的备份,在某个未联网的旧服务器中苏醒……》
  • 扩展运算符...作用介绍