2023年9月24日日曜日

Particle4 オーラ

前回に引き続きパーティクルの拡張をする。

今回は、円柱のメッシュを追加する。


円柱メッシュ


円盤と違って、円柱の場合は特に悩むことなくUV展開は出来そう。
矩形を円の分割数分つなげるだけ。

円柱

特に悩むことなくあっさり出来た。


上を広くした円柱

上を狭くした円柱

パラメータを追加して、上の半径を調整できるようにした。


オーラっぽい効果


魔法陣+オーラ

2つの円柱を、前回作った魔法陣に重ねてみた。


シェーダ

float2 uv = In.UV + float2( -0.7 * In.UV.y, 0.0 ) ;	// ひねり
Out.Col = Tex.Sample( Sampler, In.UV + float2( 0.0, Param.Time )) ;
float mask = ( sin( ( uv.x + Param.Time*0.5) * 8 * 3.1419 )+1 + cos( ( uv.x + -Param.Time) * 2 * 3.1419 )+1  ) * In.UV.y ;
Out.Col = pow( Out.Col, 1.25 ) * mask ;
Out.Col *= In.Col ;
Out.Emissive = Out.Col ;



2023年9月23日土曜日

Particle3 魔法陣

前回に引き続きパーティクルの拡張をする。

今回は魔法陣の演出をやってみたい。


円盤メッシュ


まずパーティクル用メッシュに用意したのは矩形メッシュ。
今回はこれに加えて、円盤のメッシュを追加しようと思う。
ただ、今まで円盤のメッシュを扱ったことがない。
やりたいことは、テクスチャの画像が円に沿ってぐるっと張り付いて、UVのU方向のスクロールが回転、V方向のスクロールが中心に吸い込まれていく感じに動いてほしい。

円盤のUVの展開について色々調べたけど全然見つからなかった。
仕方がないので自分で考えた方法がこれ。

UV展開図


円の分割数を指定すると各頂点の位置を計算する。
x = cos( i ÷ 分割数 ✕ 2π ) ※ i は0~分割数までのループカウンタ
y = sin( i ÷ 分割数 ✕ 2π )
u = i  ÷ 分割数

この図の場合、分割数3で中心に点0、UVは点0が一番上の中心。
そこから半径 (x,y) ✕ 0.5の位置に点2、(x,y) ✕ 約10%の位置に1の点を置く。
Vは点2が1、点1が上から約10%の位置。
後は分割数分ループする。


謎のエネルギー


とりあえずはメッシュにテクスチャを貼り付けて確認してみる。

セルラーノイズにフラクタルノイズを掛けたもの

まずノイズテクスチャを用意する。


円盤メッシュにテクスチャ展開

メッシュに貼り付けた結果。
なんかうまく行ってるっぽい。

マスクイメージ

上記にこのマスクを掛けて


ひねりを加えるとこうなって


色をつけたらこうなる。


最後にUVスクロールでvのマイナス方向に動かすとこんな感じになった。

シェーダ


float2 uv = In.UV + float2( -0.7 * In.UV.y, 0.0 ) ;	// ひねり
Out.Col = Tex.Sample( Sampler, uv + float2( 0.0, -Param.Time )) ;	// マイナス:外側 / プラス:内側
float mask = ( 1 - cos( In.UV.y*2*3.14159265 )) * 0.5 ;
Out.Col = pow( Out.Col, 1.25 ) * mask ;
Out.Col *= In.Col ;
Out.Emissive = Out.Col ;




魔法陣


魔法陣テクスチャ

絵かきツールで適当に画像を用意して、背景をアルファ0になるようにして保存。


シェーダ


Out.Col = Tex.Sample( Sampler, In.UV + float2( Param.Time*-(( step( 0.5,In.UV.y ) + step( 0.5,In.UV.y ) - step( 0.75,In.UV.y ))*2-1)*0.1, 0.0f )) ;
Out.Col *= float4( 0.25f, In.Col.gba ) ;
Out.Emissive = Out.Col * 2 + In.Col.r * float4(1,1,1,1) ;

