したかみ ぶろぐ

Unity成分多め

ComputeShaderでGPUパーティクルを実装する

f:id:vxd-naoshi-19961205-maro:20200904162614p:plain

始めに

ComputeShaderについて調べてみますと「GPUパーティクル」という単語を何度か目にしました。

今回はこのGPUパーティクルについてまとめようと思います。


こちらのサイトを参考に実装を行っています。

qiita.com


また、今回の内容は以下のリポジトリにあります。

github.com



GPUパーティクルの処理の流れ

パーティクルを見えないようにしてスタックに入れる

始めにパーティクルをすべて用意して、大きさを 0 にしたり透明にして見えないようにします。(今回は大きさを 0 にしています)

次に各パーティクルのインデックスを1つのスタックに保存します。

f:id:vxd-naoshi-19961205-maro:20200904164704p:plain



スタックから要素を取り出してパーティクルを生成する

パーティクルを生成する際はスタックからパーティクルのインデックスを取得します。

取得されたインデックスのパーティクルは大きさを戻す、不透明にする(今回は大きさを戻す)ことでパーティクルが生成されたように見せます。

また、生成されたパーティクルには「使用している」フラグを立てます。

f:id:vxd-naoshi-19961205-maro:20200904170747p:plain



生成されたパーティクルの更新を行う

パーティクルの更新を行う際は「使用している」フラグが立っているもののみを更新します。

パーティクルの更新では速度計算、座標計算、大きさの調整、寿命の減算などを行います。

寿命が尽きたパーティクルをスタックに戻す

パーティクルの更新で寿命が尽きたパーティクルは再び見えないようにして、「使用しているフラグ」を降ろします。

また、そのパーティクルのインデックスをスタックに追加して再び使えるようにします。

f:id:vxd-naoshi-19961205-maro:20200904170603p:plain



Append/ConsumeStructuredBufferについて

上の解説ではスタックと解説しましたが、ComputeShaderにスタックはありません。

スタックの代わりにAppendStructuredBuffer、ConsumeStructuredBufferを使用します。

  • AppendStructuredBuffer : 要素の追加のみが出来る
  • ConsumeStructuredBuffer : 要素の取り出しのみが出来る

このAppendStructuredBufferとConsumeStructuredBufferが1つのComputeBufferを参照することで疑似的にスタックの機能を実現することが出来ます。

ComputeBufferに値を追加する際はAppendStructuredBuffer.Append()を使い、値を取り出す際はConsumeStructuredBuffer.Consume()を使います。

f:id:vxd-naoshi-19961205-maro:20200904150408p:plain



実装

C#プログラム

using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.Rendering;

public class TestGPGPUParticle : MonoBehaviour {

    #region DrawMeshInstancedIndirect_Parameters
    [Header ("DrawMeshInstancedIndirectのパラメータ")]
    [SerializeField]
    private Mesh m_mesh;

    [SerializeField]
    private Material m_material;

    [SerializeField]
    private Bounds m_bounds;

    [SerializeField]
    private ShadowCastingMode m_shadowCastingMode;

    [SerializeField]
    private bool m_receiveShadows;
    #endregion

    #region GPUParticle_Parameters
    [Header ("GPUパーティクルのパラメータ")]
    [Space (20)]
    [SerializeField]
    private int m_instanceCount;

    [SerializeField]
    private int m_emitCount;

    [SerializeField]
    private float m_lifeTime;
    [SerializeField]
    private float m_force;
    [SerializeField]
    private float m_forceAngle;
    [SerializeField]
    private float m_gravity;
    [SerializeField]
    private float m_scale;
    #endregion

    #region ComputeShader_Parameters
    private ComputeBuffer m_argsBuffer;

    [SerializeField]
    private ComputeShader m_gpuParticleCalculator;

    private ComputeBuffer m_particlesBuffer;

    private ComputeBuffer m_particlePoolBuffer;

    private ComputeBuffer m_particlePoolCountBuffer;
    private int[] m_poolCount = { 0, 0, 0, 0 };

    private int m_updateKernel, m_emitKernel;

    private Vector3Int m_updateGroupSize, m_emitGroupSize;
    #endregion

