Back to Overview

single node IkFk R&D

Tools & Techart Topics

2010年、友人のJan Pijpersと私は、学生らしい勢いで「通常のIK/FKブレンドシステムを取り除き、腕を1つの要素として扱えないか」を試していました。

当時のテストはかなり荒いものでしたが、検証としては有効でした。そして同時に、この方向性には多くの追加問題があることも分かりました。

そのセットアップは実用的ではなく、標準的な方法を置き換えられるものでもありませんでした。それでも、得られた知見は大きいものでした。

それから16年後、私はこのアイデアをもう一度開いてみることにしました。既存のシステムを置き換えるためではありません。新しい知見を得るためです。

今回のテストで見たいことはこれです。

Mayaの腕コンポーネントは、ポーズデータ、ビューポート表示、マニピュレータ、出力行列をplugin shapeの中に持ちながら、ほぼ1つのリグコントロールオブジェクトのように振る舞えるのか。

この記事では、ここまで検証してきた内容を分解してまとめます。


基本ルール

ルールは次の通りです。

  1. すべてを1つの.mllプラグイン内に収める
  2. できれば、見えるコンポーネントオブジェクトは1つにする
  3. テスト段階ではPythonのセットアップコードを使ってもよいが、最終的な挙動はプラグイン内へ移すか、捨てる
  4. インタラクションとUXに集中する

目的は完成したリグを作ることではありません。どこでこのアイデアが破綻するのかを見つけることです。

ブロックアウト

最初のノードブロックアウト。どのような属性データが必要か

アイデアは、腕を構成するシンプルな3ボーンセットアップです。

開始地点となるベースポーズと、外部へ出力する必要がある要素が必要になります。

  1. shoulder
  2. elbow
  3. wrist

これらを行列として持てば、ロケータやジョイントのworld matrixデータを接続して、それをベースにできます。

同時に、inverse matrixをskinClusterのbindPreMatrixへ接続すれば、スキニングを壊さずにコントロールをリポーズできるはずです。これはこちらで行った内容に近い考え方です。

次に、最初からプラグインへ埋め込みたい挙動を整理します。

  1. poseデータを使ってコントロールを配置する
  2. 腕データからpole vectorを定義する
  3. デバッグ要素を描画する
  4. IKでFKを、FKでIKを駆動する
  5. マニピュレータ

単一プラグインのセットアップを扱っているので、すべてが本当に成立するかを知るには、小さな検証単位に分ける必要があります。

この個別テスト群が、今回の主な学習経路になります。

ここから、次のような開発の流れを組みました。

part 1 - コンポーネントを定義する
part 2 - ポーズセットアップを検証する
part 3 - Mayaのビルトインマニピュレータを置き換える
part 4 - メッシュベースのコントロール
part 5 - IKとpole vectorの統合
part 6 - shoulderとelbowのFK統合
part 7 - 再評価

part 7まで到達すれば、コントロールの操作をどのように分配するかを考えるだけのデータが揃います。

FKとIKは異なる挙動パターンを持つので、それをどのように公開するかを見る必要があります。ハンドル、マニピュレータ、ノード全体を使って腕をポーズ・操作しつつ、ポーズからポーズへのブレンドを扱う属性を公開できます。そのとき、IKでは直線的な動き、FKでは球面的な動きを使う、という方向性も考えられます。


基本セットアップ

Mayaでは、shapeはtransformの下に存在します。つまり、これは厳密には1ノードではありません。

このプロトタイプは次のように説明できます。

parent transform
└── non-keyable DAG container

plugin shape
├── owns pose attributes
├── owns rest matrices
├── owns output matrices
├── owns control display data
└── owns active handle state

custom manipulator layer
├── draws the controls
├── handles picking
├── handles dragging
└── writes pose attributes

compute
├── reads rest matrices and pose attributes
├── outputs matrices
└── does not write pose attributes

このプロジェクトで最も重要なルールはこれでした。

computeはpose dataを修正したり同期したりしない。

