본문 바로가기
Web Development/ThreeJS

[Three.js / Javascript] ThreeJS 카메라 키보드 이동 구현하기(직접 구현)

by 감자맹고우 2022. 6. 8.
728x90
반응형

Three.js는 WebGL을 이용한다. 예전에 Direct 3D를 배웠기 때문에 OpenGL 사용 경험이 있는데, 그래서 그런지 친숙했다. 하지만 삼각함수, 보간 등 여러 가지 수학적 이론이나 계산이 어려워서 힘들었는데 ThreeJS도 기본 구현된 것에 커스텀으로 무언가 기능을 만들어 쓸려니 쉽지만은 않았다.

 

결국 프로젝트에 쓰기 위해, 카메라 이동을 처리하는 과정에서 머리가 복잡해지기 시작했다...

 

ThreeJS는 편의상 여러가지 화면 컨트롤을 제공한다.

흔히 사용하는 OrbitControl도 있지만 TrackballControl, FirstPersonControl, FlyControl, PointerLockControl 등이 제공된다. 이 중에서 FlyControl이나 PointerLockControl 등 다른 컨트롤에서 충분히 키보드 이동을 제공하고 있지만, 내 프로젝트에서 적합해보이는 것은 OrbitControl이었다. 그러나 OrbitControl의 기본 기능만으로는 화면 이동이 불편하여 키보드 이동을 직접 구현하여 추가해보기로 했다.

 

 

[ 생각의 흐름 ]

 

문제는 '어떤 방식으로 구현하는가' 였는데,

처음 생각했던 것은 W/S를 누를 때는 줌인/줌아웃, A / D를 누를 때는 오브젝트의 좌 / 우로 이동하는 방식이었다.

 

이를 위해 가장 처음 생각해본 것은 그냥 카메라를 특정 축으로 +나 -로 이동시키는 것이었다.

그러나 정말이지 문과스러운 생각이었다...

타겟도 고정에, 마우스 이동으로 타겟 지점이나 카메라의 위치를 변경시키면 의도와는 전혀 일치하지 않는 움직임이 발생한다.

 

두 번째는 오브젝트를 회전 또는 이동시키는 것이었다.

회전이나 이동 자체는 잘 되긴하지만 오브젝트 자체를 건드리기 때문에 변수가 발생한다. 특히 메쉬가 많은 경우 사라지는 경우도 발생하였다.

또한, 의도와도 전혀 다르다.

 

세 번째는 이론적으로는 좀 더 진보했다.

줌인/줌아웃은 카메라의 위치에 따라 Z축을 + / - 시켰고, A/D는 삼각함수를 이용해 처리해보기로 했다.

하지만 줌인/줌아웃은 카메라의 위치에 따라 많은 오작동이 발생했고,

회전은 각도를 제대로 처리하지못해 속도가 반영되지 않는 등 오작동이 발생했다.

 

그렇게 한참을 삼각함수, 구글링으로 씨름하다가 또다시 키보드 지원이 되는 다른 컨트롤을 만져보았는데,,,

아뿔싸! 생각의 오류를 발견했다.

 

애초에 카메라 키보드 이동은 고정된 타겟을 대상으로 진행되는 것이 아니다.

카메라는 전/후/좌/우로 이동할 뿐, 타겟을 확대/축소/회전하는 것이 아니었던 것이다...

 

반응형

 

[ 구현 방법 ]

 

생각이 바뀐 후에는 긴 말 필요없이, 바로 구현하기로 했다.

카메라 이동 기능 구현에 집중하기 위해, 사용된 변수 선언을 위한 정도의 설정을 제외하고 기본 설정은 생략하였음을 양해바란다.

 

/** 3d-loader.js **/
const CAMERA_SPEED = 5;