    #region Struct_Particle
    private struct Particle {
        public Vector3 position;
        public Vector3 velocity;
        public Vector3 angle;
        public float duration;
        public float scale;
        public bool isActive;
    }
    #endregion

    // Start is called before the first frame update
    void Start () {

        InitializeArgsBuffer ();
        InitializeGPUParticle ();

    }

    // Update is called once per frame
    void Update () {

        m_gpuParticleCalculator.SetFloat ("Time", Time.time);
        m_gpuParticleCalculator.SetFloat ("deltaTime", Time.deltaTime);

        if (Input.GetMouseButton (0)) {
            EmitParticles ();
        }

        UpdateParticles ();

    }

    void LateUpdate () {

        Graphics.DrawMeshInstancedIndirect (
            m_mesh,
            0,
            m_material,
            m_bounds,
            m_argsBuffer,
            0,
            null,
            m_shadowCastingMode,
            m_receiveShadows
        );

    }

    private void EmitParticles () {

        int poolCount = GetParticlePoolCount ();

        // 未使用のパーティクルが一度に生成する個数より低い場合は
        // パーティクルを生成しない
        if (poolCount < m_emitCount)
            return;

        m_gpuParticleCalculator.Dispatch (m_emitKernel, m_emitGroupSize.x, m_emitGroupSize.y, m_emitGroupSize.z);

    }

    private int GetParticlePoolCount () {

        // ComputeShader内のDeadListに入っているデータの個数を取得する
        ComputeBuffer.CopyCount (m_particlePoolBuffer, m_particlePoolCountBuffer, 0);
        m_particlePoolCountBuffer.GetData (m_poolCount);
        int restPool = m_poolCount[0];

        Debug.Log ("restPool = " + restPool);

        return restPool;

    }

    private void UpdateParticles () {

        m_gpuParticleCalculator.Dispatch (m_updateKernel, m_updateGroupSize.x, m_updateGroupSize.y, m_updateGroupSize.z);

    }

    private void InitializeArgsBuffer () {

        Assert.IsNotNull (m_mesh, "メッシュが設定されていません");
        Assert.IsNotNull (m_material, "マテリアルが設定されていません");

        var args = new uint[] { 0, 0, 0, 0, 0 };

        args[0] = m_mesh.GetIndexCount (0);
        args[1] = (uint) m_instanceCount;

        m_argsBuffer = new ComputeBuffer (1, 4 * args.Length, ComputeBufferType.IndirectArguments);

        m_argsBuffer.SetData (args);

    }

    private void InitializeGPUParticle () {

        Assert.IsFalse (m_instanceCount < m_emitCount, "一度に出すパーティクルの個数がパーティクルの総数以上になっています");

        // インスタンスの個数、一度に出すパーティクルの個数は2の累乗に設定(計算しやすくするため)
        m_instanceCount = Mathf.ClosestPowerOfTwo (m_instanceCount);
        m_emitCount = Mathf.ClosestPowerOfTwo (m_emitCount);

        InitializeComputeBuffers ();

        InitializeParticlePool ();

        m_gpuParticleCalculator.SetFloat ("lifeTime", m_lifeTime);
        m_gpuParticleCalculator.SetFloat ("force", m_force);
        m_gpuParticleCalculator.SetFloat ("forceAngle", m_forceAngle);
        m_gpuParticleCalculator.SetFloat ("scale", m_scale);
        m_gpuParticleCalculator.SetFloat ("gravity", m_gravity);

        m_updateKernel = m_gpuParticleCalculator.FindKernel ("UpdateParticles");

        uint x, y, z;
        m_gpuParticleCalculator.GetKernelThreadGroupSizes (m_updateKernel, out x, out y, out z);
        m_updateGroupSize = new Vector3Int (m_instanceCount / (int) x, (int) y, (int) z);
        m_gpuParticleCalculator.SetBuffer (m_updateKernel, "particles", m_particlesBuffer);
        m_gpuParticleCalculator.SetBuffer (m_updateKernel, "deadList", m_particlePoolBuffer);

        m_emitKernel = m_gpuParticleCalculator.FindKernel ("EmitParticles");

        m_gpuParticleCalculator.GetKernelThreadGroupSizes (m_emitKernel, out x, out y, out z);
        m_emitGroupSize = new Vector3Int (m_emitCount / (int) x, (int) y, (int) z);
        m_gpuParticleCalculator.SetBuffer (m_emitKernel, "particles", m_particlesBuffer);
        m_gpuParticleCalculator.SetBuffer (m_emitKernel, "particlePool", m_particlePoolBuffer);

    }

