2022年8月13日土曜日

カメラを動かす

もしかしたら、3Dのプログラムで一番最初に用意しないといけなかったものかもしれない。
今まで見てきたサンプルプログラムでもカメラクラスが存在するものもあり、いろいろな機能を持っていた。
自分でもそのうち用意しなくては、とは思っていた。

カメラクラスを作る


そもそもカメラってなんだ。
3Dのプログラムを作る時に出てくるのが、World、View、Projectionの行列。
この行列のViewがカメラではないかと思う。Projectionはどうなんだろ?アスペクト比を出すのにスクリーンの幅と高さが必要になる。この点が、カメラに入らない感じがするんだけど、視野角とZの描画範囲はカメラのような気もするがとりあえず含めないでおく。
具体的にどんな機能が必要になるかもわからないので、最低限コントローラの左スティックで、XZ平面を移動、右スティックで横(Y軸)回転、縦(XZ軸)回転、縦(Y軸)移動ができるようにする。

XZ平面移動


横軸がXで奥行きがZなのでXZ平面になる。
カメラに保持する変数何を持ったらいいか考えたけど、View行列を作るために必要な3つのVectorを持つことにした。
1つは視点、もう1つは注視点、最後にカメラの上方向。

視点(eye) 


視点はカメラの位置。これを動かせば移動するだろう。

注視点(focus)


注視点はカメラが見る場所。移動する場合はこれも一緒に動かす必要がある。
動かさない場合はそこをずっと見ることになるので、例えば注視点を中心に回転した場合は見てる場所を中心に回れる。

カメラの上方向(up)


これはイマイチよくわからないんだけど、通常のカメラを持った状態であれば{0.0f,1.0f,0.0f}とすれば良い。これを回すと見てる映像が回転するのかと思ったけど回転はせず、ある一定値を超えると反転するだけだった。

移動

tV	Move( tF4 x, tF4 y, tF4 z ) {
	this->oEye.x += x ;
	this->oEye.y += y ;
	this->oEye.z += z ;
	this->oFocus.x += x ;
	this->oFocus.y += y ;
	this->oFocus.z += z ;
}

コントローラの左スティックのxとyの移動量をeyeとfocusのxとzに足してみると、とりあえず思い通りに動いた。yは後で上下移動のために用意したので0を渡している


横(Y軸)回転


次に周りを見渡せるようにY軸で回転させてみる。
右スティックのxの移動量を角度として利用する。

tV	Rotate( tF4 x ) {
	tDVector eye = this->Eye() ;
	tDVector focus = this->Focus() ;
	focus = DirectX::XMVectorSubtract( focus, eye ) ;
	tDVector q = DirectX::XMQuaternionRotationAxis( mDVector3( this->Up()), DirectX::XMConvertToRadians( x )) ;
	focus = DirectX::XMVector3Rotate( focus, q ) ;
	mDVectorToF3( this->oFocus, DirectX::XMVectorAdd( focus, eye )) ;
}

これで左右を見渡せるようになった。

だけど、ちょっとおかしい。
回転自体は問題ないけど、回転した後の移動がおかしくなる。
左スティックを前に倒したとき、向いている方向に進むのではなく、常に最初向いていた方向に進む。これは当たり前で、eye.xとeye.zにそのまま加算しているから。
向いた方向に対し進むようにするには現在の回転状況を加味してxとzを加算する必要がある。

移動修正版

tV	Move( tF4 x, tF4 y, tF4 z ) {
	tF4 r = ::atan2( this->oFocus.x - this->oEye.x, this->oFocus.z - this->oEye.z ) ;	// 目線の角度を求める
	tDVector qt = DirectX::XMQuaternionRotationAxis( mDVector3( this->Up()), r ) ;
	tDMatrix mq = DirectX::XMMatrixRotationQuaternion( qt ) ;
	tDFloat3 d ;
	mDVectorToF3( d, DirectX::XMVector3TransformCoord( mDVector3( x, y, z ), mq )) ;
	this->oEye.x += d.x ;
	this->oEye.y += d.y ;
	this->oEye.z += d.z ;
	this->oFocus.x += d.x ;
	this->oFocus.y += d.y ;
	this->oFocus.z += d.z ;
}

どうやって前に進ませるか調べてみたが、現在の角度を持ってそれを元に計算していた。
角度は持ちたくなくて、視点の注視点から角度が求められるはずと思い調べてみると、アークタンジェントを使ってラジアンが得られることがわかった。
求めた角度を使ってクォータニオン(qt)を作って、それをマトリックス(mq)にする。
肝の関数がXMVector3TransformCoordらしく、これにクォータニオンのマトリックスを渡すと、実際に動かす量が得られる。

縦(XZ軸)回転


tV	Rotate( tF4 x, tF4 y ) {
	tDVector eye = this->Eye() ;
	tDVector focus = this->Focus() ;
	tDVector up = this->Up() ;
	focus = DirectX::XMVectorSubtract( focus, eye ) ;
	tDVector axis = DirectX::XMVector3Normalize( DirectX::XMVector3Cross( focus, up )) ;
	tDVector qx = DirectX::XMQuaternionRotationAxis( axis, DirectX::XMConvertToRadians( y )) ;
	tDVector qy = DirectX::XMQuaternionRotationAxis( mDVector3( this->Up()), DirectX::XMConvertToRadians( x )) ;
	focus = DirectX::XMVector3Rotate( focus, DirectX::XMQuaternionMultiply( qx, qy )) ;
	mDVectorToF3( this->oFocus, DirectX::XMVectorAdd( focus, eye )) ;
}

回転の関数を改良して、右スティックの上下の移動量も渡せるようにした。
qxを算出するためのaxisを最初{ 1.0f, 0.0f, 0.0f }としていた。
これも最初の向きの場合は問題なく動くが、90度回転した状態では変な動きになり、180度回転した状態では上下が反対になってしまった。これも今の回転状態に合わせてx軸だけではなく、xz軸で動かす必要がある。この軸を作るのが、XMVector3Crossで外積の結果を正規化したものが軸になる。
出来上がったqxとqyをXMQuaternionMultiplyで合成して回転させる。

縦(Y軸)移動


縦移動は、Move関数のyにコントローラの左右のトリガーを渡すようにした。
	auto oLStick = this->oIDev.LStick() ;
	auto oRStick = this->oIDev.RStick() ;
	auto oLTrigger = this->oIDev.LTriger() ;
	auto oRTrigger = this->oIDev.RTriger() ;
	this->oCam.Move( oLStick.x, oLTrigger > 0.0f ? -oLTrigger : oRTrigger, oLStick.y ) ;
	this->oCam.Rotate( oRStick.x, oRStick.y ) ;
oIDevがコントローラで、oCamがカメラ。
トリガーは両方同時に押せてしまうので、左トリガーの値がある場合はマイナスの左トリガー、ない場合は右のトリガーの値を渡すようにした。

おっさんの後ろ姿

これでマイクラのクリエイティブモードみたいな感じで3D空間を自由に動かせて、見渡せるようになった。

2022年8月11日木曜日

XInput

DirectX11用のライブラリを作ったとき、DirectInputでコントローラを使えるようにした。
DirectX12用のライブラリではこれは捨てて、新しくXInputを採用することにした。

初期化


DirectInputの時はDirectInput8Createで初期化して、CreateDevice、SetDataFormat、SetCooperativeLevel、Acquire、Pollとこれだけの関数を呼び出してやっと使えた。
CreateDeviceをすることによりキーボード、マウス、コントローラを同じように扱える。

