Unity/URPで流体シミュレーション

023/12/24 : 初稿
Unity : 2022.3.12f1
URP : 14.0.9
マシン : M1Mac(Metal)

# やりたいこと

ラテアートみたいなやつ。液体の表面をなぞるとウニョーって色が混ざる感じの。

# 実現方法

↓こちらの記事を参考にさせていただきました。
https://edom18.hateblo.jp/entry/2020/02/23/103512

参考というか、公開されているGitHubのコードを見て、コンピュートシェーダーを使わずレンダリングパイプラインで実装し直しただけです。理論はさっぱりですが、コード自体はシンプルなのでなんとかなった感じです。

### RendererFeatureの用意

といっても描画タイミングになったらコールバック呼び出すだけのものです。本筋ではないのでさらっと読み飛ばしていただいてOKです。
```CallbackRendererFeature.cs
///
/// @file   CallbackRendererFeature.cs
/// @author Kyosei Yukimoto
/// @date   
///
/// @brief　コールバック
///         ファイル名は*RendererFeature.csでないとダメっぽい
///
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using System.Collections.Generic;

namespace Utils
{
    public class CallbackRendererFeature : ScriptableRendererFeature
    {
        // コールバック
        public interface ICallback
        {
            int GetCallbackRendererFeaturePriority();
            void Callback(ScriptableRenderContext context, ref RenderingData renderingData, SharedData data, RenderPassEvent ev);
            bool IsCallbackRendererFeatureDestroyed();
        }

        // Instance
        static CallbackRendererFeature Instance;

        // pass
        class PassData
        {
            RendererPass Pass;
            List<ICallback> Callbacks = new List<ICallback>();

            // コールバックを登録
            public bool Add(ICallback callback)
            {
                if (callback == null || Callbacks.Contains(callback)) {
                    return false;
                }
                Callbacks.Add(callback);
                return true;
            }

            // コールバックの登録抹消
            public void Remove(ICallback callback)
            {
                Callbacks.Remove(callback);
            }

            // 呼び出しすべきか
            public bool IsValid { get { return Pass != null && Pass.HasSetup && Callbacks.Count > 0; } }

            // Enqueue
            public void Enqueue(ScriptableRenderer renderer, SharedData shared, RenderPassEvent ev)
            {
                // null削除
                Callbacks.RemoveAll((c) => c == null || c.IsCallbackRendererFeatureDestroyed()); // 中身がDestroyされたMonoBehaviorだった場合は==nullで削除されない！

                // 必要ある？
                if (renderer == null || Callbacks.Count <= 0) {
                    return;
                }

                // ないなら作る
                if (Pass == null) {
                    Pass = new RendererPass();
                }

                // まだならSetup
                if (!Pass.HasSetup) {
                    Pass.Setup(shared, ev, Callbacks);
                }

                // このフレームで呼び出す必要がある
                renderer.EnqueuePass(Pass);
            }

            // Cleanup
            public void Cleanup()
            {
                if (Pass != null) {
                    Pass.Cleanup();
                }
            }
        }
        Dictionary<RenderPassEvent, PassData> PassDic = new Dictionary<RenderPassEvent, PassData>();

        // コールバック登録
        public static bool Add(RenderPassEvent ev, ICallback callback)
        {
            // null check
            if (Instance == null) {
                return false;
            }

            // set
            return Instance.AddCore(ev, callback);
        }

        // Remove
        public static void Remove(RenderPassEvent ev, ICallback callback)
        {
            // null check
            if (Instance == null) {
                return;
            }

            // set
            Instance.RemoveCore(ev, callback);
        }

        // Add
        bool AddCore(RenderPassEvent ev, ICallback callback)
        {
            // 辞書から取得/追加
            PassData data;
            if (!PassDic.TryGetValue(ev, out data)) {
                data = new PassData();
                PassDic.Add(ev, data);
            }

            // 登録
            return data.Add(callback);
        }

        // Remove
        void RemoveCore(RenderPassEvent ev, ICallback callback)
        {
            // 辞書から取得/追加
            PassData data;
            if (!PassDic.TryGetValue(ev, out data)) {
                return;
            }

            // 登録
            data.Remove(callback);
        }

        // Create
        public override void Create()
        {
            Instance = this;
            Setup();
        }

        // OnDestroy
        void OnDestroy()
        {
            Cleanup();
            if (Instance == this) {
                Instance = null;
            }
        }

        // Setup
        void Setup()
        {
            if (Shared == null) {
                Shared = new SharedData();
                Shared.Setup();
            }
        }

        // Cleanup
        void Cleanup()
        {
            foreach (var data in PassDic.Values) {
                data.Cleanup();
            }
        }

        // OnEnable
        void OnEnable()
        {
            Setup();
        }

        // OnDisable
        void OnDisable()
        {
            Cleanup();
        }

        // Dispose
        protected override void Dispose(bool disposing)
        {
            // Cleanup
            Cleanup();

            // base
            base.Dispose(disposing);
        }

        // AddRenderPasses : 毎フレーム呼ばれる
        public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
        {
            // プレイ中のみ
            if (!Application.isPlaying) {
                return;
            }

            // パスを登録
            foreach (var pair in PassDic) {
                if (pair.Value == null) {
                    continue;
                }
                pair.Value.Enqueue(renderer, Shared, pair.Key);
            }
        }

        // shared
        public class SharedData
        {
            // feature
            public RendererPass RendererPass;
            
            // カメラ
            public Camera CurrentCamera;
            public bool IsSceneViewCamera;

            // オリジナルカラーバッファ
            public RenderTargetIdentifier OriginalColorIdentifier;
            public RenderTargetIdentifier OriginalDepthIdentifier;
            public RenderTextureDescriptor OriginalDesc;

            // デストラクタ
            ~SharedData()
            {
                Cleanup();
            }

            // Setup
            public void Setup()
            {
            }

            // Cleanup
            public void Cleanup()
            {
                CurrentCamera = null;
            }

            // カメラの設定
            public void SetRenderingData(ref RenderingData renderingData)
            {
                CurrentCamera = renderingData.cameraData.camera;
                IsSceneViewCamera = renderingData.cameraData.isSceneViewCamera;
                OriginalDesc = renderingData.cameraData.cameraTargetDescriptor;
#if UNITY_2022_3_OR_NEWER
                OriginalColorIdentifier = renderingData.cameraData.renderer.cameraColorTargetHandle;
                OriginalDepthIdentifier = renderingData.cameraData.renderer.cameraDepthTargetHandle;
#else
                OriginalColorIdentifier = renderingData.cameraData.renderer.cameraColorTarget;
                OriginalDepthIdentifier = renderingData.cameraData.renderer.cameraDepthTarget;
#endif
            }
        }
        SharedData Shared;

        // pass
        public class RendererPass : ScriptableRenderPass
        {
            // Setup
            public bool HasSetup { get; private set; }

            // shared
            SharedData Shared;

            // callbacks
            List<ICallback> Callbacks;

            // Setup
            public void Setup(SharedData shared, RenderPassEvent ev, List<ICallback> callbacks)
            {
                // check
                if (HasSetup) {
                    return;
                }

                // null check
                if (shared == null || callbacks == null) {
                    return;
                }

                // store
                Shared = shared;
                renderPassEvent = ev;
                Callbacks = callbacks;
                
                // Setupしました
                HasSetup = true;
            }

            // Cleanup
            public void Cleanup()
            {
                Callbacks = null;
                Shared = null;
                HasSetup = false;
            }

            // Execute
            public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
            {
                // setup/null check
                if (!HasSetup || Callbacks == null) {
                    return;
                }

                // コールバックが一つ以上登録されているか？
                Callbacks.RemoveAll((c) => c == null || c.IsCallbackRendererFeatureDestroyed()); // 中身がDestroyされたMonoBehaviorだった場合は==nullで削除されない！
                if (Callbacks.Count <= 0) {
                    return;
                }

                // shared
                Shared.RendererPass = this;
                Shared.SetRenderingData(ref renderingData);

                // シーンビューはおかしい？
                if (Shared.IsSceneViewCamera) {
                    return;
                }

                // コールバック呼び出し
                Callbacks.Sort((a, b) => a.GetCallbackRendererFeaturePriority() - b.GetCallbackRendererFeaturePriority());
                foreach (var callback in Callbacks) {
                    callback.Callback(context, ref renderingData, Shared, renderPassEvent);
                }
            }
        }
    }
}
```
### CPU側で流体処理のコマンドバッファを構築

