【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で行う一般的なパーティクルの実装をやっていきます

f:id:notargs:20180917213618g:plain

内部のコードを理解する

中を読んでみる

Packages/Lightweight Render Pipeline/LWRP/Shaders/LightweightStandardを追うことで、シェーダーの大まかな流れを掴めます

f:id:notargs:20180917213408p:plain

中を見ると、大きく4つのパスでStandardシェーダーが構築されていることが分かります

f:id:notargs:20180917213544p:plain

StandardLit Pass

ここが物体の色やライティングを決めるメインとなるパスです

ちょっと長いですが、LightweightPassLit.hlslで定義されているLitPassVertexLitPassFragmentを呼び出していることが分かります

f:id:notargs:20180917213431p:plain

実際にPackages/Lightweight Render Pipeline/LWRP/ShaderLibrary/LightweightPassLitを覗いてみると、内部では割と普通の頂点シェーダーとフラグメントシェーダーが書かれていることが分かります

ShadowCaster Pass

影を描画するために、ライト視点での深度をShadowMapとして描きこむパスです

色情報などはいらないため、StandardLitよりかなりシンプルな頂点シェーダー/フラグメントシェーダーが使われています

f:id:notargs:20180917214038p:plain f:id:notargs:20180917214050p:plain

DepthOnly Pass

あらかじめ深度を計算しておくことで不要なピクセルのライティングが行われないようにするほか、影を落とす処理などで使うための深度バッファを予め生成するためのパスです

こちらも深度バッファを描くだけのパスなのでコードはシンプルです

f:id:notargs:20180917214104p:plain f:id:notargs:20180917214125p:plain

Meta Pass

LightMap用の色情報を出力するパスです パーティクルをLightMapに焼き込むことも無いと思うので、今回は省略します

f:id:notargs:20180917214518p:plain

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"
}

まとめ

いい感じのパーティクルができました!

f:id:notargs:20180917213618g:plain

次回、ComputeShader対応編(予定)

参考リンク