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

2022年7月28日木曜日

Screen Space Ambient Occlusion(SSAO)

いままでの影つながりでスクリーンスペースアンビエントオクルージョンをやってみることにした。

某書籍のチャプター15でやり方が載っているが、いまいちこの本の信頼性が薄れているのでネットで探すことにした。
DeferredRenderingがうまくいくきっかけとなったサイトにSSAOの記事とソースも公開されていたのでこれを参考にやっていこうと思う。

シェーダ内でランダムなデータが必要になるようで、CPU側でこれの準備をする。
シェーダにわたす際、サンプルでは2つのデータをそれぞれ違う方法で渡していた。
1つはStructuredBuffer<float4>という定義でテクスチャとして渡す方法。
もう1つはTexture2D<float4>でテクスチャとして渡す方法。

1つ目の渡し方は自分のライブラリでは未実装だったのでどういったものか調べてみると、定数バッファと同じデータの書き込み方で、シェーダ内では配列として扱えるものだった。
現状メッシュのボーンは255決め打ちで配列を用意して必要分だけ書き込んでいるが、StructuredBufferの方法なら要素数は別途定数バッファで渡す必要はあるが可変個扱えるのは良さそう。
今回の実装は定数バッファで行って、そのうち使えるようにしようと思う。

もう1つは通常テクスチャと同じだけど、サイズが16×16と小さいデータ。
これは自分のライブラリでテクスチャの実装をした際出来ないと結論付けたもので、どうしても64×64以下のデータは作れなかった。MMDの実装をした時、Toonレンダリング用のテクスチャとか、やたらと小さいテクスチャばかりだったので、読み込んだ画像が64×64以下だった場合は、一旦GDI+で拡大してからテクスチャに渡すようにしていた。
もしこのソースでやり方がわかるならラッキーと思い、ソースを調べていったけどどう見てもCPU側で作ったノイズデータを使ってない。
通常GPUに渡すために2つのリソースを用意する。
1つはD3D12_HEAP_TYPE_UPLOADで、もう一つはD3D12_HEAP_TYPE_DEFAULT。一旦UPLOADの方にMapして書き込んで、UPLOADからDEFAULTにCopyTextureRegionでコピーするというのが手順だけど、サンプルではCopyTextureRegionを呼んでいない。
64x64以下のデータで関数を呼び出すとエラーが出てしまいコピー出来ないんだけどどういうことなんだろう?

実際に動かしてみてPIXでデバッグしてみるとやっぱりノイズテクスチャは真っ黒(全部0)で、データが入っていない。シェーダでテクスチャを参照している箇所を固定値に変えても結果は同じだった。この作者は気づいてないのだろうか?

この部分は残念だったけど、ノイズデータは不要とわかったので1つの定数バッファだけで実装してみた。
結果は予想していたものとは違って変な状態。
ソースを見比べても同じようにしたつもりだけどうまくいかない。
PIXでデバッグしてみると、色んなところで値がinfやnanになっている。
サンプルのプログラムも確認してみると、結構infになっている。でもちゃんと表示されている。

サンプルのデバッグ


あと、PIXで実行時間が表示されるが、全体が19.39msかかっていて、SSAOが13.01msとなってた。

SSAOのパフォーマンス

レンダリングのサイズが1920×1080なので、いまテストで動かしている1024×1024よりは大きいけど、ちょっとかかりすぎのような気がする。
SSAOの記事を探している時、SSAOにはいろいろな問題があってそれぞれの問題をこうやって対処したみたいな感じに書かれていたので、期待していたんだけど一気に採用見合わせ。

仕方ないので、某書籍の実装でやってみることにした。
こっちの方はシェーダ内でランダム計算をしていたがその部分は実装済みのデータを利用するようにして修正。

	const int3 puv = int3( In.Pos.xy, 0 ) ;
	const float dp = TexDepth.Load( puv ).r ;
	float4 pos = DepthToPos( dp, In.UV, gCam.InversePV ) ;
	pos.xyz /= pos.w ;

	float3 normal ;
	DeferredDecode( TexNormal.Load( puv ), normal ) ;

	float div = 0.0f ;
	float ao = 0.0f ;
	if( dp < 1.0f ) {
		for( uint i = 0 ; i < gSsao.SampleCount ; i++ ) {
			float3 omega = gSsao.SampleKernel[ i ].xyz ;
			float dt = dot( normal, omega ) ;
			float s = dt < 0.0f ? -1 : 1 ;
			omega *= s ;
			float4 rpos = mul( gCam.TransPV, float4( pos.xyz + omega * gSsao.Radius, 1 )) ;
			rpos.xyz /= rpos.w ;

			const int3 ruv = int3(( rpos.x + 1.0f ) * gSsao.Width * 0.5f, ( 1.0f - rpos.y ) * gSsao.Height * 0.5f, 0 ) ;
			const bool IsOutside = ( ruv.x < 0.0f ) || ( ruv.x > gSsao.Width ) || ( ruv.y < 0.0f ) || ( ruv.y > gSsao.Height ) ;
			if( !IsOutside ) {
				dt *= s ;
				div += dt ;
				float z = TexDepth.Load( ruv ).r ;
				ao += step( z, rpos.z ) * dt ;
			}
		}
		ao /= div ;
	}
	Out.AO = saturate( pow( 1.0f - ao, gSsao.SsaoPower )) ;
	return Out ;

