본문 바로가기
Graphic/R3F

60 3D Text

by curious week 2025. 8. 18.

React Three Fiber – 3D Text

  • Text3D, Center 같은 drei 헬퍼 사용
  • Matcap 머티리얼 활용
  • Donut 100개 랜덤 생성 및 최적화
  • 애니메이션 (useFrame) 적용

1. 기본 환경 설정

export default function Experience() {
  return (
    <>
      <Perf position="top-left" />
      <OrbitControls makeDefault />

      {/* 기본 텍스트 (폰트 필요) */}
      <Text3D font="./fonts/helvetiker_regular.typeface.json">
        HELLO R3F
        <meshNormalMaterial />
      </Text3D>
    </>
  );
}
  • <Text3D>: drei 제공, 내부적으로 TextGeometry 사용
  • font="./fonts/helvetiker_regular.typeface.json": JSON 폰트 지정
  • <meshNormalMaterial />: 기본적으로 법선 기반 머티리얼

2. 텍스트 중앙 정렬 (Center)

<Center>
  <Text3D
    font="./fonts/helvetiker_regular.typeface.json"
    size={0.75}
    height={0.2}
    curveSegments={12}
    bevelEnabled
    bevelThickness={0.02}
    bevelSize={0.02}
    bevelOffset={0}
    bevelSegments={5}>
    HELLO R3F
    <meshNormalMaterial />
  </Text3D>
</Center>
  • <Center>: 자동 중앙 정렬
  • size, height, bevel*: TextGeometry의 파라미터 그대로 사용 가능

3. Matcap 머티리얼 적용

import { useMatcapTexture } from '@react-three/drei';

export default function Experience() {
  // @react-three/drei 내부에 matcap texture 컬렉션에 포함된 matcap을 CDN으로 로딩함
  const [matcapTexture] = useMatcapTexture(
    '7B5254_E9DCC7_B19986_C8AC91', // matcap ID
    256, // 해상도
  );

  return (
    <Center>
      <Text3D
        font="./fonts/helvetiker_regular.typeface.json"
        size={0.75}
        height={0.2}>
        HELLO R3F
        <meshMatcapMaterial matcap={matcapTexture} />
      </Text3D>
    </Center>
  );
}
  • useMatcapTexture(ID, size): matcap 자동 로드
  • <meshMatcapMaterial matcap={...} />: 재질 적용

4. Donuts 추가

단일 Donut

<mesh>
  <torusGeometry />
  <meshMatcapMaterial matcap={matcapTexture} />
</mesh>

100개 랜덤 Donut

{
  [...Array(100)].map((_, index) => (
    <mesh
      key={index}
      position={[
        (Math.random() - 0.5) * 10,
        (Math.random() - 0.5) * 10,
        (Math.random() - 0.5) * 10,
      ]}
      scale={0.2 + Math.random() * 0.2}
      rotation={[Math.random() * Math.PI, Math.random() * Math.PI, 0]}>
      <torusGeometry />
      <meshMatcapMaterial matcap={matcapTexture} />
    </mesh>
  ));
}
  • key={index}: React warning 방지
  • Math.random() 활용해 위치/스케일/회전 무작위 설정

5. 성능 최적화

문제점

  • Donut마다 geometry, material 생성 → 퍼포먼스 저하
  • 실제로는 동일한 torusGeometry, 동일한 material을 공유하면 충분

개선 (Three.js 변수 공유)

import * as THREE from 'three';

const torusGeometry = new THREE.TorusGeometry(1, 0.6, 16, 32);
const material = new THREE.MeshMatcapMaterial();

