挿絵画家になろう(その8)


「挿絵画家になろう(その8)」です。
Blenderのアドオン、MB-Labの啓蒙です。
MB-Labを使って秀逸な少女キャラクターのメッシュオブジェクトを生成し、VRoidStudioの髪をインポートして装着させ、そこそこの服を着せ、キャラクターのマテリアルも弄れるようになり、更に表情も変更できるようになりました。

今回はポーズについてです。
しかしながらMB-LabのキャラクターはBlenderの人型モデルなので単にポーズをつけるだけならばBlenderの操作方法を学べばよいことになります。
ググればポーズのつけかたを解説しているサイトがいくつもあるので今更私が説明する必要もないでしょう。
難しいですが慣れるとわりと自由にポーズを付けられるようになります。

ではあるものの、我らがMB-Labにはポーズライブラリが付属します。
Blenderで人型キャラクターのポーズ付けに挫折した人でもポーズライブラリから選択し、ポーズをつけることができます。
らくちんですね。

ただ決められたポーズだけでは、挿絵を欲する同胞の諸兄姉は満足できないでしょう。
そんな諸兄姉にポーズライブラリを組み合わせて新しいポーズを作成する方法を展開しましょう。
Blenderの知識が要らない代わりにスクリプト言語の知識が必要になってきます。
諸兄姉はPythonの知識がありますよね?
あるといいな。
無くても、今回は言語的な知識はなくても良いように記事を書く予定です。
以前の記事』にPythonのインストールに書いてますので参考にしてください。

MB-LabはBlenderのアドオンなのですが、Windows10の場合、難しい事をしなければ通常以下にインストールされます。

C:\Users\%USERNAME%\AppData\Roaming\Blender Foundation\Blender\2.79\scripts\addons\manuelbastionilab

MB-Labで生成されるキャラクターの雛形は以下のBlender Projectファイルに収められています。

C:\Users\%USERNAME%\AppData\Roaming\Blender Foundation\Blender\2.79\scripts\addons\manuelbastionilab\data
humanoid_library.blend

このファイルを開くと分かるのですが、メッシュやアーマチュア、マテリアル、その他が定義されています。

基本ポーズはAポーズですね。
で、ポーズライブラリは以下のディレクトリになります。

C:\Users\%USERNAME%\AppData\Roaming\Blender Foundation\Blender\2.79\scripts\addons\manuelbastionilab\data\poses

このディレクトリには以下の三つのディレクトリがあります。
female_poses
male_poses
rest_poses

rest_poseのディレクトリには以下のファイルがあります。
a-pose.json
a-pose.json
lambda-pose.json
lambda-pose.json
lambda-pose.json

このファイル名、どこかで見たことがありますね。
そうです、MB-LabのRest Poseの選択肢です。

各ポーズはjson形式というテキストファイルで記述されています。
試しにa-pose.jsonの中身を確認してみましょう。
テキストファイルなので普通にテキストエディッタで開けば見ることができます。
しかし改行がないので若干見にくいので次のようなスクリプトを使って解析します。

import json

pose = './a-pose.json'

with open(pose) as f_b:
    df_b = json.load(f_b)
    for w in df_b:
        print(w," = ",df_b[w])

jsonのファイルはKeyとValueの対を定義するリスト構造となっています。
このスクリプトはKeyとValueの対を表示します。
結果は以下のようになります。

