前回実装したGPUパーティクルは、影の描画に対応していませんでした。
今回はUnity5に合わせ、物理ベースレンダリング、影の描画に対応したバージョンを作成したのでご紹介します。
最初は気楽に考えていましたが、ドキュメントがまだ少ないのもあり、実際に作ってみるとピクセルシェーダーによる影の切り抜きや、頂点シェーダからサーフェイスシェーダーへの値の受け渡しなどがかなり大変でした。
▼前回
[Unity]コンピュートシェーダ(GPGPU)で1万個のパーティクルを動かす | notargs.com
動作
いい感じにアニメーションします。
物理ベースシェーダ/影の描画/ポイントライトなどにも対応しています。
ソース
CircleParticle.cs
「擬似インスタンシング」と呼ばれる技術を使っています。
あらかじめ一つのメッシュ内に全パーティクル用のジオメトリを仕込んでおき、UVによってパーティクルIDを表すことで、一回のDrawCallで数千個のパーティクルを描画できます。
パーティクル情報は構造化バッファとして保存しておき、更新、生産にはコンピュートシェーダを活用しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
using UnityEngine; using System.Collections; using System.Collections.Generic; using System.Runtime.InteropServices; public class CircleParticle : MonoBehaviour { // インスタンスごとの情報 struct Instance { Vector3 position; float time; float totalTime; }; static readonly int indicesCount = 6; // 1インスタンスあたりのインデックス数 static readonly int verticesCount = 4; // 1インスタンスあたりの頂点数 static readonly int instanceCount = 65000 / indicesCount; // インスタンスの数 Mesh mesh; // メッシュ Material material; // マテリアル ComputeBuffer computeBuffer; // コンピュートバッファ ComputeShader particleUpdater; // コンピュートシェーダ int emitIDOffset; // 生産するIDのオフセット // 初期化 void Start () { // コンピュートバッファの生産 computeBuffer = new ComputeBuffer(instanceCount, Marshal.SizeOf(typeof(Instance)), ComputeBufferType.Default); // コンピュートシェーダの初期化 particleUpdater = Resources.Load<ComputeShader>("Shaders/Particle/CircleParticleUpdater"); // マテリアルの初期化 material = new Material(Shader.Find("Particles/CircleParticle")); // メッシュ用の配列を初期化 var vertices = new List<Vector3>(instanceCount * verticesCount);//(-1, -1); var uv0 = new List<Vector2>(instanceCount * verticesCount); var uv1 = new List<Vector2>(instanceCount * verticesCount); var indices = new int[instanceCount * indicesCount]; for (int id = 0; id < instanceCount; id++) { // 頂点データ vertices.Add(new Vector2(-1, -1)); vertices.Add(new Vector2( 1, -1)); vertices.Add(new Vector2(-1, 1)); vertices.Add(new Vector2( 1, 1)); // UV0の初期化 uv0.Add(new Vector2(0, 0)); uv0.Add(new Vector2(1, 0)); uv0.Add(new Vector2(0, 1)); uv0.Add(new Vector2(1, 1)); // UV1はバッファアクセス用のID代わりに使う for (int i = 0; i < 4; i++) { uv1.Add(new Vector2(id, 0)); } // インデックスを初期化 indices[id * indicesCount + 0] = id * verticesCount + 0; indices[id * indicesCount + 1] = id * verticesCount + 2; indices[id * indicesCount + 2] = id * verticesCount + 1; indices[id * indicesCount + 3] = id * verticesCount + 1; indices[id * indicesCount + 4] = id * verticesCount + 2; indices[id * indicesCount + 5] = id * verticesCount + 3; } // メッシュを初期化 mesh = new Mesh(); mesh.SetVertices(vertices); mesh.SetUVs(0, uv0); mesh.SetUVs(1, uv1); mesh.SetIndices(indices, MeshTopology.Triangles, 0); mesh.RecalculateBounds(); mesh.RecalculateNormals(); var bounds = mesh.bounds; bounds.min = -new Vector3(100, 100, 100); bounds.max = new Vector3(100, 100, 100); mesh.bounds = bounds; } // 更新処理 void Update () { // パーティクルの追加 particleUpdater.SetFloat("_Time", Time.time); var emitKernel = particleUpdater.FindKernel("Emit"); particleUpdater.SetBuffer(emitKernel, "_Instances", computeBuffer); particleUpdater.SetInt("_InstanceCount", instanceCount); particleUpdater.SetInt("_EmitIDOffset", emitIDOffset); particleUpdater.SetInt("_EmitCount", 10); particleUpdater.Dispatch(emitKernel, instanceCount / 8 + 1, 1, 1); emitIDOffset += 10; // パーティクルの更新 particleUpdater.SetFloat("_DeltaTime", Time.deltaTime); particleUpdater.SetBuffer(particleUpdater.FindKernel("Update"), "_Instances", computeBuffer); particleUpdater.Dispatch(particleUpdater.FindKernel("Update"), instanceCount / 8 + 1, 1, 1); // メッシュのレンダリング material.SetBuffer("_Instances", computeBuffer); Graphics.DrawMesh(mesh, Matrix4x4.TRS(transform.position, transform.rotation, transform.lossyScale), material, 0); } /// <summary> /// 破棄 /// </summary> public void OnDestroy() { computeBuffer.Release(); } } |
CircleParticle.shader
どうしてもサーフェイスシェーダを使いたかったのですが、頂点シェーダからサーフェイスシェーダへIDを受け渡す術が見つからなかったためuv_MainTex.xの整数部をID、小数部をUV座標とみなしてIDの受け渡しを行っています。
ここについては今回のコードの中でも最上級のクソースだと思っているので、何か良い方法があったら教えて下さい。
また、ShadowCasterパスを自前で定義し、影についてもサーフェイスシェーダー同様にクリッピングを行っています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
Shader "Particles/CircleParticle" { Properties { _MainTex("MainTex", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } Cull Off LOD 200 //============================ // ポリゴンを描画するPass CGPROGRAM #pragma surface surf Standard fullforwardshadows vertex:vert #pragma target 3.0 // インスタンスごとの情報 struct CircleParticleInstance { float3 position; float time; float totalTime; }; // 入力 struct Input { float2 uv_MainTex; }; // 構造化バッファ #ifdef SHADER_API_D3D11 StructuredBuffer<CircleParticleInstance> _Instances; #endif // 頂点シェーダ void vert(inout appdata_full v) { v.vertex.xyz += _Instances[(int)v.texcoord1].position; // 整数部をID、小数部をUV座標として保持 v.texcoord.x += (int)v.texcoord1; } // サーフェイスシェーダ void surf (Input IN, inout SurfaceOutputStandard o) { int id = (int)IN.uv_MainTex.x; CircleParticleInstance instance; #ifdef SHADER_API_D3D11 instance = _Instances[id]; #endif float2 uv = fmod(IN.uv_MainTex, 1); clip(pow(instance.time / instance.totalTime, 0.5f) * 0.5f - length(uv - 0.5)); clip(length(uv - 0.5) - pow(instance.time / instance.totalTime, 2) * 0.5f); o.Albedo = float3(1, 1, 1); o.Metallic = 0; o.Smoothness = 0; o.Alpha = 1; } ENDCG //============================ // 影を描画するPass Pass{ Name "ShadowCaster" Tags{ "LightMode" = "ShadowCaster" } ZWrite On ZTest LEqual Cull Off CGPROGRAM #pragma exclude_renderers gles #pragma multi_compile_shadowcaster #pragma target 3.0 #pragma vertex vert #pragma fragment frag #include "UnityStandardShadow.cginc" // インスタンスごとの情報 struct CircleParticleInstance { float3 position; float time; float totalTime; }; // 構造化バッファ #ifdef SHADER_API_D3D11 StructuredBuffer<CircleParticleInstance> _Instances; #endif // 頂点シェーダ出力構造体 struct VertexOutput { V2F_SHADOW_CASTER_NOPOS float2 tex : TEXCOORD1; float2 tex1 : TEXCOORD2; float id : TEXCOORD3; float4 pos : TEXCOORD4; }; // 頂点シェーダ入力構造体 struct VertexIn { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv0 : TEXCOORD0; float2 id : TEXCOORD1; }; // 頂点シェーダ void vert(VertexIn v, out VertexOutput o, out float4 opos : SV_POSITION) { v.vertex.xyz += _Instances[(int)v.id].position; TRANSFER_SHADOW_CASTER_NOPOS(o, opos) o.tex = TRANSFORM_TEX(v.uv0, _MainTex); o.tex1 = v.uv0; o.id = v.id; o.pos = opos; } // フラグメントシェーダ half4 frag( VertexOutput i ) : SV_Target { CircleParticleInstance instance = _Instances[(int)i.id]; float2 uv = i.tex1; clip(pow(instance.time / instance.totalTime, 0.5f) * 0.5 - length(uv - 0.5)); clip(length(uv - 0.5) - pow(instance.time / instance.totalTime, 2) * 0.5f); SHADOW_CASTER_FRAGMENT(i) } ENDCG } } FallBack "Diffuse" } |
Resources/Shaders/Particle/CircleParticleUpdater.compute
パーティクルの更新処理と生産処理です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
#pragma kernel Emit #pragma kernel Update float _Time; // 現在時間 float _DeltaTime; // 経過時間 int _InstanceCount; // インスタンス数 int _EmitIDOffset; // 追加するIDのオフセット int _EmitCount; // 追加する数 // インスタンスごとの情報 struct CircleParticleInstance { float3 position; float time; float totalTime; }; // インスタンスバッファ RWStructuredBuffer<CircleParticleInstance> _Instances; // ランダム関数 float rand(float2 co){ return frac(sin(dot(co.xy, float2(12.9898,78.233))) * 43758.5453); } // パーティクルを生産する [numthreads(8, 1, 1)] void Emit (uint3 id : SV_DispatchThreadID) { if (fmod((float)id + _EmitIDOffset, _InstanceCount) > _EmitCount) return; CircleParticleInstance instance; instance.position = (float3(rand(float2(id.x, _Time + 0)), rand(float2(id.x, _Time + 1)), rand(float2(id.x, _Time + 2))) - 0.5f) * 100; instance.time = 0; instance.totalTime = 2; _Instances[id.x] = instance; } // パーティクルを更新する [numthreads(8, 1, 1)] void Update (uint3 id : SV_DispatchThreadID) { CircleParticleInstance instance = _Instances[id.x]; instance.time += _DeltaTime; _Instances[id.x] = instance; } |
コメントを残す