レイキャスト
3D 空間にレイ(半直線)を飛ばして、当たったオブジェクトを取得する。 マウスクリックで何を選んだかの判定、視線判定、当たり判定の基本中の基本。
4 ステップで使う
- マウス座標を NDC(-1..1)に変換する
Raycasterを カメラ + NDC から構築するraycaster.intersectObjects(targets)で当たり判定- 戻り値の先頭が「最も手前」
const raycaster = new THREE.Raycaster()
const pointer = new THREE.Vector2()
window.addEventListener("pointermove", (e) => {
// 画面座標 → NDC(-1..1)
pointer.x = (e.clientX / window.innerWidth) * 2 - 1
pointer.y = -((e.clientY / window.innerHeight) * 2 - 1)
})
renderer.setAnimationLoop(() => {
raycaster.setFromCamera(pointer, camera)
const hits = raycaster.intersectObjects(scene.children, true) // 再帰
for (const m of meshes) m.material.color.set("#7dd3fc")
if (hits.length > 0) {
hits[0].object.material.color.set("hotpink")
}
renderer.render(scene, camera)
})
Raycaster.setFromCamera
マウスの NDC 座標とカメラから、レイの origin と direction を自動で組み立てる。 Perspective でも Orthographic でも正しく動く(カメラの種類を内部で判別)。
当たり判定 API
intersectObject(object, recursive = false)
1つのオブジェクトに対して。recursive: true で子孫も対象に。
intersectObjects(objects, recursive = false)
複数のオブジェクトに対して。大半はこれを使う。
戻り値(手前順にソート済み)
| プロパティ | 意味 |
|---|---|
object | 当たった Object3D |
distance | カメラからの距離 |
point | ワールド座標での当たり点(Vector3) |
face | 当たった面(Face。{a, b, c, normal, materialIndex}) |
faceIndex | 面のインデックス |
uv | その点の UV 座標(Vector2) |
uv2 | 2セット目の UV |
instanceId | InstancedMesh の場合のインスタンス番号 |
クリックで選択
window.addEventListener("click", (e) => {
pointer.x = (e.clientX / window.innerWidth) * 2 - 1
pointer.y = -((e.clientY / window.innerHeight) * 2 - 1)
raycaster.setFromCamera(pointer, camera)
const hits = raycaster.intersectObjects(selectables, false)
if (hits.length > 0) {
const picked = hits[0].object
selectMesh(picked)
} else {
deselectAll()
}
})
テクスチャ上の位置を取る
hits[0].uv から「画像のどこをクリックしたか」が分かる。
テクスチャに描いた UI に対応させて反応する、というパターン。
InstancedMesh のヒット
InstancedMesh 上でレイキャストすると、instanceId が返る。
const hits = raycaster.intersectObject(instancedMesh)
if (hits.length > 0) {
console.log("instance:", hits[0].instanceId)
}
パフォーマンス
1. シーン全体に飛ばさない
scene.children に対して再帰でレイを飛ばすと、メッシュが多いと重い。
判定対象だけを配列で渡すのが基本。
const selectables = [meshA, meshB, meshC]
raycaster.intersectObjects(selectables, false)
2. raycast = () => {} で外す
絶対に判定対象にならないオブジェクト(パーティクル、HUD、装飾)は raycast を空関数で上書きすると判定スキップ。
decorationMesh.raycast = () => {} // 何もしない
3. Layers でグルーピング
Object3D にはレイヤー(0〜31)がある。レイキャスター側のレイヤーマスクと AND が立つ オブジェクトだけ判定される。
selectableMesh.layers.set(1) // 判定対象だけ layer 1 に
raycaster.layers.set(1) // raycast を layer 1 に絞る
4. near/far を絞る
raycaster.near = 0.5
raycaster.far = 20
線・点群への当たり判定
Raycaster は線と点もサポートする。
ただし数学的にピッタリ当てるのは難しいので、しきい値(threshold)を設ける。
raycaster.params.Line.threshold = 0.05
raycaster.params.Points.threshold = 0.05
ホバー検知の典型パターン
毎フレーム判定して、現在のホバー対象が変わった時だけ処理する。
let hovered = null
renderer.setAnimationLoop(() => {
raycaster.setFromCamera(pointer, camera)
const hits = raycaster.intersectObjects(selectables, false)
const next = hits[0]?.object ?? null
if (next !== hovered) {
if (hovered) onLeave(hovered)
if (next) onEnter(next)
hovered = next
document.body.style.cursor = next ? "pointer" : "auto"
}
renderer.render(scene, camera)
})
カメラから真っ直ぐ飛ばす(クロスヘア)
FPS 視点で「画面の中央」から飛ばしたい場合は、pointer を (0, 0) にすれば良い。
raycaster.setFromCamera(new THREE.Vector2(0, 0), camera)
任意の origin / direction
マウスとは関係なく、好きな点から好きな方向にレイを飛ばすこともできる。 NPC の視線判定、銃弾の貫通判定など。
const origin = new THREE.Vector3(0, 1, 0)
const dir = new THREE.Vector3(0, 0, -1).normalize() // 必須: normalize
raycaster.set(origin, dir)
const hits = raycaster.intersectObjects(scene.children, true)
raycaster.set(origin, dir) の dir は長さ 1を前提に距離計算される。
.normalize() を必ず通す。
R3F でのイベント
R3F は内部で raycaster を毎フレーム回している。R3F インタラクションを参照。 生 Three.js でも上記のパターンで同等のことができる。