glb, gltf 파일을 jsx형식으로 재사용할 수 있게 해주는 라이브러리(해당 파일이 있는 곳으로 터미널 이동 한다음 명령어 입력)
npx gltfjsx phantoms.glb --transform
/*
Auto-generated by: https://github.com/pmndrs/gltfjsx
Command: npx gltfjsx@6.2.16 phantoms.glb --transform
Files: phantoms.glb [6.41MB] > /Users/jini.choi/Documents/jini/3d_gsap_test/public/models/robot/phantoms-transformed.glb [192.39KB] (97%)
*/
import React, { useRef } from "react";
import { useGLTF } from "@react-three/drei";
export function Robot(props) {
//경로 수정
const { nodes, materials } = useGLTF(
"./models/robot/phantoms-transformed.glb"
);
return (
<group {...props} dispose={null}>
<mesh
geometry={nodes.Cylindre005.geometry}
material={nodes.Cylindre005.material}
position={[-0.017, 2.754, -0.066]}
rotation={[-Math.PI, 0, 0]}
scale={0.582}
/>
<mesh
geometry={nodes.Cube001.geometry}
material={materials.Metal}
position={[-0.214, 0.163, 0.365]}
rotation={[0, -0.152, 0]}
scale={0.146}
/>
</group>
);
}
useGLTF.preload("./models/robot/phantoms-transformed.glb");
필요없는 부분은 지우고 재질을 메탈로 변경하기 위해 닫는 태그를 생성하고 자식으로 material을 추가한다.
구성 확인하는 방법
useEffect(() => {
console.log(nodes);
}, []);
gltfjsx는 때때로 모델을 더 단순하고 접근하기 쉬운 구조로 평탄화하여 변환할 수 있기 때문에 원본과 구조가 달라질 수 있다.(내가 겪은 문제...후..)
3d 노드가 children: [Object3D, Mesh, Mesh, Group]로 되어있고, 3번째 index인 Group에는 children:[Mesh, Mesh]로 되어있었다.
근데 gltfjsx로 압축변형을 하니까 Group를 압축해버려서 하나의 mesh 가 되어버렸고, 디테일한 material 수정을 하지 못하는 문제가 발생했다.
하여 다음과 같은 방법으로 해결했다.
import { useGLTF } from "@react-three/drei";
export function RobotTwo() {
const robotTwo = useGLTF("./models/robot/phantoms.glb");
const scene = robotTwo.scene;
let cube003, cube003_1;
scene.traverse((child) => {
if (child.isMesh) {
if (child.name === "Cube003") {
cube003 = child;
} else if (child.name === "Cube003_1") {
cube003_1 = child;
}
}
});
if (!scene) {
return null;
}
return (
<group
position={[-0.214, 0.163, 0.365]}
rotation={[0, -0.152, 0]}
scale={0.146}
>
<mesh
geometry={cube003.geometry}
position={cube003.position}
rotation={cube003.rotation}
scale={cube003.scale}
>
<meshPhysicalMaterial
color="#aaa"
roughness={0.2}
metalness={1}
reflectivity={0.5}
iridescence={0.3}
iridescenceIOR={1}
iridescenceThicknessRange={[100, 100]}
/>
</mesh>
<mesh
geometry={cube003_1.geometry}
position={cube003_1.position}
rotation={cube003_1.rotation}
scale={cube003_1.scale}
>
<meshPhysicalMaterial
color="#000"
roughness={1}
emissive={"#000"}
clearcoat={1}
metalness={0}
reflectivity={0.2}
iridescence={0.1}
iridescenceIOR={1}
iridescenceThicknessRange={[100, 1000]}
/>
</mesh>
</group>
);
}
3D 객체에 애니메이션 적용하려면 ref사용해야됨
robotTwoTl.current .to 세번째 인자는 해당 애니메이션 항목의 시작 시간을 지정
to() 함수는 세 가지 주요 인자를 받는다.
Target: 애니메이션을 적용할 대상 객체.
Vars: 애니메이션의 최종 상태를 설명하는 객체, 예를 들어 { x: 100 }는 x 위치를 100으로 이동시키는 것을 목표.
Position: (선택적) 타임라인 내에서 이 애니메이션 이벤트가 시작될 시간 또는 레이블을 지정
매 프레임마다 실행되는 함수를 등록할 때 사용
애니메이션 또는 지속적으로 업데이트해야 하는 로직을 처리할 때 사용
useFrame에 전달된 콜백 함수는 렌더링 루프의 일부로서 주기적으로 호출되며, 이를 통해 실시간으로 객체의 상태를 업데이트
useFrame((state, delta) => {
robotTwoTl.current.seek(
robotTwoScroll.offset * robotTwoTl.current.duration()
);
});
여기서 useFrame 훅은 매 프레임마다 실행되는 콜백 함수를 받는다. 콜백 함수는 두 개의 인자를 받을 수 있다.
robotTwoTl.current.seek(...) 을 호출하여 GSAP 타임라인의 진행 위치를 조정. seek 메소드는 타임라인을 특정 시간 위치로 이동시키는 데 사용
robotTwoScroll.offset: useScroll 훅을 통해 얻은 스크롤 위치 값.
이 값은 스크롤이 시작될 때 0에서 스크롤이 끝날 때 1까지의 비율로 표현.
robotTwoTl.current.duration() : 타임라인의 총 지속 시간을 반환.
robotTwoScroll.offset * robotTwoTl.current.duration() : 이 계산은 스크롤의 비율을 타임라인의 총 시간으로 환산하여, 타임라인의 해당 시점으로 이동.
즉, 사용자가 페이지를 스크롤함에 따라 타임라인의 진행 상태가 스크롤 위치에 따라 동기화되어 변화
useLayoutEffect 는 컴포넌트들이 render 된 후 실행되며, 그 이후에 paint 가 된다. 이 작업은 동기적(synchronous) 으로 실행. 사용자는 깜빡임을 경험하지 않는다.
useEffect 는 컴포넌트들이 render 와 paint 된 후 실행. 비동기적(asynchronous) 으로 실행. 사용자는 화면의 깜빡임을 보게된다.
기본적으로는 항상 useEffect 만을 사용하는 것을 권장하나, state 이 조건에 따라 첫 painting 시 다르게 렌더링 되어야 할 때는 useEffect 사용 시 처음에 0이 보여지고 이후에 re-rendering 되며 화면이 깜빡거려지기 때문에 useLayoutEffect 를 사용하는 것이 바람직
const robotTwoRef = useRef();
const robotTwoScroll = useScroll();
const robotTwoTl = useRef();
useFrame((state, delta) => {
robotTwoTl.current.seek(
robotTwoScroll.offset * robotTwoTl.current.duration()
);
});
useLayoutEffect(() => {
robotTwoTl.current = gsap.timeline({
defaults: { duration: 2, ease: "power1.inOut" },
});
robotTwoTl.current
.to(robotTwoRef.current.rotation, { y: -1 }, 2)
.to(robotTwoRef.current.position, { x: 1 }, 2)
.to(robotTwoRef.current.rotation, { y: 1 }, 6)
.to(robotTwoRef.current.position, { x: -1 }, 6)
.to(robotTwoRef.current.rotation, { y: 0 }, 11)
.to(robotTwoRef.current.rotation, { x: 1 }, 11)
.to(robotTwoRef.current.position, { x: 0 }, 11)
.to(robotTwoRef.current.rotation, { y: 0 }, 13)
.to(robotTwoRef.current.rotation, { x: -1 }, 13)
.to(robotTwoRef.current.position, { x: 0 }, 13)
.to(robotTwoRef.current.rotation, { y: 0 }, 16)
.to(robotTwoRef.current.rotation, { x: 0 }, 16)
.to(robotTwoRef.current.position, { x: 0 }, 16)
.to(robotTwoRef.current.rotation, { y: 0 }, 20)
.to(robotTwoRef.current.rotation, { x: 0 }, 20)
.to(robotTwoRef.current.position, { x: 0 }, 20);
}, []);
drei 제공
반딧불이 같음
<Sparkles size={2} color={"#fff"} scale={[10, 10, 10]}></Sparkles>
- receiveShadow (boolean): Backdrop이 그림자를 받을지 여부를 결정. true일 때, 광원에 의해 그림자 생성.
- material (Material): Three.js의 Material을 사용하여 Backdrop의 재질을 정의할 수 있다. 이를 통해 색상, 텍스처, 반사 등의 특성을 커스텀할 수 있다.
<Backdrop
receiveShadow
floor={20.5} // Stretches the floor segment, 0.25 by default
segments={100} // Mesh-resolution, 20 by default
scale={[50, 30, 10]}
position={[4, -10, 0]}
>
<meshStandardMaterial color="#0a1a1f" />
</Backdrop>
<Float
speed={4} // Animation speed, defaults to 1
rotationIntensity={1} // XYZ rotation intensity, defaults to 1
floatIntensity={1} // Up/down float intensity, works like a multiplier with floatingRange,defaults to 1
floatingRange={[1, 1]} // Range of y-axis values the object will float within, defaults to [-0.1,0.1]
>
<Ring
scale={3.5}
position-z={-2.5}
position-y={-1}
args={[0, 0.95, 60]}
receiveShadow
>
<meshStandardMaterial color={"#2a3a3f"} />
</Ring>
</Float>
텍스트를 임의의 문자로 일시적으로 "가리는" 작업을 수행하여 시각적으로 혼란을 주는 효과를 만들어내는 JavaScript 라이브러리
useEffect(() => {
const target = baffle(".title");
target.set({
characters: "░P░h░a░n░t░o░m░", //텍스트가 가려질 때 사용할 문자
speed: 100, / 가려진 텍스트가 변경되는 속도를 지정합니다. (단위: 밀리초)
});
target.start(); // 텍스트 가리기를 시작
target.reveal(1000, 1000); // 1000밀리초 후에 시작하여, 1000밀리초 동안 텍스트를 점차 원래대로 드러냄
}, []);
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { Canvas } from "@react-three/fiber";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Canvas shadows>
<App />
</Canvas>
</React.StrictMode>
);
import {
Backdrop,
Environment,
Float,
Ring,
Scroll,
ScrollControls,
Sparkles,
} from "@react-three/drei";
import { useEffect, useRef } from "react";
import { RobotTwo } from "./components/RobotTwo";
import baffle from "baffle";
function App() {
useEffect(() => {
const target = baffle(".title");
target.set({
characters: "░P░h░a░n░t░o░m░",
speed: 100,
});
target.start();
target.reveal(1000, 1000);
}, []);
return (
<>
<color attach="background" args={["#333"]} />
<ambientLight intensity={0.2} />
<spotLight
position={[0, 25, 0]}
angle={1.3}
penumbra={1}
castShadow
intensity={2}
shadow-bias={-0.0001}
/>
<Environment preset="warehouse" />
<ScrollControls pages={6} damping={0.1}>
{/* Canvas contents in here will *not* scroll, but receive useScroll! */}
<RobotTwo scale={0.8} />
<Sparkles size={2} color={"#fff"} scale={[10, 10, 10]}></Sparkles>
<Backdrop
receiveShadow
floor={20.5} // Stretches the floor segment, 0.25 by default
segments={100} // Mesh-resolution, 20 by default
scale={[50, 30, 10]}
position={[4, -10, 0]}
>
<meshStandardMaterial color="#0a1a1f" />
</Backdrop>
<Float
speed={4} // Animation speed, defaults to 1
rotationIntensity={1} // XYZ rotation intensity, defaults to 1
floatIntensity={1} // Up/down float intensity, works like a multiplier with floatingRange,defaults to 1
floatingRange={[1, 1]} // Range of y-axis values the object will float within, defaults to [-0.1,0.1]
>
<Ring
scale={3.5}
position-z={-2.5}
position-y={-1}
args={[0, 0.95, 60]}
receiveShadow
>
<meshStandardMaterial color={"#2a3a3f"} />
</Ring>
</Float>
<Scroll></Scroll>
<Scroll html style={{ width: "100%" }}>
<h1
className="title"
style={{
color: "#cdcbca",
position: "absolute",
top: `65vh`,
left: "50%",
fontSize: "13em",
transform: `translate(-50%,-50%)`,
}}
>
PHANTOM
</h1>
<div className="row" style={{ position: "absolute", top: `132vh` }}>
<h2>Be a Man of the Future.</h2>
<p style={{ maxWidth: "400px" }}>
Featuring a sleek, metallic design inspired by advanced
technology, this aftershave bottle is as stylish as it is
functional. But it's not just a pretty face - inside, you'll find
a nourishing and protective aftershave formula that will leave
your skin feeling refreshed and hydrated.
</p>
<button>Read more</button>
</div>
<div className="row" style={{ position: "absolute", top: `230vh` }}>
<div
className="col"
style={{ position: "absolute", right: `40px`, width: "540px" }}
>
<h2 style={{ maxWidth: "440px" }}>Tech-Savvy Side</h2>
<p style={{ maxWidth: "440px" }}>
Featuring a sleek, metallic design inspired by advanced
technology, this aftershave bottle is as stylish as it is
functional. But it's not just a pretty face - inside, you'll
find a nourishing and protective aftershave formula that will
leave your skin feeling refreshed and hydrated.
</p>
<button>Read more</button>
</div>
</div>
<h2
style={{
position: "absolute",
top: "350vh",
left: "50%",
transform: `translate(-50%,-50%)`,
}}
>
Cutting-Edge of Grooming
</h2>
<button
style={{
position: "absolute",
top: `590vh`,
left: "50%",
transform: `translate(-50%,-50%)`,
}}
>
Buy now
</button>
</Scroll>
</ScrollControls>
</>
);
}
export default App;
import { useGLTF, useScroll } from "@react-three/drei";
import { useLayoutEffect, useRef } from "react";
import gsap from "gsap";
import { useFrame } from "@react-three/fiber";
export function RobotTwo(props) {
const robotTwo = useGLTF("./models/robot/phantoms.glb");
const robotTwoRef = useRef();
const robotTwoScroll = useScroll();
const robotTwoTl = useRef();
useFrame((state, delta) => {
robotTwoTl.current.seek(
robotTwoScroll.offset * robotTwoTl.current.duration()
);
});
useLayoutEffect(() => {
robotTwoTl.current = gsap.timeline({
defaults: { duration: 2, ease: "power1.inOut" },
});
robotTwoTl.current
.to(robotTwoRef.current.rotation, { y: -1 }, 2)
.to(robotTwoRef.current.position, { x: 1 }, 2)
.to(robotTwoRef.current.rotation, { y: 1 }, 6)
.to(robotTwoRef.current.position, { x: -1 }, 6)
.to(robotTwoRef.current.rotation, { y: 0 }, 11)
.to(robotTwoRef.current.rotation, { x: 1 }, 11)
.to(robotTwoRef.current.position, { x: 0 }, 11)
.to(robotTwoRef.current.rotation, { y: 0 }, 13)
.to(robotTwoRef.current.rotation, { x: -1 }, 13)
.to(robotTwoRef.current.position, { x: 0 }, 13)
.to(robotTwoRef.current.rotation, { y: 0 }, 16)
.to(robotTwoRef.current.rotation, { x: 0 }, 16)
.to(robotTwoRef.current.position, { x: 0 }, 16)
.to(robotTwoRef.current.rotation, { y: 0 }, 20)
.to(robotTwoRef.current.rotation, { x: 0 }, 20)
.to(robotTwoRef.current.position, { x: 0 }, 20);
}, []);
const scene = robotTwo.scene;
let cube003, cube003_1;
scene.traverse((child) => {
if (child.isMesh) {
if (child.name === "Cube003") {
cube003 = child;
} else if (child.name === "Cube003_1") {
cube003_1 = child;
}
}
});
if (!scene) {
return null;
}
return (
<group {...props} dispose={null} ref={robotTwoRef}>
<group
position={[-0.214, 0.163, 0.365]}
rotation={[0, 0, 0]}
scale={0.146}
>
<mesh
castShadow
geometry={cube003.geometry}
position={cube003.position}
rotation={cube003.rotation}
scale={cube003.scale}
>
<meshPhysicalMaterial
color="#aaa"
roughness={0.2}
metalness={1}
reflectivity={0.5}
iridescence={0.3}
iridescenceIOR={1}
iridescenceThicknessRange={[100, 100]}
/>
</mesh>
<mesh
castShadow
geometry={cube003_1.geometry}
position={cube003_1.position}
rotation={cube003_1.rotation}
scale={cube003_1.scale}
>
<meshPhysicalMaterial
color="#000"
roughness={1}
emissive={"#000"}
clearcoat={1}
metalness={0}
reflectivity={0.2}
iridescence={0.1}
iridescenceIOR={1}
iridescenceThicknessRange={[100, 1000]}
/>
</mesh>
</group>
</group>
);
}
<다음은 MeshPortalMaterial 배우기>
https://github.com/pmndrs/drei?tab=readme-ov-file#meshportalmaterial