[유용한 라이브러리]

JAMEe_·2024년 7월 1일

R3F

목록 보기
4/24

gltfjsx

gltf 혹은 glb 파일을 jsx 파일로 만들어주는 라이브러리
npx gltfjsx public/dancer.glb -o src/components/Dancer.jsx

primitive 컴포넌트에서 선언한 glb 와는 다르게
내부적으로 다 뜯어서 수정이 가능하여 제약이 없음

/* eslint-disable react/no-unknown-property */
import React, { useEffect, useRef, useState } from "react";
import { useGLTF, useAnimations } from "@react-three/drei";

export function Dancer(props) {
  const group = useRef();
  const { nodes, materials, animations } = useGLTF("/dancer.glb");
  const { actions } = useAnimations(animations, group);

  const [currentAnimation, setCurrentAnimation] = useState("wave");

  useEffect(() => {
    group.current!.traverse((obj) => {
      if (obj.isMesh) {
        obj.castShadow = true;
        obj.receiveShadow = true;
      }
    });
  }, []);

  useEffect(() => {
    actions[currentAnimation]?.fadeIn(0.5).play();
    return () => {
      actions[currentAnimation]?.fadeOut(0.5).stop();
    };
  }, [actions, currentAnimation]);
  
  return (
    <group
      onClick={() => {
        setCurrentAnimation((prev) => {
          if (prev === "wave") return "windmill";
          return "wave";
        });
      }}
      scale={0.01}
      position-y={0.8}
      ref={group}
      {...props}
      dispose={null}
    >
      <group name="AuxScene">
        <group position={[0, -82.942, -1.295]}>
          <primitive object={nodes.mixamorigHips} />
          <skinnedMesh
            name="Ch03"
            geometry={nodes.Ch03.geometry}
            material={materials.Ch03_Body}
            skeleton={nodes.Ch03.skeleton}
          >
            // 색상 커스텀
            <meshStandardMaterial color={0xff0000} />
          </skinnedMesh>
        </group>
      </group>
    </group>
  );
}

useGLTF.preload("/dancer.glb");

postProcessing

후처리로써 렌더링된 3D 장면에 추가적인 처리를 통해 특수한 효과를 주는 프로세스
yarn add @react-three/postprocessing

EffectComposer 컴포넌트 children 에 효과를 넣어주는 방식으로 작성

import { EffectComposer } from "@react-three/postprocessing";
import React from "react";

export default function PostProcessor() {
  return (
      <EffectComposer enableNormalPass/>
        <Bloom />
      </EffectComposer>
  );
}

enableNormalPass: 법선 벡터 허용 여부
※ 법선 벡터란? 3D 그래픽에서 표면의 특정 지점에서 수직으로 뻗어나오는 벡터.
바닥에서는 위쪽으로, 구의 경우엔 태양이 빛을 내듯 360도 방향에서 뻗어나감. 알록달록 효과

효과들 하나씩 알아보기

Bloom: 화면의 밝은 영역 주변에 후광 효과를 줄 때
mipmapBlur: 3D 표면에 텍스쳐를 입히는 기술 / 뿌연 안개 효과
luminanceThreshold: 밝기가 이 값보다 높은 픽셀만 Blur 효과를 갖도록 ( 0 ~ 1 )
노랑색 밝기는 대략 0.928 이므로 Blur 효과를 받음
luminanceSmoothing: Blur 효과 받는 범위 ( 0 ~ 1 )

BrightnessContrast
brightness: 밝기 ( -1 ~ 1 )
contrast: 밝기 대조 ( 값이 클수록 밝은 곳은 더 밝게 어두운곳은 더 어둡게 )

DotScreen
angle: 점들의 위치
scale: 점의 크기

Glitch: 지지직 효과
delay: 최솟값과 최댓값 사이의 시간에 무작위로 효과 발생
duration: 지지직 효과를 최솟값과 최댓값 사이의 무작위 시간에 걸쳐 지속
strength: 지지직 효과 강도를 최솟값과 최댓값 사이의 값으로 적용
ratio: 값이 클수록 strength 의 강한 강도 효과 일어날 확률 증가

Grid
scale: grid 의 크기
lineWidth: 선의 굵기

HueSaturation
hue: 색상환을 기준으로 해당 각도의 색상을 입힘
saturation: 채도. 색상의 선명한 정도

Pixelation: 모자이크 느낌 픽셀화
granularity: 픽셀화의 강도

Sepia: 빛이 바랜듯한 효과


Cannon

간단하게 R3F 에서 물리엔진 적용
자세한 내용: https://velog.io/@jamee_/%EB%AC%BC%EB%A6%AC%EC%97%94%EC%A7%84-%EC%9E%90%EB%8F%99%EC%B0%A81

동작원리
보이지 않는 가상의 3D 캐논 요소를 Mesh 요소에 대입함으로써
대입이 된 순간부터는 Mesh 가 position 혹은 rotation 과 같은 동작이
넣어준 값대로 변경되는것이 아닌 물리적인 상황이나 충돌에 따라서
3D 캐논 요소가 position 혹은 rotation 설정을 하도록 위임

