チュートリアル / Bifrost for Maya Rigging Challenge~一歩先のリグ・アニメーションに挑戦~
第11回:Rodソルバーで拡張するインタラクション植物リグ

  • Maya
  • アニメ
  • キャラクター・リグ
  • ゲーム
  • コラム
  • シミュレーション
  • チュートリアル
  • 上級者
  • 中級者
  • 映画・TV

みなさん、こんにちは。

本コラムではMayaのプラグイン"Bifrost"を使って、リグ、アニメーション、物理シミュレーションなどの観点から作例を紹介していきます。今回は既存のFKリグを「キャラクターが植物をかき分けて進むインタラクションリグ」へ、機能拡張してみようと思います。

成果物は以下の通りです。

今回の成果物

【環境】
・Windows 11
・Maya 2027.2
・Bifrost 3.2.0.8


※3.2.0.8でアップデートされた最新の simulate_rods コンパウンドを使用します。

状況の確認

まずはこちらをご確認ください。竹のモデルにスケルトンが入っており、単純なFKコントローラだけ入れてあります。

竹のモデルにスケルトンが入っており、単純なFKコントローラだけ入れてある

この状態で枝葉を押しのけながらキャラクターを横切らせたいとします。枝葉は少なめに見えますが、これでも200近い数のコントローラが入っています。これだけの数を手付けでアニメーションさせるのは現実的ではありません。

そこで今回は、キャラクターとの接触による変形や揺り戻しを Bifrost のsimulate_rods ソルバーを使って表現してみます。

simulate_rods の概要

今回使用するsimulate_rodsコンパウンドについて概要を確認していきます。simulate_rods は Cosserat Rod (*1) をベースとしたシミュレーションソルバーで、細長い棒状の物体を扱います。Mayaで細長いものを揺らす手段としてはまず nHair が思いつきますが、curveを入出力として点の位置を取り出す nHair と違い、simulate_rods は Matrix 列を直接入出力に使います。分岐ができるのも特徴のひとつです。

*1) 細長い物体の、曲げ(Bend)、捻り(Twist)、伸縮(Stretch)、せん断(Shear)といった変形を表現するための物理モデルです。髪や枝、ロープなどのシミュレーションに使われます。

curveを入出力とする nHair と、Matrix 列を直接入出力に使う simulate_rods

今回の題材である竹は、すでにコントローラやジョイントの transform 階層があるため、そのまま入出力に使えるという点で都合が良いです。さらにrodsは1本のチェーンだけでなく枝分かれした分岐構造も扱えますので、植物リグとの相性はピッタリと言えます。Bifrost 3.2 よりコリジョンや外力にも対応したため、今回のキャラクターとの接触にコリジョンを活用していきます。(*2)

*2) シミュレーションのパラメータについては、この場ですべて説明すると長くなってしまいますので割愛します。詳細はInformation Panel から確認できます。

simulate_rodsのInfoパネル
simulate_rodsのInfoパネル

simulate_rods を使った公式のサンプルグラフはすでに複数用意されています。

simulate_rods を使った公式のサンプルグラフ

swinging_coil:コイル状のtransform配列を揺らすデモです。
elastic_skeleton:人型のskeletonを入力し、手足を引っ張って離すデモです。constrained_transformsの挙動を確認できます。
falling_spicule:ウニのようなトゲトゲのskeletonが複数のコライダー上を転がり落ちるデモです。対応しているコライダーの確認に有用です。
strands_as_rods:多数のstrandをskeletonに変換し風を適用するデモです。strandをシミュレートしたいときの参考になります。
bumped_plant:植物のskeletonにrodを適用し、球コライダーを衝突させるデモです。
rope_module:Rigging Module Framework のモジュールです。両端を固定したシミュレーション機能付きのIKチェーンを作成できます。
tail_module:Rigging Module Framework のモジュールです。根本を固定したシミュレーション機能付きのFKチェーンを作成できます。こちらは Bifrost Browser にはありませんが、コンパウンドとしては追加されています。

次は、この中でも今回の課題にかなり近い『bumped_plant』を見ていきます。

公式サンプル「bumped_plant」を確認してみる

bumped_plantのサンプルシーンを開くと、Maya側にはジョイント階層と植物メッシュがあり、スキンが入っています。

bumped_plant
bumped_plant

