クロスシミュレーションされた布の頂点を動的に制御する方法

記事のまとめ

  • クロスシミュレーションがついているメッシュの頂点を制御する方法を紹介
  • 制御したいときだけ有効にできるような方法
  • HookモディファイアとVertex Weight Mixモディファイアを利用
  • 面倒な初期設定を行うためのスクリプトも紹介

やりたいこと

オブジェクトに被さった布をつまみ上げたり、お姫様キャラのモデルに挨拶をさせるときにスカートを摘まみ上げたり、「布をつまみあげる」操作をしたいケースって結構あると思います。

ただ基本的には常に頂点を制御したいわけではなく、布をつまみ上げている時だけ制御して、他のフレームでは普通にクロスシミュレーションに従って欲しいわけです。

やりたいことの実現例

要は、こちらの動画のようなことをやりたいわけです。

f:id:ronwall1701:20201218151709g:plain

こちらの例では、クロスシミュレーションされている布上のある頂点(片方のEmptyが追従している点)を、一時的に制御用のEmptyに追従させ持ち上げています。それ以外の時点では、頂点は他の部分と同じようにクロスシミュレーションに従っています。

どうやるのか

クロスシミュレーションで一部の頂点の位置を固定するには、ピン止めする頂点グループ(以降pinグループと呼ぶ)をあらかじめ設定しておけばよいです。

しかし今回は

  1. 制御対象の頂点を動的に変更したい
  2. (例えば今回のEmptyのように)制御対象の頂点を別オブジェクトに追従させたい

という2つの要求があるので、単にピン止めする頂点を静的に設定しておくだけではだめです。

今回は、この要求をそれぞれVertex Weight Mixモディファイアと、Hookモデイファイアで解決しました。

Vertex Weight Mixモディファイア

このモディファイアは、オブジェクトの各頂点がある頂点グループAに対して持つ重みを、別の頂点グループBの重みを用いて変更する機能をもっています。

今回のケースでは、常に固定したい頂点グループ(pinグループ)の重みに、一時的に制御したい頂点グループ(hookグループ)の重みを足し合わせるような処理を行えば良さそうです。そうすることによって、クロスシミュレーションで固定される頂点グループを動的に変更することができます。

そのような処理を行うためのパラメータは、例えば以下のようになります。ここで、pinは常に固定する頂点を含む頂点グループで、hook1が動的に制御を切り替える頂点を含む頂点グループです。(どちらも同じクロスオブジェクトの頂点グループです。)

f:id:ronwall1701:20201218151841p:plain

Hookモディファイア

このモディファイアは、オブジェクトの一部の頂点の位置を別のオブジェクトに追従させるような機能をもっています。

今回のケースでは、一時的に制御したい頂点を含む頂点グループ(hookグループ)を、(先ほどのEmptyのような)制御用のオブジェクトに追従させれば良さそうです。そうすることによって、制御用オブジェクトの位置変化を制御対象の頂点に反映させ、クロスシミュレーションに用いることができます。

パラメータ設定は、今回は以下のようにしました。

f:id:ronwall1701:20201218151845p:plain

パラメータの動的変更

以上の2つのモディファイアのパラメータを(同時に)変更することによって、対象となる頂点を動的に変更しながら位置を制御することができます。より具体的には、以下のパラメータを設定します:

  • Vertex Weight MixモディファイアのGlobal Influenceパラメータ
  • 0.0でオフ、1.0でオン
  • HookモデイファイアのStrengthパラメータ
  • 0.0でオフ、1.0でオン

ただし、このパラメータの変更速度が速すぎるとシミュレーションの結果が破綻するケースが確認されたので、これらのパラメータはある程度の余裕をもたせてゆっくり変更すると良いかもしれません。

モディファイア設定コード

布の複数の場所を別々につまみたい場合、以上の設定を場所ごとにすべて設定しなければなりません。それでは面倒なので、今回は以上のモディファイアの設定を自動で行うスクリプトを用意しました。

