[Three.js journey 강의노트] 19

9rganizedChaos·2021년 7월 9일
3
post-thumbnail

🙌🏻 해당 글은 Three.js Journey의 강의 노트입니다.

19 Raycaster

Raycaster는 기본적으로 특정 방향으로 광선을 쏘고 어떤 물체가 해당 광선과 교차하는지 테스트할 수 있는 것을 말한다. 게임에서 플레이어 앞에 어떤 벽이 있는지, 레이저 총이 무엇을 명중했는지, 마우스 아래에 현재 무엇이 있는지 감지 하는 것 등이 결국 모두 Raycasting이다. 넓은 의미로 본다면 three.js에서 어떻게 인터랙션을 구성하는가와 맥을 공유한다고 할 수도 있겠다.

Create the Raycaster

너무 다행스럽게도 raycaster는 클래스로 작성되어 있어, 별다른 복잡한 코드 없이 인스턴스화할 수 있다. 기본 설정을 위해 set함수를 사용한다. 첫 번째 매개변수로는 origin의 위치, 그리고 두 번째 매개변수로는 나아가는 방향을 받는다. 둘다 Vector3이지만 normalize()메서드를 쓰면 복잡하지 않게 사용할 수 있다.

/**
 * Raycaster
 */
const raycaster = new THREE.Raycaster()
const rayOrigin = new THREE.Vector3(- 3, 0, 0)
const rayDirection = new THREE.Vector3(10, 0, 0)
rayDirection.normalize()

raycaster.set(rayOrigin, rayDirection)

scene.add(new THREE.ArrowHelper(raycaster.ray.direction, raycaster.ray.origin, 300, 0x00ff00) );

raycaster를 눈으로 확인하고 싶다면, 두 가지 방법이 있다.
1) 해당 경로에 LineBasicMaterial을 만들어준다.
2) ArrowHelper를 이용한다 (이게 간편하다고 생각한다!)

Cast a ray

ray가 교차하는 Object를 담은 객체를 얻으려면 intersectObject()intersectObjects()를 이용하면 된다! 둘의 차이는 단수와 복수 차이!아래와 같이 console.log를 찍어보면 이미지와 같이 콘솔이 찍히는 것을 확인할 수 있다.

const intersect = raycaster.intersectObject(object2)
console.log(intersect)

const intersects = raycaster.intersectObjects([object1, object2, object3])
console.log(intersects)

Result of an intersection

하나의 개체만 테스트하더라도 교차의 결과는 항상 배열로 출력된다. 도넛 혹은 구불구불한 모양의 경우 광선이 같은 물체를 여러분 통과할 수 있기 때문이다. 리턴된 배열안에는 많은 유용한 정보들이 담겨있다.

distance: ray의 오리진부터 충돌 포인트까지의 거리
face: geometry의 어떤 면이 ray에 의해 부딪혔는지
faceIndex: 해당 face의 index
object: 어떤 object가 충돌과 관련되어 있는지
point: 충돌 지점의 Vector3 정보
uv: 해당 geometry의 UV 좌표

(근데 왜 위 이미지에서 distance가 모두 2.5가 찍힐까)

Test on each frame

만일 애니메이션이 적용된, 움직이는 Object들에 대해 raycasting을 하고 싶다면, 각 프레임에서 테스트를 수행해야 한다. 우선 현재 화면에 보이는 구체에 애니메이션을 적용해준다. 그리고 나서 ray를 드리워 주고, raycaster와 만나는 objects를 intersects 변수에 담아준다.

const clock = new THREE.Clock()

const tick = () =>
{
    const elapsedTime = clock.getElapsedTime()

    // Animate objects
    object1.position.y = Math.sin(elapsedTime * 0.3) * 1.5
    object2.position.y = Math.sin(elapsedTime * 0.8) * 1.5
    object3.position.y = Math.sin(elapsedTime * 1.4) * 1.5
  
      // Cast a ray
    const rayOrigin = new THREE.Vector3(- 3, 0, 0)
    const rayDirection = new THREE.Vector3(1, 0, 0)
    rayDirection.normalize()

    raycaster.set(rayOrigin, rayDirection)

    const objectsToTest = [object1, object2, object3]
    const intersects = raycaster.intersectObjects(objectsToTest)
    console.log(intersects)

    // ...
}

이제 objectsToTest에 담긴 세 개의 구체의 색상을 빨간색으로 설정해준다.
그리고나서 intersects를 순회하며, 안에 담긴 object들의 색상만 파란색으로 변경해준다.

    for(const object of objectsToTest)
    {
        object.material.color.set('#ff0000')
    }

    for(const intersect of intersects)
    {
        intersect.object.material.color.set('#0000ff')
    }

