前回に引き続き、ゲルストナー波をやります。
ゲルストナー波の法線を計算してみます。安直な方法としては、CalcGerstnerWaveOffset関数が波のあらゆる地点の頂点位置の移動量を求められるものであることを利用し、x方向とz方向に1センチずらしてそれぞれCalcGerstnerWaveOffsetを呼び、これを三角形に見立てて法線を求めます。
これでも実用上問題無いレベルになるとは思いますが、GPU GemsのEffective Water Simulation from Physical Modelsによると、Xで偏微分するとBinormal、Yで偏微分するとTangent、それらの外積からNormalを求めることができます。親切にもNormal, Binormal, Tangentそれぞれの式がEquation 10~12として提示されていますので、そのままHLSL化してみます。ただし、Island11はYが上、論文ではZが上なので、YとZを入れ替えてあります。そのせいで、Normal = Binormal X Tangentとはならず、Normal = Tangent X Binormalになっています。
あえてくどくどと関数を分けてありますが、論文で使われている式や変数名をなるべく残すためで、現場で使う場合は1ループで一気にfloat3x3の回転行列まで求めると良いと思います。
Island11のドメインシェーダの既存の法線はそのままワールド空間の法線として使われていましたが、それをゲルストナー波の接空間(tangent space)とみなしてワールド空間に配置します。
次回は水面に物を浮かべてみます。
Showing posts with label HLSL. Show all posts
Showing posts with label HLSL. Show all posts
Monday, October 9, 2017
Saturday, October 7, 2017
HLSLでゲルストナー波を実装
NVIDIAのIsland11サンプルを改造してゲルストナー波を加えてみました。
Copyright (c) 2011 NVIDIA Corporation.

ゲルストナー波の実装例はいくつかのサイトで紹介されていますが、多くはGPU GemsのEffective Water Simulation from Physical ModelsのEquation 9の式が引用されています。
以下はこの式をHLSLで実装してみたものです。
引数のvで海面の頂点座標を受け取り、その位置においてどれだけ頂点を移動させればいいのかオフセットを返しています。C++側から乱数で生成した100個の波のパラメ ータを受け取っていますが、試行錯誤の結果20個だけ使っています。
C++でパラメータを生成するコードは以下です。
randomRadの計算で、最後の0.3fを無くすと全方向への波をランダムに生成することになります。0.3を掛けてある程度似た方向に向けて生成するようにしています。
amplitudeは波の高さで、waveLengthは波の幅です。特に決まった式があるわけではなく、見た目から数値や式を適当に選んだ結果です。ただし、GPU GemsによるとwaveLengthは中央値を基準に半分の長さから2倍の長さの波を作るといいと書いてあります。
下のgifはオリジナルのIsland11の波です。RGBをノーマル、Aをハイトマップとしたテクスチャを元に頂点を移動するだけというシンプルなアルゴリズムですが、このテクスチャの出来がいいのでとてもきれいです。
Copyright (c) 2011 NVIDIA Corporation.

波による頂点移動処理はドメインシェーダ内あり、そこをゲルストナー波に置き換えてみます。うねる波が表現できた半面、海らしい荒さが足りない気がします。
Copyright (c) 2011 NVIDIA Corporation.

そこで、Island11のオリジナルの波と合成してみると、それなりに見られるものになりました。結局ゲルストナー波単体できれいな絵はできず、さざ波や白波の表現も工夫していく必要がありそうです。
Copyright (c) 2011 NVIDIA Corporation.
次回は法線を求めてみます。
References:
Effective Water Simulation from Physical Models
Ocean Shader with Gerstner Waves
GERSTNER WAVE IMPLEMENTATION
DX11 Samples
Copyright (c) 2011 NVIDIA Corporation.

