본문 바로가기
Three.js/Intro

[threejs-journey 1-6] Camera

by SL123 2025. 6. 10.

개요 

이번 글에서는 Three.js에서 제공하는 다양한 카메라 클래스들의 특성과 용도를 알아보고, 마우스 인터랙션을 통해 카메라를 조작하여 3D 메쉬를 관찰하는 방법까지 실습을 통해 정리합니다. 특히 PerspectiveCamera, OrthographicCamera, CubeCamera 등 카메라의 종류와 차이점을 설명하며, 마지막에는 마우스를 이용해 카메라를 회전 궤도로 이동시키는 방법을 삼각함수를 이용해 구현합니다.

 

- Camera

https://threejs.org/docs/#api/en/cameras/Camera

 

three.js docs

 

threejs.org

 

Camera는 추상 클래스이기 때문에 직접 인스턴스를 생성해 사용할 수 없습니다. 대신 다양한 용도에 맞는 하위 클래스들(ArrayCamera, StereoCamera, CubeCamera, OrthographicCamera, PerspectiveCamera)와 통해 실제 장면을 렌더링합니다.

 

 

 

- ArrayCamera

https://threejs.org/docs/#api/en/cameras/ArrayCamera

 

three.js docs

 

threejs.org

ArrayCamera는 여러 개의 카메라 시점을 배열 형태로 설정하여, 하나의 프레임에서 여러 시점을 동시에 렌더링할 수 있게 해주는 카메라입니다. 화면 분할(multiview rendering)기능이 필요할 때 유용하며, 예를 들어 It Takes Tow, A Way Out 같은 분할 화면(co-op) 게임에서 사용될 수 있습니다. 또한 디버깅 용도에도 적합합니다.

 

 

 

- StereoCamera

https://threejs.org/docs/#api/en/cameras/StereoCamera

 

three.js docs

 

threejs.org

StereoCamera는 좌안(left eye)과 우안(right eye)을 위한 두 개의 시점을 만들어 3D 입체 효과를 제공합니다. 이 카메라는 3D Anaglyph(적청 필터 방식), Parallax Barrier(시차 장벽) 같은 3D 디스플레이 방식과 함께 사용할 수 있습니다. 즉, 입체감을 주거나 VR/AR용 콘텐츠에 적합한 카메라입니다.

 

 

 

- CubeCamera

https://threejs.org/docs/#api/en/cameras/CubeCamera

 

three.js docs

 

threejs.org

CubeCamera는 장면의 6방향(+X, -X, +Y, -Y, +Z, -Z)을 각각 렌더링하여 큐브 형태의 텍스처를 생성합니다. 이렇게 만들어진 큐브 맵은 반사(reflection), 굴절(refraction), 환경 조명(evironment mapping) 등에 활용됩니다.

 

예를 들어, 금속 구체 표면에 CubeCamera로 만든 큐브 맵을 적용하면, 실제로 주변 환경이 구체 표면에 비치는 것처럼 보이게 할 수 있습니다. 이는 픽셀이 바라보는 방향에 따라 큐브 맵의 해당 이미지를 샘플링해 색상을 정하는 방식으로 구현됩니다.

 

 

 

- PerspectiveCamera

https://threejs.org/docs/#api/en/cameras/PerspectiveCamera

 

three.js docs

 

threejs.org

PerspectiveCamera는 원근감을 표현하기 위해 사용되며, 실제 인간의 시각과 유사한 방식으로 장면을 렌더링합니다.

FOV(Field Of View) : 수직 시야각을 의미하며, 일반적으로 75 정도가 인간의 시야각과 유사합니다.
aspect ratio : canvas.width / canvas.height 로 계산하며, 비율이 맞지 않으면 왜곡이 발생합니다.
near, far : 렌더링 범위를 결정하며, 이 범위를 벗어난 객체를 컬링(culling)되어 그려지지 않습니다.

 

 

- near, far, Frustum Culling 정리

Three.js에서 카메라는 nearfar 파라미터를 사용해 시야 범위(View Frustum)를 설정합니다. 이 범위를 벗어난 객체를 자동으로 렌더링되지 않는데, 이를 프러스텀 컬링(Frustum Culling)이라 합니다.

 

- 예시

console.log(camera.position.length());

 

  • 카메라는 원점 (0, 0, 0)에서 약 3.4 떨어진 곳에 위치
  • 큐브가 원점에 있다면 카메라로부터의 거리 3.4

 

