2022年7月25日月曜日

VertexBufferなしでレンダリング

遅延レンダリングとかで、1枚絵に対してレンダリングを行う際でもわざわざ頂点バッファとUVの情報を用意していた。

	tF4 nVData[] = {
		//  x      y      z     u     v
		-1.0f,  1.0f,  0.0f, 0.0f, 0.0f,	// 0:左上
		 1.0f,  1.0f,  0.0f, 1.0f, 0.0f,	// 1:右上
		-1.0f, -1.0f,  0.0f, 0.0f, 1.0f,	// 2:左下
		 1.0f, -1.0f,  0.0f, 1.0f, 1.0f,	// 3:右下
	} ;
	tU4 nIndex[] {	// 時計回り
		0, 1, 2,	3, 2, 1,
	} ;

この頂点バッファと、インデックスバッファでPostレンダリングしていたんだけど、このバッファを用意しなくてもレンダリングができるらしい。

	D3D12_GRAPHICS_PIPELINE_STATE_DESC	oGPSD = {} ;
	oGPSD.InputLayout = { nullptr, 0 } ;

Inputレイアウトは何もなしで初期化する。

	pGCL->IASetPrimitiveTopology( D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP ) ;
	pGCL->IASetVertexBuffers( 0, 0, nullptr ) ;
	pGCL->DrawInstanced( 4, 1, 0, 0 ) ;

描画時、IASetVertexBuffersも空指定で、IASetPrimitiveTopologyにD3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP、DrawInstancedに4を指定する。
struct Input {
    uint VID : SV_VertexID ;
} ;

struct Output {
    float4 Pos : SV_POSITION ;
    float2 UV  : TEXCOORD ;
} ;

Output VSMain( Input In )
{
    Output Out ;

    Out.UV = float2( In.VID & 1, In.VID >> 1 ) ;
    Out.Pos = float4(
    	2.0f * Out.UV.x - 1.0f,
        1.0f - 2.0f * Out.UV.y,
        0.0f,
        1.0f
    ) ;

    return Out ;
}

そうすると、VertexShaderのInputのVIDに0~3の値が入って呼ばれる。
VID    u    v
0        0 0
1        1 0
2        0 1
3        1 1
VIDを使って、このUVを作り出している
個人的にLayoutなし、頂点データなしでも描画できることに驚き。

System-Valueセマンティクスにはいくつも種類があって、DirectX10からの機能みたい。
今回は渡すデータなしで試したけど他のデータを渡した場合でも使えて、Inputに「SV_」の引数を付け加えれば、自動的に渡してくれる。

ちなみにピクセルシェーダではPosは使わないので、OutputのPosをなくしてみたら実行時エラーが出たのでSV_POSITIONは必須なのかな。

