VMDエクスポートスクリプト

はじめに

Blenderのモーションデータをvmd形式で出力するスクリプトを作成したので、
コードと使用方法をまとめます。

また、MMDを触ったことがある方で「Blenderでのモーション付けってどんな感じ?」という方向けに、
付録にBlenderのアニメーション機能の特徴を少しまとめました。

出力したvmdをMMDで読み込んだ様子は以下のツイートをご確認ください。













注意事項

モーション作成のため、MMD用モデルをBlenderにインポートして使用しています。 MMD以外での使用を禁止しているモデルが多いので、 ご利用の際はモデルのREADMEをよくご確認ください。
本ページの内容を実行する際は、すべて自己責任でお願いします。

前提条件

前提条件として、ここでの手順で使用しているソフト・ツールのバージョンを以下にまとめます。
といってもバージョンが多少前後しても問題無いと思います。
blender_mmd_toolsは
2016/05/20時点のmasterブランチのもの
を使用しています。

ソフト・ツール名 バージョン ダウンロードURL
Windows 7 -
Blender 2.77 www.blender.org/download/
blender_mmd_tools 0.5.0開発版 github.com/sugiany/blender_mmd_tools
MikuMikuDance 9.26 www.geocities.jp/higuchuu4/

準備 (4ステップ)

1. mmd toolsインストール

sugiany氏作成の
mmd tools
アドオンをインストールし、有効化します。
アドオンをインストールする方法は
こちら
の記事が詳しいです。

2. MMDモデルインポート

「ファイル > インポート > MikuMikuDance Model」メニューを選択し、
MMDモデルを読み込みます。





MMD以外での使用を禁止しているモデルもあるので、 モデルをインポートする際は利用規約を必ず確認してください。

3. モーション作成

インポートしたモデルを右クリックで選択すると、
右側のプロパティパネルに「アーマチュア」のチェックボックスがあるので、
これをONにしてボーンを表示させます。
(モデルを選択しないとチェックボックスは表示されません)

表示されたボーンを使用して、アニメーションを設定します。
アニメーションの設定は
こちら

こちら

こちら
のサイトなどを参考にしてください。




Blenderのフレームレートの初期値は24fpsです。 モーション作成前に、MMDに合わせたフレームレートに変更するようにしてください。 (大抵30fpsか60fps)
プロパティパネルは3Dビューの「ビュー > プロパティ」メニューで表示します。 ショートカットは「N」です。
「ポーズを付けて1フレームだけ出力する」場合は、 アクションを作成したりキーフレームを打つ必要はありません。 ボーンの位置や角度を変更するだけでOKです。

4. タイムライン範囲設定

vmdエクスポートスクリプトは、Blenderで設定した開始~終了(最終)フレーム間のデータを出力します。
出力したい範囲をBlenderで指定しておいてください。
フレームレートもここで再度確認しておきます。




開始フレームと終了(最終)フレームを同じ値に設定すると、1フレームだけデータを出力します。

これで準備は完了です。


スクリプト実行手順 (7ステップ)

1. コンソール表示

Blenderメニューの「ウィンドウ > システムコンソール切り替え」をクリックし、
コンソールを表示させておきます。
スクリプトを実行したときのエラーメッセージはこちらに表示されるので、
コンソールの内容を確認しつつ作業します。




コンソールを右上の「閉じる」ボタンで閉じてしまうと、 Blender全体が終了してしまうので注意してください。 コンソールを表示させた場合は操作ミスに備え、こまめに保存してください。

2. テキスト作成

スクリーンレイアウトをScriptingに変更し、
テキストエディタを二つ用意します。
そしてそれぞれのエディタの「新規」ボタンをクリックし、
テキストファイルを二つ作成します。
画面の操作は
こちら

こちら
を参照してください。





3. 設定ファイル作成

以下のソースコードを片方のテキストエディタにコピペし、
テキストのファイル名を”config.ini”に変更します。

