Three.js와 R3F(React Three Fiber) 기초

기운찬곰·2023년 8월 10일
7
post-thumbnail

Overview

이번 사이드 프로젝트는 어쩌다보니 Blender와 Three.js를 활용한 3D 모델 조작과 관련된 개발을 진행해보려고 합니다. 작년에도 한차례 Three.js 에 대한 관심이 있어서 살짝 공부하고 다뤄보긴 했었습니다. 그래서 개념은 조금 익숙한 상태이긴 합니다만 다시 한번 공식문서나 관련 자료를 살펴보며 가볍게 실습을 진행해보고 블로그에 글로 정리해보려고 합니다.


Three.js Creating a scene

참고 : https://threejs.org/docs/index.html#manual/ko/introduction/Creating-a-scene

아무래도 가장 먼저 읽어봐야 될 부분이 Three.js에 있는 Creating a scene라는 문서가 아닐까 생각합니다. 여기서는 간단한 실습을 통해 spinning cube 라는 것을 만들어보면서 대략적으로 원리(개념)을 이해해볼 것입니다. 참고로 한국어 번역 문서가 어느 정도 잘 되어있는 거 같습니다. 부자연스러운 부분도 있지만 그걸 감안해도 볼만 합니다.

Scene 만들기

Three.js로 무언가를 표현하려면 scene, camera 그리고 renderer가 필요합니다.

const scene = new THREE.Scene();

// add a camera
// THREE.PerspectiveCamera(fov, aspect, near, far)
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );

// add a renderer
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );

// add the renderer element to the DOM so it is in our page
document.body.appendChild( renderer.domElement );
  • Scene은 물체, 카메라 및 조명을 배치할 수 있는 3D 공간입니다.
  • Three.js에는 여러가지 종류의 카메라가 있는데 여기서는 PerspectiveCamera를 사용했습니다. 해석해보면 원근감이라는 뜻이 있으므로 원근감이 있는 카메라인 것을 유추할 수 있습니다. 참고로 원근감이 없는 카메라도 있습니다.
    • 첫 번째 속성은 field of view(FOV, 시야각)입니다. 해당 시점의 화면이 보여지는 정도를 나타냅니다.
    • 두 번째 속성은 aspect ratio(종횡비)입니다. vertical을 기준으로 horizontal 시야를 만드는 데 사용하는 가로 세로 비율입니다. 대부분의 경우 요소의 높이와 너비에 맞추어 표시합니다.
    • 다음 두 속성은 near 와 far plane 입니다. 무슨 뜻인가 하면, far 값 보다 멀리 있는 요소나 near 값보다 가까이 있는 오브젝트는 렌더링 되지 않는다는 뜻입니다. 지금 시점에서 이것까지 고려할 필요는 없지만, 앱 성능 향상을 위해 사용할 수 있습니다.
  • 다음은 renderer입니다. 마법이 일어나는 곳입니다. 같이 사용하는 WebGLRenderer와 더불어, Three.js는 다른 몇가지 renderer를 사용합니다.
    • renderer 인스턴스를 생섬함과 동시에, 렌더링 할 곳의 크기를 설정해줘야 합니다. 렌더링할 구역의 높이와 너비를 설정하는 것은 좋은 방법입니다. 성능 개선을 중시하는 앱의 경우, setSize를 사용하거나 window.innerWidth/2, window.innerHeight/2를 사용해서 화면 크기의 절반으로 구현할 수도 있습니다.
    • 사이즈는 그대로 유지하고 싶지만 더 낮은 해상도로 렌더링하고 싶을 경우 setSize의 updateStyle (세 번째 인자)를 false로 불러오면 됩니다.
  • 마지막으로 제일 중요한 renderer 엘리먼트를 HTML 문서 안에 넣었습니다. 이는<canvas> 엘리먼트로, renderer가 scene을 나타내는 구역입니다.

이미지 출처 : https://codepen.io/rachsmith/post/beginning-with-3d-webgl-pt-1-the-scene

큐브 만들기

이제 물체를 추가해볼 차례입니다. 간단하게 큐브를 추가해보도록 합니다.

// we're creating a cube to put in our scene
const geometry = new THREE.BoxGeometry( 1, 1, 1 ); // widht, height, depth
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );

camera.position.z = 5;
  • 큐브를 만드려면, BoxGeometry가 필요합니다. Three.js에서는 다양한 Geometry를 제공하고 있습니다.
  • Geometry와 더불어, 무언가를 색칠해줄 요소가 필요합니다. 여러가지 Material이 있지만 여기서는 기본적인 MeshBasicMaterial를 사용하겠습니다.
  • 세 번째로 필요한 것은 Mesh입니다. Mesh는 geometry을 받아서, material을 적용해줍니다. 즉, mesh 안에는 geometry와 material이 있어야 합니다. (이 관계를 이해하면 좋습니다)
  • 기본 설정상 scene.add()를 불러오면, 추가된 모든 것들은 (0,0,0) 속성을 가집니다. 이렇게 되면 카메라와 큐브가 동일한 위치에 겹치게 되겠죠. 이를 피하기 위해, 카메라를 z축으로 약간 이동시켜줍니다.