D3D12 ERROR: ID3D12Device::CreateGraphicsPipelineState: Rasterization Unit is enabled (PixelShader is not NULL or Depth/Stencil test is enabled and RasterizedStream is not D3D12_SO_NO_RASTERIZED_STREAM) but position is not provided by the last shader before the Rasterization Unit. [ STATE_CREATION ERROR #682: CREATEGRAPHICSPIPELINESTATE_POSITION_NOT_PRESENT]

2022年7月24日日曜日

Deferred Cascade Shadowmap

前回遅延レンダリングで影ができるようになった。

実はシャドウマップが出来てすぐに遅延レンダリングで試してみていた。
このときは、おっさんメッシュに対して遅延レンダリングをした結果、かなり精度が落ちた影がついてはいた。
全く表示されなかったり、ずれているわけでもないのでこれでうまく行っていて単純に遅延レンダリングにすると、立体から2Dになるので精度が落ちてしまうものなのかと勘違いしていた。

精度を上げる方法をネットで調べた結果、カスケードシャドウマップというものを使えば見栄えが良くなるのではないかと思った。
そこで見つけたのがマイクロソフトのサンプル。

これはDirectX11のサンプルなので、12に移植して無謀にもいきなり最初から遅延レンダリングでチャレンジしていた。
そもそもシャドウマップの遅延レンダリングもまともに出来てない状態だったので、カスケードシャドウマップの方はもうめちゃくちゃで全くうまくいかなかった。
幸いサンプルは完全に動作するものだったので、パラメータなどを同じにして移植時に計算が間違っていないか細かく確認していった。
計算結果もほぼ完璧に同じになった状態なのにうまくいかない。
うまくいかない原因は前回の内容で、G-Buffer経由で渡した位置情報では精度が落ちてしまうというものだったんだけど、1週間以上掛けても解決しなかったので遅延レンダリングは一旦諦めた。

そもそもフォワードレンダリングでカスケードシャドウマップが動くのかも確認していなかったので、ちょっと手直しして確かめてみることにした。
これはあっさりうまく行って、やる気が復活。

そこで普通のシャドウマップに戻って遅延レンダリングができるようになったのが前回。
ついにカスケードシャドウマップを遅延レンダリングで試すときが来た。

その前にそもそもシャドウマップにはいろいろ問題がある。
シャドウマップ

この画像は、深度バッファ1024で影プロジェクションのNear、Farを適当に表示される範囲で調整したもの。
表示されるマップの範囲が大きくなると奥のほうが収まらない。その状態で表示すると深度バッファに収まっていない範囲の影がなくなったり、全部影になったりする。影プロジェクションのNear、Farが表示範囲ギリギリに調整されている状態を保っていないと影の精度が悪くなる。
精度が悪いとシャドウアクネが出て、バイアスの値を上げないといけなくなる。上げ過ぎると左の壁の天井の様に隙間ができる。

カスケードシャドウマップ

これがカスケードシャドウマップ。
カスケードレベルは4で、深度バッファは4096×1024。
4回光源から見た深度バッファをレンダリングする。
近くは精度の高い深度バッファを使って、遠くは精度の低い深度バッファ使う。

利用深度バッファ確認

どの深度バッファを利用しているのか色分けするとこんな感じ。
赤が一番手前で、黄緑、青、黄色の順で遠くになる。

カスケードレベル1

ちなみにカスケードレベルを1にして描画した状態がこれ。
なんの考慮もしていないシャドウマップよりかはNear、Farの自動調整がついているので精度が高くなりそう。

2022年7月19日火曜日

Deferred Shadowmap

前回シャドウマップができたけど、これって1シーンに複数のメッシュがある場合はまず深度バッファに全部分書き込んで、各メッシュを描画する度に影の処理をする必要がある。

某書籍でディファードレンダリングという言葉を目にした。
色々調べていくと、どうやら通常のレンダリングのことをフォワードレンダリングというのに対して、ディファードレンダリングというその名の通り遅延して後からレンダリングする方法で、ライトを大量に扱える仕組みらしい。
ライトはまだ良くわからないけど、なんとなく影と同じじゃないかと思った。
あるメッシュが作る影が別のメッシュに影響を与えるから、メッシュをレンダリングする度に影の考慮も必要になる。
もしかするとディファードレンダリングを使えば、各メッシュをレンダリングするときは影は考慮せずレンダリングして、最後に1回だけ影描画すれば行けるんじゃないかと。

某書籍を順を追って実装していた時、マルチレンダーターゲットでピクセルシェーダから色と法線に分けて出力して、そのあとそれぞれをテクスチャとして受け取ってディフューズの計算をしてレンダリング出力することはやった。
シャドウマップで必要なのは、色、法線に加えて、頂点の位置を影の行列で変形した位置情報が必要なのでそれを出力してみた。

ピクセルシェーダ
	Out.Col = In.Col ;			// 色
	Out.Normal = In.Normal ;	// 法線
	Out.SPos = In.SPos ;		// 影空間の位置


ディファードレンダリングのシャドウマップ

なんかそれっぽく描かれたけどおかしなところがたくさんある。
まず気づいたのが法線。
立体物の面が黒くなってディフューズの計算がうまく行ってない。
前にやったときはうまく行ってたはずなので本を読み返してみるとちょっと違っていた。


法線テクスチャ出力時
	Out.Normal = float4( In.Normal.xyz * 0.5 + 0.5, 1.0 ) ;
最初何をしているかわからなかったんだけど、法線の値は-1~1の範囲なので0.5を掛けて-0.5~0.5の範囲にして、0.5を足すことにより、0~1の範囲におさめているっぽい。
この変換をしてなかったときはマイナスのデータが消えてしまっている。

法線テクスチャ参照時
	float3 Normal = TexNormal.Sample( Sampler, In.UV ).xyz * 2.0 - 1.0 ;
参照時は0~1に変換されたデータを、2を掛けて0~2にして、1を引くことで-1~1の範囲に復元している。

ディファードレンダリング 法線修正版

ディフューズの計算はうまく行ったみたいだけど、影が足りない。
何がどうなってるかわからないので、ネットで調べているとUVのグリッドを描画することで確認をする方法を見つけた。

フォワードレンダリング グリッド表示

ディファードレンダリング グリッド表示

フォワードレンダリングではUVのグリッドが全体をカバーしているが、ディファードレンダリングのUVは右上だけで左下の範囲がない感じだ。
このUVの計算は、SPosのテクスチャを利用している。
もしかして、法線みたいにマイナスの値がだめなのかもしれない。
いろいろ調べていくと、位置のVectorはwで割ることにより-1~1の範囲に収められるらしいことがわかった。

位置テクスチャ出力時
	Out.SPos = float4(( In.SPos.xyz / In.SPos.w ) * float3( 0.5, -0.5, 0.5 ) + 0.5, 1.0 ) ;
出力時、xyz / wで-1~1の範囲に変換して、xyは送り込んだ先でuvとしてそのまま使うため、0.5を掛けて0.5を足すことで0~1の範囲に変換する。
y座標に関しては上が大きくて下が小さい。
v座標は上が0で、下が1なので逆にするために-0.5を掛けている。
zはxと同じなのでここでは0~1の範囲に収まっている。

位置テクスチャ参照時
	float3 Shadow = TexSPos.Sample( Sampler, In.UV ).xyz ;
    float z = Shadow.z * 2.0 - 1.0 ;
    float2 ShadowUV = Shadow.xy ;
参照時は、xyは変換無しでそのままをUVとして利用、zは2を掛けて1を引くことで、-1~1の範囲に変換している。

ディファードレンダリング 位置修正版

位置情報を補正した結果、UVの範囲が全体になって影が表示されるようになった。
精度があらすぎてものすごい分厚いシャドウアクネ出てるので、それも補正しておく

ディファードレンダリング バイアス補正版

フォワードシェーディングでのzのバイアスは0.001だったのに対して、ディファードレンダリングのバイアスは0.01で10倍。
影の精度もめちゃくちゃ悪く、フォワードレンダリングよりも粗すぎてこの状態では使い物にならない。精度の粗さで本来1本のグリッドが2つに分かれて広がっているんだろう。
ネットで情報を集めつつ試行錯誤を1週間続けたが解決できなかった。
ディファードレンダリングは諦めて、フォワードレンダリングで頑張っていこうと決意した矢先、気になるページを見つけた。

ここには、zの座標から元の位置を算出する方法が書かれていた。
元の座標がわかれば影描画につかった行列を掛けて直接SPosを作り出せるはず。
そしたらもっと精度が良くなるかも?という淡い期待を胸に実装してみるが結果はだめ。
UVのGridも全く描画されなかったり斜めに1本だけ書かれたりで全くうまくいかない。

そもそも法線や、位置のデータはなんで直接そのまま渡せないんだろう?
よく考えればわかることだったが、なぜかそこまで考えが至らなかった。
きっとVSからPSへは思った通りにデータが受け渡されているから余計に勘違いしやすかった。

ピクセルシェーダのアウトプットはfloat4となっているので、単純にfloat4つ分のデータが出力されていると勘違いしていたが、用意しているテクスチャのフォーマットがDXGI_FORMAT_R8G8B8A8_UNORMなので、実際はunsigned intの4バイトだろう。各rgbaは0~255までの範囲のデータでしかない。だから0~1の範囲に収めてやるとテクスチャ出力時には0~255に変換され、そのテクスチャを読み込む際は、自分で-1~1に復元しないといけなかった。
(DXGI_FORMAT_R16G16B16A16_UNORMにしたり、DXGI_FORMAT_R32G32B32A32_FLOATにすれば精度は上がるんだろうけど、その分サイズが4倍、16倍になるので現実的ではなくなる気がする)
	float4 DepthToPos(
		in		float		z,		// Depth
		in		float2		uv,		// UV
		in		float4x4	ipv		// Inverse Proj * View
	) {
		float4 p = float4( uv.x * 2.0f - 1.0f, ( 1.0f - uv.y ) * 2.0f - 1.0f, z, 1.0f ) ;
		p = mul( ipv, p ) ;
		p.xyz = p.xyz / p.w ;
		p.w = 1.0f ;
		return p ;
	}
この関数のzにG-Buffer経由の位置情報のZを渡していたが、精度が圧倒的に足りてないと気づいたので、32bitをZに全振りしてる深度バッファの値を渡してみた結果・・・

ディファードレンダリング 深度バッファ版

ついに、フォワードシェーディングと同じくらいの精度でディファードレンダリングでもシャドウマップがレンダリングできた。

ディファードレンダリング 複数メッシュ

最初に言ったディファードレンダリングなら、影の深度バッファを作ってしまえばそれぞれのメッシュを描画する際は影のことは一切考えなくても大丈夫というのは本当だった。
マップとおっさんを同時に描画して、影は最後に付けているけど、マップの影はおっさんにかかっているし、おっさんの影も自分自身とマップにも反映されている。

Diferred Shadowのキーワードで探しまくったが出てくるのはOpenGLやUnityの情報ばかりでDirectXの情報はかなり少なかった。途中ディファードレンダリングでは影は扱えないのか?と不安になりつつも情報を集め続けた結果、意味の分からなかった計算もなんとなくわかったし、良かった・・・。

2022年7月8日金曜日

Shadowmap

3Dの記事を見てると、その描画に必要だった材料を目視で確認できるように画面の端にワイプで表示させてたりする。
影を描画するのに深度バッファを参照して云々みたいな時に、その時の深度バッファを同時に表示させていてどうやってやるんだろうと思っていた。

で、やり方はレンダーターゲットがテクスチャとして使えるみたいに、深度バッファも同じようにテクスチャとして使える。単純にそれだけだった。

影用の深度バッファ書き込みだけならレンダリングターゲットの設定も、PSのシェーダも必要ないみたい。


テクスチャとして深度バッファを使う場合、フォーマットをDXGI_FORMAT_D32_FLOATではなくDXGI_FORMAT_R32_TYPELESSで作成する。
	D3D12_RESOURCE_DESC oDesc = {} ;
	oDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D ;
	oDesc.Width = tNum( oSize.W()) ;
	oDesc.Height = tNum( oSize.H()) ;
	oDesc.Alignment = 0 ;
	oDesc.DepthOrArraySize = 1 ;
	oDesc.MipLevels = 0 ;
//	oDesc.Format = DXGI_FORMAT_D32_FLOAT ;
	oDesc.Format = DXGI_FORMAT_R32_TYPELESS ;
	oDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN ;
	oDesc.SampleDesc.Count = 1 ;
	oDesc.SampleDesc.Quality = 0 ;
	oDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL ;

	D3D12_CLEAR_VALUE oCV ;
	oCV.Format = DXGI_FORMAT_D32_FLOAT ;	// こっちはD32のまま
	oCV.DepthStencil.Depth = 1.0f ;
	oCV.DepthStencil.Stencil = 0 ;
パイプラインステート設定
	D3D12_GRAPHICS_PIPELINE_STATE_DESC	oGPSD = {} ;

	// PSは設定無し
	D3D12_SHADER_BYTECODE & oSB = oGPSD.PS ;
	oSB.pShaderBytecode = nullptr ;
	oSB.BytecodeLength = 0 ;

	// 深度バッファは有効
	auto & oDSS = oGPSD.DepthStencilState ;
	oDSS.DepthEnable = TRUE ;
	oDSS.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL ;
	oDSS.DepthFunc = D3D12_COMPARISON_FUNC_LESS ;
	oDSS.StencilEnable = FALSE ;
	oDSS.StencilReadMask = D3D12_DEFAULT_STENCIL_READ_MASK ;
	oDSS.StencilWriteMask = D3D12_DEFAULT_STENCIL_WRITE_MASK ;
	const D3D12_DEPTH_STENCILOP_DESC defaultStencilOp = { D3D12_STENCIL_OP_KEEP, D3D12_STENCIL_OP_KEEP, D3D12_STENCIL_OP_KEEP, D3D12_COMPARISON_FUNC_ALWAYS } ;
	oDSS.FrontFace = defaultStencilOp ;
	oDSS.BackFace = defaultStencilOp ;
	oGPSD.DSVFormat = DXGI_FORMAT_D32_FLOAT ;

	// レンダーターゲットも0
	oGPSD.NumRenderTargets = 0 ;
	oGPSD.RTVFormats[0] = DXGI_FORMAT_UNKNOWN ;
描画準備
	// レンダーターゲットはないけど、ビューポートとシザー矩形の指定は必須
	D3D12_VIEWPORT oVP = pDB->GetViewPort() ;
	pGCL->RSSetViewports( 1, &oVP ) ;
	tRECT oSR = pDB->GetRect() ;
	pGCL->RSSetScissorRects( 1, &oSR ) ;

	// レンダーターゲットは0で、深度バッファのみ指定
	auto oDBDH = p3D->GetCPUDH( pDB->Type(), pDB->SlotDB()) ;
	pGCL->OMSetRenderTargets( 0, nullptr, false, &oDBDH ) ;
影用の深度バッファを描画したら、次は普通の描画を行う際に上記の深度バッファをテクスチャとして設定する。
その際、バリアで深度バッファのリソースステートを「DEPTH_WRITE」から「PIXEL_SHADER_RESOURCE」へ変更する。
参照が終わったら、逆に戻す。 
	D3D12_RESOURCE_BARRIER oRB = {} ;
	oRB.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION ;
	oRB.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE ;
	oRB.Transition.StateBefore = D3D12_RESOURCE_STATE_DEPTH_WRITE ;
	oRB.Transition.StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE ;
	oRB.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES ;
	oRB.Transition.pResource = pR->GetResource() ;
 某書籍では、そのテクスチャのシェダー定義を「Texture2D<float>」にすると書かれていたけど、「Texture2D」のままで大丈夫だった。 
渡したリソースにより勝手に変わってくれるので「Texture2D」のままのほうが便利。 ただ、「Texture2D<float>>」の方はそのテクスチャがfloatと解っているので最適化されて処理が速いとかあったらやだな。



右上のは通常描画時の深度バッファで、その下がシャドウマップ用の深度バッファ。 
モデルのまわりでライトを動かして見た感じ。
セルフシャドウと、床のシャドウができた。
平面の床限定の簡易的な方法もあるみたいだけど、これができればいらなさそう。

2022年7月6日水曜日

いろいろなノイズのアルゴリズム


Minecraftとかマップを自動生成するような時に、パーリンノイズというものが使えるというのを読んだことがあり、いつか用意しようと思っていた。

ノイズというもを調べてみるといろいろあるみたい。

ホワイトノイズ


ホワイトノイズ

これは単純にランダムの値をそのままドットに置き換えただけ。

バリューノイズ

ここから先のノイズには乱数の取得に仕組みが必要になる。
あるx,y座標についての値を乱数で決めたとき、再び同じx,y座標で同じ値を取得できるようになっていないといけない。
通常乱数は取得を繰り返すと次々に違う値が得られる。
ノイズを生成するときは、毎回Seedを指定するようなイメージで、指定した引数に対する値は毎回同じものが返る必要がある。

・テーブルを準備する方法
単純な仕組みは引数に渡す範囲の配列を用意して、固定値を設定しておいたり初期化時に乱数値を代入して順にする方法。
メリットは参照時に計算無しで値が得られる。
デメリットはサイズ分のメモリが必要なのと、その値の準備が必要。ソース上に固定値で準備する場合はサイズ変更が容易ではない。

・疑似乱数で計算する方法
Xorshiftのような疑似乱数で計算する方法。
メリットは準備不要。テーブル用のメモリも不要。
デメリットは参照のたびに計算が必要なので、テーブル参照よりは遅くなる。適切なSeedを指定しないと、乱数の結果に偏りがでる。


同じ地点で同じ値が得られるようになったら、各座標の4点で値を取得してそれぞれの点を補間するとこんな感じになる。

バリューノイズ1マス分

これをつなげていくとこんな感じになる。

バリューノイズ

ネットで調べていると、バリューノイズをこの後出てくるパーリンノイズと勘違いしているものがいくつかあった。

パーリンノイズ

パーリンノイズはバリューノイズの座標の4点の乱数値ではなく、その乱数値を利用して勾配ベクトルを取得して、各頂点から入力のx,y座標の距離ベクトルとの内積で求めるらしいが、この勾配ベクトルの選択が乱数の内容によって偏ってしまいいい感じにならない場合が多かった。
パーリンノイズ失敗例

単純にmod4で分けると計算で疑似乱数で生成した値だと下2ビットが偏っててうまく分かれないと予想して、もう少し大きい数字の範囲で4分割する感じにしてみた。
パーリンノイズ


シンプレックスノイズ

シンプレックスノイズはパーリンノイズを改良して計算量を減らしたものらしい。
パーリンノイズは四角で考えるが、シンプレックスノイズは3角形で考えて、1点分少なく計算できる。
ただ、高次元の場合に効果を発揮するらしく、2次元の場合はむしろ重い気がする。

シンプレックスノイズ

セルラーノイズ

セルラーノイズは細胞みたいなノイズ。格子内を9分割して一番近い距離を値にする。
セルラーノイズ

セルラーノイズ反転版

反転したのはキモい。

ボロノイズ

ボロノイ図というものがあるらしく、セルラーノイズを利用して作れる。距離の最小になった点の座標自体を値にする。
ボロノイズ
岩のテクスチャとかに使えそう。

非整数ブラウン運動(フラクタルノイズ)

大小の周波数を重ね合わせてフラクタル化することで、ノイズの粒度を細かくすることができる。この手法は「フラクタルブラウン運動」(fBM)または単に「フラクタルノイズ」と呼ばれる。
バリューノイズ&fBM

パーリンノイズ&fBM

シンプレックスノイズ&fBM

ノイズ画像加工


バリューノイズに色付け
バリューノイズ&fBMで得られた値を見て、半分よりも上なら緑、下なら青にするようにするとこんな画像になる。
auto fSeaCB = []( tF4 n ) -> tU4 {
	tU1 c = tU1( n * 255.0f ) ;
	if( c > 127 ) {
		return 0xff << 8*3 | RGB( 0, c, 0 ) ;
	}
	return 0xff << 8*3 | RGB( 0, 0, c*2 ) ;
} ;


パーリンノイズの木目
パーリンノイズで得られた値に適当な数値を掛けて、小数部だけを使うとこんな画像になる。

auto fWoodCB = []( tF4 n ) -> tU4 {
	n *= 20.0f ;
	n = n - tU4( n ) ;
	tU1 c = tU1( n * 255.0f ) ;
	return 0xff << 8*3 | RGB( c, c, c ) ;
} ;

ドメインワーピング

これまではCPU側でノイズ関数を使って画像を作って、それをテクスチャとして表示していた。
これを、GPU側のピクセルシェーダでノイズ関数を動かして出力するようにすれば、リアルタイムに動かすことができるようになる。

パーリンノイズとfBMのシェーダ
float2 rand( float2 st )
{
	float2 s = float2( dot( st, float2( 127.1, 311.7 )) + _Seed, dot( st, float2( 269.5, 183.3 )) + _Seed ) ;
	return -1 + 2 * frac( sin( s ) * 43758.5453123 ) ;
}
float Noise( float2 st )
{
	float2 p = floor( st ) ;
	float2 f = frac( st ) ;
 
	float w00 = dot( rand( p                 ), f                 ) ;
	float w10 = dot( rand( p + float2( 1, 0 )), f - float2( 1, 0 )) ;
	float w01 = dot( rand( p + float2( 0, 1 )), f - float2( 0, 1 )) ;
	float w11 = dot( rand( p + float2( 1, 1 )), f - float2( 1, 1 )) ;
				
	float2 u = f * f * f * ( f * ( f * 6 - 15 ) + 10 ) ;
 
	return lerp( lerp( w00, w10, u.x ), lerp( w01, w11, u.x ), u.y ) * 0.5 + 0.5 ;
}
float fBM( float2 st )
{
	float v = 0.0 ;
	float a = 0.5 ;
	for( int i = 0 ; i < _Octave ; i++ ) {
		v += a * Noise( st ) ;
		st *= 2.0 ;
		a *= 0.5 ;
	}
	return v ;
}
_Seedと_OctaveがConstantBufferで指定できる定義。
_Seedは乱数Seed。
_Octaveは周波数を重ね合わせる回数。4回か5回


ドメインワーピングのシェーダ
float4	DomainWarping( float2 st )
{
	float time = _Time / 60.0 * _Speed ;
	st *= _Scale2 ;

	float2 q ;
	q.x = fBM( st ) ;
	q.y = fBM( st + float2( 1.0, 1.0 )) ;
	float2 r ;
	r.x = fBM( st + _Scale1 * q + float2( 1.7, 9.2 ) + 0.15 * time ) ;
	r.y = fBM( st + _Scale1 * q + float2( 8.3, 2.8 ) + 0.126 * time ) ;
	float f = fBM( st + _Scale1 * r ) ;
	float3 color ;
	color = lerp( _Color1.rgb, _Color2.rgb, saturate( f * f * 4.0 )) ;
	color = lerp( color, _Color3.rgb, saturate( length( q ))) ;
	color = lerp( color, _Color4.rgb, saturate( length( r.x ))) ;
	return float4(( f*f*f+0.6*f*f+0.5*f ) * color, 1.0 ) ;
} 
_Time、_Speed、_Scale1、_Scale2、_Color1~4がConstantBufferで指定できる定義。
_Timeは60fpsで1フレーム毎に1ずつ増加する値。
_Speedは変化のスピード調整用。1.0fを指定。
_Scale1、_Scale2はそれぞれスケール調整用。1.0fを指定。
_Color1~4は色の指定。
デモの指定色。
_Color1 = { 0.101961f, 0.619608f, 0.666667f, 1.0f } ;
_Color2 = { 0.666667f, 0.666667f, 0.498039f, 1.0f } ;
_Color3 = { 0.0f, 0.0f, 0.164706f, 1.0f } ;
_Color4 = { 0.666667f, 1.0f, 1.0f, 1.0f } ;


シェーダでパーリンノイズ、fBMを計算して、ドメインワーピングしたのがこれ。
パーリンノイズ版ドメインワーピング


シンプレックスノイズと歪み追加版fBMのシェーダ
float3 mod289( float3 x ) { x += _Seed ; return x - floor( x * ( 1.0 / 289.0 )) * 289.0 ; }
float2 mod289( float2 x ) { x += _Seed ; return x - floor( x * ( 1.0 / 289.0 )) * 289.0 ; }
float3 permute( float3 x ) { return mod289((( x * 34.0 ) + 1.0 ) * x ) ; }
float Noise( float2 v ) {
	const float4 C = float4( 0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439 ) ;

	float2 i  = floor( v + dot( v, C.yy )) ;
	float2 x0 = v - i + dot( i, C.xx ) ;

	float2 i1 = float2( 0.0, 0.0 ) ;
	i1 = ( x0.x > x0.y ) ? float2( 1.0, 0.0 ) : float2( 0.0, 1.0 ) ;
	float2 x1 = x0.xy + C.xx - i1 ;
	float2 x2 = x0.xy + C.zz ;

	i = mod289( i ) ;
	float3 p = permute( permute( i.y + float3( 0.0, i1.y, 1.0 )) + i.x + float3( 0.0, i1.x, 1.0 )) ;
	float3 m = max( 0.5 - float3( dot( x0, x0 ), dot( x1, x1 ), dot( x2, x2 )), 0.0 ) ;

	m = m*m ;
	m = m*m ;

	float3 x = 2.0 * frac( p * C.www ) - 1.0 ;
	float3 h = abs( x ) - 0.5 ;
	float3 ox = floor( x + 0.5 ) ;
	float3 a0 = x - ox ;

	m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h ) ;

	float3 g ;
	g.x  = a0.x  * x0.x  + h.x  * x0.y ;
	g.yz = a0.yz * float2( x1.x, x2.x ) + h.yz * float2( x1.y, x2.y ) ;
	return (( 130.0 * dot( m, g )) + 1.0 ) * 0.5 ;
}

