2023年2月26日日曜日

FrustumCullingと別カメラの視点

技術関連のページを調べていると、Unityの記事が多い。
Unityのエディタ上で視錐台と各種オブジェクトが表示されている画面と、視錐台の視点(実際の見た目)の画面を同時に見ていたりする。

次は視錐台カリングにチャレンジしてみようと思ったが、まず上記のような別視点で確認できるようにして、本当に裏で消えているのか確認できるようにしたい。


別カメラ画面


まず通常の描画を行い、それをレンダーターゲットではなく中間レンダーターゲットに出力する。
次に、サブカメラを追加してそのカメラ視点の描画を同じシーンに対して行う。こっちの描画はレンダーターゲットに直接行い、通常の描画で作った中間レンダーターゲットを最後に描画する。影の処理は通常処理で済んでいるので、2回目ではスキップできるようにしている。
これはあっさりと出来た。

メインカメラとサブカメラの画像


視錐台の描画


次に視錐台の描画をやってみる。
カスケードシャドウをやったとき、ライトカメラからの視錐台を作っていたのでそれを応用してみる。各頂点はメインカメラからの情報で作って、VertexBufferに書き込む。FarZは30ぐらいにしておく。プリミティブはD3D_PRIMITIVE_TOPOLOGY_LINELISTで描画する。

これもそれっぽく表示されたけどなんか実際のカメラの回転とずれている。
視錐台のWorldマトリクスをどうすればいいのか2日ぐらいハマった。
いろいろ試行錯誤した結果、Viewの逆行列でいいということにたどり着いた。

視錐台描画


視錐台カリング


視錐台カリングで検索するといくつか見つかる。
点であれば、View×ProjectionでXMVector4Transformを呼び出して、あとは-wからwの範囲に、x,y,zが含まれていれば表示対象ということはわかった。
AABBでのやり方も出てくるが、スマートではない。
視錐台と球の判定がやりたい。

マイクロソフトのサンプルにMeshShaderでGPU側でFrustumCullingをやっているものを見つけた。これを参考にすれば出来るかもしれない。
実装した結果それっぽくはなるが、視錐台を横から対象のオブジェクトみて視錐台の左右の壁にぶつけたとき、かなり早めにカリングされてしまう状況になった。

面の作成

	auto oPV = oCam.oCamera.GetViewMatrix() * oCam.oCamera.GetProjectionMatrix() ;
	tDMatrix vp = DirectX::XMMatrixTranspose( oPV ) ;
	tDVector oPlanes[] = {
		DirectX::XMPlaneNormalize( DirectX::XMVectorAdd( vp.r[3], vp.r[0])),		// Left
		DirectX::XMPlaneNormalize( DirectX::XMVectorSubtract( vp.r[3], vp.r[0])),	// Right
		DirectX::XMPlaneNormalize( DirectX::XMVectorAdd( vp.r[3], vp.r[1])),		// Bottom
		DirectX::XMPlaneNormalize( DirectX::XMVectorSubtract( vp.r[3], vp.r[1])),	// Top
//		DirectX::XMPlaneNormalize( vp.r[2]),										// Near
//		DirectX::XMPlaneNormalize( DirectX::XMVectorSubtract( vp.r[3], vp.r[2])),	// Far
	} ;

これも2日ぐらいハマった。
各メッシュの中心と半径でチェックを行う際、World×View×ProjectionのマトリクスでXMVector4Transformを呼び出して、半径はスケール調整して後は面ごとに内積した結果を-半径以下になったら範囲外という判定をする。判定の面はNearとFarは除外した。

判定部分

	auto oV = DirectX::XMVector4Transform( oCenter, oTrans ) ;
	auto r = oM.nRadius * DirectX::XMVectorGetX( oScale ) ;

    for( auto & oP : oPlanes ) {
		auto d = DirectX::XMVectorGetX( DirectX::XMPlaneDotCoord( oP, oV )) ;
        if( d < -r ) {
			o.bCulling = true ;
			break ;
        }
    }

内積の関数をXMVector3Dot、XMVector4Dotを使っていたが、XMPlaneDotCoordの存在を知り変更。若干良くなるがまだ判定がおかしい。
最終的はoTransがWorld×View×Projectionではなく、Worldだけにしたらうまく行った。

視錐台カリング