Scene 렌더링

지금까지 한 것을 실행해보면 아무것도 보이지 않을 것입니다. 왜냐하면 아직 아무것도 렌더링하지 않았기 때문입니다. 이를 해결하려면 render or animate loop라는 것이 필요합니다.

function animate() {
	requestAnimationFrame( animate );
	renderer.render( scene, camera );
}
animate();
  • 이 코드는 화면이 새로고침 될 때마다 계속해서 렌더링을 해 줄 것입니다. (일반적인 경우에 1초에 60번 렌더링 됩니다).
  • "왜 그냥 setInterval을 쓰지 않는거죠?"라고 질문할 수도 있을 겁니다. 단적으로 말하면 가능은 합니다. 하지만 requestAnimationFrame 을 사용하는 것이 훨씬 이점이 많습니다. 아마 가장 큰 이점은 유저가 브라우저 창에서 이탈했을때 멈춰주는 기능일 것입니다. 이를 통해 소중한 전력과 배터리를 아낄 수 있죠.

결국에 이를 계층 구조로 생각해봤을때 이런식으로 표현할 수 있겠네요. 아직 조명(light)는 다루지 않았지만요.

큐브 애니메이션

큐브를 회전시켜 보면서 조금 더 재미있게 만들어봅시다.

function animate() {
	requestAnimationFrame( animate );

	cube.rotation.x += 0.01;
	cube.rotation.y += 0.01;

	renderer.render( scene, camera );
}
animate();

위 코드는 모든 프레임마다 실행되면서 (일반적으로 1초에 60번), 큐브가 멋지게 돌아가도록 만들어 줄 것입니다. 기본적으로 앱을 실행하는 동안 무언가를 움직이거나 변형하고 싶을때, animate loop를 사용하면 됩니다.

💻 최종 결과 화면 : https://jsfiddle.net/0c1oqf38/

사용자 입장에서는 카메라를 통해 z=5라는 위치(위에서 아래를 내려다보고 있는 형태)에서 (0, 0, 0)에 위치한 물체가 회전하는 걸 보고 있는 것입니다. 지금은 조명이 없어서 깜깜한 방에 있는 것과 같습니다.

이로써, Three.js의 대략적인 구조에 대해 알아봤습니다. 어떤 느낌인지 아시겠죠?


R3F(React Three Fiber)

Introduction

참고 : https://docs.pmnd.rs/react-three-fiber/getting-started/introduction

React-three-fiber is a React renderer for three.js.

상태에 반응하고 쉽게 상호작용하며 React의 생태계에 참여할 수 있는 재사용 가능한 자체 구성요소로 장면을 선언적으로 구축하도록 도와줍니다. 즉, Three.js를 React에서 사용하기 쉽도록 wrapping 했다고 보면 됩니다. React 혹은 Next.js에서 Three.js를 사용하신다면 R3F를 사용하는 것이 편하고 좋습니다.

npm install three @types/three @react-three/fiber

R3F를 사용하기 전에 React와 Three.js에 대해 어느정도 능통해야 합니다.

큐브 만들기

위 Three.js 예시를 R3F를 통해 React에서 구현하는 실습을 진행해보록 하겠습니다. 먼저 Box 컴포넌트를 만들어봅니다.

"use client";

import * as THREE from "three";
import { useFrame, ThreeElements } from "@react-three/fiber";
import { useState, useRef } from "react";

type BoxProps = ThreeElements["mesh"];

export default function Box(props: BoxProps) {
  const meshRef = useRef<THREE.Mesh>(null!);
  const [hovered, setHover] = useState(false);
  const [active, setActive] = useState(false);

  useFrame((state, delta) => {
    meshRef.current.rotation.x += delta;
    meshRef.current.rotation.y += delta;
  });

  return (
    <mesh
      ref={meshRef}
      scale={active ? 1.5 : 1}
      onClick={(event) => setActive(!active)}
      onPointerOver={(e) => setHover(true)}
      onPointerOut={(e) => setHover(false)}
      {...props}
    >
      <boxGeometry args={[5, 5, 5]} />
      <meshStandardMaterial color={hovered ? "hotpink" : "orange"} />
    </mesh>
  );
}

Box 컴포넌트에는 mesh와 그 안에 boxGeometry, meshStandardMaterial이 사용된 것을 알 수 있습니다. Three.js에서의 구조와 명칭이 동일한 것을 알 수 있습니다.

useFrame은 animate loop 일 것입니다. 1초에 60번 콜백함수가 실행되는 듯 합니다. delta는 이전 프레임과 현재 프레임 사이 경과 시간(ms) 입니다. 그니까 프레임이 변경되기 전에 실행되는 함수라고 보면 됩니다.

