レイキャスト

3D 空間にレイ(半直線)を飛ばして、当たったオブジェクトを取得する。 マウスクリックで何を選んだかの判定、視線判定、当たり判定の基本中の基本

4 ステップで使う

  1. マウス座標を NDC(-1..1)に変換する
  2. Raycasterカメラ + NDC から構築する
  3. raycaster.intersectObjects(targets) で当たり判定
  4. 戻り値の先頭が「最も手前」
最小コード
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
uv22セット目の UV
instanceIdInstancedMesh の場合のインスタンス番号

クリックで選択

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)
direction は normalize

raycaster.set(origin, dir)dir長さ 1を前提に距離計算される。 .normalize() を必ず通す。

R3F でのイベント

R3F は内部で raycaster を毎フレーム回している。R3F インタラクションを参照。 生 Three.js でも上記のパターンで同等のことができる。