今回はメッシュのグループ単位でカリングを行ったが、マテリアルが変わる単位でもカリングができそう。ただそれをやるとDrawコールを分ける必要が出てきてむしろ遅くならないかが心配。
いろいろ調べてみると、全部GPU側で判断させてドローコールもGPU側でしてしまうのが速いっぽい。MeshShaderに手をだす日も近いか・・・

 


2023年2月18日土曜日

DrawInstanced

前回StructuredBufferを作ったが、これを利用して今回は複数インスタンスを同時に描画してみる。


同一Bone(CacheVertexBuffer)


今までWorldMatrixは32BitConstBufferで渡していたが、StructuredBufferで複数渡せるようになったので複数インスタンス対応させてみる。

#define DefRS "RootFlags(ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT | DENY_HULL_SHADER_ROOT_ACCESS | DENY_DOMAIN_SHADER_ROOT_ACCESS | DENY_GEOMETRY_SHADER_ROOT_ACCESS | DENY_PIXEL_SHADER_ROOT_ACCESS),DescriptorTable( SRV(t0, flags=DATA_STATIC_WHILE_SET_AT_EXECUTE),visibility=SHADER_VISIBILITY_VERTEX),RootConstants( num32BitConstants=16, b0,visibility=SHADER_VISIBILITY_VERTEX)"

StructuredBuffer<float4x4> World : register(t0) ;

struct _Cam
{ 
	float4x4	TransPV ;	// offset:   0	Size: 64
} ;
ConstantBuffer<_Cam> Cam : register(b0) ;

struct VSInput
{
	float3 Pos : POSITION ;
	float3 Normal : NORMAL ;
	float2 UV : TEXCOORD ;
	uint IID : SV_InstanceID ;
} ;
struct VSOutput
{
	float4 Pos : SV_POSITION ;
} ;

[RootSignature(DefRS)]
VSOutput VSMain( VSInput In )
{
	VSOutput Out ;
	Out.Pos = mul( Cam.TransPV, mul( World[ In.IID ], float4( In.Pos, 1 ))) ;
	return Out ;
}

VertexShaderに「uint IID : SV_InstanceID」の引数を追加し、WorldMatrixを「StructuredBuffer<float4x4>」で定義する。
WorldMatrixの参照は「World[ In.IID ]」でインスタンス別に参照位置を変える。
Bone情報はこのShader以前のパスでキャッシュしてあり、固定のMeshと同じShaderで描画できるようになっている。



すべてのキャラクタが同じ動きをしてしまうので、兵士の行進とか特殊な状況でしか使えなさそう。


個別Bone


今度は個別のアニメーションにも対応できるように、Bone自体をインスタンス分保持して描画してみる。


#define DefRS "RootFlags(ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT | DENY_HULL_SHADER_ROOT_ACCESS | DENY_DOMAIN_SHADER_ROOT_ACCESS | DENY_GEOMETRY_SHADER_ROOT_ACCESS | DENY_PIXEL_SHADER_ROOT_ACCESS),RootConstants( num32BitConstants=1, b0,visibility=SHADER_VISIBILITY_VERTEX),DescriptorTable( SRV(t0, flags=DATA_STATIC_WHILE_SET_AT_EXECUTE),visibility=SHADER_VISIBILITY_ALL),DescriptorTable( SRV(t1, flags=DATA_STATIC_WHILE_SET_AT_EXECUTE),visibility=SHADER_VISIBILITY_VERTEX),RootConstants( num32BitConstants=16, b1,visibility=SHADER_VISIBILITY_VERTEX)"

struct _Mesh
{ 
	uint	BoneCount ;
} ;

ConstantBuffer<_Mesh> Mesh : register(b0) ;

StructuredBuffer<float4x4> Bone : register(t0) ;

StructuredBuffer<float4x4> World : register(t1) ;

struct _Cam
{ 
	float4x4	TransPV ;	// offset:   0	Size: 64
} ;
ConstantBuffer<_Cam> Cam : register(b1) ;

struct VSInput
{
	float3 Pos : POSITION ;
	float3 Normal : NORMAL ;
	float2 UV : TEXCOORD ;
	uint4 BoneID : BONE ;
	float4 Weight : WEIGHT ;
	uint IID : SV_InstanceID ;
} ;
struct VSOutput
{
	float4 Pos : SV_POSITION ;
} ;