3つの階層に分かれているので、中心を時計回り、真ん中を反時計回りで高速、外側も反時計回りで低速で回すようにしてみた。
あと、出現時にEmissiveを最大にして光らせるようにした。
更新関数から受け取るrの値で制御している。


魔法陣




2023年9月19日火曜日

Particle2 炎

前回パーティクルの仕組みを作ったので今回はその続き

前回からの課題は、パーティクルを追加とともにシェーダ作成も行っていたので、当たり前なんだけどシェーダ作成は切り離すことにした。


パーティクルシェーダ登録


これまでのシェーダは基本的には固定で、決められたデータを追加するとそれに応じて描画されていた。
パーティクル用のシェーダはそれぞれがカスタムシェーダで、パーティクルの種類毎にシェーダが増えていくことになる。
システムから渡される固定のリソースは、カメラ、パーティクルオブジェクトのWorldマトリックス、時間。
これに加えて前回は入れてなかったカラー、ID、インデックスも追加した。

オプションでサンプラ、テクスチャ、ピクセルシェーダソースを指定できるようにした。



炎のエフェクト


パーティクルの仕組みが出来たので、なにか作ってみようと思う。
定番の炎にチャレンジする。

よく見るのが、ノイズのテクスチャを用意してUVスクロールさせるというもの。

まず、セルラーノイズを動的に作成してテクスチャとして登録する。
パーティクルは適当に散らばせて、上昇させる。
表面にノイズテクスチャを貼り付けるとこんな感じ。

シェーダ

Out.Col = Tex.Sample( Sampler, ( In.UV * 0.5f )) ;


テクスチャを貼り付けただけ


全部同じ見た目になってるので、ランダムにテクスチャの表示位置をずらして、タイマーでUVスクロールをする。
distanceを使って、円形にくり抜く。

シェーダ

float d = 1 - distance( float2( 0.5, 0.5 ), In.UV ) ;
float2 r = rand( In.ID * 0.0001 ) ;
Out.Col = Tex.Sample( Sampler, ( In.UV * 0.5f ) + r + float2( 0.0f, Param.Time*0.3 )) ;
Out.Col.a *= step( 0.5, d ) * d ;


丸くなった


登録時に色をランダムに設定する。(赤は192~255、緑は96~159、青は64~95)
また、更新タイミングで全体を減らしていく。
アルファは時間により255~0にする。
Emissiveにも同じように書き込む。

シェーダ

float d = 1 - distance( float2( 0.5, 0.5 ), In.UV ) ;
float2 r = rand( In.ID * 0.0001 ) ;
float c = Tex.Sample( Sampler, In.UV * 0.5f + r + float2( 0.0f, Param.Time*0.3 )).r ;
Out.Col = In.Col * step( 0.5, c ) * c ;
Out.Col.a *= step( 0.5, d ) * d ;
Out.Emissive = Out.Col ;


それっぽくなった


ランダムのパラメータにIDを指定しているが、これをインデックスにすることにより、別のオブジェクトが消えたときにインデックスがずれて、テクスチャの表示する場所がワープすることにより、メラメラ感が増す。

シェーダ

float d = 1 - distance( float2( 0.5, 0.5 ), In.UV ) ;
float2 r = rand( In.Idx * 0.0001 ) ;
float c = Tex.Sample( Sampler, In.UV * 0.5f + r + float2( 0.0f, Param.Time*0.3 )).r ;
Out.Col = In.Col * step( 0.5, c ) * c ;
Out.Col.a *= step( 0.5, d ) * d ;
Out.Emissive = Out.Col ;

メラメラ増し


パフォーマンス


調子に乗って連打しまくっていると、FPSがどんどん落ちて30台まで落ちた。
5000パーティクルぐらいでこれだと、先が辛そう。

