Skip to main content

SOLARISでInstancerを使う(1) 配置編

SOLARIS のメニューには、 Insatncing というカテゴリが存在している通り、SOLARIS でのレイアウトでの
Instance というのはかなり重要な要素になってきます。

というわけで、今回は Instancer 周りの構造の使い方とかが
だいぶわかってきたのでまとめをば。

Instance とは

SOLARIS での Instance とは、

たくさんの"同じ"オブジェクトのインスタンスが UsdStage 上で同じ表現(合成した Prim)を共有することができる機能のことです (USD docs 日本語訳版より)

とあるように、ある共通の Prim を参照・共有化して表示する機能のことです。

SOLARIS ではこの Instance の機能で、Houdini の Points に対して Instance Objects を作成 することができます。

基本構造

まずは基本的な構造から。

まずは Points に配置したい Prim を用意します。
そして、AddPointInstancer ノードを作ります。

今回のサンプルの場合だと Sphere と Cube の2つを用意します。 そしてそれを Merge して AddPointInstancer の Input2 に接続します。

次に SOP ネットワークで、オブジェクトを配置したい Points を作成します。

今回は、 Primitive Type を Points にした Grid を用意しておきます。

最後に、AddPointInstancer ノードの Target Points の SOP Path に
先程作った配置用 Points を指定して、

配置したい Prim(Prototypes と呼ぶ)を指定するために Prototype Source を Second Input にして、 Use Entire Stage をはずします。

そうすると、Grid の Points に対してオブジェクトがランダムに配置されます。

できあがったシーングラフがこちら。 Second Inputs に入力したオブジェクトは Prototypes スコープ下に配置され
PointInstancerPrim が作成されます。

PointInstancer の特徴は

#sdf 1.4.32

def HoudiniLayerInfo "HoudiniLayerInfo" (
customData = {
string HoudiniCreatorNode = "/stage/addpointinstancer1"
string[] HoudiniEditorNodes = ["/stage/addpointinstancer1"]
}
)
{
}

def PointInstancer "addpointinstancer1"
{
int64[] invisibleIds = []
quath[] orientations = [(1, 0, 0, 0), (1, 0, 0, 0), (1, 0, 0, 0), (1, 0, 0, 0), (1, 0, 0, 0), (1, 0, 0, 0), (1, 0, 0, 0), (1, 0, 0, 0), (1, 0, 0, 0)]
point3f[] positions = [(-15, 0, -15), (0, 0, -15), (15, 0, -15), (-15, 0, 0), (0, 0, 0), (15, 0, 0), (-15, 0, 15), (0, 0, 15), (15, 0, 15)]
int[] protoIndices = [0, 1, 0, 1, 1, 1, 1, 0, 1]
rel prototypes = [
</addpointinstancer1/Prototypes/cube1>,
</addpointinstancer1/Prototypes/sphere1>,
]

def Scope "Prototypes" (
customData = {
token HoudiniSourceNode = "/stage/merge1"
}
)
{
def Sphere "sphere1" (
customData = {
int HoudiniPrimEditorNodeId = 194
}
)
{
double radius = 1
matrix4d xformOp:transform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) )
uniform token[] xformOpOrder = ["xformOp:transform"]
}

def Cube "cube1" (
customData = {
int HoudiniPrimEditorNodeId = 195
}
)
{
double size = 2
matrix4d xformOp:transform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) )
uniform token[] xformOpOrder = ["xformOp:transform"]
}
}
}

出来上がった USD ファイルを見るとわかりやすいですが、
Prototype となる(生成元になっている)Prim があるけれども 実際に配置されているオブジェクトは生成されず、PointInstancer ノードの Position と Orientation、そして Prototypes への Index が保持されたじょうたいになっています。

で、これの何が良いかというと尋常じゃないぐらい軽く、数万どころか数百万の オブジェクトにすら耐えられたり(Houdini だとさすがにきつかった) ビューポート上での表示も非常に軽く、大量のオブジェクトをばらまいてシーンを構成するときには 非常に効果を発揮します。

追記メモ https://graphics.pixar.com/usd/docs/api/class_usd_geom_point_instancer.html#details

The PointInstancer schema is designed to scale to billions of instances

らしいので、数百万インスタンスどころではなかった。

PointInstancer と CopyToPoint

この PointInstancer と似た機能というかほぼ同じような結果になるノードが「CopyToPoint」ノードです。 CopyToPoint ノードの指定方法やノードネットワークのつなぎ方は AddPointInstancer とほぼ共通 になりますが、結果できあがるシーングラフは大きく異なります。