thumb02_L  =  [1.0, 0.0, 0.0, 0.0]
calf_twist_R  =  [1.0, 0.0, 0.0, 0.0]
ring03_L  =  [1.0, 0.0, 0.0, 0.0]
thumb02_R  =  [1.0, 0.0, 0.0, 0.0]
pinky00_L  =  [1.0, 0.0, 0.0, 0.0]
pinky02_R  =  [1.0, 0.0, 0.0, 0.0]
ring00_R  =  [1.0, 0.0, 0.0, 0.0]
breast_L  =  [1.0, 0.0, 0.0, 0.0]
ring02_R  =  [1.0, 0.0, 0.0, 0.0]
lowerarm_R  =  [1.0, 0.0, 0.0, 0.0]
middle02_L  =  [1.0, 0.0, 0.0, 0.0]
neck  =  [1.0, 0.0, 0.0, 0.0]
ring02_L  =  [1.0, 0.0, 0.0, 0.0]
middle03_L  =  [1.0, 0.0, 0.0, 0.0]
ring00_L  =  [1.0, 0.0, 0.0, 0.0]
pelvis  =  [1.0, 0.0, 0.0, 0.0]
ring01_R  =  [1.0, 0.0, 0.0, 0.0]
thigh_R  =  [1.0, 0.0, 0.0, 0.0]
index02_R  =  [1.0, 0.0, 0.0, 0.0]
upperarm_L  =  [1.0, 0.0, 0.0, 0.0]
clavicle_R  =  [1.0, 0.0, 0.0, 0.0]
pinky02_L  =  [1.0, 0.0, 0.0, 0.0]
index01_R  =  [1.0, 0.0, 0.0, 0.0]
middle00_L  =  [1.0, 0.0, 0.0, 0.0]
calf_twist_L  =  [1.0, 0.0, 0.0, 0.0]
thigh_twist_L  =  [1.0, 0.0, 0.0, 0.0]
upperarm_twist_L  =  [1.0, 0.0, 0.0, 0.0]
middle02_R  =  [1.0, 0.0, 0.0, 0.0]
hand_R  =  [1.0, 0.0, 0.0, 0.0]
ring01_L  =  [1.0, 0.0, 0.0, 0.0]
toes_R  =  [1.0, 0.0, 0.0, 0.0]
index03_R  =  [1.0, 0.0, 0.0, 0.0]
lowerarm_L  =  [1.0, 0.0, 0.0, 0.0]
spine03  =  [1.0, 0.0, 0.0, 0.0]
lowerarm_twist_L  =  [1.0, 0.0, 0.0, 0.0]
middle01_R  =  [1.0, 0.0, 0.0, 0.0]
index00_R  =  [1.0, 0.0, 0.0, 0.0]
spine01  =  [1.0, 0.0, 0.0, 0.0]
foot_L  =  [1.0, 0.0, 0.0, 0.0]
index02_L  =  [1.0, 0.0, 0.0, 0.0]
thumb01_R  =  [1.0, 0.0, 0.0, 0.0]
pinky03_L  =  [1.0, 0.0, 0.0, 0.0]
ring03_R  =  [1.0, 0.0, 0.0, 0.0]
thumb03_R  =  [1.0, 0.0, 0.0, 0.0]
index01_L  =  [1.0, 0.0, 0.0, 0.0]
middle00_R  =  [1.0, 0.0, 0.0, 0.0]
lowerarm_twist_R  =  [1.0, 0.0, 0.0, 0.0]
breast_R  =  [1.0, 0.0, 0.0, 0.0]
foot_R  =  [1.0, 0.0, 0.0, 0.0]
middle01_L  =  [1.0, 0.0, 0.0, 0.0]
upperarm_R  =  [1.0, 0.0, 0.0, 0.0]
pinky01_R  =  [1.0, 0.0, 0.0, 0.0]
middle03_R  =  [1.0, 0.0, 0.0, 0.0]
hand_L  =  [1.0, 0.0, 0.0, 0.0]
pinky01_L  =  [1.0, 0.0, 0.0, 0.0]
head  =  [1.0, 0.0, 0.0, 0.0]
spine02  =  [1.0, 0.0, 0.0, 0.0]
calf_R  =  [1.0, 0.0, 0.0, 0.0]
thigh_L  =  [1.0, 0.0, 0.0, 0.0]
clavicle_L  =  [1.0, 0.0, 0.0, 0.0]
upperarm_twist_R  =  [1.0, 0.0, 0.0, 0.0]
index03_L  =  [1.0, 0.0, 0.0, 0.0]
thumb03_L  =  [1.0, 0.0, 0.0, 0.0]
calf_L  =  [1.0, 0.0, 0.0, 0.0]
root  =  [1.0, 0.0, 0.0, 0.0]
toes_L  =  [1.0, 0.0, 0.0, 0.0]
pinky03_R  =  [1.0, 0.0, 0.0, 0.0]
pinky00_R  =  [1.0, 0.0, 0.0, 0.0]
index00_L  =  [1.0, 0.0, 0.0, 0.0]
thumb01_L  =  [1.0, 0.0, 0.0, 0.0]
thigh_twist_R  =  [1.0, 0.0, 0.0, 0.0]