[RootSignature(DefRS)]
VSOutput VSMain( VSInput In )
{
	VSOutput Out ;
	uint ofs = Mesh.BoneCount * In.IID ;
	float4x4 BoneTrans
		= Bone[ In.BoneID[0] + ofs ] * In.Weight[0]
		+ Bone[ In.BoneID[1] + ofs ] * In.Weight[1]
		+ Bone[ In.BoneID[2] + ofs ] * In.Weight[2]
		+ Bone[ In.BoneID[3] + ofs ] * In.Weight[3]
	;
	Out.Pos = mul( Cam.TransPV, mul( World[ In.IID ], mul( BoneTrans, float4( In.Pos, 1.0f )))) ;
	return Out ;
}

「StructuredBuffer<float4x4> Bone」と「uint BoneCount」をConstBufferで追加。
Boneを参照する際、「BoneCount * InIID」でインスタンス別に参照位置を変える。


 1回のDrawIndexedInstancedだけど、複数インスタンスでそれぞれ違う動きが出来るようになった。


キャッシュされたVertexBufferで1体、個別Boneで1体描画したときの時間は約174usで、それぞれ20体ずつの描画は約196usと殆ど変わらない速度で描画出来るっぽい。

1体ずつの描画

20体ずつの描画


2023年2月12日日曜日

StructuredBufferとCopyQueue


StructuredBuffer


メッシュのボーン行列をシェーダにわたす定義は、定数バッファで固定の配列にしていた。

struct _Bone {
	float4x4 Bones[255] ;
} ;
ConstantBuffer<_Bone> Bone : register(b1) ;

実際に渡す個数はボーンの数分だけで、利用するインデックスも渡した分だけを指すようになっているので問題ない。
ただ、この固定の書き方が気持ち悪いのと、読み込んだメッシュのボーンが固定値を超える場合に調整が必要になるのでなんとかしたい。

構造化バッファ(StructuredBuffer)の場合、可変の配列定義が許されているのでリソースのタイプに追加することにした。
前にテクスチャのミップマップを作る際、GPU側(ComputeShader)で更新するリソースを用意したけど、今度はCPU側から更新するタイプのSRVリソースとなる。

D3D12_HEAP_TYPE_UPLOADとD3D12_HEAP_TYPE_DEFAULTの2リソースを用意して、Uploadの方はMapしっぱなしで更新時にmemcpyしつつ、CopyBufferRegionでDefaltの方に反映する。

このコピーを行うコマンドリストは、汎用的に使えるように用意したDIRECT版のコマンドリストを使っていて、ステータス遷移(D3D12_RESOURCE_STATE_COPY_DEST)、CopyBufferRegionを追加する。

StructuredBuffer<float4x4> Bone : register(t1) ;

シェーダの定義はこんな感じになって、Bone[Index]という風に使える。

疑問点が1つあって、定数バッファはフレームのバッファ数分用意する必要がある。
マイクロソフトのサンプルでは、ダブルバッファの場合必要サイズの2倍データ領域を用意して、毎フレーム交互に書き込み、参照していく。
構造化バッファの場合、今のところ1つで済んでいる。
中間レンダーバッファの時も思ったけど、SRVをベースにしていると1つで済むのか?
ただし、UAV経由で更新する場合は領域が2つ必要だった。
更新時UploadとDefaultの2つが絡んでいるから、これが2つ分扱いになっているのか?
謎。


CopyQueue


ボーンのバッファは毎フレーム更新を行うことになるけど、そのデータのコピーについて今まで使ってなかった専用のコピーキューを用意してやってみようと思う。

D3D12_COMMAND_LIST_TYPE_COPYでCreateCommandQueueを作成する。
コマンドリスト、アロケータも同様にD3D12_COMMAND_LIST_TYPE_COPYで作成して、利用してみるとエラーになった。

