Mayaミラーアニメーションあれこれ

# あいさつ
はじめまして、Qiitaアカウントを作った記念に
[Maya Advent Calender2023](https://qiita.com/advent-calendar/2023/maya "Maya Advent Calender2023")の23日目に参加させていただきました

今回はたぶん自分にしか需要がなさそうですが
Maya内でのミラーアニメーションツールを作成したときにあれこれ試行錯誤したことをいろいろと書いていこうと思います

# 検証環境
AutoDesk Maya 2023.3

# やりたかったこと
1. シンプルなワールド空間での指定平面ミラーポーズを得ること
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637095/fc8c23b8-63da-3c5c-6892-dcd439e91dd2.png)

2. 親空間となるトランスフォームを指定して、その親空間でのミラーポーズを得ること
![mirror_matrix_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637095/7ade2beb-2675-7e64-0f76-419e4eaf55b3.png)

この２つです

# シンプルなワールド空間での指定平面ミラーポーズを得る
まずひとつ目のワールド空間のシンプルなミラーを実装します

といっても数学そのものは苦手なうえ、数ヶ月の業務時間外の検証と数多の壁にぶち当たる経験を経て
挫折しかけた途中で偉大なる先人様のオープンソースである[ryusas様](https://qiita.com/ryusas "ryusas")の[CyMel](https://qiita.com/ryusas/items/eba6ab1b5af0700799e6 "cymelの紹介")を発見

今回はシンプルなワールド空間ミラーはcymelの利用で実現しました

※挫折しかけたときにいろいろと試行錯誤したものは後述しています

```python: cymel_mirror.py
import cymel.main as cm
import maya.api.OpenMaya as om2
from enum import Enum, IntEnum, auto

class MirrorPlane(Enum):
    """ミラー平面の指定用列挙型"""

    XY = auto()
    YZ = auto()
    XZ = auto()


class MirrorAxis(IntEnum):
    """ミラーの指定軸Index"""

    X = 0
    Y = 1
    Z = 2

def mirrorMatrix(
    original,
    target,
    mirrorPlane: MirrorPlane = MirrorPlane.YZ,
    withTrans: bool = True
):
    """
    マトリックスを指定する平面方向にミラーする
    """
    originalMatrix: om2.MMatrix = original.getMatrix('world')

    # cymelのMatrixクラスをインスタンス
    cyMat = cm.Matrix(originalMatrix)
    
    if mirrorPlane.value == MirrorPlane.XY.value:
        mirrorAxis = MirrorAxis.Z
    elif mirrorPlane.value == MirrorPlane.YZ.value:
        mirrorAxis = MirrorAxis.X
    else:  # MirrorPlane.XZ
        mirrorAxis = MirrorAxis.Y
    
    # cymel Matrixクラス内のミラー関数を利用
    mirrored_Matrix = cyMat.mirror(mirrorAxis, t=withTrans)

    # Pymelなど　検証ではPymelライクな自作クラスで回転セット
    target.setRotation(mirroredMatrix.asDegrees(), "world")

    # 自作クラスで移動セット　pymel PyNodeとほぼ一緒です
    if withTrans:
        target.setTranslation(mirroredMatrix.asTranslation(), "world")

```

オブジェクトがYup向きのトランスフォームのミラー
左から右のトランスフォームに実行した例の画像です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637095/62004716-4bab-c1be-734c-c11bcc58dca9.png)

## つまづきポイント

今回のトランスフォームのミラーリングについて海外記事含めて資料を求めWebを巡る旅をしたのですが、
Maya等3DCGのアニメーションに使うミラーと一般的なマトリクスミラーは性質が異なりました

### マトリクスの平面ミラーリングを学習する

平面ミラー行列とはつまるところ、任意の1軸方向にミラーリングした行列です

YZ平面でミラーする場合、Xベクトル方向に反転すればいいわけですから計算式は
```math
Myz =
\left[
\begin{matrix}
1, 0, 0 \\
0, 1, 0 \\
0, 0, 1
\end{matrix}
\right]
・
\left[
\begin{matrix}
-1, 0, 0 \\
0, 1, 0 \\
0, 0, 1
\end{matrix}
\right]
```
になります

しかし、Mayaのトランスフォームマトリクスで取り扱う場合はここで問題が発生します

