React + Typescript + Three.js로 3D 객체 렌더링하기

약 2개월만의 포스팅이다. 방학동안 Front-End를 맡아서 프로젝트를 진행하고 있고, 이를 위해 3D 객체 렌더링에 대한 공부 기록을 남기기 위해 해당 게시글을 작성하게 되었다.


Three.js?

  Three.js는 WebGL을 기반으로 하는 JavaScript 라이브러리로, 웹 브라우저에서 3D 그래픽을 생성하고 표시하기 위해 사용되는 라이브러라이다.
  Three.js를 통해 개발자들이 복잡한 수학적 계산이나 WebGL의 낮은 수준의 API 호출 없이 3D 애니메이션, 게임, 시각화, 인터랙티브 애플리케이션을 웹 페이지에 직접 통합할 수 있다.
  렌더링, 씬 그래프, 카메라, 조명, 재질, 텍스처, 그리고 3D 모델 로딩 등 다양한 기능을 제공하며, 이를 통해 웹 기반의 3D 애플리케이션 개발을 대중화하는 데 크게 기여하고 있다.

WebGL?

  WebGL(Web Graphics Library)은 웹 브라우저에서 3D 그래픽을 렌더링하기 위해 사용되는 JavaScript API이다. OpenGL ES(Embedded Systems)의 웹 기반 버전으로, 개발자들이 HTML5 캔버스 요소를 통해 GPU 가속 그래픽을 이용할 수 있게 해준다.
  쉽게 말해서 WebGL을 사용하면 웹 페이지 내에서 직접 복잡한 3D 시각화, 게임, 그래픽 효과를 구현할 수 있으며, 추가적인 플러그인이나 외부 소프트웨어 설치 없이도 브라우저에서 직접 실행된다.

구조

  • Renderer
  • Scene graph
  • Camera
  • Mesh
  • Geometry

설치

  우리 팀의 패키지 매니저는 npm을 사용하고 있으므로 npm 기반으로 서술하도록 하겠다.

npm install three
npm i --save-dev @types/three

사용

1. DOM 요소 선언 구역

const mountRef = useRef<HTMLDivElement>(null);

  3D 객체를 랜더링하기 위해, DOM 요소를 참조하기 위한 useRef 훅을 선언한다.

2. useEffect 및 Three.js 초기화 구역

useEffect(() => {
  // Scene, Camera, Renderer 설정
  // Texture 로딩 및 설정
  // Geometry, Material, Mesh 생성 및 씬에 추가
  // Animation loop 정의 및 실행
}, []);

  컴포넌트가 마운트될 때 실행될 로직을 정의하였다. Three.js의 Scene, Camera, Renderer를 설정하고, 텍스처를 로딩하여 Mesh에 적용한다.
  또한 애니메이션 루프를 정의 및 실행하였다.

2-1. Three.js 씬 초기화 및 설정

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setClearColor(0xffffff);
renderer.setSize(480, 300);
  • Scene 생성: Scene 객체는 3D 세계의 컨테이너 역할을 한다. 여기에는 렌더링할 모든 객체, 광원 등이 포함된다.
  • Camera 설정: PerspectiveCamera는 원근감이 있는 시점에서 씬을 보기 위해 사용된다. 카메라의 시야각, 종횡비, 가까운 클리핑 평면, 먼 클리핑 평면을 설정한다.
  • Renderer 초기화: WebGLRenderer는 WebGL을 사용하여 그래픽을 렌더링한다. antialias 옵션을 true로 설정하여 장면의 가장자리가 부드럽게 처리되도록 설정하였다.
  • Renderer 설정: 배경색을 흰색으로 설정하고, 렌더러의 출력 크기를 지정한다. 이 크기는 렌더링될 <canvas> 요소의 크기를 결정한다.

2-2. 텍스처 로딩 및 속성 설정

