攻撃のヒット位置と角度の自動算出

はじめに

こんにちは。モノリスソフト テクニカルアーティストの廣瀬です。
今回はMayaのPythonでモーションから攻撃のヒット位置と角度を算出してみます。maya.cmdsだけでなくOpenMayaも使用しますので、MayaのPythonの踏み込んだ勉強にお役立てください。

スクリプトは簡単に試せるようにしてありますので、MayaのPythonを触ったことがない方にも触るきっかけになればと思います。

テストシーンの用意

テスト用のモーションシーンを準備します。既にあるよ!という人は飛ばしてください。

メイン メニュー バーから:ウィンドウ > コンテンツ ブラウザ(Windows > Content Browser) を開きます。

tech_21_02.png

参照パネルから:Examples > Animation > Motion Capture > FBX を選択します。
コンテンツ パネルから:DoorSlam.fbxをダブルクリックしてインポートします。

tech_21_03.png

これでテスト用のシーンを準備出来ました。

tech_21_04.png

実行してみる

詳しい説明の前にまずはスクリプトを実行してみます。

メイン メニュー バーから:作成 > ロケーター(Create > Locator) を2回繰り返してロケーターを2つ作成します。

作成したロケーターの1つは「target」もう1つは「hit」とリネームしておきます。ロケーターが小さくて確認しずらい場合は、必要に応じてローカルスケール (Local Scale) を見やすい大きさに変更してください。

「target」ロケーターは敵の位置を指定するためのロケーターです。
「hit」ロケーターは攻撃のヒット位置と角度の結果を出力するためのロケーターです。

tech_21_05.png

「target」ロケーターの攻撃時に敵がいるであろういい感じの位置に移動します。

tech_21_06.png

メイン メニュー バーから:ウィンドウ > スクリプト エディタ(Windows > Script Editor) を開きます。

tech_21_07.png

スクリプトエディタの入力ペインのPythonタブに下記のスクリプトをコピー&ペーストします。

Pythonタブが無い場合はこちらの公式ヘルプを参照してください。使用しているMayaのバージョンで追加方法が異なります。

スクリプト ソースを展開 ソースを折りたたむ


  1. import maya.cmds as cmds
  2. import maya.api.OpenMaya as OpenMaya
  3. def get_transform(frame, source_node, target_node, hit_node):
  4. target_pos = cmds.xform(target_node, q=True, ws=True, t=True)
  5. if cmds.ls('|trail', typ='transform'):
  6. cmds.delete(cmds.ls('|trail', typ='transform'))
  7. snapshot = cmds.createNode('snapshot')
  8. cmds.setAttr(snapshot + '.startTime', frame-3.0)
  9. cmds.setAttr(snapshot + '.endTime', frame+3.0)
  10. cmds.setAttr(snapshot + '.increment', 1.0)
  11. cmds.connectAttr(source_node + '.worldMatrix', snapshot + '.inputMatrix')
  12. points = cmds.getAttr(snapshot + '.points')
  13. points = [(p[0], p[1], p[2]) for p in points]
  14. temp = cmds.curve(d=1, p=points)
  15. xform = cmds.fitBspline(temp, tol=1e-6, ch=False, o=True, n='trail')
  16. shape = cmds.listRelatives(xform)[0]
  17. cmds.delete(temp)
  18. sel = OpenMaya.MSelectionList()
  19. sel.add(shape)
  20. curve = OpenMaya.MFnNurbsCurve(sel.getDependNode(0))
  21. parm1 = curve.getParamAtPoint(OpenMaya.MPoint(points[2]))
  22. parm2 = curve.getParamAtPoint(OpenMaya.MPoint(points[3]))
  23. parm3 = curve.getParamAtPoint(OpenMaya.MPoint(points[4]))
  24. sparm1 = parm1 + (parm2-parm1) / 2.0
  25. sparm2 = parm2
  26. inc1 = (parm2-parm1) / 20.0
  27. inc2 = (parm3-parm2) / 20.0
  28. parms = []
  29. parms.extend(sparm1 + inc1*i for i in range(0, 11))
  30. parms.extend(sparm2 + inc2*i for i in range(1, 11))
  31. points = [OpenMaya.MVector(curve.getPointAtParam(p)) for p in parms]
  32. vectors = [OpenMaya.MVector(target_pos) - p for p in points]
  33. dists = [v.length() for v in vectors]
  34. dist = min(dists)
  35. parm = parms[dists.index(dist)]
  36. p = OpenMaya.MVector(curve.getPointAtParam(parm))
  37. z = (OpenMaya.MVector(target_pos) - p).normalize()
  38. x = OpenMaya.MVector(-curve.tangent(parm)).normalize()
  39. y = (z ^ x).normalize()
  40. z = (x ^ y).normalize()
  41. mat = (
  42. x[0], x[1], x[2], 0.0,
  43. y[0], y[1], y[2], 0.0,
  44. z[0], z[1], z[2], 0.0,
  45. p[0], p[1], p[2], 1.0)
  46. cmds.delete(snapshot)
  47. cmds.xform(hit_node, m=mat)
  48. return cmds.xform(hit_node, q=True, ws=True, t=True), cmds.xform(hit_node, q=True, ws=True, ro=True)

