Three.js 顶点射线碰撞检测实现步骤详解

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) {
  // 考虑为碰撞(避免浮点数精度问题)
}

五、适用场景与限制

适用场景:

  1. 需要精确碰撞检测(如物理模拟、游戏碰撞)
  2. 不规则几何体碰撞(非AABB/球体等简单形状)
  3. 需要知道碰撞位置(不仅仅是是否碰撞)

限制:

  1. 计算量大:顶点越多越慢
  2. 凹形几何体问题:射线可能从凹处"穿过"而不碰撞
  3. 薄物体问题:对于非常薄的几何体可能漏检

六、伪代码总结

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) {
  // 考虑为碰撞(避免浮点数精度问题)
}

五、适用场景与限制

适用场景:

  1. 需要精确碰撞检测(如物理模拟、游戏碰撞)
  2. 不规则几何体碰撞(非AABB/球体等简单形状)
  3. 需要知道碰撞位置(不仅仅是是否碰撞)

限制:

  1. 计算量大:顶点越多越慢
  2. 凹形几何体问题:射线可能从凹处"穿过"而不碰撞
  3. 薄物体问题:对于非常薄的几何体可能漏检

六、伪代码总结

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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值