Three.jsのアニメーションシステムの概要
Three.jsのアニメーションシステムは、「キーフレーム」を使ってオブジェクトの動きを定義します
以下の主要な要素が使われます
- キーフレーム
特定の時間におけるプロパティ(例: 位置や回転)の値を指定するもの
キーフレーム間のプロパティの変化は自動で補間され、滑らかな動きが作られます - KeyframeTrack
「キーフレーム」をまとめたもの
1つのオブジェクトの1つのプロパティ(例: 位置、回転、スケールなど)に対応するアニメーション要素です - AnimationClip
複数の「KeyframeTrack」を組み合わせたもの
これが実際のアニメーションデータとなり、アニメーション全体を表現します
アニメーションを実装するには、以下のステップを踏みます
- アニメーションを適用したいオブジェクトを「AnimationMixer」に紐付けます
const mixer = new THREE.AnimationMixer(object);
- AnimationActionの生成
mixer.clipActionメソッドを使い、「AnimationAction」を作成します
例: const action = mixer.clipAction(animationClip);
「AnimationAction」はアニメーションの操作(再生、一時停止、ループなど)を行います - 毎フレームの更新
アニメーションを動作させるために、毎フレームでmixer.update(deltaTime)を呼び出します
// アニメーションを持つオブジェクト
let meshObj;
// アニメーションミキサーの作成
// AnimationMixerはアニメーションを管理するためのクラスです。
const mixer = new THREE.AnimationMixer(meshObj);
// オブジェクトに紐付けられたアニメーションクリップの取得
// Blenderなどで作成し、エクスポートされたアニメーションデータは
// meshObj.animations配列に格納されています。
const clips = meshObj.animations;
// 特定の名前のアニメーションを再生する
// 'dance' という名前のアニメーションクリップを探し、それを再生します。
const clipName = 'dance'; // 再生したいアニメーションの名前
const clip = THREE.AnimationClip.findByName(clips, clipName); // 名前でクリップを検索
if (clip) {
const action = mixer.clipAction(clip); // クリップからアクションを作成
action.play(); // アクションを再生
} else {
console.warn(`アニメーションクリップ "${clipName}" が見つかりませんでした。`);
}
// 全てのアニメーションを再生する
// clips 配列の中のすべてのアニメーションクリップをループして再生します。
clips.forEach(clip =>{
const action = mixer.clipAction(clip); // 各クリップからアクションを作成
action.play(); // アクションを再生
});
各フレームでミキサーを更新
const clock = new THREE.Clock();
function tick() {
const deltaTime = clock.getDelta(); // 前回のフレームからの経過時間を取得
if (mixer) {
mixer.update(deltaTime);
}
renderer.render(scene, camera)
window.requestAnimationFrame(tick)
}
tick();
再生や停止に関連するメソッド
//アニメーションを停止し、再生位置をリセット
action.stop();
//アニメーションを一時停止します(再開時には中断した位置から再生)
action.pause();
//再生位置をアニメーションの開始位置にリセット
action.reset();
再生ループの設定
action.setLoop(THREE.LoopOnce); // 1回だけ再生
action.setLoop(THREE.LoopRepeat, 3); // 3回繰り返す
//THREE.LoopPingPong: 行ったり来たりするアニメーションを再生
再生終了後の状態制御
*デフォルトはfalse
action.clampWhenFinished = true; // 最後のフレームで停止
時間の設定
//現在の再生位置(秒単位)を取得または設定できる
action.time = 1.5; // 再生位置を1.5秒目に移動
//アニメーションの再生速度を変更します(デフォルトは1)
action.timeScale = 0.5; // 再生速度を半分にする
action.timeScale = 2; // 再生速度を2倍にする
フェードイン・フェードアウト
action.fadeIn(1.0); // 1秒でフェードイン
action.fadeOut(1.0); // 1秒でフェードアウト
他のアニメーションからフェード遷移して再生します
/*
crossFadeFrom(action, duration, warp)
action: 遷移元のアクション
duration: 遷移にかかる時間(秒)
warp: 遷移中の時間スケールを補正するか(通常はtrue)
*/
action.crossFadeFrom(prevAction, 1.0, true); // 1秒で遷移
action.crossFadeTo(nextAction, 1.0, true); // 1秒で次へ遷移
アニメーションの有効/無効を切り替える
action.enabled = false; // アニメーションを無効化
Mixamoを利用する
Mixamoについて
MixamoはAdobeによって提供される、3Dキャラクターアニメーションのためのオンラインプラットフォームです
- 広範囲のアニメーションライブラリ:歩行、走行、ジャンプなど様々なアニメーションを提供しており、それらは様々なタイプのキャラクターに適用可能
- 自動リギング機能:独自の3Dキャラクターモデルをアップロードすると、Mixamoは自動的にモデルに骨格を追加し、アニメーションを適用できる状態にします
- カスタマイズ可能:アニメーションの速度、範囲、角度などを調整し、プロジェクトのニーズに合わせてカスタマイズできます
Mixamoは3Dキャラクターモデルも提供していますが
今回はSketchfabからダウンロードしたモデルにアニメーションを実装します
*Sketchfabは多様な3Dモデルを提供するプラットフォームです
Blenderで調整
Mixamoのアップロードできるファイル形式は、FBX、OBJ、ZIP です
まずはSketchfabからダウンロードしたモデルが対応するファイル形式でない場合や、スケール等の調整が必要な場合は、3Dモデリングソフトウェア(今回はBlenderを使用します)を使い調整して、FBX形式でダウンロードします
*Blenderのエクスポート設定時に注意すること
- 「パスモード」を「コピー」にしてモデルに「テクスチャを含め込む」に設定します
*ただしそれでもモデルのテクスチャが反映されない場合があるので、その場合はMixsomからエクスポートしたモデルに元のモデルのマテリアルをコピーして対応します - 「前方」と「上」はモデルの向きです(Mixamo正面にするため)
ちなみに、Blenderのシェーディングモード(3Dビューポートでオブジェクトをどのように表示するかを決定する機能)を「マテリアルプレビュー」に切り替えないと、マテリアルは表示されません
Mixamoですること
1:モデルをアップロード後に正面を向いていたらNextをクリック
2:マーカーを設定します(自動リギング)
*左右対称の場合「use symmetry」のチェックはそのまま
- 顎 (CHIN): 顔のアニメーションに必要な顎の位置にマーカーを配置(通常は下唇のちょうど下)
- 手首 (WRISTS): 両手首の中心にマーカーを配置
- 肘 (ELBOWS): 両肘の関節の中心点にマーカーを配置
- 膝 (KNEES): 両膝の関節の中心点にマーカーを配置
- 股 (GROIN): キャラクターの股間部分、つまり両足の付け根の中心にマーカーを配置
3:左上のタブの「Animations」をクリックして好きなアニメーションを選択
選択したアニメーションのカスタマイズ
- Arm Wave Range:キャラクターの腕の振り幅
- Overdrive:高く設定すると、アニメーションの動きが大げさに
- Character Arm-Space:腕が体からどれだけ離れるかを調整
- Trim:アニメーションの長さをトリミング
- Mirror:アニメーションが鏡像になります(本来右手が行う動きを左手が行うように)
4 :右上の「DownLoad」ボタンからエクスポート
- Format:ファイルの形式を指定
- Skin:「With Skin」を選択すると、スキン(キャラクターのテクスチャやマテリアル)を含む状態でエクスポートされます。
- Frames per Second (FPS):フレームレートが高いほど、動きが滑らかになりますが、ファイルサイズが大きくなる
- Keyframe Reduction:キーフレームの削減を行うと、アニメーションが持つキーフレームの数が減少し、アニメーションが単純化されますが、ファイルサイズが小さくなる
「none」はキーフレーム削減は行われません
Mixamoからダウンロードしたファイル(通常はFBX形式)をthree.jsで直接利用するためには、Blenderにアップロードして、glTF形式に変換します
備考:もしマテリアルが反映していない場合は、元ファイルのモデルのマテリアルをコピーして貼り付けます
- 最初にコピー先のオブジェクトを選択
- Shiftキーを押しながら、コピー元のオブジェクトを選択
- マテリアルプロパティを見ると、最後に選択されたオブジェクトに適用されたマテリアルが表示されています
- 「マテリアルを選択物にコピー」をクリックします
「ファイル」メニューから「エクスポート」を選び、「glTF 2.0 (.glb/.gltf)」を選択してエクスポートします
three.jsでブラウザに表示する
CDNを使用した必要最低限のコードです(OrbitControlsでマウス操作可能)
ダウンロードした「.glb」ファイルを読み込むます
*CDNを使用すると、追加のビルドプロセスや設定なしに直接ライブラリを読み込むことができますが、three.jsではモジュールバンドラーの使用が望ましいようです
<!DOCTYPE html>
<html lang="ja">
<head>
<title></title>
<style>
* { margin: 0;
padding: 0;
}
html,
body{
overflow: hidden;
}
.webgl{
position: fixed;
top: 0;
left: 0;
outline: none;
}
</style>
</head>
<body>
<canvas class="webgl"></canvas>
<script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"
></script>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.121.1/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.121.1/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three'
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
const gltfLoader = new GLTFLoader();
const sizes = {
width: window.innerWidth,
height: window.innerHeight,
aspectRatio: window.innerWidth / window.innerHeight,
};
let model
let mixer;
gltfLoader.load("ここにダウンロードしたモデルのパス", (gltf) => {
model = gltf.scene;
model.position.y =-1
scene.add(model);
mixer = new THREE.AnimationMixer(gltf.scene);
let currentAction = mixer.clipAction(gltf.animations[0]);
currentAction.play();
});
// Canvas
const canvas = document.querySelector("canvas.webgl");
// Scene
const scene = new THREE.Scene();
//Lights
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 3);
scene.add(directionalLight);
//camera
const camera = new THREE.PerspectiveCamera(35, sizes.width / sizes.height, 0.1, 100)
camera.position.z = 5
scene.add(camera);
// Controls
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
});
const clock = new THREE.Clock()
let previousTime = 0
let deltaTime
function tick() {
const elapsedTime = clock.getElapsedTime();
deltaTime = elapsedTime - previousTime;
previousTime = elapsedTime;
if (mixer) {
mixer.update(deltaTime);
}
renderer.render(scene, camera);
window.requestAnimationFrame(tick);
}
tick()
function handleResize() {
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
sizes.aspectRatio = sizes.width / sizes.height;
camera.aspect = sizes.aspectRatio;
camera.updateProjectionMatrix();
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
}
handleResize();
window.addEventListener("resize", handleResize);
</script>
</body>
</html>