tech_21_08.png

一番下の行にスクリプトを呼び出すためのコードを書きます。

  1. get_transform(frame=15, source_node='DoorSlam:RightHand', target_node='target', hit_node='hit')

frameの15は攻撃のヒット時のフレーム番号です。モーションに合わせて変更してください。

source_nodeの'DoorSlam:RightHand'は攻撃判定を発生させるノードです。モデル・モーションに合わせて変更してください。攻撃が剣だったら剣の中心など...。スケルトンやロケーターなど何でも構いません。

target_nodeの'target'とhit_nodeの'hit'は先ほど作成したロケーターのことです。

tech_21_09.png

実行するとモーションの軌跡のカーブが作成されて「hit」ロケータの位置と角度が更新されます。

tech_21_10.png

スクリプトの内容

ここではスクリプトの内容を大雑把に解説していきます。
何言ってるかよくわからん!!という人は流し見してください。

target_nodeのワールド座標での位置を取得します。

  1. target_pos = cmds.xform(target_node, q=True, ws=True, t=True)

以前作成したモーションの軌跡のカーブがあれば削除します。

  1. if cmds.ls('|trail', typ='transform'):
  2. cmds.delete(cmds.ls('|trail', typ='transform'))

スナップショットノードを作成ます。
スナップショットノードは.inputMatrixにつないだノードの指定したフレームレンジのトランスフォームを一度に取得できます。

ここではフレームレンジをframeから±3フレームにしています。

前後フレームに余裕を持たせているのは、フレーム間のカーブの補完をできるだけ正確にするためです。

  1. snapshot = cmds.createNode('snapshot')
  2. cmds.setAttr(snapshot + '.startTime', frame-3.0)
  3. cmds.setAttr(snapshot + '.endTime', frame+3.0)
  4. cmds.setAttr(snapshot + '.increment', 1.0)
  5. cmds.connectAttr(source_node + '.worldMatrix', snapshot + '.inputMatrix')

スナップショットノードからすべての位置を取得します。

スナップショットノードの位置の結果は [(tx, ty, tz, 1.0), ...] で返ってくるため18行目で [(tx, ty, tz), ...] に変換しています。

  1. points = cmds.getAttr(snapshot + '.points')
  2. points = [(p[0], p[1], p[2]) for p in points]

位置のリストからNURBSカーブを作成します。

入力位置を通るカーブを作成するためにまず次数1のNURBSカーブを作成し、そこから cmds.fitBspline コマンドでなめらかなカーブに変換しています。

  1. temp = cmds.curve(d=1, p=points)
  2. xform = cmds.fitBspline(temp, tol=1e-6, ch=False, o=True, n='trail')
  3. shape = cmds.listRelatives(xform)[0]
  4. cmds.delete(temp)

作成したNURBSカーブの名前からOpenMaya(MAYA API)の MFnNurbsCurveオブジェクトを取得します。

OpenMayaを使うことでmaya.cmdsでは扱えなかった情報を扱えるようになります。

  1. sel = OpenMaya.MSelectionList()
  2. sel.add(shape)
  3. curve = OpenMaya.MFnNurbsCurve(sel.getDependNode(0))

frameとframe±1フレームのカーブパラメーターを取得します。

上記フレームでのカーブパラメーターはそれぞれ 0.33333333, 0.5, 0.66666666 ではないため、このように取得する必要があります。

  1. parm1 = curve.getParamAtPoint(OpenMaya.MPoint(points[2]))
  2. parm2 = curve.getParamAtPoint(OpenMaya.MPoint(points[3]))
  3. parm3 = curve.getParamAtPoint(OpenMaya.MPoint(points[4]))

下の図の赤色の区間(frame±0.5フレーム)を20分割したカーブパラメーターのリストを作成します。

このリストを作成する理由はヒット位置を frame±0.5フレームの内、最もtarget_nodeに近い位置とするためです。

20分割としているのは、精度的にこの程度あれば十分と判断したためです。(要するに分割数は自由です。)

  1. sparm1 = parm1 + (parm2-parm1) / 2.0
  2. sparm2 = parm2
  3. inc1 = (parm2-parm1) / 20.0
  4. inc2 = (parm3-parm2) / 20.0
  5. parms = []
  6. parms.extend(sparm1 + inc1*i for i in range(0, 11))
  7. parms.extend(sparm2 + inc2*i for i in range(1, 11))

tech_21_11.pngのサムネイル画像

カーブパラメーターのリストからカーブ上の位置のリストを作成します。
そこからtarget_nodeまでの距離のリストを作成します。

  1. points = [OpenMaya.MVector(curve.getPointAtParam(p)) for p in parms]
  2. vectors = [OpenMaya.MVector(target_pos) - p for p in points]
  3. dists = [v.length() for v in vectors]