サンプルと某書籍のコードをミックスしてみた。
某書籍にない処理は2箇所。
IsOutSideは画面外の影も画面端に反映されてしまうのを防ぐためのもの(だけど、半径が大きいと消しきれない)
最後のpowで影の強さを調整。
参考にした2つのシェーダソースとも、Zから位置を取得するのにプロジェクションの逆行列を使っている。だけど、DeferredRenderingではプロジェクションとビューの逆行列を使った。両方とも試したけど、若干の違いがあるものの同じような結果になった。別途用意しないと行けないので、カメラに持っているPVをそのまま利用することにする。誰かプロジェクションの逆行列使わないとここがまずいぞって教えてくれないかな?

実行してみるとそれっぽいのが表示された。
SSAO 半径10 試行回数32

半径10は元のサンプルのをそのまま使ってたから。
大きすぎるので調整したのがこれ。

SSAO 半径0.25 試行回数16

一般の記事に出てくる感じの結果にはなった。
この半径って、配置する物体のスケールに合わせる必要がありそうなので、作るもののサイズが決まってから調整していく感じかな。

SSAO 半径5 試行回数128 

試行回数を増やせば半径が大きくてもそれっぽくなるみたい。
半径が小さくて、試行回数が少ないとオブジェクト自体に影がかかっておかしな結果になる。

次に、試行回数を少なくするためにブラーを掛ける処理を実装。
某書籍では試行回数を256回にしていてブラーは掛けていなかったけど、処理速度的に問題だろう。調べた感じだと、試行回数を減らして代わりにブラーを掛けるのがいいらしい。

ブラーの処理は簡単でSSAOの結果を受け取って、周辺のドットを足して平均を取るだけ。

#define BLUR_SIZE 3
	float w ;
	float h ;
	Tex.GetDimensions( w, h ) ;
	const float2 texel = 1.0f / float2( w, h ) ;
	float result = 0.0f ;
	const float b = float( BLUR_SIZE ) * -0.5f + 0.5f ;
	const float2 bo = float2( b, b ) ;
	for( int i = 0 ; i < BLUR_SIZE ; i++ ) {
		for( int j = 0 ; j < BLUR_SIZE ; j++ ) {
			const float2 offset = ( bo + float2( i, j )) * texel ;
			result += Tex.Sample( Sampler, In.UV + offset ).r ;
		}
	}
	Out.Blur = result / float( BLUR_SIZE * BLUR_SIZE ) ;

定数バッファもなし。
実行時に変更可能でないものはマクロで指定している。
シェーダソースを実行時にコンパイルしているので、上記の場合は「BLUR_SIZE %d」として、展開している。
w,hに関して、SSAOの方では他に渡すものもあって定数バッファで渡しているけど、こっちではテクスチャのGetDimensionsで取得している。
試しにGetDimensionsと固定値の処理時間を測ってみたら、GetDimensionsは452.45us、固定値は398.39usだった。
単位がマイクロ秒なので大したことない気もするけど、1024×1024のピクセルシェーダで約50us余計にかかるみたいなので、固定値で展開するようにしようか。

SSAO+Blur

このSSAOだとあまり違いがわからないけど、3×3のブラーを掛けた結果。

ここまでこの結果を普通のテクスチャに出力していたけど、影と同じでデータは1つで良いのでテクスチャをフォーマットをDXGI_FORMAT_R8_UNORMにした。これでサイズは1/4になる。

で、合成したのがこれ。

SSAO合成結果

あんまり感動がない。
これをやると「質感が上がってスゲー」を期待していたんだけど、処理速度気にしてしょぼいパラメータにしているからか?
処理速度は全体が3.4msで、SSAOが1.98msで全体の60%ぐらい使ってこれだとなくてもいいんじゃないかと思うレベル。

SSAO 半径2 試行回数128

試行回数を128回にして半径も広げた結果、不自然感はあまり変わらない。もっといい感じの場面じゃないとだめなのかもしれない。
ちなみに処理時間はギリギリ60FPSを保って15.5msだったけど、SSAOの処理は14.24ms掛かっていた。


2022年7月27日水曜日

Shader Model 6

そろそろシェーダも複雑になってきた。
cbufferの定義だと全部グローバルになってちゃんと管理しないと名前の被りとか出てきそう。
参考にしていたシェーダプログラムがやっているように、ConstantBuffer<AAA>で定義するように書き換えてみた。

するとコンパイルが通らない。
うすうす知ってたけど、シェーダモデルを5.1に上げる必要がある。
vs_5_0とps_5_0をvs_5_1とps_5_1に変えればいいだけなんだけど、なんにも表示されなくなった。いろいろ試してみたけどわからず。
5.1には下位互換がないのか?なにかが抜けているのか?

