はじめに
Blenderのモーションデータをvmd形式で出力するスクリプトを作成したので、
コードと使用方法をまとめます。
また、MMDを触ったことがある方で「Blenderでのモーション付けってどんな感じ?」という方向けに、
付録にBlenderのアニメーション機能の特徴を少しまとめました。
出力したvmdをMMDで読み込んだ様子は以下のツイートをご確認ください。
BlenderからMMD用のモーションデータを出力。kemoさんのツイートがきっかけになりました。ありがたや。 pic.twitter.com/6ofmm9jHNR
— Takosuke (@takosuke_tw) 2016年5月6日
マイムアーティストRemy pic.twitter.com/mlmLWOwqAi
— Takosuke (@takosuke_tw) 2016年5月10日
Blender側でIKを設定。手首の接続を切り離して動かす。 pic.twitter.com/pAfQjlBjnT
— Takosuke (@takosuke_tw) 2016年5月10日
動作確認、ステップドレミー。
— Takosuke (@takosuke_tw) 2016年5月7日
モデル製作者:桜餅様 pic.twitter.com/90zTxqpwNy
バグがあったので修正して完成。ふわふわRemy pic.twitter.com/E4qJ7iNGci
— Takosuke (@takosuke_tw) 2016年5月14日
Blenderの画面。センターボーンと腕の上下だけ設定して、すべての親ボーンはパスで移動&回転。
— Takosuke (@takosuke_tw) 2016年5月14日
(なんだかツイートがうまく繋がらないので再投稿) pic.twitter.com/WnN6BK16ll
注意事項
前提条件
前提条件として、ここでの手順で使用しているソフト・ツールのバージョンを以下にまとめます。
といってもバージョンが多少前後しても問題無いと思います。
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モデルを読み込みます。
3. モーション作成
インポートしたモデルを右クリックで選択すると、
右側のプロパティパネルに「アーマチュア」のチェックボックスがあるので、
これをONにしてボーンを表示させます。
(モデルを選択しないとチェックボックスは表示されません)
表示されたボーンを使用して、アニメーションを設定します。
アニメーションの設定は
こちら
や
こちら
や
こちら
のサイトなどを参考にしてください。
4. タイムライン範囲設定
vmdエクスポートスクリプトは、Blenderで設定した開始~終了(最終)フレーム間のデータを出力します。
出力したい範囲をBlenderで指定しておいてください。
フレームレートもここで再度確認しておきます。
これで準備は完了です。
スクリプト実行手順 (7ステップ)
1. コンソール表示
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 |
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]セクションは後程説明しますので、
ここでは気にしないでください。
6. スクリプト実行
まず3Dビューでボーンを選択した状態にします。
そして処理スクリプトを記述したテキストエディタ(ここでは下側のエディタ)で右クリックし、
「スクリプト実行」を選択します。
処理が成功すると指定の場所にvmdファイルが作成されます。
7. 確認
MMDを起動し、モデルとvmdファイルを読み込み動作確認します。
スクリプト実行手順は以上です。
問題と対策
本スクリプトで起こりやすい問題とその対策を以下に記述します。
MMDのボーン名が日本語のため、Windows版Blenderコンソールの内容が文字化けする場合があります。 ログファイルの方を確認すると日本語エラーの内容を確認できますが、 ファイル生成前に発生したエラーはコンソールでしか見ることが出来ません。
解決のためにはBlenderコンソールの設定を変える必要があります。 まずコンソール左上のBlenderマークをクリックしてプロパティを選択します。 そしてフォントタブで「MSゴシック」フォントを選択してOKボタンをクリックします。 最後にBlenderで新しくテキストファイルを作成し、以下の三文をコピペ&実行してみてください。 (ちょっと表示がおかしいですが、これのなおし方が分かりません…)
元に戻す場合は2行目の65001
を932
に変更して実行してください。1 | import os |
ボーン付け替え機能
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ボーンに適用することが出来ます。
その他・ツールの特徴
ここまでで挙げられていない本ツールの特徴を以下にまとめます。
- コンストレイント適用後のボーンデータを出力
- 出力後のモーションデータは、全てのフレームにキーが打たれた状態
- モーション以外のデータ(モーフなど)は出力しない
- ボーン名の変換は「.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と適当な説明で動作確認をお願いしたにも関わらず、
動画まで作っていただき感謝の極みです。
https://t.co/DfjAibDJdl
— 桜餅 (@Sakura0323moti) 2016年5月13日
Blender→MMDにモーション持ってくるテストで作ったもの pic.twitter.com/gPmOZ4BiVt
Blender>VMDエクスポートテストのやつ(グルーブ感のないトップロック)
— Gared (@ayakasikone) 2016年5月14日
基本的な準標準エクスポート(腕捻れ、手捻じれ、上半身2、グルーブ、手持ちダミー、肩キャンセル) pic.twitter.com/E2bEfuErEt
Blender>VMDエクスポートテストのやつ3
— Gared (@ayakasikone) 2016年5月14日
isolate機能てすと。
手のIKはもう必須だからな、MMDに持ってくときにつなぎかえる機能があるのは便利! pic.twitter.com/4psut0hde7
Blender>VMDエクスポートテストのやつ4
— Gared (@ayakasikone) 2016年5月15日
ちゃんと出力できてるから、もうテストじゃないな。
6歩2サイクル。
トレースすればもっときれいかもしれないけど、体動かしながらだとこんなもん。
5歩目、左足の引きが変 (´・ω・`) pic.twitter.com/jnHjdGS4AW
付録
付録A・Blenderでのモーション付けについて
ここでは「Blenderってどんなことができるの?」と思ったMMDer向けに、
Blenderのアニメーション機能の特徴(MMDとの違い)をいくつか挙げてみます。
1と2で多段ボーンの主要機能をカバーできるかと思います。
4と5で「腕切」や「手首キャンセル」などの改造を施しつつ、そのままモーション作成に移れます。
(おまけ的な機能ではありますが、
本ツールはBlenderで改造したボーンのモーションを無改造のMMDボーンに適用することが出来ます)
6を使用すれば複雑で幾何学的なモーションの作成も可能です。
また、MMDと比べたときのデメリットとしては、以下が挙げられるかと思います。
- ショートカットキーが多く、誤爆しやすい
- MMDのような軽快な表示が難しい
1については、MMDからBlenderに移る方や、多機能ツールを複数使う人にとって頭が痛い問題かと思います。
私はショートカットキーは覚えないようにし、
GUIを使用したりメニューからたどるようにしています。
そのほうがショートカットより思い出しやすいので、
Blenderを触らない期間があってもあまり問題がなくなりました。
また、MMDと同じメッシュ・剛体・ボーンをBlenderで表示するとちょっと重いです。
これを解消しようとするとかなりの手間がかかるので、
私は簡単な素体でモーション付けするようにしています。
付録B・設定ファイルテンプレート
余計なものがないconfig.iniを置いておきます。
[config] | |
folder : | |
file : | |
[bone] | |
[bone_isolated] |
付録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") + " -----") |
編集履歴
- 2016/05/22 : スクリプトコード修正
- ルートボーンの初期値が<0, 0, 0>でない場合、その位置を移動値として出力してしまっていたので修正
- 2016/05/20 : 記事公開