ゲルストナー波の実装例はいくつかのサイトで紹介されていますが、多くはGPU GemsのEffective Water Simulation from Physical ModelsのEquation 9の式が引用されています。
以下はこの式をHLSLで実装してみたものです。
引数のvで海面の頂点座標を受け取り、その位置においてどれだけ頂点を移動させればいいのかオフセットを返しています。C++側から乱数で生成した100個の波のパラメ ータを受け取っていますが、試行錯誤の結果20個だけ使っています。
C++でパラメータを生成するコードは以下です。
randomRadの計算で、最後の0.3fを無くすと全方向への波をランダムに生成することになります。0.3を掛けてある程度似た方向に向けて生成するようにしています。
amplitudeは波の高さで、waveLengthは波の幅です。特に決まった式があるわけではなく、見た目から数値や式を適当に選んだ結果です。ただし、GPU GemsによるとwaveLengthは中央値を基準に半分の長さから2倍の長さの波を作るといいと書いてあります。
下のgifはオリジナルのIsland11の波です。RGBをノーマル、Aをハイトマップとしたテクスチャを元に頂点を移動するだけというシンプルなアルゴリズムですが、このテクスチャの出来がいいのでとてもきれいです。
Copyright (c) 2011 NVIDIA Corporation.

波による頂点移動処理はドメインシェーダ内あり、そこをゲルストナー波に置き換えてみます。うねる波が表現できた半面、海らしい荒さが足りない気がします。
Copyright (c) 2011 NVIDIA Corporation.

そこで、Island11のオリジナルの波と合成してみると、それなりに見られるものになりました。結局ゲルストナー波単体できれいな絵はできず、さざ波や白波の表現も工夫していく必要がありそうです。
Copyright (c) 2011 NVIDIA Corporation.

次回は法線を求めてみます。
References:
Effective Water Simulation from Physical Models
Ocean Shader with Gerstner Waves
GERSTNER WAVE IMPLEMENTATION
DX11 Samples
Labels:
C++,
DirectX11,
DirectX12,
Gerstner Wave,
GPU Gems,
HLSL,
Japanese,
Visual Studio
Wednesday, June 28, 2017
[HLSL] シェーダ内に定数を定義したつもりだが値が割り当てられない問題
HLSLでは関数の外に変数を値付きで記述ができますが、この宣言だけでは見た目に反して変数から正常に値を読み出すことができず、エラーも出ません。
C++側から値を変更するつもりが無いならstaticをつけることで定数になり、意図通り動作します。(constではないです)
staticを付けなかった場合、NSIGHT等で見ると暗黙のコンスタントバッファが生成されることがわかります。ところが肝心の値が代入されず、0で埋まっています。なぜ文法的に許容されるか不可解に思えるところですが、今は非推奨となったD3DX11Effectを使うと、暗黙のコンスタントバッファに宣言通りの値を代入してくれます。(そして、D3DX11Effect::Applyを呼ぶと内部でVSSetConstantBuffers、PSSetConstantBuffers等の呼び出しまでしてくれます)
コードをモダン化するためにD3DX11Effectを取り除く作業中に分かり難いバグに遭遇したら、暗黙のコンスタントバッファがないか確認してみると良いかもしれません。
コードをモダン化するためにD3DX11Effectを取り除く作業中に分かり難いバグに遭遇したら、暗黙のコンスタントバッファがないか確認してみると良いかもしれません。
Saturday, February 6, 2016
DirectXとOpenGLとrow majorとcolumn majorのまとめ
DirectXの行列は、移動が4行目に来ます。
対して、OpenGLはちょうどDirectXの行列を転置したもので、移動が4列目に来ます。

ところが、行列をfloat型16個の配列として見てみると、両者のメモリレイアウトは全く同じです。
例えばXMMatrixLookAtRHで生成したビュー行列をgluLookAtと置き換える正常に動作します。OpenGLはcolumn major(列優先)で、DirectXはrow major(行優先)なので、数学的に1回、メモリレイアウトで1回、2回転置してメモリレイアウトが同じになったと見ることもできます。
では両者は同じかと言うと、数学的な定義とメモリレイアウトは別の話なので注意が必要です。頂点v、ワールド(モデル)行列W、ビュー行列V、射影行列Pの計算式はOpenGLとDirectXで逆の並びになっています。
OpenGL:

この並びはシェーダーを書く時の慣習にもなっていて、多くの参考書はこの順で書かれているはずです。両者のメモリレイアウトは同じはずなので、掛け算の順番が違うということは、GLSLはcolumn major、HLSLはrow majorの並びと推測できます。
ところが、GLSLもHLSLもcolumn majorがデフォルトになっています。OpenGLはC/C++側もGLSLもcolumn majorなので良いのですが、DirectXはC/C++側がrow major、HLSLがcolumn majorと異なっています。
しかし、これはおかしいです。row majorではないと、

