WebGL
, WebGPU
=> three.js
=> react-three-fiber
React를 사용하여 Three.js 라이브러리를 효과적으로 사용할 수 있게 해주는 라이브러리
R3F는 3D 그래픽을 위한 canvas
태그를 Canvas
라는 객체를 통하여 좀 더 쉽게 개발할 수 있도록 해줌.
ex. vanilla.js 에서는 canvas 태그에 Scene
, Camera
, Renderer
3가지 요소를 직접 작성해줘야한다면, R3F 에서는 Canvas 객체를 생성하면 3가지 요소를 객체로 자동 생성해줌.
$ yarn create vite 3D-Project --template react
$ cd 3D-Project
$ yarn install
$ yarn dev
파일구조
3D-Project
│
├── node_modules
├── public
├── src
│ ├── componetns
│ │ └── 3DEl
│ │ └── Box.jsx
│ │ └── ...
│ │
│ ├── pages
│ │ └── Main
│ │ └── index.jsx
│ │
│ ├── styles
│ │ └── fonts
│ │ └── ...
│ │
│ ├── App.jsx
│ ├── GlobalStyle.js
│ └── main.jsx
│
├── .eslintrc.cjs
├── .gitignore
├── .prettierrc
├── index.html
├── package.json
├── README.md
├── vite.config.js
└── yarn.lock
src/pages/Main/index.jsx
import { Canvas } from '@react-three/fiber';
import styled from 'styled-components';
import Cube from '../../components/3DEl/Cube';
const Main = () => {
return (
<MainBox>
<Canvas>
<Cube />
</Canvas>
</MainBox>
);
};
const MainBox = styled.div`
display: flex;
align-items: center;
width: 100%;
height: 100vh;
`;
export default Main;
src/componetnts/3DEl/Box.jsx
import { useFrame } from '@react-three/fiber';
import { useRef } from 'react';
const Box = () => {
const refMesh = useRef();
// useFrame 은 렌더링이 되기 직전에 호출되는 함수
useFrame((state, delta) => {
// delta: 이전 프레임과 현제 프레임의 ...
refMesh.current.rotation.y += delta;
});
return (
<>
<directionalLight position={[1, 1, 1]} />
{/* rotation: x: 0, y: 45도, z: 0
rotation-y={45 * (Math.PI / 180)} 와 동일
*/}
<mesh ref={refMesh} rotation={[0, 45 * (Math.PI / 180), 0]}>
<boxGeometry />
<meshStandardMaterial color='#e67e22' />
</mesh>
</>
);
};
export default Box;
drei 설치
Drei: R3F에서 사용할 수 있는 유횽한 컴포넌트들을 모아놓은 라이브러리
$ yarn add @react/drei
src/components/3DEl/Box.jsx
import { useRef } from 'react';
import * as THREE from 'three';
import { useFrame } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';
const Box = () => {
const refMesh = useRef();
useFrame((state, delta) => {
refMesh.current.rotation.z += delta;
});
return (
<>
<directionalLight position={[1, 1, 1]} />
{/* 월드(World) 좌표계 */}
<axesHelper scale={10} />
{/* 컨트롤러 */}
<OrbitControls />
<mesh
ref={refMesh}
position-y={2} // position={[0, 2, 0]} 와 동일
rotation-x={THREE.MathUtils.degToRad(45)} // rotation-x={45 * (Math.PI / 180)} 와 동일
scale={[2, 1, 1]}
>
<boxGeometry />
<meshStandardMaterial color='#e67e22' opacity={0.5} transparent={true} />
{/* 상대 좌표계 */}
<axesHelper />
<mesh position-y={1} scale={[0.1, 0.1, 0.1]}>
<sphereGeometry />
<meshStandardMaterial color='red' />
<axesHelper scale={10} />
</mesh>
</mesh>
</>
);
};
export default Box;
모든 Geometry는 BufferGeometry를 상속 받음.
three.js 에 제공되는 기본 Geometry
src/components/3DEl/Boxes.jsx
import * as THREE from 'three';
import { Box, OrbitControls } from '@react-three/drei';
const Boxes = () => {
// 방법.3-1
const MyBox = (props) => {
const geom = new THREE.BoxGeometry();
return <mesh {...props} geometry={geom}></mesh>;
};
return (
<>
<OrbitControls />
<ambientLight intensity={0.1} />
<directionalLight position={[2, 1, 3]} intensity={0.5} />
{/* 방법.1 */}
<mesh>
<boxGeometry />
<meshStandardMaterial color='#1abc9c' />
</mesh>
{/* 방법.2 */}
<Box position={[1.2, 0, 0]}>
<meshStandardMaterial color='#8e44ad' />
</Box>
{/* 방법.3-2 */}
<MyBox position={[2.4, 0, 0]}>
<meshStandardMaterial color='#e74c3c' />
</MyBox>
</>
);
};
export default Boxes;
<boxGeometry args={[1, 1, 1, 1, 1, 1]} />
new THREE.BoxGeometry(1, 1, 1, 1, 1, 1)
R3F의 boxGeometry 요소의 args 속성을 이용하여 두번째 줄의 코드를 사용하지 않고 쉽게 속성을 지정할 수 있음.
leva 설치
leva 패키지는 useControls 라는 훅을 제공하는데, 이를 활용해 실행화면에서 GUI로 원하는 값들을 조정하며 결과를 바로바로 확인해볼 수 있다.
$ yarn add leva
src/components/3DEl/BoxLeva.jsx
import { useRef, useEffect } from 'react';
import { OrbitControls } from '@react-three/drei';
import { useControls } from 'leva';
const BoxLeva = () => {
const refMesh = useRef();
const refWireMesh = useRef();
const { xSize, ySize, zSize, xSegments, ySegments, zSegments } = useControls({
xSize: { value: 1, min: 0.1, max: 5, step: 0.01 }, // value: 초기값, min: 컨트롤러 최소값, max: 컨트롤러 최대값, step: 컨트롤러 변화값
ySize: { value: 1, min: 0.1, max: 5, step: 0.01 },
zSize: { value: 1, min: 0.1, max: 5, step: 0.01 },
xSegments: { value: 1, min: 1, max: 10, step: 1 }, // segments는 1보다 큰 정수값이어야 함.
ySegments: { value: 1, min: 1, max: 10, step: 1 },
zSegments: { value: 1, min: 1, max: 10, step: 1 },
});
// geometry를 재사용하여 메모리 절약
useEffect(() => {
refWireMesh.current.geometry = refMesh.current.geometry;
}, [xSize, ySize, zSize, xSegments, ySegments, zSegments]);
return (
<>
<OrbitControls />
<ambientLight intensity={0.1} />
<directionalLight position={[2, 1, 3]} intensity={0.5} />
<mesh ref={refMesh}>
<boxGeometry args={[xSize, ySize, zSize, xSegments, ySegments, zSegments]} />
<meshStandardMaterial color='#1abc9c' />
</mesh>
<mesh ref={refWireMesh}>
<meshStandardMaterial emissive='yellow' wireframe={true} />
</mesh>
</>
);
};
export default BoxLeva;
src/components/3DEl/SphereLeva.jsx
import { useRef, useEffect } from 'react';
import { OrbitControls } from '@react-three/drei';
import { useControls } from 'leva';
const SphereLeva = () => {
const refMesh = useRef();
const refWireMesh = useRef();
const { radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength } = useControls({
radius: { value: 1, min: 0.1, max: 5, step: 0.01 },
widthSegments: { value: 32, min: 0, max: 256, step: 1 }, // 양의 정수만 가능
heightSegments: { value: 32, min: 0, max: 256, step: 1 },
phiStart: { value: 0, min: 0, max: 360, step: 0.1 }, // y 축 기준
phiLength: { value: 360, min: 0, max: 360, step: 0.1 }, // y 축 기준
thetaStart: { value: 0, min: 0, max: 180, step: 0.1 }, // y 축 기준
thetaLength: { value: 180, min: 0, max: 180, step: 0.1 }, // y 축 기준
});
useEffect(() => {
refWireMesh.current.geometry = refMesh.current.geometry;
}, [radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength]);
return (
<>
<OrbitControls />
<ambientLight intensity={0.1} />
<directionalLight position={[2, 1, 3]} intensity={0.5} />
<mesh ref={refMesh}>
<sphereGeometry
args={[
radius,
widthSegments,
heightSegments,
phiStart * (Math.PI / 180),
phiLength * (Math.PI / 180),
thetaStart * (Math.PI / 180),
thetaLength * (Math.PI / 180),
]}
/>
<meshStandardMaterial color='#1abc9c' />
</mesh>
<mesh ref={refWireMesh}>
<meshStandardMaterial emissive='yellow' wireframe={true} />
</mesh>
<axesHelper scale={10} />
</>
);
};
export default SphereLeva;
src/components/3DEl/SphereLeva.jsx
import { useRef, useEffect } from 'react';
import { OrbitControls } from '@react-three/drei';
import { useControls } from 'leva';
const CylinderLeva = () => {
const refMesh = useRef();
const refWireMesh = useRef();
const { topRadius, bottomRadius, height, radialSegments, heightSegments, bOpen, thetaStart, thetaLength } =
useControls({
topRadius: { value: 1, min: 0.1, max: 5, step: 0.01 },
bottomRadius: { value: 1, min: 0.1, max: 5, step: 0.01 },
height: { value: 1, min: 0.1, max: 5, step: 0.01 },
radialSegments: { value: 32, min: 3, max: 256, step: 1 },
heightSegments: { value: 1, min: 1, max: 256, step: 1 },
bOpen: { value: false },
thetaStart: { value: 0, min: 0, max: 360, step: 0.01 },
thetaLength: { value: 360, min: 0, max: 360, step: 0.01 },
});
useEffect(() => {
refWireMesh.current.geometry = refMesh.current.geometry;
}, [topRadius, bottomRadius, height, radialSegments, heightSegments, thetaStart, thetaLength]);
return (
<>
<OrbitControls />
<ambientLight intensity={0.1} />
<directionalLight position={[2, 1, 3]} intensity={0.5} />
<mesh ref={refMesh}>
<cylinderGeometry
args={[
topRadius,
bottomRadius,
height,
radialSegments,
heightSegments,
bOpen,
thetaStart * (Math.PI / 180),
thetaLength * (Math.PI / 180),
]}
/>
<meshStandardMaterial color='#1abc9c' />
</mesh>
<mesh ref={refWireMesh}>
<meshStandardMaterial emissive='yellow' wireframe={true} />
</mesh>
<axesHelper scale={10} />
</>
);
};
export default CylinderLeva;