すべてのpose変更は、明示的なC++の編集操作を通して行います。computeは現在の状態を読み、出力行列を作るだけです。

直接操作のためにcomputeを使うたびに、ノードがロックするか、サイクルが発生しました。


part 1 コンポーネントを定義する

最初のステップは、データのブロックアウトでした。

腕には3つのrest matrixが必要です。

restShoulderMatrix
restElbowMatrix
restWristMatrix

これらは腕のセットアップポーズを定義します。セットアップ時には、ジョイントやロケータから取得できます。

アニメーターが触るpose dataはplugin shape上にあります。

poseShoulderTranslate / Rotate / Scale
poseElbowTranslate / Rotate / Scale
poseWristTranslate / Rotate / Scale
posePoleTranslate

poleに必要なのは、今のところtranslate値だけです。shoulder、elbow、wristはtranslate、rotate、scaleを使います。

これらを行列として保存することもできましたが、それはアニメーション用途には向いていません。float属性の方がキーを打ちやすく、確認しやすく、読みやすいアニメーションカーブを作れます。

出力は行列です。

outShoulderMatrix
outElbowMatrix
outWristMatrix

テストでは、これらをロケータのoffsetParentMatrix属性へ接続できます。後でジョイントやskinClusterへの直接接続も試せます。

重要な決定の1つは、parent transformをアニメーションさせないことでした。

Mayaがparent transformにキーを打ってしまうと、コンポーネントの状態がtransformとshapeに分かれてしまいます。これは避けたい状態そのものです。Sキーを押したときにキーが入るべきなのはplugin shape上のpose属性であり、parent上の適当なtransformチャンネルではありません。

そのため、parent transformはただのコンテナにしました。実際のアニメーションデータはplugin shapeが持ちます。

part 2 ポーズセットアップを検証する

最初の実テストはこれでした。

rest matrices + local pose attributes = output matrices

pose属性は、rest matrixを基準にしたlocal値です。これにより、アニメーター用のチャンネルはクリーンなまま、プラグインはworld spaceの出力行列を生成できます。

このフェーズで検証する必要があったものは次の通りです。

pose属性にキーを打てること
parent transformをnon-keyableに保てること
rest matrixでセットアップを駆動できること
output matrixが正しく更新されること
active handle stateが動くこと
manipulator mode stateが動くこと

最初のバージョンではMayaのビルトインマニピュレータを使いました。これは、plugin shape上のデータを編集・評価できることを証明するには便利でした。

しかし、それは次の問題も見せてくれました。

ビルトインマニピュレータは、このコンポーネントには十分に厳密ではありませんでした。コンポーネントはハンドルごとに異なる挙動を必要とします。shoulderはtranslateすべきではありません。poleはrotateもscaleもすべきではありません。wristはtranslate、rotate、scaleをサポートする必要があります。

これにより、プロジェクトはカスタムマニピュレータへ進むことになりました。

drawing & manip

manip based attribute

part 3 Mayaのビルトインマニピュレータを置き換える

ハンドルのルールは次の通りです。

shoulder = rotate + scale
elbow = rotate + scale
wrist = translate + rotate + scale
pole = translate

これはシンプルに見えますが、とても重要です。

pole vectorが選択されているなら、rotateとscaleは表示されるべきではありません。shoulderが選択されているなら、translateは利用できるべきではありません。マニピュレータは、選択されたハンドルで実際に可能な操作だけを表示するべきです。

Mayaのビルトインマニピュレータでは、この制御が十分にできませんでした。選択されたハンドルに意味がないパーツが、表示されたり操作可能なまま残ったりします。

そこで、ビルトインマニピュレータを取り除き、自分で操作コントロールを描画し始めました。

カスタムマニピュレータに必要だったものは次の通りです。

handle markers = filled spheres
translate axes = filled cone arrows
translate center = filled cube with view-plane movement
rotate controls = local/object rings
scale controls = filled cubes
active state = color highlight
rotate feedback = angle arc

