一键导入
vtj-raycasting-system
Implements ray picking and collision detection. Use when selecting objects, detecting block interactions, or optimizing raycaster performance.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Implements ray picking and collision detection. Use when selecting objects, detecting block interactions, or optimizing raycaster performance.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Orchestrates World lifecycle and component initialization. Use when managing initialization order, update sequences, or implementing proper disposal chains.
Prevents common mistakes and prohibited practices in vite-threejs projects. Use before writing code to avoid bugs, memory leaks, and maintenance issues.
Configures third-person camera systems with collision avoidance. Use when adjusting follow behavior, implementing camera modes, or handling cave/obstacle detection.
Creates 3D components using the Experience singleton pattern. Use when building new classes in src/js/, implementing lifecycle methods, or managing Three.js objects.
Configures Tweakpane debug panels for 3D components. Use when adding debug controls, monitoring component state, or adjusting parameters in real-time via the
Handles mouse, keyboard, and touch input via the IMouse and InputManager systems. Use when implementing controls, raycasting, or FPS-style PointerLock.
| name | vtj-raycasting-system |
| description | Implements ray picking and collision detection. Use when selecting objects, detecting block interactions, or optimizing raycaster performance. |
本项目的射线系统主要用于 方块交互(挖掘、放置)和 目标选择。
核心原则:始终使用 iMouse.normalizedMouse 获取 NDC 坐标,射线检测结果通过 mitt 事件通知。
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
// 使用 IMouse 的 normalizedMouse(MANDATORY)
const ndc = this.iMouse.normalizedMouse
this.raycaster.setFromCamera(ndc, this.camera)
// 检测交叉
this.intersects = this.raycaster.intersectObjects(
this.scene.children,
true // recursive
)
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)
// 或者第三人称:从鼠标位置发射
// this.raycaster.setFromCamera(this.iMouse.normalizedMouse, this.camera)
const intersects = this.raycaster.intersectObjects(this.targets, true)
// ...
}
本项目的 BlockRaycaster 实现了体素方块的射线检测:
// src/js/interaction/block-raycaster.js
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, // false = 屏幕中心, true = 鼠标位置
}
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
}
}
// src/js/interaction/block-interaction-manager.js
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)
// 配置 Raycaster 只检测特定层
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++
// 每 3 帧检测一次
if (this._frameCount % 3 !== 0) return
this.raycaster.setFromCamera(...)
// ...
}
对于复杂几何体,考虑使用 three-mesh-bvh:
import { computeBoundsTree } from 'three-mesh-bvh'
// 为复杂几何体构建 BVH
mesh.geometry.computeBoundsTree()
// 射线检测会自动使用 BVH 加速
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
}
}
}
// BAD
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)
// GOOD
this.raycaster.setFromCamera(this.iMouse.normalizedMouse, this.camera)
// BAD: 检测所有对象,性能差
const intersects = this.raycaster.intersectObjects(this.scene.children, true)
// GOOD: 只检测相关对象
const intersects = this.raycaster.intersectObjects(this.interactables, false)
// BAD: 默认 far = Infinity
this.raycaster = new THREE.Raycaster()
// GOOD: 限制检测距离
this.raycaster = new THREE.Raycaster()
this.raycaster.far = 50
// BAD: 没有检查是否有效
_handleClick({ button }) {
const hit = this.intersects[0] // 可能为空!
this.doSomething(hit.object)
}
// GOOD: 检查条件
_handleClick({ button }) {
if (button !== 0) return
if (!this.result.hit) return
this.doSomething(this.result.blockPos)
}
| 需求 | 做法 |
|---|---|
| 从鼠标发射射线 | 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) |
| 面法线 | 用于确定点击的是哪个面 |