XInputではどうなるか調べたら、初期化関数に当たるものがなかった。
インクルードファイルと、リンクライブラリはこれ。

#include <xinput.h>
#pragma comment (lib, "xinput.lib")

状態取得


状態取得関数は、XInputGetState。
これにコントローラの番号とXINPUT_STATEを渡す。
取得できたら、XINPUT_GAMEPADの中に結果が返る。

コントローラの番号は0~XUSER_MAX_COUNT-1まで渡せる。
XUSER_MAX_COUNTは4で定義されているので最大4つまで認識できる。

 wButtons


下記マクロでボタンのOn/Off状態が取得できる。
  • XINPUT_GAMEPAD_DPAD_UP
  • XINPUT_GAMEPAD_DPAD_DOWN
  • XINPUT_GAMEPAD_DPAD_LEFT
  • XINPUT_GAMEPAD_DPAD_RIGHT
  • XINPUT_GAMEPAD_START
  • XINPUT_GAMEPAD_BACK
  • XINPUT_GAMEPAD_LEFT_THUMB
  • XINPUT_GAMEPAD_RIGHT_THUMB
  • XINPUT_GAMEPAD_LEFT_SHOULDER
  • XINPUT_GAMEPAD_RIGHT_SHOULDER
  • XINPUT_GAMEPAD_A
  • XINPUT_GAMEPAD_B
  • XINPUT_GAMEPAD_X
  • XINPUT_GAMEPAD_Y

bLeftTrigger、bRightTrigger


トリガーボタンの押し込み状態が0~255で取得できる。
ただし、そのまま使わずXINPUT_GAMEPAD_TRIGGER_THRESHOLD未満の値は捨てて0扱いにする。

sThumbLX、sThumbLY


左スティックの状態が-32768~32767で取得できる。
ただし、そのまま使わず絶対値でXINPUT_GAMEPAD_LEFT_THUMB_DEADZONE未満の値は捨てて0扱いにする。

sThumbRX、sThumbRY


右スティックの状態が-32768~32767で取得できる。
ただし、そのまま使わず絶対値でXINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE未満の値は捨てて0扱いにする。

バイブレーション


振動の設定はXINPUT_VIBRATIONのwLeftMotorSpeedとwRightMotorSpeedに0~65535の値を設定して、XInputSetState関数を呼び出す。


その他


XInputEnable


falseを渡すと、コントローラの状態がすべてニュートラルの状態で返却されるようになる。
アクティブ画面でない場合など、コントローラを効かなくする場合に使うかも。
ただ、false状態にするとXInputSetStateも受け付けなくなるので、もし振動状態ならリセットしてからfalseにしないと振動しっぱなしになる。

XInputGetKeystroke


1つのボタン単位で状態を取得できる。状態もOn/Offのみでなく、押された瞬間、押しっぱなし、離した瞬間が判る。
ボタンの指定は「VK_PAD_」から始まるマクロが用意されている。
自分で前回のState状態を保持するようにするのでこの関数は使わないかな。

キーボードとのマッピング


直接状態を参照するとキーボードとコントローラを両方対応する場合に面倒になるため1枚レイヤを噛ませて、キーのマッピング情報を用意してプログラムからは透過的に使えるようにする。
キーボードの状態はGetKeybordStateでまとめて取得する。

アナログの値


トリガーとスティックの値はそれぞれの最大値がありそのままでは使いづらい。
最大値で割って0~1の範囲で使えるようにする。この時それぞれのデッドゾーンがあり、あるところからいきなり値が発生するので、デッドゾーンを超えたところから最大値までの範囲で補間する。

2022年8月7日日曜日

HLSLでRootSignature定義

前々からちょこちょこと見かけてはいたんだけど、何故かサンプルのシェーダの中にRootSignatureが定義されているものがある。
CreateRootSignatureを呼ぶ前で、すごい複雑な構造体をごちゃごちゃやって作ってるのに、また別にHLSLでも定義して何なんだと思っていた。

今までスルーしてきたけど丁度ルートパラメータ周りを見直すことにしたので、HLSLで定義しているRootSignatureについて調べてみた。

ここによると、どうやら本当にHLSL側で定義出来て、しかもコンパイルしたシェーダからD3DGetBlobPartでRootSignatureのBlobを抜き出して、CreateRootSignatureに渡せるらしい。

シェーダ定義の例

this->oMipmapShader->InitCompute( this, 
{
	{ tShaderIF::lRSDefine, {
		// RootParamater
		{	// 0 テクスチャ
			{ tShaderIF::lParamRSType	, tShaderIF::lRSTypeSRV },
			{ tShaderIF::lParamName,	L"SrcTex" },
		},
		{	// 1 書き込み用テクスチャ
			{ tShaderIF::lParamRSType	, tShaderIF::lRSTypeTexUAV },
			{ tShaderIF::lParamName,	L"DstTex" },
		},
		{	// 2 Dimension
			{ tShaderIF::lParamRSType,	tShaderIF::lRSTypeCBV32 },
			{ tShaderIF::lParamSource,	LR"---(
				uint SrcMipLev ;
				float2 TexelSize ;
			)---" },
			{ tShaderIF::lParamName,	L"Dim" },
		},
		{	// 3 サンプラ
			{ tShaderIF::lParamRSType	, tShaderIF::lRSTypeStaticSampler },
			{ tShaderIF::lParamName,	L"Sampler" },
		},
	}},
	{ tShaderIF::lCSDefine, {
		{ tShaderIF::lParamName,		L"MipmapShader" },
		{ tShaderIF::lParamHeader,		L"[numthreads( 8, 8, 1 )]" },
		{	// 0
			{ tShaderIF::lParamSemantic	, L"SV_DispatchThreadID"	},
			{ tShaderIF::lParamSource	, L"uint3 ID"		},
		},
		{ tShaderIF::lParamSource, LR"---(
			float2 uv = gDim.TexelSize * ( In.ID.xy + 0.5f ) ;
			DstTex[ In.ID.xy ] = SrcTex.SampleLevel( Sampler, uv, gDim.SrcMipLev ) ;
		)---", },
	}},
}, true ) ;

これは前回のミップマップを生成するためのComputeシェーダで、大きく分けるとRSDefineのルート署名の定義と、それ以外のシェーダの定義に分かれる。この例ではRSDefineに参照元のSRV、書き込み用UAV、パラメータ用のCBV、テクスチャを参照するためのスタティックサンプラが定義されている。
それぞれRSTypeと名称を定義、必要に応じて構造体定義のソースなども定義できる。

この定義を元にD3D12_DESCRIPTOR_RANGE1、D3D12_ROOT_PARAMETER1とシェーダソースを自動生成してコンパイルを行っていた。
これが、シェーダソースのみで良くなると言うことだ。

早速試して見ようとしたがD3DGetBlobPartはShaderModel5.1までのもので、6に切り替えたせいでもう呼べない。新しい方ではコンパイルの結果のResultからGetOutputで取り出すっぽい。ただ、今までのResultはIDxcOperationResultでGetOutputはない。
IDxcCompiler2からIDxcCompiler3に変える必要があるみたいだ。