ray를 지날 때마다 intersects 배열의 요소의 갯수가 바뀌는 것을 볼 수 있다.

Use the raycaster with the mouse

raycaster를 사용하면, Object가 마우스 뒤에 있는지도 테스트 할 수 있다. 계산을 하려면 카메라에서 마우스 방향으로 광선을 투사해주어야 하기 때문에 복잡해지지만, Three.js는 이 일들을 알아서 해줍니다.

Hovering

일단 마우스 좌표가 필요하다. 일단 자바스크립트에서 기본 마우스 좌표는 픽셀로 설정되어 있기 때문에 이걸 이용할 수는 없다. 우리는 수평축, 수직축 모두 -1에서 1사이의 값이 필요하다.

/**
 * Mouse
 */
const mouse = new THREE.Vector2()

window.addEventListener('mousemove', (event) =>
{
    mouse.x = event.clientX / sizes.width * 2 - 1
    mouse.y = - (event.clientY / sizes.height) * 2 + 1

    console.log(mouse)
})

아래 이미지와 같이 콘솔값이 잘 찍히는 것을 확인할 수 있다.

광선을 올바른 방향으로 향하게 하기 위해 우리는 setFromCamera 메서드를 사용할 수 있다. raycaster의 origin을 마우스로, direction을 카메라로 설정해주면, 마우스를 기준으로 ray를 cast해주게 되고, 결국 마우스가 호버됨에 따라 object정보를 받아줄 수 있게 된다.

raycaster.setFromCamera(mouse, camera)

    // ray에 교차하는 object들의 색상을 파란색으로 교체!
    for(const intersect of intersects)
    {
        intersect.object.material.color.set('#0000ff')
    }

    // 모든 object들을 순회하면서 교차하지 않으면 빨간색으로 교체!
    for(const object of objectsToTest)
    {
        if(!intersects.find(intersect => intersect.object === object))
        {
            object.material.color.set('#ff0000')
        }
    }

Mouse enter and mouse leave events

Three.js에서 'mouseenter', 'mouseleave'등의 이벤트는 지원되지 않는다. 이를 구현해주기 위해서는 조금 다른 접근 방식이 필요하다. 직전에서 구현한 마우스 오버를 이용할 것이다. 만일 직전 구현한 방식을 기준으로 raycaster에 교차하는 object가 없었다가 생기면 이는 곧 mouseenter와 마찬가지입니다. 반대로 raycaster에 교차하는 object가 있었다가 사라지면 이는 곧 mouseleave입니다. 우리가 해야 할 것은 현재 교차하는 객체를 저장하는 것이다.

let currentIntersect = null

const tick = () =>
{
    // ...
    raycaster.setFromCamera(mouse, camera)
    const objectsToTest = [object1, object2, object3]
    const intersects = raycaster.intersectObjects(objectsToTest)

    // 교차하는 Object가 있는데, currentIntersect에 저장된 것이 없으면, mouse가 들어온 것! currentIntersect를 저장해준다.
    if(intersects.length)
    {
        if(!currentIntersect)
        {
            console.log('mouse enter')
        }

        currentIntersect = intersects[0]
    }
  // 교차하는 것이 없는데 currentIntersect에 저장된 것이 있으면, mouse가 떠난 것! currentIntersect를 다시 null로 바꾸어준다.
    else
    {
        if(currentIntersect)
        {
            console.log('mouse leave')
        }

        currentIntersect = null
    }

    // ...
}

Mouse click event

이제 이 상태에서는 클릭 이벤트를 쉽게 구현해줄 수 있다. 우선 클릭이 어디서 일어나든 감지할 수 있도록 window에 클릭이벤트를 먹여준다. 그리고 enter, leave에서 구현해둔 currentIntersect를 이용한다. 그리고 currentIntersect안에 들어있는 object에 따라 switch문 분기를 나누어준다!

window.addEventListener('click', () =>
{
    if(currentIntersect)
    {
        switch(currentIntersect.object)
        {
            case object1:
                console.log('click on object 1')
                break

            case object2:
                console.log('click on object 2')
                break

            case object3:
                console.log('click on object 3')
                break
        }
    }
})

profile
부정확한 정보나 잘못된 정보는 댓글로 알려주시면 빠르게 수정토록 하겠습니다, 감사합니다!

2개의 댓글

comment-user-thumbnail
2023년 2월 8일

큰 도움이 되었습니다 감사합니다.

답글 달기
comment-user-thumbnail
2023년 3월 23일

적어도 한번 render가 발생한 이후에 intersection을 수행 해야 distance가 제대로 출력되네요

render 되기전에는 오브젝트들이 원점에 있는것으로 취급되는것 같구요

답글 달기