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 获取几何体的顶点位置数据
const geometry = mesh.geometry
const positionAttribute = geometry.attributes.position
const positions = positionAttribute.array // [x1, y1, z1, x2, y2, z2, ...]
// 1.2 获取世界变换矩阵
const matrixWorld = mesh.matrixWorld
// 1.3 遍历所有顶点,转换为世界坐标
const worldVertices = []
for (let i = 0; i < positions.length; i += 3) {
// 创建局部坐标顶点
const localVertex = new THREE.Vector3(
positions[i], // x
positions[i + 1], // y
positions[i + 2] // z
)
// 转换为世界坐标
const worldVertex = localVertex.clone()
worldVertex.applyMatrix4(matrixWorld)
worldVertices.push(worldVertex)
}
Step 2: 计算几何体中心点(世界坐标)
// 方法1:直接使用mesh的世界位置(对于对称几何体)
const center = new THREE.Vector3()
mesh.getWorldPosition(center)
// 方法2:计算所有顶点的平均值(更精确)
let sum = new THREE.Vector3(0, 0, 0)
for (const vertex of worldVertices) {
sum.add(vertex)
}
const center = sum.divideScalar(worldVertices.length)
Step 3: 创建顶点到中心的射线
// 3.1 创建Raycaster实例
const raycaster = new THREE.Raycaster()
// 3.2 对于每个顶点,创建从顶点指向中心的射线
for (const vertex of worldVertices) {
// 计算方向向量(从顶点指向中心)
const direction = center.clone().sub(vertex).normalize()
// 设置射线
raycaster.set(vertex, direction)
// 现在raycaster就代表了一条从顶点指向中心的射线
// 我们可以用它来检测与其他几何体的交点
}
Step 4: 射线交叉计算
// 4.1 检测射线是否与另一个几何体相交
const otherMesh = ... // 另一个几何体
const intersects = raycaster.intersectObject(otherMesh)
// 4.2 分析交点信息
if (intersects.length > 0) {
const firstIntersect = intersects[0] // 最近的交点
// 交点信息包含:
// - point: 交点坐标(Vector3)
// - distance: 从射线起点到交点的距离
// - object: 被击中的物体
// - face: 被击中的面
// - faceIndex: 面的索引
}
Step 5: 通过距离判断是否碰撞
// 5.1 计算关键距离
const distanceToIntersect = vertex.distanceTo(firstIntersect.point) // 顶点到交点的距离
const distanceToCenter = vertex.distanceTo(center) // 顶点到中心的距离
// 5.2 判断逻辑
if (distanceToIntersect < distanceToCenter) {
// 情况:射线在到达自己中心之前就击中了对方
// 说明对方几何体在自己"前方"或"内部"
// 这很可能表示两个几何体相交或碰撞
console.log('这条射线穿过了对方几何体!')
// 记录碰撞信息
collisions.push({
vertex: vertex,
intersectPoint: firstIntersect.point,
rayDirection: direction
})
} else {
// 情况:射线先到达自己中心,然后才可能击中对方
// 说明对方几何体在自己"后方"或"外部"
// 这很可能表示两个几何体没有碰撞
}
Step 6: 综合判断碰撞
// 6.1 统计所有穿过的射线
let collisionCount = 0
for (所有顶点射线) {
if (射线穿过了对方几何体) {
collisionCount++
}
}
// 6.2 判断是否发生碰撞
if (collisionCount > 0) {
console.log(`发生碰撞!共有 ${collisionCount} 条射线穿过对方几何体`)
return true // 发生碰撞
} else {
console.log('没有碰撞')
return false // 没有碰撞
}
三、数学原理图解
情况1:发生碰撞(射线穿过对方)
顶点
│
│ 距离1(到交点)
│
▼ 交点(在对方几何体上)
│
│ 距离2(从交点到中心)
│
▼ 中心
距离1 < 距离1+距离2(即距离1 < 顶点到中心的距离)
∴ 射线在到达中心之前击中了对方 → 碰撞!
情况2:没有碰撞(射线先到中心)
顶点
│
│ 距离1(到中心)
│
▼ 中心
│
│ 距离2(从中心到交点)
│
▼ 交点(在对方几何体上)
距离1 < 距离1+距离2(但射线要先经过中心)
∴ 射线先到达中心,然后才可能击中对方 → 没有碰撞!
四、性能优化考虑
1. 顶点采样优化
// 不检测所有顶点,只采样一部分
const sampleRate = 0.3 // 采样率30%
for (let i = 0; i < worldVertices.length; i += Math.floor(1/sampleRate)) {
// 只检测部分顶点
}
2. 分层次检测
// 第一步:快速检测(包围盒)
const box1 = new THREE.Box3().setFromObject(mesh1)
const box2 = new THREE.Box3().setFromObject(mesh2)
if (!box1.intersectsBox(box2)) {
return false // 包围盒不相交,肯定不碰撞,快速返回
}
// 第二步:精确检测(顶点射线)
// 只有包围盒相交时,才进行更耗时的顶点检测
3. 空间分割优化
// 使用八叉树或BVH(包围盒层次结构)加速
// 只检测可能相交的几何体
4. 距离阈值优化
// 添加容差阈值
const threshold = 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 获取几何体的顶点位置数据
const geometry = mesh.geometry
const positionAttribute = geometry.attributes.position
const positions = positionAttribute.array // [x1, y1, z1, x2, y2, z2, ...]
// 1.2 获取世界变换矩阵
const matrixWorld = mesh.matrixWorld
// 1.3 遍历所有顶点,转换为世界坐标
const worldVertices = []
for (let i = 0; i < positions.length; i += 3) {
// 创建局部坐标顶点
const localVertex = new THREE.Vector3(
positions[i], // x
positions[i + 1], // y
positions[i + 2] // z
)
// 转换为世界坐标
const worldVertex = localVertex.clone()
worldVertex.applyMatrix4(matrixWorld)
worldVertices.push(worldVertex)
}
Step 2: 计算几何体中心点(世界坐标)
// 方法1:直接使用mesh的世界位置(对于对称几何体)
const center = new THREE.Vector3()
mesh.getWorldPosition(center)
// 方法2:计算所有顶点的平均值(更精确)
let sum = new THREE.Vector3(0, 0, 0)
for (const vertex of worldVertices) {
sum.add(vertex)
}
const center = sum.divideScalar(worldVertices.length)
Step 3: 创建顶点到中心的射线
// 3.1 创建Raycaster实例
const raycaster = new THREE.Raycaster()
// 3.2 对于每个顶点,创建从顶点指向中心的射线
for (const vertex of worldVertices) {
// 计算方向向量(从顶点指向中心)
const direction = center.clone().sub(vertex).normalize()
// 设置射线
raycaster.set(vertex, direction)
// 现在raycaster就代表了一条从顶点指向中心的射线
// 我们可以用它来检测与其他几何体的交点
}
Step 4: 射线交叉计算
// 4.1 检测射线是否与另一个几何体相交
const otherMesh = ... // 另一个几何体
const intersects = raycaster.intersectObject(otherMesh)
// 4.2 分析交点信息
if (intersects.length > 0) {
const firstIntersect = intersects[0] // 最近的交点
// 交点信息包含:
// - point: 交点坐标(Vector3)
// - distance: 从射线起点到交点的距离
// - object: 被击中的物体
// - face: 被击中的面
// - faceIndex: 面的索引
}
Step 5: 通过距离判断是否碰撞
// 5.1 计算关键距离
const distanceToIntersect = vertex.distanceTo(firstIntersect.point) // 顶点到交点的距离
const distanceToCenter = vertex.distanceTo(center) // 顶点到中心的距离
// 5.2 判断逻辑
if (distanceToIntersect < distanceToCenter) {
// 情况:射线在到达自己中心之前就击中了对方
// 说明对方几何体在自己"前方"或"内部"
// 这很可能表示两个几何体相交或碰撞
console.log('这条射线穿过了对方几何体!')
// 记录碰撞信息
collisions.push({
vertex: vertex,
intersectPoint: firstIntersect.point,
rayDirection: direction
})
} else {
// 情况:射线先到达自己中心,然后才可能击中对方
// 说明对方几何体在自己"后方"或"外部"
// 这很可能表示两个几何体没有碰撞
}
Step 6: 综合判断碰撞
// 6.1 统计所有穿过的射线
let collisionCount = 0
for (所有顶点射线) {
if (射线穿过了对方几何体) {
collisionCount++
}
}
// 6.2 判断是否发生碰撞
if (collisionCount > 0) {
console.log(`发生碰撞!共有 ${collisionCount} 条射线穿过对方几何体`)
return true // 发生碰撞
} else {
console.log('没有碰撞')
return false // 没有碰撞
}
三、数学原理图解
情况1:发生碰撞(射线穿过对方)
顶点
│
│ 距离1(到交点)
│
▼ 交点(在对方几何体上)
│
│ 距离2(从交点到中心)
│
▼ 中心
距离1 < 距离1+距离2(即距离1 < 顶点到中心的距离)
∴ 射线在到达中心之前击中了对方 → 碰撞!
情况2:没有碰撞(射线先到中心)
顶点
│
│ 距离1(到中心)
│
▼ 中心
│
│ 距离2(从中心到交点)
│
▼ 交点(在对方几何体上)
距离1 < 距离1+距离2(但射线要先经过中心)
∴ 射线先到达中心,然后才可能击中对方 → 没有碰撞!
四、性能优化考虑
1. 顶点采样优化
// 不检测所有顶点,只采样一部分
const sampleRate = 0.3 // 采样率30%
for (let i = 0; i < worldVertices.length; i += Math.floor(1/sampleRate)) {
// 只检测部分顶点
}
2. 分层次检测
// 第一步:快速检测(包围盒)
const box1 = new THREE.Box3().setFromObject(mesh1)
const box2 = new THREE.Box3().setFromObject(mesh2)
if (!box1.intersectsBox(box2)) {
return false // 包围盒不相交,肯定不碰撞,快速返回
}
// 第二步:精确检测(顶点射线)
// 只有包围盒相交时,才进行更耗时的顶点检测
3. 空间分割优化
// 使用八叉树或BVH(包围盒层次结构)加速
// 只检测可能相交的几何体
4. 距离阈值优化
// 添加容差阈值
const threshold = 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 * as THREE from 'three'
const containerRef = ref(null)
let cube1, cube2
onMounted(() => {
const scene = new THREE.Scene()
scene.background = new THREE.Color(0x111111)
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000)
camera.position.set(0, 5, 10)
const renderer = new THREE.WebGLRenderer()
renderer.setSize(window.innerWidth, window.innerHeight)
containerRef.value.appendChild(renderer.domElement)
// 创建立方体1(可移动)
cube1 = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true })
)
cube1.position.set(-3, 0, 0)
scene.add(cube1)
// 创建立方体2(静止)
cube2 = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshBasicMaterial({ color: 0x0000ff, wireframe: true })
)
cube2.position.set(0, 0, 0)
scene.add(cube2)
// 辅助线
scene.add(new THREE.AxesHelper(5))
// 核心算法:顶点射线碰撞检测
const checkVertexRayCollision = () => {
console.log('=== 顶点射线碰撞检测 ===')
// 1. 获取顶点(世界坐标)
const getVertices = (mesh) => {
const geometry = mesh.geometry
const positions = geometry.attributes.position.array
const vertices = []
const matrixWorld = mesh.matrixWorld
for (let i = 0; i < positions.length; i += 3) {
const vertex = new THREE.Vector3(
positions[i],
positions[i + 1],
positions[i + 2]
)
vertex.applyMatrix4(matrixWorld)
vertices.push(vertex)
}
return vertices
}
const vertices1 = getVertices(cube1)
const vertices2 = getVertices(cube2)
// 2. 获取中心点(世界坐标)
const center1 = new THREE.Vector3()
cube1.getWorldPosition(center1)
const center2 = new THREE.Vector3()
cube2.getWorldPosition(center2)
console.log('立方体1顶点数:', vertices1.length)
console.log('立方体2顶点数:', vertices2.length)
console.log('立方体1中心:', center1)
console.log('立方体2中心:', center2)
// 3. 检测立方体1的顶点射线
let collisionCount = 0
for (let i = 0; i < vertices1.length; i++) {
const vertex = vertices1[i]
const direction = new THREE.Vector3().subVectors(center1, vertex).normalize()
const raycaster = new THREE.Raycaster()
raycaster.set(vertex, direction)
const intersects = raycaster.intersectObject(cube2)
if (intersects.length > 0) {
const intersect = intersects[0]
const distanceToHit = vertex.distanceTo(intersect.point)
const distanceToCenter = vertex.distanceTo(center1)
if (distanceToHit < distanceToCenter) {
collisionCount++
console.log(`立方体1顶点${i}: 射线穿过了立方体2`)
}
}
}
// 4. 检测立方体2的顶点射线
for (let i = 0; i < vertices2.length; i++) {
const vertex = vertices2[i]
const direction = new THREE.Vector3().subVectors(center2, vertex).normalize()
const raycaster = new THREE.Raycaster()
raycaster.set(vertex, direction)
const intersects = raycaster.intersectObject(cube1)
if (intersects.length > 0) {
const intersect = intersects[0]
const distanceToHit = vertex.distanceTo(intersect.point)
const distanceToCenter = 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)
return true
} else {
console.log('✅ 没有碰撞')
cube1.material.color.set(0xff0000)
cube2.material.color.set(0x0000ff)
return false
}
}
// 自动移动并检测
let direction = 1
const animate = () => {
requestAnimationFrame(animate)
// 移动立方体1
cube1.position.x += 0.01 * direction
// 边界检测
if (cube1.position.x > 2) direction = -1
if (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>
965

被折叠的 条评论
为什么被折叠?