    private void InitializeComputeBuffers () {

        m_particlesBuffer = new ComputeBuffer (m_instanceCount, Marshal.SizeOf (typeof (Particle)));
        m_material.SetBuffer ("_ParticleBuffer", m_particlesBuffer);

        m_particlePoolBuffer = new ComputeBuffer (m_instanceCount, Marshal.SizeOf (typeof (int)), ComputeBufferType.Append);
        // Append/Consumeの追加削除位置を0に設定する
        m_particlePoolBuffer.SetCounterValue (0);

        // パーティクルの個数を求める際に使用する
        m_particlePoolCountBuffer = new ComputeBuffer (4, Marshal.SizeOf (typeof (int)), ComputeBufferType.IndirectArguments);
        m_particlePoolCountBuffer.SetData (m_poolCount);

    }

    private void InitializeParticlePool () {

        int initializeParticlesKernel = m_gpuParticleCalculator.FindKernel ("InitializeParticles");
        m_gpuParticleCalculator.GetKernelThreadGroupSizes (initializeParticlesKernel, out uint x, out uint y, out uint z);
        m_gpuParticleCalculator.SetBuffer (initializeParticlesKernel, "deadList", m_particlePoolBuffer);
        m_gpuParticleCalculator.SetBuffer (initializeParticlesKernel, "particles", m_particlesBuffer);
        m_gpuParticleCalculator.Dispatch (initializeParticlesKernel, m_instanceCount / (int) x, 1, 1);

    }

    private void OnDisable () {

        m_particlesBuffer?.Release ();
        m_particlePoolBuffer = null;

        m_particlePoolBuffer?.Release ();
        m_particlePoolBuffer = null;

        m_particlePoolCountBuffer?.Release ();
        m_particlePoolCountBuffer = null;

        m_argsBuffer?.Release ();
        m_argsBuffer = null;

    }

}


ComputeShader

#pragma kernel InitializeParticles
#pragma kernel EmitParticles
#pragma kernel UpdateParticles

#define Deg2Rad 0.0174532924
#define PI 3.14159274

struct Particle {
    float3 position;
    float3 velocity;
    float3 angle;
    float duration;
    float scale;
    bool isActive;
};


RWStructuredBuffer<Particle> particles;
AppendStructuredBuffer<uint> deadList;
ConsumeStructuredBuffer<uint> particlePool;

float lifeTime;
float force;
float forceAngle;
float gravity;
float scale;

float deltaTime;
float Time;

// 乱数生成
inline float rnd(float2 p){
    return frac(sin(dot(p ,float2(12.9898, 78.233))) * 43758.5453);
}

float4x4 eulerAnglesToRotationMatrix(float3 angles) {

    float cx = cos(angles.x); float sx = sin(angles.x);
    float cy = cos(angles.z); float sy = sin(angles.z);
    float cz = cos(angles.y); float sz = sin(angles.y);

    return float4x4(
        cz*cy + sz*sx*sy, -cz*sy + sz*sx*cy, sz*cx, 0,
        sy*cx, cy*cx, -sx, 0,
        -sz*cy + cz*sx*sy, sy*sz + cz*sx*cy, cz*cx, 0,
        0, 0, 0, 1);

}

float3 CalcAngle(float id) {

    float x = rnd(float2(deltaTime + id*3.951, deltaTime-2*id));
    float y = rnd(float2(Time+id*1.23, Time+id*3.14));
    float z = rnd(float2(deltaTime+Time+id*0.987, Time/deltaTime+id*3.23));

    return float3(x*180.0, y*180.0, z*180.0);

}