const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 1, 3.1);

far = 3.1이라면 시야 범위 밖이기 때문에 큐브가 컬링되어 보이지 않습니다.

 

 

- 컬링을 하는 이유

GPU가 멀리있거나 가까이 있는 물체를 그리지 않아 불필요한 렌더링을 하지 않는다는 이유로 3D에서 필수적입니다.

 

 

 

- PerspectiveCamera의 문제점

Z값이 비슷한 두 개의 면이 서로 겹칠 경우, 렌더링 순서에 따라 깜빡거리거나 부정확하게 보일 수 있습니다. 이는 깊이 버퍼의 정밀도 한계로 인해 발생하는 현상으로, 해결방법은 다음과 같습니다.

  • near/far 값을 가능한 좁게 설정
  • z-offset을 주거나, 겹치는 모델을 미세하게 분리
  • 시각적으로 덜 보이게 프롭(prop)을 배치

 

 

 

- OrthographicCamera

https://threejs.org/docs/#api/en/cameras/OrthographicCamera

 

three.js docs

 

threejs.org

 

OrthographicCamera는 원근감을 제거한 평면적 시야를 제공합니다. 멀거나 가까운 물체가 같은 크기로 보이며, 2D UI나 RTS(Age of Empires, Civilization 등)에 주로 사용됩니다.

생성자 파라미터 : left, right, top, bottom, near, far
비율을 고려하지 않으면 렌더링이 왜곡되어 눌려 보일 수 있습니다.

 

- 예시

const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 100);

const sizes = {
  width: 800,
  height: 600,
};

해당 설정으로 렌더링 하면, 화면 비율이 맞지 않아 정사각형 큐브가 세로로 눌린 듯한 형태로 나타납니다. 이는 가로가 상대적으로 늘어나면서 세로가 상대적으로 좁아져 발생하는 현상입니다. 

 

 

 

이를 해결하려면 화면 비율(aspect ratio)을 계산해서 카메라의 좌우 범위(left, right)를 조절해야 합니다.

const camera = new THREE.OrthographicCamera(
  -1 * aspectRadio, // left
  1 * aspectRadio, // right
  1, // top
  -1, // bottom
  0.1,
  100
);

비율을 aspectRadio로 맞춰 OrthographicCamera를 만든다면 정사각형의 큐브가 원할하게 그려지는 것을 확인할 수 있습니다.

 

 

 

 

- Mouse를 이용한 카메라 조작

JavaScript 에서는 addEventListener 를 사용해 마우스 이벤트를 처리할 수 있습니다.   

/**
 * Cursor
 */
window.addEventListener("mousemove", event => {
  console.log(event.clientX);
  //console.log(event.clientY);
});

위 코드에서 event.clientX는 마우스 커서가 브라우저 화면에서 얼마나 오른쪽으로 이동했는지를 나타냅니다. 마찬가지로 event.clientY는 아래쪽으로 얼마나 이동했는지를 나타냅니다.

 

하지만, 이렇게 절대 좌표를 사용할 경우, 모니터 해상도나 화면 크기에 따라 값이 달라지기 때문에 일관된 동작을 구현하기 어렵습니다.

 

 

 

- 화면 크기에 따른 비율로 변환

이를 해결하기 위해, 마우스 좌표를 0~1 사이의 비율 값으로 정규화합니다.

window.addEventListener("mousemove", event => {
  cursor.x = event.clientX / sizes.width;
  cursor.y = event.clientY / sizes.height;
  
  console.log(cursor.x, cursor.y);
});

 

 

이제 cursor.x, cursor.y는 항상 0(왼쪽 또는 위쪽 끝)에서 1(오른쪽 또는 아래쪽 끝) 사이의 값을 가집니다.

 

 

 

- 중앙 기준으로 -0.5 ~0.5로 변환

그래픽에서는 화면 중심을 (0, 0)으로 설정하는 것이 수학적으로 더 간편합니다. 특히 카메라 회전이나 쉐이더 프로그래밍처럼 좌표계 중심에서의 연산이 중요할 때 유용합니다. 이를 위해 비율값을 -0.5 ~ 0.5로 변환합니다:

window.addEventListener("mousemove", event => {
  cursor.x = event.clientX / sizes.width - 0.5;
  cursor.y = -(event.clientY / sizes.height - 0.5);
});

 

  • cursor.x = -0.5는 화면 왼쪽 끝
  • cursor.x = 0은 화면 중앙
  • cursor.x = 0.5는 오른쪽 끝

 