[config]
folder : D:\Blender\SampleFolder
file : sample.vmd
[bone]
### 標準ボーン ###
センター
下半身
上半身
目.L
目.R
両目
肩.L
腕.L
ひじ.L
手首.L
肩.R
腕.R
ひじ.R
手首.R
足.L
ひざ.L
足首.L
足IK.L
つま先.L
つま先IK.L
足.R
ひざ.R
足首.R
足IK.R
つま先.R
つま先IK.R
親指1.L
親指2.L
人指1.L
人指2.L
人指3.L
中指1.L
中指2.L
中指3.L
薬指1.L
薬指2.L
薬指3.L
小指1.L
小指2.L
小指3.L
親指1.R
親指2.R
人指1.R
人指2.R
人指3.R
中指1.R
中指2.R
中指3.R
薬指1.R
薬指2.R
薬指3.R
小指1.R
小指2.R
小指3.R
### 準標準ボーン ###
全ての親
# 腕捩れ.L
# 腕捩れ.R
# 手捩れ.L
# 手捩れ.R
# 上半身2
# グルーブ
# 腰
# 足IK親.L
# 足IK親.R
# 操作中心
# 足先IK.L
# 足先IK.R
# ダミー.L
# ダミー.R
# 肩P.L
# 肩P.R
# 親指0.L
# 親指0.R
# 足D.L
# 足D.R
# ひざD.L
# ひざD.R
# 足首D.L
# 足首D.R
# 足先EX.L
# 足先EX.R
### その他 ###
# 帽子
[bone_isolated]
# 手首.L : 手捩.L
# 手首.R : 手捩.R
view raw config.full.ini hosted with ❤ by GitHub

4. スクリプトファイル作成

以下のソースコードを、もう一つのテキストエディタにコピペします。
こちらはファイル名は変更しなくてOKです。