// 上方向からangle以下傾けた速度を返す
float3 CalcVelocity(float angle, float id) {

    float4 vel = float4(0, force, 0, 1);
    float angleY = rnd(float2(Time + id*2.978, Time - deltaTime + id*1.098))*2*PI;
    float angleX = rnd(float2(Time + deltaTime - id*2.131, Time + id*4.521))*angle*Deg2Rad;

    vel = mul(eulerAnglesToRotationMatrix(float3(angleX, angleY, 0)), vel);

    return float3(vel.x, vel.y, vel.z);

}


[numthreads(8, 1, 1)]
void InitializeParticles(uint id : SV_DISPATCHTHREADID) {

    particles[id.x].isActive = false;

    deadList.Append(id.x);

}


[numthreads(8, 1, 1)]
void EmitParticles() {

    uint id = particlePool.Consume();
    particles[id].isActive = true;

    particles[id].position = float3(0, 0, 0);
    particles[id].velocity = CalcVelocity(forceAngle, id);
    particles[id].angle = CalcAngle(id);
    particles[id].duration = lifeTime;
    particles[id].scale = scale;

}


[numthreads(8, 1, 1)]
void UpdateParticles(uint id : SV_DISPATCHTHREADID) {

    if(particles[id.x].isActive) {

        particles[id.x].velocity -= float3(0, gravity * deltaTime, 0);
        particles[id.x].position += particles[id.x].velocity * deltaTime;
        particles[id.x].duration = max(0, particles[id.x].duration - deltaTime);
        particles[id.x].scale = lerp(scale, 0, 1 - particles[id.x].duration/lifeTime);

        if(particles[id.x].duration <= 0) {
            particles[id.x].isActive = false;
            deadList.Append(id.x);
        }

    }

}


Shader

Shader "Custom/GPUParticleShader"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard addshadow fullforwardshadows
        #pragma multi_compile_instancing
        #pragma instancing_options procedural:setup

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input
        {
            float2 uv_MainTex;
        };

        struct Particle {
            float3 position;
            float3 velocity;
            float3 angle;
            float duration;
            float scale;
            bool isActive;
        };

#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
    StructuredBuffer<Particle> _ParticleBuffer;
#endif

        #define Deg2Rad 0.0174532924

        float4x4 eulerAnglesToRottationMatrix(float3 angles) {

            float cx = cos(angles.x * Deg2Rad); float sx = sin(angles.x * Deg2Rad);
            float cy = cos(angles.z * Deg2Rad); float sy = sin(angles.z * Deg2Rad);
            float cz = cos(angles.y * Deg2Rad); float sz = sin(angles.y * Deg2Rad);

            return float4x4(
                cz*cy + sz*sx*sy, -cz*sy + sz*sx*cy, sz*cx, 0,
                sy*cx, cy*cx, -sx, 0,
                -sz*cy + cz*sx*sy, sy*sz + cz*sx*cy, cz*cx, 0,
                0, 0, 0, 1);

        }

        void setup() {

        #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
            float3 position = _ParticleBuffer[unity_InstanceID].position;
            float3 angle = _ParticleBuffer[unity_InstanceID].angle;
            float scale = _ParticleBuffer[unity_InstanceID].scale;

            // LifeTime == 1の場合、生成されて消えるまで180度回転する
            angle += (1 - _ParticleBuffer[unity_InstanceID].duration) * 180;

            // スケーリング
            unity_ObjectToWorld._11_21_31_41 = float4(scale, 0, 0, 0);
            unity_ObjectToWorld._12_22_32_42 = float4(0, scale, 0, 0);
            unity_ObjectToWorld._13_23_33_43 = float4(0, 0, scale, 0);

            // 回転
            unity_ObjectToWorld = mul(eulerAnglesToRottationMatrix(angle), unity_ObjectToWorld);

            // 座標
            unity_ObjectToWorld._14_24_34_44 = float4(position, 1);

            // モデル行列を求める(間違っているかも. . .)
            // 参考:https://qiita.com/yuji_yasuhara/items/8d63455d1d277af4c270
            unity_WorldToObject = unity_ObjectToWorld;
            unity_WorldToObject._14_24_34 *= -1;
            unity_WorldToObject._11_12_13 = unity_ObjectToWorld._11_21_31;
            unity_WorldToObject._21_22_23 = unity_ObjectToWorld._12_22_32;
            unity_WorldToObject._31_32_33 = unity_ObjectToWorld._13_23_33;
            unity_WorldToObject._11_12_13 /= scale * scale;
            unity_WorldToObject._21_22_23 /= scale * scale;
            unity_WorldToObject._31_32_33 /= scale * scale;
        #endif

        }


        half _Glossiness;
        half _Metallic;
        fixed4 _Color;

        // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
        // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
        // #pragma instancing_options assumeuniformscaling
        UNITY_INSTANCING_BUFFER_START(Props)
            // put more per-instance properties here
        UNITY_INSTANCING_BUFFER_END(Props)

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            // Metallic and smoothness come from slider variables
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}


