[3D] glb -> jsx변환(gltfjsx) (feat. 문제점, scroll 애니매이션)

jini.choi·2024년 5월 5일

3D(R3F)

목록 보기
9/10

glb -> jsx변환(gltfjsx) 및 재사용

glb, gltf 파일을 jsx형식으로 재사용할 수 있게 해주는 라이브러리(해당 파일이 있는 곳으로 터미널 이동 한다음 명령어 입력)

npx gltfjsx phantoms.glb --transform 
  • 변환이 종료되면 glb가 있던 폴더에 jsx파일로 생김 -> component로 이동
/*
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을 추가한다.

  • 구성 확인하는 방법

    • 모델 검사하기: GLTF 파일을 브라우저에서 쉽게 검사할 수 있는 도구인 Babylon.js Sandbox 또는 Three.js Editor를 사용
    • 코드에서 노드 로깅
useEffect(() => {
    console.log(nodes);
  }, []);

gltfjsx문제점

  • gltfjsx는 때때로 모델을 더 단순하고 접근하기 쉬운 구조로 평탄화하여 변환할 수 있기 때문에 원본과 구조가 달라질 수 있다.(내가 겪은 문제...후..)

  • 3d 노드가 children: [Object3D, Mesh, Mesh, Group]로 되어있고, 3번째 index인 Group에는 children:[Mesh, Mesh]로 되어있었다.
    근데 gltfjsx로 압축변형을 하니까 Group를 압축해버려서 하나의 mesh 가 되어버렸고, 디테일한 material 수정을 하지 못하는 문제가 발생했다.

하여 다음과 같은 방법으로 해결했다.

gltfjsx 대신 Three.js의 로더를 사용하여 GLTF 모델을 직접 로드하고 조작

  • GLTF 모델을 로드하고, 특정 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>
  );
}


scroll 애니매이션

  • 3D 객체에 애니메이션 적용하려면 ref사용해야됨

  • robotTwoTl.current .to 세번째 인자는 해당 애니메이션 항목의 시작 시간을 지정

    to() 함수는 세 가지 주요 인자를 받는다.
    Target: 애니메이션을 적용할 대상 객체.
    Vars: 애니메이션의 최종 상태를 설명하는 객체, 예를 들어 { x: 100 }는 x 위치를 100으로 이동시키는 것을 목표.
    Position: (선택적) 타임라인 내에서 이 애니메이션 이벤트가 시작될 시간 또는 레이블을 지정

useFrame

  • 매 프레임마다 실행되는 함수를 등록할 때 사용

  • 애니메이션 또는 지속적으로 업데이트해야 하는 로직을 처리할 때 사용

  • useFrame에 전달된 콜백 함수는 렌더링 루프의 일부로서 주기적으로 호출되며, 이를 통해 실시간으로 객체의 상태를 업데이트

useFrame((state, delta) => {
    robotTwoTl.current.seek(
      robotTwoScroll.offset * robotTwoTl.current.duration()
    );
  });
  • 여기서 useFrame 훅은 매 프레임마다 실행되는 콜백 함수를 받는다. 콜백 함수는 두 개의 인자를 받을 수 있다.

    • state: 현재 렌더링 상태에 대한 정보를 포함하는 객체. 예) 카메라, 씬, 시간 등의 정보를 담고 있다.
    • delta: 이전 프레임과 현재 프레임 사이의 시간 간격을 초 단위로 나타내는 값. 이 값을 사용하여 프레임 간의 동작을 일관되게 유지할 수 있다.
  • robotTwoTl.current.seek(...) 을 호출하여 GSAP 타임라인의 진행 위치를 조정. seek 메소드는 타임라인을 특정 시간 위치로 이동시키는 데 사용

  • robotTwoScroll.offset: useScroll 훅을 통해 얻은 스크롤 위치 값.
    이 값은 스크롤이 시작될 때 0에서 스크롤이 끝날 때 1까지의 비율로 표현.

  • robotTwoTl.current.duration() : 타임라인의 총 지속 시간을 반환.

  • robotTwoScroll.offset * robotTwoTl.current.duration() : 이 계산은 스크롤의 비율을 타임라인의 총 시간으로 환산하여, 타임라인의 해당 시점으로 이동.
    즉, 사용자가 페이지를 스크롤함에 따라 타임라인의 진행 상태가 스크롤 위치에 따라 동기화되어 변화

useLayoutEffect 사용한 이유

  • 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);
  }, []);

추가 효과들

Sparkles

  • drei 제공

  • 반딧불이 같음

<Sparkles size={2} color={"#fff"} scale={[10, 10, 10]}></Sparkles>

Backdrop

  • 컴포넌트는 간단하게 Three.js 장면의 배경을 설정
  • receiveShadow (boolean): Backdrop이 그림자를 받을지 여부를 결정. true일 때, 광원에 의해 그림자 생성.
  • material (Material): Three.js의 Material을 사용하여 Backdrop의 재질을 정의할 수 있다. 이를 통해 색상, 텍스처, 반사 등의 특성을 커스텀할 수 있다.
  • receiveShadow 쓰려면 Canvas에 shadows 속성 추가
<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

  • 우주에 떠있는 느낌
<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>

baffle.js (3D 아님, TextAni)

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밀리초 동안 텍스트를 점차 원래대로 드러냄
  }, []);

전체 코드

  • index.js
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>
);
  • App.js
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;
  • RobotTwo.jsx
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

profile
개발짜🏃‍♀️

0개의 댓글