https://dl.dropboxusercontent.com/u/18978109/ParticleTest/ParticleTest.html
ずいぶんと更新の間が開いてしまい お待ちいただいた方には申し訳ないです。
今回はDX11を使用したパーティクル描画のサンプルを記事にしてみました。
最近話題の?GPGPUコンピューティング まだ見ぬPS4やらXBOXOneといった次世代機などで主力になるであろうGPUを使用した計算ですね。
GPGPU (汎用目的のGPU計算処理)はシェーダープログラムを使用した 描画を含む多用途な計算処理のことです。
GPUの浮動点少数計算に強い点やマルチスレッド対応などの利点を生かしてCPUの処理負荷を軽減することで
実行速度を向上させることが主眼です。
いまのところDX11をサポートした家庭用PCが市場に広く行き渡るのは2年弱かなあと見積もっているのですが、 開発期間を考えると
そろそろ勉強しておかないと置いてけぼりになってしまいそうなので、これを機会に勉強してみてはいかがでしょう。
今回のサンプルの実行数値はノートPCで (Corei7-2630(2Ghz) GeforceGT540M)
パーティクル数70万で 70FPS前後でした 直接VRAMにデータを書き込むのでドローコールは1です。
スペックは環境によってかなり差が出ると思いますので、それぞれの環境に合わせて最適値を設定してください。
記事内リンク先にコンピュートシェーダを使用したパーティクルレンダリングのサンプルと 実行処理速度の比較表が掲載されていますが、コンピュートシェーダを使用すればなんでも高速に処理できるかというとそういうわけでもなく コンピュートシェーダに向いた処理やデータ構造を工夫することで処理速度の向上が期待できるということみたいですね
GDCなどで発表されている内容では 流体計算、物理演算、スキンシェーディング、ポストエフェクト、AI など
描画処理の高速化以外にもコンピュートシェーダを使用した様々な用途がありそうです。
ちなみにコンピュートシェーダもGPUのシェーダなのでシェーダ用関数しか用意されていません。 必要な関数は自作するかライブラリを流用する必要があります。
DirectX11未サポートのGPUでシェーダに計算させたい場合はコンピュートシェーダの計算部分をレンダリング用のシェーダに移植して バッファメモリの代わりにテクスチャをメモリ代わりに使うことでGPUで計算させることができます。この場合ベクトルxyzwを カラーrgba と置き換えて計算させてください。 マルチスレッド計算の恩恵は減ってしまいますがGPUでの計算は十分速いのでCPU計算で実行速度のネックになりそうな負荷の軽減が期待できます。
今後の展望としては ライフ、コリジョン、重力、フォース、ノイズ(カールノイズ、SPH、FFT など)のパラメータを実装していくことで パーティクルシステムとして実用的なものになるのではないかと思います。
ということでそれぞれ工夫してみてくださいな ・・・という投げっぱなし(笑
■今回参考にしたリンク
Unity 4.0: Compute Shaderを使ってみる
Compute Shader Overview (Windows)
ほかにも参考にしたところがあったのですが いつの間にかのリンク切れで追跡できませんでした。
なので不明な点があればほかの詳しいサイトさんを探すか この記事のコメント欄に質問を投げてください。
※スクリプト名が違っていますが インスペクターへの設置はこの画像を参考にしてください。
■レンダリングシェーダ
|
[maxvertexcount(4)] // ジオメトリシェーダで使用する頂点数を指定 > 今回は4頂点なので4を指定 // パーティクル座標データをMVP(ビュー座標)に変換
output.pos = corLoc + float4(-dx, dy,0,0); output.uv=float2(0,0); outStream.Append (output); // output.pos =corLoc(パーティクルシステム中心座標) + float4(パーティクル頂点 左上); // output.uv = float2(UV座標 左上); // output.col = input[0].col;(頂点カラー 左上); // outStream.Append (output); //ストリームに1頂点分のデータを出力
//以下 右上、左下、右下の順に ストリームに出力 …………………………………
outStream.RestartStrip(); // ビルボードメッシュの描画
|
レンダリング用のシェーダは通常のシェーダと変わりませんが高速描画のために変更点があります
●ジオメトリシェーダーの使用
●StructuredBufferの使用
ジオメトリシェーダはシェーダを使用してGPU内でメッシュを描画するためのシェーダです。
大量のメッシュの頂点データをGPU側に送るのは処理効率が悪いため パーティクルの座標データ(センター座標) やカラーデータなどの最小限のデータをアップデートしてジオメトリシェーダでメッシュ化することで処理速度の向上をすることが期待出来ます。 ちなみにジオメトリシェーダで一度に描画できる頂点数は256です。
●さらにStructuredBuffer(シェーダモデル5)読み書きが可能なバッファ。を 使用することでCPU−GPU間でのデータやりとりする時スクリプトから高速にアクセスできます。
シェーダーモデル4にも cbuffer(コンスタントバッファ)、tbuffer(テクスチャバッファ)という似たようなバッファが
サポートされていたのですが シェーダモデル5からStructuredBufferでそれらをまとめて扱うことができるようになったようです。
構造体でも定数でもいけるので今後はコレを使ってということなのかな?
※ 今回のサンプルではカメラ方向にビルボードのフェース面を向ける処理を省略してあります
■コンピュートシェーダ
RWStructuredBuffer
スレッドは今回の例だと 1次元配列を使用するので スレッドID はuint3型で ID.xyzのうちの1次元の要素ID.xで指定します
|
#pragma kernel CSMain RWStructuredBuffer[numthreads(10,1,1)] <スレッドIDは 1次元配列を使用する場合 id.xyz のうち id.x で指定する 例: posBuffer[id.x]> |
実行カーネルの宣言 #pragma kernel CSMain
スクリプトからコンピュートシェーダのカーネルに実行を指定する時に名前を指定します
RWStructuredBuffer
構造体が使用可能な読み書き可能なバッファー
リファレンスによると register ()は省略可能という解説がされていたのですが ちょっと正確にはわかりません
[numthreads(10,1,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
numthreads が Compute Shader 1回分のスレッドの意味です。
この例だと10*1*1 =10スレッド 数値は自由に割り付けられますが計算要素のスタイルに応じて
1次元から3次元までのIDにアクセスしやすいサイズを割り付けるとアクセスが便利です。
今回の場合はわかりやすく1次元配列にしてあります。
最大スレッド数はシェーダバージョン4.0で786、シェーダバージョン5.0で1024となっています。
シェーダバージョン4.0ではzが1しかとれない点に気をつけてください。
| Compute Shader | Maximum Z | Maximum Threads (X*Y*Z) |
| cs_4_x | 1 | 768 |
| cs_5_0 | 64 | 1024 |
※スレッド最大数はあくまで目安のようで 環境によっては動作しない事があるようです。 GPUの性能なのかメーカーによる差異なのか原因が特定できていませんが、動作が安定しない場合はスレッドの数を少なめに設定することで改善する場合があるようです。
void CSMain (uint3 id : SV_DispatchThreadID)
ほかにもスレッドの引数は
SV_GroupIndex, SV_DispatchThreadID, SV_GroupThreadID, SV_GroupID
などの値を設定出来ます。
それぞれの値の関係は以下のとおり

| SV_GroupThreadID | SV_GroupID | SV_DispatchThreadID | SV_GroupIndex |
| x, y, z | x', y', z' | x'*X+x, y'*Y+y, z'*Y+z | z*X*Y+y*X+x |
DX11プログラムの詳細は記事内リンク先を参照してください
スクリプト部
カメラにセットして使用するスクリプトの場合 基本はイメージエフェクトのスクリプトと同じスタイルになります。
●Start( )メソッド内で
| particleBuffer = new ComputeBuffer(particleCount, 12); particleBuffer.SetData(particleLocations); |
ComputeBufferの初期化を行います
ComputeBuffer (配列全体の数 : int, 配列個々のサイズ : int, type : ComputeBufferType)
今回は使用していませんが3番目の引数はバッファタイプを指定できます。
SetData()を使用して ComputeBufferにスクリプト側の初期化した配列をセットします。
●Update( )メソッド内で
| cs.SetBuffer (cs.FindKernel ("CSMain"), "PosBuffer", ParticleBuffer); |
スクリプト中のcsはComputeShaderを設定する際に自由に名前を割り振れますので
public ComputeShader cs;
と宣言してスクリプトが冗長にならないような名前をつけます
cs.SetBuffer (cs.FindKernel ("CSMain"), "PosBuffer", ParticleBuffer);
FindKernel ("CSMain") はComputeShader内で”#pragma kernel CSMain”として宣言した名前のカーネルを
探してComputeShaderのバッファにスクリプトで設定したバッファをセットします。
すべてのデータをセットし終えたらDispatch( ) を使用してスレッド計算を開始させます。
void Dispatch( UINT x , UINT y , UINT z );
引数はuint(int)型で ComputeShaderで計算させるスレッドの次数にあわせてください
●OnRenderImage()メソッド内で
| mat.SetBuffer("particleBuffer", particleBuffer); Graphics.DrawProcedural (MeshTopology.Triangles, particleCount); |
matはレンダリングに使用するシェーダを示します ComputeShaderで計算された結果を
レンダリング用のシェーダにセットします。
DrawProcedural ( MeshTopology.Type , particleCount );
MeshTopology.Type には描画スタイルを設定します Triangles | Quads | Lines | LineStrip | Points
などの種類がありますが、 今回はシェーダ側でジオメトリシェーダを使用してメッシュ描画するため
描画する回数(particleCount = パーティクル数)だけ合っていれば何を指定してもよいです。
DrawProcedural ( )命令は 頂点データをGPUのに転送せずに直接VRAMに描画する命令です 類似の命令
DrawProceduralIndirect( )との違いは
DrawProceduralIndirect( )はGPU側からのコールバックが無い点です。(つまりデータの再利用不可)
■コンピュートシェーダ使用時のデータの流れを以下の図に示します
■ “ParticleController_Sample.cs
using UnityEngine;
using System.Collections;
using System.Threading;
public class ParticleController_Sample : MonoBehaviour {
public Texture2D particleTexture;
public Shader shader;
private Material mat;
public float PScale = 1.0f;
public ComputeShader cs;
public int particleCount = 1000;
public Transform OriginLocation;
private Vector3[] particleLocations;
private Vector3[] particleColors;
private Vector3[] particleVelocities;
private ComputeBuffer particleBuffer;
private ComputeBuffer particleColorBuffer;
private ComputeBuffer velocityBuffer;
Thread csThread;
void Start()
{
particleLocations = new Vector3[particleCount];
particleColors = new Vector3[particleCount];
particleVelocities = new Vector3[particleCount];
for (int i = 0; i < particleCount; i++)
{
particleLocations[i] = OriginLocation.position + Random.insideUnitSphere;
particleVelocities[i] = Random.insideUnitSphere;
particleColors[i] = new Vector3(Random.value,Random.value,Random.value);
}
particleBuffer = new ComputeBuffer(particleCount, 12);
particleBuffer.SetData(particleLocations);
velocityBuffer= new ComputeBuffer(particleCount, 12);
velocityBuffer.SetData (particleVelocities);
particleColorBuffer = new ComputeBuffer(particleCount, 12);
particleColorBuffer.SetData (particleColors);
mat = new Material(shader);
mat.hideFlags = HideFlags.HideAndDontSave;
mat.SetTexture ("_Sprite", particleTexture);
mat.SetFloat ("Size", PScale );
}
private void ReleaseResources()
{
csThread.Abort ();
particleBuffer.Release ();
particleColorBuffer.Release ();
velocityBuffer.Release ();
Object.DestroyImmediate (mat);
}
void OnDisable()
{
ReleaseResources ();
}
void Update()
{
cs.SetFloat ("Time",Time.timeSinceLevelLoad);
cs.SetVector ("Origin", OriginLocation.position);
cs.SetBuffer (cs.FindKernel ("CSMain"), "colBuffer", particleColorBuffer);
cs.SetBuffer (cs.FindKernel ("CSMain"), "posBuffer", particleBuffer);
cs.SetBuffer (cs.FindKernel ("CSMain"), "velBuffer", velocityBuffer);
cs.Dispatch (cs.FindKernel ("CSMain"), particleCount, 1, 1);
}
void OnRenderImage(RenderTexture src, RenderTexture dst)
{
Graphics.Blit( src, dst);
mat.SetBuffer("particleBuffer", particleBuffer);
mat.SetBuffer("particleColor", particleColorBuffer);
mat.SetPass (0);
Graphics.DrawProcedural(MeshTopology.Triangles, particleCount);
}
}
■ ”compute_sample.compute”
#pragma kernel CSMain
RWStructuredBufferposBuffer : register(u0);
RWStructuredBuffervelBuffer : register(u1);
RWStructuredBuffercolBuffer : register(u2);
float Time;
float4 Origin;
float PI = 3.1415926;
[numthreads(16,1,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
uint Tid = id.x;
colBuffer[Tid ].r = 0.1;
colBuffer[Tid ].g =1.0;
colBuffer[Tid ].b = 0.4;float3 NewPos;
NewPos.x = Origin.x + (sin(Time/10.0 * Tid ) ) * 30;
NewPos.y = Origin.y + (cos((Time/10.0+PI ) * Tid ) ) *30;
NewPos.z = Origin.z + (cos(Time /10.0 * Tid ) )*30;
velBuffer[Tid ].x = NewPos.x - posBuffer[Tid ].x;
velBuffer[Tid ].y = NewPos.y - posBuffer[Tid ].y;
velBuffer[Tid ].z = NewPos.z - posBuffer[Tid ].z;
posBuffer[Tid ].x += velBuffer[Tid ].x / 40;
posBuffer[Tid ].y += velBuffer[Tid ].y / 40;
posBuffer[Tid ].z += velBuffer[Tid ].z / 40;
}
■ “RenderParticles.shader”
Shader "Custom/RenderParticles" {
Properties {
_Sprite ("Sprite", 2D) = "white" {}
}
SubShader {
Pass{
ZWrite Off ZTest Always Cull Off Fog { Mode Off }
Blend SrcAlpha One
CGPROGRAM
#pragma target 5.0
#pragma vertex vert
#pragma geometry geom
#pragma fragment frag
#include "UnityCG.cginc"
StructuredBufferparticleBuffer;
StructuredBufferparticleColor;
float Size = 0.3f;
sampler2D _Sprite;
struct vs_out {
float4 pos : SV_POSITION;
float4 col : COLOR;
};
vs_out vert (uint id : SV_VertexID)
{
vs_out o;
o.pos =mul(_Object2World, float4(particleBuffer[id], 1.0f));
o.col = float4(particleColor[id], 1.0f);
return o;
}
struct gs_out {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float4 col : COLOR;
};
[maxvertexcount(4)]
void geom (point vs_out input[1], inout TriangleStreamoutStream)
{
float dx = Size;
float dy = Size * _ScreenParams.x / _ScreenParams.y;
gs_out output;
float4 corLoc = mul(UNITY_MATRIX_MVP, input[0].pos);output.pos = corLoc + float4(-dx, dy,0,0); output.uv=float2(0,0);
output.col = input[0].col; outStream.Append (output);
output.pos = corLoc + float4( dx, dy,0,0); output.uv=float2(1,0);
output.col = input[0].col; outStream.Append (output);
output.pos = corLoc + float4(-dx,-dy,0,0); output.uv=float2(0,1);
output.col = input[0].col; outStream.Append (output);
output.pos = corLoc + float4( dx,-dy,0,0); output.uv=float2(1,1);
output.col = input[0].col; outStream.Append (output);
outStream.RestartStrip();
}
fixed4 frag (gs_out i ) : COLOR0
{
fixed4 col = tex2D(_Sprite, i.uv);
col.a = 0.8f;
col *= i.col;
return col;
}
ENDCG
}
}
Fallback Off
}


