この記事はMaya Advent Calendar 2021の7日目の記事です。
戦国時代で真っ直ぐな気性の武将というと福島正則。
どうも、クリーク・アンド・リバー社 COYOTE CG STUDIO テクニカルチーム 戦国大好き人間の中林です。
今回はMayaの頂点の法線の向きで困ったことがあったので、対応した時のお話です。
そもそものキッカケは?
アーティストさんから相談を受けるまで知らなかったですが、とある条件で重なる頂点を選択して法線の平均を実行しても同じ方向を向かないことがあります。
それは、角度の違うメッシュモデルでハードエッジの境目の重なる頂点を選択して、平均化してもバラバラの方向を向きます。
この状態では境界の線が目立ってしまうので、アーティストさんから「このようなケースでも各頂点の法線の向きを合わせられないか」と相談されました。
法線の向きと角度を計算して揃える
行うのは至って単純で
1・ソースモデルとターゲットモデルの角度の差を取得する
2・ソースモデルの頂点法線の向きから角度の差を足した値を取得
3・ターゲットモデルの頂点に2の結果を代入する
これだけでOKです。
2Dだと一瞬で終わるのですが、残念ながら3Dだと簡単にはいきませんでした。
3Dで問題は普通の角度で見かける
(X:10° Y:20° Z:30°)
この表記は人間に分かり易く書かれたオイラー角になります。が、プログラムでは計算に向いていません。
3Dの角度計算にはクォータニオンが必須です。
今回は僕のブログでは珍しくmelではなくpythonのコードで紹介します。
クォータニオン情報が欲しいので、関連コマンドのあるOpenMayaを使うためpythonになります。
ちなみに僕の数学力はベクトルもクォータニオンも正確には理解してないです。それでも、構いません。
今回はそれぞれの結果を良いとこどりをするだけだからです。
簡単なクイズ
Mayaでオブジェクトの一部を分割してから(X:10° Y:20° Z:30°)で回転させてフリーズをします。
フリーズした(X:0° Y:0° Z:0°)から、分割したパーツを元に戻すにはどうしますか?
ちなみに、3Dの回転に慣れてない人は(X:-10° Y:-20 Z:-30)と、角度の逆数で元の位置に戻ると考えがちですが、そうは問屋が卸しません。
こんな感じでズレます。
最も単純な答えは分割したモデルを3段階グループ化して、それぞれにrotXYZの角度の逆数を与えるだけです。
rotX(X:-10°)
┗rotY(Y:-20°)
┗rotZ(Z:-30°)
┗分割パーツ
※Mayaのデフォルト設定の場合です。
ちなみにグループの親子関係の順番を変えると元の位置に戻りません。
このXYZの順番が俗に言う回転順序というものです。
回転順序はゲームエンジンによって変わります。
余談ですけど、この時のオブジェクトをグループから外すと謎のオイラー角が入ります。これはMayaが内部処理でクォータニオンの変換をしてくれるからです。
TAになる前はクォータニオンを知らず、melでこの条件を再現して無理やり角度を取得することは良くしていました。実はこれを利用した手抜きは今でも使うことはあります。
1・ソースモデルとターゲットモデルの角度の差を取得する
文章にすると簡単だけど3Dの計算だと一筋縄ではいきません。例えばよく見かける(X:10° Y:20° Z:30°)は人間に分かり易いオイラー角なので、クイズで試した通りに単純な引き算ではできません。
ここでクォータニオンの結果だけが必要になってきます。
import maya.cmds as cmds
import maya.api.OpenMaya as om2
import math
# オブジェクトからオイラー角を取得
rotXYZ = cmds.getAttr('オブジェクト名.r')[0]
# openMaya用のオイラー角に変換
euler = om2.MEulerRotation(math.radians(rotXYZ[0]),
math.radians(rotXYZ[1]),
math.radians(rotXYZ[2]),
om2.MEulerRotation.kXYZ)
# オイラー角をクォータニオンに変換する
quat = euler.asQuaternion()
ここで重要なのは「om2.MEulerRotation.kXYZ」が回転順序を指定してることです。
om2.MEulerRotation.kZYX
om2.MEulerRotation.kXZY
とXYZの順番を変えてしまうと結果が大きく変わります。今回はクイズで検証したとおりにMayaの回転順序はX⇒Y⇒Zなので「kXYZ」で意図したクォータニオンが取得できます。
オイラー角の足し算は慣れると単純で、クォータニオン同士を掛けるだけです。
何故、掛け算にすると角度を足したことになるかは重要ではないので省きます。僕が欲しいのは結果だけです!!
仮にソースモデルが(X:20° Y:30° Z:40°)
ターゲットモデルが(X:10° Y:20° Z:30°)
だった場合は結果は(X:30° Y:50° Z:70°)になります。
気になる人はオイラー角に戻して、結果を確認してみてください。
# クォータニオンの合成(角度の足し算)
addQuat = sourceQuat * targetQuat
# クォータニオンをオイラー角に変換する
euler = addQuat.asEulerRotation()
print(math.degrees(euler.x), math.degrees(euler.y), math.degrees(euler.z))
ここでツッコミどころは足し算ではなく引き算の結果が欲しいことです。クォータニオンを掛ければ足し算なので、割れば引き算になるのか。
そこは単純ではありません。ただ、簡単な数式で考えると
5 – 3 = 2
これは-3を逆数とするとこんな数式でもできます。
5 + (-3) = 2
クォータニオンにも逆クォータニオンに変化させるコマンドがあります。
# 角度を逆クォータニオンに変換
targetQuat = targetQuat.inverse()
# クォータニオンの合成(角度の足し算だけど、逆なので引き算)
subQuat = sourceQuat * targetQuat
これで、ソースモデルとターゲットモデルの角度の差が取得できます。
ソースモデルが(X:20° Y:30° Z:40°)
ターゲットモデルが(X:10° Y:20° Z:30°)
だった場合は結果は(X:10° Y:10° Z:10°)になります。
この後もクォータニオンを使うので確認以外では無理にオイラー角に戻す必要は無いです。
2・ソースモデルの頂点法線の向きから角度の差を足した値を取得
今のところ法線の向きはベクトル、角度の差はクォータニオンです。ベクトルとクォータニオンは簡単に足せるのか?
僕は正直知りません。でも、ベクトルにクォータニオンの角度を加えるコマンドがOpenMayaにあるのは知ってます。
# 頂点の法線情報をベクトルで取得
sourceVec = cmds.polyNormalPerVertex('ソース頂点番号', q = True, xyz = True)
sourceVec = om2.MVector(sourceVec)
# ベクトル✖クォータニオンのコマンド
targetVec = sourceVec.rotateBy(_subQuat)
これだけでソースの頂点と角度の差を足した結果が取得できます。ここでターゲットの頂点の法線のベクトルは取得できています。
cmds.polyNormalPerVertex('ターゲット頂点番号', xyz = targetVec)
『3・ターゲットモデルの頂点に2の結果を代入する』は項目を作る必要すらなく終わりました。
頂点の分だけfor文で繰り返すだけで頂点の法線を綺麗に一致させることができます。
理解も重要だけど結果を知ってることも重要
理解をして使いこなせることは凄いことだと思います。
ただ、僕は知ってる結果だけを利用して作ることも大切だと思っています。クォータニオンを理解してなくても、掛ければ全てのオイラー角の足した値が取得できる知識があれば有用なこともあります。
ベクトルは理解してなくても、足し算の結果を知っていると三角関数で数行かかる計算が1行で終わることもあります。
また、不思議なことですが同じ結果を何十回も繰り返し使うことで、逆に内容が理解できることもあります。
何をしているかを理解することも重要だと思います。だけど、時間のない時は公式やルールと割り切って何ができるか結果だけを知って前に進むことも重要だと僕は信じてます。
明日12/8は phyblasさんの記事
「『mmdpaimaya』 MMDとmayaの間でモデルを変換するpythonスクリプト」です、おたのしみに!