以前DirectXShaderCompilerに切り替えた時、IDxcCompiler2でコンパイルできるようにした。他のI/Fもそうだけど単純にこの数字を上げれば使える関数が増えるので定義をIDxcCompiler2からIDxcCompiler3に変えてみたところ、色が変わらない。
ヘッダに存在しないようだ。
自分のPCに入っているdxcapi.hのバージョンが古いみたいで、VisualStudioインストーラからWindowsSDKを入れることにした。


Windows SDK インストール

既に入っているSDKは「Windows 10 SDK (10.0.19041.0)」だった。
インクルードパスが競合して新しいのが入っているのに見れないとか嫌なので、古いのはアンインストールするためチェックを外した。
新しいのは「Windows 10 SDK(10.0.20348.0)」というのもあるけど、使ってるOSがWin11と言うのもあり「Windows 11 SDK (10.0.22000.0)」にチェックを入れてインストールした。

インストール後、IDxcCompiler3に変えてみると色が変わったのでヘッダも新しいのに入れ替わったみたいだ。

新しいコンパイル関数

	tCom<IDxcUtils> oUtil ;
	HRESULT nRet = ::DxcCreateInstance( CLSID_DxcUtils, IID_PPV_ARGS( &oUtil )) ;
	if( FAILED( nRet )) { mLogWinAPI( DxcCreateInstance, nRet ) ; return ; }
	tCom<IDxcCompiler3> oCompiler ;
	nRet = ::DxcCreateInstance( CLSID_DxcCompiler, IID_PPV_ARGS( &oCompiler )) ;
	if( FAILED( nRet )) { mLogWinAPI( DxcCreateInstance, nRet ) ; return ; }

	DxcBuffer oSource = {} ;
	oSource.Ptr = sSrc.Ptr() ;
	oSource.Size = sSrc.LenB() ;
	oSource.Encoding = DXC_CP_UTF16 ;

	tStr sTarget = tStr(L"-T %s"_fs, sModel ) ;
	tStr sEntryPoint = tStr(L"-E %s"_fs, sFunc ) ;
#if defined(_DEBUG)
	cWS pArgs[] = { DXC_ARG_ENABLE_STRICTNESS, DXC_ARG_DEBUG, DXC_ARG_SKIP_OPTIMIZATIONS, L"-Qembed_debug", L"-rootsig-define DefRS", sTarget.Ptr(), sEntryPoint.Ptr(), sSrcName } ;
#else
	cWS pArgs[] = { DXC_ARG_ENABLE_STRICTNESS, DXC_ARG_OPTIMIZATION_LEVEL3, L"-rootsig-define DefRS", sTarget.Ptr(), sEntryPoint.Ptr(), sSrcName } ;
#endif
	tCom<IDxcResult> oResult ;
	nRet = this->oCompiler->Compile( &oSource, pArgs, tU4( fArray( pArgs )), nullptr, IID_PPV_ARGS( &oResult )) ;
	if( FAILED( nRet )) {
		mLogWinAPI( IDxcCompiler::Compile, nRet ) ;
		return false ;
	}

	oResult->GetStatus( &nRet ) ;
	if( FAILED( nRet )) {
		tCom<IDxcBlobEncoding> oErr ;
		oResult->GetErrorBuffer( &oErr ) ;
		tCom<IDxcBlobUtf16> oErr16 ;
		oUtil->GetBlobAsUtf16( oErr.Get(), &oErr16 ) ;
		mLogE( sModel, cWS( oErr16->GetBufferPointer())) ;
		return false ;
	}

	// Root署名
	nRet = oResult->GetOutput( DXC_OUT_ROOT_SIGNATURE, IID_PPV_ARGS( this->oRSB.ReleaseAndGetAddressOf()), nullptr ) ;
	if( FAILED( nRet )) {
		mLogWinAPI( IDxcResult::GetOutput, nRet ) ;
		return false ;
	}

IDxcCompiler3はIDxcCompiler2とは大きく変わっていて、まずCompileの第一引数がIDxcBlobEncodingを渡していたのがDxcBufferに変わった。
IDxcLibrary::CreateBlobWithEncodingFromPinnedでUTF-8文字列をBlobに変換していたのが、DxcBufferの構造体に値を入れればいいだけになる。
Ptrにポインタ、Sizeにバイト数、EncodingにDXC_CP_UTF8/16を指定する。
コードページの定義にDXC_CP_UTF16も追加されていて、ついにUTF16のままコンパイルすることが出来た。デバッガーで見る時UTF8が文字列としてクイックウォッチなどで見れないのが地味に使いづらかった。もしかするとIDxcCompiler2でもコードページ1200を指定して、CreateBlobWithEncodingFromPinnedの第2引数に文字数ではなくバイト数を指定したらうまく行ってたのかも。

あとIDxcLibraryは、IDxcUtilsに交代するみたい。

次にソース名、エントリーポイント、シェーダモデル、オプションと引数が続いていたけど廃止されてオプションのみとなった。
IDxcUtils::BuildArgumentsで以前と同じように指定が可能だけど、IDxcCompilerArgsというオブジェクトを用意しないといけないので、使わず直接オプションですべて指定することにした。

エントリーポイントは「-E 関数名」で指定する。
シェーダモデルは「-T モデル名」で指定する。
オプションは一部マクロが用意されていた。
ソース名は最初どうやって指定するかわからなかったけど、オプションの最後にハイフンなしで名称指定したらコンパイルエラー時に指定名称が表示された。

最後の引数はIDxcOperationResultからIDxcResultに変えたオブジェクトで結果を受け取る。
で、肝心のルート署名はIDxcResult::GetOutputの第一引数にDXC_OUT_ROOT_SIGNATUREを指定することで取り出せる。
コンパイル時のオプションに「-rootsig-define」を指定してどの定義がルート署名のdefineかを教えてあげる必要がある。

今までのルート署名生成

	D3D12_FEATURE_DATA_ROOT_SIGNATURE oFDRS = {} ;
	oFDRS.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_1 ;
	nRet = pDev->CheckFeatureSupport( D3D12_FEATURE_ROOT_SIGNATURE, &oFDRS, sizeof( oFDRS )) ;
	if( FAILED( nRet )) oFDRS.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_0 ;

	tVector<D3D12_DESCRIPTOR_RANGE1> oDRL ;
	tVector<D3D12_ROOT_PARAMETER1> oRPL ;
	tVector<D3D12_STATIC_SAMPLER_DESC> oSSL ;
	auto [ sVSSrc, sPSSrc ] = this->GenerateShaderSource( oDef, oDRL, oRPL, oSSL ) ;
	if( sVSSrc.Len() <= 0 ) return false ;
	if( sPSSrc.Len()) bPS = true ;

	D3D12_VERSIONED_ROOT_SIGNATURE_DESC oVRSD = {} ;
	oVRSD.Version = oFDRS.HighestVersion ;
	auto & oDesc = oVRSD.Desc_1_1 ;
	oDesc.NumParameters = tU4( oRPL.Count()) ;
	oDesc.pParameters = oRPL.Ptr() ;
	oDesc.NumStaticSamplers = tU4( oSSL.Count()) ;
	oDesc.pStaticSamplers = oSSL.Ptr() ;
	oDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_NONE
	|	D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT
	|	D3D12_ROOT_SIGNATURE_FLAG_DENY_HULL_SHADER_ROOT_ACCESS
	|	D3D12_ROOT_SIGNATURE_FLAG_DENY_DOMAIN_SHADER_ROOT_ACCESS
	|	D3D12_ROOT_SIGNATURE_FLAG_DENY_GEOMETRY_SHADER_ROOT_ACCESS
	;

	tStr sSrcName ;
	cAny & oVSDef = oDef[ lVSDefine ] ;
	if( oVSDef.IsExist( lParamName )) sSrcName = oVSDef.Value().ToStr() ;
	if( !this->Compile( lStageVS, sVSSrc, sSrcName, lVSFunc )) break ;
	if( bPS ) {
		cAny & oPSDef = oDef[ lPSDefine ] ;
		if( oPSDef.IsExist( lParamName )) sSrcName = oPSDef.Value().ToStr() ;	// PSのNameがない場合VSのNameが受け継がれる
		if( !this->Compile( lStagePS, sPSSrc, sSrcName, lPSFunc )) break ;
	}

	tCom<ID3DBlob> oSignature ;
	tCom<ID3DBlob> oError ;
	nRet = ::D3D12SerializeVersionedRootSignature( &oVRSD, &oSignature, &oError ) ;
	if( FAILED( nRet )) { mLogWinAPI( D3D12SerializeVersionedRootSignature, nRet ) ; break ; }

	nRet = pDev->CreateRootSignature( 0, oSignature->GetBufferPointer(), oSignature->GetBufferSize(), IID_PPV_ARGS( &this->oRS )) ;
	if( FAILED( nRet )) { mLogWinAPI( ID3D12Device::CreateRootSignature, nRet ) ; break ; }