Canvas 만들기

R3F의 Canvas를 사용하면 Scene, Renderer, Camera 는 기본적으로 같이 따라옵니다.

"use client";

import Box from "@/components/Box";
import { Canvas } from "@react-three/fiber";

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <div style={{ width: "50vw", height: "50vh" }}>
        <Canvas
          camera={{
            position: [0, 0, 10],
          }}
        >
          <ambientLight />
          <Box position={[0, 0, 0]} />
        </Canvas>
      </div>
    </main>
  );
}

여기서는 조명을 설정해주었습니다. 조명이 없으면 Box의 색상이 보이지 않기 때문입니다. (Three.js에서는 조명이 없었는데 잘 보이는건 왜 그런거지...)

gridHelper와 axesHelper

근데 보다보면 공간 감각이 없어서 좀 답답합니다. 과연 저 Box가 (0, 0, 0)에서 회전하고 있는걸까요? 이걸 알 수 있는 방법이 있습니다. 바로 gridHelper와 axesHelper를 사용하면 됩니다.

아. 그 전에 먼저 좌표계가 어떻게 되어있는지부터 알면 좋을 거 같습니다. 모니터를 기준으로 x, y, z축을 그려봤습니다.

이제 코드 상에서 gridHelper와 axesHelper를 추가해보겠습니다. 카메라는 이해하기 쉽게 [0, 1, 10]에 위치해놨습니다.

"use client";

import Box from "@/components/Box";
import { Canvas } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei";

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <div style={{ width: "50vw", height: "50vh" }}>
        <Canvas
          camera={{
            position: [0, 1, 10],
          }}
        >
          <ambientLight />
          <Box position={[0, 0, 0]} />

          <gridHelper args={[10, 10]} />
          <axesHelper args={[8]} />
        </Canvas>
      </div>
    </main>
  );
}

카메라가 모니터 위에서 아래로 내려보고 있는 형태. 살짝 y축으로 아래쪽으로 있는 형태로 물체를 본다고 생각하면 됩니다. 이렇게 이해하고 생각해봤을때 파란색은 z축, 초록색은 y축, 주황색은 x축이라고 볼 수 있습니다.


참고로 mesh안에 axesHelper를 추가하면 해당 요소에 대한 좌표축을 볼 수 있습니다. 이 또한 꽤나 유용해보입니다.

<mesh
  ref={meshRef}
  onClick={(event) => setActive(!active)}
  onPointerOver={(e) => setHover(true)}
  onPointerOut={(e) => setHover(false)}
  {...props}
>
  <boxGeometry args={[2, 2, 2]} />
  <meshStandardMaterial color={hovered ? "hotpink" : "orange"} />

  <axesHelper args={[8]} />
</mesh>

마우스 컨트롤 - OrbitControls

더 이해하기 쉽도록 마우스를 통해 공간을 변경해서 볼 수 있도록 해볼 수 있습니다. 이를 위해 @react-three/drei 를 설치해줍니다.

pnpm add @react-three/drei

참고 : https://www.npmjs.com/package/@react-three/drei

drei는 fiber에 더해 여러 헬퍼기능을 추가해주는 라이브러리 입니다. R3F와 더불어 많이 사용되는 대표적인 라이브러리 입니다. @react-three/drei 안에 있는 OrbitControls를 사용하면 쉽게 마우스 컨트롤 효과를 적용해볼 수 있습니다. 휠을 이용해 확대 축소도 가능합니다.

"use client";

import Box from "@/components/Box";
import { Canvas } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei";

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <div style={{ width: "50vw", height: "50vh" }}>
        <Canvas
          camera={{
            position: [0, 1, 10],
          }}
        >
          <ambientLight />
          <Box position={[0, 0, 0]} />

          <gridHelper args={[10, 10]} />
          <axesHelper args={[8]} />

          <OrbitControls />
        </Canvas>
      </div>
    </main>
  );
}


마치면서

이번 시간에는 Three.js와 R3F(React Three Fiber) 기본적인 사용방법에 대해 알아봤습니다. 실제로는 당연히 이보다 어려울 것입니다. 무엇보다 3D 라는 공간 자체, 카메라, 조명 설정 등 어려운 부분이 존재합니다. 뭐 그래도 하다보면 늘겠죠...?

아무튼 이를 통해 유의미한 결과물을 만들어볼 수 있었으면 좋겠네요.


참고 자료

profile
배움을 좋아합니다. 새로운 것을 좋아합니다.

2개의 댓글

comment-user-thumbnail
2023년 10월 26일

안녕하세요 글 잘 읽었습니다!! 혹시 중간 중간 있는 스케치로 그린듯한 계층구조는 어떤 툴로 작성하셨는지 알 수 있을까요?!

1개의 답글