として正しい結果が得られないはずだからです。どうも、DirectXはシェーダに行列を渡す時点で転置させる習慣があったようで、結果としてこの順で掛け算して正しくなっていました。おそらく、DirectX9時代はD3DXあたりで勝手にやってくれたのでプログラマが気にする必要が無かったのだと思います。
しかし、DirectX11はConstant Bufferの配置までプログラマに委ねられているので、理解して使う必要があります。実は、DirectX11ではOpenGLと同じように、

の順で掛けると正しい結果になります。あるいはHLSLで行列を宣言するときにrow_majorを指定すると、

この従来の順で計算しても正しくなります。
対して、OpenGLはちょうどDirectXの行列を転置したもので、移動が4列目に来ます。
ところが、行列をfloat型16個の配列として見てみると、両者のメモリレイアウトは全く同じです。
例えばXMMatrixLookAtRHで生成したビュー行列をgluLookAtと置き換える正常に動作します。OpenGLはcolumn major(列優先)で、DirectXはrow major(行優先)なので、数学的に1回、メモリレイアウトで1回、2回転置してメモリレイアウトが同じになったと見ることもできます。
DirectX:
この並びはシェーダーを書く時の慣習にもなっていて、多くの参考書はこの順で書かれているはずです。両者のメモリレイアウトは同じはずなので、掛け算の順番が違うということは、GLSLはcolumn major、HLSLはrow majorの並びと推測できます。
ところが、GLSLもHLSLもcolumn majorがデフォルトになっています。OpenGLはC/C++側もGLSLもcolumn majorなので良いのですが、DirectXはC/C++側がrow major、HLSLがcolumn majorと異なっています。
しかし、これはおかしいです。row majorではないと、
として正しい結果が得られないはずだからです。どうも、DirectXはシェーダに行列を渡す時点で転置させる習慣があったようで、結果としてこの順で掛け算して正しくなっていました。おそらく、DirectX9時代はD3DXあたりで勝手にやってくれたのでプログラマが気にする必要が無かったのだと思います。
しかし、DirectX11はConstant Bufferの配置までプログラマに委ねられているので、理解して使う必要があります。実は、DirectX11ではOpenGLと同じように、
の順で掛けると正しい結果になります。あるいはHLSLで行列を宣言するときにrow_majorを指定すると、
この従来の順で計算しても正しくなります。
Friday, August 21, 2015
HLSLでフォトスフィアをLittle Planetに変換する
スマホで撮った近所の公園のフォトスフィアからLittle PlanetとかTiny Planetと呼ばれている絵ができました。
ソース画像となるフォトスフィアとはGoogleによる呼称で、「360度パノラマVR」と呼ばれたり「全方位パノラマ」と呼ばれたり呼称が統一されていない様です。最近は「HDRi」で検索すると海外の素材集がいっぱいでてきますね。投影法はEquirectangular Projection(正距円筒図法)といいます。ここではフォトスフィアで統一します。
また、結果物であるLittle Planetのような投影法をStereographic Projection(ステレオ投影)といいます。
今回の目的は、HLSLによってEquirectangular Projectionされた画像をソースとしてStereographic Projectionした画像を得ることです。 まず、簡単のためにキューブマップをソースにしてやってみます。
scaleはアスペクト比と、Little Planetの大きさを決めます。ここは見た目が良くなるように適当に数値を決めればよいところです。
キューブマップのフェッチに使うdirはWikipediaのステレオ投影で紹介されている立体射影の逆写像の式そのままです。実際にフェッチの段階でyとzを入れ替えるのは、キューブマップのY方向が上下になっているからです。どうスィズルするかによってLittle Planetの中心をどこにするか決められます。
次に、フォトスフィアをソースにしてやってみます。冒頭の絵はこれで生成します。
前半部はキューブマップと全く同じで、dirを使ってフォトスフィアのどこからフェッチするかの計算が加わりました。まず、三次元ベクトルのdirを経度(longitude、-180~180)と緯度(latitude、-90~90)に変換します。そうすると経度と緯度がフォトスフィアのX方向とY方向に対応します。あとはテクスチャUVである0~1の範囲に変換すれば完成です。
ソース画像となるフォトスフィアとはGoogleによる呼称で、「360度パノラマVR」と呼ばれたり「全方位パノラマ」と呼ばれたり呼称が統一されていない様です。最近は「HDRi」で検索すると海外の素材集がいっぱいでてきますね。投影法はEquirectangular Projection(正距円筒図法)といいます。ここではフォトスフィアで統一します。
また、結果物であるLittle Planetのような投影法をStereographic Projection(ステレオ投影)といいます。
今回の目的は、HLSLによってEquirectangular Projectionされた画像をソースとしてStereographic Projectionした画像を得ることです。 まず、簡単のためにキューブマップをソースにしてやってみます。
scaleはアスペクト比と、Little Planetの大きさを決めます。ここは見た目が良くなるように適当に数値を決めればよいところです。
キューブマップのフェッチに使うdirはWikipediaのステレオ投影で紹介されている立体射影の逆写像の式そのままです。実際にフェッチの段階でyとzを入れ替えるのは、キューブマップのY方向が上下になっているからです。どうスィズルするかによってLittle Planetの中心をどこにするか決められます。
次に、フォトスフィアをソースにしてやってみます。冒頭の絵はこれで生成します。
前半部はキューブマップと全く同じで、dirを使ってフォトスフィアのどこからフェッチするかの計算が加わりました。まず、三次元ベクトルのdirを経度(longitude、-180~180)と緯度(latitude、-90~90)に変換します。そうすると経度と緯度がフォトスフィアのX方向とY方向に対応します。あとはテクスチャUVである0~1の範囲に変換すれば完成です。
Sunday, August 9, 2015
[slide]프로그래머를 위한 360VR
로드뷰, 360VR의 기반 기술인 Equirectangular Projection(이퀴렉탱귤러/등장방형도법)을 이해해, DirectX11과 HLSL로 화면 출력, 큐브맵과 상호 변환 방법을 공부합니다.
Saturday, August 1, 2015
HLSLでフォトスフィアビューアを作る
ストリートビューなどで使われ、最近はAndroidのカメラなどでも誰でも手軽に作れるようになったフォトスフィアをDirectX11とHLSLでレンダリングしてみます。全方向の色を記録するためEquirectangular projectionという投影方法で長方形になっています。世界地図などでは正距円筒図法と呼ばれるようです。