export default function Experience() {
  const [matcapTexture] = useMatcapTexture('7B5254_E9DCC7_B19986_C8AC91', 256);

  useEffect(() => {
    matcapTexture.colorSpace = THREE.SRGBColorSpace;
    matcapTexture.needsUpdate = true;

    material.matcap = matcapTexture;
    material.needsUpdate = true;
  }, []);

  return (
    <>
      <Center>
        <Text3D
          font="./fonts/helvetiker_regular.typeface.json"
          size={0.75}
          height={0.2}
          material={material}>
          HELLO R3F
        </Text3D>
      </Center>

      {[...Array(100)].map((_, index) => (
        <mesh
          key={index}
          geometry={torusGeometry}
          material={material}
          position={[
            (Math.random() - 0.5) * 10,
            (Math.random() - 0.5) * 10,
            (Math.random() - 0.5) * 10,
          ]}
          scale={0.2 + Math.random() * 0.2}
          rotation={[Math.random() * Math.PI, Math.random() * Math.PI, 0]}
        />
      ))}
    </>
  );
}
  • geometry, material → 함수 밖에서 생성, 모든 mesh 공유
  • useEffect: matcapTexture 설정 및 업데이트

needsUpdate = true 설정하면:

  • 속성을 바꾸면 셰이더를 다시 컴파일해야 하기 때문에 반드시 업데이트 필요.
  • 처음 텍스처를 로드했을 때 → 자동으로 GPU에 업로드 되지만 중간에 텍스처 속성(wrapS, wrapT, colorSpace, minFilter 등)을 변경하면, GPU에 올라간 데이터와 CPU 쪽 데이터가 불일치.
  • 다음 렌더링 프레임에서 Three.js가 감지해서 CPU 메모리에 있는 텍스처 데이터를 다시 GPU VRAM으로 업로드(갱신).

6. Donuts 애니메이션

방법 1 – group 사용

const donutsGroup = useRef()

<group ref={donutsGroup}>
    { [...Array(100)].map((_, i) =>
        <mesh key={i} geometry={torusGeometry} material={material} />
    )}
</group>

useFrame((_, delta) => {
    for (const donut of donutsGroup.current.children) {
        donut.rotation.y += delta * 0.2
    }
})

방법 2 – 배열 ref 사용 (더 정석)

const donuts = useRef([]);

{
  [...Array(100)].map((_, i) => (
    <mesh
      key={i}
      ref={(el) => (donuts.current[i] = el)}
      geometry={torusGeometry}
      material={material}
    />
  ));
}

useFrame((_, delta) => {
  for (const donut of donuts.current) {
    donut.rotation.y += delta * 0.2;
  }
});
  • useRef([]): 초기값 빈 배열
  • ref={(el) => donuts.current[i] = el}: 각 mesh를 배열에 저장
  • useFrame: 매 프레임 회전 애니메이션 적용

📌 핵심 요약

  1. 텍스트: <Text3D> + <Center>로 간단하게 구현 가능
  2. 재질: useMatcapTexture + <meshMatcapMaterial>로 깔끔한 스타일
  3. 도넛 생성: Array(100).map() → 랜덤 위치/크기/회전
  4. 성능 최적화: geometry/material 공유 → draw call 최소화
  5. 애니메이션: useFrame + ref로 객체 반복 제어 가능

import {
  useMatcapTexture,   // Drei 제공: matcap 텍스처를 간단히 불러오기
  Center,             // Drei 제공: 자식 오브젝트를 (0,0,0) 중심에 배치
  Text3D,             // Drei 제공: 3D 텍스트 생성
  OrbitControls,      // Drei 제공: 마우스로 카메라 회전/줌/이동 제어
} from '@react-three/drei';

import { useFrame } from '@react-three/fiber'; // R3F 루프 훅 (매 프레임 호출)
import { Perf } from 'r3f-perf';               // 성능 모니터링 UI
import { useEffect, useRef } from 'react';
import * as THREE from 'three';

// ----------------------------------------------------
// ✅ 성능 최적화를 위해 매 프레임마다 Geometry/Material을 새로 만들지 않고
//   바깥에서 미리 생성해둠. (Native Three.js 스타일 최적화)
// ----------------------------------------------------
const torusGeometry = new THREE.TorusGeometry();
const material = new THREE.MeshMatcapMaterial();