1パーティクルに1スレッドをその場で生成してそれぞれ更新処理を任せてみたけど、1スレッドだけで処理したほうが速かった。
パーティクル専用スレッドを1つ用意して、処理対象を2つに分けてメインスレッドと分担するようにしてみたら、多少マシになった。
試しにリリースビルドでやってみたら60FPSから落ちることもなく、シングルスレッドでも60FPS維持してた。速くなることは確認できてるので、パーティクル専用スレッドはそのまま採用することにする。


まとめ


初めてエフェクト作ってみたけど、意外とすごいのが出来た。
エフェクトは1つだけでなく、複数のエフェクトを重ねて1つのエフェクトを作ってるみたいで、例えば今回の炎にしても、別途煙を炎の消えたあたりからモクモク出すといいらしい。


2023年9月14日木曜日

Particle

パーティクルの仕組みを実装してみる。

いろいろ調べていくと既存のツールではやれることがいろいろありすぎて、それと同じものを作ろうと考えるとなかなか進まない。

すでに1つのメッシュに対してオブジェクトを追加すると描画する仕組み自体はあるので、その仕組みにパーティクル要素を追加して肉付けしていくことにした。


パーティクル用メッシュ


色々なメッシュ形状が必要になると思うけど、とりあえず矩形を用意。
パーティクル用のメッシュの頂点データは、x,y,z,u,vのみにした。

後々、円盤とか、円柱とか用意する予定。


パーティクル用シェーダ


とりあえずパーティクル単位でシェーダを作るようにしてみる。

VS

Out.Pos = mul( Cam.TransPV, mul( World[ In.IID ], float4( In.Pos, 1.0f ))) ;
Out.UV = In.UV ;

頂点にはWorld、View、Projectionマトリックスを掛けて、UVはそのままPSに渡す。

PS

Out.Col = float4( 1, 1, 1, 1 ) ;

固定で白を出力。


パーティクル用パラメータ


既存のツールではかなり多くのパラメータが設定できる。汎用ツールなので殆ど使われないものも含んで細かくなってしまっているのだろう。
自分の実装としては、パラメータ化はせず生成と更新の関数を渡せるようにする。
たくさん作っていくうちに傾向、パターン化ができるようになれば毎回関数を作るのではなく、ライブラリ化したものを渡せるようになるはず。


生成関数


毎フレームオブジェクトを生成して100個作ったらやめるようにする。


更新関数


毎フレーム1ずつYを増やしていって、配置された位置から100行ったところで消滅するようにする。


実行結果


パーティクル自体は、オブジェクトが0になったタイミングで消滅するようにした。


この時点での実行結果

結果を見ると、とりあえず動いているように見えるが、カメラを動かすと背面カリングが効いて消えてしまう。使い方によるが、今回のパターンではビルボードの動きをしてほしい。

以前、草をGSとHSで生やした際、ビルボードについて調べたときにViewの逆行列を利用すれば良いというのを見つけたが結局出来ず、直接計算して頂点を操作した。GSではそれで問題なかったけど、今回の場合はそうは行かないので再度チャレンジしてみる。


VS

Out.Pos = mul( Cam.TransPV, mul( World[ In.IID ], mul( Cam.InverseV, float4( In.Pos, 1.0f )))) ;
Out.UV = In.UV ;

カメラの定数バッファにViewの逆行列(Cam.InverseV)を含めた。その際、移動部分_41~_43を0にする必要がある。用途によっては回転に対しての打ち消しも必要になる。
試行錯誤した結果、上記でうまく行った。

草のときには、カメラの上下には追従してしまうと、草が全部寝てしまうのでY軸を打ち消す様に_22は1、_21、_23は0にしてたことになる。
ただ、パーティクルとして使う場合は不要なので移動のみ打ち消す。


PS

Out.Col = float4( 1, 1, 1, 0.5 ) ;

パーティクルといえばアルファブレンドで加算合成だろということで、アルファ値を0.5に修正。


実行結果


向きはカメラの方を、向くようになった。

ビルボード版

