Blender には Bone に関連する行列やトランスフォームがたくさんあって何がなんのかわからない。公式のドキュメントに Space の説明はあるものの、どの行列をどうかけるとどうなるかなんも書いてなくて困る。
生きているというそういうシチュエーションに遭遇することは稀によくあると思います。完全に理解していきましょう。
Skinning の基本
階層的な親子関係からなる Bone(Blender で言うところの Armature)に、ある頂点$v_i=(x, y, z, 1)$が Bind されている(くっついている)とします。この頂点は Bind 座標系にあります。行列計算の都合上、同次座標を使って表記します。Bone の Pose (Transform でも変換でも同じ意味)
を適用した後の頂点位置$v'_i$は次のように計算されます。
v'_i = \sum_{b \in Bone_{v_i}}{w^b_iT_b v_i}
ここで$Bone_{v_i}$は$v_i$に関連付けられているBone の集合、$w^b_i$はそのスカラ値の重みです。
これらは事前にアーティストによる手作業だったり、距離による自動計算だったりで与えられているとします。シェーダによる実装の都合上、位置頂点あたりに関連づけられている Bone 数$|Bone_{v_i}|$の上限は 4 であることが多いようです。また、事前に頂点毎に重みの和を1に正規化しておくことで、和をとった後の同次座標のwによる割り算を回避するというテクニックもよく使われるようです。
$T_b$は ある Bone $b$についての 4x4 の Bind-to-World の行列であり($v'_i$にさらに行列がかかる(Blender でいえば Armature 自身の Transform)こともありますが、ここでは無視します)、世界座標における Bone のポーズを表します。具体的には以下の式でルートから再帰的に計算されます。
\begin{equation}
T_b=
\begin{cases}
B_b P_b B{_b}^{-1} , & \text{if } b \; \text{is root} \\
T_{parent\ of \ b} B_b P_b B{_b}^{-1} , & \text{otherwise}
\end{cases}
\end{equation}
$B_b$は Bone-to-Bind の 4x4 行列であり、Bind 座標における Bone のポーズを表します。
$B_b^{-1}$はその逆行列、Bind-to-Bone の行列です。
混乱しますが、Bind 座標の中に Bone 座標があるというイメージです。
そして $P_b$はその Bone 自身が持っている変換、Pose です。これは基本的には回転成分のみのことが多い気がしますが(関節は回転しかしませんよね?)、理論上は Affine 変換の範囲で任意の変換を持ちえます(後で見ますが、Blender は平行移動、回転、スケールしかサポートしません)。
この式の意味するところは
- $B_b^{-1}$をかけて Bind 座標の頂点を Bone 座標に持っていく。
- Bone 座標で$P_b$をかけて、その Bone の変換を適用する。
- $B_b$で Bind 座標系に戻す、
- ルートになるまで Bone の親を辿って再帰的に繰り返す。
$B_b$、$B_b^{-1}$はそれぞれ、Pose Bind Matrix, Inverse Pose Bind Matrix などと呼ばれることもあるようです。
わかりやすくするために着目している$n$からルート$0$まで一直線に Bone が親子になっているとして、雑に式を展開すると(実際は手足の枝分かれなどがあるので Bone のインデックスは連続するとは限りませんが)こんな感じです。
T_n = B_0 P_0 B{_0}^{-1} B_1 P_1 B{_1}^{-1}...B_{n-1} P_{n-1} B{_{n-1}}^{-1}B_n P_n B{_n}^{-1}
登場人物を整理しましょう。
Skinning 計算において必ず必要な Bone の情報は
- $P_b$:Bone の Pose
- $B_b$(あるいは$B_b^{-1}$):Bone-to-Bind
この 3 つです。
$T_b$は$B_b$と$P_b$から計算できるので一級市民ではありません。
Bone の外になりますが頂点単位の$w^b_i$ももちろん必要です。
Blender の Bone と Matrix
Armature オブジェクトarm = bpy.data.objects['ArmatureName']
と Bone 名name
を知っている時、Bone の情報にアクセスするには以下の方法があります。
天下り的ですが、リストのインデントは依存関係を表しています。
-
edit_bone = arm.data.edit_bones[name]
(読み書き可能): https://docs.blender.org/api/current/bpy.types.EditBone.html- matrix:
- head
- tail
- roll
- matrix:
-
bone = arm.data.bones[name]
(読み取り専用): https://docs.blender.org/api/current/bpy.types.Bone.html- matrix (これだけ何故か 3x3)
- head
- tail
- matrix_local
- head_local
- tail_local
- matrix (これだけ何故か 3x3)
-
pose_bone = arm.pose.bones[name]
(読み書き可能): https://docs.blender.org/api/current/bpy.types.PoseBone.html- matrix
- matrix_basis
- location
- rotation_quaternion / rotation_euler / rotation_axis_angle
- scale
- matrix_channel(読み取り専用)
- head(読み取り専用)
- tail(読み取り専用)
- matrix_basis
- matrix
はい、頭おかしいですね。これだけあっては何をどうすればいいのか全くわからないしドキュメントにもふわっとした説明しかありません。
edit_bone
は Rest Pose (Bone の Pose が全部空の状態、T ポーズ)の状態を規定しており書き込み可能。
bone
はそれから計算された Rest Pose の状態で、計算後なので全面的に書き込みは不可。
そして pose_bone
は Pose がついた状態で Pose に関するところだけは書き込み可能です。
head と tail と roll から Pose Bind Matrix edit_bone.matrix/bone.matrix_local
ができる
Blender の Bone には head と tail という 3 次元位置があります。Joint というやつですね。さらに roll という Bone 周りの回転も一応あります、あんまり使わないとは思いますが。
これらを元にして 先ほど説明した$B_b$が作られているようです。
Blender では$B_b$はedit_bone.matrix
もしくはbone.matrix_local
になります。この二つは同じ値が入っているようです。
書き込みたい場合にはedit_bone.matrix
ではなく、 edit_bone.head
と edit_bone.tail
をいじるのが無難です。
具体的にどうやって計算するのでしょうか?
かなり古いですが、これとかこれによるとtail - head を Y-Up の向きにして, X が右手方向、Z が手前方向、という OpenGL 座標系の行列を定義しているようです。
色々試しましたがこれで上手くいっているようにみえます。
4x4 が必要な時は head の位置を translation にすればよいです。
Bone 座標系は head が origin であり、tail と roll は座標系を決めるためのいわば飾りなのです。
def vec_roll_to_mat3(vec, roll):
target = Vector((0, 0.1, 0))
nor = vec.normalized()
axis = target.cross(nor)
if axis.dot(axis) > 0.0000000001: # this seems to be the problem for some bones, no idea how to fix
axis.normalize()
theta = target.angle(nor)
bMatrix = Matrix.Rotation(theta, 3, axis)
else:
updown = 1 if target.dot(nor) > 0 else -1
bMatrix = Matrix.Scale(updown, 3)
# C code:
#bMatrix[0][0]=updown; bMatrix[1][0]=0.0; bMatrix[2][0]=0.0;
#bMatrix[0][1]=0.0; bMatrix[1][1]=updown; bMatrix[2][1]=0.0;
#bMatrix[0][2]=0.0; bMatrix[1][2]=0.0; bMatrix[2][2]=1.0;
bMatrix[2][2] = 1.0
rMatrix = Matrix.Rotation(roll, 3, nor)
mat = rMatrix * bMatrix
return mat
Pose 行列 matrix_basis
Bone の Pose 行列は $P_b$は pose_bone.matrix_basis
です。
これに書き込みもできますが、サポートされてない skew 成分を間違って書き込むとバグるので
- location
- rotation_quaternion / rotation_euler / rotation_axis_angle
- scale
の 3 つで書き込むのが無難です。読み取りはどれも問題なくできます。
その他
$T_b=$ pose_bone.matrix
@ bone.matrix_local.inverted()
であり、つまり
pose_bone.matrix
$= T_{parent\ of \ b} B_b P_b$となります。
終わりに
ここまで来ると、Blender の Bone に関する行列を好きなようにバラしたりかけたりできるはずです。
完全に理解して安全安心な Blender ライフを実現しましょう。