アニメーション
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>
)
}
animated.のプリフィックスを付けた要素は spring 値を受けられる。configでtension(バネ強度)/friction(摩擦)/massを調整。- useSpring(単一)/ useSprings(配列)/ useTrail(連鎖)。
連鎖(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 は React アニメで定番の framer-motion を 3D に拡張したもの。
variants やジェスチャー連動が必要なら検討する。
ただしバンドルサイズと活用度を見ると、デフォルトは react-spring か useFrame で済むことが多い。