はじめに
前回までで、Three.js + MMD ベースの時計アプリに、複数 Actor の表示や順番トークの仕組みを作成。
今回は、未実装だった「 Morph 」を実装してみたのでそのメモです。
実装するにあたり、一覧に名前が出ていても、実際には見た目の変化がかなり弱いことや、Morph が効いていないように見えるときに、
- 実装が悪いのか
- 適用先が違うのか
- そもそもモデル側の差分が弱いのか
この切り分けがしづらい。
そこで今回は、MMD モデルの Rig 情報を確認するための Rig パネル を実装し、 Bone / Morph の一覧表示に加えて、検索、Bones / Morphs の切り替え、Morph value の入力、Apply / Reset まで揃え、表情確認用の検証 UI として使える状態にしています。
動画(YouTube):
Three.js × MMDでMorph UIを実装 | Rigパネル表示・ウィンク確認 #short
Three.js + MMD の時計アプリに Morph UI を追加しました。Rigパネルから Bone / Morph 一覧を確認し、Apply / Reset で表情変化を検証しています。今回はウィンクMorphの確認までできた実装メモです。▼実装内容 - Rigパネル追加 - Bone / Morph 一...
https://www.youtube.com/shorts/LszKIkcaCFU動画(PC):
今回作ったもの
今回追加したのは、MMD モデルの Bone / Morph 情報をまとめて確認するための Rig パネル だ。
実装した主な機能は次の通り。
- Rigパネルの追加
- Bone数 / Morph数の表示
- Search
- Bones / Morphs の切り替え
- Selection 表示
- Morph value 入力
- Apply / Reset
単に一覧を表示するだけではなく、選択した Morph をその場で適用して表情変化を確認できる ところまで実装している。 これによって、どの Morph が実際に使えそうかを UI 上で直接確認できるようになった。
今回の UI はこんな形になった。
右側に Rig パネルを配置し、Bone / Morph の件数、検索、モード切り替え、Selection、Apply / Reset をまとめている。 左上 HUD でも Mesh / Vert / Tri / Bone / Morph / Mat を確認できるようにしてあるので、モデルの状態をざっくり把握しやすい。
なぜ Rig パネルが必要だったのか
この UI が必要だった理由はかなり実務的だ。
まず、モデルによっては Morph 名が存在していても、見た目の変化がかなり弱いものがある。 一覧だけ見えても、本当に効いているのかどうか が分からない。
また、PMX / MMD 系のモデルは個体差が大きい。 笑顔やウィンクのように分かりやすい Morph もあれば、差分が小さくてほとんど視認できないものもある。 実際に適用してみないと、どれが実用的か判断しづらい。
さらに Bone 情報もデバッグ用には見たかった。 Rig 周りを触るときは、表情だけでなく Bone 名の確認も必要になる。 後でボーンマーカーや姿勢制御に広げることを考えると、一覧 UI を持っておく意味は大きい。
加えて、今回は単一モデルではなく 複数 Actor 管理 が前提になってきている。 Actor ごとに情報を把握しながら実験していくには、手元に確認パネルがないとかなり厳しい。
つまり今回の Rig パネルは、見た目の機能追加というより、 今後の表情・姿勢・デバッグを進めるための足場作り に近い。
実装の全体方針
今回の実装で中心になったのは、Morph を UI から直接適用する処理と、それを支える Rig パネル側の UI だ。
構成としては大きく次の 2 つに分かれる。
mmd.js側で Morph の適用とリセットを受け持つui.js側で Rig パネルを描画し、選択状態や Apply / Reset のイベントをつなぐ
この分離にしておくと、UI 側は「何を選んだか」に集中できて、 MMD 側は「どう適用するか」に集中できるので整理しやすい。
applyActorMorphByName の実装
まず、Morph 適用の中核が applyActorMorphByName だ。
この関数では最初に actorId を元に対象 Actor を取得する。
複数 Actor 管理になっているので、どのモデルへ適用するのかを明確にするために id ベースで引く形にしている。
export function applyActorMorphByName(actorId, morphName, value = 1) {
const actor = actors.find((a) => a.id === actorId);
if (!actor) {
console.warn("[morph] actor not found");
return false;
}
const target = actor.mesh ?? actor.root ?? actor.model ?? actor.scene;
if (!target) {
console.warn("[morph] actor target not found", actor);
return false;
}
let applied = false;
target.traverse?.((obj) => {
if (!obj?.morphTargetDictionary || !obj?.morphTargetInfluences) return;
const idx = obj.morphTargetDictionary[morphName];
if (idx == null) return;
console.log("[morph before]", obj.name, morphName, idx, obj.morphTargetInfluences[idx]);
obj.morphTargetInfluences[idx] = value;
console.log("[morph after ]", obj.name, morphName, idx, obj.morphTargetInfluences[idx]);
applied = true;
});
if (!applied && target.morphTargetDictionary && target.morphTargetInfluences) {
const idx = target.morphTargetDictionary[morphName];
if (idx != null) {
console.log("[morph before]", target.name, morphName, idx, target.morphTargetInfluences[idx]);
target.morphTargetInfluences[idx] = value;
console.log("[morph after ]", target.name, morphName, idx, target.morphTargetInfluences[idx]);
applied = true;
}
}
return applied;
}
ここで重要なのはこの行だ。
const target = actor.mesh ?? actor.root ?? actor.model ?? actor.scene;
今回、Morph が効かないように見えた原因のひとつがこの適用先だった。
最初は actor.root 側を見ていたが、実際には actor.mesh を優先して見る必要があった。
一覧は取得できているのに表情が変わらない、という状態だったので、 この適用先を見直したことで前進できた。
実際の適用では、traverse() を使って対象以下のオブジェクトを走査し、
morphTargetDictionary と morphTargetInfluences を持つものを探している。
target.traverse?.((obj) => {
if (!obj?.morphTargetDictionary || !obj?.morphTargetInfluences) return;
const idx = obj.morphTargetDictionary[morphName];
if (idx == null) return;
obj.morphTargetInfluences[idx] = value;
applied = true;
});
Morph 名から index を引き、その index に対して value を代入するだけのシンプルな処理だが、
どのオブジェクトにその辞書と influence がぶら下がっているか を正しく見極める必要がある。
さらに、traverse() を持たない単体 mesh 用のフォールバックも入れている。
if (!applied && target.morphTargetDictionary && target.morphTargetInfluences) {
const idx = target.morphTargetDictionary[morphName];
if (idx != null) {
target.morphTargetInfluences[idx] = value;
applied = true;
}
}
この形にしておくと、モデル構造の差に対して多少強くなる。
resetActorMorphs の実装
Morph の確認作業では、適用できることと同じくらい すべて元に戻せること が大事だ。
そのために resetActorMorphs を用意した。
export function resetActorMorphs(actorId) {
const actor = actors.find((a) => a.id === actorId);
if (!actor) return false;
const target = actor.mesh ?? actor.root ?? actor.model ?? actor.scene;
if (!target) {
console.warn("[morph] actor target not found", actor);
return false;
}
let reset = false;
target.traverse?.((obj) => {
if (!obj?.morphTargetInfluences) return;
for (let i = 0; i < obj.morphTargetInfluences.length; i++) {
obj.morphTargetInfluences[i] = 0;
}
reset = true;
});
if (!reset && target.morphTargetInfluences) {
for (let i = 0; i < target.morphTargetInfluences.length; i++) {
target.morphTargetInfluences[i] = 0;
}
reset = true;
}
return reset;
}
やっていることは単純で、対象内の morphTargetInfluences を順番に走査して全部 0 に戻している。
target.traverse?.((obj) => {
if (!obj?.morphTargetInfluences) return;
for (let i = 0; i < obj.morphTargetInfluences.length; i++) {
obj.morphTargetInfluences[i] = 0;
}
reset = true;
});
Morph の棚卸しをするときは、ある Morph を試して、戻して、次を試す、という流れになる。 この Reset がないと作業効率がかなり落ちる。
Rig パネル側の UI 実装
UI 側では renderRigPanel() で Rig パネルの描画とイベントバインドをまとめている。
まず、Rig パネルでは Bone数 / Morph数を表示し、検索欄と表示モードを持たせた。
表示モードは bones と morphs を切り替えられるようにしてあり、必要に応じてリスト内容を更新する。
let mode = "bones";
let q = "";
検索は入力値を受け取り、Bone 名または Morph 名でフィルタしている。 件数が多いモデルでも、目的の名前に辿り着きやすい。
const items = morphs
.filter((name) => !query || String(name).toLowerCase().includes(query))
.slice(0, 300);
リスト選択では、クリックした項目が Bone なのか Morph なのかを見て、選択状態を切り替える。
list?.addEventListener("click", (ev) => {
const btn = ev.target.closest(".list__item");
if (!btn) return;
const boneName = btn.dataset.bone;
const morphName = btn.dataset.morph;
selectedRigBoneName = null;
selectedRigMorphName = null;
if (selectionName) {
selectionName.textContent = boneName || morphName || "-";
}
if (boneName) {
selectedRigBoneName = boneName;
setSelectedBone(boneName);
} else if (morphName) {
selectedRigMorphName = morphName;
setSelectedBone(null);
}
list.querySelectorAll(".list__item").forEach((b) => b.classList.remove("is-selected"));
btn.classList.add("is-selected");
syncRigActionState();
});
Bone を選んだ場合はボーン選択処理へ、Morph を選んだ場合は Morph 適用用の状態へ入る。
選択中の名前は Selection 欄に表示されるようにしてあり、今どれを触っているのか分かりやすい。
Morph 適用用には Morph value の入力欄も用意し、0〜1 の範囲で指定できるようにした。
valueInput?.addEventListener("input", () => {
const v = Number(valueInput.value);
rigMorphValue = Number.isFinite(v) ? Math.max(0, Math.min(1, v)) : 1.0;
});
Apply ボタンでは選択中の Morph を現在の Actor に適用し、Reset ボタンでは全 Morph をリセットする。
btnApply?.addEventListener("click", () => {
if (!selectedRigMorphName) return;
const actor = actors[0];
if (!actor) return;
const ok = applyActorMorphByName(actor.id, selectedRigMorphName, rigMorphValue);
console.log("[Rig] apply morph:", selectedRigMorphName, rigMorphValue, ok);
});
btnReset?.addEventListener("click", () => {
const actor = actors[0];
if (!actor) return;
resetActorMorphs(actor.id);
syncRigActionState();
console.log("[Rig] reset morphs");
});
今回はまず 先頭 Actor に対して動かす シンプルな形にしている。 今後 Actor 個別選択を足していく余地はあるが、検証ツールとしてはまず十分だ。
実際に確認できたこと
Rig パネルを作って確認した結果、少なくとも次のことは分かった。
- Morph一覧表示は動いている
- Apply / Reset は動作する
morphTargetDictionary/morphTargetInfluencesは有効- 適用先は
actor.rootよりactor.meshを優先して見る方が正しかった - 少なくとも 「ウィンク2右」 は視認できた
- ただし、すべての Morph が強く効くわけではない
この最後の点はかなり重要だと思う。 Morph が存在していることと、見た目が大きく変わることは別問題だ。
つまり今回の収穫は、 「Morph処理自体は死んでいない。今後は使える Morph を棚卸ししていく段階に入った」 と切り分けられたことにある。
今回の実装でよかった点
今回の Rig パネル実装でよかったのは、単なる UI 追加で終わらず、 開発中の不確定要素を減らすための検証ツール として機能し始めたことだ。
今後の作業では、
- 表情差分の大きい Morph を選別する
- モデルごとに使える Morph を整理する
- Bone 情報と組み合わせて姿勢制御やデバッグを強化する
- Actor 単位で Rig 操作対象を切り替える
といった方向に広げやすい。
複数 Actor 管理に進んでいくほど、こういう内部確認 UI の価値は上がっていくはずだ。
まとめ
今回は、Three.js + MMD の時計アプリに Rig パネル を追加し、Bone / Morph 情報を UI から直接確認できるようにした。
実装したのは次のような内容だ。
- Bone数 / Morph数の表示
- Search
- Bones / Morphs の切り替え
- Selection 表示
- Morph value 入力
- Apply / Reset
特に大きかったのは、Morph が効かないように見える問題を 「実装が悪いのか」「モデルごとの差なのか」 で切り分けられるようになったことだ。
今回の確認では、morphTargetDictionary と morphTargetInfluences は有効で、
適用先を actor.mesh ベースで見ることで「ウィンク2右」のような表情変化も確認できた。
一方で、すべての Morph が強く効くわけではない。 今後はこの Rig パネルを使いながら、モデルごとの 使える Morph の棚卸し を進めていきたい。
前回の記事では複数 Actor と順番トークの仕組みを整えたが、今回はそれを支えるための 確認 UI 側の土台 が一歩進んだ形になる。 次はこの基盤を使って、表情連動や Actor ごとの細かい制御に広げていきたい。
💬 コメント