2023年5月15日月曜日

Point Light

ポイントライトを実装してみることにした。

ディファードレンダリングを見つけた時、このポイントライトがきっかけだった気がする。

ライトの処理は重くなりがちで、数個の制限を駆使してシーンを作らないと行けないらしい。
でもそんな面倒なことはしたくない。
ディファードレンダリングなら今までの制限を大幅に超えてライトが自由に配置できるとのこと。

HLSLの魔導書に書かれているのをアレンジして実装してみた。

ポイントライト1000個

1000個のポイントライトをランダムに配置してみた。
まあ、普通に動きそう。

更に視錐台カリングも実装してみた。

視錐台カリング


Tile Based Rendering


問題はここから。 
Deferred Renderingで何も考えずにライトを描画出来るけど、やはり1ピクセル単位ですべてのライトを処理するのは無駄すぎる。

そこで、描画時に画面を分割して、そこに含まれるライトのみを対象とすることで高速化する。

視錐台を分割する

タイル1つ分の視錐台を用意して、そこに含まれるポイントライトを調べる。

タイル内に含まれるポイントライト

あるタイルで描画対象のライトをUAVに書き出して、Deferred Renderingでそのバッファを元に対象のライトのみを描画することにより、重なっているライト数だけ処理をすれば良くなる。


この話題になると絶対に出てくるIntelのサンプルプログラム

HLSLの魔導書でもこのプログラムの言及があり、視錐台分割部分で下記のようなコメントがある。

    // Intelのサンプルと微妙に違うのは右手系でやっているから
    // あと、Intelのサンプルは微妙に間違ってると思う

実物のソースと見比べてみても全く同じで、間違ったところが直されていると思ったがそのままだった。もしかするとIntelのソースが直されたのかと思ったがそうでもないらしい。

その根拠が、このサイト
ここによると、左面と底面の式が間違っている。

float4 c1 = float4(mtxProj._11 * tileScale.x, 0.0, tileBias.x, 0.0);
float4 c2 = float4(0.0, -mtxProj._22 * tileScale.y, tileBias.y, 0.0);
float4 c4 = float4(0.0, 0.0, 1.0, 0.0);

frustumPlanes[0] = c4 - c1; // Right
frustumPlanes[1] = c4 + c1; // Left
frustumPlanes[2] = c4 - c2; // Top
frustumPlanes[3] = c4 + c2; // Bottom

上記は間違いで、下記が正解。

float4 c1 = float4(mtxProj._11 * tileScale.x, 0.0, tileBias.x, 0.0);
float4 c2 = float4(0.0, -mtxProj._22 * tileScale.y, tileBias.y, 0.0);
float4 c4 = float4(0.0, 0.0, 1.0, 0.0);

frustumPlanes[0] = c4 - c1; // Right
frustumPlanes[1] =      c1; // Left
frustumPlanes[2] = c4 - c2; // Top
frustumPlanes[3] =      c2; // Bottom

また、このIntelのサンプルプログラムは1つのCompute Shader内で、異なる単位の処理が行われている。説明なしには理解するのは難しい。

// [ドット単位]のZを取得(dot/thread)
float2 uv = In.DTID.xy ;
float4 Pos = DepthToPos( TexDepth[uv], uv, Cam.InverseP ) ;
uint Base = ( In.GID.y * (( Cam.Screen.x + TILE_SIZE-1 ) / TILE_SIZE ) + In.GID.x ) * PLIGHT_PER_TILE ;

if( In.GI == 0 ) {
	sMinZ = asuint( Pos.z ) ;
	sMaxZ = asuint( Pos.z ) ;
	sCount = 0 ;
}

GroupMemoryBarrierWithGroupSync() ;

// [タイル(グループ)単位]の最大/最小のZを書き込む(tile/256thread)
InterlockedMin( sMinZ, asuint( Pos.z )) ;
InterlockedMax( sMaxZ, asuint( Pos.z )) ;

GroupMemoryBarrierWithGroupSync() ;

float MinZ = asfloat( sMinZ ) ;
float MaxZ = asfloat( sMaxZ ) ;

float4 Frustum[ 6 ] ;
GetTileFrustumPlane( Frustum, Cam.TransP, In.GID.xy, Cam.Screen, MinZ, MaxZ, TILE_SIZE ) ;

// [ライト単位]で視錐台に含まれるかチェック(1024lightの場合 4light/thread)
for( uint lightIndex = In.GI ; lightIndex < Cam.PLightCount ; lightIndex += TILE_SIZE * TILE_SIZE ) {
	_PointLight light = PointLight[ lightIndex ] ;
	float4 PLPos = mul( Cam.TransV, float4( light.Pos, 1.0f )) ;

	// タイルとの判定
	bool inFrustum = true ;
	[unroll]
	for( uint i = 0 ; i < 6 ; ++i ) {
		// ライトの座標と平面の法線とで内積を使って、
		// ライトと平面との距離(正負あり)を計算する
		float d = dot( Frustum[i], PLPos ) ;

		// ライトと平面の距離を使って、衝突判定を行う
		inFrustum = inFrustum && ( d >= -light.Range ) ;
	}

	// タイルと衝突している場合
	if( inFrustum ) {
		// 衝突したポイントライトの番号を影響リストに積んでいく
		uint Index ;
		InterlockedAdd( sCount, 1, Index ) ;
		if( Index >= PLIGHT_PER_TILE ) break ;	// 1タイル内の上限に達した
		Dst[ Base + Index ] = lightIndex ;
	}
}

GroupMemoryBarrierWithGroupSync() ;

if( In.GI == 0 && sCount < PLIGHT_PER_TILE ) {
	Dst[ Base + sCount ] = 0xffffffff ;
}

GroupMemoryBarrierWithGroupSync関数を呼ぶと、グループ内のスレッドがこの呼出に達するまでブロックされる。

最初のブロックでは、グループID0番だけにグループの共有メモリを初期化させる。

次のブロックでは16×16ドットのZを調べて、最小と最大深度を共有メモリに書き込む。
ここでは1ドット/1スレッド単位で処理される。

次のブロックではポイントライト情報を参照して視錐台に含まれるかをチェックする。
ポイントライトが1024個あるとしたら、4ライト/1スレッド単位で処理される。

IntelのサンプルではCompute Shader内でG-Bufferも参照してライトの計算までしていたけど、自分のプログラムではUAVにタイル別のライト配列を出力するまでとして、Defferd/Forward Rendering両方に使えるようにしてみた。

また、1タイル分のリストの大きさはライト数分持つのが普通だけど、ライト数1024として、1タイルに1024個描くような状況は殆どないはずなので、色々調整して256個にした。


Deferred Renderingでは、ライト描画を加えた処理が2~4msぐらい掛かっていたけど、TBDRだと50~100usに短縮。ただし、UAVを作るCSが35~40usかかるので、トータルでは85usから140usぐらい。




0 件のコメント:

コメントを投稿