バージョンをチェックして1.1が使えない場合は1.0にする。GenerateShaderSourceで定義から各パラメータとシェーダソースを生成する。その結果を元にD3D12_VERSIONED_ROOT_SIGNATURE_DESCを作って、ソースはコンパイル。最後に署名をシリアライズ化して、CreateRootSignatureを呼び出す。

新しいルート署名生成

	auto [ sVSSrc, sPSSrc ] = this->GenerateShaderSource( oDef ) ;
	if( sVSSrc.Len() <= 0 ) return false ;
	if( sPSSrc.Len()) bPS = true ;

	tStr sSrcName ;
	cAny & oVSDef = oDef[ lVSDefine ] ;
	if( oVSDef.IsExist( lParamName )) sSrcName = oVSDef.Value().ToStr() ;
	tCom<IDxcBlob>	oRSB ;	// Root署名Blob
	if( !this->Compile( lStageVS, sVSSrc, sSrcName, lVSFunc, &oRSB )) break ;
	if( bPS ) {
		cAny & oPSDef = oDef[ lPSDefine ] ;
		if( oPSDef.IsExist( lParamName )) sSrcName = oPSDef.Value().ToStr() ;	// PSのNameがない場合VSのNameが受け継がれる
		if( !this->Compile( lStagePS, sPSSrc, sSrcName, lPSFunc )) break ;
	}

	nRet = pDev->CreateRootSignature( 0, oRSB->GetBufferPointer(), oRSB->GetBufferSize(), IID_PPV_ARGS( &this->oRS )) ;
	if( FAILED( nRet )) { mLogWinAPI( ID3D12Device::CreateRootSignature, nRet ) ; break ; }

シェーダソースを生成して、コンパイル。その際にルート署名も取り出してCreateRootSignatureを呼び出す。シェーダソース生成部分も余計な構造体の設定がなくなってスッキリした。
ただ、ルート署名バージョン1.1が使えない環境の場合どうなるのかわからない。
元のソースは構造体に1.1のデータが含まれているけど、バージョン指定に1.0と指定すれば無視してくれていた。こっちの場合はバージョン指定が無いので、1.1のデータをそもそも含めてはいけないのか?勝手に無視してくれないだろうか?
必要であればD3D12_FEATURE_ROOT_SIGNATUREをチェックして、シェーダソース生成時に1.1のデータは出力しないようにしよう。

今回の嵌まりポイント


ルート署名の定義にTabを含められない


ルート署名の文字列にTabが含まれるとエラーになる。
Tabと改行を全部空文字に置き換え後、全体をダブルクォートで囲んで1行の定義にした。

1.1のflagsなしだと動作がかわる


リソースの状態遷移について大量にエラーが出力されていた。
今まではD3D12_DESCRIPTOR_RANGE_FLAG_NONEで済ませてきたので、とりあえず無指定にして実行したらだめだった。
応急処置としてUAVはDATA_VOLATILE、それ以外にはDATA_STATIC_WHILE_SET_AT_EXECUTEを指定したけどまだだめ。
SRVの一部はテクスチャとRTVとして使うリソースもあるので、SRVもDATA_VOLATILEにしたらとりあえずエラーがでなくなった。
D3D12_DESCRIPTOR_RANGE_FLAG_NONEの動作はSRVの場合DATA_STATIC_WHILE_SET_AT_EXECUTEのはず。エラーにはリソースステータスはD3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE|D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCEでないとだめなのにD3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCEになってると言われる。
今まで通常のRenderとComputeで、テクスチャを扱う際にステータスをそれぞれ設定していたけど、同時に設定しておくものらしい。D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE部分をすべてD3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE|D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCEに書き換えたらDATA_STATIC_WHILE_SET_AT_EXECUTEでエラーが出なくなった。

 


2022年8月6日土曜日

Compute ShaderでMipmap生成

前回ComputeShaderの仕組みを作った。
結構前にこのサイトを見つけて、ComputeShaderの仕組みができたらMipmap機能を作ろうと思っていた。
このサイト曰く、今までは自動的にミップマップを生成してくれる仕組みは提供されていたけど、DirectX12になってからその機能がなくなってしまったとのこと。確かテクスチャ作る際にミップマップのレベルに0を指定すれば勝手に作ってくれていた記憶はある。このひとが、マイクロソフトのサンプルにあるMiniEnginから必要な部分のみ抜き出して公開してくれている。

このソースを参考に作ろうと思ったんだけど色々とわからない部分が多い。
ソース自体はそんなに長くもなく、直ぐにできるかと思ったんだけどAPIではないMiniEngin内のクラスや関数が使われていてこのソースだけでは作れなさそうだった。

別のサイトを探したら、ここが見つかった。
すごく詳しく書いてあって、ちゃんとしてそう。
ただ、元テクスチャサイズが奇数の場合の対処方法や、SRGBについての処理などもしてあってものすごい汎用性が高いので、その分ソース量が多くなる。
自分で用意するリソースは、テクスチャサイズは2の累乗にするし、SRGBとかも扱わないから、その辺の処理を外すと元のサイトぐらいにシンプルになりそう。

でも今回は結構嵌った。

コンピュートシェーダに対する思い違い


前回はComputeShaderで頂点バッファを扱ったけど、今回はテクスチャを扱う。
リソースの使い方が変わるのでステータス遷移させる必要があるけど、そこで怒られる。
テクスチャはD3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCEの状態なので、D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCEに遷移させようとすると怒られる。1つ目のサンプルはD3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCEから D3D12_RESOURCE_STATE_UNORDERED_ACCESSにしようとしているし、2つ目のサンプルは、D3D12_RESOURCE_STATE_COMMONからD3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCEに遷移させている様に見える。コメントには「Beforeは重要ではなく、トラッカーによって解決される」と書かれている。このトラッカーの仕組みでBeforeの状態を適切に設定しているのか?
試しにBeforeにD3D12_RESOURCE_STATE_COMMONを設定してみたら通ったので、とりあえずこれでやってみる。

