1-10 骨其の壱


 

歩くことを覚える前に、先ず這うことを覚えなければならない。

Amazing(Aerosmith)


1.座標系の階層構造

 長い間椅子に座っていると腰が痛くなります。ここらでちょっと立ち上がって腰の運動でもしましょう。


腰を回すと当然上半身も回ります。頭も腕も回ります。でも動かしているのは腰だけです。
というわけで1.骨のサンプルプログラムの骨は、大体次のような構造になっています。

(図1-10.1)

骨は(たぶんみんな)こんな感じのツリー構造にします。この骨ではツリーのルートは体の外にあり、
ルート自体は骨を表してはいません。もちろんこのサンプルに合わせる必要はまったく無いです。

さて体の一部が動くと、その子も親の動きに従って動くようにしたいのですが、
これはプログラムでどのように表現したらよいのでしょうか?
簡単のため、次のような4つのノードからなるツリーを考えます。

(図1-10.2)

AがルートでBがAの子、C、DがBの子です。Bを動かすとそれにつれてC、Dも動きます。
Aを動かすとBもそれにつれて動きます。このときC、Dも当然Bにつれて動きます。
いいかえると、Bを動かしてもC、DのBとの相対的な位置・方向は変わらず、
Aを動かしてもBのAに対する位置・方向は変わらないということです。
これは、子が親に対する相対的な位置・方向を保持することで表現できます。
子自身が動いたときは、親に対する相対的な位置・方向を変化させます。
この"親に対する相対的な位置・方向"を表す行列をローカル行列と呼ぶことにします。

最終的には各ノードをワールド座標系に変換しなければなりません。このための行列を
グローバル行列と呼ぶことにします。このグローバル行列はどのように求めるのでしょうか?
あるノードbのローカル行列は、bのローカル座標からその親ノードaのローカル座標へ変換する
行列となります。bをワールド座標に変換することはbをaのローカル座標に変換した後
aのローカル座標からワールド座標に変換することと等しくなります。したがって、bのグローバル行列は

bのグローバル行列 = bのローカル行列 × aのグローバル行列

となります。プログラムでは、例えばこんな感じになります。

void
CNode::update( const Matrix& parentMatrix )
{
    globalMatrix = localMatrix * parentMatrix;

    for_each child
    {
        child->update( globalMatrix );
    }
}

部分的にVBのかほりがしますがキニシナイ。
ルートノードに対してupdate( 単位行列 )を呼び出せば、ツリー内の各ノードの
グローバル行列が求まります。ではサンプルプログラムいきます。



スペースキーを押しながら左ドラッグで平行移動、右・中ドラッグで回転です。
初期状態で上のほうにあるノードが親です。


蛇足ですが、親のグローバル行列Pと子のグローバル行列Gが既知である場合、
子のローカル行列Lは

G = LP
の両辺に右からP-1を掛けて
L = GP-1

で求められます。かしこ。


2.インバースキネマティクス

今度はごく簡単なインバースキネマティクスをやってみましょう。
SoftImage3D(以下SI)でいう2Dチェインというやつです。
例として人間の腕を考えます。いままでの知識で作るとすると、
肩<-肘<-手首という階層にし、肩のグローバル行列と肘のローカル行列から
肘のグローバル行列を求め、肘のグローバル行列と手首のローカル行列から
手首のワールド行列を求めるという形になります。 これは始点(肩)の位置、
各関節の向きと骨の長さ(ローカル位置)から先端(手首)の位置を求めている
ということができます。このような方法をフォワードキネマティクス(以下FK)と呼ぶようです。
これとは逆に先端の位置、始点の位置、骨の長さから各関節の向きを決める方法を
インバースキネマティクス(以下IK)と呼んでるみたいです。
なんでそんなことをやるかというと、例えばモデルが歩いているシーンを考えてください。
ゲームなどユーザーがモデルを操作する場合、地面の起伏に合わせて
歩くモーションを作成/再生するというのはちょっと無理です。が、平地を歩くモーションでは
足が地面に埋まってしまったちょっと萎えます。こんなときIKであれば!
足の位置を地面の起伏に合わせて調整するだけで、脚の各関節がそれなりにそれっぽく
曲がってくれます。これをFKでやるのは…どうする? アイフ●
また、私はモーションデザイナーではないので素人意見ですが、IKの方が
モーション付けがやりやすい(部位が多い)のではないかと思います。
うちのサンプルプログラムでは、始点は固定、例えば腕であれば肩の位置は
体の向き、位置によって決定され、手首を動かしても肩は動かないとしているので
あんまり面白くないですが、中には全身をIKで制御している方もいらっしゃいます。
わたしゃそんなんようやりませんが。