これにPhoto Sphere XMP Metadataと呼ばれるメタデータが埋まることで、対応アプリから認識されます。非対応アプリからは普通のJPGファイルです。
全方向画像と言えば、ゲーム開発で主に使われるのはcube mapです。実際、cube mapで遠景を描いているゲームも多いと思いますが、フォトスフィアビューアでやる事とほとんど同じです。各シェーダの全文の例を掲示します。何をしているかというと、各ピクセルの視線ベクトルを求め、その方向の色をcubemap又はPhoto Sphereから読み取っています。見ての通り頂点シェーダ(mainVS)からピクセルシェーダ(mainPS)の第一行目まで全く同じです。
C++からは頂点は4つ出力しています。頂点バッファをバインドしない代わりに頂点シェーダで画面の四隅の位置を設定して全画面を塗りつぶします。また、pos2をラスタライザに渡してピクセルシェーダからピクセルの画面上の位置を取得できるようにします。ピクセルシェーダの一行目で画面上の位置にinvVPを掛けることで画角とカメラの方向が反映された視線方向を求められます。invVPはView行列とProjection行列を掛けたものの逆行列です。
ここから先はcube mapとphotosphereで処理が変わりますが、キューブマップの場合は視線ベクトルから色を求めるのはたったの1行です。キューブマップは元々三次元ベクトルから色を取得するものなので当然ですね。それに対し、Photo Sphereは三次元ベクトルをEquirectangularで投影した二次元テクスチャの座標に変換する作業が間に入ります。
このプログラムの三次元の座標系は、右が+x、上が+y、奥が+zと定義しています。フォトスフィア上の二次元の座標系coordX, coordYは、フォトスフィアの中心を(0,0)とし、左上を(-1,-1) 右下を(+1,+1)と定義します。便宜上の定義であって実際のテクスチャの座標ではないことに注意してください。

