ν™ˆ three.js Raycaster μ‚΄νŽ΄λ³΄κΈ°
포슀트
μ·¨μ†Œ

three.js Raycaster μ‚΄νŽ΄λ³΄κΈ°

πŸ’» Ray Casting μ΄λž€?

Ray Casting 은 κ°€μƒμ˜ κ΄‘μ„ (ray)을 μ΄μ„œ, μ–΄λ–€ 물체와 κ΅μ°¨ν•˜λŠ”μ§€ ν™•μΈν•˜λŠ” κΈ°μˆ μž…λ‹ˆλ‹€. ν•΄λ‹Ή 기법을 μ‚¬μš©ν•˜μ—¬, 좩돌 감지, 물체의 선택 및 μƒν˜Έμž‘μš©, 가렀진 λ©΄ νŒλ³„λ“±μ„ κ΅¬ν˜„ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

πŸ’» Raycaster λ§Œλ“€κΈ°

Raycaster( origin : Vector3, direction : Vector3, near : Float, far : Float )

three.js μ—μ„œ Raycaster λŠ” μœ„ μƒμ„±μžλ₯Ό μ΄μš©ν•΄μ„œ λ§Œλ“€ 수 μžˆμŠ΅λ‹ˆλ‹€. μƒμ„±μž 각각의 μΈμžλ“€μ€ λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

  • origin
    Ray (κ΄‘μ„ )κ°€ μ‹œμž‘λ˜λŠ” μ’Œν‘œλ‘œ Vector3 ν˜•μ‹μ„ κ°–μŠ΅λ‹ˆλ‹€.
  • direction
    Ray 의 λ°©ν–₯을 λ‚˜νƒ€λ‚΄λŠ” Vector3 μ’Œν‘œλ‘œ, λ°˜λ“œμ‹œ normalize ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•΄ μ •κ·œν™” μ‹œμΌœμ•Ό ν•©λ‹ˆλ‹€.
  • near
    μ΅œμ†Ÿκ°’μ„ μ§€μ •ν•˜λŠ” 인자둜, λ°˜ν™˜λœλŠ” λͺ¨λ“  결과듀은 near μ—μ„œ μ„€μ •ν•œ 값보닀 ν½λ‹ˆλ‹€.
    기본값은 0 이며, μŒμˆ˜λŠ” 될 수 μ—†μŠ΅λ‹ˆλ‹€.
  • far μ΅œλŒ“κ°’μ„ μ§€μ •ν•˜λŠ” 인자둜, 결과듀은 far 의 값보닀 클 수 μ—†μŠ΅λ‹ˆλ‹€.
    기본값은 λ¬΄ν•œλŒ€ μž…λ‹ˆλ‹€.
1
2
3
4
5
6
7
// ...μƒλž΅...
const rayOrigin = new THREE.Vector3(-5, 0, 0);
const rayDirection = new THREE.Vector3(10, 0, 0);
rayDirection.normalize();

const raycaster = new THREE.Raycaster(rayOrigin, rayDirection);
// ...μƒλž΅...

μœ„ μ½”λ“œλŠ” μ‹œμž‘μ μ΄ (-5, 0, 0) 이며 λ°©ν–₯이 (10, 0, 0) 을 ν–₯ν•˜λŠ” ray λ₯Ό λ§Œλ“  μ˜ˆμ‹œμž…λ‹ˆλ‹€.
rayDirection 이 λ‹¨μœ„λ²‘ν„°κ°€ μ•„λ‹ˆλ―€λ‘œ normalize λ₯Ό μ‹€ν–‰ν•œ 것을 λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€.

RayCaster λ₯Ό 생성할 λ•Œ, μƒμ„±μžκ°€ μ•„λ‹Œ set ν•¨μˆ˜λ₯Ό μ΄μš©ν•˜μ—¬ λ§Œλ“€ μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.

1
2
3
4
5
6
7
8
9
// ...μƒλž΅...
const raycaster = new THREE.Raycaster();

const rayOrigin = new THREE.Vector3(-5, 0, 0);
const rayDirection = new THREE.Vector3(10, 0, 0);
rayDirection.normalize();

rayCaster.set(rayOrigin, rayDirection);
// ...μƒλž΅...