import bpy
import collections
import configparser
import datetime
import mathutils
import os
import struct
class VmdExporter():
scene = bpy.context.scene
timeline_markers = bpy.context.scene.timeline_markers
obj = bpy.context.active_object
arm = None
# export prop
frame_start = scene.frame_start
frame_end = scene.frame_end
frame_size = frame_end - frame_start + 1
scale = 1.0 / 0.2
joint_opt = False
# config file prop
config_file_name = "config.ini"
Section = collections.namedtuple("Section", "config bone bone_isolated")
section = Section("config", "bone", "bone_isolated")
ConfigKey = collections.namedtuple("ConfigKey", "folder file")
config_key = ConfigKey("folder", "file")
config_section_bone = []
config_section_bone_with_constraints = []
config_section_bone_isolated = {}
export_bone_number = 0
# vmd data prop
ipo_list = []
path = None
meta = "Vocaloid Motion Data 0002"
name = ""
# internal data struct
BonePair = collections.namedtuple("BonePair", "child parent")
# option
frame_offset = 0
option_marker_mode = False
option_export_log = True
# log text
log = []
def execute(self):
self.export_vmd()
if self.option_export_log:
if self.path is not None:
self.export_log()
def export_vmd(self):
if self.check() is False: return
if self.get_config() is False: return
self.init_ipo_list()
with open(self.path, "wb") as file:
self.write_str(file, 30, self.meta)
self.write_str(file, 20, self.name)
self.export_all_bone_data(file)
self.write_long(file, 0) # 表情キーフレーム数
self.write_long(file, 0) # カメラキーフレーム数
self.write_long(file, 0) # 照明キーフレーム数
self.write_long(file, 0) # セルフ影キーフレーム数
self.write_long(file, 0) # モデル表示・IK on/offキーフレーム数
def check(self):
if self.obj is None:
self.print_all('can not find object')
return False
if self.obj.type != 'ARMATURE':
self.print_all('selected object is not armature : ' + self.obj.type)
return False
if self.obj.mode == 'EDIT':
self.print_all('selected object is edit mode')
return False
self.arm = self.obj.pose
# オブジェクト名じゃなくアーマチュア名を使用
self.name = self.obj.data.name
if self.arm is None:
self.print_all('can not find armature')
return False
if self.config_file_name not in bpy.context.blend_data.texts:
self.print_all('can not find config file : name=' + self.config_file_name)
return False
print("check process : OK")
return True
def init_ipo_list(self):
self.ipo_list = [20] * 2
self.ipo_list.extend([0] * 2)
self.ipo_list.extend([20] * 4)
self.ipo_list.extend([107] * 8)
self.ipo_list.extend([20] * 7)
self.ipo_list.extend([107] * 8)
self.ipo_list.extend([0])
self.ipo_list.extend([20] * 6)
self.ipo_list.extend([107] * 8)
self.ipo_list.extend([0] * 2)
self.ipo_list.extend([20] * 5)
self.ipo_list.extend([107] * 8)
self.ipo_list.extend([0] * 3)
def get_config(self):
text = bpy.context.blend_data.texts[self.config_file_name]
config = configparser.ConfigParser(allow_no_value=True)
config.optionxform = str
try:
config.read_string(text.as_string())
except Exception as ex:
print("read config file error : " + str(ex))
return False
# セクション存在確認
for section_name in self.section:
if section_name not in config.sections():
print("can not find section : " + section_name)
return False
# configセクション取得
folder = config[self.section.config][self.config_key.folder]
file = config[self.section.config][self.config_key.file]
if not folder:
print("can not find folder")
return False
if not file:
print("can not find file")
return False
# フォルダ権限チェック
if os.access(folder, os.W_OK) is False:
print("permission denied : " + folder)
return False
# if os.path.exists(folder) is False:
# os.makedirs(folder)
self.path = os.path.join(folder, file)
self.print_all("export file path : " + self.path)
# ボーン名取得
self.config_section_bone = config[self.section.bone]
self.config_section_bone_isolated = config[self.section.bone_isolated]
# セクション間重複チェック
bone_names_all = list(self.config_section_bone.keys()) + list(self.config_section_bone_isolated.keys())
if self.check_duplicate(bone_names_all) is False: return False
# ボーン存在確認
if self.check_bone_exist(self.config_section_bone) is False: return False
if self.check_bone_exist(self.config_section_bone_isolated) is False: return False
if self.check_bone_exist(self.config_section_bone_isolated.values()) is False: return False
self.export_bone_number = len(bone_names_all)
self.print_all("export bone number : " + str(self.export_bone_number))
print("get config process : OK")
return True
def check_bone_exist(self, bone_names):
for bone_name in bone_names:
if bone_name not in self.arm.bones:
self.print_all("can not find bone : " + bone_name)
return False
return True
def check_duplicate(self, name_list):
names_tmp = set()
duplicated_names = [x for x in name_list if x in names_tmp or names_tmp.add(x)]
if len(duplicated_names) > 0:
for name in duplicated_names:
self.print_all("duplicate name exists : " + name)
return False
return True
def export_all_bone_data(self, file):
if self.option_marker_mode:
export_markers = [marker for marker in self.timeline_markers if (self.frame_start <= marker.frame and marker.frame <= self.frame_end)]
self.write_long(file, len(export_markers) * self.export_bone_number)
else:
self.write_long(file, self.frame_size * self.export_bone_number)
if self.export_bone_number <= 0: return # 出力するボーンが無い場合は、出力フレーム数(0)だけ出力して終了
# データ出力するボーンをリスト化
export_bones = []
export_bones_isolated = []
for bone_name in self.config_section_bone:
export_bones.append(self.arm.bones[bone_name])
for bone_name in self.config_section_bone_isolated:
export_bones_isolated.append(self.BonePair(self.arm.bones[bone_name], self.arm.bones[self.config_section_bone_isolated[bone_name]]))
export_marker_frame = []
if self.option_marker_mode:
export_marker_frame = [marker.frame for marker in self.timeline_markers if (self.frame_start <= marker.frame and marker.frame <= self.frame_end)]
for i in range(self.frame_start, self.frame_end + 1):
if self.option_marker_mode:
if i not in export_marker_frame:
self.print_all("skip bone frame : " + str(i))
continue
self.scene.frame_set(i)
self.print_all("export bone frame : " + str(self.scene.frame_current))
for bone in export_bones:
self.export_bone_data(file, i, bone)
for bone_pair in export_bones_isolated:
self.export_bone_data_isolated(file, i, bone_pair)
def export_bone_data(self, file, frame, bone):
mat_edit_bone_local_inv = bone.bone.matrix_local.inverted()
location_local = bone.matrix.to_translation()
offset = bone.bone.matrix_local.to_translation()
location_mmd = location_local - offset
quaternion_mmd = (bone.matrix * mat_edit_bone_local_inv).to_quaternion()
if bone.parent is not None:
bone_parent = bone.parent
mat_edit_bone_local_inv_parent = bone_parent.bone.matrix_local.inverted()
location_local = (bone.parent.matrix.inverted() * bone.matrix).to_translation() * bone.parent.bone.matrix_local.inverted()
offset = (bone.parent.bone.matrix_local.inverted() * bone.bone.matrix_local).to_translation() * bone.parent.bone.matrix_local.inverted()
location_mmd = location_local - offset
quaternion_parent = (bone_parent.matrix * mat_edit_bone_local_inv_parent).to_quaternion()
quaternion_mmd = quaternion_parent.rotation_difference(quaternion_mmd)
self.write_bone_data(file, bone.name, frame, location_mmd, quaternion_mmd)
def export_bone_data_isolated(self, file, frame, bone_pair):
bone_child = bone_pair.child
quaternion_child = (bone_child.matrix * bone_child.bone.matrix_local.inverted()).to_quaternion()
bone_parent = bone_pair.parent
quaternion_parent = (bone_parent.matrix * bone_parent.bone.matrix_local.inverted()).to_quaternion()
quaternion_mmd = quaternion_parent.rotation_difference(quaternion_child)
location_mmd = None
if self.joint_opt:
location_mmd = mathutils.Vector((0.0, 0.0, 0.0))
else:
location_local = (bone_parent.matrix.inverted() * bone_child.matrix).to_translation() * bone_parent.bone.matrix_local.inverted()
offset = (bone_parent.bone.matrix_local.inverted() * bone_child.bone.matrix_local).to_translation() * bone_parent.bone.matrix_local.inverted()
location_mmd = location_local - offset
self.write_bone_data(file, bone_child.name, frame, location_mmd, quaternion_mmd)
def write_bone_data(self, file, name, frame, vector, quaternion):
self.print_log(name + " : " + str(vector) + " : " + str(quaternion))
self.write_bone_name(file, name) # ボーン名
self.write_long(file, frame + self.frame_offset) # フレーム番号
self.write_location(file, vector)
self.write_quaternion(file, quaternion)
self.write_ipo(file) # 補間
def write_location(self, file, vector):
# self.print_all(vector)
self.write_float(file, vector.x * self.scale)
self.write_float(file, vector.z * self.scale)
self.write_float(file, vector.y * self.scale)
def write_quaternion(self, file, quaternion):
# self.print_all(quaternion)
self.write_float(file, -quaternion.x)
self.write_float(file, -quaternion.z)
self.write_float(file, -quaternion.y)
self.write_float(file, quaternion.w)
def write_ipo(self, file):
for i in self.ipo_list:
file.write(struct.pack("b", i))
def write_float(self, file, float):
file.write(struct.pack("f", float))
def write_long(self, file, long):
# unsigned long(DWORD)
file.write(struct.pack("=L", long))
def write_int(self, file, int):
file.write(struct.pack("i", int))
def write_bone_name(self, file, name):
barray = bytearray(self.change_bone_name_to_mmd(name).encode('shift_jis'))
self.write_bytearray(file, 15, barray)
def write_str(self, file, array_size, str):
barray = bytearray(str.encode('shift_jis'))
self.write_bytearray(file, array_size, barray)
def write_bytearray(self, file, array_size, barray):
ba_base = bytearray(array_size)
ba_base[:len(barray)] = barray
file.write(ba_base)
def change_bone_name_to_mmd(self, name):
if name.endswith(".L"):
return "左" + name[:-2]
elif name.endswith(".R"):
return "右" + name[:-2]
else:
return name
def print_log(self, str):
self.log.append(str)
def print_all(self, str):
print(str)
self.print_log(str)
def export_log(self):
with open(self.path + ".log", "w", encoding="utf-8") as log:
for line in self.log:
print(line, sep=' : ', file=log)
if __name__ == "__main__":
print("----- start " + datetime.datetime.now().strftime("%H:%M:%S") + " -----")
VmdExporter().execute()
print("----- end " + datetime.datetime.now().strftime("%H:%M:%S") + " -----")

