커스텀 카메라와 컨트롤을 만들어 PointerLockControls 에 대해 OrbitControls 과 합치고 거기에 lerp 를 주어 조금더 자연스럽게 커스텀하기 위해 각각의 카메라와 컨트롤을 직접 만들어보자.
// custom orbit control 의 중심이 될 지점.
const pivot = new THREE.Object3D()
// 좌, 우
const yaw = new THREE.Object3D()
// 상, 하
const pitch = new THREE.Object3D()
즉, pivot 은 아래 사진의 가운데 점에 점을 찍으면 (0,1,0) 의 Vecotor3 를 갖는 지점에 OrbitControl 을 고정하겠다는 뜻이다.
따라서 중심, 좌우, 상하를 표시하기위한 각각의 변수를 생성한다.
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 을 수동으로 작성할 수 있다.
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)은 각도를 제한하기 위해 별도의 변수를 생성하여 관리한다.
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
)를 곱하여 적절한 값으로 치환한 다음 계산해준다.
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 의 요소일 때 모든 키, 마우스 이벤트에 작성한 함수들을 넣어준다.
앞서 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 에서 실행 될 함수 )에서 키가 해당 키가 눌렸으면 방향 혹은 속도를 설정하는 코드로 쓰였다.
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)
}
}
})