### 崩れないXYZの強い絆

たとえばMayaのトランスフォームマトリクスにおいては
Yベクトルの左隣(且つ90度)には必ずXベクトルがある状態でないとスケールやシアーがはいっている
と判断されていると推測されます

つまりscaleに値が入った状態のマトリクスが得られてしまい
これをトランスフォームに適応すると意図しないスケールがはいります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637095/adeebbd4-18a8-c24c-7799-31098a7c2019.png)

上の画像は右のトランスフォームのマトリクスに対して、
Xベクトルだけが-1の単位行列をかけるた結果を左の同じものに適応した結果です

ScaleZにマイナス値が入ってしまいました

### Mayaのマトリクスを求める計算によるものか？

Mayaのトランスフォームのマトリクス算出方法についてしらべると、
計算式的にはスケールから計算を行う記述や情報がありました

逆にマトリクスから移動回転スケールを求める場合も、
同様に移動をうちけし＝＞スケール計算する

この段階で回転行列の各軸のベクトルが使われてしまうがゆえに
比較したときにスケールともっというとシアーも入ってしまうのではないかと思っています


#### 回転行列を利用しているのか検証
検証してみると回転行列部分のベクトルの長さをかえると、
結果を適応したトランスフォームはスケールとシアーもはいってしまいました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637095/339a8c5d-03e6-8693-03bc-6798e6b73614.png)

検証コード
```python: test_matrix_to_transform.py
import maya.api.OpenMaya as om2
import maya.cmds as cmds

_OM2M = om2.MMatrix

def getMatrix(dagPath, space="object"):
    """
    ノードのマトリクスを取得する。
    """
    if space == "world":
        nodeMatrix = _OM2M(
            cmds.getAttr("{}.worldMatrix".format(dagPath.fullPathName()))
        )
    elif space == "object":
        mfnTrfm = om2.MFnTransform(dagPath)
        nodeMatrix = mfnTrfm.transformation().asMatrix()
    else:
        raise ValueError("[Error] 不明な指定空間名です: [{}]".format(space))

    return nodeMatrix


def setMatrix(dagPath, matrix, space="object"):
    """
    ノードのマトリクスを設定する。
    """

    nodeName = dagPath.fullPathName()

    if space == "world":
        cmds.xform(nodeName, ws=True, m=matrix)
    elif space == "object":
        cmds.xform(nodeName, os=True, m=matrix)
    else:
        raise ValueError("[Error] 不明な指定空間名です: [{}]".format(space))


def test_matrix_to_transform():
    # ノードを2つ選択して実行
    # 前者のマトリクスを、ベクトルに長さを持つ行列とかけあわせ後者に適応する
    sellist = om2.MGlobal.getActiveSelectionList()
    original = sellist.getDagPath(0)
    target = sellist.getDagPath(1)

    originalMatrix = getMatrix(original, "world")
    originalMatrix *= _OM2M([
        -2, 0, 0, 0,
         0, 1, 0, 0,
         0, 0, 1, 0,
         0, 0, 0, 1
     ])

    setMatrix(target, originalMatrix, "world")


test_matrix_to_transform()

```

また、クォータニオンを調べるにあたって、
回転行列のみでスケールを表現することもできるという記述がありました

```math
S = \left[
\begin{matrix}
s, 0, 0 \\
0, s, 0 \\
0, 0, s
\end{matrix}
\right]
```

Mayaのスケール計算も同じかは検証していませんが、
今回の検証ではXベクトルの長さを増やしてしまったので
横方向に伸びたと言う結果になったと思っています

やはり回転行列部分のベクトル方向や長さはかなり重要そうです


### そのまま適応はできない
このままではworldMatrixアトリビュートへの接続やxformのセットは使えません
移動回転を取り出して適応しますが、それをすると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637095/c486727c-ebd2-c5d8-e121-877ac9601db1.png)
このようにスケールがない移動回転に変換しても、クオータニオン変換の時点で
XYZの関係性を保持した状態に変換され先程のスケールZが前後逆転してしまいます

一度方針を変えるため、
マトリクスのミラーをやめてクオータニオンのYZ平面ミラーも試しました

クオータニオンでYZ平面ミラーを行う場合、
こちらかなり直感的で以下で良いようです

