SolarisでPrimをカメラの子にせずカメラに追従させる

# 0. 概要
　この記事は [Universal Scene Description Advent Calendar 2024](https://qiita.com/advent-calendar/2024/usd) の20日目の記事です。
　USD も Houdini もまだまだ初心者なので間違ってるところがあったら気軽に教えてください...

### 0-1. 動作環境

OS: RedHat Enterprise Linux 9.4
Houdini: 20.5.410 Py3.11 (最新の Production Build) Education Edition

### 0-2. 免責

　この記事をもとに被った不利益について私は一切責任を負いません．
 
# 1. モチベーション
　カラーチャートなど Prim をカメラについてこさせたいことがあったのですが、cameras の Prim の下にカメラ以外がいるのが気持ち的に嫌だったのでこの仕組みを作りました。

# 2. 実装
### 2-1. ゴール
　今回作るものです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565633/ff9f374e-6c54-ee16-2619-14b5ef49639b.png)
　Target Prim(今回ならチャート)をReference Prim(今回ならカメラ)の子にせずにトランスフォーム情報を継承します。
　これによってカメラについて越させることができます。
　Target Prim のトランスフォームがカメラからのローカルになります。
### 2-2. 戦略
　cameras の下にいるのが嫌なだけなので、カメラのトランスフォーム情報を持った xform Prim を任意のパスに作成し、Target Prim をその子にします。
　やることは、
  1. カメラのトランスフォーム情報を取得する。
  2. 指定したパスに Xform Prim を作る。
  3. Xform Prim にトランスフォーム情報を流す。
  4. Target Prim を Xform Prim に子にする。 

で 1-3. に関しては Python でやります。
4. は Restructure Scene Graph LOP でやります。

### 2-3. カメラからトランスフォーム情報を取得
　このPython Script LOP には ```refprim```(つまりカメラのパス) と ```destpath```(つまり Xform Primのパス) という String 型のパラメータを用意します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565633/22709972-6ea6-edd1-dcda-a11cc1eb76f0.png)
　Python で refprim のトランスフォーム情報を取得します。
```python
# destpath のパスの健全性をチェックする関数
# OKかだめかのboolとだめな内容をタプルで返す
# 例外処理がイケてないのは今回は許して...
def is_valid_usd_path(dest_path: str) -> Tuple[bool, str]:
    if not dest_path:
        return False, "Error: The USD path is empty."

    try:
        sdf_path: Sdf.Path= Sdf.Path(dest_path)
        
        if not sdf_path.IsAbsolutePath():
            return False, "Error: The USD path should be an absolute path."
            
        if not sdf_path.IsPrimPath():
            return False, "Error: The USD path should be a prim path."
            
    except Exception as e:
        return False, f"Error: Invalid USD path format. {str(e)}"

    return True, "The USD path is valid."

# ステージの定義
node: LopNode = hou.pwd()
stage: Usd.Stage = node.editableStage()

# refprim パラメータを評価してカメラのパスを文字列で受け取る
ref_prim_path: str = node.evalParm("refprim")
# パスの文字列から Prim オブジェクトを取得する
ref_prim: Usd.Prim = stage.GetPrimAtPath(ref_prim_path)

# destprim パラメータを評価して Xform のパスを文字列で受け取る
dest_path: str = node.evalParm("destpath")

# Xform のパスの健全性をチェック
is_valid, message = is_valid_usd_path(dest_path)

if not is_valid:
    raise ValueError(message)

if not ref_prim:
    raise ValueError(f"Connot find Reference Prim: {ref_prim_path}")

# カメラから XformOp を取得する
xform_attrib: Usd.Attribute = ref_prim.GetAttribute("xformOp:transform")
if not xform_attrib:
    raise ValueError(f"Cannot fetch xform attribute from {ref_prim_path}")

# XformOp からトランスフォーム情報を取得
transform: Gf.Matrix4d = xform_attrib.Get()
```
### 2-4. Xform Prim をつくる
　Xform Primを指定したパスに作ります。
```python
# destprim パラメータのパスに Xform Prim を作る
xform: UsdGeom.Xform = UsdGeom.Xform(dest_prim)
```
### 2-5. Xform Prim にトランスフォーム情報をセットする
　あまりよくわかりませんが、Prim のトランスフォームは XformOp オブジェクトとして表現されるトランスフォームオペレーションをスタックして行われる？みたいなので、Xform Prim に XformOp を追加して XformOp に トランスフォーム情報を取り付けます。
　Prim 固有のトランスフォーム情報を変更せずに XformOp を積むことでプロシージャル性を担保する設計？
```python
# Xform Prim に XformOp を追加する
xform_op: UsdGeom.XformOp = xform.AddXformOp(UsdGeom.XformOp.TypeTransform, UsdGeom.XformOp.PrecisionDouble)
# UsdGeom.Xform.AddXformOp(opType: UsdGeom.XformOp.Type,
#                          precision: UsdGeom.XformOp.Precision = UsdGeom.XformOp.PrecisionDouble,
#                          opSuffix: TfToken = TfToken(),
#                          isInverseOp: bool = false
#                          ) -> UsdGeom.XformOp
# opTypeに指定できるのは 'TypeInvalid', 'TypeOrient', 'TypeRotateX', 'TypeRotateXYZ', 'TypeRotateXZY', 'TypeRotateY', 'TypeRotateYXZ', 'TypeRotateYZX', 'TypeRotateZ', 'TypeRotateZXY', 'TypeRotateZYX', 'TypeScale', 'TypeTransform', 'TypeTranslate' の14種類?
# opSuffix は TfToken って書いてますが C++ の方なので Python なら普通に str だと思います
# precision は 'PrecisionDouble', 'PrecisionFloat', 'PrecisionHalf' の3種類?

# XformOp にトランスフォーム情報を取り付ける
xform_op.Set(transform)
```
　例えば下の例だと Sphere に対して transform ノードで２回 transform を掛けていますが、その分だけ xformop が積まれていってることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565633/d8d49b7e-8fc6-86bd-d572-b4d9bbde8b1a.png)

