Three.js 顶点射线碰撞检测实现步骤详解
一、基本思路
核心算法流程:
第1步:遍历几何体所有顶点,分别创建与几何体中心坐标构成的射线
对于 每个几何体A 的 每个顶点V: 顶点位置 = V 的世界坐标位置 中心位置 = 几何体A 的世界坐标中心点 方向向量 = 中心位置 - 顶点位置 创建射线Raycaster: 起点 = 顶点位置 方向 = 方向向量(归一化)实现细节:
- 获取几何体的顶点坐标数组(Float32Array,每3个值表示一个顶点的x,y,z坐标)
- 将顶点从局部坐标系转换到世界坐标系
- 计算几何体的世界中心点(通常是物体位置)
- 对每个顶点创建从顶点指向中心的射线
第2步:射线交叉计算
对于 几何体A 的 每条射线R: 使用Raycaster的intersectObject()方法: 检测射线R 是否与 几何体B 相交 如果相交: 获取交点信息(交点坐标、距离、面信息等)关键点:
- Raycaster.intersectObject(几何体B) 返回交点数组
- 交点数组按距离从近到远排序
- 如果射线与几何体B有交点,说明这条射线可能穿过了几何体B
第3步:通过距离判断两个网格模型是否碰撞
对于 几何体A 的 每条射线R 的 每个交点I: 计算:起点到交点的距离 = 距离1 计算:起点到几何体A中心的距离 = 距离2 如果 距离1 < 距离2: 说明射线在到达自己中心之前就击中了对方 判断为"这条射线穿过了对方几何体" 如果 至少有一条射线穿过对方几何体: 判断为"两个几何体发生碰撞"逻辑解释:
- 射线是从自己顶点指向自己中心的
- 如果射线在到达自己中心之前就击中了对方,说明对方在自己"内部"或"前方"
- 如果射线先到达自己中心,说明对方在自己"后方"或"外部"
二、具体实现步骤详解
Step 1: 获取几何体所有顶点(世界坐标)
// 1.1 获取几何体的顶点位置数据constgeometry=mesh.geometryconstpositionAttribute=geometry.attributes.positionconstpositions=positionAttribute.array// [x1, y1, z1, x2, y2, z2, ...]// 1.2 获取世界变换矩阵constmatrixWorld=mesh.matrixWorld// 1.3 遍历所有顶点,转换为世界坐标constworldVertices=[]for(leti=0;i<positions.length;i+=3){// 创建局部坐标顶点constlocalVertex=newTHREE.Vector3(positions[i],// xpositions[i+1],// ypositions[i+2]// z)// 转换为世界坐标constworldVertex=localVertex.clone()worldVertex.applyMatrix4(matrixWorld)worldVertices.push(worldVertex)}Step 2: 计算几何体中心点(世界坐标)
// 方法1:直接使用mesh的世界位置(对于对称几何体)constcenter=newTHREE.Vector3()mesh.getWorldPosition(center)// 方法2:计算所有顶点的平均值(更精确)letsum=newTHREE.Vector3(0,0,0)for(constvertexofworldVertices){sum.add(vertex)}constcenter=sum.divideScalar(worldVertices.length)Step 3: 创建顶点到中心的射线
// 3.1 创建Raycaster实例constraycaster=newTHREE.Raycaster()// 3.2 对于每个顶点,创建从顶点指向中心的射线for(constvertexofworldVertices){// 计算方向向量(从顶点指向中心)constdirection=center.clone().sub(vertex).normalize()// 设置射线raycaster.set(vertex,direction)// 现在raycaster就代表了一条从顶点指向中心的射线// 我们可以用它来检测与其他几何体的交点}Step 4: 射线交叉计算
// 4.1 检测射线是否与另一个几何体相交constotherMesh=...// 另一个几何体constintersects=raycaster.intersectObject(otherMesh)// 4.2 分析交点信息if(intersects.length>0){constfirstIntersect=intersects[0]// 最近的交点// 交点信息包含:// - point: 交点坐标(Vector3)// - distance: 从射线起点到交点的距离// - object: 被击中的物体// - face: 被击中的面// - faceIndex: 面的索引}Step 5: 通过距离判断是否碰撞
// 5.1 计算关键距离constdistanceToIntersect=vertex.distanceTo(firstIntersect.point)// 顶点到交点的距离constdistanceToCenter=vertex.distanceTo(center)// 顶点到中心的距离// 5.2 判断逻辑if(distanceToIntersect<distanceToCenter){// 情况:射线在到达自己中心之前就击中了对方// 说明对方几何体在自己"前方"或"内部"// 这很可能表示两个几何体相交或碰撞console.log('这条射线穿过了对方几何体!')// 记录碰撞信息collisions.push({vertex:vertex,intersectPoint:firstIntersect.point,rayDirection:direction})}else{// 情况:射线先到达自己中心,然后才可能击中对方// 说明对方几何体在自己"后方"或"外部"// 这很可能表示两个几何体没有碰撞}Step 6: 综合判断碰撞
// 6.1 统计所有穿过的射线letcollisionCount=0for(所有顶点射线){if(射线穿过了对方几何体){collisionCount++}}// 6.2 判断是否发生碰撞if(collisionCount>0){console.log(`发生碰撞!共有${collisionCount}条射线穿过对方几何体`)returntrue// 发生碰撞}else{console.log('没有碰撞')returnfalse// 没有碰撞}三、数学原理图解
情况1:发生碰撞(射线穿过对方) 顶点 │ │ 距离1(到交点) │ ▼ 交点(在对方几何体上) │ │ 距离2(从交点到中心) │ ▼ 中心 距离1 < 距离1+距离2(即距离1 < 顶点到中心的距离) ∴ 射线在到达中心之前击中了对方 → 碰撞! 情况2:没有碰撞(射线先到中心) 顶点 │ │ 距离1(到中心) │ ▼ 中心 │ │ 距离2(从中心到交点) │ ▼ 交点(在对方几何体上) 距离1 < 距离1+距离2(但射线要先经过中心) ∴ 射线先到达中心,然后才可能击中对方 → 没有碰撞!四、性能优化考虑
1. 顶点采样优化
// 不检测所有顶点,只采样一部分constsampleRate=0.3// 采样率30%for(leti=0;i<worldVertices.length;i+=Math.floor(1/sampleRate)){// 只检测部分顶点}2. 分层次检测
// 第一步:快速检测(包围盒)constbox1=newTHREE.Box3().setFromObject(mesh1)constbox2=newTHREE.Box3().setFromObject(mesh2)if(!box1.intersectsBox(box2)){returnfalse// 包围盒不相交,肯定不碰撞,快速返回}// 第二步:精确检测(顶点射线)// 只有包围盒相交时,才进行更耗时的顶点检测3. 空间分割优化
// 使用八叉树或BVH(包围盒层次结构)加速// 只检测可能相交的几何体4. 距离阈值优化
// 添加容差阈值constthreshold=0.01// 1厘米容差if(distanceToIntersect<distanceToCenter+threshold){// 考虑为碰撞(避免浮点数精度问题)}五、适用场景与限制
适用场景:
- 需要精确碰撞检测(如物理模拟、游戏碰撞)
- 不规则几何体碰撞(非AABB/球体等简单形状)
- 需要知道碰撞位置(不仅仅是是否碰撞)
限制:
- 计算量大:顶点越多越慢
- 凹形几何体问题:射线可能从凹处"穿过"而不碰撞
- 薄物体问题:对于非常薄的几何体可能漏检
六、伪代码总结
function 顶点射线碰撞检测(几何体A, 几何体B): // Step 1: 获取顶点 顶点数组A = 获取世界坐标顶点(几何体A) 顶点数组B = 获取世界坐标顶点(几何体B) // Step 2: 计算中心 中心A = 计算世界中心(几何体A) 中心B = 计算世界中心(几何体B) // Step 3-4: 检测A的顶点射线 for 每个顶点V in 顶点数组A: 方向 = 归一化(中心A - 顶点V) 射线 = 创建射线(顶点V, 方向) 交点 = 射线.检测相交(几何体B) if 交点存在: if 距离(顶点V, 交点) < 距离(顶点V, 中心A): 碰撞计数器++ // Step 3-4: 检测B的顶点射线 for 每个顶点V in 顶点数组B: 方向 = 归一化(中心B - 顶点V) 射线 = 创建射线(顶点V, 方向) 交点 = 射线.检测相交(几何体A) if 交点存在: if 距离(顶点V, 交点) < 距离(顶点V, 中心B): 碰撞计数器++ // Step 5: 判断结果 if 碰撞计数器 > 0: return true // 发生碰撞 else: return false // 没有碰撞—# Three.js 顶点射线碰撞检测实现步骤详解
一、基本思路
核心算法流程:
第1步:遍历几何体所有顶点,分别创建与几何体中心坐标构成的射线
对于 每个几何体A 的 每个顶点V: 顶点位置 = V 的世界坐标位置 中心位置 = 几何体A 的世界坐标中心点 方向向量 = 中心位置 - 顶点位置 创建射线Raycaster: 起点 = 顶点位置 方向 = 方向向量(归一化)实现细节:
- 获取几何体的顶点坐标数组(Float32Array,每3个值表示一个顶点的x,y,z坐标)
- 将顶点从局部坐标系转换到世界坐标系
- 计算几何体的世界中心点(通常是物体位置)
- 对每个顶点创建从顶点指向中心的射线
第2步:射线交叉计算
对于 几何体A 的 每条射线R: 使用Raycaster的intersectObject()方法: 检测射线R 是否与 几何体B 相交 如果相交: 获取交点信息(交点坐标、距离、面信息等)关键点:
- Raycaster.intersectObject(几何体B) 返回交点数组
- 交点数组按距离从近到远排序
- 如果射线与几何体B有交点,说明这条射线可能穿过了几何体B
第3步:通过距离判断两个网格模型是否碰撞
对于 几何体A 的 每条射线R 的 每个交点I: 计算:起点到交点的距离 = 距离1 计算:起点到几何体A中心的距离 = 距离2 如果 距离1 < 距离2: 说明射线在到达自己中心之前就击中了对方 判断为"这条射线穿过了对方几何体" 如果 至少有一条射线穿过对方几何体: 判断为"两个几何体发生碰撞"逻辑解释:
- 射线是从自己顶点指向自己中心的
- 如果射线在到达自己中心之前就击中了对方,说明对方在自己"内部"或"前方"
- 如果射线先到达自己中心,说明对方在自己"后方"或"外部"
二、具体实现步骤详解
Step 1: 获取几何体所有顶点(世界坐标)
// 1.1 获取几何体的顶点位置数据constgeometry=mesh.geometryconstpositionAttribute=geometry.attributes.positionconstpositions=positionAttribute.array// [x1, y1, z1, x2, y2, z2, ...]// 1.2 获取世界变换矩阵constmatrixWorld=mesh.matrixWorld// 1.3 遍历所有顶点,转换为世界坐标constworldVertices=[]for(leti=0;i<positions.length;i+=3){// 创建局部坐标顶点constlocalVertex=newTHREE.Vector3(positions[i],// xpositions[i+1],// ypositions[i+2]// z)// 转换为世界坐标constworldVertex=localVertex.clone()worldVertex.applyMatrix4(matrixWorld)worldVertices.push(worldVertex)}Step 2: 计算几何体中心点(世界坐标)
// 方法1:直接使用mesh的世界位置(对于对称几何体)constcenter=newTHREE.Vector3()mesh.getWorldPosition(center)// 方法2:计算所有顶点的平均值(更精确)letsum=newTHREE.Vector3(0,0,0)for(constvertexofworldVertices){sum.add(vertex)}constcenter=sum.divideScalar(worldVertices.length)Step 3: 创建顶点到中心的射线
// 3.1 创建Raycaster实例constraycaster=newTHREE.Raycaster()// 3.2 对于每个顶点,创建从顶点指向中心的射线for(constvertexofworldVertices){// 计算方向向量(从顶点指向中心)constdirection=center.clone().sub(vertex).normalize()// 设置射线raycaster.set(vertex,direction)// 现在raycaster就代表了一条从顶点指向中心的射线// 我们可以用它来检测与其他几何体的交点}Step 4: 射线交叉计算
// 4.1 检测射线是否与另一个几何体相交constotherMesh=...// 另一个几何体constintersects=raycaster.intersectObject(otherMesh)// 4.2 分析交点信息if(intersects.length>0){constfirstIntersect=intersects[0]// 最近的交点// 交点信息包含:// - point: 交点坐标(Vector3)// - distance: 从射线起点到交点的距离// - object: 被击中的物体// - face: 被击中的面// - faceIndex: 面的索引}Step 5: 通过距离判断是否碰撞
// 5.1 计算关键距离constdistanceToIntersect=vertex.distanceTo(firstIntersect.point)// 顶点到交点的距离constdistanceToCenter=vertex.distanceTo(center)// 顶点到中心的距离// 5.2 判断逻辑if(distanceToIntersect<distanceToCenter){// 情况:射线在到达自己中心之前就击中了对方// 说明对方几何体在自己"前方"或"内部"// 这很可能表示两个几何体相交或碰撞console.log('这条射线穿过了对方几何体!')// 记录碰撞信息collisions.push({vertex:vertex,intersectPoint:firstIntersect.point,rayDirection:direction})}else{// 情况:射线先到达自己中心,然后才可能击中对方// 说明对方几何体在自己"后方"或"外部"// 这很可能表示两个几何体没有碰撞}Step 6: 综合判断碰撞
// 6.1 统计所有穿过的射线letcollisionCount=0for(所有顶点射线){if(射线穿过了对方几何体){collisionCount++}}// 6.2 判断是否发生碰撞if(collisionCount>0){console.log(`发生碰撞!共有${collisionCount}条射线穿过对方几何体`)returntrue// 发生碰撞}else{console.log('没有碰撞')returnfalse// 没有碰撞}三、数学原理图解
情况1:发生碰撞(射线穿过对方) 顶点 │ │ 距离1(到交点) │ ▼ 交点(在对方几何体上) │ │ 距离2(从交点到中心) │ ▼ 中心 距离1 < 距离1+距离2(即距离1 < 顶点到中心的距离) ∴ 射线在到达中心之前击中了对方 → 碰撞! 情况2:没有碰撞(射线先到中心) 顶点 │ │ 距离1(到中心) │ ▼ 中心 │ │ 距离2(从中心到交点) │ ▼ 交点(在对方几何体上) 距离1 < 距离1+距离2(但射线要先经过中心) ∴ 射线先到达中心,然后才可能击中对方 → 没有碰撞!四、性能优化考虑
1. 顶点采样优化
// 不检测所有顶点,只采样一部分constsampleRate=0.3// 采样率30%for(leti=0;i<worldVertices.length;i+=Math.floor(1/sampleRate)){// 只检测部分顶点}2. 分层次检测
// 第一步:快速检测(包围盒)constbox1=newTHREE.Box3().setFromObject(mesh1)constbox2=newTHREE.Box3().setFromObject(mesh2)if(!box1.intersectsBox(box2)){returnfalse// 包围盒不相交,肯定不碰撞,快速返回}// 第二步:精确检测(顶点射线)// 只有包围盒相交时,才进行更耗时的顶点检测3. 空间分割优化
// 使用八叉树或BVH(包围盒层次结构)加速// 只检测可能相交的几何体4. 距离阈值优化
// 添加容差阈值constthreshold=0.01// 1厘米容差if(distanceToIntersect<distanceToCenter+threshold){// 考虑为碰撞(避免浮点数精度问题)}五、适用场景与限制
适用场景:
- 需要精确碰撞检测(如物理模拟、游戏碰撞)
- 不规则几何体碰撞(非AABB/球体等简单形状)
- 需要知道碰撞位置(不仅仅是是否碰撞)
限制:
- 计算量大:顶点越多越慢
- 凹形几何体问题:射线可能从凹处"穿过"而不碰撞
- 薄物体问题:对于非常薄的几何体可能漏检
六、伪代码总结
function 顶点射线碰撞检测(几何体A, 几何体B): // Step 1: 获取顶点 顶点数组A = 获取世界坐标顶点(几何体A) 顶点数组B = 获取世界坐标顶点(几何体B) // Step 2: 计算中心 中心A = 计算世界中心(几何体A) 中心B = 计算世界中心(几何体B) // Step 3-4: 检测A的顶点射线 for 每个顶点V in 顶点数组A: 方向 = 归一化(中心A - 顶点V) 射线 = 创建射线(顶点V, 方向) 交点 = 射线.检测相交(几何体B) if 交点存在: if 距离(顶点V, 交点) < 距离(顶点V, 中心A): 碰撞计数器++ // Step 3-4: 检测B的顶点射线 for 每个顶点V in 顶点数组B: 方向 = 归一化(中心B - 顶点V) 射线 = 创建射线(顶点V, 方向) 交点 = 射线.检测相交(几何体A) if 交点存在: if 距离(顶点V, 交点) < 距离(顶点V, 中心B): 碰撞计数器++ // Step 5: 判断结果 if 碰撞计数器 > 0: return true // 发生碰撞 else: return false // 没有碰撞这个算法的主要思想是:如果一个几何体的顶点发出的、指向自己中心的射线,在到达中心之前就击中了另一个几何体,那么这两个几何体很可能发生了碰撞或相交。
这个算法的主要思想是:如果一个几何体的顶点发出的、指向自己中心的射线,在到达中心之前就击中了另一个几何体,那么这两个几何体很可能发生了碰撞或相交。
<template><div ref="containerRef"></div></template><script setup>import{onMounted,ref}from'vue'import*asTHREEfrom'three'constcontainerRef=ref(null)letcube1,cube2onMounted(()=>{constscene=newTHREE.Scene()scene.background=newTHREE.Color(0x111111)constcamera=newTHREE.PerspectiveCamera(60,window.innerWidth/window.innerHeight,0.1,1000)camera.position.set(0,5,10)constrenderer=newTHREE.WebGLRenderer()renderer.setSize(window.innerWidth,window.innerHeight)containerRef.value.appendChild(renderer.domElement)// 创建立方体1(可移动)cube1=newTHREE.Mesh(newTHREE.BoxGeometry(1,1,1),newTHREE.MeshBasicMaterial({color:0xff0000,wireframe:true}))cube1.position.set(-3,0,0)scene.add(cube1)// 创建立方体2(静止)cube2=newTHREE.Mesh(newTHREE.BoxGeometry(1,1,1),newTHREE.MeshBasicMaterial({color:0x0000ff,wireframe:true}))cube2.position.set(0,0,0)scene.add(cube2)// 辅助线scene.add(newTHREE.AxesHelper(5))// 核心算法:顶点射线碰撞检测constcheckVertexRayCollision=()=>{console.log('=== 顶点射线碰撞检测 ===')// 1. 获取顶点(世界坐标)constgetVertices=(mesh)=>{constgeometry=mesh.geometryconstpositions=geometry.attributes.position.arrayconstvertices=[]constmatrixWorld=mesh.matrixWorldfor(leti=0;i<positions.length;i+=3){constvertex=newTHREE.Vector3(positions[i],positions[i+1],positions[i+2])vertex.applyMatrix4(matrixWorld)vertices.push(vertex)}returnvertices}constvertices1=getVertices(cube1)constvertices2=getVertices(cube2)// 2. 获取中心点(世界坐标)constcenter1=newTHREE.Vector3()cube1.getWorldPosition(center1)constcenter2=newTHREE.Vector3()cube2.getWorldPosition(center2)console.log('立方体1顶点数:',vertices1.length)console.log('立方体2顶点数:',vertices2.length)console.log('立方体1中心:',center1)console.log('立方体2中心:',center2)// 3. 检测立方体1的顶点射线letcollisionCount=0for(leti=0;i<vertices1.length;i++){constvertex=vertices1[i]constdirection=newTHREE.Vector3().subVectors(center1,vertex).normalize()constraycaster=newTHREE.Raycaster()raycaster.set(vertex,direction)constintersects=raycaster.intersectObject(cube2)if(intersects.length>0){constintersect=intersects[0]constdistanceToHit=vertex.distanceTo(intersect.point)constdistanceToCenter=vertex.distanceTo(center1)if(distanceToHit<distanceToCenter){collisionCount++console.log(`立方体1顶点${i}: 射线穿过了立方体2`)}}}// 4. 检测立方体2的顶点射线for(leti=0;i<vertices2.length;i++){constvertex=vertices2[i]constdirection=newTHREE.Vector3().subVectors(center2,vertex).normalize()constraycaster=newTHREE.Raycaster()raycaster.set(vertex,direction)constintersects=raycaster.intersectObject(cube1)if(intersects.length>0){constintersect=intersects[0]constdistanceToHit=vertex.distanceTo(intersect.point)constdistanceToCenter=vertex.distanceTo(center2)if(distanceToHit<distanceToCenter){collisionCount++console.log(`立方体2顶点${i}: 射线穿过了立方体1`)}}}// 5. 判断结果if(collisionCount>0){console.log(`💥 发生碰撞!共有${collisionCount}条射线相交`)cube1.material.color.set(0xff00ff)cube2.material.color.set(0xff00ff)returntrue}else{console.log('✅ 没有碰撞')cube1.material.color.set(0xff0000)cube2.material.color.set(0x0000ff)returnfalse}}// 自动移动并检测letdirection=1constanimate=()=>{requestAnimationFrame(animate)// 移动立方体1cube1.position.x+=0.01*direction// 边界检测if(cube1.position.x>2)direction=-1if(cube1.position.x<-4)direction=1// 每10帧检测一次if(Math.floor(cube1.position.x*10)%10===0){checkVertexRayCollision()}renderer.render(scene,camera)}animate()})</script><style scoped>div{width:100vw;height:100vh;}</style>