```python: mirrorYZQuat.py
import maya.api.OpenMaya as om2

_OM2Q = om2.MQuaternion

yzPlMirrorQ =  _OM2Q(q.x, -q.y, -q.z, q.w);
```



<details><summary>動作検証コード</summary><div>

```python: test_quatMirror.py
import maya.api.OpenMaya as om2
import maya.cmds as cmds
import math

_OM2M = om2.MMatrix
_OM2Q = om2.MQuaternion


def getMatrix(dagPath, space="object"):
    """
    ノードのマトリクスを取得する。
    """
    if space == "world":
        nodeMatrix = _OM2M(
            cmds.getAttr("{}.worldMatrix".format(dagPath.fullPathName()))
        )
    elif space == "object":
        mfnTrfm = om2.MFnTransform(dagPath)
        nodeMatrix = mfnTrfm.transformation().asMatrix()
    else:
        raise ValueError("[Error] 不明な指定空間名です: [{}]".format(space))

    return nodeMatrix


def getspace(space):
    """
    maya.api.OpenMaya.MSpaceの「world」と「object」Enumを返す。
    """
    if space == "world":
        m_space = om2.MSpace.kWorld
    elif space == "object":
        m_space = om2.MSpace.kTransform
    else:
        raise RuntimeError("please object space of matrix.")

    return m_space


def eulerToDegress(eulerRot):
    return [
        math.degrees(angle) for angle in (eulerRot.x, eulerRot.y, eulerRot.z)
    ]


def matrixToQuatMirror(matrix):
    mftMatrix = om2.MTransformationMatrix(matrix)
    quatRot = mftMatrix.rotation(asQuaternion=True)
    quatRot.normalizeIt()

    quatRot = _OM2Q(-quatRot.x, quatRot.y, quatRot.z, quatRot.w)

    mftMatrix.setRotation(quatRot)

    return mftMatrix.asMatrix()


def decomposeMatrix(matrix, rotOrder=0, space="object"):
    """
    渡されたマトリクスの要素をトランスフォームの値へ分解する。 現在移動回転のみ
    """
    m_space = getspace(space)
    mftMatrix = om2.MTransformationMatrix(matrix)
    # Decomposes a MMatrix.
    trans = mftMatrix.translation(m_space)

    quatRot = mftMatrix.rotation(asQuaternion=True)
    quatRot.normalizeIt()
    eulerRot = quatRot.asEulerRotation()

    eulerRot.reorderIt(rotOrder)
    rot = eulerToDegress(eulerRot)

    return trans, rot


def setTraRot(dagPath, tra, rotate, space="object"):
    """
    ノードの移動・回転を設定する。
    """

    nodeName = dagPath.fullPathName()

    if space == "world":
        cmds.xform(nodeName, ws=True, t=tra, ro=rotate)
    elif space == "object":
        cmds.xform(nodeName, os=True, t=tra, ro=rotate)
    else:
        raise ValueError("[Error] 不明な指定空間名です: [{}]".format(space))


def test_matrix_to_transform():
    sellist = om2.MGlobal.getActiveSelectionList()
    original = sellist.getDagPath(0)
    target = sellist.getDagPath(1)

    originalMatrix = getMatrix(original, "world")
    mirroredMatrix = matrixToQuatMirror(originalMatrix)

    tra, rot = decomposeMatrix(mirroredMatrix, 0, "world")
    tra = [tra[0]*-1, tra[1], tra[2]]

    setTraRot(target, tra, rot, "world")


test_matrix_to_transform()

```

</div></details>

クォータニオンについてあまり詳しく調べたことがなかったのですが、
クォータニオンはこれがそのまま回転行列として扱えるようなので、
回転行列のYZのベクトルにそって反転回転させることと同義になるようです

これでYupのトランスフォームは概ねうまくいくのですが、
後述する壁にぶちあたり再びマトリクスの回転行列をどうにかしようともがいていた矢先
cymelに出会います

変換などの関数が豊富なcymelのミラーを使うことに決めたきっかけになりました


## 壁にぶちあたる

cymelを使い、「いえー！ミラーなんて楽勝だぜー」と思っていた矢先強敵があらわれます
Xupで入っている骨の軸通りにむいたトランスフォームの存在です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637095/a4842952-5c87-e5aa-252b-7f529a50eb50.png)

