Three.js Custom follow up camera

강정우·2025년 1월 16일
0

three.js

목록 보기
23/24
post-thumbnail

Custom follow up camera, Custom pointlock control

커스텀 카메라와 컨트롤을 만들어 PointerLockControls 에 대해 OrbitControls 과 합치고 거기에 lerp 를 주어 조금더 자연스럽게 커스텀하기 위해 각각의 카메라와 컨트롤을 직접 만들어보자.

1. Custom Camera 를 만들기 위한 변수 생성

// custom orbit control 의 중심이 될 지점.
const pivot = new THREE.Object3D()
// 좌, 우
const yaw = new THREE.Object3D()
// 상, 하
const pitch = new THREE.Object3D()

즉, pivot 은 아래 사진의 가운데 점에 점을 찍으면 (0,1,0) 의 Vecotor3 를 갖는 지점에 OrbitControl 을 고정하겠다는 뜻이다.

따라서 중심, 좌우, 상하를 표시하기위한 각각의 변수를 생성한다.

2. Hierarchy 설정

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100)
camera.position.set(0, 0, 4)

... // 1.번의 변수 생성 외 기타 renderer, light 등 기본적인 코드 작성.

scene.add(pivot)
pivot.add(yaw)
yaw.add(pitch)
pitch.add(camera)

즉, 계층 설정을 해줌으로써 추후 Car 객체에 pivot 만 넣어도 yaw, pitch, camera 가 모두 자식으로 붙어있으니 pivot 을 설정을 하면 카메라 위치도 함께 수정된다. 이를 통하여 추후 OrbitControl 을 수동으로 작성할 수 있다.

2. 마우스 상하, 좌우 move 이벤트 생성

function onDocumentMouseMove(e: MouseEvent) {
    yaw.rotation.y -= e.movementX * 0.002
    const v = pitch.rotation.x - e.movementY * 0.002

    // pitch 의 한계를 설정하는 부분이다.
    if (v > -1 && v < 0.1) {
        pitch.rotation.x = v
    }
}

실제로 마우스가 욺직인 만큼, 카메라를 욺직이면 굉장히 큰 값이 이동하기 때문에 0.002 를 곱하여 굉장히 작은 값을 부여한다.
또한 pitch 값(Vector3 중 x)은 각도를 제한하기 위해 별도의 변수를 생성하여 관리한다.

3. 마우스 스크롤 이벤트 생성

function onDocumentMouseWheel(e: WheelEvent) {
  	// 원래의 스크롤을 막고
    e.preventDefault()
  	
  	// 원, 근을 설정하는 부분이다.
    const v = camera.position.z + e.deltaY * 0.005

    // limit range
    if (v >= 1 && v <= 10) {
        camera.position.z = v
    }
}

e.preventDefault() 로 원래 스크롤 이벤트를 막고 zoomLv 역시 마우스 delta 의 값에 작은 수(여기선 0.005)를 곱하여 적절한 값으로 치환한 다음 계산해준다.

4. 키 맵핑

const keyMap: { [key: string]: boolean } = {}

const onDocumentKey = (e: KeyboardEvent) => {
    keyMap[e.code] = e.type === 'keydown'
}

onDocumentKey 는 keyMap의 해당 키 코드에 대해 키 타입을 확인하여 키가 눌려 있으면 true, 떼어졌으면 false로 설정하였다.

document.addEventListener('click', () => {
    renderer.domElement.requestPointerLock()
})

화면을 클릭하면 requestPointerLock() 함수를 사용하여 마우스를 중앙 고정 후 숨겨준다.

document.addEventListener('pointerlockchange', () => {
    if (document.pointerLockElement === renderer.domElement) {
        document.addEventListener('keydown', onDocumentKey)
        document.addEventListener('keyup', onDocumentKey)

        renderer.domElement.addEventListener('mousemove', onDocumentMouseMove)
        renderer.domElement.addEventListener('wheel', onDocumentMouseWheel)

    } else {
        document.removeEventListener('keydown', onDocumentKey)
        document.removeEventListener('keyup', onDocumentKey)

        renderer.domElement.removeEventListener('mousemove', onDocumentMouseMove)
        renderer.domElement.removeEventListener('wheel', onDocumentMouseWheel)
    }
})