export default function Experience() {
  // ----------------------------------------------------
  // useMatcapTexture(ID, size)
  // - drei에서 제공하는 Matcap 컬렉션을 CDN으로부터 로드
  // - 첫 번째 인자: matcap의 고유 ID (PNG 파일명 기반)
  // - 두 번째 인자: 텍스처 해상도(px) → 보통 64/128/256/512
  // ----------------------------------------------------
  const [matcapTexture] = useMatcapTexture(
    '7B5254_E9DCC7_B19986_C8AC91', // 고유 matcap ID (갈색/베이지 톤)
    256,                           // 해상도(px)
  );

  // ----------------------------------------------------
  // useEffect: 텍스처와 머티리얼 초기화
  // - R3F는 colorSpace 자동 설정을 해주지만,
  //   Native Three.js처럼 직접 관리할 때는 sRGB 세팅 필요.
  // ----------------------------------------------------
  useEffect(() => {
    matcapTexture.colorSpace = THREE.SRGBColorSpace; // 색공간 보정
    matcapTexture.needsUpdate = true;

    material.matcap = matcapTexture; // 불러온 텍스처를 머티리얼에 적용
    material.needsUpdate = true;
  }, []);

  // ----------------------------------------------------
  // Ref 배열 활용: 여러 개의 mesh를 회전시키기 위해 개별 참조 저장
  // - donuts.current = [mesh1, mesh2, mesh3, ...]
  // ----------------------------------------------------
  const donuts = useRef([]);

  // ----------------------------------------------------
  // useFrame: 매 프레임 호출
  // - delta: 이전 프레임과의 시간 차 (초 단위)
  // - 모든 도넛 mesh를 순회하며 회전값을 갱신
  // ----------------------------------------------------
  useFrame((state, delta) => {
    for (const donut of donuts.current) {
      donut.rotation.y += delta * 0.1;
    }
  });

  return (
    <>
      {/* FPS 및 GPU 메모리 등 성능 정보 표시 */}
      <Perf position="top-left" />
      {/* 카메라 제어기 */}
      <OrbitControls makeDefault />

      {/* Center: 자식들을 중심좌표에 맞춤 */}
      <Center>
        {/* Text3D: Drei 제공, 3D 폰트 출력 */}
        <Text3D
          material={material} // 미리 만든 matcap 머티리얼 적용
          font="./fonts/helvetiker_regular.typeface.json" // JSON 폰트
          size={0.75}        // 텍스트 크기
          height={0.2}       // 깊이(Extrude)
          curveSegments={12} // 곡선 분할 수 (품질에 영향)
          bevelEnabled       // 베벨(엣지 라운딩) 활성화
          bevelSize={0.02}
          bevelThickness={0.02}
          bevelOffset={0}
          bevelSegments={5}>
          HELLO R3F
        </Text3D>
      </Center>

      {/* ------------------------------------------------
          도넛 100개 생성
          - geometry와 material은 재사용
          - 각 mesh는 랜덤 위치, 크기, 회전값을 가짐
          ------------------------------------------------ */}
      {[...Array(100)].map((_, index) => (
        <mesh
          ref={(element) => {
            donuts.current[index] = element; // 각 mesh를 배열에 저장
          }}
          key={index}
          geometry={torusGeometry} // 성능 최적화: 미리 정의한 geometry
          material={material}      // 성능 최적화: 미리 정의한 material
          position={[
            (Math.random() - 0.5) * 10, // X: -5 ~ +5
            (Math.random() - 0.5) * 10, // Y: -5 ~ +5
            (Math.random() - 0.5) * 10, // Z: -5 ~ +5
          ]}
          scale={0.2 + Math.random() * 0.2} // 크기: 0.2 ~ 0.4
          rotation={[Math.random() * Math.PI, Math.random() * Math.PI, 0]} // 초기 회전
        />
      ))}
    </>
  );
}