写真だと位置をイメージしにくいので、Equirectangular projectionされた世界地図を使って、実例で座標を指定してみます。
東経0度、北緯0度 =(0, 0)、ガーナ南方の赤道上の地点、ここは視線ベクトル(0, 0, 1)になります。
東経0度、北緯45度 =(0, 0.5)、フランスのボルドー、ここは視線ベクトルは(0, 0.7071, 0.7071)です。
西経90度、北緯45度 =(-0.5, 0.5) ウィスコンシン州、ここは視線ベクトルは(-0.7071, 0.7071, 0)です。
こうして数値を見比べてみると数式が見えてこないでしょうか。
coordXは視線ベクトルのうちy成分を無視して、xとzからatan2で角度を求めればそれがそのまま経度になります。
coordYは視線ベクトルy成分からのみ算出します。どちらも-1から1の範囲ですが、経度は球面上の距離なのでasinで変換します。
最後に、coordX, coordYは実際のテクスチャ座標系ではないので0から1の範囲に変換してテクスチャを読み込みます。

これにPhoto Sphere XMP Metadataと呼ばれるメタデータが埋まることで、対応アプリから認識されます。非対応アプリからは普通のJPGファイルです。
全方向画像と言えば、ゲーム開発で主に使われるのはcube mapです。実際、cube mapで遠景を描いているゲームも多いと思いますが、フォトスフィアビューアでやる事とほとんど同じです。各シェーダの全文の例を掲示します。何をしているかというと、各ピクセルの視線ベクトルを求め、その方向の色をcubemap又はPhoto Sphereから読み取っています。見ての通り頂点シェーダ(mainVS)からピクセルシェーダ(mainPS)の第一行目まで全く同じです。
C++からは頂点は4つ出力しています。頂点バッファをバインドしない代わりに頂点シェーダで画面の四隅の位置を設定して全画面を塗りつぶします。また、pos2をラスタライザに渡してピクセルシェーダからピクセルの画面上の位置を取得できるようにします。ピクセルシェーダの一行目で画面上の位置にinvVPを掛けることで画角とカメラの方向が反映された視線方向を求められます。invVPはView行列とProjection行列を掛けたものの逆行列です。
ここから先はcube mapとphotosphereで処理が変わりますが、キューブマップの場合は視線ベクトルから色を求めるのはたったの1行です。キューブマップは元々三次元ベクトルから色を取得するものなので当然ですね。それに対し、Photo Sphereは三次元ベクトルをEquirectangularで投影した二次元テクスチャの座標に変換する作業が間に入ります。
このプログラムの三次元の座標系は、右が+x、上が+y、奥が+zと定義しています。フォトスフィア上の二次元の座標系coordX, coordYは、フォトスフィアの中心を(0,0)とし、左上を(-1,-1) 右下を(+1,+1)と定義します。便宜上の定義であって実際のテクスチャの座標ではないことに注意してください。

写真だと位置をイメージしにくいので、Equirectangular projectionされた世界地図を使って、実例で座標を指定してみます。
東経0度、北緯0度 =(0, 0)、ガーナ南方の赤道上の地点、ここは視線ベクトル(0, 0, 1)になります。
東経0度、北緯45度 =(0, 0.5)、フランスのボルドー、ここは視線ベクトルは(0, 0.7071, 0.7071)です。
西経90度、北緯45度 =(-0.5, 0.5) ウィスコンシン州、ここは視線ベクトルは(-0.7071, 0.7071, 0)です。
こうして数値を見比べてみると数式が見えてこないでしょうか。
coordXは視線ベクトルのうちy成分を無視して、xとzからatan2で角度を求めればそれがそのまま経度になります。
coordYは視線ベクトルy成分からのみ算出します。どちらも-1から1の範囲ですが、経度は球面上の距離なのでasinで変換します。
最後に、coordX, coordYは実際のテクスチャ座標系ではないので0から1の範囲に変換してテクスチャを読み込みます。
Subscribe to:
Posts (Atom)