Never give up

Three js - mouse event(feat. Raycaster) 본문

Three js

Three js - mouse event(feat. Raycaster)

대기만성 개발자 2023. 4. 28. 22:18
반응형

이번에는 mesh에 이벤트를 넣어주는 작업을 해봤는데

 

html에서 사용하던 onClick이나 mouseEnter 같은 이벤트를 따로 걸어줄 수 없어서

 

eventListener를 걸어서 사용했고, 3d에서는 조금 더 복잡한 방법으로 사물의 위치를 추적해야되는거 같습니다

 

그래서 raycaster를 사용할텐데 자세한 내용은 아래 링크를 참고해주세요

(링크 : https://threejs.org/docs/#api/en/core/Raycaster)

 

화면부분은 간단한 div에 ref만 할당하는 부분으로 동일합니다

 

use_raycasting.ts

import { useEffect, useRef } from "react";
import * as Three from "three";

const useRaycasting = () => {
    const ref = useRef<HTMLDivElement>(null);

    const width = window.innerWidth;
    const height = window.innerHeight;

    const scene = new Three.Scene();

    let camera: Three.PerspectiveCamera;

    let light: Three.AmbientLight;

    let renderer: Three.WebGLRenderer;

    const raycaster = new Three.Raycaster();

    const mousePosition = new Three.Vector2();

    let intersects: Three.Intersection<Three.Object3D<Three.Event>>[] = [];

    let debounce: number | undefined;

    let hovered: any = null;

    useEffect(() => {
        window.addEventListener('mousemove', mouseMoveListener);

        window.addEventListener('click', clickListener);

        window.addEventListener('resize', handleResize);

        return () => {
            window.removeEventListener('mousemove', mouseMoveListener);

            window.removeEventListener('click', clickListener);

            window.removeEventListener('resize', handleResize);
        };
    }, []);

    useEffect(() => {
        if (ref.current !== null) {
            setRenderer();

            ref.current.appendChild(renderer.domElement);

            setCamaera();

            setLight();

            setMeshes();

            animate();
        }
    }, [ref]);

    const setRenderer = () => {
        renderer = new Three.WebGLRenderer();

        renderer.setSize(width, height);
    };

    const setCamaera = () => {
        camera = new Three.PerspectiveCamera(75, width / height, 0.1, 1000);

        camera.position.z = 5;
    };

    const setLight = () => {
        light = new Three.AmbientLight(0xffffff, 5);
        scene.add(light);
    };

    const setMeshes = () => {
        for (let i = 0; i < 3; i++) {
            const cube = makeCube();
            cube.position.set(2 - (i * 2), 0, 0);

            scene.add(cube);
        }
    };

    const makeCube = () => {
        const geometry = new Three.BoxGeometry(1, 1, 1);
        const material = new Three.MeshMatcapMaterial({ color: 0x00ff00 });
        return new Three.Mesh(geometry, material);
    };

    const animate = () => {
        requestAnimationFrame(animate);
        renderer.render(scene, camera);
    };

    const mouseMoveListener = (e: MouseEvent) => {
        clearTimeout(debounce);

        debounce = setTimeout(() => {
            mousePosition.set((e.clientX / width) * 2 - 1, -(e.clientY / height) * 2 + 1);
            raycaster.setFromCamera(mousePosition, camera);

            intersects = raycaster.intersectObjects(scene.children);

            if (intersects.length > 0) {
                hovered = intersects[0].object as any;

                hovered.material.color.set('#229922');
            } else {
                if (hovered !== null) {
                    hovered.material.color.set('#00ff00');
                }
            }
        }, 10);
    };

    const clickListener = (e: MouseEvent) => {
        if (intersects.length > 0) {
            const selectedCube = intersects[0].object;

            const isUpScailed = selectedCube.scale.x === 1.1;

            selectedCube.scale.setScalar(isUpScailed ? 1.0 : 1.1);
        }
    };

    const handleResize = () => {
        if (ref !== null) {
            const width = ref.current!.clientWidth;
            const height = ref.current!.clientHeight;

            renderer.setSize(width, height);
            camera.aspect = width / height;

            camera.updateProjectionMatrix();
        };
    };

    return { ref };
};

export default useRaycasting;

 

useEffect에서 mouse move, click event에 등록해주고

 

먼저 mouseMove할 때 위치 셋팅하게 되는데

 

마우스 좌표의 범위가 -1에서 1까지이기 때문에 화면에 위치한 마우스의 좌표값에 따른 계산을 해줘야됩니다

 

계산이 왜 이렇게 되는지 간단하게 표현해보자면

(만든애들이 이렇게 하라고해서...)

 

< 필자의 발만도 못한 손그림 >

화면을 그림과 같이 2등분 하고, 마우스 위치에 따라 -1에서 1까지 표현을 할 때

 

마우스의 위치 / 화면의 넓이 * 2 (여기까지가 2등분을 하고) - 1을 해주면

 

A영역에 있을 때는 음수(최소 -1), B영역에 있을 때는 양수(최대 1) 까지 표현 가능합니다

 

그 후 raycast에 마우스의 위치와 카메라가 방향을 같이 넣어줍니다

 

raycasting에 대해 자세히 알고싶으신 분들을 위해 링크 남겨드립니다

(링크 : https://en.wikipedia.org/wiki/Ray_casting)

 

그 후 raycaster가 포착(?)한 object들을 배열에 넣어주고

 

클릭할 때 scale 변경, hover일 때 색상 변경을 넣어주는 코드를 만들어봤습니다

 

hover할 때 성능을 조금 개선해보고자 debounce를 넣어봤는데

 

나중에 조금 더 거대한(?) 사물에 이벤트를 넣을 때 도움이 되지않을까 하는 헛된 상상을 하고 있습니다

 

그리고 ts 라이브러리 만드신분이 interface에 따로 material을 안넣어두셔서 any로 캐스팅을 해줬습니다

 

< 마우스 이벤트 발생 화면 >

사실 따로 라이브러리를 사용 안해서 코드가 길어지고 복잡도도 높아지고 있는데

 

웬만하면 이미 자주 사용하는 기능들이 만들어진 react three fiber같은 좋은 라이브러리가 있으니

 

필자처럼 삽질안해보셔도 됩니다

 

필자는 왜 삽질하는지 궁금하신 분들이 있으실텐데

(사실 저도 잘 모르겠습니다 왜 굳이 안해도 되는 노력을 하고 있는지..)

 

다음에 포스팅 예정중인 3d model부분을 적용해보고 나서

 

바로 사용해볼 예정입니다

반응형

'Three js' 카테고리의 다른 글

Three js - Fiber example  (0) 2023.06.03
Three js - GLTF example  (0) 2023.05.07
Three js - gauge example  (0) 2023.04.08
Three js - tutorial  (0) 2023.04.08
Comments