今度はシェーダ実行後、やっぱりテクスチャのステータス遷移で怒られる。D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCEからD3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCEに戻そうとするとエラーになる。
どうやっても通らないので、ここでステータスを変更するのは諦めた。
レンダリングする時に遷移させるのでComputeShaderの最後では戻さないようにした。

すべての処理が終わった後ExecuteCommandListsを呼び出すと、今度はここでテクスチャのリソースがD3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCEに戻ってないぞと怒られる。一体何なんだ!戻したいのに戻せないからそのままにしたら、今度は戻せと怒られる。でも戻そうとしても戻せない。

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

このエラーcompute command listでは扱えないステータスが指定されたということ?
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCEはComputeShaderでは扱えない?サンプルもあるし、そんなことはないはず。
もしかして、ComputeShaderでもGraphicCommandListを使ってもいいのか?

ComputeShaderの場合、CommandAllocator、CommandList、CommandQueueをD3D12_COMMAND_LIST_TYPE_COMPUTEで固定にしていたけど、テクスチャリソースを扱う場合は、D3D12_COMMAND_LIST_TYPE_DIRECTでCreateするように修正してみた。DIRECTのCommandListであれば、問題なくテクスチャの状態遷移ができるようになった。

他にもステータス遷移を今までD3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES固定で行っていたけど、ミップマップではサブリソースとして取り扱うのでここにSubResourceIndexを指定できるように拡張した。

ミップマップデータのイメージ


Mipmapのデータがどういう状態なのか最初イメージがつかなくて苦労した。

まず、Texture2D.MipLevelsについて。
今まではテクスチャを作る際、Texture2D.MipLevelsに1を指定していた。
サンプルを見ると、この値が2以上の場合にMipmap処理するようになっている。
ミップマップを作りたかったら、自分で値を決めて設定するらしい。
MipLevelsの値は、元画像の幅と高さの小さい方のlog2+1にした。


テクスチャメモリの最終的な自分の解釈は、CreateCommittedResource時にMipLevelsに指定した値分メモリが用意される。SRVはその全体を指している。これまではUploadで一番上のデータのみコピーしていたが、データがあれば同じようにUploadでコピーもできるし、今回やるComputeShaderで書き込むこともできる。
ComputeShaderで書き込むときは、個別にCreateUnorderedAccessViewでUAVを用意する。
CreateUnorderedAccessViewで指定するリソースは、元になるテクスチャのリソースを指定する。D3D12_UNORDERED_ACCESS_VIEW_DESCのTexture2D.MipSliceに対応するMipLvを指定する。

MipLv0を元にMipLv1を作って、次にMipLv1を元にMipLv2を作る。
この時、MipLv1が完成するのを待つ必要があり、今まで使っていなかった新しいバリアを使うことになる。
リソース状態の遷移にはD3D12_RESOURCE_BARRIER_TYPE_TRANSITIONというタイプを使っていたが、UAVの書き込み待ちにはD3D12_RESOURCE_BARRIER_TYPE_UAVを使うらしい。

ComputeShaderで使うリソース


サンプルでは、自分が今まで使ってきたリソースとは別の種類のリソースを使っている。
1つはStaticSampler。一番最初の頃使っていて、複数使う意味がわかってからはStaticSamplerは廃止していたんだけど、別途リソースを作る必要がなく扱いやすいので復活させることにした。
また、定数バッファをSetComputeRoot32BitConstantsという関数で指定している。
これは今までのD3D12_DESCRIPTOR_RANGE_TYPE_CBVとは違って、D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTSというタイプ。これもStaticSamplerと同じで、シェーダ側にくっついてるイメージになるので別途定数バッファリソースを用意する必要がなくなる。なのでこれも定義できるように拡張した。

Mipmapテスト用のテクスチャで確認してみる


Mipmapをやってみたけど本当にうまくいっているかを確認する方法があった方が安心できる。別の画像を各階層に設定すればはっきり確認できる。
そこで4096~64までのサイズの画像をそれぞれ用意して、各サブリソースに対して個別にUploadしてみることにした。

案の定、ここでも嵌った。
テクスチャを読み込む際D3D12_HEAP_PROPERTIESとD3D12_RESOURCE_DESCを用意してリソースを用意するけど、Upload、Defaultの順にCreateCommittedResourceを呼び出して、PropとDescを使い回していた。MipmapではUploadを繰り返す必要があるので、CreateCommittedResourceする順番を逆にして、Default、Uploadの順に変更。
すると、1回目からエラーが発生した。