적용방법
물리엔진을 적용할 Mesh 들을 Physics 컴포넌트로 감싸주면서 활성화 시킴
Box drei 를 사용하는 경우 useBox 를 이용해 물리엔진을 적용할 Box ref 를 만들어주고, 해당 ref 를 Mesh 컴포넌트의 ref 로 할당해주면 useBox 로써 반환된 초기값에 따라 position 과 rotation 값이 물리적인 상황의 충돌에 따라 결정

주의점
두께가 있는 Mesh 를 사용해야 충돌이 정상적으로 발생함
여기서는 Plane drei 는 두께가 없어 임의로 Box 요소의 Plane 을 생성

// MainCanvas.tsx

import { Physics } from "@react-three/cannon";
import { Canvas } from "@react-three/fiber";
import React from "react";
import { Color } from "three";
import MeshPhysics from "./MeshPhysics";

export default function MainCanvas() {
  return (
    <Canvas
      gl={{ antialias: true }}
      shadows={"soft"}
      // shadows={{ enabled: true, type: THREE.PCFSoftShadowMap }}
      camera={{
        fov: 60,
        aspect: window.innerWidth / window.innerHeight,
        near: 0.1,
        far: 100,
        // 카메라의 위치
        position: [10, 10, 10],
      }}
      scene={{ background: new Color(0x000000) }}
    >
      <Physics
        // 0, -9, 0 방향으로 힘이 작용. 여기선 바닥으로 힘이 작용한다 보면 된다
        gravity={[0, -9, 0]}
        // 별도로 특정 오브젝트끼리 충돌할 때 적용할 물리 속성을 선언하지 않으면 이 값이 적용됨
        defaultContactMaterial={{
          restitution: 0.1,
          friction: 1,
        }}
      >
        <MeshPhysics />
      </Physics>
    </Canvas>
  );
}
// MeshPhysics.tsx

import { useBox, useSphere } from "@react-three/cannon";
import { Box, Plane, Sphere } from "@react-three/drei";
import React, { useEffect } from "react";

export default function MeshPhysics() {
  const [planeRef] = useBox(() => ({
    args: [50, 1, 50],
    // 물리적인 충돌이나 중력이 있어도 해당 ref 는 움직임이 발생하지 않음
    // default 값은 Dynamic
    type: "Static",
    // 질량 ( 무거운 정도 )
    mass: 1,
    position: [0, 0, 0],
    // mesh 의 material 이 아닌 cannon 의 material
    material: {
      // 탄성력
      restitution: 1,
      // 마찰력
      friction: 0.5,
    },
    // 충돌 후 이벤트
    onCollide: () => {
      console.log("바닥에 충돌하였습니다");
    },
  }));
  
  // api : 자연적인 힘(충돌)이 아닌 인위적인 힘을 줄때 사용
  // applyForce : 지속적으로 힘을 가함
  // applyImpulse : 한번에 충격을 주고 그 뒤론 힘을 가하지 않음
  const [boxRef, api] = useBox(() => ({
    args: [1, 1, 1],
    mass: 1,
    position: [1, 3, 1],
    material: {
      restitution: 0.4,
      friction: 0.2,
    },
  }));

  const [sphereRef1, sphereApi] = useSphere(() => ({
    mass: 5,
    position: [0.5, 8, 0],
    material: {
      restitution: 0.4,
      friction: 0.1,
    },
  }));
  
  const [sphereRef2] = useSphere(() => ({
    mass: 0.1,
    position: [1, 10, 0],
    material: {
      restitution: 0.2,
      friction: 0.1,
    },
  }));

  useEffect(() => {
    // 전체좌표 [1,0,0] 에서 [555,50,0] 방향으로 힘을 가함
    api.applyForce([550, 50, 0], [1, 0, 0]);
    // 해당 Mesh 의 [1,0,0] 에서 [-2000,0,0] 방향으로 힘을 가함
    sphereApi.applyLocalForce([-2000, 0, 0], [1, 0, 0]);
  }, [api, sphereApi]);

  useEffect(() => {
    const timeout = setTimeout(() => {
      api.applyLocalImpulse([0, 20, 0], [1, 0, 0]);
      sphereApi.applyImpulse([100, 10, 0], [0, 0, 0]);
    }, 3000);

    return () => {
      clearInterval(timeout);
    };
  }, [api, sphereApi]);
  
  return (
    <>
      <Box ref={planeRef} args={[50, 1, 50]}>
        <meshStandardMaterial
          color={0xfefefe}
          roughness={0.3}
          metalness={0.8}
        />
      </Box>
      <Box ref={boxRef} args={[1, 1, 1]} position-y={1}>
        <meshStandardMaterial
          color={0xff0000}
          roughness={0.3}
          metalness={0.8}
        />
      </Box>
      <Sphere ref={sphereRef1}>
        <meshStandardMaterial
          color={0x9000ff}
          roughness={0.3}
          metalness={0.8}
        />
      </Sphere>
      <Sphere ref={sphereRef2}>
        <meshStandardMaterial
          color={0xff00ff}
          roughness={0.3}
          metalness={0.8}
        />
      </Sphere>
    </>
  );
}
profile
안녕하세요

0개의 댓글