これはこのプロジェクトの大きな検証ポイントの1つでした。

プラグイン自身が、pose属性を描画し、pickし、dragし、書き込む必要がありました。

最も重要なUXルールはこれです。

矢印がある方向を向いているなら、その矢印をドラッグしたときの移動もその方向でなければならない。

見えているギズモが真実である必要があります。見た目の方向と実際の編集方向が一致しなければ、そのマニピュレータは信用できません。

一番難しかったのは回転でした。最初は、回転軸を親子化して表示する、よりEuler寄りのセットアップを試しました。gimbal lockを見せるという意味では面白かったですが、アニメーター向けではありませんでした。最終的には、見た目はlocal rotation ringを使い、内部ではquaternionベースの回転差分を適用してから、属性用にEuler値へ戻す形にしました。

これで、先へ進めるだけのカスタムマニピュレータパスができました。


MStatus RigControlMarkerManip::doPress(M3dView& view)
{
    resetCustomDrag();

    if (fNode.isNull())
    {
        return MS::kUnknownParameter;
    }

    updateCachedPoints();

    MGLuint activeName = 0;
    MStatus status = glActiveName(activeName);
    CHECK_MSTATUS_AND_RETURN_IT(status);

    if (activeName == 0)
    {
        return MS::kUnknownParameter;
    }

    if (activeName == fShoulderHandleName)
    {
        return pressMarkerHandle(kMarkerHandleShoulder, fShoulderPoint);
    }

    if (activeName == fElbowHandleName)
    {
        return pressMarkerHandle(kMarkerHandleElbow, fElbowPoint);
    }

    if (activeName == fWristHandleName)
    {
        return pressMarkerHandle(kMarkerHandleWrist, fWristPoint);
    }

    if (activeName == fPoleHandleName)
    {
        return pressMarkerHandle(kMarkerHandlePole, fPolePoint);
    }

    const short customComponent = customComponentFromSelectionName(activeName);
    if (customComponent != kCustomManipComponentNone)
    {
        return pressCustomManipulatorComponent(customComponent, view);
    }

    return MS::kUnknownParameter;
}

ここで、コンポーネントはMayaのデフォルトマニピュレータ挙動に依存しなくなります。

描画されたハンドル、またはマニピュレータの一部が、選択された対象になります。そこからプラグインは、そのハンドルにどの編集を許可するかを正確に決められます。

customManipulators

part 4 メッシュベースのコントロール

最初のコントロール表示は、単純なsphereとringでした。テストには十分ですが、実際に作者が作るコントロールとしては足りません。

通常のリグでは、アニメーターはNURBSコントロールシェイプを操作することが多いです。このプロトタイプでは、plugin shapeが独自のcontrol display dataを保存できるかを試したいと思いました。

テスト内容は次の通りです。

シーン内のメッシュオブジェクトを取得する
そのshapeデータをplugin shapeへ保存する
保存したデータをビューポートで描画する
元のメッシュオブジェクトを不要にする

各control meshについて、プラグインは次のものを保存します。

vertex positions
triangle indices

このテストにはそれだけで十分でした。normal、UV、material、その他の追加情報は保存していません。メッシュは両面描画するので、データを小さく保てます。

これは表示データだけです。

mesh controlが腕をsolveするわけではありません。manipulatorを置き換えるものでもありません。コンポーネントに作者が用意したcontrol shapeを与えるだけで、実際のpicking、dragging、writingは引き続きcustom manipulatorが担当します。

少し苦労しましたが、この仕組みは動くようになりました。メッシュはMPxDrawOverrideを使って描画しています。この用途では、MPxGeometryOverrideは使っていません。使うとプラグイン全体の構成が変わりすぎるからです。

MFnMesh mesh(meshPath, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);

MPointArray sourceWorldPoints;
status = mesh.getPoints(sourceWorldPoints, MSpace::kWorld);
CHECK_MSTATUS_AND_RETURN_IT(status);