D3D12 ERROR: ID3D12CommandList::CopyTextureRegion: D3D12_SUBRESOURCE_FOOTPRINT::Format is not supported at the current feature level with the dimensionality implied by the D3D12_SUBRESOURCE_FOOTPRINT::Height and D3D12_SUBRESOURCE_FOOTPRINT::Depth. Format = UNKNOWN, Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D, Height = 1, Depth = 1, and FeatureLevel is D3D_FEATURE_LEVEL_12_1. [ RESOURCE_MANIPULATION ERROR #867: COPYTEXTUREREGION_INVALIDSRCDIMENSIONS]

これはおかしい。少なくとも1回はうまく行って、2回目からエラーならわかるけど1回目からエラーになる。通常のテクスチャ読み込み時のメモリと、ミップマップのテクスチャ読み込みのメモリを比べていくと、Footprintのデータが違っていた。エラーにも書いてあるがフォーマットにUnknownが指定されている。

GetCopyableFootprintsを呼び出す際、CreateCommittedResourceで使ったDescを指定するけど、Defaultのものを渡す必要がある。順番を逆にしたせいでUploadのものを渡していてエラーになっていた。共用にせず、コピーしてUpload用のDescを用意すると1回目はうまく行った。

D3D12 ERROR: ID3D12CommandList::CopyTextureRegion: The region specified by D3D12_TEXTURE_COPY_LOCATION:PlacedFootprint extends past the end of the buffer it is placed on. The size required by PlacedFootprint is 67108864, as the fields of PlacedFootprint::Placement are as follows: RowPitch is 16384, Height is 4096, and Format is R8G8B8A8_UNORM. PlacedFootprint::Offset is 0, which requires the buffer to have 67108864 bytes; but the buffer only has 16777216 bytes. [ RESOURCE_MANIPULATION ERROR #869: COPYTEXTUREREGION_INVALIDSRCPLACEMENT]

次のエラーは、MipLv1に対してもMipLv0のサイズをしているからエラーになっていた。GetCopyableFootprintsの第2引数にMipLvを指定すると、幅と高さが半分のFootprintが返却されるようになった。

うまく読み込めるようになったので、平面の頂点4つを用意してUV指定したものをレンダリングしてみた。

Mipmapテストテクスチャ

すべての画像が混ざり合って表示されているのがわかる。
次にこの頂点にMipmapしてないテクスチャを貼り付けてみる

Mipmap無効

これは今までMipLv1でやってきた結果のもの。
では今回作ったMipmap自動生成機能をOnにするとどうなるか?

Mipmap有効

奥のほうがガチャガチャせず、きれいに表示されている。
これがMipmapの効果か。
昔、Mipmapは同じ画像の縮小をいくつも持ってしまうので、それだけたくさんメモリも使うし必要ないと思ってた。
でもこんな風にきれいになるし、今ではメモリもふんだんにあるし、使用メモリが増えるといっても実は約1.3倍程度なので気にする必要もなかったかも。


なぜMipmapが無いと表示が汚くなるのか


テクスチャと貼り付ける面が1対1になっている場合は画像そのままを貼り付けられる。
貼り付ける面が画像よりも小さいと、たくさんのドットからどれを貼り付けるか選ばないといけなくなる。その時、隣り合うドットが元の絵とはかけ離れた色を選択してしまうと、見た目が汚くなる。なので、予め縮小画像を作っておくと、そのギャップが埋まってきれいに見える。縮小は縦横半分にするので、元画像の2×2の4つの色を合成して1つの色を出力する。

2022年8月2日火曜日

Compute Shaderで頂点バッファを事前計算

このサイトの記事を見つけて震えた。
こんなことができるのかと。

要約すると、シャドウとかで同じメッシュを1フレーム内でも複数回レンダリングするなら、それを計算シェーダで行列計算結果をキャッシュして、あとはそのまま使えばいいじゃないという話。カスケードシャドウなんか効果ありそう。

必要な要素は、頂点バッファとUAVリソースの準備と、計算シェーダの仕組み。

まず、頂点バッファのリソースについて。
現状のライブラリ内ではVertexというクラスになっている。頂点バッファとインデックスバッファをまとめて持っていて外からは操作できない。
以前もリソース関連のリファクタリングをしたんだけど、その時Vertexは除外した。

Resourceクラスは、定数バッファ、テクスチャ、レンダーターゲット、中間レンダーターゲット、深度バッファを扱える。
BeginRender、EndRender時に中間レンダーターゲット、深度バッファについてはResourceBarrierで、リソースステータスを変えてそれぞれテクスチャとして利用できるようにしていた。

頂点バッファの利用箇所はDraw時のIASetVertexBuffersに渡すだけかと思っていたけど、今回計算シェーダでシェーダリソースとして使う。
計算シェーダでStructuredBufferとして利用するために、リソースステータスをD3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFERからD3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCEに変える必要がある。
PSではD3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCEでテクスチャとして参照できるけど、CSではD3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCEにするらしい。

というわけでVertexクラスを廃止して、Resourceクラスに頂点バッファとインデックスバッファを追加した。

UAVは定数バッファと違って、決まった利用方法はなく用途別に書き込みと参照をする必要があるためResourceクラスには頂点バッファ(UAV版)として追加する。Create時にD3D12_VERTEX_BUFFER_VIEWも用意して、IASetVertexBuffersに渡せるように準備する。

計算シェーダの仕組みは通常のグラフィックスパイプラインのソースをコピーして、CreateGraphicsPipelineStateをCreateComputePipelineStateに変える。

	oDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_NONE
	|	D3D12_ROOT_SIGNATURE_FLAG_DENY_VERTEX_SHADER_ROOT_ACCESS
	|	D3D12_ROOT_SIGNATURE_FLAG_DENY_HULL_SHADER_ROOT_ACCESS
	|	D3D12_ROOT_SIGNATURE_FLAG_DENY_DOMAIN_SHADER_ROOT_ACCESS
	|	D3D12_ROOT_SIGNATURE_FLAG_DENY_GEOMETRY_SHADER_ROOT_ACCESS
	|	D3D12_ROOT_SIGNATURE_FLAG_DENY_PIXEL_SHADER_ROOT_ACCESS
	;
フラグは全部不許可にしてD3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUTもなくすと、GPUによっては最適化される模様。

いざ実行してみるとランタイムエラー地獄。
1つ1つ潰していく。

まずコマンドラインアロケータとコマンドリスト。
CreateCommandAllocator、CreateCommandListに渡す引数がD3D12_COMMAND_LIST_TYPE_DIRECTからD3D12_COMMAND_LIST_TYPE_COMPUTEにかわる。

次にルートパラメータ。
D3D12_ROOT_PARAMETER1のShaderVisibilityフラグを、SRVと、SamplerであればD3D12_SHADER_VISIBILITY_PIXELとしていたけど、CSの場合はD3D12_SHADER_VISIBILITY_ALLにする。

ヒープの設定方法。
SetGraphicsRootDescriptorTableからSetComputeRootDescriptorTableに変わる。

コマンドキュー。
CreateCommandQueueを別途用意する必要があり、D3D12_COMMAND_QUEUE_DESCのTypeをD3D12_COMMAND_LIST_TYPE_DIRECTからD3D12_COMMAND_LIST_TYPE_COMPUTEに変える。

最後に一番嵌ったのが、このエラー。
D3D12 ERROR: ID3D12CommandAllocator::Reset: A command allocator 0x00000230EAF471E0:'Unnamed ID3D12CommandAllocator Object' is being reset before previous executions associated with the allocator have completed. [ EXECUTION ERROR #552: COMMAND_ALLOCATOR_SYNC]

探してもNvidiaのドライバの問題でパッチで直るよ、ありがとうぐらいしか見つからなくて困った。
内容は、前の実行が完了してないのに次の処理するためにリセットしてるという感じで、意味がわからない。
デバッグでステップ実行しつつちょっとずつ回していくと出ないんだけど、F5押して一気に進めるとこのエラーが出続ける。

原因は、スワップチェーンのPresent後にフレームの同期を取るためMoveToNextFrameを呼んでいるけど、この中でコマンドキューにシグナルを送っている。
ComputeShader用のコマンドキューの方もシグナルを送って同期する必要があった。

ここまでは計算シェーダだけ追加して、結果には何も反映させていなかった。
やっとエラーは出なくなったので、計算シェーダで出力したUAバッファを頂点バッファとして使ってみる。

結果は真っ暗だったり、ほんの少しだけ何かが表示されたりしていた。
冒頭の記事、シェーダ関数の先頭部分[numthreads( 32, 1, 1 )]をそのまま写していた。リファレンスを確認するとプログラム側から指定するDispatchの3つの引数と、このnumthreadsの掛け算の数だけ実行されるらしい。
Dispatch(1,1,1)として、numthreads( 32,1,1 )なので、SV_DispatchThreadIDのxには0~31が渡ってくると思われる。記事では三角形1つ分の処理をしているので、頂点数は3つ。3頂点に対して32スレッドは多いけど、範囲外アクセスしても大丈夫ってことを言っていたんだな。なんで大丈夫かはわからないけど。

イマイチ2段階で指定する意味がわからない。今回のは1次元データで1000頂点分処理させたいとする。numthreadsは32スレッドにして、Dispatchに32を指定する。32×32=1024で、24回分オーバするけど範囲外アクセスは大丈夫ということでいいんだろうか?
Dispatchに渡す値の計算式は(頂点数+スレッド数-1)÷スレッド数。
このひとはオーバしない様に考慮してる。あと、Dispatchに指定できる範囲が65535までなのもこのひとの記事で知った。

1542頂点
1スレッド 29.01us
2スレッド 18.02us
4スレッド 13.18us
8スレッド 12.08us
16スレッド 11.67us
32スレッド 11.98us
64スレッド 12.19us
128スレッド 12.76us

PIXで測ってみるとIntelのGPUだとこんな感じだった。
頂点数がオーバする可能性があることと、1スレッドだと結構遅い(遅くないと思ってた)ので、シェーダの指定は16スレッドにして、1メッシュの頂点数は最大16×65535で制限をかけることにする。
そういえば1スレッドのときには問題なかったけど複数スレッドで実行すると、レンダリングしているオブジェクトがちらつくようになった。
計算シェーダが書き込んでいる途中で頂点バッファとして使っているんだろうか?
計算シェーダの最後にWaitForGPUを追加したら安定した。

参考にした記事と違っていたところが数点。
まずUAVを頂点バッファとして使う時にリソースのステータスをD3D12_RESOURCE_STATE_GENERIC_READにすると書かれているが、これに遷移させようとするとエラーになった。もともとD3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFERにしてたんだけど、気になって試してみたらだめだった。
あと、頂点バッファでfloat3で入れたデータをUAVに出力する際float4になると言う記述。
これは、他でもアライメントの関係でfloat4になってしまう場面を見てきたのでそういうものかなと思ってたんだけど、float3のまま渡すことが出来た。


今までの処理結果


頂点キャッシュの処理結果

処理結果を比べてみると、今までの方が僅かに速い。
全体で1.4081msから1.4169msに増えた。おかしい・・・。
キャッシュ対象のメッシュは全部で6回レンダリングされる。メッシュ内でパーツが6個に分かれているので、同じメッシュでDrawIndexedInstancedは36回呼ばれる。
計算シェーダでは6つ分のパーツをまとめて計算する。
DrawIndexedInstancedはかなり並列に動いている&計算シェーダで行った処理がそこまで重くないということか?
同じDrawIndexedInstancedで比べると、旧は147.92us、新は147.19usで僅かに縮まっているので、個々の処理についてはキャッシュが効いているみたいだ。もっと頂点数が増えたり、頂点シェーダでやることが増えたら効果が出てくるだろう。

計算シェーダ
	uint i = In.ID.x ;
	float4 Pos = float4( gSrc[ i ].Pos, 1.0f ) ;
	float4 Normal = float4( gSrc[ i ].Normal, 0.0f ) ;
	uint4 BoneID = gSrc[ i ].BoneID ;
	float4 Weight = gSrc[ i ].Weight ;
	float4x4 BoneTransform
		= gMesh.Bones[ BoneID[0]] * Weight[0]
		+ gMesh.Bones[ BoneID[1]] * Weight[1]
		+ gMesh.Bones[ BoneID[2]] * Weight[2]
		+ gMesh.Bones[ BoneID[3]] * Weight[3]
	;
	gDst[ i ].Pos = mul( gMesh.World, mul( BoneTransform, Pos )).xyz ;
	gDst[ i ].Normal = mul( gMesh.World, Normal ).xyz ;
	gDst[ i ].UV = gSrc[ i ].UV ;

メッシュをレンダリングするシェーダが絡むと常にBoneTransformと、gMesh.Worldの行列計算が必要だった。
キャッシュすることにより後続のデータではBoneIDとWeightをなくせる。また、ボーンなしのマップとかでも頂点データにダミーのBoneID、Weightを入れて同じシェーダでレンダリングできるように合わせていたけど、それをやめて最初からキャッシュ後と同じ形にすることが出来た。


最後に
「この記事を書いた時点ではMesh Shaderがあるのですが,いまの時点では頂点シェーダを使うシーンは多くあると思います」

と、気になる文章。メッシュシェーダなるものがある。

先は長い・・・。

Unordered Access View(UAV)

DirectX12でプログラムを組む際、扱うリソースのD3D12_DESCRIPTOR_HEAP_DESCを用意することになる。
タイプがいくつもあって最初はなんのことかよくわからない。

D3D12_DESCRIPTOR_HEAP_TYPE_RTV

RenderTargetView。レンダーターゲット用。描画対象になるリソース分用意する。
ダブルバッファリングする場合は画面用に2つ、ポストエフェクト用に中間レンダーターゲットを用意する場合はその枚数分とか。

D3D12_DESCRIPTOR_HEAP_TYPE_DSV

DepthStencilView。深度バッファ用。深度バッファは理解して使えるようになったけど、ステンシルってなんだ?

D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER

Sampler。サンプラー用。ルートシグネチャにStaticでついてるサンプラ1つで十分だろと思ってたときがありました。今では複数使う理由がわかり、必要なサンプラー分用意する。

D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV

ConstantBuffer、ShaderResource、UnorderedAccess View。定数バッファ、シェダーリソース、順序付けられていないアクセス用。
定数バッファは、動的にシェーダにパラメータを渡すためのもの。
シェーダリソースはこの名前だとわかりにくいけど、最初に扱うのはテクスチャだろうからテクスチャと思っておけばいい。あとから普通のデータもテクスチャのレジスタで渡せることに気付く。
順序付けられていないアクセスってなんだ?最初にライブラリを作ったときにはUAVは謎なので扱わなかった。

計算シェーダというものがあり、レンダリングはしないんだけどGPUの豊富なスレッドに一気に計算せさて結果を得られる仕組みがある。別にピクセルシェーダとか他のシェーダでも書き込めるんだろうけども。
その結果を格納するバッファがこのUAVで、Unorderedというのは複数スレッドが順不同に処理するので、その時に同時に書き込めるよという意味だと解釈した。

初めて見たDirectX12のサンプルはD3D12Fullscreen。
何故かsceneとpostで2つ用意しているし、更にダブルバッファリングで配列で2つになっている部分もある。たいしたことをしていないのに複雑な構成になっている。
今では、sceneは中間レンダーターゲットに対してレンダリングする用で、postが画面(レンダーターゲット)に描画する用と理解できたが最初は戸惑った。

それでも、疑問点が残ってる。
定数バッファを使いたいと思った時、フレーム数分の領域を用意する必要がある。
50バイトのデータを使う場合はダブルバッファリングなら100バイト分・・・ではない。
定数バッファの最低単位は256バイトなので50バイトしか使っていなくても、512バイト分必要になる。
リソース自体は1つで連続したデータで用意して、2つのデスクリプタヒープでそれぞれの先頭ポインタを指すイメージ。
バックバッファが0番なら、CPU側で0バイト目からデータを書き込んで、GPU側は256バイト目からのデータを参照し、スワップ後バックバッファが1番になったとき、CPU側は256バイト目からデータを書き込んで、GPU側は0バイト目からを参照する。
お互いに並列で処理していく為に2つ必要なのだと理解した。
ただ、今回リソースライブラリをリファクタリングする際、ReadOnly属性のものも作れるようにした。1つ分のバッファでデスクリプタヒープも1つ分。値が動的に変わらないのであれば、2つ用意する必要はない。

ここからが疑問点なんだけど、SRVはどうなのか?サンプルでは中間レンダーターゲットは1つしか用意してないし、同じものに書き込んでいる。レンダーターゲットは2つでバックバッファ用意しているのに、中間レンダーターゲットいらないのか?
この点を某質問サイトで質問してみたが誰からも回答はなかった。
マイクロソフトのサンプルなので、SRVは1つで行けると思っておく。

ではUAVについてだけど、フレーム毎に書き換わるようなデータであればフレーム数分用意する必要があると判断した。リファレンスに丁度そのサンプルが書かれていたのを見つけた。
定数バッファと違って、UAVはリソースから2つ用意する必要がある。
そのリソースを使って、CreateUnorderedAccessViewを呼ぶんだけどこの引数がまた謎。
4つ引数があり、2つ目以外は他と同じなんだけど、2つめのpCounterResourceの意味がわからない。
最初別のネット記事を参考に実装していたときは、ここをnullprtにしていたので無視しておけばいいのかと思っていたけど、リファレンスのサンプルを見つけたので実装の仕方はわかった。別のリファレンスではこのカウンターの利用方法も書かれている。
全然理解は出来ていないけど、同期のための仕組みで用意しておくと内部で勝手に使われて、ある状態のときにはこのカウンタのリセットも必要なると解釈し、一応用意しておくことにした。

カウンタを使う場合は、データの単位が4096になる。
nSize = (nSize + 0xFFF) & ~0xFFF ;

サイズをこれで調節してリソースを作成する。
定数バッファでも256単位にする必要があり、よく構造体にダミーデータを入れて調整しているのを見るけどデータが変わる度に調整が必要だしこっちの方がおすすめ。

2022年7月30日土曜日

Depth Of Field (被写界深度)

影ができたらポストプロセスエフェクトをやってみようと思っていた。
その中でも気になってたのが被写界深度というもの。

例のごとく某書籍でやり方をみると、その前の章で作ってきたものを利用して作るのですぐには取りかかれなさそう。

まず前回やったSSAOの結果に掛けていたブラーの処理。
参考にしたサイトの処理では単純にループで指定した回数×回数の範囲の平均を取るだけだった。
書籍の方ではガウシアンブラーという、中心から重みを付けてぼかしていく方法でなんとなくこっちの方が良さそう。
すでに用意したブラー処理は回数を指定すると範囲を広げることができるんだけど重たくなるので3~5の範囲でしか使わないと思う。
書籍の方の処理はfor文の2重ループではなく、展開した形で実装されていた。
シェーダの最適化とか、[unroll]指定で同じようにはなるんだろうけど5×5の固定で真似してみた。

次に、元画像の横幅半分のテクスチャを用意して、ミップマップみたいに1/4の画像を階層的に作る処理。
書き込む際に上記で作ったブラー処理を掛けながら出力する。

縮小テクスチャ

書籍では8段階で縮小画像を作っていたが、こんなに必要ない。
例えば元が1024だった場合、8回分割すると最後は4×4。
最低でも64×64だろうと思い、とりあえずlog2( width )-2をループ回数に設定した。

最後に縮小テクスチャを使って、最終的な画像を出力するんだけどぼかす対象とぼかさない対象を決める方法を某書籍では深度バッファを使って、中心のZとの差にしていた。
その差は微々たるものなので、powで拡張して最後に8を掛ける。
その結果の整数部分を縮小画像のインデックスになる。

書籍の通りに実装して動かしてみるも実行時エラーが発生する。
落ちているのは最終的に出来たテクスチャをレンダリングしている箇所なんだけど、今作った部分を外して代わりに縮小テクスチャを表示する分には問題は起きない。

処理をよく見ると気持ち悪いfor文があり、このfor文の最後のif文に問題があった。

	for (int i = 1; i <= 8; ++i) {
		if (i - no < 0) continue;
		retColor[i-no]= Get5x5GaussianBlur(texShrink, smp, input.uv*uvSize + uvOfst, dx, dy, float4(uvOfst, uvOfst + uvSize));
		uvOfst.y += uvSize.y;
		uvSize *= 0.5f;
		if (i - no > 1) break;
	}

某書籍のソースはこんな感じになっておりfor直後のif文で、iがnoと同じになるまで空ループする。中に入るようになったら、i-noが0,1,2で周ることになる。ただし、retColorの配列は2で定義されているので落ちているっぽい。
ここでやりたかったことはおそらく、noに当たる段階の画像を決めて、その画像とその次の画像を使ってぼかすということなんだけど、この処理だと常に1番目の画像と2番目の画像しか使ってない。最初のif文で空ループしてしまってるので、uvを参照するオフセットが動かないから。8個も画像準備してるのに意味ない・・・。
やりたいことが理解できたので、ループをやめて直接計算するようにした。

被写界深度

一応それっぽく表示された。
周りのボケ感がもっとほしいのでZとの差を計算している部分を調整してみた。

被写界深度 パラメータ調整版

一番離れているところが4番目、5番目ぐらいの画像を使うようにしてみたら、ボケはすごくなったけど、下の影にアーティファクトが発生。

中心もボケてる

更に中心のオブジェクトもボケてしまっている。

小さすぎる画像は使い物にならないので画像の分割数はもっと少なくても良さそう。
1024で4、4096で6になるようにこうした。

	this->nLoop = fMax( 4, fMin( 6, tI4( ::log2( oSize.width ))-6 )) ;	// 1024で4段階 4096で6段階

差の計算も8掛けているのはやめて差が大きいほど指数的に増えるように調整した。

被写界深度 再調整版

アーティファクトが出るレベルで差が激しいパラメータでも、中心のオブジェクトはそんなにボケなくなった。

でもZ面が同じようなオブジェクトの配置であればこれで大丈夫そうだけど、奥行きのある物体でこれをやるとぶれてしまうと思う。フォーカスを当てるオブジェクトだけくっきりして、周りがボケてる感じにしたい。
そこで考えたのが、対象オブジェクトだけの深度バッファを用意すればそこだけマスクできるのではないかと。


被写界深度 マスク版

パラメータ調整前に中心のオブジェクトもぶれてしまう状態のシェーダでマスクを適用させてみたけど、うまくいってる。


被写界深度 マスク反転

ちなみに反転させると対象のみブレさせてモザイク掛けてるみたいにもできる。

	tStr( LR"---(
	#define MASK %d
	#define LOOP_COUNT %d
		float2 texel = float2( 1.0f / %ff, 1.0f / %ff ) ;
		float d = abs( TexDepth.Sample( Sampler, float2( 0.5, 0.5 )).r - TexDepth.Sample( Sampler, In.UV ).r ) ;
		d = pow( 1.0f + d, %ff ) - 1.0f ;
	#if MASK
		d = TexMask.Sample( Sampler, In.UV ).r < 1.0f ? 0.0f : d ;
	#endif
		int no ;
		d = modf( d, no ) ;
		no = min( no, LOOP_COUNT-1 ) ;
		float2 size = float2( 1, 0.5 ) ;
		float2 osf = float2( 0, 0 ) ;
		float4 col[ 2 ] ;
		if( no == 0 ) {
			col[ 0 ] = Tex.Sample( Sampler, In.UV ) ;
			col[ 1 ] = GetColorBlur( TexShrink, Sampler, In.UV * size + osf, texel.x, texel.y, float4( osf, osf + size )) ;
		} else {
			size *= pow( 0.5f, no-1 ) ;
			osf.y = ( pow( 0.5f, no )-1 ) / ( 0.5f - 1.0f ) - 1.0f ;
			col[ 0 ] = GetColorBlur( TexShrink, Sampler, In.UV * size + osf, texel.x, texel.y, float4( osf, osf + size )) ;
			osf.y += size.y ;
			size *= 0.5f ;
			col[ 1 ] = GetColorBlur( TexShrink, Sampler, In.UV * size + osf, texel.x, texel.y, float4( osf, osf + size )) ;
		}
		Out.Col = lerp( col[0], col[1], d ) ;
	)---"_fs, bMask ? 1 : 0, this->nLoop, oSize.width, oSize.height, nPow )

最終的なシェーダはこれ。
マスクはオプションで、使う場合は深度バッファを別途用意する。

3人バージョン

比較のためにZ座標を変えたおっさんを2体追加してみたけど、なんか思ってたのと違う。
素材が悪いんだろうけど、きれいなレベルで抑えるとボケが足りない気もするし、まだ改善の余地がたくさんある。書籍にも「簡易的な被写界深度の実装」と書かれているので、これから改良を加えていこう。