次に、呼び出される側のMonoBehaviorを追加します。無駄なテクスチャのコピーを減らすためちょっとわかりにくくなってるところもありますが、参考にさせていただいたコードの順番通りです。
```FluidMuddler.cs
///
/// @file   FluidMuddler.cs
/// @author Kyosei Yukimoto
/// @date   
///
/// @brief　ラテアート的な液体表面シミュレーション
///
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using System.Collections.Generic;

namespace Utils
{
    public class FluidMuddler : MonoBehaviour, CallbackRendererFeature.ICallback
    {
        // inspector
        [SerializeField] int RenderTextureWidth = 1024;
        [SerializeField] int RenderTextureHeight = 1024;
        [SerializeField] Vector2 TouchPixelSize = new Vector2(5, 5);
        [SerializeField] Material FluidMuddlerMaterial;
        [SerializeField] int CalcPressureCount = 20;
        [SerializeField] RenderPassEvent RenderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;

        // メッシュ
        protected Mesh Mesh { get; private set; }

        // マテリアル
        protected Material RuntimeMaterial { get; private set; }

        // propId
        protected int PropId_TempTex { get; private set; }
        protected int PropId_DeltaTime { get; private set; }
        protected int PropId_TouchVel { get; private set; }
        protected int PropId_VelocityTex { get; private set; }
        protected int PropId_DivergenceTex { get; private set; }
        protected int PropId_PressureTex { get; private set; }

        // rt
        public RenderTexture VelocityTex { get; private set; }
        public RenderTexture DivergenceTex { get; private set; }
        public RenderTexture PressureTex { get; private set; }
        public RenderTexture RenderTex { get; private set; }
        protected RenderTexture TempTex { get; private set; } // cmd.GetTemporaryRTで取得したいがwrap設定ができない
        protected MaterialPropertyBlock Block { get; private set; }

        // 現在のパス
        protected RenderPassEvent CurrentRenderPassEvent { get; private set; }

        // main camera
        protected Camera MainCamera { get; private set; }

        // DeltaTime
        protected float DeltaTime { get; private set; }

        // state
        protected enum State
        {
            None,
            Awaken,
            Setup,
            Destroyed,
        }
        protected State CurrentState { get; private set; }

        // タッチ
        public interface ITouch
        {
            (bool isTouching, Vector2 uv) GetTouchUV();
        }

        // タッチ管理
        protected struct Touch
        {
            public ITouch Owner { get; private set; }
            public bool IsTouching { get; private set; }
            public Vector2 UVPos { get; private set; }
            public Vector2 Velocity { get; private set; }
            
            // コンストラクタ
            public Touch(ITouch owner)
            {
                // init
                this = default;

                // store
                Owner = owner;
            }

            // Update
            public void Update(float deltaTime)
            {
                // null check
                if (Owner == null) {
                    Owner = null;
                    IsTouching = false;
                    return;
                }

                // 速度更新
                var (isTouching, uvPos) = Owner.GetTouchUV();
                if (!isTouching) {
                    Velocity = Vector2.zero;
                } else if (!IsTouching) {
                    UVPos = uvPos;
                    Velocity = Vector2.zero;
                } else {
                    Velocity = (uvPos - UVPos) / deltaTime;
                    UVPos = uvPos;
                }
                IsTouching = isTouching;
            }
        }
        protected List<Touch> Touches = new List<Touch>();

        // AddTouch
        public void AddTouch(ITouch touch)
        {
            if (touch == null) {
                return;
            }
            foreach (var t in Touches) {
                if (t.Owner == touch) {
                    return;
                }
            }
            Touches.Add(new Touch(touch));
        }

        // RemoveTouch
        public void RemoveTouch(ITouch touch)
        {
            if (touch == null) {
                return;
            }
            for (int i = 0; i < Touches.Count; i++) {
                var t = Touches[i];
                if (t.Owner == touch) {
                    Touches.RemoveAt(i);
                    return;
                }
            }
        }

        // OnDestroy
        protected virtual void OnDestroy()
        {
            // cleanup
            Cleanup();

            // 死にました
            CurrentState = State.Destroyed;
        }

        // Awake
        protected virtual void Awake()
        {
            // null check
            if (FluidMuddlerMaterial == null) {
                return;
            }

            // state
            CurrentState = State.Awaken;

            // store
            CurrentRenderPassEvent = RenderPassEvent;

            // propIds
            PropId_TempTex = Shader.PropertyToID("_TempTex");
            PropId_DeltaTime = Shader.PropertyToID("_DeltaTime");
            PropId_TouchVel = Shader.PropertyToID("_TouchVel");
            PropId_VelocityTex = Shader.PropertyToID("_VelocityTex");
            PropId_DivergenceTex = Shader.PropertyToID("_DivergenceTex");
            PropId_PressureTex = Shader.PropertyToID("_PressureTex");

            // material
            RuntimeMaterial = new Material(FluidMuddlerMaterial);
            Block = new MaterialPropertyBlock();
        }

        // Cleanup
        protected virtual void Cleanup()
        {
            // 解除
            CallbackRendererFeature.Remove(CurrentRenderPassEvent, this);

            // RenderTexture破棄
            VelocityTex = Tools.DestroyObject(VelocityTex);
            DivergenceTex = Tools.DestroyObject(DivergenceTex);
            PressureTex = Tools.DestroyObject(PressureTex);
            RenderTex = Tools.DestroyObject(RenderTex);
            TempTex = Tools.DestroyObject(TempTex);

            // mesh破棄
            Mesh = Tools.DestroyObject(Mesh);

            // material廃棄
            RuntimeMaterial = Tools.DestroyObject(RuntimeMaterial);

            // clear refs
            MainCamera = null;

            // state
            CurrentState = State.None;
        }

        // CreateRT
        protected virtual RenderTexture CreateRT(string name, Texture origin)
        {
            var rt = new RenderTexture(RenderTextureWidth, RenderTextureHeight, 0, RenderTextureFormat.ARGBHalf, RenderTextureReadWrite.Linear);
            rt.name = name;
            rt.useMipMap = false;
            rt.wrapMode = TextureWrapMode.Clamp;
            rt.filterMode = FilterMode.Bilinear;
            rt.Create();
            Graphics.Blit(origin, rt);
            return rt;
        }

        // Setup
        public virtual bool Setup(Camera mainCamera, Texture originTex)
        {
            // state check
            if (CurrentState != State.Awaken) {
                return false;
            }
            CurrentState = State.Setup;

            // store
            MainCamera = mainCamera;

            // レンダーテクスチャ
            VelocityTex = CreateRT("Utils.FluidMuddler.VelocityTex", Tools.GetClearTexture());
            DivergenceTex = CreateRT("Utils.FluidMuddler.DivergenceTex", Tools.GetClearTexture());
            PressureTex = CreateRT("Utils.FluidMuddler.PressureTex", Tools.GetClearTexture());
            RenderTex = CreateRT("Utils.FluidMuddler.RenderTex", originTex);
            TempTex = CreateRT("Utils.FluidMuddler.TempTex", Tools.GetClearTexture()); // cmd.GetTemporaryRTで取得したいがwrap設定ができない

            // メッシュ
            Mesh = CreateMesh();

            // callback
            CallbackRendererFeature.Add(CurrentRenderPassEvent, this);

            // OK
            return true;
        }

        // CreateMesh
        protected virtual Mesh CreateMesh()
        {
            // mesh
            var mesh = new Mesh();
            mesh.name = "Utils.FluidMuddler.CreateMesh";

            // 頂点
            Vector3[] vertices = new Vector3[]
            {
                new Vector3(-1, -1, 0),
                new Vector3(1, -1, 0),
                new Vector3(-1, 1, 0),
                new Vector3(1, 1, 0),
            };
            Vector2[] uvs = new Vector2[]
            {
                new Vector2(0, 0),
                new Vector2(1, 0),
                new Vector2(0, 1),
                new Vector2(1, 1),
            };
            int[] tris = new int[]
            {
                0, 2, 1,
                1, 2, 3,
            };

            // メッシュ構築
            mesh.vertices = vertices;
            mesh.uv = uvs;
            mesh.triangles = tris;
            mesh.RecalculateNormals();
            mesh.RecalculateTangents();
            mesh.RecalculateBounds();
            return mesh;
        }

        // Tick
        public virtual void Tick(float deltaTime)
        {
            // state check
            if (CurrentState != State.Setup) {
                return;
            }

            // 時間経過なし
            if (deltaTime <= 0 || Mathf.Approximately(deltaTime, 0)) {
                return;
            }

            // 更新
            for (int i = 0; i < Touches.Count; i++) {
                var touch = Touches[i];
                touch.Update(deltaTime);
                Touches[i] = touch;
            }

            // 死んだやつ削除
            Touches.RemoveAll((i) => i.Owner == null);

            // 時間記録
            DeltaTime = deltaTime;
        }

        // OnCallback
        protected virtual void PreDoCallback(ScriptableRenderContext context, ref RenderingData renderingData, CallbackRendererFeature.SharedData data, RenderPassEvent ev) { }
        protected virtual void PostDoCallback(ScriptableRenderContext context, ref RenderingData renderingData, CallbackRendererFeature.SharedData data, RenderPassEvent ev) { }

        // CallbackRendererFeature.ICallback.GetPriority
        public virtual int GetCallbackRendererFeaturePriority() { return 0; }

        // CallbackRendererFeature.ICallback.GetPriority
        public virtual bool IsCallbackRendererFeatureDestroyed() { return CurrentState == State.Destroyed; }
        
        // Callback
        public virtual void Callback(ScriptableRenderContext context, ref RenderingData renderingData, CallbackRendererFeature.SharedData data, RenderPassEvent ev)
        {
            // check
            if (ev != CurrentRenderPassEvent || data.CurrentCamera != MainCamera) {
                return;
            }

            // state check
            if (CurrentState != State.Setup) {
                return;
            }

            // 不要
            if (DeltaTime <= 0 || Mathf.Approximately(DeltaTime, 0)) {
                return;
            }

            // ユーザー処理呼び出し
            PreDoCallback(context, ref renderingData, data, ev);

            // コマンドバッファを取得
            CommandBuffer cmd = CommandBufferPool.Get("FluidMuddler");

            // deltaTime
            RuntimeMaterial.SetFloat(PropId_DeltaTime, DeltaTime);
            DeltaTime = 0;

#if false
            // 一時テクスチャ作成
            var desc = VelocityTex.descriptor;
            cmd.GetTemporaryRT(PropId_TempTex, desc, FilterMode.Bilinear);
            // ToDo : clampモードに設定
#endif
            
            // [0]移流項の更新
            cmd.Blit(VelocityTex, TempTex, RuntimeMaterial, 0);

            // レンダリング先をVelocityTexへ
            cmd.SetRenderTarget(TempTex);
            
            // set matrix
            cmd.SetViewProjectionMatrices(Matrix4x4.identity, Matrix4x4.Ortho(0, RenderTextureWidth, 0, RenderTextureHeight, -1, 1));

            // [1]触った場所の速度更新 : ToDo : GPUInstancing
            RuntimeMaterial.SetTexture(PropId_TempTex, VelocityTex);
            foreach (var touch in Touches) {
                if (!touch.IsTouching) {
                    continue;
                }
                Block.SetVector(PropId_TouchVel, new Vector4(touch.Velocity.x, touch.Velocity.y));
                var pos = new Vector3(touch.UVPos.x * RenderTextureWidth, touch.UVPos.y * RenderTextureHeight);
                cmd.DrawMesh(Mesh, Matrix4x4.TRS(pos, Quaternion.identity, TouchPixelSize), RuntimeMaterial, 0, 1, Block);
            }

            // フレームバッファ戻す
            cmd.SetRenderTarget(data.OriginalColorIdentifier);

            // restore matrix
            cmd.SetViewProjectionMatrices(data.CurrentCamera.worldToCameraMatrix, data.CurrentCamera.projectionMatrix);
            
            // [2]発散の更新 : TempTexは最新のVelocityTex
            cmd.Blit(TempTex, DivergenceTex, RuntimeMaterial, 2);

            // [3]圧力の更新 : VelocityTexはワークとして使う(内容は失われる)
            RuntimeMaterial.SetTexture(PropId_DivergenceTex, DivergenceTex);
            var (pressureTo, pressureFrom) = (VelocityTex, PressureTex);
            for (int i = 0; i < CalcPressureCount; i++) {
                cmd.Blit(pressureFrom, pressureTo, RuntimeMaterial, 3);
                (pressureTo, pressureFrom) = (pressureFrom, pressureTo);
            }
            if (pressureTo == PressureTex) { // CalcPressureCountが奇数だった場合
                cmd.CopyTexture(pressureFrom, PressureTex);
            }

            // [4]速度の更新 : TempTexは最新のVelocityTex
            RuntimeMaterial.SetTexture(PropId_PressureTex, PressureTex);
            cmd.Blit(TempTex, VelocityTex, RuntimeMaterial, 4);

            // [5]最終画像の更新
            RuntimeMaterial.SetTexture(PropId_VelocityTex, VelocityTex);
            cmd.CopyTexture(RenderTex, TempTex);
            cmd.Blit(TempTex, RenderTex, RuntimeMaterial, 5);

#if false
            // 一時テクスチャ廃棄
            cmd.ReleaseTemporaryRT(PropId_TempTex);
#endif

            // CommandBuffer実行
            context.ExecuteCommandBuffer(cmd);

            // CommandBuffer解放
            CommandBufferPool.Release(cmd);

            // ユーザー処理呼び出し
            PostDoCallback(context, ref renderingData, data, ev);
        }
    }
}
```
中で使用している関数の中身はこちら。毎回記事ごとに書くのもアレなんで、この辺りは別記事でまとめておいた方がいいかな・・・
```Tools.cs
///
/// @file   Utilities.cs
/// @author Kyosei Yukimoto
/// @date   
///
/// @brief　雑多な便利関数
///
using UnityEngine;

namespace Utils
{
    public static class Tools
    {
        // DestroyObject
        public static T DestroyObject<T>(T obj) where T : UnityEngine.Object
        {
            if (obj == null) {
                return null;
            }
            if (Application.isPlaying) {
                UnityEngine.Object.Destroy(obj);
            } else {
                UnityEngine.Object.DestroyImmediate(obj);
            }
            return null;
        }
        
        // 透明テクスチャ
        static Texture2D _ClearTexture;
        public static Texture2D GetClearTexture()
        {
            if (_ClearTexture == null) {
                int size = 4;
                _ClearTexture = new Texture2D(size, size);
                _ClearTexture.name = "Utils.Tools.ClearTexture";
                Color32[] colors = new Color32[size * size];
                for (int i = 0; i < colors.Length; i++) {
                    colors[i] = Color.clear;
                }
                _ClearTexture.SetPixels32(colors);
                _ClearTexture.Apply();
            }
            return _ClearTexture;
        }
    }
}
```
### シェーダー本体
そしてFluidMuddlerMaterialに渡すシェーダーはこちら。これも参考記事のコンピュートシェーダーとやってることは同じです。
```FluidMuddler.shader
Shader "Utils/FluidMuddler"
{
    Properties
    {
        // パスによって内容が変わる
        [MainTexture] _MainTex ("MainTex", 2D) = "black" {}

        // touch
        _TouchVel("TouchVel", Vector) = (0, 0, 0, 0) // xy : ドラッグ速度/s

        // 速度減衰
        _Attenuation ("Attenuation", Range(0.0, 1.0)) = 0.999
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        // [0]移流項の更新 : _MainTexは前フレームのVelocityTex
        Pass
        {
            Name "UpdateAdvection"
            ZTest Always
            ZWrite Off
            
            HLSLPROGRAM
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
            #pragma vertex vert
            #pragma fragment frag

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            sampler2D _MainTex;
            float _DeltaTime;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex.xyz);
                o.uv = v.uv;
                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                float2 velocity = tex2Dlod(_MainTex, float4(i.uv, 0, 0)).xy;
                float2 result = tex2Dlod(_MainTex, float4(i.uv - velocity * _DeltaTime, 0, 0)).xy;
                return float4(result, 0, 1);
            }
            ENDHLSL
        }

        // [1]触ったところに外圧を加える : _TempTexは直前のVelocityTex
        // (0, 0)-(RTW, RTH)の平行投影空間に対してメッシュを使う
        Pass
        {
            Name "Touch"
            ZTest Always
            ZWrite Off
            Cull Off
            Blend One One
            
            HLSLPROGRAM
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
            #pragma vertex vert
            #pragma fragment frag

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            sampler2D _TempTex;
            float4 _TouchVel;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex.xyz);
                o.uv = v.uv;
                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                if (length(i.uv * 2 - 1) > 1) {
                    clip(-1);
                }
                return _TouchVel;
                //float4 vel = tex2Dlod(_TempTex, float4(i.vertex.xy * 0.5 + 0.5, 0, 0));
                //if (length(i.uv * 2 - 1) < 1) {
                //    vel.xy += _TouchVel.xy;
                //}
                //return vel;
            }
            ENDHLSL
        }

        // [2]発散の計算 : _MainTexはVelocityTex
        Pass
        {
            Name "UpdateDivergence"
            ZTest Always
            ZWrite Off
            
            HLSLPROGRAM
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
            #pragma vertex vert
            #pragma fragment frag

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            sampler2D _MainTex;
            float4 _MainTex_TexelSize;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex.xyz);
                o.uv = v.uv;
                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                float x0 = tex2Dlod(_MainTex, float4(i.uv - float2(_MainTex_TexelSize.x, 0), 0, 0)).x;
                float x1 = tex2Dlod(_MainTex, float4(i.uv + float2(_MainTex_TexelSize.x, 0), 0, 0)).x;
                float y0 = tex2Dlod(_MainTex, float4(i.uv - float2(0, _MainTex_TexelSize.y), 0, 0)).y;
                float y1 = tex2Dlod(_MainTex, float4(i.uv + float2(0, _MainTex_TexelSize.y), 0, 0)).y;
                float divergence = (x1 - x0 + y1 - y0);
                return float4(divergence, 0, 0, 1);
            }
            ENDHLSL
        }

        // [3]圧力の計算 : _MainTexは前フレームのPressureTex
        Pass
        {
            Name "UpdatePressure"
            ZTest Always
            ZWrite Off
            
            HLSLPROGRAM
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
            #pragma vertex vert
            #pragma fragment frag

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            sampler2D _MainTex;
            float4 _MainTex_TexelSize;
            sampler2D _DivergenceTex;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex.xyz);
                o.uv = v.uv;
                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                float x0 = tex2Dlod(_MainTex, float4(i.uv - float2(_MainTex_TexelSize.x, 0), 0, 0)).x;
                float x1 = tex2Dlod(_MainTex, float4(i.uv + float2(_MainTex_TexelSize.x, 0), 0, 0)).x;
                float y0 = tex2Dlod(_MainTex, float4(i.uv - float2(0, _MainTex_TexelSize.y), 0, 0)).x;
                float y1 = tex2Dlod(_MainTex, float4(i.uv + float2(0, _MainTex_TexelSize.y), 0, 0)).x;
                float d = tex2Dlod(_DivergenceTex, float4(i.uv, 0, 0)).x;
                float relaxed = (x0 + x1 + y0 + y1 - d) * 0.25;
                return float4(relaxed, 0, 0, 1);
            }
            ENDHLSL
        }

        // [4]速度の更新 : _MainTexは直前の_VelocityTex
        Pass
        {
            Name "UpdateVelocity"
            ZTest Always
            ZWrite Off
            
            HLSLPROGRAM
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
            #pragma vertex vert
            #pragma fragment frag

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            sampler2D _MainTex;
            sampler2D _PressureTex;
            float4 _PressureTex_TexelSize;
            float _Attenuation;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex.xyz);
                o.uv = v.uv;
                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                float x0 = tex2Dlod(_PressureTex, float4(i.uv - float2(_PressureTex_TexelSize.x, 0), 0, 0)).x;
                float x1 = tex2Dlod(_PressureTex, float4(i.uv + float2(_PressureTex_TexelSize.x, 0), 0, 0)).x;
                float y0 = tex2Dlod(_PressureTex, float4(i.uv - float2(0, _PressureTex_TexelSize.y), 0, 0)).x;
                float y1 = tex2Dlod(_PressureTex, float4(i.uv + float2(0, _PressureTex_TexelSize.y), 0, 0)).x;
                float2 v = tex2Dlod(_MainTex, float4(i.uv, 0, 0)).xy;
                float2 v2 = v - (float2(x1, y1) - float2(x0, y0)) * 0.5;
                v2 *= _Attenuation;
                return float4(v2, 0, 1);
            }
            ENDHLSL
        }

        // [5]最終画像の更新 : _MainTexは前フレームの最終画像
        Pass
        {
            Name "UpdateTexture"
            ZTest Always
            ZWrite Off
            
            HLSLPROGRAM
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
            #pragma vertex vert
            #pragma fragment frag

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            sampler2D _VelocityTex;
            sampler2D _MainTex;
            float _DeltaTime;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex.xyz);
                o.uv = v.uv;
                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                float2 vel = tex2Dlod(_VelocityTex, float4(i.uv, 0, 0)).xy;
                return tex2Dlod(_MainTex, float4(i.uv - vel * _DeltaTime, 0, 0));
            }
            ENDHLSL
        }
    }
}
```
### あとはプロジェクト次第