ポーズデーターファイルはボーンとその回転量を表す四つの実数の組のリストデーターなのです。
対応するボーンは以下のように実際に使われているアーマチュアのボーン名と一致します。

ボーンは親のボーンがあって子のボーンは親の位置と回転量に影響をうけます。
親子関係のツリー構造は以下のようになっています。

root:すべてのボーンの親
└pelvis:腰
  ├thigh_R:太腿
  │├thigh_twist_R
  │└calf_R:脛
  │  ├calf_twist_R
  │  └foot_R:足の甲
  │    └toes_R:爪先
  ├thigh_L:太腿
  │├thigh_twist_L
  │└calf_L:脛
  │  ├calf_twist_L
  │  └foot_L:足の甲
  │    └toes_L:爪先
  └spine01:脊椎
    └spine02
      └pine03
        ├neck:首
        │└head:頭
        ├breast_R:乳房
        ├breast_L:乳房
        ├clavicle_R:鎖骨
        │└upperarm_R:上腕
        │  ├upperarm_twist_R
        │  └lowerarm_R:下腕
        │    ├lowerarm_twist_R
        │    └hand_R:手首
        │     ├thumb01_R:親指
        │     │└thumb02_R
        │     │  └thumb03_R
        │     ├index00_R:人差し指
        │     │└index01_R
        │     │  └index02_R
        │     │    └index03_R
        │     ├middle00_R:中指
        │     │└middle01_R
        │     │  └middle02_R
        │     │    └middle03_R
        │     ├ring00_R:薬指
        │     │└ring01_R
        │     │  └ring02_R
        │     │    └ring03_R
        │     └pinky00_R:小指
        │       └pinky01_R
        │         └pinky02_R
        │           └pinky03_R
        └clavicle_L:鎖骨
          └upperarm_L:上腕
            ├upperarm_twist_L
            └lowerarm_L:下腕
              ├lowerarm_twist_L
              └hand_L:手首
                ├thumb01_L:親指
                │└thumb02_L
                │  └thumb03_L
                ├index00_L:人差し指
                │└index01_L
                │  └index02_L
                │    └index03_L
                ├middle00_L:中指
                │└middle01_L
                │  └middle02_L
                │    └middle03_L
                ├ring00_L:薬指
                │└ring01_L
                │  └ring02_L
                │    └ring03_L
                └pinky00_L:小指
                  └pinky01_L
                    └pinky02_L
                      └pinky03_L

面白いことに 親の位置と回転量に影響をうけるものの、子の相対的な回転は子自身の回転量によってのみ決まります。
つまり、肩の位置がどれだけ変わろうが、腕の回転量は影響を受けないということです。

ボーンの回転量はQuaternionという形式の四つの実数のリストによって表現されます。
Quaternion が何であるのかはさておいて、回転を意味する以上はどのポジションからの回転かが重要です。
答えを言えばa-poseを基準とする回転です。
なので、a-pose.jsonはすべての要素が[1.0, 0.0, 0.0, 0.0]、回転なしと定義されています。

さて、ここからが本題なのですが Quaternion で表現する任意の二つの回転量は球面線形補間ができるのです。
つまり二つのポーズの中間のポーズを計算によって求めることができてしまうのです。
実際にやってみましょう。

次のスクリプトをpose_slerp.pyとして保存してください。
スクリプトの意味を理解する必要はとりあえずはありません。

import sys
import json
import numpy
from pyquaternion import Quaternion

if __name__ == '__main__':
    args = sys.argv
    if 2 <= len(args):
        base_pose = args[1]
    else:
        print("no base pose!")
        exit()
    if 3 <= len(args):
        reference_pose = args[2]
    else:
        print("no reference_pose!")
        exit()
    if 4 <= len(args):
        amount = args[3]
    else:
        print("no amount!",args[3])
        exit()
    if 5 <= len(args):
        out_pose = args[4]
    else:
        out_pose = './output.json'

with open(base_pose) as f_b:
    print("Base pose",base_pose, "is loaded")
    df_b = json.load(f_b)

with open(reference_pose) as f_r:
    print("Reference pose",reference_pose, "is loaded")
    df_r = json.load(f_r)