使い方ですが、動的に制御したい頂点を編集モードで選択した状態でScriptingウィンドウなどから実行してください。この時スクリプトの最初の方にあるNAME変数とPIN_GROUPはそれぞれ適切に変更してから実行してください。(以下の説明も参照)

  • NAME
  • 動的に制御される頂点の頂点グループ、制御・追従用のEmptyの名前に利用される。
  • _Tracer.{NAME}, _Handler.{NAME}がそれぞれ追従用、制御用のEmpty
  • 追従用のEmptyは制御対象の頂点に常に追従する
  • PIN_GROUP
  • 常に固定される頂点を含む頂点グループの名前
  • この頂点グループがクロスシミュレーションのpin止め頂点グループとして登録されていると仮定

実際に制御を開始する際には、追従用のEmptyの位置を利用するなどして、制御対象の頂点の位置と制御用のEmptyの位置があまり離れないようにした方が、破綻などが起こりづらいかもしれません。

# NOTE: 編集モードで、つかみたい頂点を選択している状態で実行すること

from functools import reduce
import bpy

NAME = "hook1"
PIN_GROUP = "pin"

# 選択頂点の座標を読み込む
mode = bpy.context.active_object.mode
bpy.ops.object.mode_set(mode='OBJECT')
selectedVerts = [v for v in bpy.context.active_object.data.vertices if v.select]
selectedVertsId = [v.index for v in bpy.context.active_object.data.vertices if v.select]
target_obj = bpy.context.active_object

# 掴む対象の頂点グループを作成
hook_group = target_obj.vertex_groups.new(name=NAME)
hook_group.add(selectedVertsId, 1.0, "ADD")

# 選択頂点のグローバル座標を計算
local2world = target_obj.matrix_world
pos = reduce(sum, [local2world @ v.co for v in selectedVerts]) / len(selectedVerts)
print(pos)

# 頂点位置追従用のEmptyと、位置設定用のEmptyを作成する
bpy.ops.object.add(type="EMPTY", location=pos)
objs = bpy.context.selected_objects
assert len(objs)==1
tracer_empty = objs[0]
tracer_empty.name = f"_Tracer.{NAME}"

bpy.ops.object.add(type="EMPTY", location=pos)
objs = bpy.context.selected_objects
assert len(objs)==1
handle_empty = objs[0]
handle_empty.name = f"_Handle.{NAME}"

# 対象のオブジェクトのトップにモディファイアを設定する関数
def add_modifier(type, obj):
initial_names = set([m.name for m in obj.modifiers])
bpy.context.view_layer.objects.active = obj
bpy.ops.object.modifier_add(type=type)
after_names = set([m.name for m in obj.modifiers])
modifier_name = list(after_names - initial_names)[0]
while obj.modifiers[0].name != modifier_name:
bpy.ops.object.modifier_move_up(modifier=modifier_name)

return obj.modifiers[modifier_name]

# Hookモデイファイアの設定
modifier = add_modifier("HOOK", target_obj)
modifier.falloff_type = "NONE"
modifier.object = handle_empty
modifier.strength = 1.0
modifier.vertex_group = NAME

# Vertex Weight Mixモデイファイアの設定
modifier = add_modifier("VERTEX_WEIGHT_MIX", target_obj)
modifier.vertex_group_a = PIN_GROUP
modifier.vertex_group_b = NAME
modifier.mix_set = "B"
modifier.mix_mode = "ADD"

# 
constraint = tracer_empty.constraints.new("COPY_TRANSFORMS")
constraint.target = target_obj
constraint.subtarget = NAME

さいごに

物理シミュレーションのキャッシュが残っていたり、制御が急すぎると破綻したりするので結構扱いが難しいです。とはいえいろいろ調べても他に良さそうな方法も出てこず…。何か別のアプローチなどご存じの方はお教えください。