シェーダモデルについて調べてみると、今の最新が6.6らしい。
どうせハマるなら5.1とかやってる場合じゃないと思い、6にあげようとするも6からはコンパイラが変わって、d3dcompiler_47.dllからdxcompiler.dllになる。

#include <D3Dcompiler.h>
#pragma comment( lib, "d3dcompiler.lib")
旧インクルードとライブラリ
#include <dxcapi.h>
#pragma comment (lib, "dxcompiler.lib")
新インクルードとライブラリ

5.1まではD3DCompile関数でコンパイル出来た。
ただ、この関数受け付ける文字列がsjisで、若干使いづらい。

6.0からはグローバル関数ではなく、COMオブジェクトに変わる。

	tCom<IDxcLibrary> oLibrary ;
	::DxcCreateInstance( CLSID_DxcLibrary, IID_PPV_ARGS( &oLibrary )) ;
	tCom<IDxcCompiler2> oCompiler ;
	::DxcCreateInstance( CLSID_DxcCompiler, IID_PPV_ARGS( &oCompiler )) ;

	tCom<IDxcBlobEncoding> oSource ;
	oLibrary->CreateBlobWithEncodingFromPinned( sSrc.Ptr(), sSrc.Len(), CP_UTF8, &oSource ) ;

#if defined(_DEBUG)
	cWS pArgs[] = { L"-Ges", L"-Zi", L"-Od" } ;
#else
	cWS pArgs[] = { L"-Ges", L"-O3" } ;
#endif
	tCom<IDxcOperationResult> oResult ;
	oCompiler->Compile( oSource.Get(), sName, sFunc, sModel, pArgs, fArray( pArgs ), nullptr, 0, nullptr, &oResult ) ;

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

	tCom<IDxcBlob> oVS ;
	oResult->GetResult( oVS.ReleaseAndGetAddressOf()) ;

IDxcLibraryとIDxcCompiler2を予め用意しておいて、コンパイル時にIDxcLibrary::CreateBlobWithEncodingFromPinnedを呼び出す。
ここに渡せる文字列がコードページ指定できるけど、UTF16の渡し方がわからず。
見つけたサンプルがCP_UTF8を渡していたのでUTF-8で渡すことにした。

コンパイルはIDxcCompiler::Compile関数を呼び出す。
ここで渡す文字列は全部UTF16でそのまま渡せるようになった。
最初の引数はCreateBlobWithEncodingFromPinnedの結果をそのまま渡す。
次がソース名。エラー表示とかで役立どのソースかの識別に役立つ。
次がエントリポイントでシェーダの関数名を渡す。
その次がシェーダモデル。vs_6_6とps_6_6を渡す。
その次がコンパイルスイッチ。今までフラグで渡していたけど文字列で渡す形式にかわった。いままで渡していたものと同じようにしてみた。
あとはマクロと、インクルードなのでnullにしておいた。

実行後、IDxcOperationResultが返って、IDxcOperationResult::GetStatusを呼び出して結果を確認する。

エラーの場合はIDxcOperationResult::GetErrorBufferでエラー情報を取り出し、GetBlobAsUtf16でUTF16に変換してIDxcBlobEncoding::GetBufferPointerでエラーの文字列が取り出せる。
今までのエラー情報は、行番号とカラム位置だけだったけど、今度のはソースと一緒にエラー箇所が表示されるのですごく直しやすい。

うまく行った場合はIDxcOperationResult::GetResultでIDxcBlobを取得する。

	D3D12_GRAPHICS_PIPELINE_STATE_DESC	oGPSD = {} ;
	D3D12_SHADER_BYTECODE & oSB = oGPSD.VS ;
	oSB.pShaderBytecode = oVS->GetBufferPointer() ;
	oSB.BytecodeLength = oVS->GetBufferSize() ;

これが今までのBlobと同じで、バイトコードに設定する関数も同じ。

コンパイル部分の修正が終わって、いざ実行してみると起動しない。
dxcompiler.dllがないからだ。
正規の方法はどうなるかわからないが嫌な予感がしたのはgithubでこのコンパイラのソースが公開されているので、ここのバイナリかソースを自分でコンパイルするのか?
とりあえず動かしたいので自分のPCを探すとたくさん見つかった。
PIXのが新しそうだったのでそれをコピーした。

改めて実行してみるとシェーダのコンパイルエラーが発生した。
ネットで検索してみると、開発者モードでないとだめと書いてあったけどすでに他の件で開発者モードには設定済み。オプションに-Vdを指定すればいいとか、デバイスを作る前にD3D12ExperimentalShaderModels機能を有効にするとかあったけどこれもだめだった。
コンパイル時に署名をつけるらしく、dxil.dllというのも必要みたい。
これもPIXの同じフォルダにあったのでコピーした。

これでやっと動くようになったと思いきや、冒頭の状態に戻った。
何も表示されない状態。
PIXでデバッグしてみると、メッシュのボーン情報がシェーダに渡ってない。
定数バッファの書き換えの時に管理名を変えたんだけど、描画時に指定している名称を変え忘れていた。
5.1に変えたからとか全く関係なかったけど、5から6へ移行するための契機としていい不具合だった。

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%ぐらいに下がっていた。

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

参考記事

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