上の例だと、PointInstancer ノードが出来上がっただけでしたが CopyToPoint の場合は、実際の Prim が生成されます。

Use Entire Stage がオンの場合は Prototypes の Prim が Instance として各 Point に生成され

こんな感じですべての Prim が配置されるし、

Use Entire Stage がオフの場合は

PointInstancer と同じように、ランダムで Prim が配置されますが
Prototypes 以下にある Prim をリファレンスで配置した状態になります。

PointInstancer と違い、実際に Prim が作られるので表示は遅いものの
各 Prim に対しての編集・加工はやりやすいというメリットがあります。

使い分けとしては、草とか石ころとかのようなランダムに大量にばらまきたいものは
AddPointInstancer で配置して、街灯とか信号とかのような
配置系アセットは CopyToPoint を使う...というような使い方をするのが良さそうです。

Prototypes オブジェクトのコントロール

基本的な例の場合は、Merge したオブジェクトを Input2 で渡す...といった方法で
セットしていました。

これでもまぁ良さそうですが、他のやり方も確認してみます。

Input1 でわたすか Input2 で渡すか

そもそも、上で「Input2 につないでわたす」と書いたのですが
何故に 2 つ方法があるのでしょうか。

単純に Input1 にしてみても「No prototype primitives found.」になってしまい
エラーになってしまいます。

と、エラーを見ればそのとおりですが
Input1 につないだ場合は、Prototypes へのノードの移動は行われず

Prototype Primitives での指定が lopinputprims では NG になってしまう模様。

つまりは、Input1 で渡す場合は、入力で与えられた Prim ではなく
別の手段で明示的に(Stage の「どれか」)を指定する必要がありました。

これに限らずですが、SOLARIS のノードの Input1 はそれまでの段階で編集された
Stage をつなぐことが多く、この AddPointInstancer も
Input1 は Stage を受け取るものとして動作しているからっぽいです。
対して Input2 は Prototypes を渡すための口として働くので、Input の Prim を Scope に移動して
すべて Prototypes として動作させる...という意図なのかなと思います。

つーわけで、明示的に指定してみます。 書き方は公式の Help 内の プリミティブマッチングパターンに記載されています。

名前で入れる

まずは単純な例として。
こんな感じで特定の名前で Prim を作り、graft ノードでまとめておきます。 こんな感じ。

そして、名前でマッチするように Prototype Primitives を指定します。 インスタンスはこれで作ることができますが

こんな感じで指定した Prim は移動されずその場にのこり、
AddPointInstancer の下の Prim は、元になった Prim をリファレンスで読み込む構造になります。

なので、元の Prim を編集すれば Instance したオブジェクトも自動で更新されるようになります。

Collection で指定する

名前ではなく、特定の Collection に含まれる Prim を指定する事もできます。
複雑なネットワークなどで、特定の Prim を入れたい場合や
他の Layer から読み込んできた(SdfPath が不定な)Prim を追加したい場合などでは
このやり方が良さそうです。

Collection を使う場合は、 collection ノードを使用して、Collection に Prim を追加します。

そして、作成した Collection ノードで Collection Name を指定して

こんな感じで CollectionName を指定します。 書き方は、

/PrimitivePath.collection:CollectionName

で、「collection」はお約束でかならず入れます。 公式 Docs では collections となってるのですが、こちらだと正しく動かないので注意が必要です。
あるいは、 %/PrimitivePath/CollectionName でも OK のようです。

こんな感じで、

  1. 元 Prim を残す or 残さない で Input 1 or 2 を選び
  2. Prototype Primitives で配置する Prim を制御して
  3. Prim を PointInstancer で作るか Reference の Instance(実際に Prim を作る)で CopyToPoint AddPointInstancer を使う

ことで、自分のやりたい作業のための大量のオブジェクト配置ができるようになります。

配置する Prim 側メインで書いてますが、 配置する Point 側もコントロールできて、Point Group を SOP 側で指定すると
配置に使用する Point をコントロールすることができます。

配置 Point は SOP 内で作成するわけですが
SOP 側での配置オブジェクトのコントロールを Point Group で行い
LOP 側はその Group で Instancer の制御をする(PointInstancer か Reference かなど) ...そんな棲み分けになるのかなと思います。

編集周りもやろうと思いましたが長くなったので続きは次回。