float fBM( float2 st )
{
	float v = 0.0 ;
	float a = 0.5 ;
	float2 shift = float2( 50.0, 50.0 ) ;
	float2x2 rot = { cos(0.5), sin(0.5), -sin(0.5), cos(0.5)} ;
	for( int i = 0 ; i < _Octave ; i++ ) {
		v += a * Noise( st ) ;
		st = mul( st * 2.0, rot ) + shift ;
		a *= 0.5 ;
	}
	return v ;
}
_Seedと_OctaveがConstantBufferで指定できる定義。

シンプレックスノイズ、歪みを追加したfBMをつかってドメインワーピングしたのがこれ。
シンプレックスノイズ版ドメインワーピング

テクスチャ参照版ドメインワーピング

シェーダでノイズ画像を作ってドメインワーピングしてると、タスクマネージャのGPU使用率が30%~40%ぐらいになっていた。
グラフィックボードは高くて買えないので、Intelの内蔵GPUが頑張ってる。
Intelのせいなのか、AMDやNVIDIAでも同じようなものなのかはわからないがGPUがすごく頑張ってる。
テクスチャの画像を元に、ドメインワーピングしたらドメインワーピング内で5回fBMを呼び出している部分がテクスチャ画像参照5回に置き換えられる。

CPU側で用意したシンプレックスノイズ&fBM画像をテクスチャに指定して、ピクセルシェーダで、そのテクスチャを参照するように置き換えたバージョンがこれ。


