UVを逆向きに読む — テクスチャ上の位置から3D座標を求める
はじめに
3DCGでUVを使うとき、つい「この面が、テクスチャのどこを参照するか」と考えがちです。
でも、UVが作っているのは、3Dの表面と2Dの画像との対応関係です。
ならば逆に、「画像のこの位置は、モデルのどの面にあたるか」と考えることもできます。
この記事では、そんなUVの逆向きの使い方を、Houdiniで建物の窓を配置する例を通して見ていきます。
今回のシーンファイルは、こちらからダウンロードできます。実験的に実装したので綺麗なシーンファイルではないですが、使用したテクスチャとPythonファイルも同梱しているので、Readmeを読んで使い方を確認していただければと思います。
https://drive.google.com/drive/folders/1zk9bNKUf_oVKuG5pLS8WEP_7dFNlVMxl?usp=sharing
考え方
座標は共有できないが、住所は共有できる
UV上の位置から3D上の位置を求めようとすると、座標そのものを変換する計算が必要に思えます。しかし実際に必要なのは、座標を変換することではなく、住所を経由することです。
3Dモデルの表面上のある点は、次の2つの情報で一意に指定できます。
- どのポリゴンか
- そのポリゴンの中のどのあたりにあるか
この2つの情報から定まる位置を、ここでは仮に「住所」と呼びます。重要なのは、住所が絶対座標をまったく含んでいないことです。「7番目のポリゴンの、中央よりやや左上」という記述は、そのポリゴンがどこにあろうと、どんな向きだろうと、どんな大きさだろうと成立します。
2つのモデルで、同じ住所を持つ点
ここで、元のモデルとまったく同じ構造を持ちながら、各頂点を「UV座標そのもの」の位置に置いたコピーを考えます。要するに、モデルを平面に開いた状態です。
VEXで書くと、次の1行だけです。
@P = set(@uv.x, 0, @uv.y);
これら2つのモデルは、ポリゴンの数、並び順、頂点の順番がすべて同じです。違うのは頂点の位置だけ。つまり「7番目のポリゴンの中央よりやや左上」は、平面版でも元モデルでも同じ場所を指します。
元のCubeと平面版のGridとでは面の大きさに違いがありますが、ここで重要なのは、そのプリミティブ内での相対的な位置が両者で一致するという点です。
平面上の点から立体へ
立体モデルの平面版が手に入れば、あとは簡単です。
以下の画像のような処理を行うことで、平面上の点を元の立体上の点に対応づけられます。
平面上で場所を特定し、その場所を元の立体に戻す。やっていることはそれだけです。
Houdiniで試してみる
抽象論だけでは実感が湧かないので、具体例で試してみます。
題材
建物のテクスチャが貼られたモデルがあるとします。壁はテクスチャのままで十分でも、窓だけは立体感が欲しい、というケースを考えます。
窓モデルを一つずつ手で置いていくのは、建物や窓の数が多いと骨が折れます。テクスチャを差し替えたり、スケールを調整したりすれば、その都度やり直しになります。
ここで先ほどの逆引きが使えます。テクスチャ画像を解析して窓の中心を求め、その位置に対応する3D座標を引けばいいのです。
平面版のコピーを作る
先ほどのVEXと同じ処理を行うと、このような結果が得られます。
UVから位置を逆引きする
Houdiniには、まさにこの用途の関数が用意されています。これらはセットで使うことが多いです。
xyzdist
指定した位置から最も近い表面上の点を探し、その場所を「何番目のポリゴンの、その中のどこか」という形で返してくれる関数です。つまり、住所を教えてくれます。
float dist = xyzdist(1, @P, prim, puv);
戻り値そのものは、最寄りの点までの距離です。肝心の住所は、引数として渡した変数に書き込まれます。prim がポリゴンの番号、puv がそのポリゴンの中での位置です。近くに表面が見つからなければ prim は -1 になります。
primuv
住所を渡すと、その場所の値を返してくれる関数です。位置でも、法線でも、色でも、任意の情報を引けます。
vector p = primuv(2, "P", prim, puv);
第2引数にアトリビュート名を渡すと、その住所でのそのアトリビュートの値が返ってきます。
ここでは、次のような使い方をしています(入力0: 窓のポイント群、入力1: 平面版のモデル、入力2: 元のモデル)。
int prim;
vector puv;
xyzdist(1, @P, prim, puv);
// 上で取得した住所に対応する、元のモデルにおける位置を取得
@P = primuv(2, "P", prim, puv);
// 元のモデルからN(法線)を取得
@N = primuv(2, "N", prim, puv);
これら2つの関数を理解するには、こちらの記事がおすすめです。
The joy of xyzdist() and primuv()
この2つを順に呼ぶだけで逆引きは完成します。平面版で住所を調べ、元モデルでその住所を開く。住所を調べるときにNも一緒に取得しておくと、窓を配置する際の向きを決めるのにそのまま使えます。
窓のポイント
モデルをUVどおりに展開できても、窓を自動的に配置するには、テクスチャに対応した窓のポイントが必要です。そこで今回は、Pythonでテクスチャ画像を解析し、テクスチャ上での窓の中心座標を書き出しました。
Pythonで解析したポイントをHoudiniに読み込むと、次のように窓の中心にポイントが生成されます。
以下の画像は、上のポイントを建物全体に繰り返し並べた結果です。このポイント群を、先ほど展開した平面版のモデルに対して逆引きしていきます。
結果
以下が実行結果です。
図2はUVスケールを2倍にした場合です。UVを2倍にするにあたって元のモデルのスケールも2倍にしています。ポイントは、UVタイリングと同じように配置されているので、スケールが変わっても破綻しないです。しかし、スケールが自然数倍の時に限ります。
図1: UVスケール1の場合
図2: UVスケール2の場合
補足 — 住所の正体はパラメトリック座標
ここまで「住所」と呼んできたものには、ちゃんとした名前があります。ポリゴンの中の位置を表す部分は、パラメトリック座標と呼ばれるものです。
こちらのモノリストストさんの記事でも紹介されています。
https://www.monolithsoft.co.jp/techblog/articles/000633.html
これは「そのポリゴンの頂点を、どの割合で混ぜた場所か」を表す比率です。たとえば三角形なら、3つの頂点にそれぞれ重みを割り当て、合計が1になるようにします。中央なら3つとも同じ割合、ある頂点の近くならその頂点の重みが大きくなる、という具合です。
重要なのは、この値が絶対座標をまったく含んでいないことです。「頂点0を3割、頂点1を5割、頂点2を2割」という記述は、それらの頂点がどこにあるかを一切参照していません。
だから、こういうことが起こります。
平面版: 0.3×(UV上の頂点0) + 0.5×(頂点1) + 0.2×(頂点2) = 窓のUV位置
元モデル: 0.3×(3D上の頂点0) + 0.5×(頂点1) + 0.2×(頂点2) = 窓の3D位置
上の式は三角形の場合です。四角形なら重みは4つになりますが、「各頂点をどの割合で混ぜるか」という形は変わりません。
UVから3Dの位置を特定できるのは、この「割合だけを取り出せる」性質があるからです。逆に言えば、頂点の並び順やポリゴンの分割が変わってしまえば、同じ割合が別の場所を指すことになり、すべて破綻します。
おわりに
汎用的な画像解析スクリプトを書くのは簡単ではないので、実運用ではテクスチャの傾向がある程度揃っている前提が必要になります。今回はテクスチャの窓枠の色から、その内側にある窓を検出する処理を行いましたが、窓ガラスや窓枠が特徴的な色でない場合は、検出そのものが別の課題になります。
これまでテクスチャは、3Dモデルにリアリティを持たせる目的でしか使ってきませんでした。今回のように2Dの段階で処理を施し、3Dモデルの側をコントロールするという方法もあるというのは、面白い発見でした。
UVを双方向に読めると分かれば、応用先は窓の配置に限りません。テクスチャに描かれた汚れの位置にジオメトリを生やしたり、静止した状態のフレームで住所を保存しておき、変形するサーフェスにポイントを追従させるなど様々な手法が考えられます。「住所を経由する」という発想自体が、汎用的な道具になります。







