アニメーション

R3F でのアニメーションは大きく3層。(1) useFrame で素直に(2) react-spring で物理ベース(3) 既存の THREE.AnimationMixer を再生。 用途に応じて使い分ける。

レイヤー1: useFrame で直接動かす

一番シンプル。ref.current のプロパティを直接ミューテートする。React の再レンダーは挟まないので軽い。

function Spinner() {
  const ref = useRef<THREE.Mesh>(null!)
  useFrame((_, dt) => {
    ref.current.rotation.y += dt
  })
  return (
    <mesh ref={ref}>
      <torusGeometry />
      <meshStandardMaterial color="orange" />
    </mesh>
  )
}

滑らかに目標に近づける(lerp)

「クリック地点にスムーズに移動」「マウスに追従」など、線形補間で十分なケースが多い。

function FollowTarget({ target }: { target: THREE.Vector3 }) {
  const ref = useRef<THREE.Mesh>(null!)
  useFrame(() => {
    ref.current.position.lerp(target, 0.1)
  })
  return (
    <mesh ref={ref}>
      <sphereGeometry args={[0.3]} />
      <meshStandardMaterial color="aqua" />
    </mesh>
  )
}

スカラ値なら THREE.MathUtils.lerp(from, to, 0.1)。 色は color.lerp(targetColor, 0.1)。回転(クォータニオン)は quaternion.slerp

damp(フレームレート非依存の lerp)

単純な lerp はフレームレート依存になる(120fps では速い、30fps では遅い)。 厳密にやるなら drei の damp 系ヘルパー:

import { damp, damp3 } from "maath/easing"

useFrame((_, dt) => {
  damp3(ref.current.position, target, 0.25, dt)
})

イージング

useFrame 内で時間(state.clock.getElapsedTime())から 進捗 0..1 を計算して、自前のイージングをかけるのが定石。

const easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3)

function PopIn({ from = 0, to = 1, duration = 0.6 }) {
  const ref = useRef<THREE.Mesh>(null!)
  const start = useRef<number | null>(null)

  useFrame((state) => {
    if (start.current === null) start.current = state.clock.getElapsedTime()
    const t = Math.min((state.clock.getElapsedTime() - start.current) / duration, 1)
    const v = from + (to - from) * easeOutCubic(t)
    ref.current.scale.setScalar(v)
  })

  return (
    <mesh ref={ref} scale={from}>
      <icosahedronGeometry args={[0.6, 1]} />
      <meshStandardMaterial color="hotpink" />
    </mesh>
  )
}
よくあるバグ: フレームレート依存

position.x += 0.01 のように固定値で増やすと、 120fps の端末と 30fps の端末で速度が4倍違う。
useFrame((_, dt) => { x += speed * dt }) のように必ず delta を使う

レイヤー2: react-spring(@react-spring/three)

宣言的に「この値からこの値へバネ的に」と書ける。状態と連動するアニメに強い。

npm install @react-spring/three
import { animated, useSpring } from "@react-spring/three"

function ToggleBox() {
  const [active, setActive] = useState(false)
  const { scale, color } = useSpring({
    scale: active ? 1.6 : 1,
    color: active ? "#7dd3fc" : "#fbbf24",
    config: { tension: 220, friction: 18 },
  })

  return (
    <animated.mesh scale={scale} onClick={() => setActive((v) => !v)}>
      <boxGeometry />
      <animated.meshStandardMaterial color={color} />
    </animated.mesh>
  )
}

連鎖(trail)

import { useTrail, animated } from "@react-spring/three"

function StaggeredList({ items }: { items: string[] }) {
  const trail = useTrail(items.length, { from: { y: -2 }, to: { y: 0 } })
  return (
    <>
      {trail.map((style, i) => (
        <animated.mesh key={i} position-x={i * 1.2} position-y={style.y}>
          <boxGeometry />
          <meshStandardMaterial color="aqua" />
        </animated.mesh>
      ))}
    </>
  )
}

命令的に発火: useSpringRef + useChain

複雑な順序のアニメ(A が終わったら B、B が終わったら C)は useChain で。

import { useSpringRef, useSpring, useChain, animated } from "@react-spring/three"

const apiA = useSpringRef()
const apiB = useSpringRef()
const A = useSpring({ ref: apiA, scale: open ? 1 : 0 })
const B = useSpring({ ref: apiB, position: open ? [0, 1, 0] : [0, 0, 0] })

useChain(open ? [apiA, apiB] : [apiB, apiA], [0, 0.3])

レイヤー3: GLTF のアニメーションを再生