テクスチャ参照版ドメインワーピングのシェーダ
float4	DomainWarping( float2 st )
{
	float time = _Time / 60.0 * 0.05f * _Speed ;
	st *= _Scale2 * 0.03f ;

	float2 q ;
	q.x = Texture0.Sample( Sampler0, st ).r ;
	q.y = Texture0.Sample( Sampler0, st + float2( 1.0, 1.0 )).r ;

	float s = _Scale1 * 0.02 ;
	float2 r ;
	r.x = Texture0.Sample( Sampler0, st + s * q + float2( 1.7, 9.2 ) + 0.15 * time ).r ;
	r.y = Texture0.Sample( Sampler0, st + s * q + float2( 8.3, 2.8 ) + 0.126 * time ).r ;

	float f = Texture0.Sample( Sampler0, st + s * r ).r ;

	float3 color ;
	color = lerp( _Color1.rgb, _Color2.rgb, saturate( f * f * 4.0 )) ;
	color = lerp( color, _Color3.rgb, saturate( length( q ))) ;
	color = lerp( color, _Color4.rgb, saturate( length( r.x ))) ;

	return float4(( f*f*f+0.6*f*f+0.5*f ) * color, 1.0 ) ;
}
_Time、_Speed、_Scale1、_Scale2、_Color1~4がConstantBufferで指定できる定義。
fBMを呼び出しているところをテクスチャ参照に置き換えただけ。