그리고 Point Lock 2.0 API 를 사용하여 현재 pointerLock 된 대상 요소가 renderer 의 요소일 때 모든 키, 마우스 이벤트에 작성한 함수들을 넣어준다.

5. pivot 설정

앞서 pivot 은 카메라의 중심이라고 말 했다. 따라서 자동차를 따라다며 보여줘야하기에 자동차 객체에 pivot 을 부여한다.

const car = new Car(keyMap, pivot)

이는 바로 앞 Impulse Joint 포스팅의 자동차 객체 생성의 속성값으로 들어갔다.

그리고 Car Class 에서는 자체적으로 followTarget 을 Object3D 로 설정한다.

export default class Car {
  	...
    followTarget = new Object3D()
    v = new Vector3()
    keyMap: { [key: string]: boolean }
    pivot: Object3D

    constructor(keyMap: { [key: string]: boolean }, pivot: Object3D) {
        this.followTarget.position.set(0, 1, 0)
        this.lightLeftTarget.position.set(-0.35, 1, -10)
        this.lightRightTarget.position.set(0.35, 1, -10)
        this.keyMap = keyMap
        this.pivot = pivot
    }

    async init(scene: Scene, world: World, position: [number, number, number]) {
        await new GLTFLoader().loadAsync('models/sedanSports.glb').then((gltf) => {
            const carMesh = gltf.scene.getObjectByName('body') as Group
            carMesh.position.set(0, 0, 0)
            carMesh.traverse((o) => {
                o.castShadow = true
            })

            carMesh.add(this.followTarget)

그리고 이를 Car 객체를 생ㅅ어하는 과정에서 carMesh 에 followTarget 을 자식으로 추가해준다.

update(delta: number) {
  for (let i = 0, n = this.dynamicBodies.length; i < n; i++) {
    this.dynamicBodies[i][0].position.copy(this.dynamicBodies[i][1].translation())
    this.dynamicBodies[i][0].quaternion.copy(this.dynamicBodies[i][1].rotation())
  }

  this.followTarget.getWorldPosition(this.v)
  // lert 함수로 pivot 에 대하여 부드럽게 
  this.pivot.position.lerp(this.v, delta * 5)

  // 속도 조절 함수
  let targetVelocity = 0
  if (this.keyMap['KeyW']) {
    targetVelocity = 500
  }
  if (this.keyMap['KeyS']) {
    targetVelocity = -200
  }
  (this.wheelBLMotor as PrismaticImpulseJoint).configureMotorVelocity(targetVelocity, 2.0);
  (this.wheelBRMotor as PrismaticImpulseJoint).configureMotorVelocity(targetVelocity, 2.0);

  // 방향 조절 함수
  let targetSteer = 0
  if (this.keyMap['KeyA']) {
    targetSteer += 0.6
  }
  if (this.keyMap['KeyD']) {
    targetSteer -= 0.6
  }
  (this.wheelFLAxel as PrismaticImpulseJoint).configureMotorPosition(targetSteer, 100, 10);
  (this.wheelFRAxel as PrismaticImpulseJoint).configureMotorPosition(targetSteer, 100, 10);
}


위 사진은 굉장히 빠른 속도로 후진 중인데 기존에 차체 중심에 있던 pivot 이 lerp() 로 인하여 뒤로 밀려나 있는 것을 알 수 있다.

또한 위 4. 키 맵핑 에서 작성된 keyMap 은 생성자 함수로 넘어가 update 함수( animate 에서 실행 될 함수 )에서 키가 해당 키가 눌렸으면 방향 혹은 속도를 설정하는 코드로 쓰였다.

.getWorldPosition()

const v = new Vector3()
let positions: number[] = []
carMesh.updateMatrixWorld(true) // 월드 변환 행렬 갱신
carMesh.traverse((o) => {
    if (o.type === 'Mesh') {
        const positionAttribute = (o as Mesh).geometry.getAttribute('position')
        for (let i = 0, l = positionAttribute.count; i < l; i++) {
            v.fromBufferAttribute(positionAttribute, i)
            v.applyMatrix4((o.parent as Object3D).matrixWorld)
            positions.push(...v)
        }
    }
})
profile
智(지)! 德(덕)! 體(체)!

0개의 댓글