for w in df_b:
    q1 = Quaternion(df_b[w])
    q2 = Quaternion(df_r[w])
    q3 = Quaternion.slerp(q1, q2, float(amount))
    df_b[w] = q3.elements.tolist()

f_out = open(out_pose, 'w')
f_out.write(json.dumps(df_b))
print("Output pose file is",out_pose)

pyquaternionというモジュールが必要です。
以下のようにしてインストールしてください。

python -m pip install pyquaternion

同じディレクトリに以下のポーズライブラリファイルを保存します。
 a-pose.json
 shojo_classic01.json

コマンドラインから以下を実行します。

python pose_slerp.py a-pose.json shojo_classic01.json 0.50 output.json

コマンドの意味は以下です。
python <実行スクリプト名> <ベースポーズファイル名> <ターゲットポーズファイル名> <補間の割合> <出力ポーズファイル名>

<補間の割合>は0~1の実数を指定してください。
0.5でちょうど中間の補間となります。

output.jsonという新しいポーズファイルができるので、MB-Labのポーズとしてloadすれば、生成したポーズを読み込むことができます。
次のアニメーションは0.00~1.00まで0.05刻みで変化させ、一枚ずつMB-Labに読み込ませたものです。

いかがでしょうか?
任意の二つのポーズから、二つのポーズを補完する新しいポーズを生成できるのです。
MB-Labにはたくさんの魅力的なポーズが用意されています。
※ただしfemaleに限る
更に新しく作ったポーズと別のポーズを組み合わせることも可能。
つまり組み合わせは無限。
「あのポーズとこのポーズを組み合わせれば」と夢が膨らむと思いませんか?

とはいえ、これでも自作小説に挿絵を欲する諸兄姉には満足いただけないかもしれません。
「ポーズは分かった。でも手が思うようにポージングできない」
「手指のボーンって、なにげに全体の半分以上あって収取つかないんですけれど」
そう思われるでしょう。

ごもっともです。
体のポーズに引きずられて、手を思うように変形させることができません。
手のポーズが思い通りにならないのは困ります。
手は口ほどにものをいうのですから。
指差し、ピース、サムアップ、ブーイング、グー、チョキ、パー。
フレミングの左手の法則。
シーンに応じて手のポーズを付けなければなりませんよね?

大丈夫です。
ソリューションを用意してあります。
Quaternion では部分の差し替えができるのです。
任意のポーズの任意のボーンだけの回転量を移植できるのです。
手はHand_R,Hand_Lを親とする一連のボーンツリーで構成されています。
よって他のポーズファイルの手の構成を移植することができます。
実際にやってみましょう。

次のスクリプトをpose_copy_hands.pyとして保存してください。
スクリプトの意味を理解する必要はとりあえずはありません。
ただすごくベタなスクリプトなので逆になにをやっているかは想像できると思います。

import sys
import json

if __name__ == '__main__':
    args = sys.argv
    if 2 <= len(args):
        base_pose = args[1]
    else:
        print("no base pose!")
        exit()
    if 3 <= len(args):
        reference_pose = args[2]
    else:
        print("no reference_pose!")
        exit()
    if 4 <= len(args):
        out_pose = args[3]
    else:
        out_pose = './output.json'

with open(base_pose) as f_b:
    print("Base pose",base_pose, "is loaded")
    df_b = json.load(f_b)

with open(reference_pose) as f_r:
    print("Reference pose",reference_pose, "is loaded")
    df_r = json.load(f_r)

df_b['hand_R']      = df_r['hand_R']      
df_b['thumb01_R']   = df_r['thumb01_R']   
df_b['thumb02_R']   = df_r['thumb02_R']   
df_b['thumb03_R']   = df_r['thumb03_R']   
df_b['index00_R']   = df_r['index00_R']   
df_b['index01_R']   = df_r['index01_R']   
df_b['index02_R']   = df_r['index02_R']   
df_b['index03_R']   = df_r['index03_R']   
df_b['middle00_R']  = df_r['middle00_R']  
df_b['middle01_R']  = df_r['middle01_R']  
df_b['middle02_R']  = df_r['middle02_R']  
df_b['middle03_R']  = df_r['middle03_R']  
df_b['ring00_R']    = df_r['ring00_R']    
df_b['ring01_R']    = df_r['ring01_R']    
df_b['ring02_R']    = df_r['ring02_R']    
df_b['ring03_R']    = df_r['ring03_R']    
df_b['pinky00_R']   = df_r['pinky00_R']   
df_b['pinky01_R']   = df_r['pinky01_R']   
df_b['pinky02_R']   = df_r['pinky02_R']   
df_b['pinky03_R']   = df_r['pinky03_R']   

