ํ‹ฐ์Šคํ† ๋ฆฌ ๋ทฐ

๋ฐ˜์‘ํ˜•

๐Ÿ“Œ Three.js์—์„œ 3D ๋ชจ๋ธ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ & ์ œ์–ดํ•˜๊ธฐ

์ด์ „ ๊ธ€์—์„œ๋Š” GLTFLoader๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ 3D ๋ชจ๋ธ์„ ๋ถˆ๋Ÿฌ์˜ค๊ณ , Three.js ์”ฌ์— ๋ฐฐ์น˜ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์› ์Šต๋‹ˆ๋‹ค.
์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ํฌํ•จ๋œ GLTF ๋ชจ๋ธ์„ ๋ถˆ๋Ÿฌ์˜ค๊ณ , ์žฌ์ƒ/์ •์ง€/์†๋„ ์กฐ์ ˆ ๋“ฑ์˜ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.


๐Ÿš€ 1. Three.js์—์„œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•

GLTF ๋ชจ๋ธ(.gltf ๋˜๋Š” .glb)์—๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
Three.js์—์„œ๋Š” AnimationMixer๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ“Œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ ์šฉ ๋ฐฉ๋ฒ•

  1. useGLTF()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ GLTF ๋ชจ๋ธ ๋กœ๋“œ
  2. AnimationMixer๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์• ๋‹ˆ๋ฉ”์ด์…˜ ํด๋ฆฝ์„ ์„ค์ •
  3. useFrame()์„ ํ™œ์šฉํ•˜์—ฌ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์—…๋ฐ์ดํŠธ
  4. ๋ฒ„ํŠผ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์žฌ์ƒ / ์ผ์‹œ์ •์ง€ / ์†๋„ ์กฐ์ ˆ ๊ธฐ๋Šฅ ๊ตฌํ˜„

๐Ÿ›  2. ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ํฌํ•จ๋œ 3D ๋ชจ๋ธ ์ถ”๊ฐ€ํ•˜๊ธฐ

โœ… 1) ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ํฌํ•จ๋œ 3D ๋ชจ๋ธ ๋‹ค์šด๋กœ๋“œ

์•„๋ž˜ ์‚ฌ์ดํŠธ์—์„œ .glb ํŒŒ์ผ์„ ๋‹ค์šด๋กœ๋“œํ•˜์—ฌ ํ”„๋กœ์ ํŠธ์˜ public/models ํด๋”์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”น ๋ฌด๋ฃŒ GLTF ๋ชจ๋ธ ๋‹ค์šด๋กœ๋“œ ์‚ฌ์ดํŠธ

์˜ˆ์ œ ํŒŒ์ผ:

๐Ÿ“‚ public/models/
 โ”ฃ ๐Ÿ“œ animated_robot.glb

โœ… 2) Three.js์—์„œ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์žˆ๋Š” GLTF ๋ชจ๋ธ ๋กœ๋“œ

๐Ÿ“Œ src/components/AnimatedModel.jsx

import { useRef, useEffect } from "react";
import { useGLTF } from "@react-three/drei";
import { useFrame } from "@react-three/fiber";
import * as THREE from "three";

const AnimatedModel = ({ modelPath, animationIndex = 0, isPlaying = true }) => {
  const { scene, animations } = useGLTF(modelPath);
  const mixer = useRef(null);

  useEffect(() => {
    if (animations.length > 0) {
      mixer.current = new THREE.AnimationMixer(scene);
      const action = mixer.current.clipAction(animations[animationIndex]);
      if (isPlaying) {
        action.play();
      } else {
        action.stop();
      }
    }
  }, [scene, animations, animationIndex, isPlaying]);

  useFrame((_, delta) => {
    if (mixer.current) {
      mixer.current.update(delta);
    }
  });

  return <primitive object={scene} scale={1.5} />;
};

export default AnimatedModel;

๐Ÿ“Œ ์„ค๋ช…

  • useGLTF()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ 3D ๋ชจ๋ธ์„ ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค.
  • AnimationMixer๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
  • useFrame()์„ ์‚ฌ์šฉํ•˜์—ฌ ๋งค ํ”„๋ ˆ์ž„๋งˆ๋‹ค ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.

๐ŸŽ› 3. ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ œ์–ด ๊ธฐ๋Šฅ ์ถ”๊ฐ€ํ•˜๊ธฐ

์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์žฌ์ƒ / ์ •์ง€ / ์†๋„ ์กฐ์ ˆํ•  ์ˆ˜ ์žˆ๋„๋ก UI๋ฅผ ์ถ”๊ฐ€ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

๐Ÿ“Œ src/components/AnimationControls.jsx

import { useState } from "react";

const AnimationControls = ({ onPlay, onPause, onSpeedChange }) => {
  const [speed, setSpeed] = useState(1);

  const handleSpeedChange = (e) => {
    const newSpeed = parseFloat(e.target.value);
    setSpeed(newSpeed);
    onSpeedChange(newSpeed);
  };

  return (
    <div style={{ textAlign: "center", margin: "10px" }}>
      <button onClick={onPlay} style={{ margin: "5px" }}>โ–ถ Play</button>
      <button onClick={onPause} style={{ margin: "5px" }}>โธ Pause</button>
      <label style={{ marginLeft: "10px" }}>Speed: </label>
      <input
        type="range"
        min="0.1"
        max="2"
        step="0.1"
        value={speed}
        onChange={handleSpeedChange}
      />
    </div>
  );
};

