【Unity2018】LWRP対応のGPU Instancingパーティクルを作る
概要
Unity2018
にて、ScriptableRenderPipeline(SRP)
を活用して作られた、Standard Shader
より軽量なLightWeightRenderPipeline(LWRP)
が追加されました
Surface Shader
が使えないため少し面倒ですが、内部のコードを引っ張ってきて拡張することで、LWRPでもGPUパーティクルなどのシェーダーを活用したテクニックを使うことができます
また、Standard Shader
と違って中のコードがすべて読めるようになっているため、LWRPの限界にぶち当たったときも、LWRPをベースとして独自のSRPを構築していけます
今回は、LWRPのLightweightStandard
シェーダーを拡張して、GPUインスタンシングを使ったパーティクルを実装してみました
全てGPUで行った方が処理は早くなるのですが、今回はシンプルに、位置計算はCPUで行い、描画のみをGPUで行う一般的なパーティクルの実装をやっていきます
内部のコードを理解する
中を読んでみる
Packages/Lightweight Render Pipeline/LWRP/Shaders/LightweightStandard
を追うことで、シェーダーの大まかな流れを掴めます
中を見ると、大きく4つのパスでStandardシェーダーが構築されていることが分かります
StandardLit Pass
ここが物体の色やライティングを決めるメインとなるパスです
ちょっと長いですが、LightweightPassLit.hlsl
で定義されているLitPassVertex
とLitPassFragment
を呼び出していることが分かります
実際にPackages/Lightweight Render Pipeline/LWRP/ShaderLibrary/LightweightPassLit
を覗いてみると、内部では割と普通の頂点シェーダーとフラグメントシェーダーが書かれていることが分かります
ShadowCaster Pass
影を描画するために、ライト視点での深度をShadowMapとして描きこむパスです
色情報などはいらないため、StandardLitよりかなりシンプルな頂点シェーダー/フラグメントシェーダーが使われています
DepthOnly Pass
あらかじめ深度を計算しておくことで不要なピクセルのライティングが行われないようにするほか、影を落とす処理などで使うための深度バッファを予め生成するためのパスです
こちらも深度バッファを描くだけのパスなのでコードはシンプルです
Meta Pass
LightMap用の色情報を出力するパスです パーティクルをLightMapに焼き込むことも無いと思うので、今回は省略します
GPUパーティクルを作る
DrawMeshInstancedIndirect
DrawMeshInstancedIndirectを使うと、InstanceIDのみを使って、大量のオブジェクトを一気に描画できます 予めComputeBufferとして位置情報などを渡しておくことで、InstanceIDごとに違う位置、違う色で描画することが出来ます。
DrawMeshInstancedIndirectの使い方はこちらの公式マニュアル(https://docs.unity3d.com/ScriptReference/Graphics.DrawMeshInstancedIndirect.html)に大体書いてあります
ArgsBufferクラス
DrawMeshInstancedIndirect
を使う時に要求されるバッファのWrapperを用意しました
メッシュの頂点数、インデックス数やインスタンス数などを設定してGPUへ渡します
using UnityEngine; namespace Sandbox { public class ArgsBuffer { readonly uint[] args = new uint[] {0, 0, 0, 0, 0}; readonly ComputeBuffer buffer = new ComputeBuffer(1, sizeof(uint) * 5 , ComputeBufferType.IndirectArguments); public ComputeBuffer Buffer => buffer; public void SetData(Mesh mesh, int subMeshIndex, uint instanceCount) { args[0] = mesh.GetIndexCount(subMeshIndex); args[1] = instanceCount; args[2] = mesh.GetIndexStart(subMeshIndex); args[3] = mesh.GetBaseVertex(subMeshIndex); buffer.SetData(args); } } }
Particleクラス
パーティクルの情報を保持する構造体です
今回は位置計算などもCPUで行うため、更新処理などもこの中に書いてしまいます
using System; using UnityEngine; using Random = UnityEngine.Random; namespace Sandbox { [Serializable] public struct Particle { Vector3 position; Vector3 velocity; Vector3 axis; float angle; float lifeTime; float time; float size; Vector3 albedo; Vector3 emission; public void Update(float deltaTime) { position += velocity * deltaTime; angle += deltaTime; time += deltaTime; size = (1 - Mathf.Pow(time / lifeTime * 2 - 1, 2.0f)) * 0.2f; if (time > lifeTime) { this = Create(); } } public static Particle Create() { var color = new Vector3(Random.Range(0.0f, 1.0f), Random.Range(0.0f, 1.0f), Random.Range(0.0f, 1.0f)); var emission = Random.Range(0, 2) < 1 ? color * 4 : Vector3.zero; return new Particle { position = Vector3.zero, velocity = Random.rotation * Vector3.forward * 2.0f, axis = Random.rotation * Vector3.forward, angle = Random.Range(0, Mathf.PI * 2), lifeTime = Random.Range(0.2f, 0.7f), albedo = color, emission = emission }; } } }
Particles クラス
パーティクルのライフサイクルの管理や描画を行うクラスです
UniRxとunsafeに依存してますが、ここは状況に合わせてよしなに書き換えて下さい
using UniRx; using UniRx.Triggers; using UnityEngine; namespace Sandbox { public class Particles : MonoBehaviour { [SerializeField] Mesh mesh; [SerializeField] Material material; unsafe void Start() { const int instanceCount = 100; var bounds = new Bounds(Vector3.zero, Vector3.one * 1000); var argsBuffer = new ArgsBuffer(); argsBuffer.SetData(mesh, 0, instanceCount); var particleBuffer = new ComputeBuffer(instanceCount, sizeof(Particle)); var particles = new Particle[instanceCount]; for (var i = 0; i < instanceCount; ++i) { particles[i] = Particle.Create(); } this.UpdateAsObservable().Subscribe(_ => { for (var i = 0; i < instanceCount; ++i) { particles[i].Update(Time.deltaTime); } particleBuffer.SetData(particles); material.SetBuffer("_Particles", particleBuffer); Graphics.DrawMeshInstancedIndirect(mesh, 0, material, bounds, argsBuffer.Buffer); }); } } }
Quaternion.hlsl
こちらのコードを適当に突っ込んで下さい https://gist.github.com/mattatz/40a91588d5fb38240403f198a938a593
Particles.shader
少し長いですが、基本的にはLightweightStandard.shader
をコピーしてきて、書き換えたい箇所だけ独自の関数に置き換えているだけです
Shader "LightweightPipeline/Particles (Physically Based)" { Properties { // Specular vs Metallic workflow [HideInInspector] _WorkflowMode("WorkflowMode", Float) = 1.0 _Color("Color", Color) = (0.5,0.5,0.5,1) _MainTex("Albedo", 2D) = "white" {} _Cutoff("Alpha Cutoff", Range(0.0, 1.0)) = 0.5 _Glossiness("Smoothness", Range(0.0, 1.0)) = 0.5 _GlossMapScale("Smoothness Scale", Range(0.0, 1.0)) = 1.0 _SmoothnessTextureChannel("Smoothness texture channel", Float) = 0 [Gamma] _Metallic("Metallic", Range(0.0, 1.0)) = 0.0 _MetallicGlossMap("Metallic", 2D) = "white" {} _SpecColor("Specular", Color) = (0.2, 0.2, 0.2) _SpecGlossMap("Specular", 2D) = "white" {} [ToggleOff] _SpecularHighlights("Specular Highlights", Float) = 1.0 [ToggleOff] _GlossyReflections("Glossy Reflections", Float) = 1.0 _BumpScale("Scale", Float) = 1.0 _BumpMap("Normal Map", 2D) = "bump" {} _Parallax("Height Scale", Range(0.005, 0.08)) = 0.02 _ParallaxMap("Height Map", 2D) = "black" {} _OcclusionStrength("Strength", Range(0.0, 1.0)) = 1.0 _OcclusionMap("Occlusion", 2D) = "white" {} _EmissionColor("Color", Color) = (0,0,0) _EmissionMap("Emission", 2D) = "white" {} _DetailMask("Detail Mask", 2D) = "white" {} _DetailAlbedoMap("Detail Albedo x2", 2D) = "grey" {} _DetailNormalMapScale("Scale", Float) = 1.0 _DetailNormalMap("Normal Map", 2D) = "bump" {} [Enum(UV0,0,UV1,1)] _UVSec("UV Set for secondary textures", Float) = 0 // Blending state [HideInInspector] _Surface("__surface", Float) = 0.0 [HideInInspector] _Blend("__blend", Float) = 0.0 [HideInInspector] _AlphaClip("__clip", Float) = 0.0 [HideInInspector] _SrcBlend("__src", Float) = 1.0 [HideInInspector] _DstBlend("__dst", Float) = 0.0 [HideInInspector] _ZWrite("__zw", Float) = 1.0 [HideInInspector] _Cull("__cull", Float) = 2.0 _ReceiveShadows("Receive Shadows", Float) = 1.0 } SubShader { // Lightweight Pipeline tag is required. If Lightweight pipeline is not set in the graphics settings // this Subshader will fail. One can add a subshader below or fallback to Standard built-in to make this // material work with both Lightweight Pipeline and Builtin Unity Pipeline Tags{"RenderType" = "Opaque" "RenderPipeline" = "LightweightPipeline" "IgnoreProjector" = "True"} LOD 300 HLSLINCLUDE #include "Quaternion.hlsl" #include "LWRP/ShaderLibrary/InputSurfacePBR.hlsl" struct Particle { float3 position; float3 velocity; float3 axis; float angle; float lifeTime; float time; float size; float3 color; float3 emission; }; StructuredBuffer<Particle> _Particles; void TransformVertex(inout float3 pos, inout float3 normal, int instanceID) { Particle particle = _Particles[instanceID]; pos.xyz *= particle.size; float4 rot = rotate_angle_axis(particle.angle, particle.axis); pos.xyz = rotate_vector(pos.xyz, rot); normal.xyz = rotate_vector(normal.xyz, rot); pos.xyz += particle.position; } void CalcSurface(inout SurfaceData surfaceData, int instanceID) { Particle particle = _Particles[instanceID]; surfaceData.albedo = particle.color; surfaceData.emission = particle.emission; } ENDHLSL // ------------------------------------------------------------------ // Forward pass. Shades all light in a single pass. GI + emission + Fog Pass { Name "StandardLit" Tags{"LightMode" = "LightweightForward"} Blend[_SrcBlend][_DstBlend] ZWrite[_ZWrite] Cull[_Cull] HLSLPROGRAM // Required to compile gles 2.0 with standard SRP library // All shaders must be compiled with HLSLcc and currently only gles is not using HLSLcc by default #pragma prefer_hlslcc gles #pragma exclude_renderers d3d11_9x #pragma target 2.0 // ------------------------------------- // Material Keywords #pragma shader_feature _NORMALMAP #pragma shader_feature _ALPHATEST_ON #pragma shader_feature _ALPHAPREMULTIPLY_ON #pragma shader_feature _EMISSION #pragma shader_feature _METALLICSPECGLOSSMAP #pragma shader_feature _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A #pragma shader_feature _OCCLUSIONMAP #pragma shader_feature _SPECULARHIGHLIGHTS_OFF #pragma shader_feature _GLOSSYREFLECTIONS_OFF #pragma shader_feature _SPECULAR_SETUP #pragma shader_feature _RECEIVE_SHADOWS_OFF // ------------------------------------- // Lightweight Pipeline keywords #pragma multi_compile _ _ADDITIONAL_LIGHTS #pragma multi_compile _ _VERTEX_LIGHTS #pragma multi_compile _ _MIXED_LIGHTING_SUBTRACTIVE #pragma multi_compile _ _SHADOWS_ENABLED #pragma multi_compile _ _LOCAL_SHADOWS_ENABLED #pragma multi_compile _ _SHADOWS_SOFT #pragma multi_compile _ _SHADOWS_CASCADE // ------------------------------------- // Unity defined keywords #pragma multi_compile _ DIRLIGHTMAP_COMBINED #pragma multi_compile _ LIGHTMAP_ON #pragma multi_compile_fog //-------------------------------------- // GPU Instancing #pragma multi_compile_instancing #pragma vertex Vertex #pragma fragment Fragment #include "LWRP/ShaderLibrary/LightweightPassLit.hlsl" LightweightVertexOutput Vertex(LightweightVertexInput v, inout uint instanceID : SV_InstanceID) { LightweightVertexOutput o = (LightweightVertexOutput)0; UNITY_SETUP_INSTANCE_ID(v); UNITY_TRANSFER_INSTANCE_ID(v, o); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); TransformVertex(v.vertex.xyz, v.normal.xyz, instanceID); float3 posWS = TransformObjectToWorld(v.vertex.xyz); o.clipPos = TransformWorldToHClip(posWS); half3 viewDir = VertexViewDirWS(GetCameraPositionWS() - posWS); #ifdef _NORMALMAP o.normal.w = viewDir.x; o.tangent.w = viewDir.y; o.binormal.w = viewDir.z; #else o.viewDir = viewDir; #endif OUTPUT_NORMAL(v, o); OUTPUT_LIGHTMAP_UV(v.lightmapUV, unity_LightmapST, o.lightmapUV); OUTPUT_SH(o.normal.xyz, o.vertexSH); half3 vertexLight = VertexLighting(posWS, o.normal.xyz); half fogFactor = ComputeFogFactor(o.clipPos.z); o.fogFactorAndVertexLight = half4(fogFactor, vertexLight); #if defined(_SHADOWS_ENABLED) && !defined(_RECEIVE_SHADOWS_OFF) #if SHADOWS_SCREEN o.shadowCoord = ComputeShadowCoord(o.clipPos); #else o.shadowCoord = TransformWorldToShadowCoord(posWS); #endif #endif #ifdef _ADDITIONAL_LIGHTS o.posWS = posWS; #endif return o; } half4 Fragment(LightweightVertexOutput IN, uint instanceID : SV_InstanceID) : SV_Target { UNITY_SETUP_INSTANCE_ID(IN); SurfaceData surfaceData; InitializeStandardLitSurfaceData(IN.uv, surfaceData); InputData inputData; InitializeInputData(IN, surfaceData.normalTS, inputData); CalcSurface(surfaceData, instanceID); half4 color = LightweightFragmentPBR(inputData, surfaceData.albedo, surfaceData.metallic, surfaceData.specular, surfaceData.smoothness, surfaceData.occlusion, surfaceData.emission, surfaceData.alpha); ApplyFog(color.rgb, inputData.fogCoord); return color; } ENDHLSL } Pass { Name "ShadowCaster" Tags{"LightMode" = "ShadowCaster"} ZWrite On ZTest LEqual Cull[_Cull] HLSLPROGRAM // Required to compile gles 2.0 with standard srp library #pragma prefer_hlslcc gles #pragma exclude_renderers d3d11_9x #pragma target 2.0 // ------------------------------------- // Material Keywords #pragma shader_feature _ALPHATEST_ON //-------------------------------------- // GPU Instancing #pragma multi_compile_instancing #pragma shader_feature _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A #pragma vertex Vertex #pragma fragment ShadowPassFragment #include "LWRP/ShaderLibrary/LightweightPassShadow.hlsl" VertexOutput Vertex(VertexInput v, uint instanceID : SV_InstanceID) { VertexOutput o; UNITY_SETUP_INSTANCE_ID(v); float3 normal = float3(0, 0, 0); TransformVertex(v.position.xyz, normal, instanceID); o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); o.clipPos = GetShadowPositionHClip(v); return o; } ENDHLSL } Pass { Name "DepthOnly" Tags{"LightMode" = "DepthOnly"} ZWrite On ColorMask 0 Cull[_Cull] HLSLPROGRAM // Required to compile gles 2.0 with standard srp library #pragma prefer_hlslcc gles #pragma exclude_renderers d3d11_9x #pragma target 2.0 #pragma vertex Vertex #pragma fragment DepthOnlyFragment // ------------------------------------- // Material Keywords #pragma shader_feature _ALPHATEST_ON #pragma shader_feature _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A //-------------------------------------- // GPU Instancing #pragma multi_compile_instancing #include "LWRP/ShaderLibrary/LightweightPassDepthOnly.hlsl" VertexOutput Vertex(VertexInput v, uint instanceID : SV_InstanceID) { VertexOutput o = (VertexOutput)0; UNITY_SETUP_INSTANCE_ID(v); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); float3 normal = float3(0, 0, 0); TransformVertex(v.position.xyz, normal, instanceID); o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); o.clipPos = TransformObjectToHClip(v.position.xyz); return o; } ENDHLSL } } FallBack "Hidden/InternalErrorShader" CustomEditor "LightweightStandardGUI" }
まとめ
いい感じのパーティクルができました!
次回、ComputeShader対応編(予定)