はじめに
(VRoidは)初投稿です。
今回は3Dキャラクターモデル作成ソフトVRoidStudioで作成したモデルに、ポージングデザインソフトVRM Posing Desktopでモーションを付け加えて、Three.jsでブラウザ上で表示させるまでのスリリング(個人感)なチャレンジを行い、その手順を簡潔にまとめようと思います。
実動作
以下で公開しています。
GtiHub Action - VRoidByThreejs
使用した機器情報
- MacStudio2023(M2 Max)
- macOS Tahoe(26.1)
モーション作成手順とツール紹介
1. VRoidStudioでモデル作成
- 公式サイトからツールのダウンロードしてインストール(無料)
- 動作確認のためにシンプルなデザインをテンプレートから選択
- 共有アイコンから「VRMエクスポート」で
.vrmファイル出力
2. VRM Posing Desktopでモーション作成
VRM Posing Desktopは、ゲーム配信サービスのSteamで、有料で配信されています。(2026年2月現在では¥1,400)
起動までの流れ
- Steam公式からSteamアプリをダウンロードしてインストール(無料)
- Steamアカウントでログイン(登録は無料)
- VRMPosingDesktopのストアページへ移動(「vrmposing」で検索すれば見つかる)
- 購入のための情報を入力して、カートに入れてから購入決定
- 「ライブラリ」タグからツールのインストール
- Steamアプリから(重要)
VRM Posing Desktopを起動
ポーズからモーション作成
VRoidで作成したモデルは仕様が共通なので、モーション作成時にモデルが一致していなくても問題ないです。
- 「歯車」ボタンから設定を開いて「FPS」の数値を「60」にする
- 「ポーズ選択」ボタンを押してポーズ一覧から好みのポーズを選択
- 右側のウィンドウの「追加」ボタンを押して、モーションとして追加する
- ポーズを追加すると書くポーズの差分を補完してモーションが形成されていく
- 「再生」ボタンを押してモーションの確認
- ポーズ項目のスライダーで「次の」ポーズに移行するまでのフレーム数(1〜32)を調整
ポーズ組み合わせ例
注意点
-
VRMAはループ時に最後のポーズと最初のポーズとの保管を行わないため、最後には最初と同じポーズを登録しておく必要があります
| 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
エクスポート
モーションウィンドウ左下の「出し入れ」ボタンを押して、「エクスポート」から「.vrma」を選択してファイル出力します。
- ツールを閉じるとモーションデータは保持されないので、「エクスポート」から「.vpdloop」を選択して保存もしたほうが良いです
ブラウザ表示手順とツール紹介
1. Node.jsでプロジェクト作成
今回はTypeScriptで書きました。
npm init -y
npm i -D vite typescript
npx tsc --init
2. VRMとVRMAの読み込み
インストール
npm install -S @pixiv/three-vrm @pixiv/three-vrm-animation
インポート
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { VRMLoaderPlugin, VRMUtils, VRM } from '@pixiv/three-vrm';
import { createVRMAnimationClip, VRMAnimationLoaderPlugin, VRMAnimation } from "@pixiv/three-vrm-animation";
VRM系ファイルローダーの準備
const loader = new GLTFLoader();
loader.register((parser) => {
return new VRMLoaderPlugin(parser);
});
loader.register((parser) => {
return new VRMAnimationLoaderPlugin(parser);
});
VRM系ファイルの読み込み関数
function load(url: string) {
loader.load(
url,
// ロード時に呼ばれる
(gltf) => {
const vrm = gltf.userData.vrm;
const vrmAnimations = gltf.userData.vrmAnimations;
// VRMが有効なら
if (vrm) {
tryInitVRM(vrm);
}
// VRMAが有効なら
if (vrmAnimations) {
tryInitVRMA(vrmAnimations);
}
},
// プログレス時に呼ばれる
(progress) => console.log(
"Loading model...",
100.0 * (progress.loaded / progress.total), "%"
),
// エラー時に呼ばれる
(error) => console.error(error)
);
}
VRMインスタンスをシーンに追加する関数
function tryInitVRM(vrm: VRM) {
currentVrm = vrm;
scene.add(vrm.scene);
VRMUtils.rotateVRM0(vrm); // VRM0.x読み込み時の回転補正(必要であれば)
initAnimationClip();
}
VRMAインスタンスを保持する関数
function tryInitVRMA(vrmAnimations: VRMAnimation[]) {
currentVrmAnimation = vrmAnimations[0] ?? null;
initAnimationClip();
}
VRMとVRMAをミキシングする関数
function initAnimationClip() {
if (currentVrm && currentVrmAnimation) {
currentMixer = new THREE.AnimationMixer(currentVrm.scene);
const clip = createVRMAnimationClip(currentVrmAnimation, currentVrm);
currentAction = currentMixer.clipAction(clip);
currentAction.play();
setupGUI(currentAction, currentMixer);
}
}
ファイル読み込み
load("./models/SimpleGirl02.vrm");
load("./models/vpdloop_kick01.vrma");
3. 表示処理
// clockの準備
const clock = new THREE.Clock();
clock.start();
// アニメーションループ
function animate() {
requestAnimationFrame(animate);
// モーション更新
const deltaTime = clock.getDelta();
if (currentMixer) {
if (!settings.paused) {
currentMixer.update(deltaTime);
if (currentAction) {
settings.time = currentAction.time;
}
}
}
// VRMの更新
if (currentVrm) {
currentVrm.update(deltaTime);
}
// レンダリング
renderer.render(scene, camera);
}
animate();
4. その他
カメラ制御はOrbitControlsを使用
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
...
const controls = new OrbitControls(camera, renderer.domElement);
// 自動回転設定
controls.autoRotate = true;
controls.autoRotateSpeed = 4.0;
// 注視点設定
controls.target.set(0, 1.0, 0);
controls.update();
function animate() {
requestAnimationFrame(animate);
controls.update(); // 自動回転のためにフレーム実行
...
}
モーション時間操作にlil-guiを使用
npm install lil-gui
import GUI from 'lil-gui'
...
function setupGUI(action: THREE.AnimationAction, mixer: THREE.AnimationMixer) {
if (!action) return;
// モーションの長さを取得
const clip = action.getClip();
const duration = clip.duration;
// ポーズ|レジュームボタン
gui.add(settings, 'togglePause').name('Pause | Resume');
// モーション時間指定スライダー
gui.add(settings, 'time', 0, duration).name('Time').listen().onChange((t: number) => {
if (action) {
action.time = t;
// 停止中もタイムスライダーを動かしたときは強制的にポーズ状態として更新
settings.paused = true;
// アニメーションを即座に反映
if (mixer) {
mixer.update(0);
}
}
});
}
コード全体
GitHub Pages での公開方法
大枠はTypeScript × Vite × GitHub Pages 超入門に従っています。
- プロジェクトのカレントに
.github/workflows/deploy.ymlファイルを新規作成 -
deploy.ymlの内容はここ - カレントに
vite.config.tsファイルを新規作成 -
vite.config.tsの内容はここ- 記述
<repo>はGitのリポジトリ名を記載
- 記述
- 変更をPushして、GitHubプロジェクトの
Setting->PagesのBuild and deploymentでGitHub Actionsを選択
今後の展望
- 複数のモーションの切り替え
- 背景やオブジェクトの追加
- モーションから独立したモデル操作
参考文献
- 【three-vrm】せっかくなのでTypeScript+webpack+three.jsに入門して、Web上でVRMモデルを表示してみる
- TypeScript × Vite × GitHub Pages 超入門
あとがき
自分語りになりますが、まだ社会人になる30年以上前に出会ったゲーム、「バーチャファイター(セガ)」に大変衝撃を受けまして、当時は3DCG黎明期でプリレンダーなら映像作品で広まり始めてましたが、ゲームのようなインタラクティブなものはデザインもスピードも不満だらけだった時代に、ゲームの超絶進化を感じ取って自分もゲーム開発の道を歩む事を決めた歴史的瞬間でした。
そして現在、高品質な3Dキャラクターはもとよりモーションまでもが個人でも作成可能で、しかもプログラムでも制御可能な所まで来ているのは本当に感慨深いです。
ちなみにバーチャファイターが動作するシステム「MODEL1」の性能の一部をウィキペディアから抜粋しました。正直この性能で60fpsで機敏に動いているのは魔法か何かかと今でも思わざるを得ません。
- CPU
- V60(32bit) @ 16MHz
- 数値演算プロセッサ
DSP(32bit) @ 40MHz
3Dマトリックス・軸回転・浮動小数点演算機能搭載 - RAM
1MB(ROM:47MB)
おわりに
ここまでご閲覧いただきありがとうございます。
今後も思いつくままに記事投稿を続けて行きたい所存であります。
功夫が足りないわ!