__どうしたというのでしょうか__
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637095/b679b7be-0321-bcb5-0802-26a23bd77b25.png)

理由は簡単でオブジェクト的にはXupを向いているつもりでも基本ワールド空間はYupであることは
覆らないのでY方向が横に向いちゃってる状態でも正常にYupでの姿勢のミラーが発生しているのでした

計算上は正しい・・・となると

## 発想の転換
軸向きをYup軸に入れ替えたマトリクスに一度変換してしまおう
![mirror_matrix_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637095/0867b908-d288-b32a-814d-792e3d67dad3.png)


<details><summary>長いので色々省略していますがコード</summary><div>

```python: switching_matrix.py
import maya.api.OpenMaya as om2
import cymel.main as cm

_OM2M = om2.MMatrix

class MirrorPlane(Enum):
    """ミラー平面の指定用列挙型"""

    XY = auto()
    YZ = auto()
    XZ = auto()


class MirrorAxis(IntEnum):
    """ミラーの指定軸Index"""

    X = 0
    Y = 1
    Z = 2

class UpType(Enum):
    Xup = auto()
    Yup = auto()
    Zup = auto()

class CustomMatrix:
    """4x4の行列から回転行列部分を入れ替えるだけのクラス"""

    __valueError = ValueError(
        "sequence of 16 float values or four tuples of four float values each."
    )
    __sequence: list
    __xVec: list
    __yVec: list
    __zVec: list
    __tVec: list

    def __init__(self, sequence) -> None:
        """
        Args:
            sequence (list):
                マトリクスを表す16要素の配列または
                ベクトル要素ごとに区切った入れる
                または MMatrix
        """

        if isinstance(sequence, list):
            if len(sequence) != 16 and len(sequence) != 4:
                raise self.__valueError

            # ４つのタプルが来る場合は中身を取り出していく
            if len(seaquence) == 4:
                tupleSequence: list = []
                for row in sequence:
                    if len(row) != 4:
                        raise self.__valueError
                    for column in row:
                        tupleSequence.append(column)
                self.__sequence = tupleSequence
            # 16要素の配列の場合はそのままいれる
            else:
                self.__sequence = sequence
        # MMatrixの場合は配列として扱う
        elif isinstance(sequence, _OM2M):
            self.__sequence = sequence
        else:
            raise self.__valueError

        self.__xVec = [
            self.__sequence[0],
            self.__sequence[1],
            self.__sequence[2],
            self.__sequence[3],
        ]
        self.__yVec = [
            self.__sequence[4],
            self.__sequence[5],
            self.__sequence[6],
            self.__sequence[7],
        ]
        self.__zVec = [
            self.__sequence[8],
            self.__sequence[9],
            self.__sequence[10],
            self.__sequence[11],
        ]
        self.__tVec = [
            self.__sequence[12],
            self.__sequence[13],
            self.__sequence[14],
            self.__sequence[15],
        ]

    @property
    def XVec(self) -> list:
        return self.__xVec

    @property
    def YVec(self) -> list:
        return self.__yVec

    @property
    def ZVec(self) -> list:
        return self.__zVec

    @property
    def TVec(self) -> list:
        return self.__tVec

    @property
    def Sequence(self) -> list:
        """
        MMatrixをそのままいれてしまうので配列化
        """
        return [
            self.__sequence[0],
            self.__sequence[1],
            self.__sequence[2],
            self.__sequence[3],
            self.__sequence[4],
            self.__sequence[5],
            self.__sequence[6],
            self.__sequence[7],
            self.__sequence[8],
            self.__sequence[9],
            self.__sequence[10],
            self.__sequence[11],
            self.__sequence[12],
            self.__sequence[13],
            self.__sequence[14],
            self.__sequence[15],
        ]

    def __setSeaquence(self):
        sequence = []
        for row in self.get4x4():
            for column in row:
                sequence.append(column)

        self.__sequence = sequence

    def get4x4(self):
        """入れ子の4x4の配列を返す"""
        return [self.__xVec, self.__yVec, self.__zVec, self.__tVec]

    def composeXup2Yup(self):
        self.swapXtoY()
        self.swapXtoZ()

    def composeYup2Xup(self):
        self.swapXtoZ()
        self.swapXtoY()

    def composeZup2Yup(self):
        self.swapZtoY()
        self.swapXtoY()

    def composeYup2Zup(self):
        self.swapXtoY()
        self.swapZtoY()

    def swapXtoY(self):
        oldX = self.__xVec
        oldY = self.__yVec

        self.__xVec = oldY
        self.__yVec = oldX

        self.__setSeaquence()

    def swapZtoY(self):
        oldZ = self.__zVec
        oldY = self.__yVec

        self.__zVec = oldY
        self.__yVec = oldZ

        self.__setSeaquence()

    def swapXtoZ(self):
        oldX = self.__xVec
        oldZ = self.__zVec

        self.__xVec = oldZ
        self.__zVec = oldX

        self.__setSeaquence()

    def asMatrix(self):
        """MMatrixに変換"""
        return _OM2M(self.__sequence)

def mirrorMatrix(
    original,
    target,
    mirrorPlane: MirrorPlane = MirrorPlane.YZ,
    withTrans: bool = True
):

    # ~ 省略 ~

    # ユーザーにXup指定をさせる（現在の姿勢の軸向きなどからXupであることを判断するのは難しい）
    upAxisType: UpType = UpType.Xup
    isNotYup = upAxisType.value != UpType.Yup.value
    
    if isNotYup:
        switchMatrix = CustomMatrix(originalMatrix) 
        # 一度Yupマトリクスに変換する
        if upAxisType.value == UpType.Xup.value:
            switchMatrix.composeXup2Yup() # 各軸のVector値を入れ替える
        else:
            switchMatrix.composeZup2Yup()
    
        cyMat = cm.Matrix(switchMatrix.asMatrix())
    
    print("up Axis => {}".format(upAxisType.name))
    
    mirrored_Matrix = cyMat.mirror(mirrorAxis, t=withTrans)

```