Blender 等で作ったキャラのアニメ(歩く・手を振る等)は AnimationMixer で再生する。 drei の useAnimations がそれを楽にする。

import { useGLTF, useAnimations } from "@react-three/drei"

function Robot() {
  const group = useRef<THREE.Group>(null!)
  const { scene, animations } = useGLTF("/robot.glb")
  const { actions, names } = useAnimations(animations, group)

  useEffect(() => {
    actions["Walk"]?.reset().play()
  }, [actions])

  return <primitive ref={group} object={scene} />
}

クロスフェード(歩く → 走る)

const walk = actions.Walk
const run  = actions.Run

useEffect(() => {
  walk.reset().fadeIn(0.2).play()
  return () => { walk.fadeOut(0.2) }
}, [walk])

function startRunning() {
  walk.fadeOut(0.3)
  run.reset().fadeIn(0.3).play()
}

1回限り再生(LoopOnce)

import { LoopOnce } from "three"

const action = actions.Jump
action.setLoop(LoopOnce, 1)
action.clampWhenFinished = true
action.play()

再生位置を制御

action.time = 0.5             // 秒指定
action.timeScale = 0.5        // 半速
action.timeScale = -1         // 逆再生
action.weight = 0.5           // ブレンド重み

モーフターゲット(Shape Keys)

Blender の Shape Keys / Maya の Blendshapes。GLTF にエクスポートされる。

function Face() {
  const ref = useRef<THREE.Mesh>(null!)
  const { nodes } = useGLTF("/face.glb")

  useFrame((state) => {
    const t = (Math.sin(state.clock.elapsedTime * 2) + 1) / 2
    // morphTargetInfluences は配列。名前→indexは morphTargetDictionary
    const dict = ref.current.morphTargetDictionary!
    const inf  = ref.current.morphTargetInfluences!
    inf[dict.smile] = t
  })

  return <primitive ref={ref} object={nodes.Face} />
}

マウス追従の典型パターン

マウスに合わせて頭がチラ見、というよくある演出は useFrame + lerp の組み合わせで簡単。

function ParallaxGroup({ children }: { children: React.ReactNode }) {
  const ref = useRef<THREE.Group>(null!)
  useFrame((state) => {
    const x = state.pointer.x * 0.3
    const y = state.pointer.y * 0.2
    ref.current.rotation.y = THREE.MathUtils.lerp(ref.current.rotation.y, x, 0.05)
    ref.current.rotation.x = THREE.MathUtils.lerp(ref.current.rotation.x, -y, 0.05)
  })
  return <group ref={ref}>{children}</group>
}

カメラのアニメ

カメラを React Spring で動かす定番パターン。animated.perspectiveCamera は標準では無いので、 useFrame + lerp が安全。

function CameraRig({ targetPos }: { targetPos: [number, number, number] }) {
  useFrame((state, dt) => {
    state.camera.position.lerp(new THREE.Vector3(...targetPos), 1 - Math.exp(-dt * 5))
    state.camera.lookAt(0, 0, 0)
  })
  return null
}

スクロール連動(drei の ScrollControls

ページのスクロール量を 0..1 の値として取れる。スクロールで 3D シーンが進むパターンに最適。

import { ScrollControls, useScroll, Scroll } from "@react-three/drei"

<Canvas>
  <ScrollControls pages={3} damping={0.2}>
    <Scene />
    <Scroll html>
      <div style={{ height: "300vh" }}>HTML overlay</div>
    </Scroll>
  </ScrollControls>
</Canvas>

function Scene() {
  const ref = useRef<THREE.Mesh>(null!)
  const scroll = useScroll()
  useFrame(() => {
    const t = scroll.offset       // 0..1
    ref.current.rotation.y = t * Math.PI * 2
  })
  return <mesh ref={ref}> ... </mesh>
}

使い分けの目安

やりたいこと使うもの
連続的に動かす(回転・追従・パーティクル)ループ + lerp(useFrame)
「A から B」を1回だけtween.js / GSAP
UI 連動の状態遷移(ボタンで開閉等)react-spring
キャラのモーションAnimationMixer + GLTF アニメ
スクロール連動drei の ScrollControls
大量のオブジェクトを同期InstancedMesh + シェーダ
framer-motion-3d も選択肢

framer-motion-3d は React アニメで定番の framer-motion を 3D に拡張したもの。 variants やジェスチャー連動が必要なら検討する。
ただしバンドルサイズと活用度を見ると、デフォルトは react-spring か useFrame で済むことが多い。