웹에서 3d를 구현할 수 있는 자바스크립트 라이브러리입니다.
Three.js 를 사용한 페이지 https://github.com/home 깃허브 https://eyes.nasa.gov/apps/mars2020/#/home 나사 https://www.midwam.com/en midwam
웹에서 3d를 구현했으면 모두 Three.js를 사용한 것
https://threejs.org/ 에서 더많은 three.js를 사용한 페이지들을 볼 수 있습니다.
-
리액트 프로젝트 생성 create-react-app
npx create-react-app 3d-app -
라이브러리 설치
npm install threeThree.jsnpm install @react-three/fiberReact에서 Three.js를 편리하게 사용하기 위한 Libnpm install use-cannon물리엔진 Lib
import * as THREE from "three";three.js에서 3d 오브젝트를 표시하려면 3가지 요소가 필요합니다.
- scene
- camera
- renderer
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const renderer = new THREE.WebGLRenderer();camera의 생성자에 들어가는 4가지 속성은 다음과 같습니다.
- FOV(시야각) : 해당 시점의 화면이 보여지는 정도. 각도로 값을 설정합니다.
- aspect ratio(종횡비) : 대부분 요소의 높이와 너비에 맞추어 표시하고 그렇지 않으면 와이드 스크린에 옛날 영화를 트는 것처럼 이미지가 틀어집니다.
- near : 이 값보다 가까이 있는 오브젝트는 렌더링 되지 않음
- far : 이 값보다 멀리 있는 오브젝트는 렌더링 되지 않음
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.innerHTML = "";
document.body.appendChild(renderer.domElement);다음으로 렌더링 할 곳의 크기를 설정해주고 HTML 문서에 추가해줘야 합니다. 일단 높이와 너비를 각각 윈도우의 크기로 설정하였습니다.document.body.innerHTML = ""는 renderer만 화면에 표시하기 위해서입니다. renderer는 <canvas> 엘리먼트로 HTML 문서에 추가됩니다.
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({
color: "blue"
});
camera.position.z = 5;
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);Three.js 에서 3d 오브젝트는 mesh라 부릅니다. mesh는 2가지 요소로 구성됩니다.
- geometry : 기하학적 형태, 뼈대를 담당하는 부분
- material : 질감, 색, 반사율 등 물체의 표면
2가지 속성으로 cube라는 mesh를 만들고 scene에 추가했습니다. 그리고 기본 설정상 scene.add()로 물체를 추가하면 (0,0,0)의 위치를 갖습니다. 이렇게 되면 camera와 cube가 동일한 위치에 겹쳐서 제대로 보이지 않을 것입니다. 이를 방지하기 위해서 camera의 위치를 약간 변경하였습니다.
아직 화면에는 아무것도 나오지 않을 것입니다. 아무것도 렌더링하지 않았기 때문입니다. 화면에 cube를 렌더링하고 회전시키기 위한 코드를 추가합니다.
function animate() {
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();1초에 60번 렌더링(60fps) 할 것이고 그럴 때마다 x 방향으로 0.01, y 방향으로 0.01만큼 회전할 것입니다.
이미 멋지게 cube가 렌더링되고 회전하고 있겠지만 창 크기를 변경했을 때 비율이 뭉게지는게 보입니다. 왜냐하면 renderer와 camera의 설정이 처음 창을 켰을 때의 윈도우 사이즈로 고정되어 있기 때문입니다. addEventListener를 통해 창이 resize될 때마다 renderer와 camera의 설정을 바꿔줍시다.
window.addEventListener("resize", () => {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
});import * as THREE from "three";
import "./App.css";
function App() {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.innerHTML = "";
document.body.appendChild(renderer.domElement);
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({
color: "blue"
});
camera.position.z = 5;
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
function animate() {
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();
window.addEventListener("resize", () => {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
});
return null;
}
export default App;사실 @react-three/fiber를 활용하면 훨씬 쉽게 간단한 scene을 만들 수 있습니다.
import { Canvas, useFrame } from "@react-three/fiber";return (
<div style={{ width: "100vw", height: "100vh" }}>
<Canvas style={{ background: "black" }}>
<Box />
</Canvas>
</div>
);Canvas를 만들면 scene, camera, renderer를 따로 만들필요가 없습니다. Canvas의 스타일은 다른 HTML 태그들 처럼 style속성으로 지정할 수 있습니다.
Canvas안에 렌더링될 Box는 따로 컴포넌트로 빼서 만들었습니다.
const Box = () => {
const ref = useRef();
useFrame((state) => {
ref.current.rotation.x += 0.01;
ref.current.rotation.y += 0.01;
});
return (
<mesh ref={ref}>
<boxBufferGeometry />
<meshBasicMaterial color="blue" />
</mesh>
);
};Canvas안에는 mesh를 직접 사용할 수 있습니다. mesh는 안에 geometry, material 속성을 가져야 합니다. boxBufferGeometry, meshBasicMaterial는 각각 presetting된 기본 상자 geometry, 기본 material 입니다.
mesh에 useRef를 연결합니다. 매 프레임마다 동작하는 useFrame 훅에서 ref를 조작해 mesh를 회전시킵니다.
import { Canvas, useFrame } from "@react-three/fiber";
import { useRef } from "react";
const Box = () => {
const ref = useRef();
useFrame((state) => {
ref.current.rotation.x += 0.01;
ref.current.rotation.y += 0.01;
});
return (
<mesh ref={ref}>
<boxBufferGeometry />
<meshBasicMaterial color="blue" />
</mesh>
);
};
function App() {
return (
<div style={{ width: "100vw", height: "100vh" }}>
<Canvas style={{ background: "black" }}>
<Box />
</Canvas>
</div>
);
}
export default App;기본 three.js만 사용해서 scene을 만들 때보다 코드가 훨씬 짧고 간단해진 것을 볼 수 있습니다. 이후 내용들은 전부 @react-three/fiber를 활용한 코드입니다.

canvas안에 axesHelper를 추가해주면 됩니다.
<Canvas style={{ background: "black" }} camera={{ position: [3, 3, 3] }}>
<Box />
<axesHelper args={[5]} />
</Canvas>camera={{ position: [3, 3, 3] }} 기존 카메라 위치가 [0,0,5]여서 x, y축 밖에 안보이기 때문에 카메라의 위치를 바꿔주었습니다
axesHelper의 args는 사이즈를 나타냅니다.
Orbit Controls는 카메라(화면)의 각도를 돌리거나 확대하거나 이동할 수 있는 컨트롤러 입니다.
- 회전 : 좌클릭 후 드래그
- 확대/축소 : 스크롤
- 이동 : 우크릭 후 드래그
import { Canvas, useFrame, extend, useThree } from "@react-three/fiber";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
extend({ OrbitControls });Orbit Controls를 jsx 형태로 사용하려면 three.js에서 import 한 뒤 react-three/fiber로 extend해서 사용해야 합니다.
const Orbit = () => {
const { camera, gl } = useThree();
return <orbitControls args={[camera, gl.domElement]} />;
};orbitControls 생성자는 camera객체와 이벤트 리스너에 사용되는 HTML 엘리먼트가 필요합니다. 우리는 react-three/fiber에서 제공하는 useThree()를 객체 구조 분해해서 사용하면 됩니다.
<Canvas style={{ background: "black" }} camera={{ position: [3, 3, 3] }}>
<Box />
<axesHelper args={[5]} />
<Orbit />
</Canvas>지금은 빛이 없는 상태지만 큐뷰가 잘 보입니다. 그 이유는 meshBasicMaterial은 빛의 영향을 받지 않는 material이기 때문에 그 자체의 색을 내고 있던 것입니다. 빛의 영향을 받는 material을 적용시켜 보겠습니다.
<mesh ref={ref} {...props}>
<boxBufferGeometry />
<meshPhysicalMaterial color="blue" />
</mesh>meshBasicMaterial대신 meshPhysicalMaterial을 사용하니 더이상 큐브가 보이지 않습니다. 이제 빛을 추가해봅시다.
ambientLight는 모든 mesh가 모든 각도에서 동일한 양을 받게하는 빛입니다. 당연히 그림자도 생기지 않습니다.
<ambientLight intensity={0.2} />canvas안에 추가해 줍시다. intensity로 빛의 세기를 정할 수 있습니다.
pointLight는 한 점에서 나오는 빛입니다. 거리에 따라 빛의 양이 줄어들고 각도에 따라 빛을 받지 못할 수 있는 흔히 우리가 아는 빛입니다. 다만 빛 자체는 관측되지 않고 빛을 받는 mesh들에게 영향을 줄 뿐입니다.
<pointLight />canvas안에 추가해 줍시다.

지금까지는 빛 자체는 관측할 수 없었습니다. 하지만 실제 세상에서는 태양과 같이 빛을 분출하는 오브젝트가 있습니다. 우리는 meshPhongMaterial안에 pointLight를 넣음으로써 태양 비슷한 mesh를 만들 수 있습니다.
meshPhongMaterial은 빛을 반사시켜 표면이 빛나는 material입니다.
const Sun = (props) => {
return (
<mesh {...props}>
<pointLight castShadow />
<sphereBufferGeometry args={[0.3]} />
<meshPhongMaterial emissive="yellow" />
</mesh>
);
};그동안은 계속 boxBufferGeometry를 사용했는데 구를 만들기 위해서 sphereBufferGeometry를 사용했습니다. args는 반지름의 길이 입니다.
meshPhongMaterial에서 emissive로 빛이 반사되는 색을 정할 수 있습니다.
<Sun position={[0, 3, 0]} />분명 방향와 거리가 유효한 빛을 만들었는데 그림자가 생기지 않습니다. 그림자를 만들려면 추가적인 설정이 필요합니다.
Canvas에 shadows속성을 추가하면 그림자를 그릴 수 있는 Canvas가 됩니다.
<Canvas
shadows
style={{ background: "black" }}
camera={{ position: [3, 3, 3] }}
>
Sun은 그림자를 그리는 오브젝트이고 바닥인 Floor는 그림자가 그려지는 오브젝트 입니다.
각각 castShadow 와 receiveShadow를 추가해줍니다.
const Sun = (props) => {
return (
<mesh {...props}>
<pointLight castShadow />
...const Floor = (props) => {
return (
<mesh {...props} receiveShadow>
...Box는 어떨까요? Box는 그림자를 Floor에 그리기도 하지만 Sun이 만드는 그림자를 자신에게 그리기도 합니다. castShadow 와 receiveShadow 둘다 추가해줍니다.
const Box = (props) => {
...
return (
<mesh ref={ref} {...props} castShadow receiveShadow>
...meshMaterial에서 몇가지 자주 사용하는 속성들을 훑어보려고 합니다.
opacity는 불투명도입니다. 1이면 완전한 불투명이고 0이면 완전한 투명입니다. 하지만 opacity만으로는 mesh가 실제로 투명해지지 않습니다. transparent 속성이 true일 때만 opacity속성이 작동합니다. material 속성을 잘 확인하기 위해서 큐브의 위치를 변경하였습니다.
<meshPhysicalMaterial
color="blue"
opacity={0.3}
transparent
/>mesh의 프레임만 나오게 하는 속성입니다. 프레임의 색은 color속성으로 적용됩니다.
<meshPhysicalMaterial
color="blue"
opacity={0.3}
transparent
wireframe
/>metalness는 금속 재질을 만들어 주고 1이면 완전한 금속입니다. roughness는 표면의 거칠기를 뜻하고 0이면 빛을 그대로 반사합니다. metalness의 default 는 0, roughness는 1입니다.
<meshPhysicalMaterial
color="blue"
metalness={1}
roughness={0}
/>
roughness가 0이기 때문에 표면에서 한점으로 빛을 그대로 반사하는 것이 보입니다.
더 많은 속성들은 https://threejs.org/docs/index.html?q=mater#api/en/materials/Material 에서 추가로 확인하실 수 있습니다.
텍스쳐를 적용시키는 법도 간단합니다. 먼저 텍스쳐 사진을 준비해서 작업폴더에 넣어줍니다.
const Box = (props) => {
...
const texture = useLoader(THREE.TextureLoader, "/assets/wood.jpg");
...
return (
<mesh ref={ref} {...props} castShadow>
<boxBufferGeometry />
<meshPhysicalMaterial map={texture} />
</mesh>
);
};@react-three/fiber의 훅 useLoader로 이미지를 활용해 텍스쳐를 만듭니다. meshMaterial의 map 속성으로 텍스쳐를 적용시킵니다.
하지만 이 상태면 texture가 로딩되기 전에 화면이 렌더링되기 때문에 오류가 발생합니다. <Suspense>안에 Box를 넣어 texture 로딩이 완료되면 보여주기로 합시다. Suspense는 리액트에서 제공하는 기능입니다.
<Canvas
shadows
style={{ background: "black" }}
camera={{ position: [3, 3, 3] }}>
<Suspense fallback={null}>
<Box position={[0, 1, 0]} />
</Suspense>
...이번에는 배경에 텍스쳐를 넣어보겠습니다. 다만 3d 배경을 사용하려면 360도 이미지(파나로마)가 필요합니다.
https://cdn.pixabay.com/photo/2018/07/11/16/34/landscape-3531355_960_720.jpg 사용한 이미지
const BackGround = (props) => {
const texture = useLoader(THREE.TextureLoader, "/assets/back.jpg");
const { gl } = useThree();
const formatted = new THREE.WebGLCubeRenderTarget(
texture.image.height
).fromEquirectangularTexture(gl, texture);
return <primitive attach="background" object={formatted.texture} />;
};텍스쳐를 만드는건 기존과 같습니다. 우리는 이미지를 화면에 표시하기 전, 후에 처리를 하는 WebGlRenderTarget을 사용할 것입니다. WebGLCubeRenderTarget는 큐브 카메라로 촬영한 360도 이미지 용 WebGlRenderTarget입니다. 그 중에서 fromEquirectangularTexture 메소드는 파나로마 이미지를 360도 큐브맵(3d 배경)으로 컨버팅해줍니다.
<Suspense fallback={null}>
<BackGround />
</Suspense>BackGround도 텍스쳐 로딩이 필요하기 때문에 Suspense 사이에 껴서 Canvas에 넣어줍니다.