window.onload = function(){

    const scene = new THREE.Scene();
    const renderer = new THREE.WebGLRenderer({antialias: true, alpha: true});
    const container = document.querySelector('#container');
    container.appendChild(renderer.domElement);
    
    const controls = new OrbitControls(camera, renderer.domElement);
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);

    scene.add(camera);

    const keyController = new KeyController();

    function walk(target) {
    
        if(target != null) {
            //카메라 position
            const origpos = new THREE.Vector3().copy(camera.position);
            //타겟 position
            const targetpos = new THREE.Vector3().copy(controls.target);
            
            //방향 벡터(x2-x1, y2-y1, z2-z1)를 구하고 normalize
            const dir = new THREE.Vector3(origpos.x - targetpos.x, origpos.y - targetpos.y, origpos.z - targetpos.z).normalize();
            
            if(keyController.keys['KeyA']) {
            
                const newpos = new THREE.Vector3(
                    targetpos.x + (-dir.z * CAMERA_SPEED)
                    , targetpos.y
                    , targetpos.z + (dir.x * CAMERA_SPEED)
                );
                
                const camnewpos = new THREE.Vector3(
                    origpos.x + (-dir.z * CAMERA_SPEED)
                    , origpos.y
                    , origpos.z + (dir.x * CAMERA_SPEED)
                );
                
                //타겟 지점 변경
                controls.target.set(newpos.x, newpos.y, newpos.z);
                //카메라 위치 변경
                camera.position.set(camnewpos.x, camnewpos.y, camnewpos.z);
                
            } else if(keyController.keys['KeyD']) {
            
                const newpos = new THREE.Vector3(
                    targetpos.x + ( dir.z * guiData.CAMERA_SPEED )
                    , targetpos.y
                    , targetpos.z + ( -dir.x * guiData.CAMERA_SPEED )
                );

                const camnewpos = new THREE.Vector3(
                    origpos.x + ( dir.z * guiData.CAMERA_SPEED )
                    , origpos.y
                    , origpos.z + ( -dir.x * guiData.CAMERA_SPEED )
                );

                controls.target.set(newpos.x, newpos.y, newpos.z);
                camera.position.set(camnewpos.x, camnewpos.y, camnewpos.z);
                
            } else if(keyController.keys['KeyW']) {
            
                const newpos = new THREE.Vector3(
                    targetpos.x - (dir.x * guiData.CAMERA_SPEED)
                    , targetpos.y - (dir.y * guiData.CAMERA_SPEED)
                    , targetpos.z - (dir.z * guiData.CAMERA_SPEED)
                );

                const camnewpos = new THREE.Vector3(
                    origpos.x - (dir.x * guiData.CAMERA_SPEED)
                    , origpos.y - (dir.y * guiData.CAMERA_SPEED)
                    , origpos.z - (dir.z * guiData.CAMERA_SPEED)
                );
                
                controls.target.set(newpos.x, newpos.y, newpos.z);
                camera.position.set(camnewpos.x, camnewpos.y, camnewpos.z);
                
            } else if(keyController.keys['KeyS']) {
            
                const newpos = new THREE.Vector3(
                    targetpos.x + (dir.x * guiData.CAMERA_SPEED)
                    , targetpos.y + (dir.y * guiData.CAMERA_SPEED)
                    , targetpos.z + (dir.z * guiData.CAMERA_SPEED)
                );

                const camnewpos = new THREE.Vector3(
                    origpos.x + (dir.x * guiData.CAMERA_SPEED)
                    , origpos.y + (dir.y * guiData.CAMERA_SPEED)
                    , origpos.z + (dir.z * guiData.CAMERA_SPEED)
                );
                
                controls.target.set(newpos.x, newpos.y, newpos.z);
                camera.position.set(camnewpos.x, camnewpos.y, camnewpos.z);
                
            } else if(keyController.keys['KeyE']) {
            
                const newpos = new THREE.Vector3(
                    targetpos.x
                    , targetpos.y + (1 * guiData.CAMERA_SPEED)
                    , targetpos.z
                );

                const camnewpos = new THREE.Vector3(
                    origpos.x
                    , origpos.y + (1 * guiData.CAMERA_SPEED)
                    , origpos.z
                );

                controls.target.set(newpos.x, newpos.y, newpos.z);
                camera.position.set(camnewpos.x, camnewpos.y, camnewpos.z);
            
            } else if(keyController.keys['KeyQ']) {
            
                const newpos = new THREE.Vector3(
                    targetpos.x
                    , targetpos.y - (1 * guiData.CAMERA_SPEED)
                    , targetpos.z
                );

                const camnewpos = new THREE.Vector3(
                    origpos.x
                    , origpos.y - (1 * guiData.CAMERA_SPEED)
                    , origpos.z
                );

                controls.target.set(newpos.x, newpos.y, newpos.z);
                camera.position.set(camnewpos.x, camnewpos.y, camnewpos.z);
            
            }
            
            //컨트롤 업데이트
            controls.update();
            //카메라 업데이트
            camera.updateProjectionMatrix();
            
        }
        
    }

    const draw = () => {

        walk(targetObj);
    
        controls.update();
        renderer.render(scene, camera);
        renderer.setAnimationLoop(draw); //draw 함수 계속 호출

    }
    
    draw();
    
}