5. コンフィグ設定

config.iniの一番上の[config]セクションに、
出力先フォルダパスと出力ファイル名を記述します。
その下の[bone]セクションには、
データを出力したいボーン名を羅列します。
[bone_isolated]セクションは後程説明しますので、
ここでは気にしないでください。

img

最初は[bone]セクションに「センター」ボーンだけ記述して、 動作を確認してみることをおすすめします。 また、先頭に#を付けてコメントアウトできます。
Cドライブ直下などを出力先に指定すると、 権限が無いためエラーとなる場合があります。 自分のユーザーフォルダ(私の場合C:\Users\takosuke)などを指定することをおすすめします。
[bone]セクション内に同じボーン名が二つあると、 iniファイル読み込み時にエラーとなります。

6. スクリプト実行

まず3Dビューでボーンを選択した状態にします。
そして処理スクリプトを記述したテキストエディタ(ここでは下側のエディタ)で右クリックし、
「スクリプト実行」を選択します。
処理が成功すると指定の場所にvmdファイルが作成されます。




7. 確認

MMDを起動し、モデルとvmdファイルを読み込み動作確認します。

スクリプト実行手順は以上です。


問題と対策

本スクリプトで起こりやすい問題とその対策を以下に記述します。

コンソールに"duplicate name exists"と表示される。
[bone]セクション内で同じボーン名を記述するとエラーとなる他、 [bone]セクションと[bone_isolated]セクションのキー値で重複があった場合もエラーとなります。 ボーン名を検索するなどして、ボーン名に重複が無いか確認してください。
"***.vmd.log"というファイルが生成される。
これはエラーの内容や出力したデータの内容を記述したログファイルです。 中身は普通のテキストファイルなので、メモ帳などのテキストエディタで中身を確認できます。
足の角度がBlenderとMMDで違う。
ikの挙動がBlenderとMMDで異なります。きっちりと合わせたい場合はMMD側のikをoffにしてください。
コンソールが文字化けして、エラーの内容が分からない。