cursor.y도 같은 방식이지만, 일반적으로 y축은 위쪽이 양수가 되도록 부호를 반전시킵니다.

 

 

 

- 중앙 기준으로 -0.5 ~0.5로 변환

쉐이더에서는 일반적으로 좌표계의 중심이 (0, 0)입니다. 따라서 -0.5 ~ 0.5 범위의 값은 중심을 기준으로 한 대칭적인 연산을 가능하게 해줍니다. 이를 통해 마우스 위치에 따라 객체 회전, 시점 변경 마우스를 중심으로 한 인터렉션 구현 등을 더욱 직관적으로 구현할 수 있습니다.

 

 

 

- 마우스를 이용한 카메라 회전 및 메쉬 관찰

마우스 움직임에 따라 카메라가 회전하고, 중심에 있는 메쉬를 다양한 각도에서 관찰할 수 있도록 합니다.

 

 

 

- 기본적인 카메라 조작

마우스 위치(cursor.x, cursor.y)를 기반으로 카메라의 위치를 직접 제어할 수 있습니다. 예를 들어 아래와 같이 마우스 좌표를 곱해 카메라 위치를 조정할 수 있습니다.

const tick = () => {
  const elapsedTime = clock.getElapsedTime();

  // Update camera
  camera.position.x = cursor.x * 10;
  camera.position.y = cursor.y * 10;
  // camera.lookAt(new THREE.Vector3());
  camera.lookAt(mesh.position);

  // Render
  renderer.render(scene, camera);

  // Call tick again on the next frame
  window.requestAnimationFrame(tick);
};

lookAt(mesh.position)은 카메라가 항상 메쉬를 바라보게 합니다. 이때, camera.position이 너무 커지거나 가까워지면 관찰에 불편함이 발생할 수 있습니다. 마우스를 약간만 움직여도 카메라가 크게 이동하기 때문입니다.

 

 

 

- 마우스로 메쉬 주위를 회전하는 방식

이 문제를 해결하기 위해 삼각함수(sin, cos)를 사용해 카메라가 원형 궤도를 그리며 메쉬를 중심으로 회전하도록 만듭니다. 이렇게 하면 거리를 일정하게 유지하면서 부드러운 회전이 가능합니다.

cursor.x * Math.PI * 2 :  마우스 X축을 0~1 범위에서 360도 회전 가능한 각도로 변환
Math.sin, Math.cos : 회전 궤도를 만들기 위한 삼각함수
radius : 카메라와 중심 메쉬 간의 거리
cursor.y * 5 : 마우스 Y축을 카메라 높이를 조절
const tick = () => {
  const elapsedTime = clock.getElapsedTime();

  // Update objects
  // mesh.rotation.y = elapsedTime;

  // Update camera
  const radius = 3;
  camera.position.x = Math.sin(cursor.x * Math.PI * 2) * radius;
  camera.position.z = Math.cos(cursor.x * Math.PI * 2) * radius;
  camera.position.y = cursor.y * 5;
  
  // center
  camera.lookAt(mesh.position);

  // Render
  renderer.render(scene, camera);

  // Call tick again on the next frame
  window.requestAnimationFrame(tick);
};

이제 마우스를 좌우로 움직이면 메쉬를 중심으로 카메라가 회전하고, 상하로 움직이면 카메라가 위아래로 이동합니다. 이를 활용하면 제품 뷰어, 캐릭터 프리뷰, 3D 전시관 등의 인터렉션을 구현할 수 있습니다.

 

결론

이번 정리를 통해 Three.js에서 카메라를 어떻게 활용하는지, 그리고 사용자의 입력(마우스)을 통해 시점을 어떻게 제어할 수 있는지에 대해 실습과 함께 배워보았습니다. 

특히 카메라의 위치를 직접 조작하는 것보다는 삼각함수를 활용한 회전 방식이 관찰에 더 적합하다는 점이 중요합니다. 향후 제품 뷰어나 캐릭터 선택 화면 등에서 이러한 방식이 어떻게 활용될 수 있는지 더 응용해보면 좋을 것 같습니다.

다음 단계에서는 카메라 컨트롤을 좀 더 고도화하기 위해 Controls를 활용하는 방법도 이어서 다뤄볼 예정입니다.