ただ、アルファブレンドがおかしい。白いオブジェクトが連なってるのでずっと白のハズなのに途切れてる部分が見える。
これは深度バッファを利用して物体の前後関係は維持しつつ、パーティクル自体は深度バッファに書き込まないようにしないといけないけど、いつも通り書き込んでしまっているために同じZのオブジェクトが描かれなくなっているせいで起こっている。

機能を拡張して、シェーダを作るときに色々パラメータ指定をできるようにする必要がある。


ラスタライザ


D3D12_GRAPHICS_PIPELINE_STATE_DESC.RasterizerStateの項目でいくつか外部から設定できるようにする。


FillMode


たまにワイヤーフレームで確認したいときがあって、ライブラリを直接変えて実行していたけど、指定できるようにする。デフォルトはD3D12_FILL_MODE_SOLID。


CullMode


これもメッシュが表示されないとき、両面描画に変えたりして確認していたけど、パーティクルやエフェクトの描画の場合種類によって両面描画する必要があったりするので、指定できるようにする。デフォルトはD3D12_CULL_MODE_BACK。


アルファブレンド


アルファブレンドについては、D3D12_GRAPHICS_PIPELINE_STATE_DESC.BlendStateを設定する必要がある。
元々アルファブレンドOn/Off自体はあったんだけど、今回いろんなブレンド方法を追加しておこう。


アルファブレンドなし


D3D12_BLEND_DESC & oBD = oGPSD.BlendState ;
oBD.AlphaToCoverageEnable = FALSE ;
oBD.IndependentBlendEnable = FALSE ;
for( tNum i = 0 ; i < D3D12_SIMULTANEOUS_RENDER_TARGET_COUNT ; i++ ) {
	auto & oRT = oBD.RenderTarget[i] ;
	oRT.BlendEnable = FALSE ;
	oRT.LogicOpEnable = FALSE ;
	oRT.SrcBlend = D3D12_BLEND_ONE ;
	oRT.DestBlend = D3D12_BLEND_ZERO ;
	oRT.BlendOp = D3D12_BLEND_OP_ADD ;
	oRT.SrcBlendAlpha = D3D12_BLEND_ONE ;
	oRT.DestBlendAlpha = D3D12_BLEND_ZERO ;
	oRT.BlendOpAlpha = D3D12_BLEND_OP_ADD ;
	oRT.LogicOp = D3D12_LOGIC_OP_NOOP ;
	oRT.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL ;
}

アルファブレンドあり


switch( eABT ) {
case tAlphaBlendType::Normal :		// 通常ブレンド
	oRT.BlendEnable = TRUE ;
	oRT.SrcBlend = D3D12_BLEND_SRC_ALPHA ;
	oRT.DestBlend = D3D12_BLEND_INV_SRC_ALPHA ;
	break ;
case tAlphaBlendType::Add :			// 加算合成
	oRT.BlendEnable = TRUE ;
	oRT.SrcBlend = D3D12_BLEND_ONE ;
	oRT.DestBlend = D3D12_BLEND_ONE ;
	break ;
case tAlphaBlendType::AddAlpha :	// 加算半透明合成
	oRT.BlendEnable = TRUE ;
	oRT.SrcBlend = D3D12_BLEND_SRC_ALPHA ;
	oRT.DestBlend = D3D12_BLEND_ONE ;
	break ;
case tAlphaBlendType::Sub :			// 減算合成
	oRT.BlendEnable = TRUE ;
	oRT.SrcBlend = D3D12_BLEND_ZERO ;
	oRT.DestBlend = D3D12_BLEND_INV_SRC_COLOR ;
	break ;
case tAlphaBlendType::Mul :			// 乗算合成
	oRT.BlendEnable = TRUE ;
	oRT.SrcBlend = D3D12_BLEND_ZERO ;
	oRT.DestBlend = D3D12_BLEND_SRC_COLOR ;
	break ;
case tAlphaBlendType::Mul2 :		// 乗算合成2
	oRT.BlendEnable = TRUE ;
	oRT.SrcBlend = D3D12_BLEND_DEST_COLOR ;
	oRT.DestBlend = D3D12_BLEND_SRC_COLOR ;
	break ;
case tAlphaBlendType::Screen :		// スクリーン合成
	oRT.BlendEnable = TRUE ;
	oRT.SrcBlend = D3D12_BLEND_INV_DEST_COLOR ;
	oRT.DestBlend = D3D12_BLEND_ONE ;
	break ;
case tAlphaBlendType::Reverse :		// リバース
	oRT.BlendEnable = TRUE ;
	oRT.SrcBlend = D3D12_BLEND_INV_DEST_COLOR ;
	oRT.DestBlend = D3D12_BLEND_INV_SRC_COLOR ;
	break ;
}