MIntArray triangleCounts;
status = mesh.getTriangles(triangleCounts, fNewTriangleIndices);
CHECK_MSTATUS_AND_RETURN_IT(status);

const MMatrix probeWorldInverse = probePath.inclusiveMatrixInverse(&status);
CHECK_MSTATUS_AND_RETURN_IT(status);

const MMatrix localHandleInverse = handleMatrix(probeNode, handle).inverse();

fNewPoints.setLength(sourceWorldPoints.length());

for (unsigned int i = 0; i < sourceWorldPoints.length(); ++i)
{
    const MPoint probeLocalPoint = sourceWorldPoints[i] * probeWorldInverse;
    fNewPoints.set(probeLocalPoint * localHandleInverse, i);
}

source meshは、そのハンドル用のlocal dataへ変換されます。その後、元のメッシュはシーンに残っている必要がありません。

このテストではpointsとtriangle indicesだけを保存しています。これによりcontrol displayをシンプルに保ち、プラグインが表示データを所有していることを証明しやすくなります。

customConstrols

part 5 IKとpole vectorの統合

control displayとcustom manipulator pathが動いたので、次は実際の腕の挙動です。

最初のターゲットは、最小限のtwo-bone IKでした。

wrist translate handleとpole translate handleがIK solveを起動します。結果はplugin shape上のpose属性へ書き戻されます。

ここが重要です。

シーン内に別のIK chainはありません。IK編集は、コンポーネントの他の部分が使っている同じpose stateへ書き戻されます。

流れは次の通りです。

wrist or pole drag
-> C++ IK solve
-> write pose attributes
-> compute output matrices

MStatus RigControlMarkerManip::solveAndWriteIkTranslateVector(const MVector& value)
{
    const PoseSnapshot currentPose = readPose();

    TwoBoneIkInput input;
    input.restShoulderMatrix = readMatrixPlug(fNode, PersonalRigControlProbeNode::aRestShoulderMatrix);
    input.restElbowMatrix = readMatrixPlug(fNode, PersonalRigControlProbeNode::aRestElbowMatrix);
    input.restWristMatrix = readMatrixPlug(fNode, PersonalRigControlProbeNode::aRestWristMatrix);

    input.currentShoulderTranslate = currentPose.shoulderTranslate;
    input.currentShoulderRotate = currentPose.shoulderRotate;
    input.currentShoulderScale = currentPose.shoulderScale;

    input.currentElbowTranslate = currentPose.elbowTranslate;
    input.currentElbowRotate = currentPose.elbowRotate;
    input.currentElbowScale = currentPose.elbowScale;

    input.currentWristTranslate = currentPose.wristTranslate;
    input.currentWristRotate = currentPose.wristRotate;
    input.currentWristScale = currentPose.wristScale;

    input.currentPoleTranslate = currentPose.poleTranslate;

    if (fDragHandle == kHandleWrist)
    {
        input.wristTarget = MPoint(value.x, value.y, value.z);
        input.poleTarget = MPoint(
            currentPose.poleTranslate.x,
            currentPose.poleTranslate.y,
            currentPose.poleTranslate.z
        );
    }
    else
    {
        input.wristTarget = pointForActiveHandle(kHandleWrist);
        input.poleTarget = MPoint(value.x, value.y, value.z);
    }

    TwoBoneIkPose solvedPose;
    if (!solveTwoBoneIk(input, solvedPose))
    {
        return MS::kFailure;
    }

    PoseSnapshot nextPose;
    nextPose.shoulderTranslate = solvedPose.shoulderTranslate;
    nextPose.shoulderRotate = solvedPose.shoulderRotate;
    nextPose.shoulderScale = solvedPose.shoulderScale;
    nextPose.elbowTranslate = solvedPose.elbowTranslate;
    nextPose.elbowRotate = solvedPose.elbowRotate;
    nextPose.elbowScale = solvedPose.elbowScale;
    nextPose.wristTranslate = solvedPose.wristTranslate;
    nextPose.wristRotate = solvedPose.wristRotate;
    nextPose.wristScale = solvedPose.wristScale;
    nextPose.poleTranslate = solvedPose.poleTranslate;

    return writePoseDirect(nextPose);
}

