はじめに
※本記事ではHoudini 20.5.522、UE 5.2.1 を使用しています。
ここしばらく、こんなものを作っていました。
ご覧の通り、撃った場所に好きな形の穴を開ける、というものです。
これはUEのデカールの機能を使っていますが、マテリアルの中身としてはInteriorMappingを元に考えたものになっています。
ご存じの方も多いかもですが、本来のInteriorMappingは四角い部屋をたくさん並べるような表現として用いられます。
そんなInteriorMappingをどうすれば好きな形の部屋にできるか?
結構なエネルギーを注いで研究してみたので、まとめてみようと思います。
注意点
- 本文は前編と後編に分かれています。前編は、一応それらしいものができたもののクオリティが低く挫折した話で、後編はそこからブラッシュアップを達成した話です。実用的な情報だけが欲しい場合、前編は流し読みでも問題ないと思います。
- InteriorMappingの基礎的な知識について解説することはしません。同様に、やや専門性の高い用語を使うことがありますが、これも解説は割愛します。
動機
ある時、別のところでInteriorMappingの記事を書く機会がありました。
自分なりに改めて勉強しつつ、無事記事を書き上げることはできたのですが…。
その時、こう思ったわけです、 「四角い部屋だけじゃなくてもっと別の形の部屋はできないものだろうか」 と。
そう思い探してみると、シリンダー状の縦穴はやってる人がいました。
また、半球状の部屋、というのも頑張ってみるとできました。
![]() |
---|
では、任意の形はできないのか?というところが挑戦の発端です。
もしこれができるなら、ParallaxOcclusionMappingに置き換わる新技術になるのでは!?
俄然やる気が湧いてきたところで実際にチャレンジすることにしました。
実現方法として、シェーダ単体では無理と判断しましたがHoudiniを活用すればできそうな気がします。
ということで、やってみました。
前編 - 挫折編
基本的なアプローチ
そもそものInteriorMappingがどういうものかですが、
- 各ピクセル毎に、ある視点から見た時の、注目ピクセルから仮想的な壁までの距離を適切に求める
- さらにそこからUV座標を計算する
といったものです。
![]() |
---|
こんな感じのイメージです。 |
つまり入力として、ピクセル位置、視線ベクトルがあり、出力としてUV座標値があればいいわけです。
通常のInteriorMapping(立方体状)やシリンダー状、半球状の部屋の形であれば、形状がシンプルなので、壁までの距離は決まった計算で求めることができました。
しかし任意の形状となるとそうはいきません。
そこで今回選んだ手法は、Houdiniで、「各ピクセルに相当する矩形範囲内に、各視線方向におけるUV座標値を予め記録した」テクスチャを作っておく、といったアプローチです。
まあ要するにゴリ押しということです。
![]() |
---|
こんなテクスチャ。64x64ピクセルのInteriorMappingを表現したいならば、64x64個のグリッドを定義し、そのグリッド内のひとマス毎に、各視線方向におけるUV座標値を書き込みます。 |
この手法のことを以降Grid Interior Mapping、略してGIMと呼び、同様にHoudiniから書き出されるGIMのためのテクスチャをGIMテクスチャと呼ぶことにします。
HoudiniでGIMテクスチャを出力する
GIMテクスチャを書き出すためのSOPネットワークですが、コア部分は意外とシンプルです。
以下のような感じです。
![]() |
---|
まず、事前に部屋の形状をモデリング & UV展開しておきます。
次に各グリッドの中心に、視線方向のバリエーション数ぶんのポイントを配置します。
このポイント群は視線方向に一致する@N
アトリビュートを持たせておきます。
そうしたら@N
の方向にRay SOPでポイントを投射することで部屋の内壁に衝突させます。
あとはHit判定から取得できたUVアトリビュートをCdに移し替え、グリッド内でマス目上に並べている、といった感じです。
![]() |
---|
やや複雑なことをしているのは視線ベクトルをグリッドに変換する部分でしょうか。
ちょっと煩雑になるのでここの解説は割愛します。
![]() |
---|
また、同じような要領でセルフシャドウ情報を格納したテクスチャも作れそうだったのでやってみました。
こちらは視線方向ではなく平行光源方向になりますが、方向のバリエーション数ぶんレイを飛ばし、遮られたらそのピクセル・その方向では影の中とみなす、という感じです。
![]() |
---|
今回はシェーディング用にオブジェクトスペース法線も使いたいので、そのテクスチャも一緒に出力しておきます。
さらにデカールのマスク用テクスチャとウィジェットに表示するUI用テクスチャも作ります。
![]() |
---|
テクスチャはすべてCOPから出力します。
これで必要なテクスチャが準備できました。
なお、ここまでの一連の流れは簡単なPDGで一括処理できるようにしてあります。
![]() |
---|
PDG、便利。 |
UEマテリアルで実装する & 細かい問題点への対応
次にUE5上でマテリアルを組んでいきます。
いきなりデカールとして動作させるのではなく、まずは回転しないポリゴン上で想定の見た目になることを確認しましょう。
GIMテクスチャのサンプリング
事前準備として、GIMテクスチャの設定は以下のようにしておきます。
項目 | 設定値 |
---|---|
ミップ全般設定 | NoMipmaps |
圧縮設定 | VectorDisplacementmap (RGBA8) |
sRGB | オフ |
Filter | 隣接した項目を選択(Nearest) |
使用するGIMテクスチャは64x64グリッド、1グリッド32x32マスの2048x2048pixelとしました。
実装内容としては、
- 視線方向のベクトルをUVオフセット量に変換
- 上記を用い、注目しているグリッド内において、適切なひとマスを選択
- 選択したマスに記録されたUV値をテクスチャサンプリング
とすればよいはずです。
GIMテクスチャからサンプリングされたUV値を使い、タイリングテクスチャを貼るグラフは以下のような感じです。
![]() |
---|
この記事を書く用に思い出しつつ組み直しているので実際の過程とは違っているかも。。。 |
MF_DirectionToUvGrid
の中身は以下です。
![]() |
---|
描画結果は次のようになりました。
![]() ![]() |
---|
1枚目:UV値をそのまま表示 2枚目:テクスチャを貼って表示 |
……奥行きはうっすら感じられますが、ドット絵にしか見えないですね。。。
考えてみれば、各グリッドは言わば離散値の状態になっているわけですから、これは当たり前です。
この問題を解決するために、今回はバイリニア補間を使ってみました。
コストはかかりますがGIMテクスチャを1グリッドずつずらして4回サンプリングし、それを縦横方向に線形補間します。
するとこんな感じの結果になります。
![]() |
---|
ドット絵状態からは脱却できましたね!
全体的にガタガタ感は気になりますが、これはGIMテクスチャに解像度の制限がある以上しょうがないでしょう……。
この状態にすると気になってくるのは、UVの境界です。
見ての通り、UVの境界でバイリニア補間が行われた結果、変な境界線のようなものができてしまっています。
これを回避するために、UVの境界ではバイリニア補間をキャンセルするような処理を追加しました。
![]() ![]() |
---|
UVの境界であるか≒縦または横のバイリニア補間をキャンセルするか、はSOPネットワークの以下の部分で、GIMテクスチャのBAチャンネルに情報として仕込むようにしています。
![]() |
---|
![]() ![]() |
---|
1枚目:キャンセルマスクなし 2枚目:キャンセルマスクあり |
ガタガタ感は更に増してしまいましたが、まあ境界線が出るよりはマシかな……と判断しました。
ここまでで、固定のポリゴン上で想定の見た目にすることができました。
既にこの手法の限界が見えてきているような気もしますが。。。
動的に生成されるデカールに対応させる
次にデカールでも使えるようにします。
今回はランダムに回転したり、平面であればどんな向きの壁・床であっても正しく描画されてほしいので、単にMaterialDomainを変更するだけでなく、視線方向ベクトルの回転対応が必要になります。
つまりワールド空間の視線方向ベクトルをローカル空間にTransformするということです。
普通にTransformVectorノードを使えたら苦労しないのですが、MaterialDomainがDeferredDecalの場合以下のようなエラーを吐かれてしまいます。
![]() |
---|
そこでBPからDecalActorの向きをDynamicMaterialInstance経由で受け取り、これを基底としてInverseTransformMatrix
にかけることで解決するようにしました。
![]() |
---|
最初はタンジェント空間にTransformすればいいと考えたのですが、投影面の法線の向きを取得できたとしても、接線と従法線の向きは取得できないということでうまくいきませんでした。
部屋内部の法線を出力できるようにする
デカールとして投影されたときに違和感ない見た目にするには、ライティングの影響も受ける必要があります。
このために、部屋内部の法線も出力できるようにします。
GIMテクスチャのUV値を使ってオブジェクト空間法線テクスチャをサンプリングします。
これを適切にワールド空間に変換し、TangentSpaceNormalをオフにしたうえで結果ノードに出力してあげます。
座標変換については先ほど同様、BPから受け取ったDecalActorの向き情報を使いますが、今回はローカル(オブジェクト)空間からワールド空間の変換となるので、Transform3x3Matrix
マテリアル関数の方を使用しています。
![]() |
---|
ライティングの影響を受けるデカールにできました。
![]() |
---|
セルフシャドウも実装する
もう一歩進んで、法線によるシェーディングだけではなく、セルフシャドウも表現できるようにしてみましょう。
GIMテクスチャから取得したUV値と、UVオフセット量に変換した平行光源方向ベクトルを使い、Houdiniで作ったセルフシャドウ用テクスチャをサンプリングします。
テクスチャの設定はGIMテクスチャと同じにします。テクスチャ解像度については、128x128グリッド、1グリッド16x16マスの2048x2048pixelにしてあります。64x64グリッドでは結果が粗すぎたのでこのようにしました。
![]() |
---|
これだけだとシャドウの境界のガタガタが目立つので、ここでもバイリニア補間を使って滑らかにすることにしました。
といってもサンプリング回数をさらに増やすのは避けたかったので、Houdiniから出力する際に予めオフセットしたシャドウ情報をRGBAに格納するように工夫してみました。
![]() |
---|
ここまでの内容を実装したものがこちらの動画になります。
クオリティの限界
…と、そんなこんなで一応形にはなりました。
が、ここまでご覧になればわかる通り。
だいぶ汚いです。
目標であったPOMについて、UE公式のContentsExampleを見直してみましたが、そのクオリティとは比ぶべくもないです。
![]() |
---|
公式のPOM。 |
視線を動かしたときのパカパカも気になるし、UV境界でガタガタしたり、シャドウが途切れていたり……。
正直製品で実用できるほどの品質には達することはできなかったといえるでしょう。
![]() |
---|
うーん、汚い。 |
そもそもこれはInteriorMappingなのか?
本来のInteriorMappingでは、視線方向ベクトルには 「カメラから各ピクセルを向くベクトル(=CameraVectorノードをNegateしたもの)」 を使います。
ちゃっかりスルーしていたのですが、今回はこれを使わず、以下の計算で済ませていました。
![]() |
---|
つまり、ピクセル単位の視線の向きではなく、アクター単位の視線の向きにしていたということです。
なぜかというと……。
ピクセル単位(正確にはグリッド単位)の視線の向きで計算しようとすると、割れたガラスのように像がずれた見た目になったからです。
今回の場合視線の向きは32x32通りまで対応していますが、これだと情報量が全然足りないのが原因と思われます。
かと言ってこれ以上テクスチャサイズを大きくするのもなあ……と思ったので、ピクセル単位の視線の向きを使うのは諦めた次第です。
ただ、この判断により、ここで提案しているGridInteriorMappingは、InteriorMappingというよりImpostorに近いものになってしまいました。
Impostorはビルボードに対するアクター単位の視線ベクトルを入力として、複数の絵柄を含んだテクスチャから1つの絵柄を選択して出力する手法です。
字面だけ見ると、今回やってることとそう変わりないですよね。
そのうえで、GridInteriorMappingがImpostorより綺麗な見た目かというと、正直そうでもないのも残念なところです。
後編 - 延長戦
そんなわけで実用に足る「部屋の形を自由に決められるInteriorMapping」の開発は挫折で終わろうとしていました。
ですが、最後の最後の思い付きが、事態を好転させました。
その思い付きとは、 「最終出力をUVとするのを諦めれば、バイリニア補間のゴリ押しで品質を上げられるのでは?」 というものです。
新たなアプローチ
前編で見てきたGridInteriorMappingは部屋メッシュのUVを最終出力としていました。
UV座標は、隣り合うピクセルが不連続な値である場合があります。
そのため、一応バイリニア補間を適用してはいたものの、UV境界でわざわざ補間をキャンセルしたりしていたわけです。
InteriorMappingの計算において、UV座標値の前段階で求まるのは、「ピクセルから仮想的な壁(衝突点)までの距離」です。
こちらであれば、UV座標と異なり、隣り合うピクセルでは(例外はあるにせよ)連続値になるケースが多いはず、と考えました。
であれば、バイリニア補間を真っ当に適用させてあげれば、変な境界線が出たりしない像になることが期待できそうです。
つまり、GIMテクスチャに書き込まれる内容を、「ピクセルから衝突点までの距離」 になるように作り変える、というアプローチです。
Re: HoudiniでGIMテクスチャを出力する
改めてSOPネットワークを見ていきます。
基本的な流れは変わっていません。
違うのは、UVアトリビュートをCdに入れていた部分です。
Ray SOPによる移動の前後位置の距離 を正規化して入れるように変更しています。
あとは以前同様にCOPで書き出せばOKです。
この時、後々のバイリニア補間にかかる負荷を減らすための工夫として、前編のセルフシャドウ用テクスチャでやっていたのと同様にRGBAにずらした値が書き込まれるようにしておきます。
また、UV座標を使えなくなったので、この時点でオブジェクトスペース法線マップとセルフシャドウ用テクスチャは不要になります。
![]() |
---|
Re: UEマテリアルで実装する
再びマテリアルエディタに移動します。
視線方向のベクトルをUVオフセット量に変換し、マスを選択する部分は変更ありません。
テクスチャサンプリングとバイリニア補間の処理は以下のように変えました。
![]() |
---|
実は、前編でもやっていたグリッド毎のバイリニア補間をするだけでなく、もう一つ別のバイリニア補間もするようにしています。
それが赤で示した部分。
やっていることとしては、隣り合うグリッド(ピクセル位置)でなく隣り合うマス(視線ベクトルが微妙にずれている時の結果同士) でのバイリニア補間です。
前編のときに、「『ピクセル単位の視線の向き』を入力に使おうとすると、像がずれたような見た目になってしまったので諦めた、結果Impostorのような処理になってしまった」、というお話をしました。
この箇所の処理は、上記の問題について、UV座標ではなくピクセルからの距離を求めるようにしたことで解決できないか、という意図で入れたものです。
実際これはまあまあうまくいっており、視線を動かしたときのパカつきを解消できました。
計算的にも純粋なInteriorMappingに近い計算になったので、一安心という気持ちです。
テクスチャサンプリングによって各ピクセル(グリッド)から衝突点までの距離が求まったので、次はここから3次元座標を計算し、さらにそこからカメラ深度と法線も計算します。
![]() |
---|
そして、ここまでの処理を、さらにもう一度、座標を少しずらして実行しています。
![]() |
---|
こちらはセルフシャドウ簡易的に計算するためのものです。
ちょっとずらしたカメラ深度を計算し、差を取ることで影っぽいものが描画できる、という発想です。
UV座標を得られなくなったことでセルフシャドウを事前計算できなくなったので、このような形で対応しました。
(いろんなやり方を検討しましたがこれが一番バランスのいい手法だと判断しています。)
延長戦 結果
そんなこんなでできた最終的な見た目がこちらです!
まだ低解像度感があり、近距離で見てしまうと若干キツイですが、諸問題が改善され、だいぶ印象よくなったのではないでしょうか?
個人的にはこれならギリギリ実用レベル、かなと思っています。
![]() |
---|
各手法の比較
前編と後編に亘って、GridInteriorMappingの紹介をしてきました。
最後に、それぞれの手法を比較してみたいと思います。
手法 | 使用テクスチャ | テクスチャサンプリング回数 | メリット | デメリット |
---|---|---|---|---|
Grid Interior Mapping(前編) | 2kテクスチャ2枚、~1kテクスチャ2枚 | 8回 | 事前にUV展開しておいたUV座標を使用してテクスチャを貼れる。入り組んだ形状を表現できる。 | 汚い。品質が悪い。テクスチャサイズの問題で高解像度を要するものは表現できない。 |
Grid Interior Mapping(後編) | 2kテクスチャ1枚、~1kテクスチャ1枚 | 9回 | 入り組んだ形状を表現できる。 | POMに比べると品質が良くない。テクスチャサイズの問題で高解像度を要するものは表現できない。 |
Parallax Occlusion Mapping | 256x256テクスチャ2枚~ | 多い。Step数に依存?(ちゃんと調べてないので間違っていたらすみません。。。) | Step数次第だが品質が良い。ハイトマップとノーマルマップだけで実現できるのでリソース作成が簡単。 | 重いらしい。ハイトマップで描かれる形状しか表現できない。 |
GridInteriorMappingがPOMに勝っている点としては、ハイトマップでは表現できないような入り組んだ形状にも対応できる点ですね。
どうでしょう、後編のGridInteriorMappingならPOMとも少しは戦えるんですかね?
そうなってくると気になるのは処理負荷ですよね。
ちゃんとした負荷計測はできていませんが、とりあえずシェーダー複雑度だけでもと思い確認してみました。
(もちろん何の参考にもならない可能性も頭に入れつつですが。)
![]() ![]() |
---|
デカールの状態だとシェーダ複雑度のビューに写ってくれなかったので、MaterialDomainをSurface、BlendModeをMaskedにして普通のPlaneに貼った状態で比較しています。 |
残念ながらPOMよりは結構複雑度が高いようですね。。。
(というかPOMが思いのほか複雑じゃない)
MF_DirectionToUvGrid
が結構複雑な処理ということはわかっているので、ここをシンプルに出来たら改善するかもしれません。
補足
本文中ではSOPネットワークやマテリアルグラフの一部しかお見せできなかったので、全体図も載せておきます。
出来のいい後編バージョンのみですがご容赦ください。
SOPネットワーク
COPネットワーク(GIMテクスチャの部分のみ)
マテリアルグラフ
まとめ
InteriorMappingを元にした新しいフェイク表現として、GridInteriorMappingというものを作ってみました。
一時は挫折しかけましたが、頑張った甲斐あって、ギリギリ実用圏内?な見た目なものはできました。
よろしければ皆さんも、本手法を試してみていただければ幸いです。
ここまでお読みいただきありがとうございました。