0. はじめに
HoudiniのScript Solver DOPについて調べたので、備忘録がてらまとめます.
質問や内容に関する指摘がありましたら、遠慮なくコメントとかください.
0-1. 動作環境
Red Hat Enterprise Linux 9.2
Houdini Education Edition Version 20.0 Build 547 Py39
0-2. 免責
この記事をもとに被った不利益について私は一切責任を負いません.
1. Script Solverとは?
SideFXの公式ドキュメントによると
Script Solver DOPは、接続されたオブジェクトに対してSolveステップとしてHScriptコマンドを実行します。 通常では、このコマンドは、外部スクリプトファイルをソースとします。 このスクリプトは、どのHScriptコマンドも実行することができ、さらには、いくつかのコマンドは、Script Solver内からのみ実行することができます。
(中略)
Code Snippetモードは、Pythonを使用してSolveステージを定義することができます。
らしいです.つまりDOPオブジェクトに対してHScriptなりPythonなりを使用して何かしらの処理が書けるわけです.
この記事では、Code SnippetモードでPythonを使用する場合のみを扱います.
1-1. Script Solverでもノードでもできること
- 新しいDOPオブジェクトを作成する
- シミュレーションからDOPオブジェクトを削除する
- DOPオブジェクトにサブデータを取り付ける
- DOPオブジェクトからサブデータを削除する
- リレーションシップを作成する
など...
1-2. Script Solverでしかできないこと
- シミュレーションのメタデータ(メモリ使用量など)を取得する
- 複数のDOPオブジェクト間でのデータのやり取り
- 複雑な条件をもとにリレーションシップを組む
など...
1-3. Script Solverでできないこと
ない(たぶんある)
2. DOPオブジェクトに対する操作
の前に、まずサンプルのシーンを作ります.
下の画像のようにempty objectとscript solverをつなぐだけです.
script solverの"Use Code Snippet"にチェックを入れるとPython Snippetがアクティブになるのでそのまま再生してみると、コンソールに文字が表示されると思います.
すでにコードの雛形がコメントアウトされているのでこれを参考に書いていけばよさそうですが長くなってしまうのでいったん全削除します.
def solveForObjects(
solver_data, new_dop_objects, existing_dop_objects, time, timestep):
"""Solve for the objects that use this solver.
solver_data:
The hou.DopData for the solver data created by the Python solver DOP
node. A shared instance of this data is attached to each of the
objects being solved. The parameter values from this node will have
been copied into fields of the options record on this solver data.
The solver_data is read-only while the solver is running.
new_dop_objects:
hou.DopObjects that have never been solved before by this solver.
You may want to create new data on each of these objects.
existing_dop_objects:
hou.DopObjects that have been solved by this solver in previous
timesteps. You may want to update the data on each of these
objects.
time:
The current simulation time. This time may be different from the
playbar's current time.
timestep:
The amount of simulation time since the solver was last invoked.
"""
# Fields on the DOP data will correspond to parameters on the solver node.
# ******** Put your code here ********
print('solving new', new_dop_objects, 'existing', existing_dop_objects)
#for dop_object in new_dop_objects:
# initial_position = solver_data.options().field('initialposition')
# sub_data = dop_object.createSubData('MySolverData', 'SIM_EmptyData')
# sub_data.options().setField('position', initial_position)
#
#for dop_object in existing_dop_objects:
# sub_data = dop_object.findSubData('MySolverData')
# position = sub_data.options().field('position')
# position += hou.Vector3(1, 2, 3)
# sub_data.options().setField('position', position)
#
# with dop_object.editableGeometry() as geo:
# ...do something to modify the geometry...
scipt solverが処理された時点でこのsolveForObjectsという関数が実行され、solver_data, new_dop_objects, existing_dop_objects, time, timestepという引数が渡されます.
意味はそれぞれ
- solver_data
- このソルバによって作られたソルバデータ
- hou.DopDataオブジェクト
- new_dop_objects
- このソルバによってまだ処理されたことのないDOPオブジェクト
- hou.DopObjectsオブジェクトのリスト
- existing_dop_objects
- これまでに処理されたことのあるDOPオブジェクト
- hou.DopObjectsオブジェクトのリスト
- time
- シミュレーションの現行時間
- timestep
- このソルバのタイムステップの長さ
となっています.
2-1. サブデータの追加
早速上で作ったempty objectにサブデータを追加していきます.毎タイムステップ追加するわけにも行かないので今回はnew_dop_objects
を使用します.
まずは空のサブデータを作ります.
def solveForObjects(solver_data, new_dop_objects, existing_dop_objects, time, timestep):
for dop_object in new_dop_objects:
#hou.DopObject.createSubData(サブデータ名: str, Dopデータタイプ名: str) -> hou.DopData
dop_object.createSubData('MySolverData', 'SIM_EmptyData')
シミュレーションをリクックしたあとジオメトリスプレッドシートを見ると、ちゃんとMySolverDataというサブデータが作られていることがわかります.
では、ジオメトリのサブデータを作りたいときはどうすれば良いのでしょうか.
もう一度ジオメトリスプレッドシートをよく見ると、なにやらサブデータのBasic
レコードにはdatatype
なるフィールドがありそれが関係してそうです.
これをもとにコードを追加します.
def solveForObjects(solver_data, new_dop_objects, existing_dop_objects, time, timestep):
for dop_object in new_dop_objects:
#hou.DopObject.createSubData(サブデータ名: str, Dopデータタイプ名: str) -> hou.DopData
dop_object.createSubData('MySolverData', 'SIM_EmptyData')
+ dop_object.createSubData('MyGeometryData', 'SIM_SopGeometry')
2-1-1. サブデータの種類
一応参考としてサブデータの種類を一部列挙します.
- SIM_EmptyData
- SIM_SopGeometry
- SIM_GeometryCopy
- SIM_SolverScript
- SIM_Object
- SIM_Relationship
- SIM_ColliderLabel
- SIM_PhysicalParms
など...
hou.DopDataTypeクラスのページが消えてる...
DOPデータタイプ名とhou.DopDataTypeインスタンスが一対一対応してないらしい
2-2. サブデータの修正
2-2-1. ジオメトリサブデータを修正する
まずはサンプルシーンを作ります.ジオメトリがほしいのでsop geometryを使って適当にSOPsから持ってきます.
テンプレートをチラ見しつつコードを書きます.
def solveForObjects(solver_data, new_dop_objects, existing_dop_objects, time, timestep):
for dop_object in new_dop_objects:
#hou.DopObject.editableGeometry(name = サブデータ名: str) -> hou.EditableDopGeometryGuard or None
with dop_object.editableGeometry(name = "Geometry") as geo:
#初回だけ実行される
#@Cdつくって初期化
geo.addAttrib(hou.attribType.Point, "Cd", [0.0, 0.0, 0.0])
for dop_object in existing_dop_objects:
with dop_object.editableGeometry(name = "Geometry") as geo:
#2回目以降、毎タイムステップ実行される
#色を変更する(別にsolverでやる必要はない)
for point in geo.points():
color = [time % 3 / 2, time % 5 / 4, time % 7 / 6]
point.setAttribValue("Cd", color)
再生するとジオメトリの色が変わっていくのがわかると思います.
テンプレートではeditableGeometry()
に引数は渡してないですが与えといたほうがお行儀が良いので与えときます.
よく見ると、with
ステートメント以下はPython SOPでやっていることと全く同じことに気がつくかと思います.
SOPsのPythonが使える方であればあとは何でもできますね.
数値計算させることもできますがマイクロソルバにやらせたほうが早いので、どちらかというとデータフローにおけるジオメトリに対するアドホックな処理みたいな使い方が正しい気がします.
2-2-2. データを修正する
ジオメトリ以外のデータも修正してみたいと思います.
まず、script solverの上にgravityを追加してForces/Gravityというデータを追加します.
そしてこの重力を10倍にしてみたいと思います.
def solveForObjects(solver_data, new_dop_objects, existing_dop_objects, time, timestep):
for dop_object in existing_dop_objects:
#hou.DopObject.findSubData(サブデータ名もしくはパス: str) -> hou.DopData or None
#サブデータの探索
gravitySubData = dop_object.findSubData("Forces/Gravity_gravity1")
#hou.DopData.options() -> hou.DopRecord
#gravityデータのoptionsレコードを取得
optionsRecord = gravitySubData.options()
#hou.DopRecord.field(フィールド名: str) -> int , bool , float , str , hou.Vector2, hou.Vector3, hou.Vector4, hou.Quaternion, hou.Matrix3, or hou.Matrix4
#forceフィールドの値を取得
force = optionsRecord.field("force")
#hou.DopRecord.setField(フィールド名: str, 値: int, float, str, hou.Vector2, hou.Vector3, hou.Vector4, hou.Quaternion, hou.Matrix3, hou.Matrix4) -> None
#forceフィールドの値を更新
optionsRecord.setField("force", force * 10)
cf.
hou.DopDataクラスのドキュメント
hou.DopRecordクラスのドキュメント
ジオメトリスプレッドシートを見るとちゃんと値が更新されていることがわかります.
2-3. サブデータの削除
ではForcesというサブデータを消してみます.
def solveForObjects(solver_data, new_dop_objects, existing_dop_objects, time, timestep):
for dop_object in existing_dop_objects:
#hou.DopObject.removeSubData(サブデータ名もしくはパス: str) -> None
#Forcesサブデータを削除する
dop_object.removeSubData("Forces")
3. シミュレーションオブジェクトに対する操作
まずはサンプルシーンを作ります.
empty objectにscript solverをつなぐだけです.
3-1. 新しいDOPオブジェクトの作成
試しに1フレームに1つ新しいDOPオブジェクトを作ってみたいと思います.
以下のコードをscript solverに入れます.
#solverForObject関数外のコードはdopnet上とscript solver上の2回実行される
#solverForObject関数内のコードはdopnet上で実行される
#script solver上で実行されているなら
#適切な名前に置き換えてください
if hou.pwd().name() == "scriptsolver1":
#現行のシミュレーションオブジェクトを取得
simObject = hou.node("./").simulation()
#hou.DopSimulation.createObject(オブジ ェクトの名前: str, 作られたフレームで処理するか: bool) -> hou.DopObject or None
#新しいオブジェクトを作る
objectName = "newObj" + str(simObject.time() * 24.0)
newObject = simObject.createObject(name = objectName, solve_on_creation_frame = True)
#Geometryサブデータを作る
newObject.createSubData("Geometry", "SIM_SopGeometry")
def solveForObjects(solver_data, new_dop_objects, existing_dop_objects, time, timestep):
pass
cf.
hou.DopSimulationクラスのドキュメント
入力したあと、Python SnippetのParameter OperationsをSet Always
にします.
ジオメトリスプレッドシートを見るときちんとDOPオブジェクトが作成されています.
コメントにも書きましたが、solveForObject関数外のコードはscript solver上とdopnet上の2回、関数内のコードはdopnet上の1回実行される仕様みたいです.
地味にハマりました.
あとsolveForObject関数は必須です.
一見なくても動いたりしますが、古いコードがいるっぽいだけですのでHoudiniを起動し直すとエラーが出ます.
3-2. DOPオブジェクトの削除
試しに直近4フレーム以前に作られたDOPオブジェクトを削除してみます.
#solverForObject関数外のコードはdopnet上とscript solver上の2回実行される
#solverForObject関数内のコードはdopnet上で実行される
#script solver上で実行されているなら
#適切な名前に置き換えてください
if hou.pwd().name() == "scriptsolver1":
#現行のシミュレーションオブジェクトを取得
simObject = hou.node("./").simulation()
#hou.DopSimulation.createObject(オブジ ェクトの名前: str, 作られたフレームで処理するか: bool) -> hou.DopObject or None
#新しいオブジェクトを作る
objectName = "newObj" + str(simObject.time() * 24.0)
newObject = simObject.createObject(name = objectName, solve_on_creation_frame = True)
#Geometryサブデータを作る
newObject.createSubData("Geometry", "SIM_SopGeometry")
+ #script solverのパス
+ thisNodePath = newObject.creator().path()
def solveForObjects(solver_data, new_dop_objects, existing_dop_objects, time, timestep):
- pass
+ for object in existing_dop_objects:
+ #Basicレコードを取得
+ basicRecord = object.record("Basic")
+
+ #creation timeフィールドの値を取得
+ creationTime = basicRecord.field("creationtime")
+
+ #creatorフィールドの値を取得
+ creator = basicRecord.field("creator")
+
+ #作られた時刻が現行から4フレーム前より小さく、このノードで作られたDOPオブジェクトなら削除する
+ referenceTime = simObject.time() - (4.0 * simObject.timestep())
+
+ if (creationTime <= referenceTime) & (creator == thisNodePath):
+ simObject.removeObject(object)
ジオメトリスプレッドシートを見ると直近4フレーム以前にscript solverによって作られたDOPオブジェクトは削除されています.
たまに5ついるのどうにかしたい...
3-3. すべてのリレーションシップの削除
まずサンプルのシーンを作ります.
気分を変えてpop object3つをmergeにつないだ後script solverにつなぎます.
その後script solverのMake Objects Mutual Affectorsのチェックを外しておきます.
これにチェックが入っていると、せっかく消したrelationshipがこのノードによってすべて互いに影響するようになってしまいます.
ジオメトリスプレッドシートのAffector Matrixを見てみると、すべてのPOPオブジェクトが互いに影響を与えていることがわかると思います.
これらのrelationshipを削除していきます.
if hou.pwd().name() == "scriptsolver1":
#現行のシミュレーションオブジェクトを取得
simObject = hou.node("./").simulation()
#hou.DopSimulation.relationships() -> hou.DopRelationship
#すべてのrelationshipを取得
rels = simObject.relationships()
#hou.DopSimulation.removeRelationship(削除するrelationship: hou.DopRelationship) -> None
#すべてのrelationshipを削除する
[simObject.removeRelationship(rel) for rel in rels]
def solveForObjects(solver_data, new_dop_objects, existing_dop_objects, time, timestep):
pass
ジオメトリスプレッドシートのAffectors Matrixを見てみるとすべてのrelationshipが削除されていることがわかると思います.
削除するhou.DopRelationship
オブジェクトを条件でフィルタすれば任意のrelationshipを作成することができます.
3-4. リレーションシップの作成
wip
4. 実例
4-1. DOPオブジェクトの処理順を動的に変更する
wip
5. おわり
wip