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 } /// /// Storage used to store and manage simulation buffers. /// [Header("Storage")] [Tooltip("Storage used to store and manage simulation buffers.")] public FluxyStorage storage; /// /// Desired buffer resolution. /// [Tooltip("Desired buffer resolution.")] [Delayed] [Min(16)] public int desiredResolution = 128; /// /// 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. /// [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; /// /// Dispose of this solver's buffers when culled by LOD. /// [Tooltip("Dispose of this solver's buffers when culled by LOD.")] public bool disposeWhenCulled = false; /// /// Allows this solver's data to be read back to the CPU. /// [Tooltip("Allows this solver's data to be read back to the CPU.")] public ReadbackMode readable = ReadbackMode.None; /// /// Material used to update fluid simulation. /// [Header("Simulation")] [Tooltip("Material used to update fluid simulation.")] public Material simulationMaterial; /// /// Maximum amount of time advanced in a single simulation step. /// [Tooltip("Maximum amount of time advanced in a single simulation step.")] [Min(0.0001f)] public float maxTimestep = 0.008f; /// /// Maximum amount of simulation steps taken in a single frame. /// [Tooltip("Maximum amount of simulation steps taken in a single frame.")] [Min(1)] public float maxSteps = 4; /// /// Type of pressure solver used: traditional, iterative Jacobi or separable poisson filter. /// [Tooltip("Type of pressure solver used: traditional, iterative Jacobi or separable poisson filter.")] public PressureSolver pressureSolver = PressureSolver.Separable; /// /// Amount of iterations when the iterative pressure solver is being used. /// [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 containers = new List(); 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(); 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(); } } simulationMaterial.SetTexture("_MainTex", null); 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()); 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()); 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); } } }