UnityのVFX Graphで使用するSigned Distance FieldをCompute Shaderで動的に生成する

以前にUnityのVFX Graphで使用するAttribute Mapを動的に生成する記事を書きました。

- [UnityのVFX Graphで使用するAttribute Mapを動的に生成する \- Qiita](https://qiita.com/aa_debdeb/items/cd46d571dcb20f75817b)
- [UnityのVFX Graphで使用するAttribute MapをCompute Shaderで動的に生成する \- Qiita](https://qiita.com/aa_debdeb/items/1c7ff4d21d3a82acdb51)

今回はUnityのVFX Graphで使用するSigned Distance FieldをCompute Shaderで動的に生成したいと思います。

使用したバージョンは以下のようになっています。

- Unity: 2019.3.10f1 Personal
- Visual Effect Graph: 7.3.1

まずそもそもですが、VFX GraphにおいてSigned Distance Fieldは`Update Particle` Contextの`Conform to Signed DistanceField` Blockまたは`Collide with Signed Distance Field` Blockで使用することができます。これらのBlockではSigned Distance Fieldとして`Texture3D`を設定します。

ということで、3次元テクスチャの各ボクセル位置における符号付き距離をCompute Shaderで書き込み、そのテクスチャをVFX Graphの`Texture3D`のプロパティに設定すればよさそうです。

---

まず、C#スクリプトは以下のようになります。ほとんど [Attribute Mapを動的に生成したとき](https://qiita.com/aa_debdeb/items/1c7ff4d21d3a82acdb51)と同じですが、`RenderTexture`の`dimension`プロパティと`volumeDeph`プロパティを設定して3次元テクスチャにしています。


```cs:GenerateSignedDistanceField.cs
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.VFX;

[ExecuteInEditMode]
[RequireComponent(typeof(VisualEffect))]
public class GenerateDynamicSignedDistanceField : MonoBehaviour
{

    struct ThreadSize
    {
        public int x;
        public int y;
        public int z;

        public ThreadSize(uint x, uint y, uint z)
        {
            this.x = (int)x;
            this.y = (int)y;
            this.z = (int)z;
        }
    }

    [SerializeField]
    ComputeShader shader;

    [SerializeField]
    int width = 64;

    [SerializeField]
    int height = 64;

    [SerializeField]
    int depth = 64;

    VisualEffect vfx;
    RenderTexture signedDistanceField;
    int kernel;
    ThreadSize threadSize;

    void Start()
    {
        if (!SystemInfo.supportsComputeShaders)
        {
            return;
        }
        vfx = GetComponent<VisualEffect>();
        SetupSignedDistanceField();
    }

    void Update()
    {
        if (!SystemInfo.supportsComputeShaders)
        {
            return;
        }

        if (width != signedDistanceField.width || height != signedDistanceField.height || depth != signedDistanceField.volumeDepth)
        {
            SetupSignedDistanceField();
        }

        UpdateSignedDistanceField();
    }

    void SetupSignedDistanceField()
    {
        signedDistanceField = new RenderTexture(width, height, 0, RenderTextureFormat.RFloat);
        signedDistanceField.enableRandomWrite = true;
        signedDistanceField.dimension = TextureDimension.Tex3D;
        signedDistanceField.volumeDepth = depth;
        signedDistanceField.Create();

        kernel = shader.FindKernel("GenerateDynamicSignedDistanceField");
        uint threadSizeX, threadSizeY, threadSizeZ;
        shader.GetKernelThreadGroupSizes(kernel, out threadSizeX, out threadSizeY, out threadSizeZ);
        threadSize = new ThreadSize(threadSizeX, threadSizeY, threadSizeZ);

        shader.SetTexture(kernel, "SignedDistanceField", signedDistanceField);

        vfx.SetTexture("Signed Distance Field", signedDistanceField);
    }

    void UpdateSignedDistanceField()
    {
        shader.SetFloat("Time", Time.time);
        shader.Dispatch(kernel, Mathf.CeilToInt(width / threadSize.x), Mathf.CeilToInt(height / threadSize.y), Mathf.CeilToInt(depth / threadSize.z));
    }
}
```

Compute Shader側では、以下のように時間に応じて球とトーラスで滑らかに切り替わるような符号付き距離場を生成しています。

```c:GenerateDynamicSignedDistanceField.compute
#pragma kernel GenerateDynamicSignedDistanceField

RWTexture3D<float> SignedDistanceField;

float Time;

float SdSphere(float3 p, float r)
{
    return length(p) - r;
}

float SdTorus(float3 p, float2 t)
{
    return length(float2(length(p.xz) - t.x, p.y)) - t.y;
}

[numthreads(8,8,8)]
void GenerateDynamicSignedDistanceField (uint3 id : SV_DispatchThreadID)
{
    float width, height, depth;
    SignedDistanceField.GetDimensions(width, height, depth);
    float3 size = float3(width, height, depth);

    float3 p = id / size * 2.0 - 1.0;
    float ds = SdSphere(p, 0.8);
    float dt = SdTorus(p, float2(0.7, 0.2));
    float t = sin(Time) * 0.5 + 0.5;
    SignedDistanceField[id] = lerp(ds, dt, t);
}
```

VFX Graph側は次のように`Signed Distance Field`という名前の`Texture3D`プロパティを作成して`Exposed`にしておきます。
![generate_dynamic_signed_distance_field_conform.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/03d6e675-8131-8196-6120-5f71a05b7e95.png)

こうすると以下のようになり、トーラスから円になり、またトーラスに戻るようなSigned Distance Fieldを動的に生成できていることがわかります(若干わかりにくいですが...)。
![GenerateDynamicSignedDistanceField_005.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/586f7c41-13f1-e101-2b87-afad4d670404.gif)

同じSigned Distance Fieldを`Collide with Signed Distance Field`にも使ってみました。
![generate_dynamic_signed_distance_field_collide.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/d10f4123-bcdf-ff22-4809-2ba7cf34b64e.png)
![GenerateDynamicSignedDistanceField_004.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/c9ce5480-00cb-48f1-d9ac-8c47806f1a61.gif)

---

VFX Graphで使用するSigned Distance FieldをCompute Shaderで動的に生成してみました。

VFX GraphのSigned Distance Fieldについては調べてもあまり情報がなく、どういうフォーマットでTexture3Dに格納すればいいのか不明でした。例えば、メッシュからSigned Distnace Fieldを生成する[SDFr](https://github.com/xraxra/SDFr)というツールでは[-1, 1]の範囲に正規化しているようですが、理由はよくわかりませんでした。

ただ、`Conform to Signed Distance Field` Blockと`Collide with Signed Distance Field` Blockで生成されるコードを見た感じでは、その地点の符号付き距離の値自体というよりもその地点の符号付き距離を微分したベクトル(つまり符号付き距離が0になる地点への最短方向)のほうが大事そうでした。そのベクトルは距離関数が正規化されていなくても求まるはずなので、細かいことは考えずにTexture3Dに符号付き距離を入れればよさそうです(生成されるコードは`Edit > Prefrences > Visual Effects > Show Additional Debug info`にチェックを入れるとBlock選択時のインスペクターに表示されるようになります)。