</div></details>

よし、成功・・・・・しない！！？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637095/3e79226a-d20b-6c2d-ccea-e5bef80f9a11.png)



## 逆転される対象に着目する
今回ミラーしようとしていたトランスフォームは主軸Xで側面軸Yでした
かつよくみるとZベクトルが反対を向いています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637095/a4842952-5c87-e5aa-252b-7f529a50eb50.png)

これ自体はXとYを正方向の設定で骨の方向づけをすれば、
標準的な手順では正しい状態といえます
![スクリーンショット 2023-12-07 124359.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637095/1f452ce4-c720-d415-a59d-0a80999330bf.png)

Mayaは右手座標系のソフトウェアなので、
XYのベクトル方向からZベクトルを割り出すとここまでは自然です

更にまずいことにXが上をむいた状態で成立するYupに変換すると、
本来横軸であるはずのYが正面になってしまいます（それ以外だとこの形は成立しない）
![mirror_matrix_05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637095/a1d352d6-185b-22fe-35cf-01bd0dadd8a1.png)

この結果反転先でZ軸が逆転します

軸入れ替えの際にここをカバーしてもいいのですが、
複雑になりそうなので今回は別の対応を考えます

## ミラーのnegAxisにZを指定する
ということで、cymelのmatrix.mirror関数のお世話になります

cymelのmirror関数には本当に使う側にとってありがたい、
ミラーしたあとのマトリクスに対して1軸を逆転させるオプションの引数がありますのでそこへ
Zベクトルを指定して実行するように対応してみます
```python: neg_z_axis.py
# ~ 省略 ~

def mirrorMatrix(
    original,
    target,
    mirrorPlane: MirrorPlane = MirrorPlane.YZ,
    withTrans: bool = True
):

    def _checkXupMirrorType(upAxisType: UpType, mirrorAxis: MirrorAxis):
        """Xupの場合のミラー平面の判定"""
    
        if upAxisType.value == UpType.Xup.value:  # 対象がXupである
            # YZ平面が指定されている、またはXZ平面が指定されている
            return (
                mirrorAxis.value == MirrorAxis.X.value
                or mirrorAxis.value == MirrorAxis.Y.value
            )
    
        return False

    # ~ 省略 ~

    if _checkXupMirrorType(upAxisType, mirrorAxis):
        # XupでYZまたはXZ平面指定の場合はミラー後の結果からZVectorを逆転する
        mirrored_Matrix = cyMat.mirror(
            mirrorAxis, negAxis=MirrorAxis.Z, t=withTrans
        )
    else:
        mirrored_Matrix = cyMat.mirror(mirrorAxis, t=withTrans)
```