df_b['hand_L']      = df_r['hand_L']      
df_b['thumb01_L']   = df_r['thumb01_L']   
df_b['thumb02_L']   = df_r['thumb02_L']   
df_b['thumb03_L']   = df_r['thumb03_L']   
df_b['index00_L']   = df_r['index00_L']   
df_b['index01_L']   = df_r['index01_L']   
df_b['index02_L']   = df_r['index02_L']   
df_b['index03_L']   = df_r['index03_L']   
df_b['middle00_L']  = df_r['middle00_L']  
df_b['middle01_L']  = df_r['middle01_L']  
df_b['middle02_L']  = df_r['middle02_L']  
df_b['middle03_L']  = df_r['middle03_L']  
df_b['ring00_L']    = df_r['ring00_L']    
df_b['ring01_L']    = df_r['ring01_L']    
df_b['ring02_L']    = df_r['ring02_L']    
df_b['ring03_L']    = df_r['ring03_L']    
df_b['pinky00_L']   = df_r['pinky00_L']   
df_b['pinky01_L']   = df_r['pinky01_L']   
df_b['pinky02_L']   = df_r['pinky02_L']   
df_b['pinky03_L']   = df_r['pinky03_L']   

f_out = open('out_pose', 'w')
f_out.write(json.dumps(df_b))

コマンドラインから以下を実行します。

python pose_copy_hands.py a-pose.json shojo_classic01.json output.json

コマンドの意味は以下です。
python <実行スクリプト名> <ベースポーズファイル名> <手のポーズを取り込むポーズファイル名> <出力ポーズファイル名>

出力結果ファイル output.json を MB-Labの少女キャラクターに読み込むと以下のようになります。

a-poseにshojo_classic01の手のポーズが移植されました。
つまり体のポーズを仕上げたあと、手のポーズだけを目的のポーズに差し替えることができるのです。
スクリプトを改造すれば右手だけ、もしくは左手だけ、もしくは人差し指だけというように任意の部位を差し替えることができるので応用が利きます。

ついでに右手のポーズを左手に移植、またはその逆を行う例を紹介しましょう。
次のスクリプトをpose_copy_hand_one2other.pyとして保存してください。
スクリプトの意味を理解する必要はとりあえずはありません。
ただこのスクリプトもベタです。
諸兄姉は何を行っているのか推測できるでしょう。

import sys
import json

if __name__ == '__main__':
    args = sys.argv
    if 2 <= len(args):
        base_pose = args[1]
    else:
        print("no base pose!")
        exit()
    if 3 <= len(args):
        print(args[2])
        if ((args[2] != 'l2r') and (args[2] != 'r2l')):
            print("sub-command must be l2r or r2l") 
            exit()

    if 4 <= len(args):
        out_pose = args[3]
    else:
        out_pose = './output.json'

def x_axis_mirrord(a):
    b = a[:]
    b[2] *= -1.0
    b[3] *= -1.0
    return(b[:])

with open(base_pose) as f:
    print("Base pose",base_pose, "is loaded")
    df = json.load(f)