テクスチャ参照版ドメインワーピング

GPU使用率は8%ぐらいに下がっていた。

ライブラリの準備ができたけど、これらをどう使っていくのかがまだ良くわからない。
マップの自動生成とかは予想がつくが、水とか火とかの表現につかったりできるんだろうなぁ。

参考記事

このサイト超すごい。
プログラムのソースを触れてリアルタイムで結果が反映される。

2022年6月29日水曜日

DirectX12 Skeletal Animation With Assimp

DirectX11で3Dライブラリを用意していたが、DirectX12で作り直すことにした。

経緯はDirectX11で作るときにも11にするか12にするか検討したが、12はハードルが高いということだったので手っ取り早く11で作ってみた。
作ってからほかが忙しくなり、それからしばらく半年?1年くらい経って、ライブラリ開発を再開しようとしたら何がなにやらわからなくなっていた。

というわけで11のライブラリは捨てて12で作り直しすることにした。
11と同じことができるようになるまで1ヶ月ぐらいかかったがなんとかできた。
ここから更にメッシュデータの表示に手をだして見ることに。
DirectX3ぐらいのときにスキンメッシュについては全く理解できなかったので、敬遠していたが「DirectX 12の魔導書 3Dレンダリングの基礎からMMDモデルを踊らせるまで」という本を買って、pmdのロードからvmdのロード、FK、IK、表示までを作ってみた。
FKまではうまく行っているように見えるが、IKの部分でクリーチャーが出現する。

