ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • TIL-2024.03.17 - 3js - 3D 케릭터 만들기 - 2. Raycaster
    > Frontend/ThreeJS 2024. 3. 18. 01:10

     

     

     

     

     

    목표:

     

    1.Raycaster 란?

    2.Raycaster 적용

     

     

     

     

    Raycaster 란?

     

    설명

    • 레이캐스터는 가상의 광선이 씬(Scene) 안에서 물체와 교차하는지를 감지하는데 사용.
    • 이 광선은 보통 카메라(Camera) 위치에서 특정 방향으로 뻗어나가는 것과 동일.

    사용처

    1. 마우스 클릭과 같은 상호작용 이벤트 처리:
      - 사용자가 마우스를 클릭하면, 레이캐스터를 사용하여 마우스 클릭 지점과 씬 안의 객체 간의 교차점을 찾기.
      - 이를 통해 사용자가 클릭한 객체를 감지하고 해당 객체에 대한 작업을 수행.
    2. 충돌 감지(Collision Detection):
      - 레이캐스터를 사용하여 특정 객체나 지역과 광선이 교차하는지를 감지.
      - 이는 게임이나 시뮬레이션 등에서 객체 간의 충돌을 감지하고 처리하는 데 사용.

     

    동작 원리

    1. 레이 생성: 시작점(보통 카메라 위치)과 방향을 지정하여 광선(레이)을 생성.
    2. 충돌 감지: 생성한 레이를 씬 안의 객체들과 비교하여 교차점을 찾음.
    3. 결과 반환: 교차한 객체나 지점에 대한 정보를 반환합니다. 이 정보를 사용하여 원하는 동작을 수행.

     

    공식문서

    https://threejs.org/docs/#api/ko/core/Raycaster 

     

     

    Raycaster 적용

    - 목적: 화면을 클릭한 경우, 클릭한 지점이 모델과 교차하는 경우 춤추는 액션을 수행

    /* Raycaster - 교차점 찾기 */
    const raycaster = new THREE.Raycaster(); // 레이캐스터를 생성합니다.
    const pointer = new THREE.Vector2(); // 포인터를 생성합니다.
    
    const clock = new THREE.Clock(); // 클락을 생성합니다.
    const render = () => { // 렌더 함수를 정의합니다.
        const delta = clock.getDelta(); // 시간 간격을 가져옵니다.
        mixer.update(delta); // 믹서를 업데이트합니다.
        renderer.render(scene, camera); // 씬을 렌더링합니다.
        requestAnimationFrame(render); // 다음 프레임을 요청합니다.
    };
    
    
    const handleResize = () => { // 리사이즈 이벤트 핸들러를 정의합니다.
        camera.aspect = window.innerWidth / window.innerHeight; // 카메라의 종횡비를 설정합니다.
        camera.updateProjectionMatrix(); // 카메라의 프로젝션 매트릭스를 업데이트합니다.
        renderer.setSize(window.innerWidth, window.innerHeight); // 렌더러의 크기를 조정합니다.
        renderer.render(scene, camera); // 새로 렌더된 내용을 반영합니다.
    };
    
    const handleFinished = () => { // 애니메이션 종료 이벤트 핸들러를 정의합니다.
        const previousAction = currentAction; // 이전 액션을 저장합니다.
        currentAction = mixer.clipAction(combatAnimations[0]) // 현재 액션을 설정합니다.
        previousAction.fadeOut(0.5); // 이전 액션을 페이드 아웃합니다.
        currentAction.reset().fadeIn(0.5).play(); // 현재 액션을 재생합니다.
    }
    
    const handlePointerDown = event => { // 포인터 다운 이벤트 핸들러를 정의합니다.
        pointer.x = (event.clientX / window.innerWidth - 0.5) * 2; // 포인터의 x 좌표를 설정합니다.
        pointer.y = -(event.clientY / window.innerHeight - 0.5) * 2; // 포인터의 y 좌표를 설정합니다.
        raycaster.setFromCamera(pointer, camera); // 레이캐스터를 카메라 위치에서 포인터 방향으로 설정합니다.
        const intersects = raycaster.intersectObjects(scene.children); // 씬의 객체들과 레이의 교차점을 찾습니다.
        const object = intersects[0]?.object; // 첫 번째 교차 객체를 가져옵니다.
        if (object.name === "Ch46") { // 객체의 이름이 "Ch46"인 경우
            const previousAction = currentAction; // 이전 액션을 저장합니다.
            const index = Math.round(Math.random() * (dancingAnimations.length - 1)); // 임의의 인덱스를 가져옵니다.
            currentAction = mixer.clipAction(dancingAnimations[index]); // 현재 액션을 설정합니다.
            currentAction.loop = THREE.LoopOnce; // 한 번만 재생하도록 설정합니다.
            currentAction.clapWhenFinished = true; // 종료 시 손뼉을 치도록 설정합니다.
    
            if (previousAction !== currentAction) { // 이전 액션이 현재 액션과 다른 경우
                previousAction.fadeOut(0.5); // 이전 액션을 페이드 아웃합니다.
                currentAction.reset().fadeIn(0.5).play(); // 현재 액션을 재생합니다.
            }
    
            mixer.addEventListener("finished", handleFinished) // 애니메이션 종료 이벤트를 처리합니다.
        }
    };
    
    render(); // 렌더 함수를 호출하여 렌더링을 시작합니다.
    window.addEventListener("resize", handleResize); // 리사이즈 이벤트를 처리합니다.
    window.addEventListener("pointerdown", handlePointerDown); // 포인터 다운 이벤트를 처리합니다.
    

     

     

     

     

     

    전체 코드: 

     

    import * as THREE from "three"; // Three.js 라이브러리를 가져옵니다.
    import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"; // GLTF 모델 로더를 가져옵니다.
    import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; // OrbitControls를 가져옵니다.
    
    window.addEventListener("load", () => { // 페이지가 로드되면 init 함수를 호출합니다.
        init();
    });
    
    
    const init = async () => { // 초기화 함수를 정의합니다.
    
        const renderer = new THREE.WebGL1Renderer({ // WebGL1Renderer를 생성합니다.
            antialias: true, // 안티앨리어싱을 활성화합니다.
        });
        renderer.outputColorSpace = THREE.SRGBColorSpace; // 출력 색상 공간을 sRGB로 설정합니다.
        renderer.setSize(window.innerWidth, window.innerHeight); // 렌더러의 크기를 창의 크기로 설정합니다.
        renderer.shadowMap.enabled = true; // 그림자 맵을 활성화합니다.
    
    
        document.body.appendChild(renderer.domElement); // 렌더러의 DOM 요소를 문서에 추가합니다.
        const scene = new THREE.Scene(); // 씬을 생성합니다.
    
        /* camera */
        const camera = new THREE.PerspectiveCamera(75, // 원근 카메라를 생성합니다.
            window.innerWidth / window.innerHeight,
            1,
            500,
        );
        camera.position.set(0, 5, 20); // 카메라의 위치를 설정합니다.
    
        /* OrbitControls */
        const controls = new OrbitControls(camera, renderer.domElement); // OrbitControls를 생성합니다.
        controls.enableDamping = true; // 덤프링을 활성화합니다.
        controls.minDistance = 15; // 카메라와의 최소 거리를 설정합니다.
        controls.maxDistance = 25; // 카메라와의 최대 거리를 설정합니다.
        controls.minPolarAngle = Math.PI / 4; // 카메라의 최소 고도를 설정합니다.
        controls.maxPolarAngle = Math.PI / 3; // 카메라의 최대 고도를 설정합니다.
    
        /* Progressive Bar Variables*/
        const progresssBar = document.querySelector("#progress-bar"); // 프로그레시브 바 요소를 가져옵니다.
        const progressBarContainer = document.querySelector("#progress-bar-container"); // 프로그레시브 바 컨테이너 요소를 가져옵니다.
    
        /*Loading Manager*/
        const loadingManager = new THREE.LoadingManager(); // 로딩 매니저를 생성합니다.
        loadingManager.onProgress = (url, loaded, total) => { // 로딩 진행률이 업데이트될 때마다 호출됩니다.
            progresssBar.value = (loaded / total) * 100; // 로딩 진행률을 업데이트합니다.
        };
        loadingManager.onLoad = () => { // 모든 리소스가 로드되면 호출됩니다.
            progressBarContainer.style.display = "none"; // 프로그레시브 바 컨테이너를 숨깁니다.
        };
    
        /*GLTF Loader*/
        const gltfLoader = new GLTFLoader(loadingManager); // GLTF 로더를 생성합니다.
        const gltf = await gltfLoader.loadAsync("./models/character.gltf"); // GLTF 모델을 비동기적으로 로드합니다.
        const model = gltf.scene; // GLTF 모델의 씬을 가져옵니다.
        model.scale.set(0.1, 0.1, 0.1); // 모델의 크기를 조정합니다.
        model.traverse(obj => { // 모델의 각 객체를 순회하면서
            if (obj.isPush) obj.castShadow = true; // 그림자를 생성할 객체에 그림자를 캐스팅합니다.
        });
        scene.add(model); // 씬에 모델을 추가합니다.
    
        camera.lookAt(model.position); // 카메라가 모델을 바라보도록 설정합니다.
    
        /* Plane Geometry*/
        const planeGeometry = new THREE.PlaneGeometry(10000, 10000, 10000); // 평면 지오메트리를 생성합니다.
        const planeMaterial = new THREE.MeshPhongMaterial({color: 0x000000}); // 평면 재질을 생성합니다.
        const plane = new THREE.Mesh(planeGeometry, planeMaterial); // 평면 메쉬를 생성합니다.
        plane.rotation.x = -Math.PI / 2; // 평면을 x축 기준으로 회전합니다.
        plane.position.y = -7.5; // 평면의 y 좌표를 설정합니다.
        plane.receiveShadow = true; // 그림자를 받도록 설정합니다.
        scene.add(plane); // 씬에 평면을 추가합니다.
    
    
        /*Light -hemiLight*/
        const hemiLight = new THREE.HemisphereLight(0xffffff, 0x333333); // 반구광을 생성합니다.
        hemiLight.position.set(0, 20, 10); // 빛의 위치를 설정합니다.
        scene.add(hemiLight); // 씬에 반구광을 추가합니다.
    
        /*Light - spotLight*/
        const spotLight = new THREE.SpotLight(0xffffff, 1.5, 30, Math.PI * 0.15, 0.5, 0.5); // 스포트라이트를 생성합니다.
        spotLight.position.set(0, 20, 0); // 빛의 위치를 설정합니다.
        spotLight.castShadow = true; // 그림자를 캐스팅하도록 설정합니다.
        spotLight.shadow.mapSize.width = 1024; // 그림자 맵의 너비를 설정합니다.
        spotLight.shadow.mapSize.height = 1024; // 그림자 맵의 높이를 설정합니다.
        spotLight.shadow.radius = 8; // 그림자의 흐림 정도를 설정합니다.
        scene.add(spotLight); // 씬에 스포트라이트를 추가합니다.
    
    
        let currentAction;
    
        /*Mixer*/
        // 애니메이션 믹서(Animation Mixer)는 Three.js에서 애니메이션을 제어하고 관리하는 객체.
        // 이 객체를 사용하면 3D 모델의 애니메이션을 부드럽게 재생하고 조작.
        // 애니메이션 믹서는 애니메이션 클립(ActionClip)과 함께 사용.
        // 애니메이션 클립은 모델의 각 부분에 대한 애니메이션 데이터를 포함하고 있습니다.
        // 예를 들어, 캐릭터의 다리가 걷는 애니메이션 클립, 팔이 흔들리는 애니메이션 클립 등이 존재.
        // 애니메이션 믹서는 이러한 애니메이션 클립을 받아서 재생하고, 두 개 이상의 애니메이션을 믹싱하거나, 특정 시간대에 애니메이션을 페이딩(fading)하는 등의 작업을 수행
        // 또한, 애니메이션 믹서는 각 애니메이션 클립에 대한 재생 상태, 시간, 속도 등을 관리.
        const mixer = new THREE.AnimationMixer(model); // 애니메이션 믹서를 생성합니다.
        const combatAnimations = gltf.animations.slice(0, 5); // 전투 애니메이션을 가져옵니다.
        const dancingAnimations = gltf.animations.slice(5); // 춤추는 애니메이션을 가져옵니다.
        const hasAnimation = gltf.animations.length !== 0; // 애니메이션이 있는지 확인합니다.
        if (hasAnimation) { // 애니메이션이 있는 경우
            currentAction = mixer.clipAction(gltf.animations[0]); // 현재 액션을 설정합니다.
            currentAction.play(); // 애니메이션을 재생합니다.
        }
    
        /* Button for combat animations */
        const buttons = document.querySelector(".actions"); // 액션 버튼 컨테이너를 가져옵니다.
        combatAnimations.forEach(animation => { // 각 전투 애니메이션에 대해 반복합니다.
            const button = document.createElement("button"); // 버튼을 생성합니다.
            button.innerText = animation.name; // 버튼의 텍스트를 설정합니다.
            button.addEventListener("click", () => { // 버튼에 클릭 이벤트를 추가합니다.
                const previousAction = currentAction; // 이전 액션을 저장합니다.
                currentAction = mixer.clipAction(animation); // 현재 액션을 설정합니다.
                if (previousAction !== currentAction) { // 이전 액션이 현재 액션과 다른 경우
                    previousAction.fadeOut(0.5); // 이전 액션을 페이드 아웃합니다.
                    currentAction.reset().fadeIn(0.5).play(); // 현재 액션을 재생합니다.
                }
            });
            buttons.appendChild(button); // 버튼을 액션 버튼 컨테이너에 추가합니다.
        });
    
    
        /* Raycaster - 교차점 찾기 */
        const raycaster = new THREE.Raycaster(); // 레이캐스터를 생성합니다.
        const pointer = new THREE.Vector2(); // 포인터를 생성합니다.
    
        const clock = new THREE.Clock(); // 클락을 생성합니다.
        const render = () => { // 렌더 함수를 정의합니다.
            const delta = clock.getDelta(); // 시간 간격을 가져옵니다.
            mixer.update(delta); // 믹서를 업데이트합니다.
            renderer.render(scene, camera); // 씬을 렌더링합니다.
            requestAnimationFrame(render); // 다음 프레임을 요청합니다.
        };
    
    
        const handleResize = () => { // 리사이즈 이벤트 핸들러를 정의합니다.
            camera.aspect = window.innerWidth / window.innerHeight; // 카메라의 종횡비를 설정합니다.
            camera.updateProjectionMatrix(); // 카메라의 프로젝션 매트릭스를 업데이트합니다.
            renderer.setSize(window.innerWidth, window.innerHeight); // 렌더러의 크기를 조정합니다.
            renderer.render(scene, camera); // 새로 렌더된 내용을 반영합니다.
        };
    
        const handleFinished = () => { // 애니메이션 종료 이벤트 핸들러를 정의합니다.
            const previousAction = currentAction; // 이전 액션을 저장합니다.
            currentAction = mixer.clipAction(combatAnimations[0]) // 현재 액션을 설정합니다.
            previousAction.fadeOut(0.5); // 이전 액션을 페이드 아웃합니다.
            currentAction.reset().fadeIn(0.5).play(); // 현재 액션을 재생합니다.
        }
    
        const handlePointerDown = event => { // 포인터 다운 이벤트 핸들러를 정의합니다.
            pointer.x = (event.clientX / window.innerWidth - 0.5) * 2; // 포인터의 x 좌표를 설정합니다.
            pointer.y = -(event.clientY / window.innerHeight - 0.5) * 2; // 포인터의 y 좌표를 설정합니다.
            raycaster.setFromCamera(pointer, camera); // 레이캐스터를 카메라 위치에서 포인터 방향으로 설정합니다.
            const intersects = raycaster.intersectObjects(scene.children); // 씬의 객체들과 레이의 교차점을 찾습니다.
            const object = intersects[0]?.object; // 첫 번째 교차 객체를 가져옵니다.
            if (object.name === "Ch46") { // 객체의 이름이 "Ch46"인 경우
                const previousAction = currentAction; // 이전 액션을 저장합니다.
                const index = Math.round(Math.random() * (dancingAnimations.length - 1)); // 임의의 인덱스를 가져옵니다.
                currentAction = mixer.clipAction(dancingAnimations[index]); // 현재 액션을 설정합니다.
                currentAction.loop = THREE.LoopOnce; // 한 번만 재생하도록 설정합니다.
                currentAction.clapWhenFinished = true; // 종료 시 손뼉을 치도록 설정합니다.
    
                if (previousAction !== currentAction) { // 이전 액션이 현재 액션과 다른 경우
                    previousAction.fadeOut(0.5); // 이전 액션을 페이드 아웃합니다.
                    currentAction.reset().fadeIn(0.5).play(); // 현재 액션을 재생합니다.
                }
    
                mixer.addEventListener("finished", handleFinished) // 애니메이션 종료 이벤트를 처리합니다.
            }
        };
    
        render(); // 렌더 함수를 호출하여 렌더링을 시작합니다.
        window.addEventListener("resize", handleResize); // 리사이즈 이벤트를 처리합니다.
        window.addEventListener("pointerdown", handlePointerDown); // 포인터 다운 이벤트를 처리합니다.
    
    };

    댓글

Designed by Tistory.