export default AnimationControls;

๐Ÿ— 4. ์ตœ์ข…์ ์œผ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ปจํŠธ๋กค๋Ÿฌ ์ ์šฉ

์ด์ œ AnimatedModel๊ณผ AnimationControls๋ฅผ ModelViewer์— ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“Œ src/components/ModelViewer.jsx

import { useState } from "react";
import { Canvas } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei";
import AnimatedModel from "./AnimatedModel";
import AnimationControls from "./AnimationControls";

const ModelViewer = () => {
  const [isPlaying, setIsPlaying] = useState(true);
  const [speed, setSpeed] = useState(1);

  return (
    <div>
      <Canvas camera={{ position: [0, 2, 5], fov: 50 }}>
        <ambientLight intensity={0.5} />
        <directionalLight position={[5, 5, 5]} intensity={1} />
        <AnimatedModel modelPath="/models/animated_robot.glb" isPlaying={isPlaying} />
        <OrbitControls />
      </Canvas>

      {/* ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ปจํŠธ๋กค UI */}
      <AnimationControls
        onPlay={() => setIsPlaying(true)}
        onPause={() => setIsPlaying(false)}
        onSpeedChange={setSpeed}
      />
    </div>
  );
};

export default ModelViewer;

๐ŸŽฏ 5. App.jsx์—์„œ ModelViewer ์ ์šฉ

๐Ÿ“Œ src/App.jsx

import ModelViewer from "./components/ModelViewer";

function App() {
  return (
    <div>
      <h1 style={{ textAlign: "center", marginTop: "20px" }}>3D ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ทฐ์–ด</h1>
      <ModelViewer />
    </div>
  );
}

export default App;

โœ… 6. ์ตœ์ข… ๊ฒฐ๊ณผ

โœ” GLTF ๋ชจ๋ธ์—์„œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋ถˆ๋Ÿฌ์™€ ์žฌ์ƒ
โœ” ์• ๋‹ˆ๋ฉ”์ด์…˜ ์žฌ์ƒ / ์ •์ง€ ๋ฒ„ํŠผ ์ถ”๊ฐ€
โœ” ์†๋„ ์กฐ์ ˆ ๊ธฐ๋Šฅ ์ ์šฉ
โœ” OrbitControls๋กœ ๋งˆ์šฐ์Šค๋กœ 3D ๋ชจ๋ธ ์กฐ์ž‘ ๊ฐ€๋Šฅ

๐Ÿš€ ์ด์ œ npm run dev๋กœ ์‹คํ–‰ํ•˜๋ฉด 3D ์บ๋ฆญํ„ฐ๊ฐ€ ์›€์ง์ด๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!


๐Ÿ”ฅ 7. ๋‹ค์Œ ๋‹จ๊ณ„: ์กฐ๋ช… & ๊ทธ๋ฆผ์ž ์ ์šฉํ•˜๊ธฐ

์ง€๊ธˆ๊นŒ์ง€ ์šฐ๋ฆฌ๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ํฌํ•จ๋œ 3D ๋ชจ๋ธ์„ ๋กœ๋“œํ•˜๊ณ , ์žฌ์ƒ ๋ฐ ์ •์ง€, ์†๋„ ์กฐ์ ˆ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
๋‹ค์Œ ๊ธ€์—์„œ๋Š” ์กฐ๋ช…๊ณผ ๊ทธ๋ฆผ์ž๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋”์šฑ ์‚ฌ์‹ค์ ์ธ 3D ํ™˜๊ฒฝ์„ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

โœ… ๋‹ค์Œ ํŽธ ์˜ˆ๊ณ :
"Three.js์—์„œ ์กฐ๋ช…๊ณผ ๊ทธ๋ฆผ์ž ์ ์šฉํ•˜๊ธฐ – ๋” ํ˜„์‹ค์ ์ธ 3D ์”ฌ ๋งŒ๋“ค๊ธฐ" ๐Ÿš€

 

 

โ€ป ์ด ํฌ์ŠคํŒ…์€ ์ฟ ํŒก ํŒŒํŠธ๋„ˆ์Šค ํ™œ๋™์˜ ์ผํ™˜์œผ๋กœ, ์ด์— ๋”ฐ๋ฅธ ์ผ์ •์•ก์˜ ์ˆ˜์ˆ˜๋ฃŒ๋ฅผ ์ œ๊ณต๋ฐ›์Šต๋‹ˆ๋‹ค.
๊ณต์ง€์‚ฌํ•ญ
์ตœ๊ทผ์— ์˜ฌ๋ผ์˜จ ๊ธ€
์ตœ๊ทผ์— ๋‹ฌ๋ฆฐ ๋Œ“๊ธ€
Total
Today
Yesterday
๋งํฌ
ยซ   2025/07   ยป
์ผ ์›” ํ™” ์ˆ˜ ๋ชฉ ๊ธˆ ํ† 
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
๊ธ€ ๋ณด๊ด€ํ•จ
๋ฐ˜์‘ํ˜•