MMDのボーン名が日本語のため、Windows版Blenderコンソールの内容が文字化けする場合があります。 ログファイルの方を確認すると日本語エラーの内容を確認できますが、 ファイル生成前に発生したエラーはコンソールでしか見ることが出来ません。

解決のためにはBlenderコンソールの設定を変える必要があります。 まずコンソール左上のBlenderマークをクリックしてプロパティを選択します。 そしてフォントタブで「MSゴシック」フォントを選択してOKボタンをクリックします。 最後にBlenderで新しくテキストファイルを作成し、以下の三文をコピペ&実行してみてください。 (ちょっと表示がおかしいですが、これのなおし方が分かりません…)

元に戻す場合は2行目の65001932に変更して実行してください。
1
2
3
import os
os.system("chcp 65001")
print("日本語表示確認")






ボーン付け替え機能

BlenderとMMDで、異なる親子関係を持ったデータを出力することができます。
俗にいう「腕切IK」や「手首キャンセル」状態でモーションを作成しつつ、
通常のボーン構造でデータを出力したい場合に使用します。

具体的には以下のようなモーションを作成する場合に使用します。

この機能の使い方はちょっと特殊です。
具体例として、ここでは手首ボーンを切り離し、腕ikを設定する方法を説明していきます。

1. ボーン設定変更

MMDボーンの腕周りには、腕捩れボーンや肩Pボーンなどさまざまなボーンが存在する場合があります。
まずはこの構造を変更し、足ボーンのようにシンプルな構造にします。

レミリアモデルの場合、親子関係が「腕 -> 腕捩 -> ひじ」となっているので、
これを「腕 -> ひじ」となるように
「ひじ」ボーンの親を「腕捩」から「腕」に変更します。