さてそろそろIKのやり方を。例として人間の腕を考えます。ってさっきも書いたな。
まず前提条件を。肩の位置Rは体の位置、方向により決定されているものとし、骨の長さl1、l2
手首を持っていくべき位置(以下ターゲット)Tが与えられるとします。もう1つ位置ベクトルが必要になりますが、
これについては後述します。また、各関節の向きを表す座標軸は、X軸を骨と平行にするものとします。
求めるのは肩の向き、肘の位置及び向き、手首の位置です。

ではちょいと肘を伸ばしたり曲げたりしてみてください。普通の人間であれば
骨折でもしていない限り肘は大体一方向にしか曲がりません。蝶番みたいなかんじです。
というわけで肘は蝶番とします。手首、肘、肩の3つの関節の位置を考えると、これらを
含む平面(以下解平面)というのが存在するわけですが、肘関節を蝶番とすると解平面は
肩関節の向きのみで決定されることになります。真柴(@はじめの一歩)のヒットマンスタイルの
真似をしてみれば、言いたいことが分かってもらえるかと思います。
各関節のX軸は骨と平行にするとしているので、X軸はこの解平面上にあることになります。
つまり、解平面の向きは肩関節のY軸の向き(Z軸としても良い)によって決まることになります。
そこで、計算を簡単にするために解平面は肩関節のY軸と平行であるとすることにします。
このとき肘関節のY軸も解平面と平行になります。当然Z軸は解平面と直交します。
念のためにいっておくとこの条件はモーション付けに制限を課すものではありません。
この条件の下でも、肘が一方向にしか曲がらないことを除けば、任意のポーズをとらせることができます。

解平面は既に決定されているとして解平面上での各関節の関係を図にすると

(図1-10.3)

こんな感じです。解平面上での座標は、図のように肩の位置Rを原点とし、
RからターゲットTに向かう直線と平行にX軸をとります。Y軸の正方向は解平面を決定するときに決めます。
このXY軸とZ=X×Yの3軸を基準とする座標系を、解平面空間と呼ぶことにします。
RとTの距離dが骨の長さl1+l2よりも大きい場合は腕はTに向かって
まっすぐ伸びた状態になります。つまり肘の位置W=(l1, 0)、肘の位置W=(l2+l2,0)です。
d<l2+l2の場合、WはTと一致させるので、後はEを求めるだけになります。
Eの位置は次のように求められます。



sinθの符号は(図10-2.1)のE、E'に対応します。肘がどっちに曲がるかです。
いいかえると、解平面のY軸が肘のどちら側を向くかです。SIでは各ジョイントの初期配置で
決定しているようです。わたしのサンプルでは±1を指定しています。
E、Wが求まれば肩、肘の軸方向も決まります。これらを解平面空間から元に戻せば完了です。

では解平面を求めましょう。先ほどの話から解平面空間を表す行列とするのがよさそうです。
つまり基準となる座標系から解平面空間への変換行列を求めます。
基準となる座標系は、例えばワールドやら骨のルートです。簡単のため、R、Tはこの基準座標で
表されているとします。解平面空間のX軸は前述の通りT-Rと平行な単位ベクトルです。
Y、Z軸はT、Rだけでは決定できないので、3つめの位置ベクトルUを指定してもらいます。
Uは解平面上の点を表し(いいかえるとT、R、Uで解平面を決定する)、RからみてUのある側を
Y軸の正方向と約束します。Uをアップベクトルと呼びます。
Uを用いると、Z軸はX×(U-R)、Y軸はZ×Xとなります。
これらから解平面空間への変換行列及びその逆行列を求め(もうできるよね)、R、Tを解平面空間に変換し
肩の向き、肘の位置及び向き、手首の位置を求め、それらを逆行列で基準座標系に戻します。

ではサンプルいってみましょう。



緑の球がアップベクトル、赤球がターゲットです。


これで第1章は終わりです。
1.骨程度のものはもう十分につくれるはず…です。


目次に戻る