なんと、一応動作する所までできました。
という訳で、いきなりソースコード
import bpy
import math
class B2S_data:
def __init__(self):
self.bone : bpy_types.PoseBone = None
self.length = 0.0
# direct full
self.shape_df : bpy.types.ShapeKey = None
# direct 1/2
self.shape_dh : bpy.types.ShapeKey = None
# direct 1/3
self.shape_dt : bpy.types.ShapeKey = None
# direct 1/4
self.shape_dq : bpy.types.ShapeKey = None
# inverse full
self.shape_if : bpy.types.ShapeKey = None
# inverse 1/2
self.shape_ih : bpy.types.ShapeKey = None
# inverse 1/3
self.shape_it : bpy.types.ShapeKey = None
# inverse 1/4
self.shape_iq : bpy.types.ShapeKey = None
# Shape Prefix
preShape = ("b2sdf","b2sdh","b2sdt","b2sdq","b2sif","b2sih","b2sit","b2siq")
while bpy.context.mode == 'OBJECT':
scene = bpy.context.scene
frame_backup = scene.frame_current
frame_start = scene.frame_start
frame_end = scene.frame_end
frame_step = scene.frame_step
for obj in bpy.context.selected_objects:
if obj.type != 'MESH':
continue
# find allocation bone
arm = obj.find_armature()
if not arm:
continue
shape_keys = bpy.data.meshes[obj.name].shape_keys
if not shape_keys:
continue
# make work table
listB2S = []
for bone in arm.pose.bones:
if bone.name[:3] != "b2s":
continue
mydata = B2S_data()
bFindShape = False
mydata.bone = bone
mydata.length = arm.data.bones[bone.name].length
# find shapes
for i in range(0,8,1):
nameShape = preShape[i]+bone.name[3:]
if shape_keys.key_blocks.find(nameShape) == -1:
continue
shape = shape_keys.key_blocks[nameShape]
match i:
case 0:
mydata.shape_df = shape
case 1:
mydata.shape_dh = shape
case 2:
mydata.shape_dt = shape
case 3:
mydata.shape_dq = shape
case 4:
mydata.shape_if = shape
case 5:
mydata.shape_ih = shape
case 6:
mydata.shape_it = shape
case 7:
mydata.shape_iq = shape
bFindShape = True
if not bFindShape:
continue
listB2S.append(mydata)
# frame loop
for frInsert in range(frame_start,frame_end+frame_step,frame_step):
scene.frame_set(frInsert)
for work in listB2S:
vec = work.bone.head - work.bone.tail
base_rate = math.sqrt(vec.x*vec.x + vec.y+vec.y + vec.z*vec.z) / work.length
if base_rate > 1.0:
base_rate = 1.0
# insert key
if work.shape_df:
work.shape_df.value = base_rate
work.shape_df.keyframe_insert("value",frame=frInsert)
if work.shape_dh:
work.shape_dh.value = base_rate/2.0
work.shape_dh.keyframe_insert("value",frame=frInsert)
if work.shape_dt:
work.shape_dt.value = base_rate/3.0
work.shape_dt.keyframe_insert("value",frame=frInsert)
if work.shape_dq:
work.shape_dq.value = base_rate/4.0
work.shape_dq.keyframe_insert("value",frame=frInsert)
if work.shape_if:
work.shape_if.value = (1.0-base_rate)
work.shape_if.keyframe_insert("value",frame=frInsert)
if work.shape_ih:
work.shape_ih.value = (1.0-base_rate)/2.0
work.shape_ih.keyframe_insert("value",frame=frInsert)
if work.shape_it:
work.shape_it.value = (1.0-base_rate)/3.0
work.shape_it.keyframe_insert("value",frame=frInsert)
if work.shape_iq:
work.shape_iq.value = (1.0-base_rate)/4.0
work.shape_iq.keyframe_insert("value",frame=frInsert)
scene.frame_set(frame_backup)
break
コードここまで
…どちらかというと、Python に慣れていないことが露呈しまくっているコードだと思います。そもそも私は現役時代は C++ だったのでここの知識をベースに作成しています。故に差異があるととたんに躓くのですよね。
では、解説…というか、躓いた部分の言い訳を列挙していきます。
class B2S_data:
構造体が無いので class を使うというのは知っていたのですが、メンバーを直接読み書きする形で利用しています。正直行儀が悪いのは解っているのですが、こうでもしないと使えません。
- 8個の shape を持ったクラスをノードとしたリストを組んでいますが
まず、ボーンとシェイプキーを関連付けるために名前の接頭語(プレフィクス)にルールを設けます。
最初に両者とも最初の3文字を b2s としています。これは from Bone to Shape からとっています。
次に shape の4文字目は d もしくは i これは direct inverse の頭文字で正作用、逆作用を意味します。
筋肉は引っ張る動きしかできないため、たとえば肘を曲げる筋肉があれば、肘を伸ばす筋肉が別途必要になります。
屈筋、伸筋と言いますが、肘ならば上腕二頭筋が屈筋、上腕三頭筋が伸筋になります。
肘を曲げると延びる係数ボーンを観測するなら、屈筋は正作用、伸筋は逆作用(つまり曲げる程しぼむ)すれば良い訳です。
で、shape の5文字目は f h t q となっておりそれぞれ 1/1 1/2 1/3 1/4 の反映率で分けています。
肘のように1方向のみに曲げる関節の方が稀で大抵は筋肉と関節は複雑に関係している場合が多く、例えば背筋をパンプアップさせようとしたら背骨を逸らした上で両腕も後ろに動かす必要があります。
このような場合に、同じ形状のシェイプキーを複数用意し、半分ずつ影響させる…といった動作を想定して作りました。
まとめると、1本のボーンに対して正作用、逆作用のそれぞれ4種の反映率の最大8個のシェイプキーに影響させる為のデータを保存するためのクラスという事になります。
# Shape Prefix
今回始めてタプルを使っていますが、配列っぽく使える define 文程度の認識で使っていますがこれで良かったのでしょうか?
# make work table より前の部分
これは第二回で行っていた処理と同じなので解説は割愛します。
# make work table
先述の B2S_data クラスのリストを作ります。ボーン名に b2s プレフィクスを持つボーン(係数用ボーン)抽出し、発見できたら同プレフィクスを持つシェイプキー(係数に影響されるシェイプキー)を探して登録していきます。
尚、[]がC言語で言う所の配列ではなく、VC++ の CList のようなもの…ということは何年か前にビットマップ表示プログラムを書いた際にハマったので覚えていましたが、未だにこのリストのスライス等の扱いが曖昧です。
今回はじめて match 文を使っていますが、これは Python3 から導入されたそうです。まんまC言語の switch 文と同じ使い方をしていますが、case には条件文をそれぞれ入れられる…のですが、調べてみるとこの条件文の書き方がだいぶ怪しい感じで…次期 Python で又大幅変更があるんじゃないかと危惧するのは私だけでしょうか?
Python の下位互換性を信用できないのは私だけでは無い(2から3への移行のグダグダを知っているので)とは思いますが、今後に不安を感じる文だなと感じました。
# frame loop
各フレームでのボーンの影響度を計算して、各シェイプキーのキーフレームを打っていきます。
尚、シェイプキーを打っている処理そのものは
keyframe_insert("value",frame=frInsert)
なのですが、「Pythonツールチップを表示」機能を使って、シェイプキーのキーフレーム追加部分をマウスオーバーしても「プロパティのアニメーション」と表示されるだけ(ver4.51時点)で Python のコードは表示されず、結構ハマりました。
特に、メンバー変数の value に値を入れた後に、keyframe_insert では "value" と文字列で指定して追加するというやり方は直感で思いつくものでは無いと思います。
プログラムを通して
B2S_data クラスにある shape_xx メンバーを shape[] 等のリスト型にすればソースコードが大分短くなる…というのは重々承知しています。
ただ、こう書かないと意味を忘れた時に困るだろうと思って可読性を優先しました。
係数ボーンの係数は
if base_rate > 1.0:
base_rate = 1.0
とある通り、1以下(100%以下)に制限されています。又、この係数はスカラ量を元にしていますので、必ず正の値になります。
ボーンが短くなる限界を突き抜けてベクトルが逆(マイナス方向)に動いた場合でもスカラ量を取っているため正の値が増える事になります。
実際に動かしてみる
まず第一回でやっていたなんちゃってデータに対して実行してみます。
すると、当然大量のキーが打たれまして…
こんな感じに。第一回はシェイプキーを開始と終了の線形補完で動かしていたので今回の方がより生物的な動きをしている…のですが正直そんなに変わらないですね。
次に伸筋を作ってみる
伸筋(上腕三頭筋)を作ってみます。
肘の曲げの係数を取るボーンを b2s_hizi
屈筋のシェイプキーを b2sdf_hizi
伸筋のシェイプキーを b2sif_hizi
と命名しています。命名の規約は前述の class B2S_data の解説の通り。
※ところで hizi ってなんだよ、hiji じゃないのかよ、と作ってから気づきました。平たい顔族にローマ字は難しいのです。
で、こちらは単体ではこんな感じに動きます。
いざ実行
とりあえず、正作用、逆作用の同時処理はできているようです。
作ってみた感想
…いやあ、半年かかって出来なかった物が一週間で出来てしまいました。脳が活性化しているって大事なんだなあと思いましたね。
これにて筋肉の造形はシェイプキーに任せる事が可能になりました。
※以前作っていたモデルの背面。肩口に筋肉を表現する為のボーンが密集している。
通常のリギングと筋肉のパンプアップを分けて作る事が出来る上に動作の破綻部分の調整にもシェイプキーを活用できるので相当効率が上がりそうです。
こういった物を作っておかないと、モデルが出来る前に自分の寿命が終わりかねないのですよねえ。
又、この時期は働きながらモデリングやプログラムができる環境にありましたが、直近2年はそういった環境にありませんでした。
日本のトップすらライフワークバランスを棄てる昨今ですが、ライフに重点を置いた生活がしたいなあとしみじみ感じますね。