本の誤植もひどく、10回ぐらいソースを見直して著者のgithubと見比べながら直したが、そもそも著者のソースの中にこっちが正しいはずなのになんで?みたいなコメントもあり信用できなくなった。PCのkindleも使いづらく、まともにスクロールも検索もできない。
著者のソースはライブラリを別途用意しないとコンパイルできなかったのでずっとソースだけ参照していたが、どうしてもうまくいかないのでライブラリも準備してコンパイルを通して実行してみたところ、著者のプログラムでも全く同じ結果になっていた。
それはいくらやっても無理なはずだ・・・。

そもそもpmxならまだしもpmdが表示できるようになっても意味はなく、blenderとかで用意できる汎用的なファイルフォーマットはないのかと調べたところfbxがいいらしいということがわかった。

今度はfbxの読み込みライブラリについて調べていくとassimpというものにたどり着いた。
このライブラリを使えば、fbxだけでなくいくつかのフォーマットを読み込むことができるのでこのライブラリを使ってみることにした。

オープンソースのバージョン管理に疲れたので、NuGetに対応してないと適用をしたくない病にかかっていたが、問題なくあった。ただし、個人ではなく公式のものがいいので、Assimp_native、Assimp_native_4.1、Assimp_native_v142が候補だが、Assimp_nativeはVer4.0.1で他のVer4.1よりも古い。Assimp_native_v142はvs2019用で、ラインタイムを別途用意しないと動かない。一番いいのは開発しているvs2022用のv143だが、存在せず。Assimp_native_4.1はv140だが、ランタイムがwin11にデフォルトで入ってるっぽくしばらくはこれを使ってみることにする。
assimpの最新バージョンは5.2.4(2022/6/29時点)らしいので、どこかで新しくしたい。
v143がないので最悪自分でビルドか・・・

