土屋つかさの技術ブログは今か無しか

土屋つかさが主にプログラミングについて語るブログです。

Unityで投影テクスチャマッピングを実装する際に抑えておくべき基礎情報

 ブログで告知する余裕がなかったのですが、10月22日に開催された技術書典4では既刊のUnityシェーダー本を過去最多部数の再販と、新刊コピー本の「ユニティちゃんトゥーンシェーダーのコードをひたすら読む本」を持ち込んだ所、技術書典始まって以来の晴天に恵まれたこともあって無事完売しました。

 ユニティちゃんシェーダー本はPDF版がBOOTHにて頒布中です。近いうちにちゃんと記事起こします。ちなみに紙版を再販する予定はありません。する時は2.0.4対応にして、オフセットで作る予定です。

https://s-games.booth.pm/items/662396

投影テクスチャマッピング

 突然ですが現在シェーダー本2の執筆に入っています。その過程で投影テクスチャマッピングについて調べてまして、その際に得た知見を共有しておきます。

投影テクスチャマッピングとはなにか

 投影テクスチャマッピング(projection texture mapping. 射影テクスチャマッピングとも言いますが、どちらもprojectionの訳語です)とは、いわゆるプロジェクションマッピングのように、テクスチャマップを任意のモデル形状の上に貼り付けるように描画する手法のことです。

 投影テクスチャマッピングは、AAAタイトルではポピュラーに使われている手法です。よく見るのは壁に弾痕を刻んだり、雪原に足跡の軌跡を描くなどのシチュエーションです。背景BGモデルが複雑な凹凸を描いている時、単に弾痕のテクスチャを新規に作成して配置しただけでは、凹凸に沿わせることが出来ません。ヒットした場所の凹凸を解析して弾痕プリミティブを変形させるのも現実的ではないです。

 逆に、背景BGモデルのテクスチャに弾痕なり足跡なりを重ねてしまうという事も考えられますが、これもうまくいきません。というのも、テクスチャはプリミティブに対してuv座標を介してバインドされていますが、この時テクスチャは自由にスケールできるので、弾痕をどの大きさで貼り付ければいいのか判断できません。また、背景モデルは一般に広大な空間を描くことになるので、相対的にテクスチャ解像度が低くなります。弾痕や足跡はカメラに寄りがちなので、解像度が荒いとユーザー体験に結びつかない可能性があります。

 さらに言えば、プリミティブが隣り合っている場所がテクスチャ上でも隣り合っているかは、uvマッピングに依存していて確実ではありません。仮に隣り合っていたとしても、プリミティブ同士が角度を持っていれば、単に弾痕を上書きしただけでは求める画像にはなりません。

 このように、広大かつ複雑にUV展開されている背景BGモデルには、高精度な画像を動的に合成することができません。対処する方法は幾つかあるのですが、その一つが今回解説する「投影テクスチャマッピング」という手法です。

注意

 以下投影テクスチャマッピングの仕組みの説明が続きますが、注意して欲しいのは、シェーダーを実行するのは「投影する側のテクスチャマップ」ではなく「投影される側のプリミティブ」だという点です(そもそも「投影する側のテクスチャマップ」はパラメータに過ぎず、オブジェクトですらありません)。土屋が調べている際、なぜかこれをごっちゃにしてしまい、かなり時間を浪費してしまったので、ここでエクスキューズを出しておきます。

投影テクスチャマッピングのメカニズム

 プリミティブ(ポリゴンの最小単位。3Dプログラミングではほぼ3角形のことです)にテクスチャを描画する方法は、それが通常のテクスチャだろうが投影テクスチャだろうが違いはありません。即ち「uv座標を元にテクスチャルックアップ関数を実行し、テクスチャから対応するカラー値をサンプリングする」です。

 uv座標というのは画像の左上を原点とした2次元座標です。通常のテクスチャの場合、頂点ごとに対応するuv座標がモデルデータに同梱されているので、それをそのまま使えます。しかし、投影テクスチャマッピングの場合、プリミティブに投影される位置は動的に決まるため、uv座標を計算して求めなければなりません。

 投影テクスチャのuv座標を取得するには、専用の射影変換が必要になります。ここでの「射影変換」というのは「プロジェクション変換行列を使って、3次元座標を2次元座標に変換すること」だと思ってください。

 以下の図を元にして、この射影変換について説明します。

 説明の都合上、まず通常の射影変換について解説してから、投影テクスチャの射影変換について説明します。

 通常、ローカル空間に存在するプリミティブは、ワールド変換行列によってワールド空間に配置され、続いてビュー変換行列でビュー空間、プロジェクション変換行列でプロジェクション空間(=射影空間)に配置されます。このプロジェクション空間の時のxy座標が、スクリーン上(即ち2次元)での位置になります。

 この処理は言い換えると「ある視点(ここではカメラ)から見た時、矩形(ここではスクリーン)上のどの位置にプリミティブの頂点が存在するか」を計算していることになります。ということは、同じ方法を使って、仮想の視点と矩形を用意して射影変換を行い、算出された2次元座標を投影テクスチャのuv座標としてサンプリングすれば、あたかもそのテクスチャがプリミティブに投影されているかのように描画することができます。

 これが、投影テクスチャマッピングの仕組みです。「プリミティブの頂点座標スクリーン上の対応する座標に変換する」という通常処理を、uv座標を計算する為にテクスチャマップに対して行う所がミソです。それほど計算コストも必要とせずにインパクトのある描画が可能になる、とても巧妙な手法だと感じます(ただ、直感的に理解するのがなかなか大変だと思います。土屋は相当手こずりました……)。