πŸ’» Raycasterλ₯Ό ν™œμš©ν•˜μ—¬ κ΅μ°¨ν•˜λŠ” 물체 μ°ΎκΈ°

Raycaster 의 ray 와 κ΅μ°¨ν•˜λŠ” 물체λ₯Ό μ°ΎκΈ° μœ„ν•΄μ„œλŠ”, intersectObject 와 intersectObjects λ©”μ†Œλ“œλ₯Ό μ΄μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€. 두 ν•¨μˆ˜μ˜ μ°¨μ΄λŠ” ray 와 ꡐ차됨을 νŒλ³„ν•˜λŠ” 물체가 λ‹¨μˆ˜μ΄λ‚˜, λ³΅μˆ˜μ΄λƒ μž…λ‹ˆλ‹€.

intersectObject(object: Object3D, recursive: Boolean, optionalTarget: Array): Array

intersectObject λ©”μ†Œλ“œ μΈμžλ“€μ˜ μ˜λ―ΈλŠ” μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.

  • object
    Ray 와 κ΅μ°¨λ˜λŠ”μ§€λ₯Ό νŒλ³„ν•  Object3D μΈμŠ€ν„΄μŠ€ μž…λ‹ˆλ‹€
  • recursive
    true 둜 μ„€μ •ν•  경우, ray 와 object 인자의 λͺ¨λ“  μžμ†λ“€μ΄ κ΅μ°¨ν•˜λŠ”μ§€λ₯Ό νŒλ³„ν•©λ‹ˆλ‹€. 그렇지 μ•Šμ„ 경우, object 인자만 νŒλ³„ν•©λ‹ˆλ‹€.
    기본값은 true μž…λ‹ˆλ‹€.
  • optionalTarget
    λ°˜ν™˜λœ κ²°κ³Όλ₯Ό λ‹€λ₯Έ 배열에 μ €μž₯ν•˜κ³  싢을 λ•Œ, μ§€μ •ν•˜λŠ” μ˜΅μ…˜μž…λ‹ˆλ‹€.
    λ§Œμ•½ ν•΄λ‹Ή μΈμžμ— 배열을 λ„˜κ²¨μ£Όμ—ˆλ‹€λ©΄, ν˜ΈμΆœμ‹œλ§ˆλ‹€ ν•΄λ‹Ή 배열을 λΉ„μ›Œμ•Ό ν•©λ‹ˆλ‹€.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ...μƒλž΅...
const mesh1 = new THREE.Mesh(
  boxGeometry,
  new THREE.MeshBasicMaterial({ color: Math.random() * 0xffffff })
);
mesh1.position.x = -1;

const mesh2 = new THREE.Mesh(
  boxGeometry,
  new THREE.MeshBasicMaterial({ color: Math.random() * 0xffffff })
);

const mesh3 = new THREE.Mesh(
  boxGeometry,
  new THREE.MeshBasicMaterial({ color: Math.random() * 0xffffff })
);
mesh3.position.x = 1;

// ...μƒλž΅...

const rayOrigin = new THREE.Vector3(-5, 0, 0);
const rayDirection = new THREE.Vector3(10, 0, 0);
rayDirection.normalize();

const raycaster = new THREE.Raycaster(rayOrigin, rayDirection);
const intersect = raycaster.intersectObject(mesh1);
// ...μƒλž΅...

μœ„ μ½”λ“œλŠ”, 각각 (-1, 0, 0), (0, 0, 0), (1, 0, 0) 에 μœ„μΉ˜ν•˜λŠ” λ„ˆλΉ„ 0.5 인 μ •μœ‘λ©΄μ²΄ μ„Έκ°œ 쀑, 제일 μ™Όμͺ½μ˜ μ •μœ‘λ©΄μ²΄μ™€ (-5, 0, 0) μ—μ„œ (10, 0, 0) λ°©ν–₯으둜 ν–₯ν•˜λŠ” ray μ™€μ˜ ꡐ차지점을 κ΅¬ν•˜λŠ” μ˜ˆμ‹œμž…λ‹ˆλ‹€.

ray-caster-intersect-1

intersect 의 κ²°κ³Όλ₯Ό μ½˜μ†”μ— 좜λ ₯해보면 λ‹€μŒκ³Ό 같이 λ‚˜μ˜΅λ‹ˆλ‹€.