D3D12 ERROR: ID3D12CommandList::ResourceBarrier: D3D12_RESOURCE_STATES has invalid flags for copy command list. [ RESOURCE_MANIPULATION ERROR #537: RESOURCE_BARRIER_INVALID_COMMAND_LIST_TYPE]

コピーで作ったリストにはコピーコマンドしか入れることができず、リソースの状態遷移はできないのか?
いろいろ調べてみるとコピーコマンドリストが理解できるステータスなら許容されるみたいだ。Beforeステータスには現在のステータスを設定していたが、これをD3D12_RESOURCE_STATE_COMMONにしてしまえばコピーコマンドのリストでも状態遷移ができた。真面目に今の状態をセットしていたのに、適当な値でいいなんて・・・。それまでは別のDIRECTコマンドのリストで遷移させていたがプログラムが多少スッキリした。


改善前

改善後

処理時間を計測してみた結果、改善前は11usぐらいで改善後は14us秒ぐらい掛かっている。



改善前のComputeコマンド

改善前は状態遷移で5us、コピーに1us、Dispatch前の状態遷移に0us、Dispatchに4us掛かっている。


改善後のCopyコマンド

改善後のComputeコマンド

改善後は、状態遷移に2us、コピーに1us、Dispatch前の状態遷移に7us、Dispatchに4us掛かっている。

これは失敗か?
更に改良を加えて、表示するメッシュを10に増やして測ってみた。
加えた改造は、状態遷移に時間が掛かっているように見えたので処理前にまとめて状態遷移をさせてしまうようにした。
今までは、対象メッシュが使うリソースを状態遷移させてシェーダ実行、次のメッシュが使うリソースを状態遷移させてシェーダ実行、というように状態遷移とシェーダ実行を交互に行っていた。それを描画対象のメッシュすべてのリソースを状態遷移させてから、シェーダ実行するようにした結果がこれ。

改善前

改善後

改善前は45usで、改善後は18us程度。
改善前は描画対象のメッシュが増えると処理も線形に延びていってしまう感じだが、改善後は1メッシュが14usで10メッシュでも18usなので、ほとんど増えていない。

改善前のComputeコマンド

改善前は並列に処理することが出来ず、何かする度に待ち時間が発生してしまっている感じになっている。


改善後のCopyコマンド

改善後のComputeコマンド

改善後はほとんど並列に処理できている。

ResourceBarrierは至るところにまとめてやれという風に書かれていたので、シェーダで使う単位ではまとめて遷移させるようにしていたけど、メッシュを横断してまとめてやった結果、かなり改善されてびっくり。1つだけの場合は逆に処理時間延びてまずいと思ったけど、数が増えるほど効果が出そう。


2023年2月1日水曜日

Cascade Shadow 2

以前カスケードシャドウを作ったけど、ベースはマイクロソフトのサンプルを元に自分のライブラリに移植した。
サンプルにはオブションがいくつもあり、その中から自分が使う部分のみを残して移植した。それでも結構なコード量で、半分以上は理解ができていない状態だった。

HLSLの魔導書にもカスケードシャドウについて書かれていて必要最小限の実装になっている。
今回はこれを改造して、自分のライブラリに移植する。


分割エリアを定義


最低限の実装なので魔導書では固定値で定義していた。

カメラに設定されているNearZ(1.0f)とFarZ(10000.0f)を取り出して、中間の分割位置をパラメータで指定する実装になっている。

このままでは色んな場面に対応できないので配置するメッシュからNearZとFarZを計算して、分割位置はパーセント指定するように修正した。

まず視線(カメラからフォーカス)ベクトルを準備する。
メッシュロード時に中心と半径はメッシュデータに保持しておくようにしてあるので、それを利用して、カメラからメッシュの中心までのベクトルと視線ベクトルの内積を計算。
結果に半径を足したものがMaxFarZを超えるなら更新、半径を引いたものがMinNearZよりも小さければ更新する。
最後にカメラのNearZよりも小さければNearZは補正する。


分割エリアを描画するためのライトビュープロジェクション行列の計算


この部分がカスケードシャドウのいちばん重要な部分だと思うが、難しくてよくわからない。
・ライトカメラのプロジェクション行列(XMMatrixOrthographicLH)とビュー行列(XMMatrixLookAtLH)からライトビュープロジェクション行列を求める。
・分割した領域の視錐台の8頂点を求める。
・頂点をライトビュープロジェクション空間に変換
・各頂点の最大、最小値を求めてクロップ行列を求める。
・クロップ行列にライトビュープロジェクション行列を乗算。
・出来上がった行列をシェーダに渡す。

仕組みとしてはこの行列でシェーダ内の頂点を変換して、XYが-1~1の範囲内に収まっていれば、その分割区間内ということが判断できるらしい。


分割区間別の深度バッファを用意


魔導書では一番近い場所の深度バッファから、遠くに行くに連れ縦横半分のサイズの深度バッファを複数枚を用意していた。
これを1枚の横長に分割数分拡張した深度バッファで処理することにする。

深度バッファ

こうすれば最終的なシェーダにわたすリソースは分割数が変わっても1つ分固定になる。


影描画


魔導書のシェーダはフォワードレンダリングで、頂点シェーダで4領域分の頂点計算をして、ピクセルシェーダに渡していた。
これは気持ち頂点計算が無駄な気がした(描画対象の領域が手前だとしても奥の分まですべて頂点計算をしてしまう)のと、この方法をディファードレンダリングではできないので、ピクセルシェーダ側に頂点計算を持っていった。
その後ディファードレンダリングで実装し直してみたら、今回はあっさりうまく行った。

影なし

影あり

カスケードデバッグ


問題点


つなぎ目のアーティファクト


デバッグ表示

つなぎ目にアーティファクト

たまに一番手前と次の影の境目に点線が表示される。
深度バッファギリギリまで参照していることが原因なので、一定範囲を超えたら一段階広い深度バッファを参照するように修正した。


謎の黒点


謎の黒い点

ディファードレンダリングで描画するように修正したら、黒い点が表示されるようになった。
原因はBRDFの計算の中にあるフレネル項の部分で、pow(1-cos, 5)という部分のcosの値が1を超えている場合、値がnanになり色が黒になっていた。
cosをmin( 1, cos )にしてpowの引数がマイナスにならないように修正した。


今後の課題


・ソフトシャドウ
・つなぎ目のぼかし

魔導書のカスケードシャドウのシェーダはすごくシンプル。
先にマイクロソフトのサンプルで実装経験があったので、今回はいろいろ改造することができるようになった。
また別の機会に改良することにする。

2022年12月27日火曜日

BlenderからExportしたFBXファイルについて

今のところBlenderからExportしたFBXファイルをAssimpで読み込んで使っている。

マテリアルの値を適当に取得はしているが、使っているのはAI_MATKEY_TEXTURE_DIFFUSEのみ。
DirectX12の魔導書に書いてある通り、Diffuse、Specular、Toonのテクスチャはマテリアルにあろうが、無かろうが用意して、シェーダは常にそれらをサンプリングしている。
この形がいいのかよくわからないけど、よくよく調べてみるとあらゆる項目に対してテクスチャを用意できるみたいなので、項目を増やすとこの実装はまずい気がする。

そもそも適当に決めた読み込むパラメータの見直しと、読み込んだパラメータをシェーダに反映させるように作り直したい。


読み込むパラメータの見直し


現状読み込んでいるのは、テクスチャが以下。
AI_MATKEY_TEXTURE_DIFFUSE
AI_MATKEY_TEXTURE_SPECULAR

各種色、パラメータが以下。
AI_MATKEY_COLOR_AMBIENT
AI_MATKEY_COLOR_DIFFUSE
AI_MATKEY_COLOR_SPECULAR

パラメータにはこの他に下記が定義されてた。
AI_MATKEY_SHININESS_STRENGTH
AI_MATKEY_SHININESS
AI_MATKEY_COLOR_EMISSIVE
AI_MATKEY_COLOR_TRANSPARENT
AI_MATKEY_COLOR_REFLECTIVE
AI_MATKEY_OPACITY
AI_MATKEY_REFLECTIVITY
AI_MATKEY_REFRACTI

Blenderで定義を変えてExportして、どこと紐づくのかを調べてみた。

Blenderのマテリアル

AI_MATKEY_COLOR_DIFFUSE


Blenderのラベルは「ベースカラー」
Blender側は色指定で、その値がそのまま渡る。
テクスチャを設定した場合は、AI_MATKEY_TEXTURE_DIFFUSEにファイルが設定され、カラーは0.8がRGBに返る。


AI_MATKEY_COLOR_SPECULAR


Blenderのラベルは「スペキュラー」
Blender側はスカラー指定で、受け取りはカラー。
各要素はDIFFUSEのRGB×スペキュラー値×0.5になっていた。
スペキュラー値が0なら0、0.5なら1/4、1なら1/2、2ならDIFFUSEと同じ値が返る。
テクスチャを設定した場合は、AI_MATKEY_TEXTURE_SPECULARにファイルが設定され、カラーは0.2がRGBに返る。


AI_MATKEY_SHININESS_STRENGTH
AI_MATKEY_SHININESS


Blenderのラベルは「粗さ」
どうやら、AI_MATKEY_SHININESS_STRENGTHとAI_MATKEY_SHININESSは同じ値が設定されているようだ。
Blender側はスカラー指定で、受け取りもスカラー。
設定値とは別の値が返る。

粗さとShininessの関係
横軸がBlenderの粗さで、縦軸がAssimpのShininessの値。
基本的にはBlenderで0~1の範囲で設定して、Assimpでは100~0の値が受け取れるので、0.01を掛けて1~0に変換して使う。

AI_MATKEY_COLOR_EMISSIVE


Blenderのラベルは「放射」と「放射の強さ」
Blender側はカラーとスカラー指定で、受け取りはカラー。
単純に放射の色に、放射の強さを掛けた値が返る。
テクスチャを設定した場合は、AI_MATKEY_TEXTURE_EMISSIVEにファイルが設定され、カラーは未設定になる。

AI_MATKEY_OPACITY


Blenderのラベルは「アルファ」
Blender側はスカラー指定で、受け取りはスカラー。
ちなみにテクスチャを指定してもAI_MATKEY_TEXTURE_OPACITYには設定されていなかった。

AI_MATKEY_COLOR_AMBIENT
AI_MATKEY_COLOR_TRANSPARENT
AI_MATKEY_COLOR_REFLECTIVE
AI_MATKEY_REFLECTIVITY
AI_MATKEY_REFRACTI

これらの値は紐づけがわからず。
Ambientは値自体は取得できて、常に0のカラーが返る。
ほかは設定されていない。

使ってるBlenderのバージョンは3.2、Assimpのバージョンは4.1で、最新のAssimpのヘッダを見るとかなり項目が増えていた。
Blenderのラベルと同じもの(メタリック、シーン、クリアコートなど)も用意されている。
Assimpのバージョンを上げると取れる項目は増えるのだろうか?


PBR


HLSLの魔導書に物理ベースレンダリングについて書かれていて、それを取り入れたいと思っているんだけど、この本のサンプルを見ても結果が不自然。
仕方がないのでネットで探してみると、Unityのシェーダが見つかったのでそれを参考に作ってみた。

マテリアルについてなんとなくわかってきたので、Blenderでパラメータかテクスチャを設定する方法を用意するつもりだけど、シェーダに指定するテクスチャの数を抑えるため1テクスチャにマージして、metalic(R)、roughness(G)、emissive(B)、未定(A)のようにする。

パラメータを使う場合は下記。

metallic

 blender:スペキュラー
 assimp:AI_MATKEY_COLOR_SPECULARでRGBの最大値。

roughness

 blender:粗さ
 assimp:AI_MATKEY_COLOR_SHININESS。

emissive

 blender:放射と放射の強さ
 assimp:AI_MATKEY_COLOR_EMISSIVEでRGBの最大値。

「スペキュラー」も「放射」もBlender上、色を設定するパラメータだけど、テクスチャで指定する方との互換性でスカラー値にする。スペキュラーは計算上メタリック扱いで、放射はアルベドカラーをそのまま光らせる方針。


テクスチャのマージ

モノクロのテクスチャをそれぞれ用意して、metalic(R)、roughness(G)、emissive(B)、未定(A)に割り当てて出力する。

metallic

emissive

出力結果


Emissiveなし

Emissiveあり

右の列がMetallicが高いオブジェクトで、上段と下段はパラメータで全体に指定、中段はテクスチャで枠だけ指定。
中央の列がMetallicが低いオブジェクトで、上段と下段はベースカラーに赤を指定、中段はテクスチャを指定。
左の列はEmissiveが指定してあるオブジェクトで、上段と下段はパラメータで全体に指定、中段はテクスチャで前面のみ光るように指定。

2022年12月16日金曜日

Bloom

今回はBloomについて。

DirectX12の魔導書に続き、HLSLシェーダーの魔導書も購入した。

前書の方では結果に欠陥があって自分で解決しろというスタイルだったので試してもなかったが、今作ってるレンダリングエンジンの機能に加えるため作ってみることにした。


処理は2つに分かれていて、1つ目が高輝度部分抽出で、2つ目が高輝度部分のブラー処理。
最後に元の画像と、ぼかした画像を加算合成するという流れ。


高輝度部分抽出


まず、HDRレンダリングを行うためにDXGI_FORMAT_R32G32B32A32_FLOATの中間レンダーターゲットを用意する。
元になる画像をこの中間レンダーターゲットにコピーする際、
dot( color.rgb, float3( 0.2125f, 0.7154f, 0.0721f )) ;
この内積の結果が1以上の場合出力するとなっているが、真っ黒な絵しか出力されなかった。
ライトを強めにするとか書いてあるが、コピー部分にはライトが関係なさそうでよくわからない。
ネットで調べてみたところ、同じように出来上がった画像の高輝度部分を抽出する場合、
color.rgb = saturate( color.rgb - 0.9f ) * 10.0f ;
こんな感じになっていた。
実際にこんな方法で抽出した場所を光らせても、求めてるものと違うような気がするがとりあえずはこれでやってみることにする。


ブラー処理 


これは以前作った処理を呼び出せば良いと思ったんだけど、少し問題があった。
静止画の場合は問題なさそうだけど、画面が動くとぼかしの品質が悪くてぼやぼやする感じになった。
ブラーを掛ける前のリソースはミップマップテクスチャでないと品質的に耐えられなさそうなのでミップマップ化する処理を間に追加し、そのミップマップテクスチャでブラーをかけることにした。


最終的な処理の流れ


処理フロー図


1.元画像準備


ブルーム対象の画像。
高輝度のものだけを出力した画像が渡された場合は、次の処理をスキップできるようにした。


2.高輝度部分抽出


今回は色の各成分が0.9以上の箇所を抽出するようにしてみたが、実際には使えないと思ってる。
モデルのEmissiveが値を持っている箇所のみをレンダリングした画像を元画像として出力する仕組みを作ってみる予定。


3.高輝度画像ミップマップ化


ファイル指定のテクスチャをミップマップ付きで用意する仕組みはあったけど、動的なリソースに対してミップマップを作る仕組みがなかった。
なので任意のSRVを指定してミップマップテクスチャを作成する仕組みを用意した。


4.ブラー処理


ミップマップテクスチャを元にブラー処理をかける。


5.元画像とぼかし画像の加算合成


最後は元画像と、ブラー画像を加算合成する。
ブラー画像に対する重みパラメータと、合成後の掛け目のパラメータを用意した。


出力結果


通常レンダリング結果

このレンダリング結果に対してブルーム処理をかけても、高輝度部分が抽出できなかった。

鏡面反射

スペキュラの計算を付け加えて全体をテカテカにして、これに対してブルーム処理をかけてみる。

ブルーム結果

それっぽい結果になった。

ブルーム結果2

パラメータを変えて、ブラー画像を2倍にして、合成結果に0.5かけたもの。


HDRレンダリングってなんだ?


高輝度抽出画像は最初DXGI_FORMAT_R32G32B32A32_FLOATでやっていたが、いつものDXGI_FORMAT_R8G8B8A8_UNORMでも結果が変わらなかったので戻した。
シェーダの結果として1以上の明るさを設定できるんだろうけど、レンダーターゲットはDXGI_FORMAT_R8G8B8A8_UNORMのままなので、1以上は1扱いでそのまま出力されたのか?
レンダーターゲットもDXGI_FORMAT_R32G32B32A32_FLOATで作ればもっと眩しい感じになるのか?
そもそもそれっぽく見せるために、光源をぼかして範囲を広げて周りに光を漏れさせた絵を用意したけど、そんな事せずに光ってる部分の値を1000とか1万とかにすると、ディスプレイ自体がめちゃくちゃ光ってそういうふうに見せてくれないのかな?






2022年12月1日木曜日

CS版軽量なぼかし処理

前々回で、リアルタイムにぼかす処理を作ってみたけど、実際には静止画にぼかしフィルタを掛けているだけ。
ぼかす対象は静止画だったので毎フレーム更新する必要はない。

前に作ったのはリアルタイム用として、今回は1回だけの静止画用をコンピュートシェーダ(CS)で作って見ようと思う。


リアルタイム用は2枚の中間レンダーターゲットを用意して処理していた。
静止画用は空のテクスチャ2枚を用意して、CSで書き込むことになる。
テクスチャに書き込むのにUnorderedAccessView(UAV)をテクスチャに紐づけて、Dispatchする際には、用意したテクスチャではなくUAVの方を指定する。

#define DefRS "RootFlags(DENY_VERTEX_SHADER_ROOT_ACCESS|DENY_HULL_SHADER_ROOT_ACCESS|DENY_DOMAIN_SHADER_ROOT_ACCESS|DENY_GEOMETRY_SHADER_ROOT_ACCESS|DENY_PIXEL_SHADER_ROOT_ACCESS),DescriptorTable( Sampler(s0),visibility=SHADER_VISIBILITY_ALL),DescriptorTable( SRV(t0, flags=DATA_STATIC_WHILE_SET_AT_EXECUTE),visibility=SHADER_VISIBILITY_ALL),DescriptorTable( UAV(u0, flags=DATA_VOLATILE),visibility=SHADER_VISIBILITY_ALL),RootConstants( num32BitConstants=2, b0,visibility=SHADER_VISIBILITY_ALL)"

SamplerState Sampler : register(s0) ;
Texture2D TexColor : register(t0) ;        // ぼかし対象画像
RWTexture2D<float4> UAV : register(u0) ;    // 結果画像
struct _Param { 
	float2	Scale ;	// 倍率
} ;
ConstantBuffer<_Param> Param : register(b0) ;

static const int BLUR_SAMPLE_COUNT = 8 ;
static const float2 BLUR_KERNEL[BLUR_SAMPLE_COUNT] = {
	float2(-1.0f, -1.0f),
	float2(-1.0f,  1.0f),
	float2( 1.0f, -1.0f),
	float2( 1.0f,  1.0f),
	float2(-0.5f,  0.0f),
	float2( 0.0f,  0.5f),
	float2( 0.5f,  0.0f),
	float2( 0.0f, -0.5f),
} ;
						
struct CSInput
{
	uint3 ID : SV_DispatchThreadID ;
} ;

[RootSignature(DefRS)] 
[numthreads( 8, 8, 1 )]
void CSMain( CSInput In )
{
	float2 uv = In.ID.xy + 0.5f ;
	float4 color = 0 ;
	for( int j = 0 ; j < BLUR_SAMPLE_COUNT ; j++ ) {
		color += TexColor.Sample( Sampler, ( uv + BLUR_KERNEL[j]) * Param.Scale ) ;
	}
	UAV[ In.ID.xy ] = float4( color.rgb / float( BLUR_SAMPLE_COUNT ), 1.0f ) ;

	return ;
}

シェーダの内容はほとんど変わらない。
ただ1つ重要なポイントがあって、リアルタイム用の時は意識してなかったんだけど渡すテクスチャにミップマップをつけておくこと。
1段階目で一気に縮小するので、その画像をたった8個のサンプリングでは品質が保てなく、実はかなりの部分をミップマップに助けてもらっていた。ミップマップは事前(読み込み時)に1回だけの処理なので、パフォーマンスには影響がない。
だけど、リアルタイムの方ではレンダリング結果にブラーを掛ける場合はミップマップの恩恵は得られない。品質の話はブラー強度を徐々に高めたりして前後を比較して見たときに気になるレベルなので、強度が一定であればミップマップなしでも許容範囲だと思う。


パフォーマンス 


リアルタイムの方は、ぼかし強度によって365~178usだった。
CS版はぼかし強度によって276~14usぐらい。(Dispatch部分のみ)

結構軽そうなので毎フレーム処理するようにしてみたら432~166us。
ぼかし強度が強い場合はリアルタイムでも使ったほうがいい結果になった。

ぼかし強度が弱い場合、そんなにサンプリングしなくても品質が落ちないので試しにBLUR_SAMPLE_COUNTを4にしたら309~163usだった。
ぼかし強度に合わせてサンプル数を調節してやれば、CS版のほうが高速っぽい。

ライブラリにまとめる際、リアルタイム版は結果が中間レンダーターゲット、描画に必要なもう1つの中間レンダーターゲットも常に保持しておく必要があるのに対し、CS版は結果がテクスチャで、描画に必要だったUAV2つと、もう1つのテクスチャは削除できる。