UsdGeomXform だいたい理解した

https://qiita.com/advent-calendar/2021/usd

息切れ気味ですが、最後の記事です（主催者、よくあんなに書けるな...）


# UsdGeomXformable

UsdGeomXform はいわゆる Transform ノードで、オブジェクト単位の移動、回転、スケールなどを表現する UsdGeomXformable を実装するスキーマの一つです。

まず UsdGeomXform と UsdGeomXformable の違いが気になるでしょう。スキーマの継承図を見てみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64544/250d2d05-890b-f764-2593-2edc11d31b1b.png)

このようになっていて、実質 UsdGeom のほとんどのスキーマクラスは UsdGeomImageable と UsdGeomXformable を継承しています。これはつまり、Mesh や Cube、PointInstancer などもトランスフォームが可能ということです。

たとえば Maya では Transform と Mesh や Camera などの shape が別ノードとして定義されており、Mesh ノードそのものがトランスフォームを持つことはありません。usd では Maya に限らず様々な表現形式の DCC ツールでもインターチェンジを可能にするため、Mesh や Camera など自身も Xformable として作られています。もちろん USD でも運用上 Mesh などにはトランスフォームをいれずに、親の Xform を使うことにする、ということは可能です。

# トランスフォーム操作

一口でオブジェクト transform といっても、ツールによって非常に解釈に幅があります。3DCG では一般的に回転・スケールなどと平行移動とを 4x4 行列の積で表しますが、行列の積は可換でないので、変換を適用する順番によって結果が変わります。

実際にはほとんどのツールは Scale/Rotate/Translate の順で適用してくれるのですが、Rotate は X/Y/Z の順であったり、Y/X/Z（Yaw-Pitch-Roll）であったりするし、ジョイントアニメーションなどでは joint pivot や orient といった表現で一時的に平行移動・回転をした後に回転操作を行い再度平行移動する、といった一連の操作を表現しているケースもあります。よく利用されるこれらの座標変換の表現方法には以下のものがあります

|XformOp||
|--|--|
|translate|平行移動|
|scale|拡大縮小|
|rotate (X,Y,Z)|X,Y,Z 軸での回転、その組み合わせ|
|orient |任意軸での回転|
|transform| 4x4 行列|


レンダラなどは最終的に座標が決まりさえすればよいので、これらの変換を合成して 4x4 行列にして保存しておけばよいのですが、4x4 行列からもとの translate や rotate などの要素に分解するのはそれほど自明な操作でもなく、またアニメーションデータとしてキーフレームが translate や rotate の要素ごとに別々に設定され補間されるようなケースではうまくいきません。そこで UsdGeomXform では、これら複数の変換操作と、その変換操作の適用順をまるごと保持することにより、柔軟性の高いトランスフォーム表現を実現しています。

# XformOp

UsdGeomXform にはこれらを実現するために上の表にあるようなトークンを用いて、

```
float3 xformOp:rotateXYZ = (30, 60, 90)
float3 xformOp:scale = (2, 2, 2)
double3 xformOp:translate = (0, 100, 0)
uniform token[] xformOpOrder = [ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale" ]
```

などと表現します。xformOpOrder は、この順番に（頂点座標から見ると右から）transform 演算が適用されることになります。回転はラジアンではなく度(degree) で表記されます。

translate が複数ある場合などは : をつけて区別できます。

```
double3 xformOp:translate:first = (0, 100, 0)
float3 xformOp:rotateXYZ = (30, 60, 90)
double3 xformOp:translate:second = (0, 0, 100)
uniform token[] xformOpOrder = [ "xformOp:translate:second", "xformOp:rotateXYZ", "xformOp:translate:first" ]
```

それぞれ別々にアニメーション（time-sample）が設定できるので、情報の欠損もなく大変便利です。
また同じ変換について逆演算を行う invert オペレーションを !invert! をつけることで表記できます

```
uniform token[] xformOpOrder = [ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:translate:scalePivot", "xformOp:scale", "!invert!xformOp:translate:scalePivot" ]
```

!invert! がついた場所は、その変換の逆行列が適用されます。

# Maya/FBX transform のエンコード例

Maya/FBX では、Xform は以下のように表現されます（たぶん。shear など一部省略）

```math
X_{world} = X_{parent} * T * R_{offset} * R_{pivot} * R * R_o * {R_{pivot}}^{-1} * S_{offset} * S_{pivot} * S * {S_{pivot}}^{-1}
```

|||
|--|--|
|T|Translation|
|Roffset|Rotation offset|
|Rpivot|Rotation pivot|
|R|Rotation|
|Ro|Rotation orientation|
|Rpivot -1|Inverse of the rotation pivot|
|Soffset|Scaling offset|
|Spivot|Scaling pivot|
|S|Scaling|
|Spivot -1|Inverse of the scaling pivot|

これを UsdGeomXform で表現するとき、逆行列の箇所は !invert! で表現できますので、仮に rotate が XYZ だとすると

```
uniform token[] xformOpOrder = [
  "xformOp:translate",
  "xformOp:translate:rotateOffset",
  "xformOp:translate:rotatePivot",
  "xformOp:rotateXYZ",
  "xformOp:rotateXYZ:orient",
  "!invert!xformOp:translate:rotatePivot",
  "xformOp:translate:scaleOffset",
  "xformOp:translate:scalePivot",
  "xformOp:scale",
  "!invert!xformOp:translate:scalePivot"
]
```

このように書けます。

# UsdGeomXformCache 
最終的には 4x4 行列が一つ欲しいわけですので、これは UsdGeomXform::GetLocalTransformation を使って取得できます。しかし複雑なシーングラフの深い階層構造においてすべてのワールド変換行列を求めるのはかなりのコストになります。
UsdGeomXformCache を使えば一度計算した結果をキャッシュしてくれて、効率よくワールド変換行列を求めることができます。


# おわりに
かつて一緒に仕事していた超ベテランCGエンジニア（自分も30年以上のキャリアですが、はるかに上がいる...しかもまだ現役で）も

> I am a “shuffle the matrix order till it looks right” kind of guy 

と言っていましたが、実務においては **変換行列の掛け算は、結果が合うまで入れ替え続ける** のが基本（だよね？）

とはいえ 3D の座標変換を自分でコーディングしたりシリアライズしたことがある人はみな、Scale/Rotate/Translate の順番はほとんど一択なのでいいとしても、Rotate の種類、ピボットの扱いなどで苦労し、もういいや 4x4 行列で！と開き直ると今度は補間で苦労したり、結局 decompose するがフリップしたり、と悩み続けているかと思います。
USD は少なくともフォーマット的にはオーサリング状態をそのまま記述することができるようになっており、チャンネルごとのアニメーションデータもロスレスに記述できます（とはいえインターチェンジの際には各ツールの制約が出てくるわけですが）。とくにピボットの扱いはツールの際が出やすいところなので、これは本当に助かります。