ノードの接続関係は以下のように、各ジョイントが直接 BifrostGraph の入出力になっています。

各ジョイントが直接 BifrostGraph の入出力になっている

Bifrostグラフの全体像は以下の通り。

bumped_plant のBifrostグラフの全体像

まず、グラフ左端の説明文にはざっくり要約すると『各ジョイントの offsetParentMatrix に結果を戻す都合上、サイクル回避のため worldMatrix を直接入力することはできず、ローカルの matrix を使って階層構造を再構築する。』という内容が書かれています。その再構築をcompute_world_matricesコンパウンドが行っています。再構築された world matrix 配列はその直後に skeleton の定義に使用されています。

compute_world_matrices コンパウンドによる階層構造の再構築

次は植物特有の追加要素で、幹かそれ以外かを分けるプロパティを付与し、それをもとにシミュレーションパラメータに差をつけています。

幹かそれ以外かを分けるプロパティを付与し、シミュレーションパラメータに差をつける

skeleton から parent_index と pivot_matrix を取得し、simulate_rods に入力しています。また、根元の2ジョイントを固定したり、Sphereコライダーの定義と入力も同時に行われています。rodsから出力されたシミュレート済みの matrix配列(out_transforms)は skeleton の姿勢を更新しています。

skeleton から parent_index と pivot_matrix を取得し、simulate_rods に入力

最後にskeletonから、レスト状態を表すpivot local matrixと、シミュレーション後のlocal matrixを取得します。これらの差分を計算してレストポーズからの変位を求め、Maya側のジョイントの offsetParentMatrix へ出力しています。

レストポーズからの変位を求め、Maya側のジョイントの offsetParentMatrix へ出力

そのまま使える部分が多いので素直にこの仕組みをベースにして、次項から実際に竹のFKリグを『どう拡張するか』を考えていきます。

コントローラ階層をBifrost内で再構築

今回は、シミュレーション前後にMaya側のコントローラを配置したいと思います。レストポーズからInitialコントローラでシミュレーション前の初期姿勢を決め、その後simulate_rodsでシミュレーションし、最後にその結果を元からあったFKコントローラへ入力します。Bifrostには植物のセカンダリモーションを中心に担当してもらい、前後は通常のMayaリグとして操作できるようにします。

一番右のFKコントローラが元からある部分
一番右のFKコントローラが元からある部分

ではこちらを実際に組み上げていきます。Initialコントローラはまだありませんので、元のコントローラ階層を丸ごと複製し、○○○_init_ctl にリネームしておきます。

緑色のコントローラが複製した Initial コントローラ階層
緑色のコントローラが複製した Initial コントローラ階層

この後、複製したInitialコントローラをBifrostグラフへ入力します。今回は、コントローラ操作前をレストポーズとして扱い、Initialコントローラによる操作をレストからの変位として適用したいので、それぞれ分けて入力します。ここは、bumped_plant サンプル内にあった、ローカル matrix から階層を再構築するコンパウンド「compute_world_matrices」がそのまま使えます。

以下のグラフでは、Initialコントローラと、その親にある空のtransform(Initialコントローラとぴったり同じ位置にあるもの)のローカルmatrixをそれぞれ入力し、親 transform の matrix からレストポーズの階層を再構築してskeletonを定義します。その後 Initial コントローラの matrix を Operator (*3) として適用し、ポーズを更新している形になります。これをrodソルバーに渡す初期姿勢とします。

*3)Bifrost ヘルプ | トランスフォーム モデル:pivot_matrix や operator_matrix などの詳細はこちらをご確認ください。

bifrostGraphShape
bifrostGraphShape
Initial コントローラの matrix を Operator として適用

200近いコントローラの matrix 接続や parent_index をすべて手入力するのは現実的ではありません。そこで、選択対象の階層を親方向に探索し、Parent Index を収集、その後 matrix 接続と parent_index のsetAttrまでをPythonスクリプトで行います。以下はそのスクリプトです。

from typing import List
from maya import cmds

UINT_MAX = 4294967295

# 親のインデックスを取得する関数
def get_parent_indices(transforms: List[str]) -> List[int]:

    transforms_ln = cmds.ls(transforms, tr=True, l=True)

    parent_indices = []

    for o in transforms_ln:
        parent_index = UINT_MAX
        current = o

        # 親を順に辿っていき、transforms内に見つかった最初の親のインデックスを取得
        while True:
            parents = cmds.listRelatives(current, p=True, f=True)
            if not parents:
                break

            current = parents[0]
            if current in transforms_ln:
                parent_index = transforms_ln.index(current)
                break

        parent_indices.append(parent_index)

    return parent_indices


