Files
beyond/Assets/ThirdParty/FluXY/Scripts/FluxySolver.cs
2024-11-20 15:21:28 +01:00

610 lines
22 KiB
C#

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
/**
\mainpage FluXY documentation
Introduction:
-------------
FluXY is a GPU, grid-based, 2.5D fluid simulator. It's lightweight, fast, robust, and easy to use.
Can be used to simulate fire, smoke, paint, water, trails, and a variety of VFX.
Features:
-------------------
- Uses vanilla vertex/fragment shaders, no compute shader support required.
- Compatible with all rendering pipelines: built-in, URP and HDRP.
- Expand 2D fluids into the 3D realm
- Dynamic Level of Detail (LOD)
- Simulate fire, smoke, ink, water, and other VFX
- Turbulence, pressure, vorticity, buoyancy, external forces...
- Inertial effects
- Lighting fast pressure solver
- Parallel simulation of multiple fluid containers
- Fast support and regular updates
*/
namespace Fluxy
{
[AddComponentMenu("Physics/FluXY/Solver", 800)]
public class FluxySolver : MonoBehaviour
{
private const int MAX_TILES = 17; // 16 + 1 phantom tile.
public enum PressureSolver
{
Separable,
Iterative
}
[Flags]
public enum ReadbackMode
{
None = 0,
Density = 1 << 0,
Velocity = 1 << 1
}
/// <summary>
/// Storage used to store and manage simulation buffers.
/// </summary>
[Header("Storage")]
[Tooltip("Storage used to store and manage simulation buffers.")]
public FluxyStorage storage;
/// <summary>
/// Desired buffer resolution.
/// </summary>
[Tooltip("Desired buffer resolution.")]
[Delayed]
[Min(16)]
public int desiredResolution = 128;
/// <summary>
/// Supersampling used by density buffer. Eg. a value of 4 will use a density buffer that's 4 times the size of the velocity buffer.
/// </summary>
[Tooltip("Supersampling used by density buffer. Eg. a value of 4 will use a density buffer that's 4 times the size of the velocity buffer.")]
[Range(1, 8)]
public int densitySupersampling = 2;
/// <summary>
/// Dispose of this solver's buffers when culled by LOD.
/// </summary>
[Tooltip("Dispose of this solver's buffers when culled by LOD.")]
public bool disposeWhenCulled = false;
/// <summary>
/// Allows this solver's data to be read back to the CPU.
/// </summary>
[Tooltip("Allows this solver's data to be read back to the CPU.")]
public ReadbackMode readable = ReadbackMode.None;
/// <summary>
/// Material used to update fluid simulation.
/// </summary>
[Header("Simulation")]
[Tooltip("Material used to update fluid simulation.")]
public Material simulationMaterial;
/// <summary>
/// Maximum amount of time advanced in a single simulation step.
/// </summary>
[Tooltip("Maximum amount of time advanced in a single simulation step.")]
[Min(0.0001f)]
public float maxTimestep = 0.008f;
/// <summary>
/// Maximum amount of simulation steps taken in a single frame.
/// </summary>
[Tooltip("Maximum amount of simulation steps taken in a single frame.")]
[Min(1)]
public float maxSteps = 4;
/// <summary>
/// Type of pressure solver used: traditional, iterative Jacobi or separable poisson filter.
/// </summary>
[Tooltip("Type of pressure solver used: traditional, iterative Jacobi or separable poisson filter.")]
public PressureSolver pressureSolver = PressureSolver.Separable;
/// <summary>
/// Amount of iterations when the iterative pressure solver is being used.
/// </summary>
[Tooltip("Amount of iterations when the iterative pressure solver is being used.")]
[Range(0,32)]
public int pressureIterations = 3;
private LODGroup lodGroup;
private int visibleLOD;
private bool visible = true;
private List<FluxyContainer> containers = new List<FluxyContainer>();
private int framebufferID = -1;
private bool tilesDirty;
private int[] indices = new int[MAX_TILES];
private Vector4[] rects = new Vector4[MAX_TILES];
private Vector4[] externalForce = new Vector4[MAX_TILES];
private Vector4[] buoyancy = new Vector4[MAX_TILES];
private Vector4[] dissipation = new Vector4[MAX_TILES];
private float[] pressure = new float[MAX_TILES];
private float[] viscosity = new float[MAX_TILES];
private float[] turbulence = new float[MAX_TILES];
private float[] adhesion = new float[MAX_TILES];
private float[] surfaceTension = new float[MAX_TILES];
private Vector4[] wrapmode = new Vector4[MAX_TILES];
private Vector4[] densityFalloff = new Vector4[MAX_TILES];
private Vector4[] offsets = new Vector4[MAX_TILES];
public delegate void SolverCallback(FluxySolver solver);
public event SolverCallback OnStep;
public FluxyStorage.Framebuffer framebuffer
{
get { return storage != null ? storage.GetFramebuffer(framebufferID) : null; }
}
public Texture2D velocityReadbackTexture { get; private set; }
public Texture2D densityReadbackTexture { get; private set; }
private void OnEnable()
{
lodGroup = GetComponent<LODGroup>();
visibleLOD = GetCurrentLOD(Camera.main);
UpdateFramebuffer();
}
private void OnDisable()
{
DisposeOfFramebuffer();
}
private void OnValidate()
{
UpdateFramebuffer();
}
public bool IsFull()
{
return containers.Count >= MAX_TILES - 1;
}
public bool RegisterContainer(FluxyContainer container)
{
if (IsFull())
return false;
if (!containers.Contains(container))
{
containers.Add(container);
tilesDirty = true;
}
return true;
}
public void UnregisterContainer(FluxyContainer container)
{
if (containers.Contains(container))
{
containers.Remove(container);
tilesDirty = true;
}
}
public int GetContainerID(FluxyContainer container)
{
return containers.IndexOf(container);
}
public Vector4 GetContainerUVRect(FluxyContainer container)
{
int index = containers.IndexOf(container);
if (index >= 0)
return rects[index + 1];
return Vector4.zero;
}
private void UpdateFramebuffer()
{
if (!visible)
{
if (disposeWhenCulled)
DisposeOfFramebuffer();
else return;
}
// visible, but not yet created.
else if (framebufferID < 0)
{
// create a framebuffer.
if (storage != null)
framebufferID = storage.RequestFramebuffer(desiredResolution / (visibleLOD + 1), densitySupersampling);
}
// visible and created.
else
{
// update resolution based on LOD:
var fb = framebuffer;
if (fb != null)
{
fb.desiredResolution = desiredResolution / (visibleLOD + 1);
fb.stateSupersampling = densitySupersampling;
storage.ResizeStorage();
}
}
var b = framebuffer;
if (b != null)
{
velocityReadbackTexture = new Texture2D(b.velocityA.width, b.velocityA.height, TextureFormat.RGBAHalf, false);
densityReadbackTexture = new Texture2D(b.stateA.width, b.stateA.height, TextureFormat.RGBAHalf, false);
Color[] resetColorArray = velocityReadbackTexture.GetPixels();
for (int i = 0; i < resetColorArray.Length; i++)
resetColorArray[i] = new Color(0,0,0,0);
velocityReadbackTexture.SetPixels(resetColorArray);
velocityReadbackTexture.Apply();
resetColorArray = densityReadbackTexture.GetPixels();
for (int i = 0; i < resetColorArray.Length; i++)
resetColorArray[i] = new Color(0, 0, 0, 0);
densityReadbackTexture.SetPixels(resetColorArray);
densityReadbackTexture.Apply();
}
}
private void DisposeOfFramebuffer()
{
if (storage != null && framebufferID >= 0)
{
storage.DisposeFramebuffer(framebufferID);
framebufferID = -1;
}
Destroy(velocityReadbackTexture);
Destroy(densityReadbackTexture);
}
private int GetCurrentLOD(Camera cam = null)
{
visible = true;
if (lodGroup == null)
return 0;
var distance = (transform.position - cam.transform.position).magnitude;
float size = 1;
var relativeHeight = FluxyUtils.RelativeScreenHeight(cam, distance / QualitySettings.lodBias, size);
var lods = lodGroup.GetLODs();
for (var i = 0; i < lods.Length; i++)
{
if (relativeHeight >= lods[i].screenRelativeTransitionHeight)
return i;
}
visible = false;
return lodGroup.lodCount;
}
private void UpdateLOD()
{
int newLOD = GetCurrentLOD(Camera.main);
if (visibleLOD != newLOD)
{
visibleLOD = newLOD;
UpdateFramebuffer();
}
}
protected virtual void SimulationStep(FluxyStorage.Framebuffer fb, float deltaTime)
{
if (fb == null)
return;
// callback for custom stuff:
OnStep?.Invoke(this);
fb.velocityA.filterMode = FilterMode.Point;
fb.stateA.filterMode = FilterMode.Point;
simulationMaterial.SetFloat("_DeltaTime", deltaTime);
simulationMaterial.SetTexture("_Velocity", fb.velocityA);
simulationMaterial.SetTexture("_State", fb.stateB);
// advection (state):
Graphics.Blit(fb.stateA, fb.stateB, simulationMaterial, 0);
// advection (velocity):
Graphics.Blit(fb.velocityA, fb.velocityB, simulationMaterial, 1);
// dissipation:
Graphics.Blit(fb.stateB, fb.stateA, simulationMaterial, 2);
// calculate curl:
Graphics.Blit(fb.velocityB, fb.velocityA, simulationMaterial, 3);
// density gradient:
Graphics.Blit(fb.stateA, fb.stateB, simulationMaterial, 4);
// apply external forces and project velocity to surface:
fb.stateB.filterMode = FilterMode.Bilinear;
UpdateExternalForces(fb.velocityA, fb.velocityB);
fb.stateB.filterMode = FilterMode.Point;
// calculate divergence:
Graphics.Blit(fb.velocityB, fb.velocityA, simulationMaterial, 5);
// calculate pressure:
if (pressureSolver == PressureSolver.Separable)
{
Graphics.Blit(fb.velocityA, fb.velocityB, simulationMaterial, 11);
simulationMaterial.SetVector("axis", Vector2.right);
Graphics.Blit(fb.velocityB, fb.velocityA, simulationMaterial, 6);
simulationMaterial.SetVector("axis", Vector2.up);
Graphics.Blit(fb.velocityA, fb.velocityB, simulationMaterial, 6);
}
else
{
for (int i = 0; i < pressureIterations; ++i)
{
Graphics.Blit(fb.velocityA, fb.velocityB, simulationMaterial, 10);
Graphics.Blit(fb.velocityB, fb.velocityA, simulationMaterial, 10);
}
Graphics.Blit(fb.velocityA, fb.velocityB);
}
// subtract pressure gradient from velocity field:
Graphics.Blit(fb.velocityB, fb.velocityA, simulationMaterial, 7);
fb.velocityA.filterMode = FilterMode.Bilinear;
fb.stateA.filterMode = FilterMode.Bilinear;
}
private void UpdateExternalForces(RenderTexture source, RenderTexture dest)
{
RenderTexture old = RenderTexture.active;
RenderTexture.active = dest;
// Clear dest before copying data from source.
// This ensures velocity is set to zero outside of the mapped texture regions.
GL.Clear(false, true, Color.clear);
simulationMaterial.SetTexture("_MainTex", source);
// for each container, draw directly into the velocity map:
for (int i = 0; i < containers.Count; ++i)
{
int tile = i + 1;
int c = indices[tile];
simulationMaterial.SetInt("_TileIndex", tile);
simulationMaterial.SetFloat("_NormalScale", containers[c].normalScale);
simulationMaterial.SetVector("_NormalTiling", containers[c].normalTiling);
simulationMaterial.SetTexture("_Normals", containers[c].surfaceNormals);
// must call SetPass() *after* setting material properties:
if (simulationMaterial.SetPass(12))
{
GL.PushMatrix();
GL.LoadProjectionMatrix(Matrix4x4.Ortho(0, 1, 0, 1, -1, 1));
Graphics.DrawMeshNow(containers[c].containerMesh, containers[c].transform.localToWorldMatrix);
GL.PopMatrix();
}
}
RenderTexture.active = old;
}
public void ClearContainer(int id)
{
var fb = framebuffer;
if (fb != null && simulationMaterial != null && id >= 0 && id < indices.Length)
{
UpdateTileData();
int tile = id + 1;
int c = indices[tile];
simulationMaterial.SetInt("_TileIndex", tile);
simulationMaterial.SetColor("_ClearColor", containers[c].clearColor);
Graphics.Blit(containers[c].clearTexture, fb.stateA, simulationMaterial, 9);
}
}
public void UpdateTileData()
{
if (tilesDirty)
{
// phantom rect should span the entire UV space from -1 to 2.
rects[0] = new Vector4(-1, -1, 3, 3);
for (int i = 0; i < containers.Count; ++i)
{
rects[i+1] = new Vector4(0, 0, containers[i].size.x * 1024, containers[i].size.y * 1024);
indices[i+1] = i;
}
var boundsSize = RectPacking.Pack(rects, indices, 1, containers.Count, 0);
// normalize rect coordinates:
float size = Mathf.Max(boundsSize.x, boundsSize.y);
for (int i = 0; i < containers.Count; ++i)
{
rects[i + 1] /= size;
float res = FluxyStorage.minFramebufferSize;
rects[i + 1].x = Mathf.FloorToInt(rects[i + 1].x * res) / res;
rects[i + 1].y = Mathf.FloorToInt(rects[i + 1].y * res) / res;
rects[i + 1].z = Mathf.FloorToInt(rects[i + 1].z * res) / res;
rects[i + 1].w = Mathf.FloorToInt(rects[i + 1].w * res) / res;
}
Shader.SetGlobalVectorArray("_TileData", rects);
tilesDirty = false;
}
}
private void UpdateContainerTransforms(FluxyStorage.Framebuffer fb)
{
for (int i = 0; i < containers.Count; ++i)
{
int tile = i + 1;
int c = indices[tile];
containers[c].UpdateTransform();
containers[c].UpdateMaterial(tile, fb);
}
}
private void UpdateContainers(FluxyStorage.Framebuffer fb, float deltaTime)
{
if (fb == null)
return;
for (int i = 0; i < containers.Count; ++i)
{
int tile = i + 1;
int c = indices[tile];
simulationMaterial.SetInt("_TileIndex", tile);
Graphics.Blit(null, fb.tileID, simulationMaterial, 8);
dissipation[tile] = containers[c].dissipation;
turbulence[tile] = containers[c].turbulence;
adhesion[tile] = containers[c].adhesion;
surfaceTension[tile] = containers[c].surfaceTension;
pressure[tile] = containers[c].pressure;
viscosity[tile] = Mathf.Pow(1 - Mathf.Clamp01(containers[c].viscosity), deltaTime);
wrapmode[tile] = containers[c].boundaries;
densityFalloff[tile] = containers[c].edgeFalloff;
var acceleration = containers[c].UpdateVelocityAndGetAcceleration();
externalForce[tile] = containers[c].gravity + containers[c].externalForce - acceleration * containers[c].accelerationScale;
buoyancy[tile] = containers[c].TransformWorldVectorToUVSpace(Vector3.up, rects[tile]) * containers[c].buoyancy;
offsets[tile] = containers[c].TransformWorldVectorToUVSpace(containers[c].velocity * deltaTime, rects[tile]) * (1 - containers[c].velocityScale) + (Vector3)containers[c].positionOffset * deltaTime;
}
simulationMaterial.SetFloatArray("_Pressure", pressure);
simulationMaterial.SetFloatArray("_Viscosity", viscosity);
simulationMaterial.SetFloatArray("_VortConf", turbulence);
simulationMaterial.SetFloatArray("_Adhesion", adhesion);
simulationMaterial.SetFloatArray("_SurfaceTension", surfaceTension);
simulationMaterial.SetVectorArray("_Dissipation", dissipation);
simulationMaterial.SetVectorArray("_ExternalForce", externalForce);
simulationMaterial.SetVectorArray("_Buoyancy", buoyancy);
simulationMaterial.SetVectorArray("_WrapMode", wrapmode);
simulationMaterial.SetVectorArray("_EdgeFalloff", densityFalloff);
simulationMaterial.SetVectorArray("_Offsets", offsets);
}
private void Splat(FluxyStorage.Framebuffer fb)
{
if (fb == null || simulationMaterial == null)
return;
Shader.SetGlobalTexture("_TileID", fb.tileID);
for (int i = 0; i < containers.Count; ++i)
{
int tile = i + 1;
int c = indices[tile];
// container's target list:
for (int j = 0; j < containers[c].targets.Length; ++j)
if (containers[c].targets[j] != null)
containers[c].targets[j].Splat(containers[c], fb, tile, rects[tile]);
// see if the container has a target provider, then retrieve additional targets.
if (containers[c].TryGetComponent(out FluxyTargetProvider provider))
{
var targets = provider.GetTargets();
for (int j = 0; j < targets.Count; ++j)
if (targets[j] != null)
targets[j].Splat(containers[c], fb, tile, rects[tile]);
}
}
}
private void VelocityReadback(FluxyStorage.Framebuffer fb)
{
if (velocityReadbackTexture != null)
AsyncGPUReadback.Request(fb.velocityA, 0, TextureFormat.RGBAHalf, (AsyncGPUReadbackRequest request) =>
{
if (request.hasError)
Debug.LogError("GPU readback error.");
else if (velocityReadbackTexture != null)
{
velocityReadbackTexture.LoadRawTextureData(request.GetData<float>());
velocityReadbackTexture.Apply();
}
});
}
private void DensityReadback(FluxyStorage.Framebuffer fb)
{
if (densityReadbackTexture != null)
AsyncGPUReadback.Request(fb.stateA, 0, TextureFormat.RGBAHalf, (AsyncGPUReadbackRequest request) =>
{
if (request.hasError)
Debug.LogError("GPU readback error.");
else if (densityReadbackTexture != null)
{
densityReadbackTexture.LoadRawTextureData(request.GetData<float>());
densityReadbackTexture.Apply();
}
});
}
public void UpdateSolver(float deltaTime)
{
if (storage != null && deltaTime > 0)
{
UpdateLOD();
UpdateTileData();
var fb = framebuffer;
UpdateContainerTransforms(fb);
if (visible && simulationMaterial != null)
{
Splat(fb);
// semi-fixed timestep: if the delta is larger than the timestep, chop it up.
int steps = 0;
while (deltaTime > 0 && steps++ < maxSteps)
{
float timestep = Mathf.Min(deltaTime, maxTimestep);
deltaTime -= timestep;
UpdateContainers(fb, timestep);
SimulationStep(fb, timestep);
}
if ((readable & ReadbackMode.Density) != 0)
DensityReadback(fb);
if ((readable & ReadbackMode.Velocity) != 0)
VelocityReadback(fb);
}
}
}
protected virtual void LateUpdate()
{
UpdateSolver(Time.deltaTime);
}
}
}