ここで重要なのはIK mathそのものではありません。重要なのは、結果がどこへ行くかです。

マニピュレータは別のIK chainを作りません。現在のposeを読み、新しい腕の位置をsolveし、その完全なposeをplugin shapeへ書き戻します。

最初のバージョンではtranslateだけを更新していました。その後、solve pathはrotationも更新するように拡張されました。

この時点で、custom manipulatorがtwo-bone IK solveを駆動し、その結果をplugin shapeへ書き戻せることが証明できました。

ik math

part 6 shoulderとelbowのFK統合

IKが動いた後、より難しい部分としてshoulderとelbowの操作が出てきました。

shoulderとelbowはFK controlに近い挙動をします。これらを動かすと腕を直接ポーズするべきですが、コンポーネント全体は有効な状態を保つ必要があります。wristとpoleの位置も、それらの編集後に意味のある状態でなければなりません。

ここで、以前の判断ミスがはっきりしました。

最初は、computeの中にロジックを入れすぎていました。単独のIK挙動をテストしている間は動いていましたが、FK-styleのshoulderとelbowが追加されると問題になりました。

computeは結果を評価する以上のことをし始めていました。pose編集ロジックの一部になりかけていたのです。

それは置く場所が間違っていました。

修正は、poseの同期をmanipulator edit pathへ移すことでした。


MStatus RigControlNode::compute(const MPlug& plug, MDataBlock& dataBlock)
{
    if (plug == aOutShoulderMatrix)
    {
        return computeOutputMatrix(
            dataBlock,
            aRestShoulderMatrix,
            aPoseShoulderTranslate,
            aPoseShoulderRotate,
            aPoseShoulderScale,
            aOutShoulderMatrix
        );
    }

    if (plug == aOutElbowMatrix)
    {
        return computeOutputMatrix(
            dataBlock,
            aRestElbowMatrix,
            aPoseElbowTranslate,
            aPoseElbowRotate,
            aPoseElbowScale,
            aOutElbowMatrix
        );
    }

    if (plug == aOutWristMatrix)
    {
        return computeOutputMatrix(
            dataBlock,
            aRestWristMatrix,
            aPoseWristTranslate,
            aPoseWristRotate,
            aPoseWristScale,
            aOutWristMatrix
        );
    }

    return MS::kUnknownParameter;
}

新しい流れはこうなりました。

shoulder or elbow drag
-> C++ edit operation
-> update pose attributes
-> update pole position
-> compute output matrices

これは意図的につまらないコードです。そこが重要です。

compute関数は、poseがどうあるべきかを決めません。要求された出力に対して、現在のrest matrixとpose属性を受け取り、出力行列を作るだけです。

IK matchingなし。
FK syncingなし。
pole-vector repairなし。
隠れたpose editなし。

それらの編集は、computeが走る前に行われます。

MStatus PersonalRigControlProbeMarkerManip::writePoseDirect(
    const PoseSnapshot& pose
) const
{
    if (fNode.isNull())
    {
        return MS::kFailure;
    }

    MStatus status = setCompoundVectorValue(
        fNode,
        PersonalRigControlProbeNode::aPoseShoulderTranslate,
        pose.shoulderTranslate
    );
    CHECK_MSTATUS_AND_RETURN_IT(status);

    status = setCompoundVectorValue(
        fNode,
        PersonalRigControlProbeNode::aPoseShoulderRotate,
        pose.shoulderRotate
    );
    CHECK_MSTATUS_AND_RETURN_IT(status);

    status = setCompoundVectorValue(
        fNode,
        PersonalRigControlProbeNode::aPoseElbowTranslate,
        pose.elbowTranslate
    );
    CHECK_MSTATUS_AND_RETURN_IT(status);

    status = setCompoundVectorValue(
        fNode,
        PersonalRigControlProbeNode::aPoseElbowRotate,
        pose.elbowRotate
    );
    CHECK_MSTATUS_AND_RETURN_IT(status);

    status = setCompoundVectorValue(
        fNode,
        PersonalRigControlProbeNode::aPoseWristTranslate,
        pose.wristTranslate
    );
    CHECK_MSTATUS_AND_RETURN_IT(status);

    status = setCompoundVectorValue(
        fNode,
        PersonalRigControlProbeNode::aPoseWristRotate,
        pose.wristRotate
    );
    CHECK_MSTATUS_AND_RETURN_IT(status);

    status = setCompoundVectorValue(
        fNode,
        PersonalRigControlProbeNode::aPosePoleTranslate,
        pose.poleTranslate
    );
    CHECK_MSTATUS_AND_RETURN_IT(status);

    return MS::kSuccess;
}

