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>
</>
);
}