bf = "bifrostGraphShape1"

# すべての _init_ctl を取得してソート
init_ctls = cmds.ls("bamboo_???_init_ctl", tr=True)
init_ctls.sort()

# 親のインデックスを取得
parent_indices = get_parent_indices(init_ctls)
print(parent_indices)

for i, init_ctl in enumerate(init_ctls):

    # init_ctl の親のグループ(_init_ctl_grp)を取得
    init_ctl_grp = cmds.listRelatives(init_ctl, p=True, f=True)[0]

    # Bifrost Graph に接続
    cmds.connectAttr(f'{init_ctl}.matrix', f'{bf}.init_local_matrix[{i}]', f=True)
    cmds.connectAttr(f'{init_ctl_grp}.matrix', f'{bf}.rest_local_matrix[{i}]', f=True)
    cmds.setAttr(f'{bf}.parent_index[{i}]', parent_indices[i])

# ルートコントローラ接続
cmds.connectAttr('bamboo_root_ctl.worldMatrix[0]', f'{bf}.parent_world_matrix', f=True)
サンプルデータ:scripts/step00_input_connection.py

下のGIFはInitialコントローラ(緑)の一部を動かしたところです。FKコントローラ(黄色)はレストポーズと一致しています。skeleton_scope を使ってskeletonの可視化ができますが、Show Rest State を有効にするとレストポーズもあわせて確認することができます。正常にskeletonが再構築できているようですね。

Initialコントローラ(緑)の一部を動かしたところ

シミュレーションの適用と結果の取り出し

再構築された skeleton を simulate_rods に入力し、シミュレーションを適用します。rodsから出力された更新後のmatrix配列(out_transforms)は transform_joints コンパウンドを使用し From World モードで skeleton に適用します。ここは bumped_plant のやり方と同様です。

stiffness、thickness など、Maya側からコントロールしたいパラメータについては、Initial コントローラから操作できるようにしたいので、操作対象をBifrostグラフのinputポートに配列で公開しておきます。

最後に、skeleton から operator_matrix を取り出して SRT に分解してからMayaへ返していますが、この点は bumped_plant とは少し違います。今回は offsetParentMatrix へ返すのではなくコントローラの translate / rotate へ直接渡したいので、構成を変更しています。また、ここでの出力は、Initialコントローラで設定したレストポーズからの変位とシミュレーションによる追加変位の両方を含んでいます。

bifrostGraphShape
bifrostGraphShape
bifrostGraphShape > get_TR_from_matrix
bifrostGraphShape > get_TR_from_matrix
skeleton から operator_matrix を取り出して SRT に分解してからMayaへ返す

以下は、コントローラへのアトリビュート追加と接続を一括で行うPythonスクリプトです。スクリプト実行後、コントローラに追加されたアトリビュートを調整し、竹の幹は太め/硬め、細い枝部分は細め/柔らかめの設定に変更しておきます。

from maya import cmds

bf = "bifrostGraphShape1"

init_ctls = cmds.ls("bamboo_???_init_ctl", tr=True)
init_ctls.sort()

for i, init_ctl in enumerate(init_ctls):

    if not cmds.attributeQuery('stretch_shear_stiffness', node=init_ctl, exists=True):
        cmds.addAttr(init_ctl, ln='stretch_shear_stiffness', at='float', min=0.0, dv=250.0, k=True)

    if not cmds.attributeQuery('bend_twist_stiffness', node=init_ctl, exists=True):
        cmds.addAttr(init_ctl, ln='bend_twist_stiffness', at='float', min=0.0, dv=0.01, k=True)

    if not cmds.attributeQuery('thickness', node=init_ctl, exists=True):
        cmds.addAttr(init_ctl, ln='thickness', at='float', min=0.0, dv=0.1, k=True)

    cmds.connectAttr(f'{init_ctl}.stretch_shear_stiffness', f'{bf}.stretch_shear_stiffness[{i}]', f=True)
    cmds.connectAttr(f'{init_ctl}.bend_twist_stiffness', f'{bf}.bend_twist_stiffness[{i}]', f=True)
    cmds.connectAttr(f'{init_ctl}.thickness', f'{bf}.thickness[{i}]', f=True)