投影テクスチャマッピングに必要な変換行列について

投影テクスチャマッピングを行うには、以下の情報が必要になります。今回は、これらの値の算出方法については解説しません(ごめんなさい><)

①オブジェクトのローカル空間座標
②ワールド空間への変換行列
③仮想の視点を原点としたビュー空間への変換行列
④仮想のカメラによるプロジェクション空間への変換行列(射影変換)
⑤uv座標系変換行列

 ①②は通常と同じ、③④も通常の視点/カメラではなく仮想の視点/仮想のカメラを使っている以外は同じです。⑤だけ特殊なので解説します。

 射影変換を終えて計算された座標は、スクリーンの中心を原点としたプロジェクション空間座標系の値です。一方、サンプリング先であるテクスチャマップの座標は、左上を原点としたuv座標系の値です。これを一致させないと、正しいカラー値がサンプリング出来ません。

 この変換処理は複数の計算を伴うのですが、以下のような変換行列で乗算すると一発で変換できます。

float4x4 matrix = {
0.5f, 0.0f, 0.0f, 0.0f,
0.0f, -0.5f, 0.0f, 0.0f
0.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.0f, 1.0f };

 複数の変換行列は事前に合成が可能です。シェーダー上での演算量を軽減するために、CPU側で②から⑤までを一個の変換行列にまとめておくのが一般的です。

サンプルコード(ないですすみません)

 本当ならここでサンプルコードを提示すべきなのですが、ちょっと今時間がないので、ひとまず他サイトのサンプルコードを示しておきます。

Cg Programming/Unity/Projectors
https://en.wikibooks.org/wiki/Cg_Programming/Unity/Projectors

 このサンプルは、Projectorコンポーネント内での使用を想定しています。Projectorコンポーネントは、先述の画像で言う「仮想の視点」を用意する物で、シェーダーをセットすることで簡単に投影テクスチャマッピングを実現できます。

 Projectorコンポーネントは簡易カメラのような仕組みを持っていて、視錐台で投影範囲を指定します。投影範囲に含まれるオブジェクト(恐らく、レイを飛ばして判定してると思われる)は描画処理がフックされProjectorに設定されたシェーダーが追加実行されます(これも挙動を想像してるだけで、実際にどうしてるかはわかりません)。

 Projectorコンポーネントにシェーダーをバインドすると、"unity_Projector"変数に射影変換行列が設定されます。uv座標系への変換まで済んでいる物なので(恐らく。想像が多くてすみません。ProjectorコンポーネントってC++で書いてあるみたいで内部実装が読めないんですよね……)、これをmulすれば必要なuv座標を取得できます。

 ちなみに、サイトのサンプルコードでは"_Projector"となっていますが、5.6から名称が"unity_Projector"に変わっています。旧名を使ったシェーダーをコンパイルすると自動的に名前が書き換わるので注意してください(この挙動もイマイチな気もするんですが。オプションで抑制できたりしないのかな)

tex2Dproj関数の役割

 サンプルコードを見ると、投影テクスチャからのサンプリングするロジックが以下のようになっています。

