2022年8月23日火曜日

Tiling Noise

前回の草が揺れる風にノイズテクスチャを使っていたけど、uvのオフセットが端に行った時草の動きが激しくなる。草むらの中で動物が走ってるような感じ。

ノイズ画像を並べて見る


ノイズ画像を並べた時こんな感じになる。

ValueNoise

PerlinNoise

CellularNoise

VoroNoise

SimplexNoise

1枚の四角にUVを0~3で表示すると、3回ラップして表示される。
真ん中と隣接する絵がつながっていない。

ノイズ作成のパラメータには画像サイズとGridサイズを渡す。
画像サイズが256で、Gridサイズが64とすると、256÷64=4つ分のグリッドでノイズを生成する。
ノイズ関数は渡されたx,yを整数部分と小数部分に分解するが、この整数部分を画像サイズ÷Gridサイズの値でmodしてやるとつなぎ目がなくなる。

TilingValueNoise

TilingPerlinNoise

TilingCellularNoise

TilingVoroNoise

ただし、シンプレックスノイズだけはうまくいかない。
歪ませた三角形で処理するためmodが使えない。

Tiling Simplex Noise


ネット上でいくつかシンプレックスノイズを繋げる方法を見つけたが、うまく自分のソースに適用できなかった。
シンプレックスノイズは諦めようかと思った時、このサイトを見つけた。

4次元のシンプレックスノイズ関数を使って、縦と横にぐるっと回り込むように参照することで実現するようだ。

パラメータの指定方法などが現状とかなり違っていたので調整に時間がかかった。最初はシンプレックスノイズ単体でしか出来なくて、fBMを通すと同じように境目が出来てしまったんだけど、いろいろやっているうちにfBMを使ってもつながるようになった。

TilingSimplexNoise

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かかってないので、なかなか優秀かも。


2022年8月18日木曜日

距離に応じたテッセレーション

前回の続き

細かくしてどうするのか?


テセレーションについて調べていく中で見つけたのが、ローポリゴンで作ったメッシュをカメラの距離に応じて細分化することにより、近くの場合は細かく詳細に、遠くの場合は荒くすることにより見た目とパフォーマンスを両立させることができるというもの。他にもいろいろ使用例があったけど、これが一番イメージが付きやすかった。

さっそくこれをやってみたい。

前回のプログラムでカメラとの距離に応じて係数を計算するようにしても頂点4つだけのメッシュでうまく反映されない。そこで頂点を11×11で四角が100個(三角が200個)分のデータを用意して描画してみた。

D3D_PRIMITIVE_TOPOLOGY_4_CONTROL_POINT_PATCHLIST

4頂点に対して、インデックスバッファを6個ずつで、100個分四角を書いているつもりだけど、こんな表示になってしまう。

試しにトポロジを今までのトライアングルリストで描画してみる

D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST

正しく表示され頂点バッファ、インデックスバッファは問題ないみたいだ。

ここでふと思いつく。

トライアングルリストの場合はややこしくて、頂点0(左上)、頂点1(右上)、頂点2(左下)、頂点3(右下)の4頂点ある場合、インデックスバッファは(0,1,2),(3,2,1)というふうに、時計回りで3点ずつ2つの3角形で指定する。

コントロールポイントの場合は、そのポイントがどれかを単純に指定すればいいのではないか?
頂点データは同じで、インデックスバッファは(0,1,2,3)の4つ(D3D_PRIMITIVE_TOPOLOGY_4_CONTROL_POINT_PATCHLIST
の場合)を単純に指定すればどうか。

D3D_PRIMITIVE_TOPOLOGY_4_CONTROL_POINT_PATCHLIST修正版

正しく表示された。
うまく行ったけど、これってインデックスバッファの中身変えないと使えないのか?
いままでQuadでやっていたけどTriで確認したら、今までのインデックスバッファの指定方法で行けた。もしかしてTriしか使わないかも?