/** KeyController.js **/
export class KeyController {
    constructor() {
        this.keys = [];
        
        window.addEventListener('keydown', e => {
            //예 : this.keys['KeyW'] = true;
            this.keys[e.code] = true;
        });
        
        window.addEventListener('keyup', e => {
            delete this.keys[e.code];
        });
    }
}

 

원리는 현재 방향벡터를 구해서 방향은 그대로 유지하면서, 타겟지점과 카메라를 전/후/좌/우로 이동시키는 것이다. 카메라와 타겟지점간의 거리값도 그대로 유지되지만, 카메라가 보고 있는 타겟 지점과 카메라를 이동시켜서 자연스럽게 이동을 연출할 수 있다.

이런 식으로, 키보드로 카메라를 전(W) / 후(S) / 좌(A) / 우(D) 이동하는 기능을 구현할 수 있다!

추가로, E와 Q로는 UP/DOWN 기능을 구현하였으므로 많은 도움이 되었으면 좋겠다.

 

 

p.s.

코드 자체가 완벽하지는 않아서 일부 오류나 오작동이 있을 수 있다. Y좌표를 계산해주지 않아서 또는 식이 잘못된 부분이 있어서 발생하지 않을까 짐작하는데 해결하신 분이 있다면 공유해주시면 좋겠다.

 

 

p.s.s.

KeyController 는 아래의 강의를 참고하였으며, 메쉬를 직접 생성해서 구현하는 경우에는 해당 강의를 참고하면 좋다.

 

three.js로 시작하는 3D 인터랙티브 웹 - 인프런 | 강의

웹에서 3D를 구현할 수 있는 자바스크립트 라이브러리, three.js의 핵심 개념들을 익히고 응용 예제까지 만들어 보면서 3D 웹 개발에 대한 감을 익혀봅니다., - 강의 소개 | 인프런...

www.inflearn.com

 

그리고, NodeJS가 아닌 환경에서 module import/export 시에는 아래의 글을 참고하면 된다.

 

[Javascript / Spring Legacy / JSP] Spring Framework에서 자바스크립트 모듈 import 하는 방법 (without NodeJS)

오늘도 어김없이 스프링 프로젝트를 진행하는 도중, Javascript 라이브러리(three.js)를 쓸 일이 생겼다. 최근 Javascript 라이브러리는 NodeJS를 기본사양으로 하기 때문에 import/export로 모듈화하여 사용

devlifetestcase.tistory.com

 

물론, export로 처리할 필요없이 class로 처리해도 무방하다!

 

🤞 도움이 되셨기를 바랍니다. 한 번의 클릭과 댓글은 어딘가의 누군가에게 진실로 큰 힘이 됩니다. 🐱‍🏍

 

728x90
반응형

댓글