return tex2D(_ShadowTex, input.posProj.xy / input.posProj.w);
// alternatively: return tex2Dproj(_ShadowTex, input.posProj);

 "alternatively"は「あるいは」の意味で、つまりどちらも同じ処理になります。tex2Dprojは、テクスチャをサンプリングする前に、xy要素をw要素で除算する物なので、その計算を自前でやれば、tex2Dで代用できるわけです。

 さて、投影テクスチャマッピングについて解説している記事を見ると、多くがtex2Dprojを使っています。というのも、w除算が必要になるケースが非常に限られていて、それこそ投影テクスチャマッピング処理以外では使用する機会が少ないためです(更に言えば、前述した通り、tex2Dprojはtex2Dに書き換えが可能です)。ではtex2Dprojはどんな役割を持っているのでしょうか? これについて補足解説しておきます。

 座標変換は、元の座標float4.xyzwと、座標変換行列float4x4とのmul乗算によって行います。この時、float4.wは"1"が入っていて、mulの後も"1"が維持されます。なぜこうなっているかというと、移動/回転/拡大縮小を1回の演算で行うには4行4列の変換行列が必要になり、その為には元の座標にも4要素目が必要だからです。変換してもwは変化しないので丁度良いのですね。

 ところが、ビュー空間座標からプロジェクション空間座標に変換する「射影変換」だけは話が別で、この時はwの値が変わります。また、射影変換後のxyzはwの値に依存する結果になっており、そのままでは使用できません。これはxyzw全ての要素をwで除算すると正しい値になります。この時wは1になります。

 今、「正しい値になります」と書いてしまいましたが、wで除算さえすれば必要な値になるのだから、除算するのはその値が本当に必要になった時で構わないことになります。そこで、GPUではw除算する前の4要素と除算した後の4要素は同じ値であると見なし、透過的に処理することにしています。これを「同次座標」と言います。また、w除算した後のxyzwを指して「正規化デバイス座標」と呼びます。

 さて、シェーダーコードを書く時は「同次座標」や「正規化デバイス座標」という概念を意識する時がほとんどありません(土屋もこの記事を書くまでたいして意識していませんでした)。これは、w除算が必要になるシーンが、ユーザー処理側ではほとんど発生しないためです。以下で代表的な座標計算を幾つか確認してみましょう。

①頂点シェーダー関数から出力されるSV_POSITION

 頂点シェーダーで行った頂点の座標変換には射影変換が含まれるのでwが変化しています。しかし、SV_POSITIONはGPU側でw除算を行う(実際に行っているのかは知らないけど)ので、意識する必要がありません。また、SV_POSITIONの情報はフラグメントシェーダーには渡されません。

②通常のテクスチャルックアップ

 tex2D関数はそもそも第2要素にfloat2しか受けとりません。TEXCOORDnはfloat4ですが、tex2Dに渡すのはxyのみなわけです。逆に、この特性を利用して、TEXCOORDnのzw要素にカスタムのパラメーターを詰め込む手法もあるようです。

③法線ベクトルなど、ワールド空間座標に変換した値

 頂点シェーダーからTEXCOORDnを介してフラグメントシェーダーに送るパラメータの一つに法線ベクトルがあります。これは頂点シェーダーでワールド空間座標に変換してからフラグメントシェーダーに渡す事が多いのですが、この場合は射影変換を行っていないので、wは1のまま。なのでw除算は必要ありません。

④投影テクスチャマッピング

 ①〜③で見てきたように、ほとんどの場合において、ユーザー側でのw除算は必要ありません。しかし、投影テクスチャマッピングの場合には、仮想視点での射影変換をしているので、w除算が必要になります。その時に使えるのがtex2Dprojなわけです。内部では透過処理されるので、手動でw除算をしてtex2Dを使うようり、tex2Dprojを直接使った方が、処理速度が速いのではないでしょうか(想像です。検証したわけではありません)。

おまけ

 余談ですが、投影テクスチャマッピングでは、通常のシェーダー処理で使える手法は基本的に全部使えるので、法線マップを使って投影した箇所に対して疑似凹凸を表現することもできます。また、最近では「法線マップの疑似凹凸が現実の凹凸だった場合に、視線の角度からは見えない筈の部分の描写を詰める」という技術が使用できる(これを「視差遮蔽マッピング (Parallax Occulusion Mapping)」と言います)ので、弾痕の表現は更にリアルになっています(弾痕以外に使い道無いのかよと思うけど、あんまりないかなあ……?)。

おわりに

 いやー長かった!w ネットで投影テクスチャマッピングの事を調べると、どうもふんわりした情報が多かったり(ちゃんと書くとこれだけの分量が必要になるし、この記事だって射影変換の作り方は省略してるし!)、「Projectorコンポーネントを使いなさい」以上の情報がなかったりして調べるのが大変でした。

 本来はシェーダー本2に収録する内容になるんですが、うーん、どうしようかな……。これだけでコピー本一冊作れそうな気がしてきたな……w