UsdGeomImageable 完全に理解した

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

# UsdGeomImageable

USD の定義済みスキーマ（スキーマの説明は [@fereria さんの記事](https://fereria.github.io/reincarnation_tech/11_Pipeline/01_USD/18_schema/ ) を参照）の中で、おそらく一番重要なものが UsdGeomXform と UsdGeomMesh でしょう。この記事では UsdGeomXform の説明のために、まず UsdGeomImageable を見ていきます。UsdGeom というのは、USD で標準化された、ジオメトリ表現用のスキーマの総称です。

[API ドキュメント](https://graphics.pixar.com/usd/release/api/class_usd_geom_xform.html)の各スキーマクラスの冒頭にある継承図です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64544/0f200e37-633a-8c58-c311-6eee8ba4b2ca.png)


この階層構造を上から下に行くと、徐々に具体的な特性、API が付与されていきます。表にしてみましょう。

|SchemaType|定義される特性、API|代表的なアトリビュート、API|
|---|---|---|
|UsdSchemaBase|スキーマ全ての基底。スキーマの種類、PrimPath など|GetPath(), GetPrim()|
|UsdTyped|実体化可能な IsA スキーマの基底||
|UsdGeomImageable|なんらかの可視化ができる Prim。|visibility, purpose|
|UsdGeomXformable|任意のアフィン変換の組み合わせ|xformOp, xformOpOrder|
|UsdGeomXform|Xformable のみを実装した Prim||

なんだか無駄にいろんな階層があるようにも見えますが、3D シーンを表現する様々な要素を整理整頓した結果、このような分離が一番使い勝手が良いということが分かった、というのが USD の重要な知見の一つです。これは大事なところなので、この記事の最後にもう一度強調させてください。

# visibility

UsdGeomImageable が提供する主要な機能は、ジオメトリが表示されるかどうか、という基本的かつ重要な特性です。アーティストの皆さんは DCC ツールで日常的に表示・非表示を切り替えていると思いますが、たとえばグループ化された親を非表示にするとその子供が全部非表示になる、というのはよくあるツールの挙動だと思います。でもこれ、必ずしも自明な挙動ではないですよね。子供の中で一つだけ何か表示したいものがあるとしたらどうするでしょう？

UsdGeomImagable の visibility は、シーングラフ中の様々な要素（ポリゴン、サブディビジョンサーフェス、点群、カーブ、NURBS などなど）に対して、同一の方法で階層的に表示を制御するアトリビュートです。面白いことにこの visibility は bool ではなく token （文字列）で、次の２種類のどちらかを取ります。

|visibility|意味|
|---|---|
|inherited|親の visibility を適用する|
|invisible|不可視にする|

visible と指定することはできません。この定義によって、UsdGeom の表示非表示のふるまいは一意に決まります。

```
def Xform "A"
{
    token visibility = "inherited"
    def Xform "B"
    {
        token visibility = "invisible"
        def Xform "C"
        {
            token visibility = "inherited"
        }
    }
}
```

このような例だと、A は表示され、B, C は非表示になります。inherited はデフォルトのため、これは

```
def Xform "A"
{
    def Xform "B"
    {
        token visibility = "invisible"
        def Xform "C"
        {
        }
    }
}
```

でも同じです。さてこの usd レイヤーの挙動を python でチェックしてみましょう。

```
>>> from pxr import Usd, UsdGeom
>>> stage = Usd.Stage.Open("test.usda")
>>> UsdGeom.Xform(stage.GetPrimAtPath("/A")).ComputeVisibility()
'inherited'
>>> UsdGeom.Xform(stage.GetPrimAtPath("/A/B")).ComputeVisibility()
'invisible'
>>> UsdGeom.Xform(stage.GetPrimAtPath("/A/B/C")).ComputeVisibility()
'invisible'
```

ここで UsdGeomImageable のスキーマを利用しました。ComputeVisibility は UsdGeomImageable が提供する API です。inherited は「**親の表示状態に従う**」ですので、このレイヤーで親のない A はそのまま、B は invisible なので非表示、C は inherited なので親である B の非表示を引き継ぐ、という形で A, B, C の表示状態が決まります。厳密には A の状態は確定しませんが、レンダラは通常親の指定がない inherited を表示として扱います（Hydra に接続されている場合は [UsdImagingPrimAdapter::GetVisible](https://github.com/PixarAnimationStudios/USD/blob/release/pxr/usdImaging/usdImaging/primAdapter.cpp#L1210) の実装を参照して下さい）。

ComputeVisibility ではなく、GetVisibilityAttr を使って直接 Prim C の visibility を取得すると、inherited が返ってきます。

```
>>> UsdGeom.Xform(stage.GetPrimAtPath("/A/B/C")).GetVisibilityAttr().Get()
'inherited'
```

GetVisibilityAttr().Get() が返すのはあくまで inherited という値であり、この限りにおいては親の visibility までは見ないよ、ということがわかります。なお USD API において、Get... はそれなりに高速であることが期待できる API であり、Compute... は場合によってはコストがかかる API であることを暗示しています。なお Hydra で使われる UsdImagingPrimAdapter は内部にキャッシュ機構を持ち、ComputeVisibility のコストを隠蔽しています。

ここは USD を理解した上級者が逆に勘違いしやすいポイントですのでご注意ください。DCC ツールにとっての表示・非表示状態は、**USD コンポジションそのものでは確定しません**。もちろん LIVRPS によってこの C の visibility 自体がコンポジションで制御されることはありますが、その結果は依然として invisible か inherited です。実際にレンダラなどがシーングラフとしてこの Prim C をみたときに visible かどうかを決めるのは、USD コンポジションの後で、階層構造を UsdGeomImageable によって計算したとき（ComputeVisibility）なのです。[ComputeVisibility の実装](https://github.com/PixarAnimationStudios/USD/blob/release/pxr/usd/usdGeom/imageable.cpp#L254) を見ると、inherited の場合は Prim の parent に対して ComputeVisibility の結果を返すように実装されています。これは階層の深さの分のコストがかかるため、Compute と命名され、UsdImaging では自前でキャッシュする必要があります。

このように UsdGeom/UsdShade などのスキーマによって計算される値には、visibility 以外にもBoundingBox やマテリアル情報など様々あります。USD コンポジションは Prim によって作られた階層（名前空間）の中で値を合成することと、階層自体を操作することを行いますが、その階層をまたいで値の調停を行うことはしません。コンポジションのコアとスキーマによる値計算を分離しているところもまた、USD の極めて優れた設計です。

といいつつも、ComputeVisibility を自分で使い、かつキャッシュを使い始めたら、次の難題 --- 時間変化やコンポジションの変化によっていつキャッシュを再計算するのか、という問題にあたります。この問題には簡単な解が用意されていませんが、[UsdImaging  のコード](https://github.com/PixarAnimationStudios/USD/blob/release/pxr/usdImaging/usdImaging/resolvedAttributeCache.h) を読み解いていくとヒントが見つかります（結構荒っぽい）。まだこの辺りはコントリビュートする余地が大きいですね。

ちなみに UsdGeomImageable では visibility は "visible" は取らないので、

```
# 不正な例
def Xform "A"
{
    def Xform "B"
    {
        token visibility = "invisible"
        def Xform "C"
        {
            token visibility = "visible"
        }
    }
}
```

このような指定はできません。この例のように単純に visibility を visible/invisible と記述すると、Prim C は表示なのか非表示なのか、ツール次第で解釈に余地を残してしまいます。UsdGeom のスキーマはどれもこのような状況を避けるために極めて注意深く設計されています。またその妥当性は、USD 以前にピクサーが30年以上作ってきた映画と、Finding Dory 以後のすべてのピクサー映画と、USD がオープンソースになって以来、USD を採用した世界中のスタジオのパイプラインとその作品で証明されている、というわけです。

# 表示させたい
最初の方で挙げた問いに答えると、UsdGeomImageable においては親が非表示にされた子供はすべて非表示となり、一部だけを表示状態に強制することはできない、ということになります。ではどうするか。UsdGeomImageable には MakeVisible という API があります。これを呼んでみます。

```
>>> UsdGeom.Xform(stage.GetPrimAtPath("/A/B/C")).MakeVisible()
>>> UsdGeom.Xform(stage.GetPrimAtPath("/A/B/C")).ComputeVisibility()
'inherited'
>>> UsdGeom.Xform(stage.GetPrimAtPath("/A/B")).GetVisibilityAttr().Get()
'inherited'
>>> UsdGeom.Xform(stage.GetPrimAtPath("/A/B/C")).GetVisibilityAttr().Get()
'inherited'
```

なんとすごいことに Prim C を表示にしたら、Prim B の visibility が invisible -> inherited に変化しました。この実装を見ていきます。[UsdGeomImageable::MakeVisible](https://github.com/PixarAnimationStudios/USD/blob/release/pxr/usd/usdGeom/imageable.cpp#L387)  を見ると、MakeVisible された親を見て、その親が invisible であれば inherited に変更し、自分以外の子供をすべて invisible にする、というなかなかダイナミックな実装が書かれています。スキーマオブジェクト（ここでは UsdGeomImageable）はこのような自分の Prim 以外に対するシーン操作も実行できる存在なのでした。


# おわりに
UsdGeomImageable 一つとっても、その世界観や動機を理解するとスキーマの意味するところもだいぶ見通しが良くなると思います。コンポジションに匹敵する USD の最大の魅力は、これらの厳選され標準化されたスキーマにあります。