距離に応じたテセレーション


こんな関数を用意した。
	float DistanceTessellationFactor( float4 v, float min, float max, float t )
	{
		float d = distance( v.xyz, gCam.Eye.xyz ) ;
		return clamp( 1.0 - ( d - min ) / ( max - min ), 0.01, 1.0 ) * t ;
	}

カメラの位置と頂点の位置をdistanceに渡してその結果に掛け目を掛けて係数を返す。

#define MINDIST 50.0
#define MAXDIST 120.0
	float3 f ;
	f.x = DistanceTessellationFactor( IP[0].Pos, MINDIST, MAXDIST, gHSParam.EdgeFactor ) ;
	f.y = DistanceTessellationFactor( IP[1].Pos, MINDIST, MAXDIST, gHSParam.EdgeFactor ) ;
	f.z = DistanceTessellationFactor( IP[2].Pos, MINDIST, MAXDIST, gHSParam.EdgeFactor ) ;
	float3 a ;
	float b, c ;
	ProcessTriTessFactorsAvg( f, 1.0f, a, b, c ) ;
	Out.Edge[0] = a.x ;
	Out.Edge[1] = a.y ;
	Out.Edge[2] = a.z ;
	Out.Inside = b ;

Quadなら4つ分の係数を計算して、Process2DQuadTessFactorsAvgまたはProcessQuadTessFactorsAvgを呼び出す。
Triなら3つ分の係数を計算してProcessTriTessFactorsAvgを呼び出す。
それぞれInsideScaleは1.0fを指定しておけば、EdgeFactorに応じていい具合の値を返してくれる。(イマイチbとcの違いがわからず)

Tri版

Quad版

遠くはそのままで、近くは細分化されているのがわかる。

Tesselation(Hull Shader & Domain Shader)

3Dのプログラムを始める時、最初に説明されているのがグラフィックスパイプラインで、こんなフローを見ることになる。

Graphics Pipeline

最初に扱うのも今まで扱ってきたのもVSとPSのみ。VSだけっていうこともあった。
他のステージはよくわからないけどとりあえずそんなのもあるんだ程度で、もしかするとずっと使わないかもしれないと思ってた。

シェーダ以外の灰色の枠に関しては関与できず、パラメータを渡すのみ。
InputAssemblerに関しては、IAから始まる関数IASetPrimitiveTopology、IASetVertexBuffers、IASetIndexBufferを通して頂点バッファ、インデックスバッファを渡している。

今回の話題はTesselatoerの部分。

テッセレータ


頂点のデータをこのテッセレータが細かくしてくれるらしい。
どういうふうに細かくするかを決めるのがHull Shaderで、Hull Shaderの出力結果を使ってテッセレータが細かくする方針を立ててくれて、Domain Shaderで実際に決定する流れみたい。

だからテッセレータを使う場合はHSとDSはセットで用意する必要がある。
HSのみセットして実行すると怒られる。