シェーダーは過去のブログの内容をほぼそのまま持ってきました。

shitakami.hatenablog.com



解説

基本的に最初にお話しした"GPUパーティクルの処理の流れ"に沿っていると思います。


パーティクルの初期化

パーティクルのインデックスを保存するComputeBuffer "m_particlePoolBuffer" を生成します。

Append/Consumeを行うComputeBufferはコンストラクタで ComputeBufferType.Append を設定しなくてはいけません。

またSetCounterValue(0)を指定することで要素の追加を0から始めるよう設定します。

        m_particlePoolBuffer = new ComputeBuffer (m_instanceCount, Marshal.SizeOf (typeof (int)), ComputeBufferType.Append);
        // Append/Consumeの追加削除位置を0に設定する
        m_particlePoolBuffer.SetCounterValue (0);



ComputeBufferの初期化の次にパーティクルのデータが入ったComputeBufferと先ほどのm_particlePoolBufferをComputeShaderに渡して初期化処理を実行します。

ここで、m_particlePoolBufferはComputeShader内のAppendStructuredBuffer "deadList" から参照されます。

    private void InitializeParticlePool () {

        int initializeParticlesKernel = m_gpuParticleCalculator.FindKernel ("InitializeParticles");
        m_gpuParticleCalculator.GetKernelThreadGroupSizes (initializeParticlesKernel, out uint x, out uint y, out uint z);
        m_gpuParticleCalculator.SetBuffer (initializeParticlesKernel, "deadList", m_particlePoolBuffer);
        m_gpuParticleCalculator.SetBuffer (initializeParticlesKernel, "particles", m_particlesBuffer);
        m_gpuParticleCalculator.Dispatch (initializeParticlesKernel, m_instanceCount / (int) x, 1, 1);

    }



初期化処理では、「使用している」フラグを表す isActive をfalseに設定し未使用のパーティクルのインデックスをdeadList.Appendを利用してm_particlePoolBufferに追加します。

[numthreads(8, 1, 1)]
void InitializeParticles(uint id : SV_DISPATCHTHREADID) {

    particles[id.x].isActive = false;

    deadList.Append(id.x);

}



パーティクルの生成

パーティクルを生成する前に未使用のパーティクルがいくつあるか確認します。もし未使用の個数が一度に生成する個数より少ない場合は、パーティクルの生成を行いません。

個数が十分であればパーティクルの生成を行います。

また、一度に生成するパーティクルの個数はComputeShaderのグループサイズ x スレッドサイズとなります。

例えば、グループサイズ(16, 1, 1)、スレッドサイズ(8, 1, 1)とすると (16 x 1 x 1) x (8 x 1 x 1) = 128となり一度に128個のパーティクルを生成します。

    private void EmitParticles () {

        int poolCount = GetParticlePoolCount ();

        // 未使用のパーティクルが一度に生成する個数より低い場合は
        // パーティクルを生成しない
        if (poolCount < m_emitCount)
            return;

        m_gpuParticleCalculator.Dispatch (m_emitKernel, m_emitGroupSize.x, m_emitGroupSize.y, m_emitGroupSize.z);

    }



未使用のパーティクルの個数を求める関数は次のようになります。

ここで使用されている "m_particlePoolCountBuffer" は ComputeBufferType.IndirectArguments として生成されたComputeBufferとなります。 ComputeBuffer.CopyCountを使用してm_particlePoolBufferに入っているデータの個数を取得してm_particlPoolCountBufferの始めに保存します。

