背景
NVIDIA Flex という大変便利な Unity用流体アセットがNVIDIA社から提供されている。
https://assetstore.unity.com/packages/tools/physics/nvidia-flex-for-unity-1-0-beta-120425
https://developer.nvidia.com/flex
これで液体を簡単に実装できる。
一様重力ならコード書かずに実装できてしまう。
マジで神。
何らかの重力場を設けて流体を空中に浮かせたい場合は独自実装になる。
NVIDIA Flex の API では、粒子への干渉は速度を設定する仕様になっている。
そのため、力から速度への変換が発生する。
API
提供されている API は NVIDIA.Flex.FlexActor クラス。
これを派生さることで、独自の動きを付けられる。
NVIDIA.Flex.FlexActor.onFlexUpdate イベントに登録することで、
液体を構成する各粒子の更新処理をカスタマイズできる。
イベントの型は、NVIDIA.Flex.FlexContainer.ParticleData の引数を持つ関数。
void OnFlexUpdate(FlexContainer.ParticleData _particleData)
引数の ParticleData は FlexContainer の publicな内部クラスであり、
各粒子を操作する機能を提供する。
最終的に ParticleData.GetParticle や GetVelocity で粒子に直接アクセスする。
Vector4 GetParticle(int _index)
Vector3 GetVelocity(int _index)
Vector4 の戻り値は粒子の位置と質量が格納されている。
トリッキーなことに、(x,y,z) 成分が位置で、w 成分が質量の逆数になっている。
これで粒子の速度を計算し、ParticleData.SetVelocity で設定すれば良い。
速度を設定するだけでライブラリが各粒子を上手い具合に動かしてくれる。
void SetVelocity(int _index, Vector3 _velocity)
#実装
OnFlexUpdate
論よりコード。
void OnFlexUpdate(FlexContainer.ParticleData _particleData)
{
if (!m_actor) return;
if (!m_actor.container) return;
m_actor.container.AddFluidIndices(m_actor.indices, m_actor.indexCount);
if (!m_actor.asset) return;
if (!m_actor.handle) return;
FlexExt.Instance instance = m_actor.handle.instance;
int[] indices = new int[instance.numParticles];
FlexUtils.FastCopy(instance.particleIndices, indices);
CenterForce(_particleData, indices);
}
OnFlexUpdate に入って、まずは生きてる粒子の添字を取得する。
(※今見たら、実装がかなり手抜きだった)
とりあえず int[] indices に必要な添え字が揃うので、
これで各粒子にアクセスして速度を計算できる。
速度場は色々変えられるので、別関数に括り出した。
CenterForce
以下に例として、粒子を空間の一点に集める力場を作る関数を示す。
これで空中の一点に水玉が浮くような動きを実現できる。
public Vector3 targetCenter;
static float coefK = 1;
static float coefC = 0.8f;
void CenterForce(FlexContainer.ParticleData _particleData, int[] _indices)
{
foreach (var index in _indices)
{
Vector4 particle = _particleData.GetParticle(index);
Vector3 velocity = _particleData.GetVelocity(index);
Vector3 position = (Vector3)particle;
float inversMass = particle.w;
Vector4 force = coefK * (targetCenter - position) - coefC * velocity;
_particleData.SetVelocity(index, velocity + force * inversMass);
}
}
_indices に使える粒子の添え字が入ってるので、foreach で回して各粒子について処理する。
_particleData.GetParticle(index) で位置と質量を取得する。
_particleData.GetVelocity(index) で速度を取得する。
particle.w が質量の逆数に注意しつつ、どうせ質量で割るから逆数のままで使う。
今回は中心 $ \boldsymbol{o} = $ targetCenter に向く力ということで、
位置 $ \boldsymbol{p} = (x,y,z)$ にある粒子には、$ \boldsymbol{f_k} = -k(\boldsymbol{p} - \boldsymbol{o}) $ の力を加える。
これはバネ力と思えば良い。
このままでは勢いが良すぎて振動し出すので、念のために減衰項を入れる。
速度 $ \boldsymbol{v} = $ の粒子には、$ f_c = -c \boldsymbol{v} $ の力を加える。
これは摩擦力と思えば良い。
すると、合計で外力 $ \boldsymbol{f} = \boldsymbol{f_k} + \boldsymbol{f_v} = k(\boldsymbol{o} - \boldsymbol{p}) - c \boldsymbol{v} $ を加えることになる。
質量 $ m $ で割れば、加速度 $ \boldsymbol{a} = \boldsymbol{f} / m $ を得る。
速度の変化が加速度であるため、新しい速度は $ \boldsymbol{v}' = \boldsymbol{v} + \tau \boldsymbol{a} $ で表せる。
手抜きなため、フレームの時間間隔が一定とし、時間間隔 $ \tau $ を定数扱いとする。
さらに係数 $ k $ と $ c $ に含ませて、$ \tau $ を実装から消す。
一般に、適切な外力 $ \boldsymbol{f} $ を設計できれば、
$ \boldsymbol{v}' = \boldsymbol{v} + \displaystyle\frac{\tau}{m} \boldsymbol{f} $
と実装すれば良い。
参考文献
- Assets/NVIDIA/Flex/Assets/FlexContainer.cs
- class ParticleData
- Assets/NVIDIA/Flex/Actors/FlexActor.cs
- class FlexActor : MonoBehaviour
- Assets/NVIDIA/Flex/Actors/FlexArrayActor.cs
- class FlexArrayActor : FlexActor