アニメーション
Three.js でのアニメーションは大きく3層。(1) ループでプロパティを直接書き換える、 (2) Tween 系で値を補間、(3) AnimationMixer で GLTF などのキーフレームを再生。
レイヤー1: ループで直接動かす
最も素直な方法。setAnimationLoop の中でプロパティを書き換えるだけ。
const clock = new THREE.Clock()
renderer.setAnimationLoop(() => {
const dt = clock.getDelta()
const t = clock.getElapsedTime()
mesh.rotation.y += dt * 1.0 // 1秒で 1 ラジアン
mesh.position.y = Math.sin(t * 2) // 振動
controls?.update()
renderer.render(scene, camera)
})
delta を使う
position.x += 0.01 のような固定値は端末のリフレッシュレートに依存。
120fps と 30fps で速度が4倍違う。delta または elapsedTime ベースで書く。
線形補間(lerp)— 滑らかに目標に近づける
「マウスに追従」「クリック地点に滑らかに移動」など、残差の何割を埋めるかで書ける。
// Vector3 の lerp
mesh.position.lerp(targetPos, 0.1) // 1フレームで 10% 近づく
// スカラ値
mesh.rotation.y = THREE.MathUtils.lerp(mesh.rotation.y, targetY, 0.1)
// 色
mat.color.lerp(targetColor, 0.1)
// クォータニオン(回転は slerp)
mesh.quaternion.slerp(targetQuat, 0.1)
「0.1」は1フレームあたりの追従率。フレームレート依存なので、
厳密性を求めるなら 1 - Math.exp(-rate * dt) のようなレート補正を入れる。
イージング
独自カーブで動かしたいときは、進捗 0..1 を作って関数に通す。
const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3)
const easeInOutCubic = (t) => t < 0.5
? 4 * t * t * t
: 1 - Math.pow(-2 * t + 2, 3) / 2
let start = null
const duration = 1.0
renderer.setAnimationLoop(() => {
const now = clock.getElapsedTime()
if (start === null) start = now
const t = Math.min((now - start) / duration, 1)
const v = easeOutCubic(t)
mesh.scale.setScalar(0.1 + v * 0.9)
renderer.render(scene, camera)
})
レイヤー2: Tween 系ライブラリ
宣言的に「この値からこの値まで 1.5s でイージング付きで動かす」と書ける。
定番は tween.js と GSAP。
tween.js(@tweenjs/tween.js)
npm install @tweenjs/tween.js
import { Tween, Easing, update as tweenUpdate } from "@tweenjs/tween.js"
new Tween(mesh.position)
.to({ x: 5, y: 2, z: 0 }, 1500)
.easing(Easing.Cubic.InOut)
.onComplete(() => console.log("done"))
.start()
renderer.setAnimationLoop(() => {
tweenUpdate() // tween をフレームごとに進める
renderer.render(scene, camera)
})
GSAP
npm install gsap
import gsap from "gsap"
gsap.to(mesh.position, { x: 5, duration: 1.5, ease: "power2.inOut" })
gsap.to(mesh.rotation, { y: Math.PI * 2, duration: 2, repeat: -1, ease: "none" })
// 連鎖
const tl = gsap.timeline()
tl.to(mesh.scale, { x: 2, y: 2, z: 2, duration: 0.5 })
.to(mesh.position, { y: 3, duration: 0.5 })
.to(mesh.rotation, { y: Math.PI, duration: 0.5 })
GSAP は自分で update を呼ぶ必要がない(内部で rAF を回している)。 ループの中でレンダリングだけしておけば、ティーンが裏で値を変えてくれる。
レイヤー3: AnimationMixer — キーフレーム再生
Blender 等で作ったキャラのアニメ、GLTF に埋め込まれたアニメを再生する仕組み。
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"
const gltf = await new GLTFLoader().loadAsync("/robot.glb")
scene.add(gltf.scene)
const mixer = new THREE.AnimationMixer(gltf.scene)
const action = mixer.clipAction(gltf.animations[0]) // 最初のクリップ
action.play()
const clock = new THREE.Clock()
renderer.setAnimationLoop(() => {
mixer.update(clock.getDelta()) // 必須
renderer.render(scene, camera)
})
AnimationAction の API
| メソッド/プロパティ | 意味 |
|---|---|
play() / stop() / reset() | 再生 / 停止 / リセット |
fadeIn(duration) / fadeOut(duration) | フェードイン / アウト |
crossFadeTo(other, duration) | 別アクションに滑らかに移行 |
setLoop(LoopOnce, 1) | 1回のみ。LoopRepeat / LoopPingPong |
clampWhenFinished = true | 終了時に最終ポーズで止める |
timeScale | 再生速度(負で逆再生) |
weight | ブレンドの重み(0..1) |
クリップを名前で取得
const walk = THREE.AnimationClip.findByName(gltf.animations, "Walk")
const action = mixer.clipAction(walk)
action.play()
クロスフェード(歩く → 走る)
const walk = mixer.clipAction(walkClip)
const run = mixer.clipAction(runClip)
walk.play()
// 0.3 秒かけて run に切り替え
walk.crossFadeTo(run.reset().play(), 0.3, false)
イベント
finished や loop イベントが取れる。
mixer.addEventListener("finished", (e) => {
console.log("clip finished:", e.action)
})
mixer.addEventListener("loop", (e) => {
console.log("loop count:", e.loopDelta)
})
自前で AnimationClip を作る
コードからキーフレームを定義することもできる。プロシージャルな動きの再利用に。
const positionTrack = new THREE.VectorKeyframeTrack(
".position", // 対象プロパティ
[0, 1, 2], // 時刻(秒)
[
0, 0, 0, // t=0
1, 1, 0, // t=1
0, 0, 0, // t=2
],
)
const clip = new THREE.AnimationClip("MyClip", 2, [positionTrack])
const mixer = new THREE.AnimationMixer(mesh)
mixer.clipAction(clip).play()
主なトラック種別:
VectorKeyframeTrack— Vector2/3 用(.positionなど)QuaternionKeyframeTrack— 回転(.quaternion)ColorKeyframeTrack— 色(.material.color)NumberKeyframeTrack— スカラ値BooleanKeyframeTrack/StringKeyframeTrack— 離散値
シーンの状態に応じて切り替える
呼吸する待機モーション
renderer.setAnimationLoop(() => {
const t = clock.getElapsedTime()
mesh.scale.y = 1 + Math.sin(t * 2) * 0.05 // ふわふわ
renderer.render(scene, camera)
})
マウス追従の頭の動き
window.addEventListener("pointermove", (e) => {
pointer.x = (e.clientX / innerWidth) * 2 - 1
pointer.y = -(e.clientY / innerHeight) * 2 + 1
})
renderer.setAnimationLoop(() => {
head.rotation.y = THREE.MathUtils.lerp(head.rotation.y, pointer.x * 0.5, 0.1)
head.rotation.x = THREE.MathUtils.lerp(head.rotation.x, -pointer.y * 0.3, 0.1)
renderer.render(scene, camera)
})
使い分けの目安
| やりたいこと | 使うもの |
|---|---|
| 連続的に動かす(回転・追従) | ループ + lerp |
| 「A から B」を1回だけ | tween.js / GSAP |
| キャラのモーション | AnimationMixer + GLTF アニメ |
| UI 連動の状態遷移 | GSAP(タイムラインが書きやすい) |
| 大量のオブジェクトを同期 | ループ + シェーダ(CPU 負荷が高くなるため) |