ray-caster-intersect-2

κ²°κ³Όκ°€ λ‘κ°œμž„μ„ μ•Œ 수 μžˆμŠ΅λ‹ˆλ‹€. μ΄λŠ” segment μ˜΅μ…˜μ„ μ„€μ •ν•˜μ§€ μ•Šκ³  μ •μœ‘λ©΄μ²΄λ₯Ό 생성할 λ•Œ, ν•œλ©΄μ€ λ‘κ°œμ˜ μ‚Όκ°ν˜• face 둜 κ΅¬μ„±λ˜μ–΄ μžˆλŠ”λ°, ray κ°€ λ‘κ°œμ˜ μ‚Όκ°ν˜•μ΄ λ§žλ‹Ώμ€ 곳을 ν†΅κ³Όν•˜κΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€. Segment λ₯Ό λ³€κ²½ν•˜κ²Œλ˜λ©΄, κ²°κ³Όκ°€ λ‹¬λΌμ§ˆ 수 μžˆμŠ΅λ‹ˆλ‹€.

μ•žμ„œ μ‚΄νŽ΄λ³΄μ•˜λ“―, intersectObject 의 결괏값은 배열이며, ray 의 μ‹œμž‘μ κ³Όμ˜ 거리가 κ°€κΉŒμš΄ 순으둜 μ •λ ¬λ©λ‹ˆλ‹€. μ›μ†Œμ˜ 객체 μ†μ„±μ˜ μ˜λ―ΈλŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

  • distance
    Ray 의 좜발점과 ꡐ차점과의 거리λ₯Ό λ‚˜νƒ€λ‚΄λŠ” μ†μ„±μž…λ‹ˆλ‹€.
    μœ„ μ˜ˆμ‹œμ—μ„œ, ray 의 μΆœλ°œμ μ€ (-5, 0, 0 ) 이며 κ΅μ°¨λ˜λŠ” μ§€μ μ˜ μ’Œν‘œλŠ” (-1.25, 0, 0) (μ€‘μ‹¬μ˜ μ’Œν‘œκ°€ (-1, 0, 0) 이고, ν•œλ³€μ˜ 길이가 0.5 이기 λ•Œλ¬Έ), 3.75 κ°€ λ‚˜μ™”μŠ΅λ‹ˆλ‹€.
  • point
    ꡐ차점의 μ’Œν‘œλ₯Ό μ˜λ―Έν•©λ‹ˆλ‹€.
  • face
    κ΅μ°¨λ˜λŠ” λ©΄(face) 의 정보λ₯Ό λ‚˜νƒ€λƒ…λ‹ˆλ‹€.
  • faceIndex
    κ΅μ°¨λ˜λŠ” 면의 index λ₯Ό λ‚˜νƒ€λƒ…λ‹ˆλ‹€.
  • object
    Ray 와 ꡐ차된 mesh 의 정보λ₯Ό λ‚˜νƒ€λƒ…λ‹ˆλ‹€.
  • uv
    ꡐ차점의 uv μ’Œν‘œλ₯Ό μ˜λ―Έν•©λ‹ˆλ‹€.
  • uv1
    κ΅μ°¨μ μ—μ„œμ˜ λ‘λ²ˆμ§Έ uv μ’Œν‘œλ₯Ό μ˜λ―Έν•©λ‹ˆλ‹€.
  • normal
    κ΅μ°¨μ μ—μ„œμ˜ normal λ°±ν„°λ₯Ό λ‚˜νƒ€λƒ…λ‹ˆλ‹€.
  • instanceId
    Ray 와 InstancedMesh κ°€ κ΅μ°¨ν• λ•Œ, ν•΄λ‹Ή μΈμŠ€ν„΄μŠ€μ˜ index 번호λ₯Ό λ‚˜νƒ€λƒ…λ‹ˆλ‹€.

intersectObject λ₯Ό μ‚¬μš©ν•  λ•Œ, μ£Όμ˜ν•  점은 mesh 의 face κ°€ ray 의 μ‹œμž‘μ μ„ ν–₯ν•΄ μžˆμ–΄μ•Ό ν•œλ‹€λŠ” μ μž…λ‹ˆλ‹€.

