チュートリアル / 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コントローラだけ入れてあります。
この状態で枝葉を押しのけながらキャラクターを横切らせたいとします。枝葉は少なめに見えますが、これでも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)といった変形を表現するための物理モデルです。髪や枝、ロープなどのシミュレーションに使われます。
今回の題材である竹は、すでにコントローラやジョイントの transform 階層があるため、そのまま入出力に使えるという点で都合が良いです。さらにrodsは1本のチェーンだけでなく枝分かれした分岐構造も扱えますので、植物リグとの相性はピッタリと言えます。Bifrost 3.2 よりコリジョンや外力にも対応したため、今回のキャラクターとの接触にコリジョンを活用していきます。(*2)
*2) シミュレーションのパラメータについては、この場ですべて説明すると長くなってしまいますので割愛します。詳細はInformation Panel から確認できます。
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側にはジョイント階層と植物メッシュがあり、スキンが入っています。
ノードの接続関係は以下のように、各ジョイントが直接 BifrostGraph の入出力になっています。
Bifrostグラフの全体像は以下の通り。
まず、グラフ左端の説明文にはざっくり要約すると『各ジョイントの offsetParentMatrix に結果を戻す都合上、サイクル回避のため worldMatrix を直接入力することはできず、ローカルの matrix を使って階層構造を再構築する。』という内容が書かれています。その再構築をcompute_world_matricesコンパウンドが行っています。再構築された world matrix 配列はその直後に skeleton の定義に使用されています。
次は植物特有の追加要素で、幹かそれ以外かを分けるプロパティを付与し、それをもとにシミュレーションパラメータに差をつけています。
skeleton から parent_index と pivot_matrix を取得し、simulate_rods に入力しています。また、根元の2ジョイントを固定したり、Sphereコライダーの定義と入力も同時に行われています。rodsから出力されたシミュレート済みの matrix配列(out_transforms)は skeleton の姿勢を更新しています。
最後にskeletonから、レスト状態を表すpivot local matrixと、シミュレーション後のlocal matrixを取得します。これらの差分を計算してレストポーズからの変位を求め、Maya側のジョイントの offsetParentMatrix へ出力しています。
そのまま使える部分が多いので素直にこの仕組みをベースにして、次項から実際に竹のFKリグを『どう拡張するか』を考えていきます。
コントローラ階層をBifrost内で再構築
今回は、シミュレーション前後にMaya側のコントローラを配置したいと思います。レストポーズからInitialコントローラでシミュレーション前の初期姿勢を決め、その後simulate_rodsでシミュレーションし、最後にその結果を元からあったFKコントローラへ入力します。Bifrostには植物のセカンダリモーションを中心に担当してもらい、前後は通常のMayaリグとして操作できるようにします。
ではこちらを実際に組み上げていきます。Initialコントローラはまだありませんので、元のコントローラ階層を丸ごと複製し、○○○_init_ctl にリネームしておきます。
この後、複製した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 などの詳細はこちらをご確認ください。
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)
下のGIFはInitialコントローラ(緑)の一部を動かしたところです。FKコントローラ(黄色)はレストポーズと一致しています。skeleton_scope を使ってskeletonの可視化ができますが、Show Rest State を有効にするとレストポーズもあわせて確認することができます。正常にskeletonが再構築できているようですね。
シミュレーションの適用と結果の取り出し
再構築された 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コントローラで設定したレストポーズからの変位とシミュレーションによる追加変位の両方を含んでいます。
以下は、コントローラへのアトリビュート追加と接続を一括で行う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)
次は、出力ポートを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)
試しに root コントローラや Initial コントローラを動かしてみると、シミュレーション結果が正常にFKコントローラへと渡り、ジョイント、メッシュが駆動されていることが確認できます!
キャラクターのジョイント位置からColliderを作る
次にキャラクターとのコリジョンを追加します。対応しているコライダーはSphere、Capsule、Geometryの3種ですが、安定かつ比較的高速なSphereとCapsuleを中心に、キャラクター全体を包むようなプリミティブコライダーを作成します。コンパウンドの入力ポートを見てわかる通り、Sphereコライダーは中心位置と半径、Capsuleコライダーは両端の位置と半径を指定するだけなので、入力もシンプルです。
以下のグラフは、キャラクターの各ジョイントからworldMatrixを入力として、コライダー配列を半自動的に作成するコンパウンドです。例えば、
・headジョイントの位置にSphereコライダーを作成
・upper leg と lower leg の位置を始点/終点としてCapsuleコライダーを作成
というように各部位にコライダーを作成していきます。微調整用のoffset値とradiusは手入力を前提としています。キャラクターの各ジョイントとBifrostグラフの入力ポートとの接続は、Pythonスクリプトで一括して行います。(サンプルデータ:scripts/step02_human_connection.py)
得られた collider 配列はそのまま simulate_rods のColliderへ接続します。以下の動画は、かき分ける動作をしながら前進するアニメーションを適用したところです。狙いどおりの動きが確認できました!
シミュレーションをベイクする
ここまででインタラクティブにシミュレーションが動くところまでは完了しました。ただし、実際の制作フローでは、特にレンダリングを行う場合など、最終結果をキーフレームへ変換して固定する必要があります。そのため、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)
最後に0フレーム目へキーを打っているのは、ベイク結果を格納するanimCurveをあらかじめ作成しておくためです。ベイク後にも pairBlend の接続を維持することで、Bifrostからのシミュレーション出力(Active)と、ベイクしたキーフレームアニメーション(Baked)を切り替えられるようにしています。
スイッチを Active 側にした状態で、すべてのFKコントローラを選択し、Animationメニュー > Key > Bake Simulationから、translateとrotateをベイクします。
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,
)
ベイク完了後に、rootコントローラの sim_switch が 0(Baked)に切り替わります。これで、Bifrostのシミュレーション出力ではなくベイクしたキーフレームでアニメーションを確認できる状態になります。
では最後に、このシミュレーション機能付き竹リグを数本配置し、間にキャラクターを通過させてみます。
以上です!
今回はBifrost 3.0から登場した目玉機能のひとつ「simulate_rods」を取り上げました。サンプルグラフが複数用意されていますので、それぞれ観察することでRodソルバーにできることはだいたい見えてくるかと思います。
今回の題材はだいぶサンプルに近い状況でしたのでbumped_plantをベースとしましたが、そこからもう一歩進めて、「既存のMaya Rigへどう組み込むか」「キャラクターとどう接触させるか」「シミュレーション結果をどう扱うか」というところまで作ってみました。サンプルを見て『動くことは分かったけど、実際どう使うの?』となった時の一例になれば幸いです。
また次回をお楽しみに!
まとめ
・simulate_rods は matrix 配列を直接シミュレーションできる。分岐も可能なため、植物のような枝分かれ構造とは相性が良い。
・MayaのローカルMatrixから、Bifrost内で階層を再構築しskeletonを定義。
・キャラクターの関節位置からプリミティブコライダーを半自動的に作成。
・インタラクティブなシミュレーションだけでなく、キーフレームアニメーションへのベイクまで含めて運用可能に。



























