6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

USD/Hydra の最新プロシージャルインタフェース

Last updated at Posted at 2022-12-23

はじめに

これは USD アドベントカレンダー 2022 の記事です。もう 2022 も終わりですね。いかがお過ごしでしょうか。コロナも一進一退ですが CG 業界では今年一年で USD もすっかり浸透しきったようで今年の漢字候補にも選ばれ、日米では中学校のカリキュラムにも入るようですし、M1 のネタになったり、公園を散歩していてもそこかしこで Autodesk Aurora はもうビルドしたかとか Asset Previews のプロポーザルに一言コメントしたいとか、TBB を切り替えたらビルドできないんだけどどうしよう、とかそんな話題で子供から老人まで賑やかに議論している様子が聞こえてきます。

さて日々 USD の普及に努力する当カレンダー主催者へのリスペクトを込めて、今年も一つくらいアドベントカレンダー向けに記事を書いておこうと思いました。どんなテーマが良いか考えていたのですが、せっかくなので最新の話を実装まで含めてやってみようかと思います。いま Hydra 実装界隈では一番ホットで、かつまだほとんど誰も触っていないんじゃないかという気もする UsdProcGenerativeProcedural について、実際にプラグインを作って動かすところまで解説してみることにしましょう。何しろまだ開発途中なので直近の需要は全く見えませんし、おそらく2024年くらいに誰かがググってここにたどり着いて、かつその時には API も割と変わっててあまり役に立たない、そんな記事になるかもです。悪しからず。

UsdProcGenerativeProcedural とは何か