サンプルデータ:scripts/step01a_simulation_param.py

次は、出力ポートをMaya側のFKコントローラに接続するPythonスクリプトです。

from maya import cmds

bf = "bifrostGraphShape1"

# bamboo_???_ctl を取得してソート
outputs = cmds.ls("bamboo_???_ctl", tr=True)
outputs.sort()

for i, output in enumerate(outputs):
    cmds.connectAttr(f'{bf}.out_rotate[{i}]', f'{output}.rotate', f=True)
    cmds.connectAttr(f'{bf}.out_translate[{i}]', f'{output}.translate', f=True)
サンプルデータ:scripts/step01b_output_connection.py

試しに root コントローラや Initial コントローラを動かしてみると、シミュレーション結果が正常にFKコントローラへと渡り、ジョイント、メッシュが駆動されていることが確認できます!

サンプルデータ:step_01_bamboo_rod_rig.ma(記事の最後のダウンロードリンクからダウンロードできます。)

キャラクターのジョイント位置からColliderを作る

次にキャラクターとのコリジョンを追加します。対応しているコライダーはSphere、Capsule、Geometryの3種ですが、安定かつ比較的高速なSphereとCapsuleを中心に、キャラクター全体を包むようなプリミティブコライダーを作成します。コンパウンドの入力ポートを見てわかる通り、Sphereコライダーは中心位置と半径、Capsuleコライダーは両端の位置と半径を指定するだけなので、入力もシンプルです。

コライダーを作成するコンパウンド3種
コライダーを作成するコンパウンド3種

以下のグラフは、キャラクターの各ジョイントからworldMatrixを入力として、コライダー配列を半自動的に作成するコンパウンドです。例えば、

・headジョイントの位置にSphereコライダーを作成
・upper leg と lower leg の位置を始点/終点としてCapsuleコライダーを作成

というように各部位にコライダーを作成していきます。微調整用のoffset値とradiusは手入力を前提としています。キャラクターの各ジョイントとBifrostグラフの入力ポートとの接続は、Pythonスクリプトで一括して行います。(サンプルデータ:scripts/step02_human_connection.py)

bifrostGraphShape
bifrostGraphShape
bifrostGraphShape > create_human_collider
bifrostGraphShape > create_human_collider
bifrostGraphShape > create_human_collider > place_sphere_collider
bifrostGraphShape > create_human_collider > place_sphere_collider
bifrostGraphShape > create_human_collider > place_capsule_collider
bifrostGraphShape > create_human_collider > place_capsule_collider
キャラクター全体を包むようなプリミティブコライダー

得られた collider 配列はそのまま simulate_rods のColliderへ接続します。以下の動画は、かき分ける動作をしながら前進するアニメーションを適用したところです。狙いどおりの動きが確認できました!

サンプルデータ:step_02_add_human_collider.ma(記事の最後のダウンロードリンクからダウンロードできます。)

シミュレーションをベイクする

ここまででインタラクティブにシミュレーションが動くところまでは完了しました。ただし、実際の制作フローでは、特にレンダリングを行う場合など、最終結果をキーフレームへ変換して固定する必要があります。そのため、BifrostグラフとFKコントローラの間に pairBlend ノードを挟み、translate / rotate を切り替えられるように組み替えます。

以下のPythonスクリプトは全FKコントローラの translate / rotate 入力の間に pairBlend を追加し、切替スイッチを weight に接続するところまでを行うPythonスクリプトです。

from maya import cmds

bf = "bifrostGraphShape1"

# 切り替えスイッチを追加するノード
SWITCH_CTL = "bamboo_root_ctl"
SWITCH_ATTR = "sim_switch"

# bamboo_???_ctl を取得してソート
ctls = cmds.ls("bamboo_???_ctl", tr=True)
ctls.sort()

if not cmds.attributeQuery(SWITCH_ATTR, node=SWITCH_CTL, exists=True):
    cmds.addAttr(SWITCH_CTL, ln=SWITCH_ATTR, nn='0:Baked / 1:Active', at='double', min=0.0, max=1.0, dv=1.0, k=True)