深度バッファ


 
深度バッファについては、D3D12_GRAPHICS_PIPELINE_STATE_DESC.DepthStencilStateを設定する必要がある。


DepthWriteMask


深度バッファの書き込みをしない場合は、この値をD3D12_DEPTH_WRITE_MASK_ZEROにする。デフォルトはD3D12_DEPTH_WRITE_MASK_ALL。



シェーダコンパイル


パラメータを指定することで正しい描画になった。
調子に乗ってボタンを押しまくると、若干カクつく。
試しにパーティクル追加の時間を測ってみると、なんと追加に16~17msもかかっている。
シェーダの生成、コンパイル、PSO作成はかなり重い処理ということか。

パーティクルのシェーダはパーティクル追加とは切り離して、事前に行う必要がありそう。



とりあえず完成


PSをちょっと修正して、生成関数で各方向にランダム値を設定。更新関数で指定方向に進むように変更してみた。

それっぽい

一応それっぽいものが出来た。
後はシェーダとパーティクルの紐づけを変更しつつ、描画時に各種リソースを設定する部分ができればいろんなことができるようになる。




2023年9月6日水曜日

Radial Blur

モーションブラーについて調べていたら、このサイトを見つけて放射状ブラーのポストエフェクトの機能追加をしてみることにした。


シェーダ


float2 uv = In.UV - Param.CenterPos ;
float blur = Param.Blur * rcp( Param.Sample - 1 ) ;
float2 ofs = Param.Blur * rand( uv ) * 0.01 ;
float4 color = 0.0 ;
[unroll(16)]
for( uint i = 0 ; i < uint( Param.Sample ) ; i++ ) {
	float scale = Param.Scale + ( i * blur ) + ofs * ( Param.Sample - i ) ;
	color += Src.Sample( Sampler, uv * scale + Param.CenterPos ) ;
}
Out.Col = color / Param.Sample ;

パラメータ


Param.CenterPos


焦点。uv座標で、( 0.5, 0.5 )が中心。


Param.Blur


ぼかし強度。0.1~0.3ぐらいの範囲で使うのがよさそう。


Param.Scale


スケール。1を指定すると元画像そのままで、値を小さくすると大きくなる。
Param.Blurを大きくしていくと、画像の外側の参照が多くなるので、連動してスケールを調整すると、画像が近づくのと、外周の間延び感が消せていい感じになる。


Param.Sample


サンプリング数。8~16ぐらいの範囲で使うのがよさそう。
シェーダも「[unroll(16)]」を指定してるので、それ以上は指定不可。
Param.Blurを大きくすると間が目立つので、連動してサンプリング数も増やすといい。


ofsについて


このサイトでUnityのモーションブラーの実装について解説されており、その中でノイズを入れると少ないサンプル数でもきれいに見えるというふうに書かれていた。
そのアイデアを取り入れて、ランダム値を足すようにしてみた。


出力結果



元の画像

Blur=0.1 Sample=8 ノイズなし

Blur=0.3 Sample=16 ノイズなし

Blur=0.1 Sample=8 ノイズあり

Blur=0.3 Sample=16 ノイズあり

Blur=0.3 Sample=16 ノイズなし

Blur=0.3 Sample=16 ノイズあり

最後の2つを比べると、ノイズなしの方はアウトラインシェーダで書いた枠線が縦に何本もみえるが、ノイズありの方では縦線が目立たなくなっている。