後は取得した個数を先ほどの呼び出し元に返します。

        // パーティクルの個数を求める際に使用する
        m_particlePoolCountBuffer = new ComputeBuffer (4, Marshal.SizeOf (typeof (int)), ComputeBufferType.IndirectArguments);
        m_particlePoolCountBuffer.SetData (m_poolCount);

  . . . . . . . . . . .
  . . . . . . . . . . .

    private int GetParticlePoolCount () {

        // ComputeShader内のDeadListに入っているデータの個数を取得する
        ComputeBuffer.CopyCount (m_particlePoolBuffer, m_particlePoolCountBuffer, 0);
        m_particlePoolCountBuffer.GetData (m_poolCount);
        int restPool = m_poolCount[0];

        return restPool;

    }



ComputeShader内でのパーティクル生成処理は次のようになります。

particlePoolはm_particlePoolBufferを参照しており、particlePool.Consumeで保存されているパーティクルのインデックスを取得します。

次に、パーティクルが使用されいていることを示すisActiveをtrueにして各パラメータの初期化を行います。

[numthreads(8, 1, 1)]
void EmitParticles() {

    uint id = particlePool.Consume();
    particles[id].isActive = true;

    particles[id].position = float3(0, 0, 0);
    particles[id].velocity = CalcVelocity(forceAngle, id);
    particles[id].angle = CalcAngle(id);
    particles[id].duration = lifeTime;
    particles[id].scale = scale;

}



パーティクルの更新

パーティクルの更新処理は毎フレーム行います。

パーティクルの更新をする際、最初にisActiveを確認してそのパーティクルが生きているかを確認します。 そうでないものには更新処理を行いません。

更新処理では速度、座標、大きさ、そして寿命の更新を行います。

この時寿命が尽きたら、初期化時と同様にdeadList.Appendでm_particlePoolBufferにインデックスを保存し、isActiveをfalseに設定します。

また、寿命が尽きたパーティクルは大きさの計算で自動的に大きさ 0 となり、見えなくなります。

[numthreads(8, 1, 1)]
void UpdateParticles(uint id : SV_DISPATCHTHREADID) {

    if(particles[id.x].isActive) {

        particles[id.x].velocity -= float3(0, gravity * deltaTime, 0);
        particles[id.x].position += particles[id.x].velocity * deltaTime;
        particles[id.x].duration = max(0, particles[id.x].duration - deltaTime);
        particles[id.x].scale = lerp(scale, 0, 1 - particles[id.x].duration/lifeTime);

        if(particles[id.x].duration <= 0) {
            particles[id.x].isActive = false;
            deadList.Append(id.x);
        }

    }

}



生成時の初速について

パーティクルの初速はUnityのパーティクルのShape Coneを模して作成しました。

パーティクルの初速は上方向から 0 ~ angle 傾けた速度ベクトルとなります。

これはx軸に対して0 ~ angle度傾けて、次に0 ~ 360度y軸に対して回転させることで求めることが出来ます。

また、ベクトルの回転はshaderでも使用している回転行列を使用しています。

// 上方向からangle以下傾けた速度を返す
float3 CalcVelocity(float angle, float id) {

    float4 vel = float4(0, force, 0, 1);
    float angleY = rnd(float2(Time + id*2.978, Time - deltaTime + id*1.098))*2*PI;
    float angleX = rnd(float2(Time + deltaTime - id*2.131, Time + id*4.521))*angle*Deg2Rad;

    vel = mul(eulerAnglesToRotationMatrix(float3(angleX, angleY, 0)), vel);

    return float3(vel.x, vel.y, vel.z);

}



結果

見栄え的にはあまり良くないかもしれませんが、たくさんのcubeを出すパーティクルが出来ました。

f:id:vxd-naoshi-19961205-maro:20200904163147g:plain

f:id:vxd-naoshi-19961205-maro:20200904163513g:plain



参考

今回の内容はこちらのサイトを参考にして勉強させていただきました。

qiita.com

乱数についてはこちらのサイトも参考にしています。

qiita.com

以下Unity公式マニュアルです。

docs.unity3d.com

docs.unity3d.com

docs.unity3d.com