arget_nodeまでの距離が最も近いカーブパラメーターを取得します。

  1. dist = min(dists)
  2. parm = parms[dists.index(dist)]

カーブ上の位置からtreget_nodeへの方向(エイムベクトル)とカーブの接線(タンジェントベクトル)とカーブ上の位置から変換行列を作成します。

  1. p = OpenMaya.MVector(curve.getPointAtParam(parm))
  2. z = (OpenMaya.MVector(target_pos) - p).normalize()
  3. x = OpenMaya.MVector(-curve.tangent(parm)).normalize()
  4. y = (z ^ x).normalize()
  5. z = (x ^ y).normalize()
  6. mat = (
  7. x[0], x[1], x[2], 0.0,
  8. y[0], y[1], y[2], 0.0,
  9. z[0], z[1], z[2], 0.0,
  10. p[0], p[1], p[2], 1.0)

作成した変換行列をhit_nodeに設定します。

  1. cmds.xform(hit_node, m=mat)

TIPS:

今回はtarget_nodeに一番近い位置を取得するために20分割したものを使用しましたが、以下の様に最も近い位置を取得することもできます。

スクリプト ソースを展開 ソースを折りたたむ


  1. import maya.cmds as cmds
  2. import maya.api.OpenMaya as OpenMaya
  3. def get_transform(frame, source_node, target_node, hit_node):
  4. target_pos = cmds.xform(target_node, q=True, ws=True, t=True)
  5. if cmds.ls('|trail', typ='transform'):
  6. cmds.delete(cmds.ls('|trail', typ='transform'))
  7. snapshot = cmds.createNode('snapshot')
  8. cmds.setAttr(snapshot + '.startTime', frame-3.0)
  9. cmds.setAttr(snapshot + '.endTime', frame+3.0)
  10. cmds.setAttr(snapshot + '.increment', 1.0)
  11. cmds.connectAttr(source_node + '.worldMatrix', snapshot + '.inputMatrix')
  12. points = cmds.getAttr(snapshot + '.points')
  13. points = [(p[0], p[1], p[2]) for p in points]
  14. temp = cmds.curve(d=1, p=points)
  15. xform = cmds.fitBspline(temp, tol=1e-6, ch=False, o=True, n='trail')
  16. shape = cmds.listRelatives(xform)[0]
  17. cmds.delete(temp)
  18. sel = OpenMaya.MSelectionList()
  19. sel.add(shape)
  20. curve = OpenMaya.MFnNurbsCurve(sel.getDependNode(0))
  21. parm1 = curve.getParamAtPoint(OpenMaya.MPoint(points[2]))
  22. parm2 = curve.getParamAtPoint(OpenMaya.MPoint(points[3]))
  23. parm3 = curve.getParamAtPoint(OpenMaya.MPoint(points[4]))
  24. # frame+-0.5の位置でカーブを分割します
  25. sparm1 = parm1 + (parm2-parm1) / 2.0
  26. sparm2 = parm2 + (parm3-parm2) / 2.0
  27. xform1, xform2, xform3, _ = cmds.detachCurve(xform, ch=True, p=(sparm1, sparm2))
  28. shape2 = cmds.listRelatives(xform2)[0]
  29. # frame+-0.5のカーブのtarget_nodeへの最接近位置とカーブパラメータを取得します
  30. sel = OpenMaya.MSelectionList()
  31. sel.add(shape2)
  32. curve2 = OpenMaya.MFnNurbsCurve(sel.getDependNode(0))
  33. point, parm = curve2.closestPoint(OpenMaya.MPoint(target_pos))
  34. # 変換行列を作成します
  35. p = OpenMaya.MVector(curve2.getPointAtParam(parm))
  36. z = (OpenMaya.MVector(target_pos) - p).normalize()
  37. x = OpenMaya.MVector(-curve2.tangent(parm)).normalize()
  38. y = (z ^ x).normalize()
  39. z = (x ^ y).normalize()
  40. mat = (
  41. x[0], x[1], x[2], 0.0,
  42. y[0], y[1], y[2], 0.0,
  43. z[0], z[1], z[2], 0.0,
  44. p[0], p[1], p[2], 1.0)
  45. cmds.delete(snapshot, xform1, xform2, xform3)
  46. cmds.xform(hit_node, m=mat)
  47. return cmds.xform(hit_node, q=True, ws=True, t=True), cmds.xform(hit_node, q=True, ws=True, ro=True)

まとめ

今回はMayaのPythonでcmdsとOpenMayaを組み合わせて使うとこんなことも出来るよ、ということを紹介してみました。

この記事がPythonを使ったことがない方にはPythonデビューの第一歩、使ったことがある方にはさらなるスキルアップの第一歩になれば幸いです。

それでは、Pythonで快適なMayaライフを!

執筆者:廣瀬

映像業界を経てモノリスソフトへ入社。 以来、テクニカルアーティストとして主にエフェクト関連の業務を担当。 好きな食べものはソフトクリーム。

ABOUT

モノリスソフト開発スタッフが日々取り組んでいる技術研究やノウハウをご紹介

RECRUIT採用情報