웹에서 3d를 구현할 수 있는 자바스크립트 라이브러리입니다.
Three.js 를 사용한 페이지
https://github.com/home 깃허브
https://eyes.nasa.gov/apps/mars2020/#/home 나사
https://www.midwam.com/en midwam
https://threejs.org/ 에서 더많은 three.js를 사용한 페이지들을 볼 수 있습니다.
리액트 프로젝트 생성 create-react-app
npx create-react-app 3d-app
라이브러리 설치
npm install three
Three.js
npm install @react-three/fiber
React에서 Three.js를 편리하게 사용하기 위한 Lib
npm install use-cannon
물리엔진 Lib
import * as THREE from "three";
three.js에서 3d 오브젝트를 표시하려면 3가지 요소가 필요합니다.
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가지 속성은 다음과 같습니다.
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가지 요소로 구성됩니다.
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
를 활용한 코드입니다.
Axex Helper는 three.js에서 x, y, z축을 나타내 주는 도구입니다. 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
안에 추가해 줍시다.
default 위치 [0,0,0]을 바라보는 큐브의 면만 밝아진 것이 보입니다.
지금까지는 빛 자체는 관측할 수 없었습니다. 하지만 실제 세상에서는 태양과 같이 빛을 분출하는 오브젝트가 있습니다. 우리는 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
에 넣어줍니다.
이번엔 이벤트를 다루려고 합니다. 3d 오브젝트와 상호작용 할 수 있으면 훨씬 더 재밌을 것 같습니다.
먼저 Box
를 클릭했을 때 생기는 이벤트를 확인해보려 합니다. mesh
에는 on
으로 시작하는 eventHandler
속성이 있습니다. 그 중 onPointerDown
사용할 것입니다.
const handlePointerDown = (e) => {
console.log(e);
};
return (
<mesh
ref={ref}
{...props}
castShadow
onPointerDown={handlePointerDown}>
<boxBufferGeometry />
<meshPhysicalMaterial map={texture} />
</mesh>
onPointerDown
에 이벤트 오브젝트를 출력하는 함수를 연결해줍니다. onClick
과 같이 클릭했을 때 실행되는 속성입니다. 그럼 이제 Box
클릭해서 로그에 무엇이 찍히는지 확인해 보겠습니다. 이벤트 안에 여러 정보들이 많지만 사실 이 중에 우리는 object
만 확인하면 됩니다. object
안에는 position
scale
visible
등 여러 속성들이 들어 있고 이 값을 변경하면 Box
오브젝트와 직접적으로 상호작용을 할 수 있어 보입니다.
일단 scale
부터 바꿔봅시다. 마우스가 Box
에 들어가면 커지고 나오면 원래크기로 돌아오게 해보겠습니다.
const handlePointerEnter = (e) => {
e.object.scale.x = 1.5;
e.object.scale.y = 1.5;
e.object.scale.z = 1.5;
};
const handlePointerLeave = (e) => {
e.object.scale.x = 1;
e.object.scale.y = 1;
e.object.scale.z = 1;
};
return (
<mesh
...
onPointerEnter={handlePointerEnter}
onPointerLeave={handlePointerLeave}
...
들어갈 때 커지고 나오면 돌아오는건 좋은데 들어가서 클릭하면, 나와도 크기를 유지하게 하고 싶습니다. 이 Box
를 선택했다는 느낌을 주고 싶습니다. 그렇게 하려면 클릭했을 때 Box
오브젝트에 상태를 설정해야 합니다. active
라는 속성을 만들어 클릭시 true
값으로 바꿔줍시다. 아까 사용했떤 이벤트 핸들러 함수를 활용하겠습니다.
const handlePointerDown = (e) => {
e.object.active = true;
console.log(e);
};
클릭했을 때 로그에 e.object.active
속성이 생겼을 것이고 값은 true
일 것입니다.
const handlePointerLeave = (e) => {
if (!e.object.active) {
e.object.scale.x = 1;
e.object.scale.y = 1;
e.object.scale.z = 1;
}
};
이제 클릭하고 나오면 Box
가 원래크기로 돌아가지 않습니다. 선택의 느낌을 주려면 Box
가 한개 더 있으면 좋을 것 같습니다. canvas에 Box
를 한개 더 추가하겠습니다. 위치는 적당히 떨어지게 놓습니다. 클릭한 Box
가 커진 상태로 유지되고 반대 Box
는 다시 원래대로 돌아가게 만들겠습니다.
const handlePointerDown = (e) => {
console.log(e);
e.object.active = true;
if (window.activeMesh) {
scaleDown(window.activeMesh);
window.activeMesh = false;
}
window.activeMesh = e.object;
};
const scaleDown = (object) => {
object.scale.y = 1;
object.scale.x = 1;
object.scale.z = 1;
object.active = false;
};
window.activeMesh
에 현재 클릭된 mesh
를 저장합니다. 다른 Box
를 클릭했을 때 기존의 클릭된 mesh
에 scaleDown
함수를 실행시키면 원래 크기로 돌아갑니다.
선택된 Box
의 색상을 변경해 봅시다. 무슨 색으로 변경할지 선택할 구간을 먼저 만들겠습니다.
<div style={{ position: "absolute", zIndex: 1 }}>
<div
onClick={clickHandler}
style={{ background: "blue", height: 50, width: 50 }}
/>
<div
onClick={clickHandler}
style={{ background: "red", height: 50, width: 50 }}
/>
<div
onClick={clickHandler}
style={{ background: "white", height: 50, width: 50 }}
/>
</div>
canvas밖에 만들어 줍시다. 고정된 팔레트를 확인하실 수 있습니다. clickHandler
는 다음과 같습니다.
const clickHandler = (e) => {
if (window.activeMesh) {
window.activeMesh.material.color = new THREE.Color(
e.target.style.background
);
}
};
선택된 Box
가 있으면 해당 오브젝트의material.color
를 변경해줍니다. 해당 속성은 THREE.Color()
로 값을 만들어 넣어줘야 합니다.
Your writing is really informative, especially because it's so meaningful and updated. Thanks for sharing this wonderful post!
Your writing is really great. I’m so glad I read it. It kept me hooked the whole way through.
Thanks for this information. I really appreciate the information that you have provided.
https://www.myccpay.onl/
https://www.mcd-voicex.com/
https://www.tellpopeyes.org/
너무 좋은 글이에요. 잘보고 갑니다. 😍