現在 Pixar で Hydra 2.0 の開発を率いている Steve LaVietes による解説(https://wiki.aswf.io/display/WGUSD/Presentations にビデオがあります)に沿って見ていきましょう。Katana を使ったことがある方は、Katana のプロシージャルオーサリングを念頭におくと理解がしやすいと思います。

GenerativeProcedural は、シーングラフを入力として、なんらかの生成を行ってシーンの部分階層を出力するコードのこと、と定義されます。この入力は、プロシージャル生成を定義する Prim のプロパティで表されたり、シーングラフ中の別の Prim の情報だったりします。

なぜこれが必要なのでしょうか。USD はスケーラブルに設計されて、膨大なアセットを効率的に表現できるデータ構造だ、とは繰り返し強調されてきました。しかしそれでも、複雑なジオメトリが毎フレームアニメーションするようなデータセットを全て USD フォーマットにシリアライズ(映像制作ではキャッシュする、とも言われます)するのは重すぎる、というケースは出てきます。例えば髪やファー、植生などはそれにあたります。それ以外にも各所の遅延評価、つまり例えばレンダリングのためにあらかじめフルセットのデータは必要ないが、いざ必要になったところでは欲しい、というようなケースもあります。植生の例では「メリダとおそろしの森」で使われた Wonder Moss などが有名です。

このようにオンザフライに大量のデータ(主にジオメトリ)を生成するプロシージャル機能をデータ増幅(Data Amplification)と呼びますが、これは実際にいつ生成・評価するのが良いでしょう。一度作ってしまうと、そのデータ量ゆえにそこから後の処理にはオーバーヘッドとなります。入力となるシーン記述に対して、それを実際に消費する側になるダウンストリームのどの時点でも使えるようにしておけるととても便利です。レンダラー内で生成されるプロシージャルデータは過去もいろんな形で使われてきました。広義には曲面の細分割などもこの範疇に当てはまるでしょう。インタラクティブなゲームエンジン内でも同様に考えられます。Hydra がある今では、RenderDelegate に渡る途中で生成することも出来そうです。レンダラーが個別にプロシージャル生成に対応しなくても、Hydra がそこに対応することですべての Hydra 対応レンダラーがその恩恵を得ることができるわけです。

ところで USD に今までこうしたプロシージャル生成機能が無かったかと言うと、そうでもありません。SdfFileFormat プラグインで実装可能なパラメタライズドペイロード (Dynamic FileFormat とも呼ばれます)は、シーンコンポジションの過程において動的に Sdf の中身を生成することにより同様のことは可能です。ただ、上に挙げたような例に対してはスケーラビリティに限界はあります。USD の Hydra SceneDelegate が利用する UsdImagingPrimAdapter は、特定の UsdPrim に対して Hydra RPrim をいくつも生成することがあり、これもプロシージャル生成と言えます。ただ PointInstancer のような固定機能の実装には良いですが、Prim 間の相互作用を複雑に組み合わせていくのはなかなかに面倒です。また Hydra 2.0 で用意される HdSceneIndex プラグインはより一般的な遅延評価のインタフェースとして使うことができます。

こうした背景から、Hydra 2.0 の仕様にも合わせながら USD シーングラフにて、ダウンストリームにプロシージャルシーン記述を伝えるためのスキーマ、UsdProcGenerativeProcedural が定められ、そのベースラインのイメージング挙動が Hydra レンダラ用に提供されることになりました。もちろんこれはベースラインなので、レンダラ(あらゆるダウンストリームアプリケーション)に応じて様々にカスタマイズできる余地は残されています。

UsdProcGenerativeProcedural スキーマ

このスキーマは USD 22.08 にて追加されました。usdProc/schema.usda を見ての通り、きわめてシンプルなものです。

class GenerativeProcedural "GenerativeProcedural" (
  inherits = </Boundable>
)
{
  token proceduralSystem
}

UsdGeomBoundable を継承していますので、大きさがあります。proceduralSystem はスキーマにはフォールバック値がありませんが、これはプラグイン側の API スキーマにて、例えば "arnoldProcedural" とか "hydraGenerativeProcedural" などが設定されます。

この Prim に与えるパラメータは、全て primvars 名前空間に設定することになっています。

def GenerativeProcedural "myProc"
{
  float primvars:myScaleValue = 1.5
  rel primvars:meshICareAbout = </World/BigMesh>
}

さて実際のプロシージャル挙動はこの prim に APIスキーマをあてて、プラグインで提供することになります。Hydra 用の API スキーマは usdHydra/schema.usda にあります。

class "HydraGenerativeProceduralAPI" (
    inherits = </APISchemaBase>
    customData = {
        string className = "GenerativeProceduralAPI"
        token[] apiSchemaCanOnlyApplyTo = ["GenerativeProcedural"]
    }
){
    token primvars:hdGp:proceduralType
    token proceduralSystem = "hydraGenerativeProcedural"
}

これが UsdProcGenerativeProcedural Prim に適用可能な API スキーマで、ここでは proceduralSystem のフォールバックに "hydraGenerativeProcedural" が与えられ、primvars:hdGp:proceduralType がフォールバック無の token になっています。ここまでの関係性は USD の他の API スキーマと同様なのでそれほど難しくないでしょう。UsdProcGenerativeProcedural は Hydra だけのものではない、ということもお分かり頂けるかと思いますが、ここでは現時点で多くの人に一番興味深い Hydra GenerativeProcedural について、さらに見ていきます。

Hydra 2.0 おさらい

HdGp のプラグインを書くためには HdDataSource, HdSceneIndex についてある程度の知識があった方が良いです。そもそも Hydra とは、USD と一緒にオープンソースされたシーングラフとレンダラのインタフェース(+OpenGL のリファレンス的実装)でした。シーングラフ側の SceneDelegate、レンダラー側の RenderDelegate を Hydra の RenderIndex で取りまとめて、RenderIndex の 中に RPrim という単位で描画用情報を蓄積して更新管理することで様々なアプリケーションの接続インタフェースとして普及しました。この様子を多頭多尾のクリーチャーになぞらえて Hydra と命名されたわけです。しかし普及が進むにつれ、高速プレビュー用に最初に用意された GL レンダラ(HdStorm)向けに設計された SceneDelegate では、Hydra RenderDelegate として提供されるフル機能のレンダラに必要なデータを十分に提供することが出来なくなってきました。これらの議論の上に提案された Hydra 2.0 の新しいビルディングブロックが SceneIndex と DataSource です。

SceneIndex は HdSceneIndexBase を基底とするシーンデータにアクセスするための API で、その重要なエントリーポイントは次の二つです。

virtual HdSceneIndexPrim GetPrim(const SdfPath &primPath) const;
virtual SdfPathVector GetChildPrimPaths(const SdfPath &primPath) const;

GetPrim が返す HdSceneIndexPrim は次のような構造体で、これはシーングラフ中の任意のポイント(primPath)に対して、primType と DataSource を返します。大雑把に言ってデータそのものではなく DataSource を返す、という遅延インタフェースになっているところが今までとの違いになります。これまでは HdStorm 向けに作られた BufferSource というインフライトデータ構造がありましたが、そこからもう一段遅延評価と一般化を進めたものと言えるでしょう。

sceneIndex.h
struct HdSceneIndexPrim
{
    TfToken primType;
    HdContainerDataSourceHandle dataSource;
};

HdContainerDataSource はその名の通り DataSource に名前を付けて束ねたもので、

dataSource.h
class HdContainerDataSource : public HdDataSourceBase
{
public:
    virtual TfTokenVector 	GetNames() = 0;
    virtual HdDataSourceBaseHandle 	Get(const TfToken &name) = 0;
    ...
};

これらを順番に呼び出すことで、シーングラフ中の特定の prim の特定の dataSource を取得することができます。例えばそれが HdSampledDataSource であれば、

dataSource.h
class HdSampledDataSource : public HdDataSourceBase
{
public:
  virtual VtValue GetValue(Time shutterOffset) = 0;
  virtual bool GetContributingSampleTimesForInterval(
        Time startTime, 
        Time endTime,
        std::vector<Time> * outSampleTimes) = 0;
};

これらの API を使ってブラーにも対応した値を得ることができます。

また HdSceneIndex には Observer を追加することで、Prim が追加・削除・編集されたときに通知を受けることができます。RenderDelegate は実質的には Observer として機能します。これまでの Hydra では ハードコードされた DirtyBits と changeTracker によって更新管理が行われてきましたが、この仕組みはより一般化され、HdDataSourceLocator と呼ばれる構造化された情報になりました。

色々書きましたが、初期の Hydra のコンセプトは踏襲しつつ、より様々なレンダラが柔軟にシーングラフにアクセスできるように、データ取得を遅延インタフェースにして更新管理を Locator により一般化した、ということが出来ると思います。ではいよいよ HdGpGenerativeProcedural のプラグインポイントを見ていきましょう。

HdGpGenerativeProcedural

HdGpGenerativeProcedural には3つの重要な API インタフェースがあります。

virtual DependencyMap
UpdateDependencies (const HdSceneIndexBaseRefPtr &inputScene)

virtual ChildPrimTypeMap
Update (const HdSceneIndexBaseRefPtr &inputScene, const ChildPrimTypeMap &previousResult, const DependencyMap &dirtiedDependencies, HdSceneIndexObserver::DirtiedPrimEntries *outputDirtiedPrims)

virtual HdSceneIndexPrim
GetChildPrim (const HdSceneIndexBaseRefPtr &inputScene, const SdfPath &childPrimPath)

HdGp のプラグインは、この3つ(とコンストラクタ)を実装すればよい、ということになります。

UpdateDependencies は SdfPath から DataSourceLocators への依存関係を返します。典型的な用途としては、GenerativeProcedural がシーン中の別の Prim を入力情報として扱うときに、その Prim の DataSourceLocator を結び付けて依存関係を構築するときに使います。逆に言うと単独で処理が完結する GenerativeProceduralPrim では通常不要です。この記事のサンプルでは両方を解説します。

Update は Stevel によれば "main cook" メソッドと説明されていますが、まさに生成プロシージャル処理のメインになるような、データを増幅するコード(=Hydra prim を産むコード)を記述します。ここではシーンと、最後に cook したときからの変更を調べて Hydra prim を生成し、RenderIndex に登録します。

ところで今は Hydra 内部の シーングラフについて話をしているので、USD Prim そのものは指していないことに注意してください。ここは USD をある程度理解した人ほど混乱しやすいポイントなので注意です。UsdStage、Hydra RenderIndex のどちらも SdfPath を使って階層構造を表現していますが、Hydra 自体は USD シーングラフを直接操作するわけではありません。Hydra 本体は USD のことを知りません。UsdImaging が SceneDelegate として、USD のことを Hydra に伝えています。

例としてより複雑なケースでは、複数の USD Stage を単一の Hydra RenderIndex に与えてレンダラーに渡すような場合も考えられます。USD のコンポジションが非常に柔軟で便利なのであまり利用例はみませんが、Pixar の Presto などには初期からその機能があります。その時は同じ階層構造が重複しても個々の Stage を識別できるように、固有の Prefix(UsdImaging では delegateID と呼ばれています)をつけて RenderIndex は一意に作ることになります。Hydra はあらゆるレンダラーのハブになる RenderDelegate が注目されがちですが、このように USD に限らず複数のシーングラフをまとめて受け止められるアーキテクチャも、実に革新的な構造であると言えます。

GetChildPrim は、Update によって生成(Populate)された prim に対して DataSource を返す処理を実装します。USD などのシーングラフが背景に存在する場合は HdSceneSampledDataSource などによって遅延評価を行いますが、プロシージャル生成の場合はここで HdRetainedSampledDataSource によって直接値を計算して DataSource に与えてしまうこともできます。この後のサンプルでは RetainedDataSource を利用します。

HdDataSourceBase 自体には何もインタフェースはありません。Hydra 内部では dynamic_cast によって処理を分岐しています。特定のシーングラフ(USDなど)をバックする SceneIndex 側では Hydra のふるまいを決める適切なベースクラスを選んで、個別に継承して拡張していくことができます。最終的には SceneIndex の GetPrim 実装が返します。UsdImaging の場合は SchemaAdapter を経由して呼び分けられています(UsdImagingStageSceneIndex::_GetImagingSubprimData)。USD ディストリビューションにも実に様々な DataSource が定義されていることがドキュメントのクラス図からもわかります。

image.png

HdGp プラグイン作成~準備~

では実際に Hydra Generative Procedural を使ったプラグイン API スキーマを作成してみます。この記事の執筆時点では USD 22.11 を利用しましたが、そのままでは Windows 環境で正しい DLL を作成することができません。まずは次の5か所に "HD_API" を追加して USD をビルドしなおしてください。ここはいずれリリースブランチでも修正されると思います。

hdGp/generativeProcedural.h
class HdGpGenerativeProcedural
{
public:
    HD_API   // <これを追加
    HdGpGenerativeProcedural(const SdfPath &proceduralPrimPath);
    HD_API   // <これを追加
    virtual ~HdGpGenerativeProcedural();
...
protected:
    HD_API   // <これを追加
    const SdfPath &_GetProceduralPrimPath();
hdGp/generativeProceduralPlugin.h
protected:
    HD_API   // <これを追加
    HdGpGenerativeProceduralPlugin();

    HD_API
    ~HdGpGenerativeProceduralPlugin() override;

dumpbin を使える方は、念のためこれらが usd_hdgp.lib に出ていることを確認すると良いでしょう。これがないとプラグインビルド時に上記関数が未定義になってリンクできません。linux ではパッチの必要はないと思います。

次にプラグインを作ります。参考となるソースは usdImagingGL/testenv/TestUsdImagingGLHdGpProcedurals.cpp ユニットテストです。cmake を利用しても良いのですが、VisualStudio でコンソール DLL プロジェクトを空で作って、ユニットテストのような cpp ファイルを一つ用意するだけでも USD プラグインは作れます。ただしこの時、多くの人がハマりがちなポイントがありますので、本筋と離れますが紹介しておきます。

USD のプラグインロードは、TF_REGISTRY_FUNCTION マクロによって生成された .pxrctor セクションを使って行われるのですが、VisualStudio の標準設定で DLL を作成するとリリースビルド時にこのセクションは消えてしまいます。出来上がった dll を dumpbin にかけると次のようになります

File Type: DLL
  Summary
        1000 .data
        1000 .pdata
        2000 .rdata
        1000 .reloc
        1000 .rsrc
        2000 .text

この状態だとこのプラグインは一生ロードできません。回避方法としては /Zc:inline- をつけるとか、/OPT:NOREF をする、などがあるようです(VS のバージョンによって挙動が変わります)。
image.png
正しい状態ではこのように .pxrctor セクションが DLL 内にできます。

File Type: DLL
  Summary
        2000 .data
        2000 .pdata
        1000 .pxrctor
        8000 .rdata
        1000 .reloc
        1000 .rsrc
        F000 .text

話を戻して、いよいよ実装に入りましょう。まずこれから実装するプロシージャル処理のスキーマを決めます。サンプルですので、適当にアニメーションするグリッドメッシュを作ってみることにします。USD 記述はこのようにしてみました。

def GenerativeProcedural "myproc" (
  prepend apiSchemas = ["HydraGenerativeProceduralAPI"]
)
{
  token primvars:hdGp:proceduralType = "MyProcedural"
  float primvars:size = 1.0
  float primvars:param = 0.0
}

hdGp:proceduralType に "MyProcedural"、二つの float パラメータを入力とします。

最小構成では USD プラグインになる cpp ファイルに二つのクラスを記述すれば良いです。まずはプロシージャル処理本体を書いていきます。

gp_mesh.h
class MyProcedural : public HdGpGenerativeProcedural
{
public:
	MyProcedural(const SdfPath& proceduralPrimPath) : HdGpGenerativeProcedural(proceduralPrimPath)
	{
	}

	static HdGpGenerativeProcedural* New(const SdfPath& proceduralPrimPath)
	{
		return new MyProcedural(proceduralPrimPath);
	}

	DependencyMap UpdateDependencies(const HdSceneIndexBaseRefPtr& inputScene) override
	{
		DependencyMap result;
		return result;
	}

	ChildPrimTypeMap Update(const HdSceneIndexBaseRefPtr& inputScene, const ChildPrimTypeMap& previousResult,
		const DependencyMap& dirtiedDependencies, HdSceneIndexObserver::DirtiedPrimEntries* outputDirtiedPrims) override
	{
		ChildPrimTypeMap result;
        // これから実装する
		return result;
	}
	HdSceneIndexPrim GetChildPrim(const HdSceneIndexBaseRefPtr& inputScene, const SdfPath& childPrimPath) override
	{
		HdSceneIndexPrim result;
        // これから実装する
        return result;
    }
private:
    float size_ = 1.0f;
    float param_ = 1.0f;
};

上で説明した3つのエントリポイントを空の状態で作りました。次に続けて plugin.cpp ファイルに(もちろん別でも構いません)プラグインクラスと、登録処理を書きます。

plugin.cpp
class MyProceduralPlugin : public HdGpGenerativeProceduralPlugin
{
public:
	MyProceduralPlugin() = default;
	HdGpGenerativeProcedural* Construct(const SdfPath& proceduralPrimPath) override
	{
        // HdGpGenerativeProceduralResolvingSceneIndex から
        // GenerativeProcedural prim に対して呼ばれる
		return MyProcedural::New(proceduralPrimPath);
	}
};

TF_REGISTRY_FUNCTION(TfType)
{
	HdGpGenerativeProceduralPluginRegistry::Define
		<MyProceduralPlugin, HdGpGenerativeProceduralPlugin>();
}

cpp はひとまずこれで完成です。必要な USD ライブラリ(tf, gf, plug, hd, arch, sdf など)をライブラリに指定して、DLL を作ります。ファイル名は myGp.dll としてみました。boost とか tbb とか python のリンクあたりは各自の環境ごとに違いますので、ここでは説明しませんが頑張ってください。USD ビルドの中で作業するなら、extras/imaging/examples/hdTiny の横あたりに同じ構造で作って cmake の pxr_plugin マクロを使うのも楽かもしれません。

次にプラグインの定義を plugInfo.json に書きます。中身はこのようになります。

myGp/resources/plugInfo.json
{
  "Plugins": [
    {
      "Info": {
        "Types": {
          "MyProceduralPlugin": {
            "bases": ["HdGpGenerativeProceduralPlugin"],
            "displayName": "MyProcedural",
            "priority": 0
          }
        }
      },
      "LibraryPath": "../myGp.dll",
      "Name": "myGp",
      "ResourcePath": "resources",
      "Root": "..",
      "Type": "library"
    }
  ]
}

この displayName が、token primvars:hdGp:proceduralType = "MyProcedural" と一致すると、そのプラグインが選択されてプロシージャル処理が実行されます。
これをプラグインディレクトリ(PXR_PLUGINPATH_NAME で指定されるディレクトリ)に決められた構造で置くのですが、便利な事に 22.11 では HdGp プラグインは環境変数で指定されたフォルダを追加で読み込んでくれるようになっています。

PXR_HDGP_TEST_PLUGIN_PATH=\path\to\plugin\x64\Release

とすることで VisualStudio のビルドディレクトリを直接検索してくれるので、この Release フォルダの中に myGp/resources/plugInfo.json として上記 json を保存する、でもよいです。この場合、Release/plugInfo.json に以下のファイルも必要です(plugInfo.json が二か所に必要)。

plugInfo.json
{
    "Includes": [ "*/resources/" ]
}

ここまで出来たら、プラグインがロードできるか試してみましょう。その前にもう一つだけ注意です。22.11 では次の環境変数をセットしておいてください。またプラグインのトラブルシューティングのため、TF_DEBUG 環境変数をセットします。

set HDGP_INCLUDE_DEFAULT_RESOLVER=1
set TF_DEBUG=PLUG_*

厳密には HD_ENABLE_SCENE_INDEX_EMULATION=1 も必要ですが、こちらは 22.11 現在デフォルトでtrue になっていますので、明示的にオフしていない限り不要です。HDGP_INCLUDE_DEFAULT_RESOLVER は、HydraGenerativeProcedural は標準提供の SceneIndex プラグイン(デフォルトリゾルバの一つ)であるが、それを読むようにする、という意味です。まだ開発中なので誤動作を防ぐために通常オフにされています。HydraGenerativeProcedural は HdGpGenerativeProceduralResolvingSceneIndex クラスが prim の出入りをとらえて自分が対応すべき対象のフィルタリングと処理の実行を行うようになっています。Hydra 2.0 の Modular Filtering アーキテクチャが生きていますね。

この状態で上記の usd ファイルを usdview で読み込んでみます。たくさんログが出ますが、最後の方にこのように出れば成功です。

Loading plugin 'hdGp'.
#################################################################################
#  HDGP_INCLUDE_DEFAULT_RESOLVER is overridden to 'true'.  Default is 'false'.  #
#################################################################################
Loading plugin 'hdSt'.
Status: in HdGpGenerativeProceduralPluginRegistry at line 54 of C:\USD2211\pxr\imaging\hdGp\generativeProceduralPluginRegistry.cpp -- PXR_HDGP_TEST_PLUGIN_PATH set to C:\work\myGp\x64\Release
Will check plugin info paths
Will read plugin info C:\work\myGp\x64\Release/plugInfo.json
 Did read plugin info C:\work\myGp\x64\Release/plugInfo.json
Globbing plugin info path C:/work/myGp/x64/Release/*/resources/
Will read plugin info C:/work/myGp/x64/Release/usd_gp/resources/plugInfo.json
 Did read plugin info C:/work/myGp/x64/Release/usd_gp/resources/plugInfo.json
Registering shared library plugin 'usdGp' at 'C:/work/myGp/x64/Release/myGp.dll'.

まだ何も実装していないので、stageView にも何も出ません。

HdGp プラグイン作成~実装~

いよいよプロシージャル処理を記述してみます。今回は単独 Prim ですので、UpdateDependencies は空のままで良いです。Update 内は次のように処理していきます。

gp_mesh.cpp
ChildPrimTypeMap Update(
		const HdSceneIndexBaseRefPtr& inputScene,
		const ChildPrimTypeMap& previousResult,
		const DependencyMap& dirtiedDependencies,
		HdSceneIndexObserver::DirtiedPrimEntries* outputDirtiedPrims) override
{
    ChildPrimTypeMap result;
    HdSceneIndexPrim myPrim   = inputScene->GetPrim(_GetProceduralPrimPath());
    HdPrimvarsSchema primvars = HdPrimvarsSchema::GetFromParent(myPrim.dataSource);

    // parameter
    if (HdSampledDataSourceHandle sizeDs = primvars.GetPrimvar(_tokens->size).GetPrimvarValue())
    {
        _size = std::max(1.0f, sizeDs->GetValue(0.0f).GetWithDefault(_size));
    }
    if (HdSampledDataSourceHandle paramDs = primvars.GetPrimvar(_tokens->param).GetPrimvarValue())
    {
        _param = paramDs->GetValue(0.0f).GetWithDefault(_param);
    }
...

大げさな記述ですが、ここは usd に記述された size と param を取得しています。ここでは USD API は使っていないことに注意しましょう。今作っているのは Hydra Generative Procedural プラグインなので、USD Scene を直接扱っているわけではありません。シーングラフは Maya とか、何か他の DCC ツールから供給されているかもしれません。primvars: 名前空間を使ってパラメータを定義した理由は、こうしておくと Hydra の機構が DataSource を使って自動的に HdPrimvarsSchema を経由してデータを受け渡してくれるのです。なんと便利で美しい仕組みでしょう。

その後はプロシージャル生成する対象になる Rprim を作ります。Hydra シーングラフ中では GenerativeProcedural Prim の子にします。

gp_mesh.cpp
    // このサンプルではキャッシュは未実装で毎回全部作っているが、本来は個別に更新管理するべき
    SdfPath path      = _GetProceduralPrimPath();
    SdfPath childPath = path.AppendChild(_tokens->pc); // Hydra Rprim を一つだけ作る。名前は何でもいい
    result[childPath] = HdPrimTypeTokens->mesh;

    if (outputDirtiedPrims)
    {
        outputDirtiedPrims->emplace_back(
            childPath,
            HdDataSourceLocatorSet{
                HdPrimvarsSchema::GetPointsLocator(),
                HdMeshTopologySchema::GetDefaultLocator()
            });
    }

    return result;

名前は何でもよいですが、複数の Rprim が必要な場合は、その分名前を作る必要があります。またプラグイン内で区別できるようにその名前を記録しておくことになると思います。ここでは単純に HdMesh を一つ追加して、頂点とトポロジを動かせるように Locator を追跡対象にいれています。以前の Hydra であれば DirtyBits のビットフィールドを合成していたところですが、新 API では HdDataSourceLocatorSet に複数の Locator を追加することができます。

これで Update は終わりです。あとはこの Rprim path に対して DataSource を提供します

plugin.cpp
	HdSceneIndexPrim GetChildPrim(
		const HdSceneIndexBaseRefPtr& inputScene,
		const SdfPath& childPrimPath) override
	{
		HdSceneIndexPrim result;

		VtIntArray faceVertexCounts;
		VtIntArray faceVertexIndices;
		VtArray<GfVec3f> points;
        // (中略)size, param を使ってメッシュを作る

		result.primType = HdPrimTypeTokens->mesh;
		result.dataSource = HdRetainedContainerDataSource::New(
			HdXformSchemaTokens->xform,
			HdXformSchema::Builder()
			.SetMatrix(HdRetainedTypedSampledDataSource<GfMatrix4d>::New(GfMatrix4d().SetIdentity()))
			.Build(),

			HdMeshSchemaTokens->mesh,
			HdMeshSchema::Builder()
			.SetTopology(
				HdMeshTopologySchema::Builder()
				.SetFaceVertexCounts(HdRetainedTypedSampledDataSource<VtIntArray>::New(faceVertexCounts))
				.SetFaceVertexIndices(HdRetainedTypedSampledDataSource<VtIntArray>::New(faceVertexIndices))
				.Build())
			.Build(),

			HdPrimvarsSchemaTokens->primvars,
			HdRetainedContainerDataSource::New(
				HdPrimvarsSchemaTokens->points,
				HdPrimvarSchema::Builder()
				.SetPrimvarValue(HdRetainedTypedSampledDataSource<VtArray<GfVec3f>>::New(points))
				.SetInterpolation(HdPrimvarSchema::BuildInterpolationDataSource(HdPrimvarSchemaTokens->vertex))
				.SetRole(HdPrimvarSchema::BuildRoleDataSource(HdPrimvarSchemaTokens->point))
				.Build()
			));

		return result;

メッシュ生成部分は本質的でないので省略しました(サンプルコードは末尾の github をごらんください)。ちょっと長くて見づらいですが、RetainedDataSource をつかって、xform とトポロジ、頂点座標を DataSource に入れてみました。token, builder の順に並べているのがわかるかと思います。ContainerDataSource はこのように名前(token)で識別された DataSource をまとめることができます。Builder に関しては細かい説明は省きますが、こういうもの、と思って使ってもらって大丈夫です。

テスト

以上でサンプルコードは完成しました。usdview で見てみましょう。
image.png
無事プロシージャル生成したメッシュが出てきました。上では省略しましたが、アニメーションするグリッドを生成するようなコードにしてみましたので、myproc の primvars:size と primvars:param をアニメーションさせてみます。エディタで .timeSamples つけるだけでアニメーションできてしまうのが USD のいいところですね。

def GenerativeProcedural "myproc" (
        prepend apiSchemas = ["HydraGenerativeProceduralAPI"]
    )
{
    token primvars:hdGp:proceduralType = "MyProcedural"
    float primvars:size.timeSamples = {
        1: 1,
        20: 20
    }
    float primvars:param.timeSamples = {
        1: 0.0,
        100: 10.0
    }
}

無事パラメータをアニメーションさせてプロシージャルジオメトリを生成できました。このサンプルの見た目は全然すごくないんですが、これが Hydra 内部で動いているのがすごいです。
Animation.gif

ちなみにピッキングしようとするとエラーがでますが、これは HdStorm/UsdImaging のピッキングアルゴリズムが関与しないところで RPrim を生成しているからで、なんらかの RPrim 命名規則を作って GenerativeProcedural 自身を選択したことにする、という処理にしていかないとならなそうです。誰か直して PullRequest 送ってあげてください。

他の Prim を使う

さらに一歩進めて、シーングラフ中の他の Prim (Mesh など)のジオメトリを使ってプロシージャル生成してみます。典型的なのは Fur ですね。これはリレーションを使って表現できることになっています。

def GenerativeProcedural "myproc" (
    prepend apiSchemas = ["HydraGenerativeProceduralAPI"]
)
{
    token primvars:hdGp:proceduralType = "MyProcedural"
    rel primvars:sourceMeshPath = </mesh>
}

プラグインの実装は同じ流れですが、少しややこしくなります。細かな部分は自分もあまり自信がないので、参考程度にしてください。

まず今度はシーン中の違う Prim の変更に追従する必要があるので、UpdateDependencies を実装します。メッシュトポロジと頂点座標の変更を知りたいので、HdDataSourceLocatorSet に二つをセットして DependencyMap に登録しました。

plugin.cpp
DependencyMap UpdateDependencies(const HdSceneIndexBaseRefPtr& inputScene) override
{
    DependencyMap result;
    HdSceneIndexPrim myPrim = inputScene->GetPrim(_GetProceduralPrimPath());
    HdPrimvarsSchema primvars = HdPrimvarsSchema::GetFromParent(myPrim.dataSource);
    if (HdSampledDataSourceHandle sourceMeshDs = primvars.GetPrimvar(TfToken("sourceMeshPath")).GetPrimvarValue())
    {
        VtValue v = sourceMeshDs->GetValue(0.0f);
        if (v.IsHolding<VtArray<SdfPath>>())
        {
            VtArray<SdfPath> a = v.UncheckedGet<VtArray<SdfPath>>();
            if (a.size() == 1) {
                HdDataSourceLocatorSet locators;
                locators.append(HdMeshTopologySchema::GetDefaultLocator());
                locators.append(HdPrimvarsSchema::GetPointsLocator());
                result[a[0]] = locators;
            }
        }
    }
    return result;
}

Update は例によってキャッシュ機構をつくらなかったので、あまり説明するところはありません。完全なソースは記事末尾の github リンクから見てください。
GetChildPrim ではリレーションで張った Prim の DataSource を使って生成部分の DataSource を作ります。今回は HdBasisCurve を作りたかったので、インデクス配列を二つと頂点座標が必要になります。

plugin.cpp
HdSceneIndexPrim GetChildPrim(const HdSceneIndexBaseRefPtr& inputScene,	const SdfPath& childPrimPath) override
	{
		HdSceneIndexPrim result;
		if (!_curvePointsDs)
		{
			_curvePointsDs = _CurvePointsFromMeshPointDataSource::New();
		}
		_curvePointsDs->Update(_meshPointsDs, _meshFaceVertexCountsDs, _meshFaceIndicesDs, _numSampleDs, _lengthDs);

		if (_meshPointsDs)
		{
			// meshPointDs 頂点上にラインを生成する
			result.primType = HdPrimTypeTokens->basisCurves;
			result.dataSource = HdRetainedContainerDataSource::New(
				HdBasisCurvesSchemaTokens->basisCurves,
				HdBasisCurvesSchema::Builder()
				.SetTopology(
					HdBasisCurvesTopologySchema::Builder()
					.SetCurveVertexCounts(_CurveVertexCountsDataSource::New(_meshFaceVertexCountsDs, _numSampleDs))
					.SetCurveIndices(_CurveIndicesFromDataSource::New(_meshFaceVertexCountsDs, _numSampleDs))
					.SetBasis(HdRetainedTypedSampledDataSource<TfToken>::New(HdTokens->bezier))
					.SetType(HdRetainedTypedSampledDataSource<TfToken>::New(HdTokens->linear))
					.SetWrap(HdRetainedTypedSampledDataSource<TfToken>::New(HdTokens->segmented))
					.Build())
				.Build(),
				HdPrimvarsSchemaTokens->primvars,
				HdRetainedContainerDataSource::New(
					HdPrimvarsSchemaTokens->points,
					HdPrimvarSchema::Builder()
					.SetPrimvarValue(_curvePointsDs)
					.SetInterpolation(HdPrimvarSchema::BuildInterpolationDataSource(HdPrimvarSchemaTokens->vertex))
					.SetRole(HdPrimvarSchema::BuildRoleDataSource(HdPrimvarSchemaTokens->point))
					.Build()
				));
		}

		return result;
	}

以前のサンプルと違って _curvePointsDs を作り置きしているのですが、これが正しいやり方かは確証がないです。なぜこうしているかというと、今回メッシュの法線方向にラインプリミティブを作りたかったのですが、USD のメッシュはデフォルトで SubdivisionSurface になっていて、通常は法線ベクトルが出力されません。これを points 同様に sourcePrimvars.GetPrimvar(HdPrimvarsSchemaTokens->normals).GetPrimvarValue(); としても値が取れないのです。Hydra の中には VertexAdjacency テーブルを構築してポリゴンメッシュからスムーズノーマルを計算する機能もあるのですが、サーフェス上の fur の濃度も変えたいことですし、思い切って OpenSubdiv でリミットサーフェスを計算してしまうことにしました。

ただし OpenSubdiv の TopologyRefinement はそれなりに重い処理ではあるので、頂点座標だけが変化するようなケースでは毎回再計算はしたくありません。そこで _CurvePointsFromMeshPointDataSource の中にTopologyRefiner と PatchTable を保持するようにして、points が変わったときには PrimvarRefinement だけを通してパッチからリミット評価し、法線ベクトルも解析的に計算することにしました。

plugin.cpp
VtIntArray faceIndices = _faceIndicesDs->GetValue(shutterOffset).UncheckedGet<VtIntArray>();
if (_faceIndices != faceIndices)
{
    _CreateRefiner(faceVertexCounts, faceIndices, points);
}

ここは完全に手抜きですが、メッシュトポロジのインデクス配列が変化したら TopologyRefinement をするようにしています。なお4角形はそのまま、4角形以外は一度分割して4角形にしてからパッチと対応づけて fur の根本になる座標をパッチ座標系でランダムサンプリングして作っています。これで法線は LimitFrame にパッチ座標の基底を与えて一次微分の外積で出せます。なお patchMap もトポロジ依存なので毎回作る必要はないのですが、ここも手抜きです。今回の記事の本質ではないのであまり気にしないでください。

maya で適当にアニメーションしたメッシュに対して、今回作成した GenerativeProcedural を適用してみました。USD Stage には何も変化はないですが、Hydra で生成された linear curve がメッシュ状に無事生成されました。
fur.gif

完成・おわりに

やや駆け足でしたが UsdProcGenerativeProcedural と Hydra の一部を解説させて頂きました。まだまだ発展途上のスキーマですが、HydraGenerativeProceduralAPI だけでもかなりの可能性を感じるものであることがお分かりいただけましたでしょうか。オレオレプロシージャルを作り放題で、かつそれが自動的に RenderDelegate に対応しているレンダラ全てで利用可能になる、というのは大げさですが世界が変わる感じがします。ファーや植生のレンダリングでは真っ先に応用が出てくるでしょうが、テセレーションやディスプレイスメントなども試してみたくなってきます。ShaderToy みたいに HdGp を共有するコミュニティとか出来ると面白そうですね。

この記事で作ったソース全体を github に載せておきます。何しろ開発中の API を調べながらの実装でしたので手順も正しい保証はないですが、何かの参考になれば幸いです。次はこの HydraGenerativeProcedural を HdStorm 以外のレンダラでレンダリングしてみたいですね。

皆さまどうぞ良いお年をお迎えください。

サンプルコード
https://github.com/takahito-tejima/UsdSandbox/tree/main/myGp

6
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?