if (args[2] == 'l2r'):
    df['hand_R']     = x_axis_mirrord(df['hand_L'])
    df['thumb01_R']  = x_axis_mirrord(df['thumb01_L'])
    df['thumb02_R']  = x_axis_mirrord(df['thumb02_L'])
    df['thumb03_R']  = x_axis_mirrord(df['thumb03_L'])
    df['index00_R']  = x_axis_mirrord(df['index00_L'])
    df['index01_R']  = x_axis_mirrord(df['index01_L'])
    df['index02_R']  = x_axis_mirrord(df['index02_L'])
    df['index03_R']  = x_axis_mirrord(df['index03_L'])
    df['middle00_R'] = x_axis_mirrord(df['middle00_L'])
    df['middle01_R'] = x_axis_mirrord(df['middle01_L'])
    df['middle02_R'] = x_axis_mirrord(df['middle02_L'])
    df['middle03_R'] = x_axis_mirrord(df['middle03_L'])
    df['ring00_R']   = x_axis_mirrord(df['ring00_L'])
    df['ring01_R']   = x_axis_mirrord(df['ring01_L'])
    df['ring02_R']   = x_axis_mirrord(df['ring02_L'])
    df['ring03_R']   = x_axis_mirrord(df['ring03_L'])
    df['pinky00_R']  = x_axis_mirrord(df['pinky00_L'])
    df['pinky01_R']  = x_axis_mirrord(df['pinky01_L'])
    df['pinky02_R']  = x_axis_mirrord(df['pinky02_L'])
    df['pinky03_R']  = x_axis_mirrord(df['pinky03_L'])
else :
    df['hand_L']     = x_axis_mirrord(df['hand_R'])
    df['thumb01_L']  = x_axis_mirrord(df['thumb01_R'])
    df['thumb02_L']  = x_axis_mirrord(df['thumb02_R'])
    df['thumb03_L']  = x_axis_mirrord(df['thumb03_R'])
    df['index00_L']  = x_axis_mirrord(df['index00_R'])
    df['index01_L']  = x_axis_mirrord(df['index01_R'])
    df['index02_L']  = x_axis_mirrord(df['index02_R'])
    df['index03_L']  = x_axis_mirrord(df['index03_R'])
    df['middle00_L'] = x_axis_mirrord(df['middle00_R'])
    df['middle01_L'] = x_axis_mirrord(df['middle01_R'])
    df['middle02_L'] = x_axis_mirrord(df['middle02_R'])
    df['middle03_L'] = x_axis_mirrord(df['middle03_R'])
    df['ring00_L']   = x_axis_mirrord(df['ring00_R'])
    df['ring01_L']   = x_axis_mirrord(df['ring01_R'])
    df['ring02_L']   = x_axis_mirrord(df['ring02_R'])
    df['ring03_L']   = x_axis_mirrord(df['ring03_R'])
    df['pinky00_L']  = x_axis_mirrord(df['pinky00_R'])
    df['pinky01_L']  = x_axis_mirrord(df['pinky01_R'])
    df['pinky02_L']  = x_axis_mirrord(df['pinky02_R'])
    df['pinky03_L']  = x_axis_mirrord(df['pinky03_R'])

f_out = open(out_pose, 'w')
f_out.write(json.dumps(df))
print("Output file is",out_pose)

コマンドラインから以下を実行します。

python pose_copy_hand_one2other.py output.json l2r output2.json

これは上で生成したoutput.jsonを入力に左手のポーズを右手にコピーし、output2.jsonとしてポーズファイルを生成します。
MB-Labの少女キャラクターにoutput2.jsonを読み込んでみましょう。

手にご注目ください。
左手のポーズが右手に移植されていることが分かります。
同様に以下を実行してください。

python pose_copy_hand_one2other.py output.json r2l output3.json

これは上で生成したoutput.jsonを入力に右手のポーズを左手にコピーし、output3.jsonとしてポーズファイルを生成します。
output3.jsonを読み込んでみましょう。

期待通りの結果となっています。
いかがでしょうか?
まったくキャラクターのポーズ編集なしで、かなり思い通りのポーズをとらせることができることをご理解いただけましたでしょうか?
後は自分なりのポーズライブラリを蓄積してゆけばよいわけです。
無論Blenderでのポーズ編集ができるようになればもっと自由度が増すでしょう。
微調整を行うのはそれほど難しいことではないので、どんどんとチャレンジしてください。

今回の記事はMB-Labの説明ではないばかりか、Blenderの機能ですらありませんでした。
MB-Labのデーターファイルはどれも取り扱いやすいjson形式となっています。
だからスクリプトで変化させることができてしまうのです。

実際にデーターファイルを弄るためにはスクリプト言語の知識が必要だったりしますので、その実ポーズの編集を覚えるのとどちらが早いかは微妙なところもあります。
しかし、苦労したポーズは取っておいて、ライブラリ化すると良いと思います。

今回はこれで終わりです。
時間があれば[その9]を書きたいと思います。



コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です