次にこれを使ったサンプルを探すとこんなサイトを見つけた。
やってることはまさにこれで、このチュートリアルで勉強してみることにした。
モデルの読み込み自体はassimpがやってくれるので特に問題はなかったが、フラグがいまいちよくわからなかった。
最終的なフラグはこんな感じ
		tSStr sSFile = sFile ;	// ここはsjis
		auto nFlag = aiProcess_ConvertToLeftHanded ;
		nFlag |= aiProcess_Triangulate ;
		nFlag |= aiProcess_JoinIdenticalVertices ;
		nFlag |= aiProcess_CalcTangentSpace ;
		nFlag |= aiProcess_GenSmoothNormals ;
		nFlag |= aiProcess_GenUVCoords ;
		nFlag |= aiProcess_TransformUVCoords ;
		nFlag |= aiProcess_RemoveRedundantMaterials ;
		nFlag |= aiProcess_OptimizeMeshes ;
		nFlag |= aiProcess_LimitBoneWeights ;
		auto pScene = Importer.ReadFile( sSFile.Ptr(), nFlag ) ;
assimp内の文字列はutf-8っぽいんだけど、ReadFileにわたす文字はutf-8ではなくsjisみたい。DirectXを使うならaiProcess_ConvertToLeftHandedを指定して、左手系にする必要がある。OpenGLは右手系みたい。
マニュアルを見ると、このフラグはおすすめ、効果的みたいな感じでどれをつけていいか悩む。読み込んだあとデータを加工してくれるようでそれなりに時間がかかるのでOn/Offできるようにしよう。

頂点データ、インデックスデータ、マテリアルを読み込んで描画する分にはすぐにできた。
両手を広げたおっさんが表示されるようになった。
いざアニメーションに取り掛かろうとしたけど、アニメーションデータがない。確実に入っているはずのデータを読み込んでもアニメーションデータがない。
他のサイトを見たとき、aiProcess_PreTransformVerticesが指定されているサンプルがあって、それをつけてたんだけどこれをつけてるとアニメーションデータが削除されるみたい。罠だった。

アニメーションデータが参照できるようになって、ボーンデータとアニメーションデータの取り込みをしていざ表示してみるとうまくいかなかった。
このサイトはOpenGLのチュートリアルで、それをDirectXの関数に直してみたんだけど、どこが悪いのかがわからず1週間ぐらい悩むことになる。
DirectX assimp animationとか検索文字列を変えながら検索結果の全部のサイトを見てみるけど、DirectXで動くソースがあるサイトは見つからず普段あまり見ない中国のサイトで1つそのものズバリのソースを見つけた。
VisualStudioで読み込んでみると文字コードがおかしくて中国語が含まれてるのに、sjisで読み込むもんだから文字化けしまくり。コメントだけならどうせ読めないから問題ないとコンパイルしてみるも、エラーだらけ。コメント部分の後ろにソースが巻き込まれてる。全部改行を入れていってコンパイルしたらなんとかコンパイルは通ってモデルも表示されていた。これでなんとかなると思ってたら何回か実行すると起動しなくなった。メモリ関連に致命的な問題があるのか、動いていたexeが何回か動かすと動かなくなるなんて初めて体験した。でもまあ、動いたソースなので参考にはなった。

まず、assimpから取り出すaiMatrix4x4はすべて転置が必要。参考にしたソース全部がXMMatrixTransposeを使っていたが、下記のような変換関数で直接代入した。
(tDFloat44はDirectX::XMFLOAT4X4の別名)
	auto fConvToDFloat44 = []( const aiMatrix4x4 & m ) -> tDFloat44 {
		return tDFloat44(
			m.a1, m.b1, m.c1, m.d1,	// 転置
			m.a2, m.b2, m.c2, m.d2,
			m.a3, m.b3, m.c3, m.d3,
			m.a4, m.b4, m.c4, m.d4
//			m.a1, m.a2, m.a3, m.a4,	// そのまま
//			m.b1, m.b2, m.b3, m.b4,
//			m.c1, m.c2, m.c3, m.c4,
//			m.d1, m.d2, m.d3, m.d4
		) ;
	} ;
pScene->mRootNode->mTransformationの逆行列を用意する部分と、ボーン情報のaiBone.mOffsetMatrix、アニメーションノードのaiNode.mTransformationの取り出す際に上記関数で取り出す。

