| name | vtj-raycasting-system |
| description | Implements ray picking and collision detection. Use when selecting objects, detecting block interactions, or optimizing raycaster performance. |
vite-threejs Raycasting System
Overview
本项目的射线系统主要用于 方块交互(挖掘、放置)和 目标选择。
核心原则:始终使用 iMouse.normalizedMouse 获取 NDC 坐标,射线检测结果通过 mitt 事件通知。
When to Use
- 实现点击拾取功能
- 检测鼠标悬停对象
- 实现方块交互(挖掘、放置)
- 添加目标锁定功能
基础 Raycaster 模式
import * as THREE from 'three'
import Experience from './experience.js'
import emitter from './utils/event-bus.js'
export default class ObjectPicker {
constructor() {
this.experience = new Experience()
this.scene = this.experience.scene
this.camera = this.experience.camera.instance
this.iMouse = this.experience.iMouse
this.raycaster = new THREE.Raycaster()
this.intersects = []
this.params = {
enabled: true,
maxDistance: 100,
}
this._handleClick = this._handleClick.bind(this)
emitter.on('input:mouse_down', this._handleClick)
}
_handleClick({ button }) {
if (button !== 0 || !this.params.enabled) return
const ndc = this.iMouse.normalizedMouse
this.raycaster.setFromCamera(ndc, this.camera)
this.intersects = this.raycaster.intersectObjects(
this.scene.children,
true
)
if (this.intersects.length > 0) {
const hit = this.intersects[0]
emitter.emit('game:object-picked', {
object: hit.object,
point: hit.point,
distance: hit.distance,
})
}
}
destroy() {
emitter.off('input:mouse_down', this._handleClick)
}
}
屏幕中心射线(第一人称准星)
const CENTER_SCREEN = new THREE.Vector2(0, 0)
update() {
this.raycaster.setFromCamera(CENTER_SCREEN, this.camera)
const intersects = this.raycaster.intersectObjects(this.targets, true)
}
方块交互模式
本项目的 BlockRaycaster 实现了体素方块的射线检测:
export default class BlockRaycaster {
constructor() {
this.experience = new Experience()
this.camera = this.experience.camera.instance
this.iMouse = this.experience.iMouse
this.raycaster = new THREE.Raycaster()
this.raycaster.far = 8
this.params = {
useMouse: false,
}
this.result = {
hit: false,
blockPos: null,
faceNormal: null,
adjacentPos: null,
}
}
update(terrainMeshes) {
const ndc = this.params.useMouse
? this.iMouse.normalizedMouse
: new THREE.Vector2(0, 0)
this.raycaster.setFromCamera(ndc, this.camera)
const intersects = this.raycaster.intersectObjects(terrainMeshes, false)
if (intersects.length > 0) {
const hit = intersects[0]
const blockX = Math.floor(hit.point.x - hit.face.normal.x * 0.5)
const blockY = Math.floor(hit.point.y - hit.face.normal.y * 0.5)
const blockZ = Math.floor(hit.point.z - hit.face.normal.z * 0.5)
const adjacentX = blockX + Math.round(hit.face.normal.x)
const adjacentY = blockY + Math.round(hit.face.normal.y)
const adjacentZ = blockZ + Math.round(hit.face.normal.z)
this.result = {
hit: true,
blockPos: new THREE.Vector3(blockX, blockY, blockZ),
faceNormal: hit.face.normal.clone(),
adjacentPos: new THREE.Vector3(adjacentX, adjacentY, adjacentZ),
distance: hit.distance,
}
} else {
this.result.hit = false
}
return this.result
}
}
交互管理器
export default class BlockInteractionManager {
constructor(terrainRenderer) {
this.terrainRenderer = terrainRenderer
this.raycaster = new BlockRaycaster()
this._handleMouseDown = this._handleMouseDown.bind(this)
emitter.on('input:mouse_down', this._handleMouseDown)
}
_handleMouseDown({ button }) {
const result = this.raycaster.result
if (!result.hit) return
if (button === 0) {
this.terrainRenderer.removeBlock(
result.blockPos.x,
result.blockPos.y,
result.blockPos.z
)
} else if (button === 2) {
this.terrainRenderer.placeBlock(
result.adjacentPos.x,
result.adjacentPos.y,
result.adjacentPos.z,
this.currentBlockType
)
}
}
update() {
const meshes = this.terrainRenderer.getMeshes()
this.raycaster.update(meshes)
}
destroy() {
emitter.off('input:mouse_down', this._handleMouseDown)
}
}
层过滤
使用 layers 过滤检测对象:
const LAYER_TERRAIN = 1
const LAYER_PLAYER = 2
const LAYER_UI = 3
terrainMesh.layers.set(LAYER_TERRAIN)
playerMesh.layers.set(LAYER_PLAYER)
this.raycaster.layers.set(LAYER_TERRAIN)
this.raycaster.layers.enable(LAYER_TERRAIN)
this.raycaster.layers.enable(LAYER_PLAYER)
性能优化
限制检测距离
this.raycaster.near = 0.1
this.raycaster.far = 50
分组检测
const intersects = this.raycaster.intersectObjects(
this.interactableGroup.children,
false
)
降低检测频率
update() {
this._frameCount++
if (this._frameCount % 3 !== 0) return
this.raycaster.setFromCamera(...)
}
使用 BVH(边界体积层次)
对于复杂几何体,考虑使用 three-mesh-bvh:
import { computeBoundsTree } from 'three-mesh-bvh'
mesh.geometry.computeBoundsTree()
悬停检测
export default class HoverDetector {
constructor() {
this.hoveredObject = null
}
update() {
const ndc = this.iMouse.normalizedMouse
this.raycaster.setFromCamera(ndc, this.camera)
const intersects = this.raycaster.intersectObjects(this.targets)
const newHovered = intersects.length > 0 ? intersects[0].object : null
if (newHovered !== this.hoveredObject) {
if (this.hoveredObject) {
emitter.emit('game:hover-exit', { object: this.hoveredObject })
}
if (newHovered) {
emitter.emit('game:hover-enter', { object: newHovered })
}
this.hoveredObject = newHovered
}
}
}
Common Mistakes
❌ 手动计算 NDC
const x = (event.clientX / window.innerWidth) * 2 - 1
const y = -(event.clientY / window.innerHeight) * 2 + 1
this.raycaster.setFromCamera(new THREE.Vector2(x, y), this.camera)
this.raycaster.setFromCamera(this.iMouse.normalizedMouse, this.camera)
❌ 检测整个场景
const intersects = this.raycaster.intersectObjects(this.scene.children, true)
const intersects = this.raycaster.intersectObjects(this.interactables, false)
❌ 忘记设置 far 距离
this.raycaster = new THREE.Raycaster()
this.raycaster = new THREE.Raycaster()
this.raycaster.far = 50
❌ 在事件处理中忘记检查条件
_handleClick({ button }) {
const hit = this.intersects[0]
this.doSomething(hit.object)
}
_handleClick({ button }) {
if (button !== 0) return
if (!this.result.hit) return
this.doSomething(this.result.blockPos)
}
Quick Reference
| 需求 | 做法 |
|---|
| 从鼠标发射射线 | raycaster.setFromCamera(iMouse.normalizedMouse, camera) |
| 从屏幕中心发射 | raycaster.setFromCamera(new Vector2(0, 0), camera) |
| 限制距离 | raycaster.far = 50 |
| 层过滤 | raycaster.layers.set(LAYER_ID) |
| 获取点击位置 | intersects[0].point |
| 获取面法线 | intersects[0].face.normal |
| 体素方块特有 | 说明 |
|---|
| 方块坐标 | floor(hit.point - normal * 0.5) |
| 相邻方块 | blockPos + round(normal) |
| 面法线 | 用于确定点击的是哪个面 |