　以下がスクリプトの全文です。
```python
import re
# annotation のためなので別にいらない
from typing import Tuple

from hou import LopNode
from pxr import Usd, Gf, Sdf, UsdGeom

# Validate Destination Path
def is_valid_usd_path(dest_path: str) -> Tuple[bool, str]:
    if not dest_path:
        return False, "Error: The USD path is empty."

    try:
        sdf_path: Sdf.Path= Sdf.Path(dest_path)
        
        if not sdf_path.IsAbsolutePath():
            return False, "Error: The USD path should be an absolute path."
            
        if not sdf_path.IsPrimPath():
            return False, "Error: The USD path should be a prim path."
            
    except Exception as e:
        return False, f"Error: Invalid USD path format. {str(e)}"

    return True, "The USD path is valid."

# Define Stage and Prim
node: LopNode = hou.pwd()
stage: Usd.Stage = node.editableStage()

ref_prim_path: str = node.evalParm("refprim")
ref_prim: Usd.Prim = stage.GetPrimAtPath(ref_prim_path)

dest_path: str = node.evalParm("destpath")

# Validate
is_valid, message = is_valid_usd_path(dest_path)

if not is_valid:
    raise ValueError(message)

if not ref_prim:
    raise ValueError(f"Connot find Reference Prim: {ref_prim_path}")

#Fetch XformOp
xform_attrib: Usd.Attribute = ref_prim.GetAttribute("xformOp:transform")
if not xform_attrib:
    raise ValueError(f"Cannot fetch xform attribute from {ref_prim_path}")


#Fetch Transform
transform: Gf.Matrix4d = xform_attrib.Get()
dest_prim: Usd.Prim = stage.DefinePrim(dest_path, "Xform")

xform: UsdGeom.Xform = UsdGeom.Xform(dest_prim)
xform_op: UsdGeom.XformOp = xform.AddXformOp(UsdGeom.XformOp.TypeTransform, UsdGeom.XformOp.PrecisionDouble)
xform_op.Set(transform)

```
### 2-6. Target Prim を Xform Prim の子にする

　```Restructure Scene Graph LOP``` の Reparent Primitives でリペアレントします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565633/a5bf33d1-926f-3c12-0b95-d732a739bc8c.png)
　このノードを持つ subnet に下のようなパラメータがあって、このノードのパラメータはそこから来てます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565633/01edc78b-5e8d-ea79-1a93-b9c9071b285f.png)

　例えばこのようにパラメータを入れると、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565633/1073ebd0-c938-8d26-1ed5-d0ff03f826c1.png)
　このような階層になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565633/edfcfb0b-dca5-dd52-3d1b-e8832fd59e7a.png)

# 3. おわりに
　実際の業務でこういうことをするかどうかはわかりませんが、色々応用が効く仕組みですし USD と Python の練習になったので個人的には良かったです。
　Solaris で最近やったことを記事にして見ました。参考になれば幸いです。

　最後まで読んでいただきありがとうございました！

 # 4. 参考
[USDPython ドキュメントの読み方](https://fereria.github.io/reincarnation_tech/usd/python/usd_pydocs)
[Universal Scene Description Document](https://openusd.org/docs/api/index.html)