すべてのポイントはReadNodeHierarchy関数。
OpenGLとDirectXでは行列の掛け方が逆らしい。

位置と回転、スケールの補完したあとの行列の計算
    NodeTransformation = TranslationM * RotationM * ScalingM;
    ↓
	NodeTransformation = ScalingM * RotationM * TranslationM;

上記結果と親の行列をかけ合わせる部分
(tDMatrixはDirectX::XMMATRIXの別名)
    Matrix4f GlobalTransformation = ParentTransform * NodeTransformation
    ↓
	tDMatrix GlobalTransformation = NodeTransformation * ParentTransform ;

ボーンマトリックス計算部分    
	m_BoneInfo[BoneIndex].FinalTransformation = m_GlobalInverseTransform * GlobalTransformation * m_BoneInfo[BoneIndex].OffsetMatrix;
    ↓
	m_BoneInfo[BoneIndex].FinalTransformation = m_BoneInfo[BoneIndex].OffsetMatrix * GlobalTransformation * m_GlobalInverseTransform;

また、CalcInterpolatedScaling、CalcInterpolatedPositionの部分はDelta計算不要でXMVectorLerpで置き換えられた。
    aiVector3D Delta = End - Start;
    Out = Start + Factor * Delta;
    ↓
	return DirectX::XMVectorLerp( Start, End, Factor ) ;

CalcInterpolatedRotationはXMQuaternionSlerpで置き換えられた。
    aiQuaternion::Interpolate(Out, StartRotationQ, EndRotationQ, Factor);
    Out = StartRotationQ;
    Out.Normalize();
    ↓
	return DirectX::XMQuaternionNormalize( DirectX::XMQuaternionSlerp( StartRotationQ, EndRotationQ, Factor )) ;

VertexShaderはBoneTransformとposを掛けて、それをProjction*View*WorldかかってるTransと掛ける。
	float4x4 BoneTransform
		=  Bones[boneid[0]] * weight[0]
		+  Bones[boneid[1]] * weight[1]
		+  Bones[boneid[2]] * weight[2]
		+  Bones[boneid[3]] * weight[3]
	;
	Out.pos = mul( Trans, mul( BoneTransform, float4( pos, 1 ))) ;

一番嵌ったのは、boneidを頂点データと送り込む際、メッシュ単位でデータを作るのでメッシュ単位の頂点データのオフセットを足すのを忘れていて、これが原因で正しく描画できていなかった。
最終的にはこんな感じで動くようになった。

2022年4月21日木曜日

Moveの動作

class T
{
public :
	T( void ) { mLogV( L"Constructor") ; }
	T( const T & o ) { mLogV( L"Copy Constructor") ; }
	T( T && o ) { mLogV( L"Move Constructor") ; }

	T &			operator = ( const T & o ) { mLogV( L"Copy Operator") ; return *this ; }
	T &			operator = ( T && o ) { mLogV( L"Move Operator") ; return *this ; }

	static
	T			Func1( tV ) { T o ; return o ; }				// Make系関数 自動でmove扱いになる
	T			Func2( tV ) { return *this ; }					// コピー
	T &			Func3( tV ) { return *this ; }					// 参照 呼び出し元でstd::moveにすると、move扱いに。
//	T &&		FuncX( tV ) { return *this ; }					// コンパイルエラー
	T &&		Func4( tV ) { return std::move( *this ) ; }		// move
//	T &&		FuncX( tV ) { T o ; return o ; }				// コンパイルエラー
	T &&		Func5( tV ) { T o ; return std::move( o ) ; }	// move Func1の強制版。通常ここまでやる必要なし
	static
	T			Func6( T o ) { return o ; }						// 引数は渡し方により、copy、moveを操作可能 returnはmove
	static
	T			Func7( T & o ) { return o ; }					// 引数は参照 returnはcopy
	static
	T			Func8( T && o ) { return o ; }					// 引数はmove returnはmove

} ;
    T o ;							// Constructor

    T o1 = o ;						// Copy Constructor
    T o2 = std::move( o ) ;			// Move Constructor
    T o3 = o.Func1() ;				// Constructor - Move Constructor
    T o4 = o.Func2() ;				// Copy Constructor
    T o5 = o.Func3() ;				// Copy Constructor
    T o6 = std::move( o.Func3()) ;	// Move Constructor
    T o7 = o.Func4() ;				// Move Constructor
    T o8 = o.Func5() ;				// Constructor - Move Constructor

    o = o1;							// Copy Operator
    o1 = std::move( o );			// Move Operator
    o3 = o.Func1() ;				// Constructor - Move Constructor - Move Operator
    o4 = o.Func2() ;				// Copy Constructor - Move Operator
    o5 = o.Func3() ;				// Copy Operator
    o6 = std::move( o.Func3()) ;	// Move Operator
    o7 = o.Func4() ;				// Move Operator
    o8 = o.Func5() ;				// Constructor - Move Operator

	o = T::Func6( o ) ;				// Copy Constructor - Move Constructor - Move Operator
	o = T::Func6( std::move( o )) ;	// Move Constructor - Move Constructor - Move Operator
	o = T::Func6( T()) ;			// Constructor - Move Constructor - Move Operator

	o = T::Func7( o ) ;				// Copy Constructor - Move Operator
//	o = T::Func7( std::move( o )) ;	// コンパイルエラー
//	o = T::Func7( T()) ;			// コンパイルエラー

//	o = T::Func8( o ) ;				// コンパイルエラー
	o = T::Func8( std::move( o )) ;	// Move Constructor - Move Operator
	o = T::Func8( T()) ;			// Constructor - Move Constructor - Move Operator