2. ik設定・手首の切り離し

「ひじ」ボーンにikコンストレイントを設定し、ターゲットを「手首」に設定します。
チェーンの値は2にします。(ikの影響範囲が「ひじ」とその親の「腕」の二つになる)

そして「手首」ボーンの親を「手捩」から「全ての親」または親無しに設定します。





3. 手首のロック解除

mmd toolsでモデルをインポートした場合、
手首ボーンのトランスフォームにロックがかかっていて移動できないので、これを解除します。
解除後に手首ボーンを動かすと、腕ボーン・ひじボーンが手首ボーンの位置に合わせて回転するのが確認できます。





4. コンフィグ設定

config.iniの[bone]セクションに腕.L、ひじ.Lボーンを記述し、
[bone_isolated]セクションには「手首.L : 手捩.L」と記述します。
(左側にデータ出力するボーン名、右側に接続したいボーン名を記述します)

5. 確認

あとはスクリプトを実行し、MMDで動作確認します。
Blender側ではボーンを切り離してモーションを作成しましたが、
ボーンが接続されたMMD側でも同じ動きになります。

メモ: 指定したボーンの角度を、iniファイルで指定した親ボーンからの
相対角度として計算してデータ出力しています。
Blenderで改造したボーンのモーションを、無改造のMMDボーンに適用することが出来ます。


その他・ツールの特徴

ここまでで挙げられていない本ツールの特徴を以下にまとめます。

  1. コンストレイント適用後のボーンデータを出力
  2. 出力後のモーションデータは、全てのフレームにキーが打たれた状態
  3. モーション以外のデータ(モーフなど)は出力しない
  4. ボーン名の変換は「.L/.R」→「左/右」

本ツールはコンストレイント適用後のデータを出力しているので、
ボーンをカーブに沿って移動させたモーションなども出力可能です。

2はモーションエクスポートツールによくある仕様ですが、
補間曲線やNLAなどの関係で「Blenderで打ったキーフレームだけ出力する」というのが難しいです。

表情モーフは、MMDでは一つのpmd(pmx)データに対して設定するのですが、
Blenderではメッシュに設定するシェイプキーがこれに当たります。
シェイプキーの値を取得するのは簡単ですが、
同じ名前のシェイプキーが複数あった場合に
MMD用データに変換することがちょっと難しい状態です。
(mmdインポートツールに「素材ごとにメッシュを分割する」機能があり、
これを使用すると顔だけでなく、手や足のメッシュにも同じ名前の表情シェイプキーが残る)

また、vmdデータ構造に含まれる以下のデータも出力しません。

  • カメラ
  • 照明
  • セルフ影
  • モデル表示・IK on/off

ボーン名は、例えばBlenderで「手首.L」だった場合、
vmdでは「左手首」に変換して出力します。
この名前変換規則を変更したい場合は、
change_bone_name_to_mmdメソッドを修正して下さい。


おわりに

本ツールは、開発のしやすさからアドオン形式ではなくスクリプトとして作成しました。
ボーンの取捨選択もGUIではなくiniファイルを使用しており、少々扱いづらいかと思います。

もともとはMMD動画作成が目的で作ったツールです。
今後はのんびりとモーショントレースしたり動画作成したり切り紙絵作ったりする予定です。
なので、今後このツールのアドオン化やツールの更新などはほとんどしないと思います。

本ページ内のコードは自由にお使いください。
この記事がアドオン製作者の目にとまり、
よりよいツールが開発されることを願います。

何かありましたら@takosuke_twまで連絡ください。


謝辞

使用したレミリアモデルはフリック様配布のものを使用しています。
いつもお世話になっています。





そして今回、数人の方に動作確認をお願いしていました。
このページもまだ存在しない時に、
適当なREADMEと適当な説明で動作確認をお願いしたにも関わらず、
動画まで作っていただき感謝の極みです。











付録

付録A・Blenderでのモーション付けについて