1
2
3
4
5
6
7
8
9
// ...μƒλž΅...
const rayOrigin = new THREE.Vector3(-1, 0, 0);
const rayDirection = new THREE.Vector3(10, 0, 0);
rayDirection.normalize();

const raycaster = new THREE.Raycaster(rayOrigin, rayDirection);
// ...μƒλž΅...
const intersect = raycaster.intersectObject(mesh1);
// ...μƒλž΅...

Ray 의 μ‹œμž‘μ’Œν‘œλ₯Ό 제일 μ™Όμͺ½ μ •μœ‘λ©΄μ²΄μ˜ λ‚΄λΆ€λ‘œ 지정해 λ³΄κ² μŠ΅λ‹ˆλ‹€.

ray-caster-intersect-3

μœ„ μ½”λ“œμ—μ„œ intersect λ₯Ό 좜λ ₯해보면, λΉˆλ°°μ—΄μ΄ 좜λ ₯λ©λ‹ˆλ‹€. Ray 와 κ΅μ°¨ν•˜λŠ” face κ°€ μ‹œμž‘μ μ΄ μ•„λ‹Œ λ°˜λŒ€ λ°©ν–₯을 ν–₯ν•˜κ³  있기 λ•Œλ¬Έμž…λ‹ˆλ‹€.
이λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ material 의 side μ˜΅μ…˜μ„ THREE.DoubleSide 둜 μ„€μ •ν•˜λ©΄ λ©λ‹ˆλ‹€.

πŸ’» Raycaster λ₯Ό ν™œμš©ν•˜μ—¬ μ›€μ§μ΄λŠ” 물체 ꡐ차점 μ°ΎκΈ°

μ›€μ§μ΄λŠ” 물체의 ꡐ차점을 μ°ΎκΈ°μœ„ν•΄μ„œλŠ” ν”„λ ˆμž„λ§ˆλ‹€ intersectObject λ₯Ό μ‹€ν–‰ν•΄ μ£Όμ–΄μ•Ό ν•©λ‹ˆλ‹€.
μ›μš΄λ™ ν•˜λŠ” BoxGeometry κ°€ ray 와 ꡐ차할 λ•Œ, 색이 λ³€ν•˜λŠ” μ½”λ“œλ₯Ό μž‘μ„±ν•΄ λ³΄κ² μŠ΅λ‹ˆλ‹€.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ...μƒλž΅...
const clock = new THREE.Clock();
function animate() {
  const elapsedTime = clock.getElapsedTime();

  mesh1.material.color.set("red");

  mesh1.position.x = Math.sin(elapsedTime);
  mesh1.position.y = Math.cos(elapsedTime);

  const intersect = raycaster.intersectObject(mesh1);

  for (const intersectMesh of intersect) {
    intersectMesh.object.material.color.set("blue");
  }

  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);
// ...μƒλž΅...

μœ„ μ½”λ“œλ₯Ό μ‹€ν–‰ν•œ κ²°κ³ΌλŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

ray-caster-intersect-4

πŸ’» Raycaster λ₯Ό ν™œμš©ν•˜μ—¬ ν¬μΈν„°λ‘œ 물체 μƒν˜Έμž‘μš©ν•˜κΈ°

Raycaster λ₯Ό μ΄μš©ν•˜μ—¬, 포인터 (PC μ—μ„œλŠ” 마우슀)와 물체가 μƒν˜Έμž‘μš©ν•˜λ„λ‘ κ΅¬ν˜„ν•˜λŠ” 방법을 μ•Œμ•„λ³΄κ² μŠ΅λ‹ˆλ‹€.

μƒν˜Έμž‘μš©ν•˜κΈ° μœ„ν•΄μ„œλŠ”, ν¬μΈν„°μ˜ μ’Œν‘œκ°€ ν•„μš”ν•œλ° ν”½μ…€μ’Œν‘œκ°€ μ•„λ‹Œ -1 μ—μ„œ 1 μ‚¬μ΄μ˜ μ •κ·œν™”λœ μ’Œν‘œκ°€ ν•„μš”ν•©λ‹ˆλ‹€.

1
2
3
4
5
6
7
8
9
10
// ...μƒλž΅...
const pointerCoord = new THREE.Vector2();

