ParticleSystemForceFieldでParticleを自作のベクトル場を使って動かす

#はじめに

ParticleSystemForceFieldで3Dテクスチャを使うことにより、事前計算したベクトル場でParticleを動かすことができます。

今回はParticleをCurlNoiseで動かしてみました。

ParticleSystemForceFieldは Unity2018.3以降のバージョンで利用することができます。
ParticleSystemForceFieldについて
https://docs.unity3d.com/ja/current/Manual/class-ParticleSystemForceField.html

実装したものはGithubに置いてあります。

https://github.com/metaaaa/PSFF-3DTextureMaker


#結果
こんな感じ
![curlNoise.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440297/82cbbe83-d369-55df-1a33-f768ff85f91d.gif)


#処理の流れ
こんな感じ
![syorino_nagare.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440297/ac75ffeb-bcc3-b524-d087-63726398d48d.png)


#実装
###C#部分

```C#:Create3DTexture.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;

/// <summary>
/// 3Dテクスチャを作成する
/// </summary>
public class Create3DTexture : MonoBehaviour
{
    [SerializeField] int size = 64; //立方体3Dテクスチャの1辺のサイズ
    [SerializeField] Vector3 offset = new Vector3(0f,0f,0f);//ノイズの位相のずれ
    [SerializeField] float density = 3f;//ノイズの密度
    [SerializeField] ComputeShader computeshader = null; //計算に使うComputeShader
    [SerializeField] string savePath = "Assets/CurlNoiseParticleSystem/Textures/";//3Dテクスチャ保存パス
    [SerializeField] string fileName = "CurlNoise3DTexture";//3Dテクスチャ名
    private TextureFormat format = TextureFormat.RGBAHalf;//テクスチャの色のフォーマット
    const int threadCount = 8;//compute shader 1方向のスレッド数

    /// <summary>
    /// ContextMenuから3Dテクスチャを生成する
    /// </summary>
    [ContextMenu ("Gen 3dtexture")]
    void GenCurlNoise3DTex()
    {
        // Texture3D 初期化
        Texture3D texture3d = new Texture3D(size, size, size, format, false);

        // 色情報を保存する3次元の配列を初期化する
        Color[] colors3d = new Color[size * size * size];

        // float4 配列の compute buffer
        ComputeBuffer colorBuffer = new ComputeBuffer(size * size * size, sizeof(float) * 4);

        // ComputeShaderに値とバッファ渡して計算する
        computeshader.SetFloat("size", (float)size);
        computeshader.SetFloat("density", density);
        computeshader.SetVector("offset", offset);
        computeshader.SetBuffer(0,"velocityBuffer",colorBuffer);
        computeshader.Dispatch(0, size/threadCount, size/threadCount, size/threadCount);

        // 結果を貰う
        colorBuffer.GetData(colors3d);

        // 配列のデータを3D texture にうつす
        texture3d.SetPixels(colors3d);
        texture3d.Apply();

        // アセットファイルとして3DTextureを保存
        AssetDatabase.CreateAsset(texture3d, savePath+fileName+".asset");

        // メモリ開放
        colorBuffer.Release();
    }
}
#endif
```

　C#部分ではComputeShaderにパラメータとコンピュートバッファを渡して計算結果を受け取り、3Dテクスチャを作成しアセットとして保存しています。

　Texture3Dにデータを格納するために、Texture3DのSetPixels関数を利用します。SetPixelsは1次元のColor配列を引数として受け取り、3Dテクスチャの各ピクセルに格納します。
Color配列のindexとTexture3Dの座標の対応は　index = x + size * y + size * size * z　となっています。
図にするとこんな感じ

![Setpixels解説.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440297/25805412-12e7-4c3a-0c96-b037c86f027e.png)

```C#
// Texture3D 初期化
Texture3D texture3d = new Texture3D(size, size, size, format, false);

// 色情報を保存する3次元の配列を初期化する
Color[] colors3d = new Color[size * size * size];

// float4 配列の compute buffer
ComputeBuffer colorBuffer = new ComputeBuffer(size * size * size, sizeof(float) * 4);

// ComputeShaderに値とバッファ渡して計算する
computeshader.SetFloat("size", (float)size);
computeshader.SetFloat("density", density);
computeshader.SetVector("offset", offset);
computeshader.SetBuffer(0,"velocityBuffer",colorBuffer);
computeshader.Dispatch(0, size/threadCount, size/threadCount, size/threadCount);

// 結果を貰う
colorBuffer.GetData(colors3d);
```

Texture3Dの初期化でformatをRGBAHalfにしているため、負の値とかちょっとでかい値とかでも格納できるようにしています。

ComputeBufferはC#側ではColor配列として、ComputeShader側としてはfloat4配列として扱いたいため、float4個分のデータ*3Dテクスチャのピクセル分で宣言します。

あとはパラメータ等適当に渡してDispatchします。

###ComputeShader部分

```hlsl:CurlNoise.compute
#pragma kernel CSMain
#include "CurlNoise.cginc"

float  size;    //立方体3Dテクスチャの1辺のサイズ
float  density; //ノイズの密度
float3 offset;  //ノイズの位相のずれ
RWStructuredBuffer<float4> velocityBuffer;

[numthreads(8,8,8)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    // get voxel pos
    float3 pos = float3((float)id.x,(float)id.y,(float)id.z)/size*density;
    // get curl noise
    float3 noise = curlNoise(pos + offset);
    // set result
    velocityBuffer[id.x + id.y * size + id.z * size * size] = float4(noise.x,noise.y,noise.z,1.0);
}
```

DispatchThreadIDをsizeで割って0～1の範囲に収めて適当な数をかけて作った数字でCurlNoiseを計算しています。

CurlNoiseの実装部分をcgincに分けています。実装を見たい方はGithubを探ってみてください。
CurlNoiseの実装は[こちら](https://qiita.com/nyamadandan/items/2a8bc7a3639e7b5ce9c9)を参考に実装しました。
CurlNoise実装のためにSimplexNoiseを利用しています。SimplexNoiseは[webgl-noise](https://github.com/ashima/webgl-noise)様からお借りしました。

###計算結果を保存

```C#:
// 結果を貰う
colorBuffer.GetData(colors3d);

// 配列のデータを3D texture にうつす
texture3d.SetPixels(colors3d);
texture3d.Apply();

// アセットファイルとして3DTextureを保存
AssetDatabase.CreateAsset(texture3d, savePath+fileName+".asset");

// メモリ開放
colorBuffer.Release();

```

計算結果をTexture3DにうつしてTexture3Dをアセットファイルとして保存します。ComputeBufferも忘れずに解放します。


###ParticleSystem側

設定はこんな感じ　ExternalForcesを必ず有効にしましょう。

![ParticleSetting1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440297/afbd909a-b91f-2259-7388-c545bd21048f.png)

###ExternalForcesを必ず有効にしましょう。

ParticleSystemForceFieldの設定はこんな感じ
![ParticleSetting2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440297/5e8ef4cc-3260-e88f-ffc7-783b5a635de2.png)

こんな感じで置いたら完成！
![ParticleSetting3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440297/4a9d0408-d62e-860a-272b-6e9a69d68cbd.png)

#おわりに

3Dテクスチャのサイズを128*128*128とかにしちゃうとメモリをたくさん食べちゃうので気を付けないといけないですね。