あとは、個々のプロジェクトに合わせて組み込みます。

1. CallbackRendererFeatureをURPのレンダラーにAddRendererFeature。
2. FluidMuddlerをシーンに配置。
3. 適当なカメラと初期状態のテクスチャを指定してFluidMuddler.Setupを呼び出す。
4. かき混ぜ中かどうか、そしてそのかき混ぜたい位置のUV座標(0, 0)-(1, 1)を返すFluidMuddler.ITouchを実装してAddTouch。
5. FluildMuddler.RenderTexを、結果を表示したいマテリアルに設定する。
6. どこかのMonoBehaviour.UpdateとかでFluidMuddler.Tickを呼び出す。

手元ではEditor(Win/Mac)、iOSビルドで動いています（多分）し、毎フレームGC.Allocされることもないはず。

ただ、処理速度やメモリ消費量については考えないといけなさそう。

一応、UGUIのRawImageで表示するサンプルを置いときます。
```
///
/// @file   FluidMuddlerTest.cs
/// @author Kyosei Yukimoto
/// @date   
///
/// @brief　ラテアート的な流体表面シミュレーション
/// https://edom18.hateblo.jp/entry/2020/02/23/103512
///
using UnityEngine;
using UnityEngine.UI;
using Utils;

namespace ProjectMain
{
    public class FluidMuddlerTest : FluidMuddler, FluidMuddler.ITouch
    {
        // inspector
        [SerializeField] RawImage Preview;
        [SerializeField] Camera UICamera;

        // origin
        Texture OriginTex;

        // HasSetup
        bool HasSetup;

        // OnDestroy
        protected override void OnDestroy()
        {
            // clear refs
            OriginTex = null;

            // RemoveTouch
            RemoveTouch(this);

            // base
            base.OnDestroy();
        }

        // Setup
        void Start()
        {
            // 元テクスチャ取得
            if (Preview == null || Preview.texture == null) {
                return;
            }
            OriginTex = Preview.texture;

            // Setup
            if (!Setup(Camera.main, OriginTex)) {
                return;
            }
            Preview.texture = RenderTex;
            //Preview.texture = VelocityTex;

            // AddTouch
            AddTouch(this);

            // OK
            HasSetup = true;
            return true;
        }

        // Tick
        void Update()
        {
            // check
            if (!HasSetup) {
                return;
            }

            // base
            Tick(Time.deltaTime);
        }

        // 触れた位置
        public (bool isTouching, Vector2 uv) GetTouchUV()
        {
            // ない
            if (Preview == null) {
                return default;
            }

            // マウスクリックしてない
            if (!Input.GetMouseButton(0)) {
                return default;
            }

            // マウス位置をPreviewに変換
            var scrPos = Input.mousePosition;
            Vector2 localPos;
            RectTransform rt;
            Preview.rectTransform.parent.TryGetComponent(out rt);
            RectTransformUtility.ScreenPointToLocalPointInRectangle(rt, scrPos, UICamera, out localPos);
            float u = (localPos.x - Preview.rectTransform.rect.min.x) / Preview.rectTransform.rect.size.x;
            float v = (localPos.y - Preview.rectTransform.rect.min.y) / Preview.rectTransform.rect.size.y;
            return (true, new Vector2(u, v));
        }
    }
}
```