読みやすさのため、上のスニペットではscale値を省略しています。

結果として、IK-styleのwrist/pole編集と、FK-styleのshoulder/elbow編集の両方が、別々のIK/FK scene chainを使わず、同じpose dataへ書き込めるようになりました。

singleChain

part 7 再評価

ここまでで、大部分のセットアップは検証・確認できました。ここでいったん作業を止めて、将来の参照用にコードを整理し、次に必要なステップを考え直すつもりです。

現在のプラグインは、おおよそ次のような構成です。

DAG-visible plugin shape
owns pose data
owns rest data
owns output matrices
owns embedded control display

non-keyable parent transform
exists only as DAG container

custom manipulator
draws and picks handle-specific controls
writes through C++ edit operations

C++ write policy layer (manipulator setup)
owns pose synchronization
handles FK-style and IK-style manipulator writes

compute
remains clean
reads rest + pose attrs
outputs matrices

証明できたこと

ここまでで、このプロトタイプは次のことを証明しています。

  1. plugin shapeがpose dataを所有できる
  2. parent transformをnon-keyable containerとして保てる
  3. コンポーネントが独自のviewport controlを描画できる
  4. Mayaのbuiltin manipulatorを置き換えられる
  5. 各ハンドルがサポートする操作モードだけを公開できる
  6. mesh control shapeをplugin shapeに保存できる
  7. wristとpoleの編集でtwo-bone IK solveを駆動できる
  8. shoulderとelbowの編集でFK-styleにチェーンをポーズできる
  9. IK-styleとFK-styleの両方の編集を同じpose dataへ書き込める
  10. computeをクリーンに保ち、出力行列だけを作れる

ここまでで一番大きな学びはこれです。

edit operations change pose data
compute reads pose data and outputs matrices

この線引きを保つことが、プロトタイプを継続できる程度に安定させました。


まだ解決していないこと / 次に見ること

次の問題は以下です。

  1. embedded mesh controlとmanipulatorのselection priorityをテストする
  2. rest spaceでのmanipulator orientation
  3. rest matrixからpole vectorをきれいに初期化する
  4. manipulator sizeをauthored control shapeまたはfull chainに合わせる
  5. embedded controlのbounding boxを改善する
  6. keyingとanimation behaviorをより深く検証する
  7. stretchとsoft IKを追加する
  8. skinClusterへのdirect接続、またはjointless outputをテストする
  9. mirror matrixを使ったmirrored paired manipulationをテストする
  10. twist distributionは後で見る

これらを統合していく中で、おそらくさらに多くの疑問が出てくるはずです。

現在の結論

元の問いはこれでした。

単一プラグインコンポーネントは、リグコントロールのように振る舞えるのか。

このプロトタイプに限れば、答えはyesです。

より正確に言えば、

Mayaのplugin shapeは、parent transformをクリーンに保ち、編集を制御されたC++操作として行い、computeが結果の評価だけを担当するなら、リグコンポーネントのデータとインタラクションの大部分を所有できます。

これでproduction-readyになったわけではありません。

ただ、custom drawing、manipulator、Maya node behaviorについて多く学べているので、続ける価値は十分にあります。