Aetheria/Assets/Shaders/StableFluids/Fluid.cs
Meta Vens c6786da2b3 Keep Fluid's int2 alias, pointed at CultMath
With using static CultMath.math, int2.zero binds to the int2() factory
method (CS0119). The alias existed for exactly that; it now names
CultMath.int2, as SectorMap does for float2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:03:32 +02:00

220 lines
No EOL
7 KiB
C#

// StableFluids - A GPU implementation of Jos Stam's Stable Fluids on Unity
// https://github.com/keijiro/StableFluids
using System.Collections.Generic;
using CultMath;
using UnityEngine;
using static CultMath.math;
using int2 = CultMath.int2;
using Random = UnityEngine.Random;
public class Fluid : MonoBehaviour
{
#region Editable attributes
[SerializeField] int _resolution = 512;
[SerializeField] float _viscosity = 1e-6f;
[SerializeField] float _damping = .05f;
[SerializeField] float _range = 512;
[SerializeField] Transform _camera;
#endregion
#region Internal resources
[SerializeField, HideInInspector] ComputeShader _compute;
[SerializeField, HideInInspector] Shader _shader;
#endregion
#region Private members
Material _shaderSheet;
int2 _previousCameraIntPosition = int2.zero;
private List<Force> _forces = new List<Force>();
struct Force
{
public float2 Position;
public float2 Vector;
public float Exponent;
}
static class Kernels
{
public const int Advect = 0;
public const int Force = 1;
public const int PSetup = 2;
public const int PFinish = 3;
public const int Jacobi1 = 4;
public const int Jacobi2 = 5;
public const int Adjust = 6;
}
int ThreadCount { get { return (_resolution + 7) / 8; } }
int Resolution { get { return ThreadCount * 8; } }
// Vector field buffers
static class VFB
{
public static RenderTexture V1;
public static RenderTexture V2;
public static RenderTexture V3;
public static RenderTexture P1;
public static RenderTexture P2;
}
RenderTexture AllocateBuffer(int componentCount)
{
var format = RenderTextureFormat.ARGBHalf;
if (componentCount == 1) format = RenderTextureFormat.RHalf;
if (componentCount == 2) format = RenderTextureFormat.RGHalf;
var rt = new RenderTexture(Resolution, Resolution, 0, format);
rt.enableRandomWrite = true;
rt.Create();
return rt;
}
#endregion
#region MonoBehaviour implementation
public void AddForce(float2 position, float2 vector, float exponent)
{
_forces.Add(new Force { Position = position, Vector = vector, Exponent = exponent });
}
void OnValidate()
{
_resolution = Mathf.Max(_resolution, 8);
}
void Start()
{
_shaderSheet = new Material(_shader);
VFB.V1 = AllocateBuffer(2);
VFB.V2 = AllocateBuffer(2);
VFB.V3 = AllocateBuffer(2);
VFB.P1 = AllocateBuffer(1);
VFB.P2 = AllocateBuffer(1);
}
void OnDestroy()
{
Destroy(_shaderSheet);
Destroy(VFB.V1);
Destroy(VFB.V2);
Destroy(VFB.V3);
Destroy(VFB.P1);
Destroy(VFB.P2);
}
void Update()
{
var dt = max(Time.deltaTime, .01f);
var dx = 1.0f / Resolution;
var snapLength = _range / _resolution;
// Input point
var cameraIntPosition = int2(
(int)(_camera.position.x / snapLength),
(int)(_camera.position.z / snapLength)
);
var cameraPosition = float2(
cameraIntPosition.x * snapLength,
cameraIntPosition.y * snapLength
);
Shader.SetGlobalVector("_FluidTransform", new Vector4(cameraPosition.x, cameraPosition.y, _range));
Shader.SetGlobalTexture("_FluidVelocity", VFB.V1);
var delta = (float2)(cameraIntPosition - _previousCameraIntPosition) / Resolution;
_compute.SetVector("Delta", new Vector4(delta.x,delta.y));
_compute.SetFloat("Damping", _damping);
_compute.SetTexture(Kernels.Adjust, "U_in", VFB.V1);
_compute.SetTexture(Kernels.Adjust, "W_out", VFB.V3);
_compute.Dispatch(Kernels.Adjust, ThreadCount, ThreadCount, 1);
// Common variables
_compute.SetFloat("Time", Time.time);
_compute.SetFloat("DeltaTime", dt);
// Advection
_compute.SetTexture(Kernels.Advect, "U_in", VFB.V3);
_compute.SetTexture(Kernels.Advect, "W_out", VFB.V2);
_compute.Dispatch(Kernels.Advect, ThreadCount, ThreadCount, 1);
// Diffuse setup
var dif_alpha = dx * dx / (_viscosity * dt);
_compute.SetFloat("Alpha", dif_alpha);
_compute.SetFloat("Beta", 4 + dif_alpha);
Graphics.CopyTexture(VFB.V2, VFB.V1);
_compute.SetTexture(Kernels.Jacobi2, "B2_in", VFB.V1);
// Jacobi iteration
for (var i = 0; i < 20; i++)
{
_compute.SetTexture(Kernels.Jacobi2, "X2_in", VFB.V2);
_compute.SetTexture(Kernels.Jacobi2, "X2_out", VFB.V3);
_compute.Dispatch(Kernels.Jacobi2, ThreadCount, ThreadCount, 1);
_compute.SetTexture(Kernels.Jacobi2, "X2_in", VFB.V3);
_compute.SetTexture(Kernels.Jacobi2, "X2_out", VFB.V2);
_compute.Dispatch(Kernels.Jacobi2, ThreadCount, ThreadCount, 1);
}
float2 getUV(float2 pos, float2 camPos, float scale)
{
return -(pos-camPos)/scale + float2(.5f,.5f);
}
// Add external forces
foreach(var force in _forces)
{
var uv = getUV(force.Position, cameraIntPosition, _range);
_compute.SetVector("ForceOrigin", new Vector4(uv.x,uv.y));
_compute.SetFloat("ForceExponent", force.Exponent);
_compute.SetTexture(Kernels.Force, "W_in", VFB.V2);
_compute.SetTexture(Kernels.Force, "W_out", VFB.V3);
_compute.SetVector("ForceVector", new Vector4(force.Vector.x,force.Vector.y));
_compute.Dispatch(Kernels.Force, ThreadCount, ThreadCount, 1);
}
// Projection setup
_compute.SetTexture(Kernels.PSetup, "W_in", VFB.V3);
_compute.SetTexture(Kernels.PSetup, "DivW_out", VFB.V2);
_compute.SetTexture(Kernels.PSetup, "P_out", VFB.P1);
_compute.Dispatch(Kernels.PSetup, ThreadCount, ThreadCount, 1);
// Jacobi iteration
_compute.SetFloat("Alpha", -dx * dx);
_compute.SetFloat("Beta", 4);
_compute.SetTexture(Kernels.Jacobi1, "B1_in", VFB.V2);
for (var i = 0; i < 20; i++)
{
_compute.SetTexture(Kernels.Jacobi1, "X1_in", VFB.P1);
_compute.SetTexture(Kernels.Jacobi1, "X1_out", VFB.P2);
_compute.Dispatch(Kernels.Jacobi1, ThreadCount, ThreadCount, 1);
_compute.SetTexture(Kernels.Jacobi1, "X1_in", VFB.P2);
_compute.SetTexture(Kernels.Jacobi1, "X1_out", VFB.P1);
_compute.Dispatch(Kernels.Jacobi1, ThreadCount, ThreadCount, 1);
}
// Projection finish
_compute.SetTexture(Kernels.PFinish, "W_in", VFB.V3);
_compute.SetTexture(Kernels.PFinish, "P_in", VFB.P1);
_compute.SetTexture(Kernels.PFinish, "U_out", VFB.V1);
_compute.Dispatch(Kernels.PFinish, ThreadCount, ThreadCount, 1);
_previousCameraIntPosition = cameraIntPosition;
}
#endregion
}