本ページは次のページを元に作成いたしました:https://www.sidefx.com/tutorials/lens-shaders-for-gamedev/
イントロダクション
こんにちは! 私の名前はPaul Ambrosiussenです。効果的なアートパイプラインをサポートし、一緒に作業している世界中のアーティストのツールとワークフローの品質を最適化するためのツール開発に焦点を当てた3Dテクニカルアーティストです。オランダのブレダにあるNHTV応用科学大学で国際ゲームアーキテクチャとデザインを学びました。在学中、私ではない誰かが更に素晴らしく、素早く、より柔軟な方法で驚くべきものをつくるのを手助けすることに情熱を感じました。Houdiniを使用してSideFXでの現在の職はまさに在学中に臨んでいたこと– チュートリアルの作成、ライブ講義の提供、イベントへの出席、カスタムワークショップの顧客への提示などが含む-です。
この記事では、レンズシェーダーとレンズシェーダーを使用してできることをいくつかご紹介していきたいと思います。 この記事の中で、コードのすべてを詳細に説明するわけではありませんが、ご自身で検証できる十分な開始地点までもっていきたいと思っています。レンズシェーダはかなり複雑になりますが、楽しいこともあります。 Houdiniのレンズシェーダには十分なドキュメントがないため、理解を深めるのに役立つと思います。 この記事で使用されている主な例は、ゲーム開発を中心にしていますが、レンダリングを行っているすべての人に役に立つでしょう。
ゲーム開発向けレンズシェーダーの意味は?
レンズシェーダーの仕組みについて詳しく知る前に、「なぜGameDevのレンズシェーダーが必要なのですか?」というのは重要な質問です。それは公正な質問であり、良い答えが必要です。 「方法」や「意味」を介する前に、いくつか名前を付けます。
360コンテンツのレンダリング
レンズシェーダについて話すときにまず思い浮かぶのは、360コンテンツです。 このタイプのコンテンツをレンダリングするには、特殊なタイプのレンズが必要です。通常の画像を歪ませるだけではFacebookの360度動画やSkyboxなどで使用することはできません。シーン全体を視覚化するために、カメラの周りのすべての面をレンダリングする必要があります。これは、crossまたは latlongの手法で可能となります。 以下の例では、latlong手法を試していきます。
これは、極座標を使用してレンダリングされたlatlong画像です。 使用されたシーンは、 "ruslans3d"によって作成されsketchfabからダウンロードしました。 ここから無料でダウンロードできます:https://sketchfab.com/models/311d052a9f034ba8bce55a1a8296b6f9
Impostorテクスチャシートのレンダリング
その他の応用として、Impostorに使用されるテクスチャシートのレンダリングプロセスを最適化する方法に関するアプローチです。 この記事の例として、利点と、Impostorsの使用に伴うものについて説明します。
Impostor Texture Sheetの例です。 この記事のHemi-Octahedralの例は、上記のテクスチャをレンダリングするために使用されます。
テクスチャ/ライトマップベイク処理
レンズシェーダはテクスチャやライトマップのベイク処理にも使用できます。 OdForceのEetuには、レンズシェイダーを使用してシーンからライトマップをベイクする方法についての絶妙なアプローチが含まれています。
上記はEetuが提供した画像の一部です。 このレンズシェーダーの使用についてはこの記事では扱いませんが、興味のある方はこちらの記事をチェックすることをおすすめします:https://forums.odforce.net/topic/8471-eetus-lab/?do=findComment&comment=132784
VRのレンダリング
また、VRシェーダを使用してVR互換のコンテンツをレンダリングすることもできます。 これは15.5以降のHoudiniにデフォルトで組み込まれているものです。つまり、この記事ではご説明しません。 しかし、この作品の仕組みを学ぶことに興味があるなら、Matt Estelaのによるcgwikiの実験について読むことをお勧めします: http://www.tokeru.com/cgwiki/?title=HoudiniOculusWip
HoudiniでVR Camを使用する方法については、こちらのチュートリアルをご覧ください:https://www.sidefx.com/tutorials/vr-lens-camera/
レンズシェーダーについて
レンズシェーダが標準バーチャルカメラレンズに与える影響を理解するためには、まず、レンダリングの各ピクセルごとにどのような処理が行われているかを理解する必要があります。 この説明は簡潔にするため簡略化されていますので、ご理解いただければ幸いです。
それでは、まず、遠近法モードに設定されたカメラの近景および遠景のクリッピングプレーンを見てみましょう。上記の画像に示されているように、ニアクリッピングプレーン(近)は緑でマークされ、ファークリッピングプレーン(遠)は赤でマークされています。反射や他のすべての楽しいものを除外する最も単純化されたケースでは、2つのクリッピングプレーンに挟まれたボリューム内のすべてが、レンダリングで潜在的に表示される可能性があります。 レンズシェーダーは、そのボリュームの見え方に影響を与え、カメラの光線がどのようにトレースするかを変更しようとします。
私たちがレンダリングしたい解像度(例えば、1920x1080px)で空白のキャンバスを撮影し、それをカメラのニアクリッピングプレーンと位置合わせしたとします。上のイメージで視覚化されているものです。画像上のピクセルに値を与えるには、それらのピクセル位置からのレイを特定の方向にキャストする必要があります。その部分がレンズシェーダーが定義することです:1.現在のピクセルのどこからレイをキャストしますか? 2.現在のピクセルのレイがどの方向に向いていますか?
定義:
レンズシェーダは、スクリーン座標から一次レイを計算する役割を果たし、遠近法や正射投影としてモデル化できない新しい種類のカメラ投影を定義する柔軟な方法です。
これらの2つのものは、レンズシェーダの変数で定義されています。
vector P – カメラ空間におけるピクセルのレイ原点
vector I – カメラ空間におけるピクセルのレイ方向
この定義は、カメラ空間で定義されている2つを示しています。カメラをどこに移動するか、カメラをどこに回転させるかにかかわらず、カメラとの相対的な関係は常に同じです。これらの2つのベクトルは、カメラのように同じ変換(トランスフォーム)を取得します。もう少々お待ちください。まもなく実例をご紹介します。
機能の理解(CVEX)
CVEXオペレーターの作成
レンズシェーダを使用できるようにするには、特殊なタイプのオペレーターを作成する必要があります。 HDAのCodeタブにCVEXを含むことができるオペレーターが必要です。 CVEXは、コンテキスト外で実行されるVEXコードです。 CVEXにはあらかじめ定義された変数がありません。。これは、Houdiniが特定の引数(通常はポイントまたはポイント上のデータに対応)で呼び出すプログラムなので、事前定義済みの変数はありません。 これは、ジオメトリ操作関数などのコンテキスト固有の関数をスクリプトで使用できないことも意味します。
上の図は、CVEXで書かれたレンズシェーダを含むオペレーターの作成方法を示しています。 FileメニューのNew Assetを選択し、オペレータスタイルをVEX Typeに設定します。 これにより、ネットワークタイプをCVEXオペレータに設定できます。 “Contextless VEX”です。
オペレーターを作成したら、上記のウィンドウをポップアップする必要があります。 多くのHDAタイプでは、Codeタブが通常無効になっていることにお気づきかもしれません。 [Code]タブは、スクリプトベースのシェーダ、vops、PythonベースのHDAなど、該当するHDAでのみ使用できます。Houdiniはすでにレンズシェーダー機能の本体を作成しています。これが私たちのロジックを追加する場所です。 CVEX関数の名前は重要ではありません。 cvexの戻り値の型を持つ関数を呼び出します。
パラメーター
レンズシェーダは、デフォルトで以下のパラメータと出力を持つことができます:
- float x – -1から1の範囲のXスクリーン座標
- float y – -1から1の範囲のYスクリーン座標
- float Time – サンプルタイム
- float dofx – X被写界深度サンプル値
- float dofy – Y被写界深度サンプル値
- float aspect – 画像アスペクト比(x / y)
- export vector P – カメラ空間内のレイ原点
- export vector I – カメラ空間におけるレイ方向
- export int valid – サンプルが測定に有効かどうか
画像の外側のサンプルを生成する必要がある場合は、レンズシェーダが-1から1の範囲外のxとy値を処理できる必要があります。PとIの出力は、前述のようにカメラ変換(トランスフォーム)を無視して、カメラ空間で作成する必要があります。
レンズシェーダを見ると、次のようになります。
aspect変数を除き、入力値はすべて0に初期化されています。 Houdiniはレンダリング時に値を設定するので、実際には何を設定するかは問題ではありません。Xは、現在処理されているピクセルが画像上のどの位置にあるかに応じて、-1と1の間の値に設定されます。左端の境界ピクセルの場合は-1、右端の境界ピクセルの場合は1。 Yも同じです。上部は1、下部は-1。 これはレンダリングが技術的に正方形であることを意味することに注意してください。(後ほどそれを修正する方法がわかります)ただし、custom_variableのカスタムパラメータと変数を定義することはできます。 インターフェイスを構築する方法の詳細については、こちらをご確認ください: http://www.sidefx.com/docs/houdini/vex/pragmas.html
ここまで来ましたら、独自のカスタムロジックを構築することができます。
カメラへのレンズシェーダーの割り当て
カスタムレンズシェイダーを使用するカメラを取得するのはとても簡単です。Camera Projection Modeを "Lens Shader"に設定し、作成したLens Shaderを対応するパラメータフィールドにリンクするだけです。
Lens Shader 実例 + Code
最も簡単な例で始めます:正射投影。 Houdiniのカメラにはこのモードが搭載されており、実行するためによいサンプルとなります。 プログラマにとって "Hello World"に相当します。
Orthographic(正射投影)
Orthographicに設定されたカメラのCamera Frustumを見ると、近景と遠景のクリッピングプレーンのサイズがまったく同じであることがわかります。これは、カメラの被写体の距離にかかわらず、常にレンダリングで同じサイズに見えることを意味します。つまり、レンズシェーダに関して、基本的に、ピクセルのレイ原点が最初の位置(X; Y; 0)になることを意味します。 Rayの方向については、(0、0、1)を使用することができます。
グリッドポイント上でピクセル位置とレイの方向を視覚化すると、次のようになります。
コードは次のようになります:
{
######## Orthographic Projection ########
P = set(x, y, 0);
I = set(0,0,1);
}
レンダリングすると、対象を正射投影モードでレンダリングすることができますが、ブタの頭はY方向に少し潰れているようです。これは、出力レンダリングの縦横比を考慮していないためです。縦横比16:9の1920x1080の画像をレンダリングしています。Pを修正する必要があることを意味します。 これは、Pのy成分をアスペクト比で単純に除算することによって行うことができます。
コードは次のようになります:
{ ######## Orthographic Projection ######## P = set(x, y / aspect, 0); I = set(0,0,1); }
この結果は更に正確に見えます。 残りの1つは、ズーム機能を実装して、レンダリングでの被写体の大きさを変更できることです。 考えるより簡単です。ズームしたい分Pをスケールする必要があるだけだからです。 これはコードで行う必要があります。カメラを近づけたり離したりすることは、正射投影をレンダリングするときに被写体のサイズに影響しないためです。
float zoom = 1; // User Variable { ######## Orthographic Projection ######## P = set(x / zoom, y / (zoom * aspect), 0); I = set(0,0,1); }
Perspective(パースペクティブ)
パースペクティブに設定されたCamera Fistumを見ると、ファークリッピングプレーンがニアクリッピングプレーンよりもはるかに大きいことがわかります。 つまり、オブジェクトが近くのクリッピング平面に近いほど、レンダリングには大きく写ります。 遠近法レンダリングでは、ちょうどピンホールカメラのように、Pを0に設定することができます。 レイ方向については、(0、0、1 + lens distortion)を使うことができます。
グリッドポイント上でピクセル位置とレイ方向を視覚化すると、次のようになります。
コードは以下になります。
{ ######## Perspective ######## P = set(0, 0, 0); I = set(x / zoom, y / (zoom * aspect), 1 + (1 - 2*(x * x + y * y)) * curvature); }
ブタはそれほど平らになっていないことがわかります。 それには実際の深度があります。耳が、正射投影よりも少し小さいのは、カメラとの距離が実際に出力に影響を与えるためです。zoomとcurvatureのパラメータとともに、カメラを近づけたり遠ざけたりして、この効果を増減できます。
Polar (パノラマ)
次の例は、極座標投影レンズです。 基本的には、平らなグリッドの原点位置を取ることになります。((x; y) from -1 to 1) を、すべてをPの{0、0、0} に移します。しかし、必要とするレンズを得るためには、私たちの後ろにある極座標投影を得るために、グリッドの全ピクセルは外向きのI(レイ方向)ポイントを取得する必要があります。これは本質的に私たちがカメラの周りのシーンのすべてをレンダリングすることを可能にし、これをlat-long画像と呼んでいます。 これを達成するために、以下の球面/極座標の計算を使用します。
{ ######## Polar Projection ######## float xa = -PI*x; float ya = (0.5*PI)*y; float sx = sin(xa); float cx = cos(xa); float sy = sin(ya); float cy = cos(ya); P = set(0, 0, 0); I = set(cx*cy, sy, sx*cy); }
これは、極座標投影コードをテストするために使用するシーンです。 ブタがカメラの前にあり、色のついた球がカメラの周りに置かれていることがわかります。上下を含むカメラのあらゆる側面に、オブジェクトが置いてあります。 ロジックが求めている通りに機能するならば、カメラがどこに位置しているかにかかわらず、カメラの周りのすべてが表示できるはずです。
上にある1つめの画像は、作成したコードを使ってレンダリングしたlat-long画像です。 上で述べたように、実際にカメラの周りのシーン全体を見ることができます。 しかし、レンズの極部分でレンダリングされた領域は画像に多少なり歪みがありますが、極座標で作成されたUVを含む球面上にテクスチャとして適用すると、球面上であまり目立たなくなります。極座標投影UVを持つ球に適用されたレンダリング結果は、上の画像で見ることができます。 このレンズは、統合したいシーンでSkydomesや遠くのジオメトリをレンダリングするときには非常に便利です。
Cylindrical (カーブ)
次の例はCylindericalレンズです。 基本的にはフラットグリッドのレイ原点を取得します。 ((x; y) from -1 to 1) 、シリンダに似たような感じで曲げます。cylinder_amountを使用して、シリンダになる量を制御することができます。cylinder_amountが0の場合、X軸上のすべてのポイントが一緒に押しつぶされ、1.0の場合は完全なシリンダが得られます。0.0と1.0の間の値は、0.5がシリンダの半分であるなど、間の結果を全てもたらします。 これは基本的にカメラの周りのすべてを正射投影でレンダリングすることを可能にします。 以下のコードでは、XおよびYコンポーネントは0に設定されていますが、上の画像は視覚化目的のために設定されていません。
コードは以下になります。
float cylinder_amount = 1; // User Variable { ######## Cylinder Projection ######## float offset = 2 * PI * cylinder_amount; float HalfPI = 0.5*PI; float theta = fit(x, -1, 1, 0, offset); P = set(0, y, 0); I = set(cos(theta - (offset*0.5) + HalfPI), 0, sin(theta - (offset*0.5) + HalfPI)); }
こちらがシーンです。 pig headがカメラの前にあり、カメラの周りに色のついた球があります。pig headがカメラの前にあり、カメラの周りに色のついた球があります。ロジックが求めるように動作すれば、cylinder_amountが1に設定されていると、カメラの全方位にあるすべてのものを見ることができるはずです(上および下のものを除く)
上記のgifでは、cylinder_amountスライダを0から1にゆっくりとスライドさせるときのレンダリングの様子を見ることができます。値が1に近づくほど、より多くを見ることができます。 最終的には、カメラの後ろの紫色の球が完全に見えるようになります。
Texture Atlas (Hemi-Octahedron)
次の例は、この記事で取り上げる最も複雑なものです。 また、実際には存在し得ないレンズでもあります。なぜなら、レンズを細かく切断し、平坦なレンズを非常に特殊な方法で整列させる必要があるからです。このレンズは基本的にN ^ 2の位置からレンダリングされた画像からなるテクスチャアトラスを作成します。 このようなテクスチャアトラスは、通常、「Impostor」と呼ばれ、ゲームで使用されます。 基本的には、距離のあるオブジェクトをレンダリングするための最適化です。 詳細はこちら:https://www.sidefx.com/tutorials/generating-impostor-textures/
テクスチャアトラスを作成するには、いくつかの方法があります。 私は上記のイメージに2つの方法を示しています。最初の方法(画像左)は、被写体の周りにN ^ 2カメラを作成し、カメラごとに1つの画像をレンダリングします。 合計25台のカメラを使用する場合は、25枚の画像を出力します。 しかし、テクスチャアトラスは単一のイメージである必要があるため、それらをまとめて単一のテクスチャにする必要があります。ここが方法2(レンズシェーダー。)が優っている部分です。レンズシェーダでは、基本的に、ピクセルの各シングルレイがどこから来ているのかを再マップすることができます。したがって、レンズを「切り取って」それらのカメラが配置される場所に移動させることができます。これは、文字通り、どのような出力をリアルタイムで必要とするかについての設定を変更できることを意味します。 下のgifで見ることができます。
上記は出力テクスチャです。 ゲームテクスチャには、1:1のアスペクト比と2のn乗解像度を使用することをお勧めします。
{ ######## Hemi-Octahedron ######## float xy_size = 6; float camera_width = 0.2; float camera_zoom = 10; int nFrames = int(xy_size * xy_size); float fx = fit(x, -1, 1, 0, 1) * float(xy_size); float fy = (1 - fit(y, -1, 1, 1, 0)) * float(xy_size); float floorx = clamp(float(xy_size) - trunc(fx) - 1.0, 0, xy_size-1); float floory = clamp(float(xy_size) - trunc(fy) - 1.0, 0, xy_size-1); float fracx = (frac(fx) * 2.0 - 1) * camera_width; float fracy = (frac(fy) * 2.0 - 1) * camera_width; vector ImpostorVector(vector2 Coord){ vector2 CoordModify = Coord; CoordModify.x *= 2.0; CoordModify.x -= 1.0; CoordModify.y *= 2.0; CoordModify.y -= 1.0; vector ImpostorRenderVector; vector2 HemiOct = set(CoordModify.x + CoordModify.y, CoordModify.x - CoordModify.y); HemiOct.x *= 0.5; HemiOct.y *= 0.5; ImpostorRenderVector = set(HemiOct.x, HemiOct.y, 0); ImpostorRenderVector.z = 1.0 - dot(abs(HemiOct), {1.0, 1.0}); return normalize(ImpostorRenderVector); } vector2 Input = set(float(int(floory)) / float(xy_size-1), float(int(floorx)) / float(xy_size-1)); vector P = swizzle(ImpostorVector(Input), 0, 2, 1); vector N = normalize(P); vector XDir = normalize(cross(set(0,1,0), N)); vector2 Coord = set(floorx, floory); if (xy_size%2==1) { if(Coord == set(ceil(float(xy_size-1)/2.0), ceil(float(xy_size-1)/2.0))) XDir = set(-1,0,0); } vector YDir = normalize(cross(XDir, N)); P += ((XDir*fracx) + (YDir*-fracy)); P = (P * camera_zoom); I = N; }
結論
上記のテキストでは、さまざまなトピックを取り上げました...レンズシェーダーの種類とその仕組みを知りました。 CVEXの基本とそれをどこに保存するかによって、あなた自身でレンズシェーダを作成することができます。 上記の例では、最初にいくつかの基本的な例を見て、最後にもう少し複雑な例で終わりました:偽のアトラスをレンダリングするHemi-Octahedralのレンズシェーダ。 より多くのレンズシェーダを作成するには、正しい計算と正しい出力変数PとIに割り当てるということだけ必要です。