for i, ctl in enumerate(ctls):

    # 既存の接続を切断
    for at in ['translate', 'rotate']:
        connections = cmds.listConnections(f'{ctl}.{at}', s=True, d=False, c=True, p=True)
        if connections:
            cmds.disconnectAttr(connections[1], connections[0])

    # pairBlend ノードを作成して接続
    pb = cmds.createNode("pairBlend", n=f"{ctl}_pb")
    cmds.connectAttr(f'{SWITCH_CTL}.{SWITCH_ATTR}', f'{pb}.weight', f=True)

    for axis in 'XYZ':
        cmds.connectAttr(f'{bf}.out_rotate[{i}].{axis.lower()}', f'{pb}.inRotate{axis}2', f=True)
        cmds.connectAttr(f'{bf}.out_translate[{i}].{axis.lower()}', f'{pb}.inTranslate{axis}2', f=True)
        cmds.connectAttr(f'{pb}.outRotate{axis}', f'{ctl}.rotate{axis}', f=True)
        cmds.connectAttr(f'{pb}.outTranslate{axis}', f'{ctl}.translate{axis}', f=True)

# 0フレーム目にキーフレームを打ち、animCurveノードを作成しておく
cmds.setKeyframe(ctls, attribute=['translate', 'rotate'], time=0)
サンプルデータ:scripts/step03a_insert_pair_blend.py

最後に0フレーム目へキーを打っているのは、ベイク結果を格納するanimCurveをあらかじめ作成しておくためです。ベイク後にも pairBlend の接続を維持することで、Bifrostからのシミュレーション出力(Active)と、ベイクしたキーフレームアニメーション(Baked)を切り替えられるようにしています。

黄色の枠内がスクリプトで追加した部分
黄色の枠内がスクリプトで追加した部分

スイッチを Active 側にした状態で、すべてのFKコントローラを選択し、Animationメニュー > Key > Bake Simulationから、translateとrotateをベイクします。

Bake Simulation
Bake Simulation

bakeResultsコマンドを使用する場合は以下のようになります。結果は同じです。

from maya import cmds

bake_targets = cmds.ls("bamboo_???_ctl", tr=True)

s_frame = cmds.playbackOptions(q=True, min=True)
e_frame = cmds.playbackOptions(q=True, max=True)

cmds.bakeResults(
    bake_targets,
    simulation=True,
    time=(s_frame, e_frame),
    attribute=["tx", "ty", "tz", "rx", "ry", "rz"],
    sampleBy=1,
    oversamplingRate=1,
    preserveOutsideKeys=True,
    sparseAnimCurveBake=False,
    disableImplicitControl=True,
    minimizeRotation=True,
)
サンプルデータ:scripts/step_03b_bake.py

ベイク完了後に、rootコントローラの sim_switch が 0(Baked)に切り替わります。これで、Bifrostのシミュレーション出力ではなくベイクしたキーフレームでアニメーションを確認できる状態になります。

sim_switch が 0(Baked)に切り替わった状態
サンプルデータ:step_03_baked.ma(記事の最後のダウンロードリンクからダウンロードできます。)

では最後に、このシミュレーション機能付き竹リグを数本配置し、間にキャラクターを通過させてみます。

以上です!

今回はBifrost 3.0から登場した目玉機能のひとつ「simulate_rods」を取り上げました。サンプルグラフが複数用意されていますので、それぞれ観察することでRodソルバーにできることはだいたい見えてくるかと思います。

今回の題材はだいぶサンプルに近い状況でしたのでbumped_plantをベースとしましたが、そこからもう一歩進めて、「既存のMaya Rigへどう組み込むか」「キャラクターとどう接触させるか」「シミュレーション結果をどう扱うか」というところまで作ってみました。サンプルを見て『動くことは分かったけど、実際どう使うの?』となった時の一例になれば幸いです。

また次回をお楽しみに!

まとめ

・simulate_rods は matrix 配列を直接シミュレーションできる。分岐も可能なため、植物のような枝分かれ構造とは相性が良い。
・MayaのローカルMatrixから、Bifrost内で階層を再構築しskeletonを定義。
・キャラクターの関節位置からプリミティブコライダーを半自動的に作成。
・インタラクティブなシミュレーションだけでなく、キーフレームアニメーションへのベイクまで含めて運用可能に。


製品購入に関するお問い合わせ
オートデスク メディア&エンターテインメント 製品のご購入に関してご連絡を希望される場合は、こちらからお問い合わせください。