const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load(image[selectedPattern]);
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.center.set(0.5, 0.5);
texture.rotation = Math.PI / 2;
texture.offset.set(0, 0);
texture.repeat.set(2, 8);
  • WrapS & WrapT: 텍스처의 가로(S)와 세로(T) 방향의 래핑 모드를 THREE.RepeatWrapping으로 설정하여 텍스처가 반복되도록 설정하였다.
  • Center: 텍스처의 회전 중심점을 (0.5, 0.5)로 설정하여, 텍스처가 자신의 중앙을 기준으로 회전하도록 하였다.
  • Rotation: 텍스처를 π/2 라디안(90도)만큼 회전시켜 바퀴 패턴을 구현하도록 하였다.
  • Offset: 텍스처의 위치 오프셋을 (0, 0)으로 설정하여, 텍스처가 메쉬에 정확히 매핑되도록 설정하였다.
  • Repeat: 텍스처의 반복 횟수를 가로 방향으로 2회, 세로 방향으로 8회로 설정하였는데, 이를 통해 텍스처의 무늬가 메쉬에 여러 번 반복되어 나타나도록 하였다.

2-3. 3D 타이어 객체 생성 및 씬에 추가

const tireGeometry = new THREE.TorusGeometry(6, 2, 16, 150);
const tireMaterial = new THREE.MeshBasicMaterial({ map: texture });
const tire = new THREE.Mesh(tireGeometry, tireMaterial);
tire.rotation.y = Math.PI / 2;
scene.add(tire);

camera.position.z = 15;
  • TorusGeometry 생성: TorusGeometry 생성자를 사용하여 도넛 모양의 기하학적 형태를 가진 타이어를 나타내도록 하였다. 이 때, 주요 매개변수로는 도넛의 주 반지름(6), 튜브 반지름(2), 방사형 세그먼트 수(16), 튜브 주위의 세그먼트 수(150)를 지정하였다.
  • 재질 생성 및 텍스처 매핑: MeshBasicMaterial을 사용하여 타이어의 재질을 생성하고, 이전에 로드된 텍스처를 매핑하였다.
  • Mesh 생성: 지정된 기하학적 형태(tireGeometry)와 재질(tireMaterial)을 사용하여 타이어의 메시를 생성한다.
  • 타이어 회전 및 씬에 추가: 생성된 타이어 메시의 y축 회전을 π/2(90도)로 설정하여 타이어를 수평으로 배치하고, 타이어 메시를 씬에 추가한다.
  • 카메라 위치 설정: 카메라의 z축 위치를 15로 설정하여 적절한 거리에서 타이어를 관찰할 수 있도록 하였다.

2-4. 3D 씬의 애니메이션 루프 생성

const animate = function () {
  requestAnimationFrame(animate);
  tire.rotation.y += 0.01;
  renderer.render(scene, camera);
};
animate();
  • 애니메이션 함수 정의: animate라는 이름의 함수를 정의하며, 이 함수는 자기 자신을 requestAnimationFrame을 통해 반복적으로 호출합니다. 이는 브라우저의 다음 화면 갱신 타이밍에 맞춰 animate 함수를 다시 실행하도록 예약함으로써, 부드러운 애니메이션 효과를 만들어냅니다.
  • 타이어 회전 적용: 각 애니메이션 프레임마다 타이어 메시의 y축 회전 각도를 소량(0.01 라디안) 증가시킵니다. 이는 타이어가 지속적으로 회전하는 효과를 만들어냅니다.
  • 씬 렌더링: 변경된 씬 상태를 카메라를 통해 렌더러에게 전달하고, 이를 화면에 그려내도록 합니다.

3. 클린업 로직 구역

return () => {
  if (mountRef.current) {
    mountRef.current.removeChild(renderer.domElement);
  }
};

  useEffect 훅의 반환 값으로 클린업 함수를 정의하였다. 컴포넌트가 언마운트 될 때 실행되며, DOM에서 Three.js 렌더러가 생성한 <canvas> 요소를 제거한다.

4. 랜더링 구역

return <Wrapper ref={mountRef} className="preview" />;

  컴포넌트가 랜더링할 JSX를 정의하였다. Wrapper 스타일 컴포넌트를 사용하였고, 이 컴포넌트에 mountRef를 참조로 전달하여, Three.js 랜더러가 이 위치에 <canvas> 요소를 추가하도록 구현하였다.

결과 화면

alt text

유의사항

  • 완벽한 타이어 모양을 구현하기 위해서는 별도의 모델링이 필요할 것으로 판단됨
  • 프로젝트의 빠른 완성을 위해 모델링을 추후 과제로 미루고 Three.js에서 기본적으로 제공하는 도넛 모양의 객체를 사용하였음