ここでは「Blenderってどんなことができるの?」と思ったMMDer向けに、
Blenderのアニメーション機能の特徴(MMDとの違い)をいくつか挙げてみます。

  1. 位置xyz・回転xyzwについて、別々にキーフレームを打てる
  2. NLAエディタ
    を使用してモーションのレイヤー管理・リピート・ブレンドが可能
  3. キーフレーム補間
    に関する機能が多い
  4. コンストレイント
    でボーンにさまざまな制限/制御を追加できる
    (ikは数あるコンストレイントの一つ)
  5. ポーズモード(≒MMDでの操作)とエディットモード(≒PMXEでの編集)を瞬時に切り替え可能
  6. スクリプトによるボーン制御
  7. アニメーション作成を補助する
    アドオン
    あり

1と2で多段ボーンの主要機能をカバーできるかと思います。
4と5で「腕切」や「手首キャンセル」などの改造を施しつつ、そのままモーション作成に移れます。
(おまけ的な機能ではありますが、
本ツールはBlenderで改造したボーンのモーションを無改造のMMDボーンに適用することが出来ます)
6を使用すれば複雑で幾何学的なモーションの作成も可能です。



また、MMDと比べたときのデメリットとしては、以下が挙げられるかと思います。

  1. ショートカットキーが多く、誤爆しやすい
  2. MMDのような軽快な表示が難しい

1については、MMDからBlenderに移る方や、多機能ツールを複数使う人にとって頭が痛い問題かと思います。
私はショートカットキーは覚えないようにし、
GUIを使用したりメニューからたどるようにしています。
そのほうがショートカットより思い出しやすいので、
Blenderを触らない期間があってもあまり問題がなくなりました。

また、MMDと同じメッシュ・剛体・ボーンをBlenderで表示するとちょっと重いです。
これを解消しようとするとかなりの手間がかかるので、
私は簡単な素体でモーション付けするようにしています。


付録B・設定ファイルテンプレート

余計なものがないconfig.iniを置いておきます。

[config]
folder :
file :
[bone]
[bone_isolated]
view raw config.ini hosted with ❤ by GitHub

付録C・ボーンレイヤー操作スクリプト

自分が使っている、ボーンを特定のレイヤーに移動させるスクリプトを置いておきます。

アーマチュアを選択した状態でスクリプトを実行すると、
正規表現にマッチするボーンを指定したボーンレイヤーに移動させます。
条件はregex_setに記述していきます。
r”^sk_“:7,と記述した場合、
「先頭がsk_で始まるボーンを7番目のボーンレイヤーに移動する」
という意味になります。
r”.*リボン”:7,と記述した場合は
「名前の中にリボンを含むボーンを7番目のボーンレイヤーに移動する」
という意味になります。

import bpy
import datetime
import re
class BoneAllocator():
regex_set = {
r"^sk_":7,
r"^服裾下_":7,
r"^翼":7,
r"^ダミー":7,
r".*髪":7,
r".*リボン":7,
r"全ての親":2,
r"センター":1,
}
def execute(self):
regex_set_loc = {}
for regex, layer_number in self.regex_set.items():
regex_set_loc[regex] = self.create_layers(layer_number)
for bone in bpy.context.active_object.pose.bones:
for regex, layers in regex_set_loc.items():
if re.match(regex, bone.bone.name):
bone.bone.layers = layers
break
def create_layers(self, layer_number):
layers = [False] * 32
layers[layer_number] = True
return layers
if __name__ == "__main__":
print("----- start " + datetime.datetime.now().strftime("%H:%M:%S") + " -----")
BoneAllocator().execute()
print("----- end " + datetime.datetime.now().strftime("%H:%M:%S") + " -----")
正規表現について詳しい情報を知りたい方は、 Pythonの正規表現操作 を確認してください。 regex_setのデータ構造を詳しく知りたい方は、 Pythonのデータ構造・辞書型 を確認してください。

編集履歴

  • 2016/05/22 : スクリプトコード修正
    • ルートボーンの初期値が<0, 0, 0>でない場合、その位置を移動値として出力してしまっていたので修正
  • 2016/05/20 : 記事公開