D3D12 ERROR: ID3D12Device::CreateGraphicsPipelineState: When using tesselation, both the Hull Shader and Domain Shader must be set.  Otherwise, both must be NULL. [ STATE_CREATION ERROR #667: CREATEGRAPHICSPIPELINESTATE_HS_XOR_DS_MISMATCH]


Hull Shader


ハルシェーダは2つに分かれる。
1つは頂点データを設定するメイン関数。
もう1つは、分割の細かさを設定するパッチ定数関数。
頂点バッファをパッチという単位にして、1パッチ毎に呼ばれるのが定数関数で、パッチ&頂点単位に呼ばれるのがメイン関数。

メイン関数定義


	[domain("quad")]
	[partitioning("integer")]
	[outputtopology("triangle_ccw")]
	[outputcontrolpoints(4)]
	[patchconstantfunc("CHSMain")]
	HSOutput HSMain( InputPatch<VSOutput, 4> IP, uint CP : SV_OutputControlPointID, uint PatchID : SV_PrimitiveID )
    {
      	HSOutput Out ;
		Out.Pos = IP[CP].Pos ;
		Out.UV = IP[CP].UV ;
      	return Out ;
    }
メイン関数の先頭で色々な設定をする。

domain

domainに設定できる値は、quad(四角形)、tri(三角形)、isoline(線分)の3つ。
この値が決まると、outputcontrolpointsと、関数の引数InputPachの第二引数と、IASetPrimitiveTopologyの引数が決まるっぽい。
"quad"の場合は4、TopologyはD3D_PRIMITIVE_TOPOLOGY_4_CONTROL_POINT_PATCHLIST。
"tri"の場合は3、TopologyはD3D_PRIMITIVE_TOPOLOGY_3_CONTROL_POINT_PATCHLIST。
"isoline"の場合は2、TopologyはD3D_PRIMITIVE_TOPOLOGY_2_CONTROL_POINT_PATCHLIST。

この法則を崩して別な呼び方もあるかもしれないけど、今のところこの呼び方だけ確認した。コントロールポイントの定義は32まである。

partitioning

partitioningに設定できる値、integer、fractional_even、fractional_odd、pow2。
分割時の係数を整数(integer、pow2)で見るのか、小数(fractional_even、fractional_odd)で見るのかの違いと、係数の最小値の違いがあったけど、integerとpow2の違いはよくわからなかった。

outputtopology

outputtopologyに設定できる値はpoint(点)、line(線)、triangle_cw(面:時計回り)、triangle_ccw(面:反時計回り)
点と線を作り出すパターンはよくわからず。
domainを"tri"にした場合は"triangle_cw"で、"quad"にした場合は"triangle_ccw"にしないと、元々の面と向きが反対になった。

outputcontrolpoints

出力するコントロールポイントの数。入力のコントロールポイントの数と異なっても良いらしいが、そのパターンは未確認。

patchconstantfunc

パッチ定数関数名を指定する。

引数

InputPatch<VSの出力構造体, 入力コントロールポイント数>
この引数が必須で後はSV_系の引数がオプションで使える。
使えるセマンティックはSV_PrimitiveIDとSV_OutputControlPointID。
コントロールポイントが3の場合、SV_PrimitiveIDとSV_OutputControlPointIDは、(0,0),(0,1)(0,2),(1,0),(1,1)(1,2)...という値を受け取る。

パッチ定数関数定義

	struct CHSOutput
	{
		float Edge[4]	: SV_TessFactor ;
		float Inside[2]	: SV_InsideTessFactor ;
	} ;
	CHSOutput CHSMain( InputPatch<VSOutput, 4> IP, uint PatchID : SV_PrimitiveID )
	{
		CHSOutput Out ;
		Out.Edge[0] = Out.Edge[1] = Out.Edge[2] = Out.Edge[3] = gHSParam.EdgeFactor ;
		Out.Inside[0] = Out.Inside[1] = gHSParam.InsideFactor ;
		return Out ;
	}

引数

InputPatch<VSの出力構造体, 入力コントロールポイント数>
メイン関数と同様の引数。それ以外のSV_系の引数はSV_PrimitiveIDが使える。
呼ばれる単位はパッチID単位なので、SV_OutputControlPointIDを指定するとコンパイルエラーになる。

戻り値

これは、メイン関数のdomainにより決まる。
"quad"にした場合
	float Edge[4]	: SV_TessFactor ;
	float Inside[2]	: SV_InsideTessFactor ;
分割係数を設定する際のヘルパ関数

"tri"にした場合
	float Edge[3]	: SV_TessFactor ;
	float Inside	: SV_InsideTessFactor ;
分割係数を設定する際のヘルパ関数

"isoline"にした場合
	float Edge[2]	: SV_TessFactor ;
分割係数を設定する際のヘルパ関数

試しにProcessQuadTessFactorsAvgを使ってみたけど、RawEdgeFactorsに各辺の分割数、InsideScaleに1を指定すると、RoundedInsideTessFactorsには各辺の分割数がそのまま返ってくる感じだった。各辺の分割数がバラバラの値を指定した時、平均に分割されたように見える値に調整してくれるのかな?

Domain Shader


[domain("quad")]
	DSOutput DSMain( CHSOutput In, const OutputPatch<HSOutput, 4> OP, float2 DL : SV_DomainLocation )
	{
		DSOutput Out ;
		float4 p1 = lerp( OP[1].Pos, OP[0].Pos, DL.x ) ;
		float4 p2 = lerp( OP[3].Pos, OP[2].Pos, DL.x ) ;
		Out.Pos = lerp( p1, p2, DL.y ) ;

		float2 t1 = lerp( OP[1].UV, OP[0].UV, DL.x ) ;
		float2 t2 = lerp( OP[3].UV, OP[2].UV, DL.x ) ;
		Out.UV = lerp( t1, t2, DL.y ) ;
		return Out ;
	}

引数

CHSOutput In
ハルシェーダのパッチ定数関数の出力型。

const OutputPatch<HSの出力構造体, 入力コントロールポイント数>
ハルシェーダの出力をOutputPatchで囲った型。

float2 DL : SV_DomainLocation
テッセレータの出力結果?
この引数もdomain設定により決まる。
"quad"にした場合float2、"tri"にした場合float3、"isoline"にした場合float2。
この値を元に、出力の頂点を補間する。


quadの動作結果


domainを"quad"、partitioningを"integer"で動作させた結果。

EdgeFactor、InsideFactor共に1を指定した場合、分割は行われず元のまま。

EdgeFactorを1ずつ上げていくとこの様に辺の部分が指定した分割数になる。
最大64分割まで可能で、それ以上の値を指定しても64分割となる。
整数なので、1.1と2も同じ扱いで、小数点以下は切り上げられるようだ。

InsideFactorを1ずつ上げていくとこの様に内側に四角が出来てくる。辺の場合は数えられたが、Insideの方はどこをどう数えればいいのか分からない。
最大64分割まで可能で、それ以上の値を指定しても64分割となる。

EdgeFactorとInsideFactor両方とも同じ様に上げていくと、全体がきれいな分割になる。

fractional_even

fractional_odd
integerをそれぞれ、fractional_even、fractional_oddに変えるとこの様になる。
fractional_evenの方は、EdgeFactorの最小値が2となり、1を指定しても2を指定してもこの形になる。2を超えたところから変化が始まる。

fractional_even

EdgeFactorのみ、InsideFactorのみ、EdgeFactorとInsideFactorの値を動かしたときの分割結果。

fractional_odd

EdgeFactorのみ、InsideFactorのみ、EdgeFactorとInsideFactorの値を動かしたときの分割結果。InsideFactorが1のままEdgeFactorを上昇させても変化しない為、EdgeFactorのみ上昇時はInsideFactorは2に設定。

triの動作結果


fractional_even

EdgeFactorのみ、InsideFactorのみ、EdgeFactorとInsideFactorの値を動かしたときの分割結果。

fractional_odd


EdgeFactorのみ、InsideFactorのみ、EdgeFactorとInsideFactorの値を動かしたときの分割結果。InsideFactorが1のままEdgeFactorを上昇させても変化しない為、EdgeFactorのみ上昇時はInsideFactorは2に設定。

今回の嵌まりポイント


ハルシェーダのパッチ定数関数を定義して、patchconstantfuncでも指定しているにも関わらず、コンパイルエラーで定義されていないと言われ続けた。
関数の引数は構造体で定義して1つだけにしてスッキリさせていたんだけど、ハルシェーダとドメインシェーダの場合どうやら対応してないみたいで、引数にInputPatch、OutputPatchの引数があることが条件になっているのかもしれない。
サンプルの通り引数を並べてみたらコンパイルが通った。

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でエラーが出なくなった。