R3F(React Three Fiber) - Geometry

H.GOO·2024년 4월 2일
0

WEB-3D-Project

목록 보기
2/9

🪴R3F 정의

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

=> GitHub 3D-Project


파일구조

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



🪴R3F의 Canvas

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;



🪴변환 (Transformation)

모니터 기준의 WebGL 에서 3D 좌표계


WebGL 에서 회전 방향


3D 객체에서 mesh의 Transformation 요소

position (위치)

rotation (회전)

scale (크기)


Transformation 컨트롤

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

모든 Geometry는 BufferGeometry를 상속 받음.

three.js 에 제공되는 기본 Geometry


Geometry를 생성하는 3가지 방법

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;

Geometry의 args 속성

<boxGeometry args={[1, 1, 1, 1, 1, 1]} />

new THREE.BoxGeometry(1, 1, 1, 1, 1, 1)

R3F의 boxGeometry 요소의 args 속성을 이용하여 두번째 줄의 코드를 사용하지 않고 쉽게 속성을 지정할 수 있음.


boxGeometry 조작

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;

sphereGeometry 조작

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;

cylinderGeometry 조작

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;



0개의 댓글