🙌🏻 해당 글은 Three.js Journey의 강의 노트입니다.
우리는 이미 이전 레슨에서 PerspectiveCamera
에 대해서 다룬 바 있다. ThreeJS 공식문서를 살피면, 더 다양한 종류의 카메라가 있다는 걸 알 수 있다. ThreeJS에서 Camera는 기본적으로 추상클래스이다. 때문에 Camera를 바로 사용하는 일은 없고, Camera를 상속받은 클래스들을 통해 카메라의 속성과 메서드를 이용하게 된다.
ArrayCamera
: 여러 대의 카메라를 활용해 scene을 여러 차례 렌더해야 할 때 사용하는 카메라StereoCamera
: 스테레오 카메라는 사람의 시점을 흉내내는 카메라이다. 카메라 두 대를 통해 사람의 눈을 모사해, 깊이감을 조성하고 이를 통해, parallaxa 효과 등을 만들 수 있다. 물론 이 결과를 제대로 보기 위해서는 VR headset과 같은 장치들이 필요하다.CubeCamera
: 큐브카메라는 상하좌우전후를 각각 렌더할 때 사용된다. 대표적으로 environment map이나 shadow map을 위해 활용되는데 이는 추후에 다루도록 할 것이다.OrthographicCamera
: OrthographicCamera는 시점이나 공간감, 원근감을 가능한 배제한 형태를 보여주는 카메라이다. 오브젝트와 카메라 사이의 거리가 얼마나되는지와 무관하게 모든 요소들이 같은 사이즈로 렌더된다.PerspectiveCamera
: Orthographic 카메라와 반대로 시점이 반영되어 공간감과 원근감을 느낄 수 있는 모습을 렌더한다.Perspective Camera를 인스턴스화하기 위해서는 몇 가지 파라미터들을 넘겨주어야 한다.
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 1, 100)
각각의 파라미터들이 어떤 역할을 하는지 알아보자.
첫 번째 인자는 Field of view인데, 이는 카메라 뷰의 수직 각도 너비에 해당한다. 앵글이 작아지면 망원렌즈의 카메라처럼 보이게 되고, 앵글이 넓어지면, 카메라에 담기는 것이 많기 때문에 어안효과를 얻을 수 있게 된다다. 보통 45에서 75정도의 각도를 사용하면 적절한 결과를 얻을 수 있다.
const camera = new THREE.PerspectiveCamera(25, sizes.width / sizes.height, 1, 100)
const camera = new THREE.PerspectiveCamera(45, sizes.width / sizes.height, 1, 100)
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 1, 100)
width를 height로 나눈 값, Aspect ratio, 종횡비. 보통은 ThreeJS를 렌더하는 캔버스의 종횡비를 쓰지만, 늘 그러한 것은 아니다. Aspect Ratio는 여러 곳에서 활용하게 되므로, 가급적 sizes라는 객체를 만들어두고 활용하는 것을 추천한다.
const sizes = {
width: 800,
height: 600
}
Near and far는 카메라가 얼마나 멀리까지 볼 수 있는지, 그리고 얼마나 가까이에 있는 물체까지 볼 수 있는지를 결정해주는 인자라고 이해하면 된다. 범위 밖에 존재하는 오브젝트들은 아예 렌더되지 않는다. 애초에 카메라가 얼마나 가까이에 있는 물체까지만 인지할 것인지, 어느 정도 거리까지의 물체들까지만 렌더할지를 설정해줄 수 있는 것이다. 0.1에서 100사이의 합리적인 숫자 내에서 해당 값들을 설정해볼 수 있다. 이 인자는 z-fighting 버그를 방지하기 위해 유용한 인자이다.
현재 코드에서 다음과 같이 콘솔을 찍어서 오브젝트와 카메라 사이의 거리를 측정해보자!
console.log(mesh.position.distanceTo(camera.position))
// 3.4641016151377544
3.4641016151377544
라는 값이 출력된다.
그리고 near가 3이면 아래와 같은 결과를 낳게 된다.
남은 코스를 진행하는 동안 Orthographic Camera에 대해서 다루지는 않을 것이지만, 이 카메라의 경우 특정 프로젝트들에 아주 유용하게 활용될 수 있다. 위에서 언급한 바와 같이 Orthographic Camera는 시점이 배제된 카메라라고 이해하면 된다. 원근감이 배제되어 있는 것이다.
const camera = new THREE.OrthographicCamera(- 1, 1, 1, - 1, 0.1, 100)
Orthographic Camera에 넘겨주는 매개변수는 Perspective Camera와는 아주 다른데, 우선 field of view 대신 카메라각 각 방향으로 얼마나 멀리까지 볼 수 있는지를 알려주어야 한다. 그리고 나서 near과 far 변수를 전달한다.
원근감이 없어진 것은 확인하였으나, 큐브가 위아래로 조금 납작해져있는 점을 발견할 수 있다.
우리가 OrthographicCamera에 넘겨준 첫 네 인자들은 결국, 정방형 공간을 렌더하되 정방형 공간이 우리의 캔버스에 맞게 늘려지도록 하는 것을 의미한다. 때문에 다음과 같이 코드를 약간 수정해주자!
const aspectRatio = sizes.width / sizes.height
const camera = new THREE.OrthographicCamera(- 1 * aspectRatio, 1 * aspectRatio, 1, - 1, 0.1, 100)
이제부터는 마우스를 통해서 카메라를 조종해볼 것이다. 먼저 JS의 mousemove
이벤트를 통해서 마우스의 좌표를 알아낼 것이다.
window.addEventListener('mousemove', (event) =>
{
console.log(event.clientX, event.clientY)
})
JS에서 제공해주는 값 그대로 사용해도 되지만, 해당 레슨에서는 값을 1
이라는 크기의 표준값을 설정하고 값을 변환해서 사용하는 것을 추천한다.
예를 들어, 마우스 커서가 중앙에 있을 때는 0, 오른쪽으로 이동했을 때는 0.5, 왼쪽으로 이동했을 떄는 -0.5와 같은 값이 출력되도록 변환하는 것이다.
const cursor = {
x: 0,
y: 0
}
window.addEventListener('mousemove', (event) => {
cursor.x = event.clientX / sizes.width - 0.5
cursor.y = - (event.clientY / sizes.height - 0.5)
// ThreeJS에서와 브라우저에서 y축을 음양의 방향이 서로 다르므로 -1을 곱해준다.
console.log(cursor.x, cursor.y)
})
위와 같이 마우스 커서의 위치값을 알아내고 나면, 이 값들을 tick함수에 적용해주도록 한다.
const tick = () => {
// ...
camera.position.x = cursor.x
camera.position.y = cursor.y
// ...
}
위와 같이 코드를 수정해주면 아래와 같은 결과를 얻을 수 있게 된다!
여기에 lookAt(...)
메서드를 추가해주면 카메라의 각도도 조절해줄 수 있다!
const tick = () =>{
// ...
camera.position.x = cursor.x * 5
camera.position.y = cursor.y * 5
camera.lookAt(mesh.position)
// ...
}
Math.sin(...)
과 Math.cos(...)
를 이용하면, 오브젝트를 중심으로 카메라를 회전시킬 수도 있다. full rotation을 위한 "tau"라는 값이 존재하지만, 이는 JS에서 지원하지않으므로 우리는 sin과 cos를 조합해서 카메라의 Full Rotation을 시도할 것이다.
const tick = () => {
// ...
camera.position.x = Math.sin(cursor.x * Math.PI * 2) * 2
camera.position.z = Math.cos(cursor.x * Math.PI * 2) * 2
camera.position.y = cursor.y * 3
camera.lookAt(mesh.position)
// ...
}
tick()
지금까지 어떻게 카메라를 컨트롤할 수 있는지 알아보았다. 하지만 사실 ThreeJS는 기본적으로 카메라를 컨트롤하기 위한 몇 가지 클래스들을 내장하고 있다. 빌트인 컨트롤에 대해서 마저 알아보자!
공식문서를 살피면 ThreeJS는 이미 아주 많은 Pre-made 컨트롤들을 제공하고 있음을 알 수 있다. 우리는 Orbit Control에 대해서만 알아볼 것이지만 다양한 컨트롤들을 간략하게나마 알아보고 넘어가자.
DeviceOrientationControls
: VR experience를 조성하기 위해 활용가능한 컨트롤. 디바이스를 자동으로 검색하고 OS나 브라우저가 허용한다면 그에 따라 카메라를 움직여준다. (현재는 공식문서에서 검색되지 않는걸로 보아 지원하지 않는 것으로 보인다.)FlyControls
: 우주선에 탄듯한 카메라의 무빙을 가능하게 하는 컨트롤. 앞뒤로 이동 가능하며, xyz 세 축 모두로 회전이 가능하다.FirstPersonControls
: FlyControl과 비슷하다, 그러나 축이 고정되어있다. barrel roll이 불가능한 새의 시점이라고 생각하면 된다.PointerLockControls
: JS의 pointer lock API를 활용한 컨트롤이다. 커서를 숨긴 상태로 가운데에 고정시킨 다음, mousemove 이벤트의 콜백을 통해 그 움직임을 계속해서 감지한다. FPS게임을 만들기에 적합하다.OrbitControls
: 이전에 직접 만든 컨트롤과 가장 비슷한 형태의 컨트롤. 마우스 좌측 클릭으로 카메라 회전이 가능하며, 우클릭으로 수직 수평이동이 가능하고, 휠을 통해 줌이 가능하다.TrackballControls
: 수직 앵글에 제한이 없는 OrbitControls. scene의 위아래가 바뀌어도 여전히 회전할 수 있다.TransformControls
: 특별히 카메라를 컨트롤 할 수는 없고, 물체를 움직이기 위한 gizmo를 추가하기 위해 사용한다. (what's gizmo...?)DragControls
: 카메라를 마주본 평면에서 물체를 움직이기 위해 사용.먼저 OrbitControls 클래스를 활용하여 인스턴스화를 해준다. 이 때 주의할 점은 OrbitControls의 일부는 THREE
변수에서 기본으로 제공하지 않고 있다는 점이다. (라이브러리의 무게를 줄이기 위한 결정.) OrbitControls를 임포트하기 위해서는 /node_modules/
안에 있는 모듈을 직접 꺼내와야 한다.
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
const controls = new OrbitControls(camera, canvas)
꺼내온 후에 OrbitControls에 카메라와 캔버스를 인자로 넘겨주어 OrbitControls를 인스턴스화해준다.
위 gif는 차례로, 마우스 좌측 클릭을 통해 카메라를 회전하는 모습, 마우스 우측 클릭을 통해 카메라를 수평/수직 이동 시키는 모습, 마우스 휠을 통해 줌인/줌아웃하는 모습이다.
기본적으로 카메라는 scene의 중앙을 바라본다. 우리는 target 프로퍼티를 이용해 이를 변경해줄 수 있다. 해당 프로퍼티 역시 Vector3를 값으로 받으므로, x, y, z 각각을 조정해줄 수 있다.
controls.target.y = 2
Damping은 가속과 마찰의 공식을 활용해 애니메이션을 스무스하게 만들어주는 역할을 하는 속성이다.
Controls의 enableDamping 속성 값을 true로 만들어주면, 해당 효과를 활성화할 수 있다. 이 속성을 올바르게 활용하기 위해서는 컨트롤이 controls.update()를 통해 매 프레임마다 업데이트되어야 한다. (tick 함수를 활용하면 된다.)
const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true
const tick = () => {
controls.update()
}
확연히 부드러워진 움직임을 확인할 수 있다. 사실 이 외에도 컨트롤을 커스텀하기 위한 다양한 속성들이 존재한다. 회전 속도, 줌 속도, 줌 리미트, 각도 리미트, 댐핑의 세기, 그리고 키 바인딩 등 모두 커스텀할 수 있다!
무작정 내장 컨트롤을 활용하다보면, 클래스가 의도치 않은 방식으로 움직이는 부작용이 있을 수 있다. 항상 작업을 할 때 내가 어떤 컨트롤을 필요로 하는지 리스트업을 하고, 해당 클래스가 그 조건들을 만족하는지 확인을 해야 한다. 그렇지 않다면, 직접 커스텀 컨트롤을 만들어주는 편이 좋다!