2022年8月20日土曜日

Geometry Shader


今回はジオメトリシェーダについて。

GSを使うレンダリングパイプラインの経路は2パターンがある。
VS-GS-PS
VS-HS-DS-GS-PS

ジオメトリシェーダはVSから直接か、DS経由で呼ばれる。
前回やった距離に応じたテッセレーションの結果を元に、ジオメトリシェーダで草を生やしたいと思い準備を進めてきた。

発端はこの動画(動画1動画2動画3
この動画では距離に関係なく細分化して草を生やして、近くと遠くでポリコン数が違う草を生やすということをしている。
そもそも草の生やし方がわからないので、まずはそこから。

Geometry Shaderの関数定義


関数の定義はこんな感じ。

	[maxvertexcount(「出力頂点数」)]
	void GSMain( 「プリミティブ」 GSInput In[「添字」], inout 「ストリーム」<GSOutput> Out )

出力頂点数


出力する頂点の最大数を指定する。

プリミティブ


point、line、triangle、lineadj、triangleadjが指定できる。
triangle以外は使い方を理解できていない。

添字


プリミティブに指定した型により決まる。
pointの時1、lineの時2、triangleの時3、lineadjの時4、triangleadjの時6。

ストリーム


プリミティブに指定した型により決まる。
pointの時PointStream、line、lineadjの時LineStream、triangle、triangleadjの時TriangleStream。

三角形を出力


GSInputで渡された点の1つを使って三角形を作ってみる。

	float3 pos = In[0].Pos.xyz ;

	GSOutput o[3] ;
	
	o[0].Pos = float4( pos + float3(  0.5f, 0.0f, 0.0f ), 1 ) ;
	o[1].Pos = float4( pos + float3( -0.5f, 0.0f, 0.0f ), 1 ) ;
	o[2].Pos = float4( pos + float3(  0.0f, 4.0f, 0.0f ), 1 ) ;

	o[0].Pos = mul( gCam.TransPV, o[0].Pos ) ;
	o[1].Pos = mul( gCam.TransPV, o[1].Pos ) ;
	o[2].Pos = mul( gCam.TransPV, o[2].Pos ) ;

	Out.Append( o[0] ) ;
	Out.Append( o[1] ) ;
	Out.Append( o[2] ) ;
	Out.RestartStrip() ;
posを基準として、左右に0.5ずつずらした点を底辺、上に4.0ずらした点を頂点とした。

頂点データの関して


前回のハルシェーダ、ドメインシェーダでは、今まで通り頂点シェーダでProjectionとView行列を掛けた頂点を使っていた。でもいろいろ見ていくと、どうやらHS~GSをやる場合は、VSでは変換せず、PS直前のシェーダで変換するのが普通みたい。


ジオメトリで出力すると、それまでの点は消えてしまうらしい。
ジオメトリでAppendした分だけが次のステージに進むということか。


テッセレータで近くの点を増やした場合もそのまま動作している。


ただ、横に回り込んで見ようとするとカリングされて消えてしまう。


ビルボード


ビルボードの仕組みを使えば、常にカメラの方を向くので消えないと思い色々調べてみた。
結構たくさん見つかって、ほとんどの方法がViewの逆行列を用意して移動部分の_41、_42、_43を0にして掛けるというもの。参考にしながらいろいろ試したけどうまくいかなかった。

ただ、今回やりたいことはカメラの向きに対して基準点を中心に回転させたいだけなので、自力で計算できるかもしれない。


カメラの位置と、基準点の位置のxとzの差分をatan2に渡すとラジアンが得られる。
これにsin、cosを使ってずらせば行けるはず。
	void BillboardGrass( in float3 pos, in float a, in float y, in float w, out float4 p1, out float4 p2 )
	{
		float x = -cos( a ) ;
		float z = sin( a ) ;
		p1.x = pos.x - x * w ;
		p1.z = pos.z - z * w ;
		p2.x = pos.x + x * w ;
		p2.z = pos.z + z * w ;
		p1.y = p2.y = pos.y + y ;
		p1.w = p2.w = 1.0f ;
	}

Excelで計算した結果、xに-cos、zにsinを足せばOKっぽい。
	GSOutput o[3] ;
	float a = atan2( pos.x - gCam.Eye.x, pos.z - gCam.Eye.z ) ;
	BillboardGrass( pos, a, 0.0f, 0.5f, o[0].Pos, o[1].Pos ) ;
	o[2].Pos = float4( pos + float3(  0.0f, 4.0f, 0.0f ), 1 ) ;

呼び元でatan2を呼んで、幅(半径0.5f)を指定すると常にカメラを向く状態で三角形を作り出すことが出来た。


草ポリゴン



ただの三角形から頂点を増やして草っぽくするために頂点を4つ増やす。

	GSOutput o[3] ;
	float a = atan2( pos.x - gCam.Eye.x, pos.z - gCam.Eye.z ) ;
	BillboardGrass( pos, a, 0.0f, 0.3f, o[0].Pos, o[1].Pos ) ;
	BillboardGrass( pos, a, 1.5f, 0.2f, o[2].Pos, o[3].Pos ) ;
	BillboardGrass( pos, a, 3.0f, 0.15f, o[4].Pos, o[5].Pos ) ;
	o[6].Pos = float4( pos + float3(  0.0f, 4.0f, 0.0f ), 1 ) ;

先程用意した関数を使って、4点追加する。

出力結果も想定通りになった。


ランダム要素


最初に用意した頂点は11×11の正方形で規則正しく並んだ状態。
テッセレータで細分化されたものを上から見てみると、こちらも規則正しく分割されている。


この状態では不自然なのでランダム要素を加えていろいろなものを補正しようと思う。

準備


2つのノイズテクスチャを用意する。
1つは単純なランダム要素がほしいのでホワイトノイズ。
2つ目はなめらかな状態がほしいので、フラクタルノイズがかかっているもの。ベースはなんでもいいんだけどシンプレックスノイズにしてみた。

ホワイトノイズを使って、xz位置の補正、草の高さの補正、色の補正をする。

xz位置の補正


	float ofs = TexRandNoise.SampleLevel( Sampler, uv, 0 ).r * 2.0f - 1.0f ;
	pos.x += ofs * 5.0f ;
	pos.z += ofs * 2.5f ;

ジオメトリシェーダではTextureのSampleが使えないのでSampleLevelで代用。
テクスチャの値と取り出し、-1~1の範囲に補正した値をofsに代入。
xzそれぞれにofsを足す。

草の高さの補正


	float oy = ( ofs * 1.0f ) ;
	BillboardGrass( pos, a, 0.0f, 0.3f, o[0].Pos, o[1].Pos ) ;
	BillboardGrass( pos, a, 1.5f+oy, 0.2f, o[2].Pos, o[3].Pos ) ;
	BillboardGrass( pos, a, 3.0f+oy, 0.15f, o[4].Pos, o[5].Pos ) ;
	o[6].Pos = float4( pos + float3( 0, 4.0f+oy, 0 ), 1 ) ;

高さ補正用のoyを2~6の点にそれぞれ足す。


色の補正


	o[0].UV = float2( 0.0f, 0.0f ) ;
	o[1].UV = float2( 1.0f, 0.0f ) ;
	o[2].UV = float2( 0.2f, 0.1f ) ;
	o[3].UV = float2( 0.8f, 0.1f ) ;
	o[4].UV = float2( 0.4f, 0.25f ) ;
	o[5].UV = float2( 0.6f, 0.25f ) ;
	o[6].UV = float2( 0.5f, 0.50f + ( ofs * 0.5 )) ;

設定していなかったuvを設定。 草の先端部分は長い場合黄色っぽく、短い場合は緑になるようにずらしておく。


	Out.Col = lerp( float4(0.20f + In.Type * 0.5f,0.40f,0.1f,1.0f ), float4(0.75f,1.0f,0.25f,1.0f ), In.UV.y ) ;

ピクセルシェーダを渡されたuvから色を算出するようにする。


D3D12_FILL_MODE_WIREFRAMEをD3D12_FILL_MODE_SOLIDに戻してレンダリングしたもの。
位置、高さがバラけて、先端の色も黄色っぽいのと緑が混在している感じになった。


風を吹かせる


今のところただまっすぐ突っ立ってるだけなので、風にそよいでいる感を出してみる。
用意したノイズテクスチャのもう1つを使う。

	float Wind = TexNoise.SampleLevel( Sampler, uv + gSP.Time*0.0005f, 0 ).r * 2.0f - 1.0f
	o[2].Pos.xz += Wind * 1.0f ;
	o[3].Pos.xz += Wind * 1.0f ;
	o[4].Pos.xz += Wind * 1.5f ;
	o[5].Pos.xz += Wind * 1.5f ;
	o[6].Pos.xz += Wind * 2.0f ;

gSP.Timeは定数バッファで、起動時からのFrame数が渡ってくる様にしてある。
これをuvのオフセットとして使い、毎フレーム少しずつずらした値をWindとして取得する。
底辺以外の頂点にWindを足す。掛け目は上の方ほど影響が大きくなるようにする。



遠くは手を抜く


カメラとの距離を測って、3つの表現方法に切り替えてみる。
手前は頂点7つ版、真ん中は頂点3つ版、奥はビルボードで草のテクスチャを貼り付ける。

位置により手を抜く

草テクスチャ

ジオメトリシェーダの出力構造体に「uint Type」を追加して、それぞれの識別番号を入れる。
ピクセルシェーダで識別番号をみて、ビルボードとそれ以外に分けて処理をする。


三角部分に赤みを足して見たのがこれ。


上から見下ろすとこんな感じ。


上からのカメラがある場合は使い物にならないけど、キャラクタ視点だけなら使えそう。

PIX結果



ハルシェーダのEdgeFactorを12.25でPIXのスナップショットを取ってみたところ、869.583usだった。1msかかってないので、なかなか優秀かも。


0 件のコメント:

コメントを投稿