canvas.addEventListener("pointermove", (e) => {
  const { offsetX, offsetY } = e;

  pointerCoord.x = (offsetX / window.innerWidth) * 2 - 1;
  pointerCoord.y = -((offsetY / window.innerHeight) * 2 - 1);
});
// ...μƒλž΅...

μœ„ μ½”λ“œλŠ” ν¬μΈν„°μ˜ μ’Œν‘œλ₯Ό -1 κ³Ό 1 μ‚¬μ΄μ˜ κ°’μœΌλ‘œ μ •κ·œν™” μ‹œν‚€λŠ” μ½”λ“œμž…λ‹ˆλ‹€.
ν¬μΈν„°μ˜ x μ’Œν‘œλ₯Ό μΊ”λ²„μŠ€μ˜ λ„“μ΄λ‘œ λ‚˜λˆ„κ²Œ 되면, κ·Έ λ²”μœ„λŠ” 0 κ³Ό 1 사이가 λ©λ‹ˆλ‹€. 여기에 2 λ₯Ό κ³±ν•œ ν›„ 1 을 빼게되면 -1 κ³Ό 1 이 λ©λ‹ˆλ‹€.
y μ’Œν‘œμ— -1 을 κ³±ν•œ μ΄μœ λŠ” μœ„μͺ½ λ°©ν–₯이 μ–‘μˆ˜μ΄κΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€.

ray-caster-pointer-interact-1

이제 Ray λ₯Ό μ •κ·œν™”λœ μ’Œν‘œλ₯Ό ν–₯ν•˜λ„λ‘ λ§Œλ“€μ–΄μ•Ό ν•˜λŠ”λ°, μ΄λ•Œ μ‚¬μš©ν•˜λŠ” λ©”μ†Œλ“œκ°€ setFromCamera μž…λ‹ˆλ‹€. 이 ν•¨μˆ˜λŠ” μ‹œμž‘μ μ„ μΉ΄λ©”λΌμ˜ μœ„μΉ˜λ‘œ, λ°©ν–₯을 ν¬μΈν„°λ‘œ ν•˜λŠ” ray λ₯Ό μƒμ„±ν•˜λŠ” ν•¨μˆ˜μž…λ‹ˆλ‹€.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ...μƒλž΅...
let INTERSECTED = null;
function animate() {
  // ...μƒλž΅...
  raycaster.setFromCamera(pointerCoord, camera);
  const intersects = raycaster.intersectObjects(scene.children);

  if (intersects.length > 0) {
    if (INTERSECTED !== intersects[0].object) {
      if (INTERSECTED) INTERSECTED.material.color.set(INTERSECTED.currentColor);

      INTERSECTED = intersects[0].object;
      INTERSECTED.currentColor = new THREE.Color(INTERSECTED.material.color);
      INTERSECTED.material.color.set("yellow");
    }
  } else {
    if (INTERSECTED) {
      INTERSECTED.material.color.set(INTERSECTED.currentColor);
    }

    INTERSECTED = null;
  }
}
// ...μƒλž΅...

μœ„ μ½”λ“œλŠ” 포인터방ν–₯으둜 ν–₯ν•˜λŠ” ray 와 κ²ΉμΉ˜λŠ” mesh κ°€ μžˆμ„ λ•Œ, 색을 λ…Έλž€μƒ‰μœΌλ‘œ λ³€κ²½ν•˜κ³ , 포인터가 λ‹€λ₯Έ 물체λ₯Ό κ°€λ₯΄ν‚€κ±°λ‚˜, 아무것도 κ°€λ₯΄ν‚€μ§€ μ•Šμ„λ•Œ μ›λž˜μ˜ μƒ‰μœΌλ‘œ λŒμ•„κ°€κ²Œλ” λ™μž‘ν•˜λŠ” μ½”λ“œμž…λ‹ˆλ‹€.

πŸ“” 참고자료

κ΄‘μ„  νˆ¬μ‚¬ Raycaster

이 κΈ°μ‚¬λŠ” μ €μž‘κΆŒμžμ˜ CC BY 4.0 λΌμ΄μ„ΌμŠ€λ₯Ό λ”°λ¦…λ‹ˆλ‹€.

three.js μ™ΈλΆ€ λͺ¨λΈ 뢈러였기

React Query - κΈ°λ³Έμ„€μ • μ‚΄νŽ΄λ³΄κΈ°