이번 사이드 프로젝트는 어쩌다보니 Blender와 Three.js를 활용한 3D 모델 조작과 관련된 개발을 진행해보려고 합니다. 작년에도 한차례 Three.js 에 대한 관심이 있어서 살짝 공부하고 다뤄보긴 했었습니다. 그래서 개념은 조금 익숙한 상태이긴 합니다만 다시 한번 공식문서나 관련 자료를 살펴보며 가볍게 실습을 진행해보고 블로그에 글로 정리해보려고 합니다.
참고 : https://threejs.org/docs/index.html#manual/ko/introduction/Creating-a-scene
아무래도 가장 먼저 읽어봐야 될 부분이 Three.js에 있는 Creating a scene라는 문서가 아닐까 생각합니다. 여기서는 간단한 실습을 통해 spinning cube 라는 것을 만들어보면서 대략적으로 원리(개념)을 이해해볼 것입니다. 참고로 한국어 번역 문서가 어느 정도 잘 되어있는 거 같습니다. 부자연스러운 부분도 있지만 그걸 감안해도 볼만 합니다.
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 );
<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;
scene.add()
를 불러오면, 추가된 모든 것들은 (0,0,0) 속성을 가집니다. 이렇게 되면 카메라와 큐브가 동일한 위치에 겹치게 되겠죠. 이를 피하기 위해, 카메라를 z축으로 약간 이동시켜줍니다. 지금까지 한 것을 실행해보면 아무것도 보이지 않을 것입니다. 왜냐하면 아직 아무것도 렌더링하지 않았기 때문입니다. 이를 해결하려면 render or animate loop라는 것이 필요합니다.
function animate() {
requestAnimationFrame( animate );
renderer.render( scene, camera );
}
animate();
결국에 이를 계층 구조로 생각해봤을때 이런식으로 표현할 수 있겠네요. 아직 조명(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의 대략적인 구조에 대해 알아봤습니다. 어떤 느낌인지 아시겠죠?
참고 : 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) 입니다. 그니까 프레임이 변경되기 전에 실행되는 함수라고 보면 됩니다.
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에서는 조명이 없었는데 잘 보이는건 왜 그런거지...)
근데 보다보면 공간 감각이 없어서 좀 답답합니다. 과연 저 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>
더 이해하기 쉽도록 마우스를 통해 공간을 변경해서 볼 수 있도록 해볼 수 있습니다. 이를 위해 @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 라는 공간 자체, 카메라, 조명 설정 등 어려운 부분이 존재합니다. 뭐 그래도 하다보면 늘겠죠...?
아무튼 이를 통해 유의미한 결과물을 만들어볼 수 있었으면 좋겠네요.
안녕하세요 글 잘 읽었습니다!! 혹시 중간 중간 있는 스케치로 그린듯한 계층구조는 어떤 툴로 작성하셨는지 알 수 있을까요?!