いろいろ試したんですが、この問題はYZ,XZの平面のみで発生しそうです

## 試す
__やりました__
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637095/1e60e6e7-1d9a-5184-c062-17055ff83494.png)

成功です。
本当にありがとうございます、cymel様にはもう足をむけて寝れません


# 親空間となるトランスフォームを指定して、その親空間でのミラーポーズを得る
理想はこの状態になることです
![mirror_matrix_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637095/7ade2beb-2675-7e64-0f76-419e4eaf55b3.png)

前提条件として、
__左右のトランスフォームは中央の空間親とトランスフォーム階層的な親子関係はないものを想定します__

例：
ルートが90度横に向いているキャラクターのリグの左右の手のIKコントローラーを
ルートの向きを基準にミラーする場合など


## 計算式を導き出す
手続きのイメージ
1. 対象のワールドマトリクスを指定空間にいれた想定のローカルマトリクスを得る
2. YZ平面にミラーする
3. 指定空間に戻す

資料によると、マトリクスの親子関係付けは親の行列をかける
```math
Mworld = Mlocal * MparentW
```

親からワールドに出すには逆行列をかける
```math
Mworld = Mlocal * MparentW.inverse()
```
のようです

しかしここでまた壁にぶち当たります。

## ローカルマトリクスの得かたがわからない
単純に親子付け計算をした状態のマトリクスは「トランスフォームの親子付けと違いワールド空間にいる状態」
であることに変わりはありません

そうすると指定している親空間のXベクトル方向がワールド空間のXベクトル方向と
違う姿勢を向いている場合は移動ですら別の座標にいってしまいます

ミラーした姿勢が本来得たかった姿勢ではなくなります

親空間においても現在の姿勢を維持した相対的な姿勢を取得したいので
親にいれてからワールド空間にまた取り出すのかなと計算式を眺めると

```math
Mworld = Mlocal * Mparent * Mparent.inverse()
```

見ての通り元の姿勢に戻るだけです

でも、とりあえずものはためしだということで一旦そのまま実装
```python: in_space_mirror.py
import maya.api.OpenMaya as om2
import cymel.main as cm

_OM2M = om2.MMatrix

# ミラー対象の元の姿勢
originalMatrix: _OM2M = mainNode.getMatrix(space="world")

if spaceObject:
    parentMatrix: _OM2M = spaceObject.getMatrix(space="world")
    # 親空間に入れる（？）
    originalMatrix *= parentMatrix * parentMatrix.inverse()

# さきほどのワールド空間のミラーを少し改造
mirroredMatrix: cm.Matrix = mirrorMatrix(
    originalMatrix=originalMatrix,
    mirrorPlane=mirrorPlane,
    upAxisType=upAxisType,
    withTrans=withTrans,
)

if spaceObject:
    mirroredMatrix = _OM2M(mirroredMatrix)
    # 親空間に戻す
    mirroredMatrix *= parentMatrix
    # 移動回転を取り出すために再度cymelMatrixへ
    mirroredMatrix = cm.Matrix(mirroredMatrix)

# cymel.Matrixにはオイラーの角度を返す関数がある
target.setRotation(mirroredMatrix.asDegrees(), "world")

# Translate
if withTrans:
    # 同じく移動値値を返す関数がある
    target.setTranslation(mirroredMatrix.asTranslation(), "world")
```

結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637095/1c853c75-cf07-a8cd-e4c2-3872c46a4ec5.png)

予想どおり向きも位置関係もおかしな場所にいます

ただ、親空間が原点で回転[0,0,0]の姿勢であったと仮定すると、
ワールド空間でのミラー姿勢を親子付けした姿勢であることは確かなようです
![mirror_matrix_04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637095/d82c1cac-b9b6-4644-6368-a8ab65c68c32.png)

# 親の姿勢を打ち消す
ということは元のワールド空間の姿勢から親の姿勢が打ち消せれば、
親空間内において元の姿勢を維持したローカルマトリクスが得られることにたどり着きました
純粋に今の状態をローカルマトリクスだと仮定すればよかったのでした

```math
Mrelative = Mworld * Mparent.inverse()
```

さっそく実装します

```python: in_space_mirror.py
import maya.api.OpenMaya as om2
import cymel.main as cm

_OM2M = om2.MMatrix

# ミラー対象の元の姿勢
originalMatrix: _OM2M = mainNode.getMatrix(space="world")

# 親の姿勢を打ち消す
if spaceObject:
    parentMatrix: _OM2M = spaceObject.getMatrix(space="world")
    originalMatrix *= parentMatrix.inverse()

# さきほどのワールド空間のミラーを少し改造
mirroredMatrix: cm.Matrix = mirrorMatrix(
    originalMatrix=originalMatrix,
    mirrorPlane=mirrorPlane,
    upAxisType=upAxisType,
    withTrans=withTrans,
)

if spaceObject:
    mirroredMatrix = _OM2M(mirroredMatrix)
    mirroredMatrix *= parentMatrix
    # 移動回転を取り出すために再度cymelMatrixへ
    mirroredMatrix = cm.Matrix(mirroredMatrix)

target.setRotation(mirroredMatrix.asDegrees(), "world")

# Translate
if withTrans:
    target.setTranslation(mirroredMatrix.asTranslation(), "world")
```

__やりました__
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637095/f03bd9bb-ef19-6d54-db01-09d95b9d1423.png)

もういまならはだかで逆立ちして町内を一周できそうな気分です

## 発想の問題
マトリクスの親子付けのイメージは空間にいれたり出したりと、
トランスフォーム的な階層が変わるイメージが非常につよかったですが、
マトリクスを取り扱うときのイメージがかわりました

1.常にワールド空間にいることを想定する
2.姿勢の掛け合わせをおこなうイメージを想定する

現在ではこのように考えることにしています

また今回のワールド空間の姿勢を維持したまま相対的なローカルマトリクスを得る計算は、
トランスフォームの姿勢計算以外にもシェーダーなどでも使えるのではないかと思いました

ただいまのところ、いつ使うのかというのは正直不明です・・・・

いいアイデアを思いついた方がいらっしゃれば是非ご活用ください

# まとめ
>1,  シンプルなワールド空間での指定平面ミラーポーズを得る

こちらはMayaでミラーツール実装を行う場合、cymelのMatrix.mirror関数のご利用をおすすめします
あらゆる状況のミラーが想定されており
up軸と移動オプション指定のためのGUIをつくるくらいですぐにデザイナーに提供可能です

>2, 親空間となるトランスフォームを指定して、その親空間でのミラーポーズを得る

こちらは対象のワールドマトリクスに指定した親の逆ワールドマトリクスをかけてミラー
最後に親のワールドマトリクスをかける

これだけで親を基準としたミラーリングが実行できました

かなりピンポイントな話でしたが、
なにかのアイデアの参考になれば幸いです


# 参考文献
感謝感激恐悦至極

こちらはコード内にて利用させていただいています
[ryusas様](https://qiita.com/ryusas "ryusas")の[CyMel](https://qiita.com/ryusas/items/eba6ab1b5af0700799e6 "cymelの紹介")

参考サイト・記事等
[COYOTE流テクニカルTIPS TIPS 08：【Maya】Matrixを使ってみよう](https://cgworld.jp/regular/202108-coyote-ta08.html "COYOTE流テクニカルTIPS TIPS 08：【Maya】Matrixを使ってみよう")

[DF TALK - わかった気になるマトリックス　第４回](https://dftalk.jp/?p=13489 "わかった気になるマトリックス　第４回")

[Maya - Transform node](https://help.autodesk.com/cloudhelp/2023/ENU/Maya-Tech-Docs/Nodes/transform.html "Nodes Transform - Maya Documents")

[回転行列、クォータニオン(四元数)、オイラー角の相互変換](https://qiita.com/aa_debdeb/items/3d02e28fb9ebfa357eaf "回転行列、クォータニオン(四元数)、オイラー角の相互変換")

[Quaternionによる3次元の回転変換](https://qiita.com/kenjihiranabe/items/945232fbde58fab45681 "Quaternionによる3次元の回転変換")