using System; using UnityEngine; using UnityEngine.Rendering; namespace Fluxy { [AddComponentMenu("Physics/FluXY/Container", 800)] [ExecuteInEditMode] [ExecutionOrder(9998)] [RequireComponent(typeof(MeshFilter))] [RequireComponent(typeof(MeshRenderer))] public class FluxyContainer : MonoBehaviour { [Serializable] public struct BoundaryConditions { public enum BoundaryType { Open, Solid, Periodic }; public BoundaryType horizontalBoundary; public BoundaryType verticalBoundary; public static implicit operator Vector4(BoundaryConditions b) { return new Vector4(b.horizontalBoundary == BoundaryType.Periodic ? 1 : 0, b.verticalBoundary == BoundaryType.Periodic ? 1 : 0, b.horizontalBoundary == BoundaryType.Solid ? 1 : 0, b.verticalBoundary == BoundaryType.Solid ? 1 : 0); } } [Serializable] public struct EdgeFalloff { [Min(0)] public float densityEdgeWidth; [Min(0)] public float densityFalloffRate; [Min(0)] public float velocityEdgeWidth; [Min(0)] public float velocityFalloffRate; public static implicit operator Vector4(EdgeFalloff d) { return new Vector4(d.densityEdgeWidth, d.densityFalloffRate, d.velocityEdgeWidth, d.velocityFalloffRate); } } public enum LookAtMode { LookAt, CopyOrientation } public enum ContainerShape { Plane, Volume, Custom } /// /// Shape of the container: can be flat, can be a volume, or can be a custom mesh. /// [Tooltip("Shape of the container: can be flat, can be a volume, or can be a custom mesh.")] public ContainerShape containerShape = ContainerShape.Plane; /// /// Amount of subdivisions in the plane mesh. /// [Tooltip("Amount of subdivisions in the plane mesh.")] public Vector2Int subdivisions = Vector2Int.one; /// /// Custom mesh used by the container. If null, a subdivided plane will be used instead. /// [Tooltip("Custom mesh used by the container. If null, a subdivided plane will be used instead.")] public Mesh customMesh = null; /// /// Size of the container in local space. /// [Tooltip("Size of the container in local space.")] public Vector3 size = Vector3.one; /// /// Method using for facing the lookAt transform: look to it, or copy its orientation. /// [Tooltip("Method using for facing the lookAt transform: look to it, or copy its orientation.")] public LookAtMode lookAtMode = LookAtMode.LookAt; /// /// Transform that the container should be facing at all times. If unused, you can set the container's rotation manually. /// [Tooltip("Transform that the container should be facing at all times. If unused, you can set the container's rotation manually.")] public Transform lookAt; /// /// Transform used as raycasting origin for splatting targets onto this container. If unused, simple planar projection will be used instead. /// [Tooltip("Transform used as raycasting origin for splatting targets onto this container. If unused, simple planar projection will be used instead.")] public Transform projectFrom; /// /// Texture used for clearing the container's density buffer. /// [Tooltip("Texture used for clearing the container's density buffer.")] public Texture2D clearTexture; /// /// Color used to tint the clear texture. /// [Tooltip("Color used to tint the clear texture.")] public Color clearColor; /// /// Normal map used to determine container surface normal. /// [Tooltip("Normal map used to determine container surface normal.")] public Texture2D surfaceNormals; /// /// Tiling of surface normals. /// [Tooltip("Tiling of surface normal map.")] public Vector2 normalTiling = Vector2.one; /// /// Intensity of the surface normals. /// [Range(0, 1)] [Tooltip("Intensity of the surface normals.")] public float normalScale = 1; /// /// Falloff controls for density and velocity near container edges. /// [Tooltip("Falloff controls for density and velocity near container edges.")] public EdgeFalloff edgeFalloff; /// /// Falloff controls for density and velocity near container edges. /// [Tooltip("Falloff controls for density and velocity near container edges.")] public BoundaryConditions boundaries; /// /// Scale (0%-100%) of container's velocity. If set to zero, containers will be regarded as static. /// [Range(0, 1)] [Tooltip("Scale (0%-100%) of container's velocity. If set to zero, containers will be regarded as static.")] public float velocityScale = 1; /// /// Scale (0%-100%) of container's acceleration. This controls how much world-space inertia affects the fluid. /// [Range(0, 1)] [Tooltip("Scale (0%-100%) of container's acceleration. This controls how much world-space inertia affects the fluid.")] public float accelerationScale = 1; /// /// Local-space positional offset applied to the fluid. /// [Tooltip("Local-space positional offset applied to the fluid.")] public Vector2 positionOffset = Vector2.zero; /// /// World-space gravity applied to the fluid. /// [Tooltip("World-space gravity applied to the fluid.")] public Vector3 gravity = Vector3.zero; /// /// World-space external force applied to the fluid. /// [Tooltip("World-space external force applied to the fluid.")] public Vector3 externalForce = Vector3.zero; /// /// Lightsource used for volume rendering. /// [Tooltip("Lightsource used for volume rendering.")] public Light lightSource = null; /// /// List of targets that should be splatted onto this container. /// [Tooltip("List of targets that should be splatted onto this container.")] #if UNITY_2020_2_OR_NEWER [NonReorderable] #endif public FluxyTarget[] targets; /// /// Scales fluid pressure. /// [Range(0, 1)] [Tooltip("Scales fluid pressure.")] public float pressure = 1; /// /// Scales fluid viscosity. /// [Range(0, 1)] [Tooltip("Scales fluid viscosity.")] public float viscosity = 0; /// /// Amount of turbulence (vorticity) in the fluid. /// [Tooltip("Amount of turbulence (vorticity) in the fluid.")] public float turbulence = 5; /// /// Amount of adhesion to the container's surface. /// [Range(0, 1)] [Tooltip("Amount of adhesion to the container's surface.")] public float adhesion = 0; /// /// Amount of surface tension. Higher values will make the fluid tend to form round shapes. /// [Range(0, 1)] [Tooltip("Amount of surface tension. Higher values will make the fluid tend to form round shapes.")] public float surfaceTension = 0; /// /// Upwards buoyant force applied to fluid. It is directly proportional to the contents the density buffer's alpha channel (temperature). /// [Tooltip("Upwards buoyant force applied to fluid. It is directly proportional to the contents the density buffer's alpha channel (temperature).")] public float buoyancy = 1; /// /// Amount of density dissipated per second. /// [Tooltip("Amount of density dissipated per second.")] public Vector4 dissipation = Vector4.zero; // rate at which state channels decrease. [SerializeField] [HideInInspector] private FluxySolver m_Solver; private Renderer m_Renderer; private MeshFilter m_Filter; private MaterialPropertyBlock propertyBlock; private Vector3 m_Velocity; private Vector3 m_AngularVelocity; private Vector3 oldPosition; private Quaternion oldRotation; private Vector3 oldVelocity; protected Mesh proceduralMesh; protected Vector3[] vertices; protected Vector3[] normals; protected Vector4[] tangents; protected Vector2[] uvs; protected int[] triangles; public delegate void ContainerCallback(FluxyContainer container); public event ContainerCallback OnFrameEnded; public FluxySolver solver { get { return m_Solver; } set { m_Solver = value; #if UNITY_EDITOR if (Application.isPlaying) #endif { SetSolver(m_Solver, true); } } } public Vector3 velocity { get { return m_Velocity; } } public Vector3 angularVelocity { get { return m_AngularVelocity; } } public Renderer containerRenderer { get { return m_Renderer; } } public Mesh containerMesh { get { return customMesh != null ? customMesh : proceduralMesh; } } protected virtual void OnEnable() { m_Renderer = GetComponent(); m_Filter = GetComponent(); propertyBlock = new MaterialPropertyBlock(); UpdateContainerShape(); SetSolver(m_Solver, false); oldPosition = transform.position; oldRotation = transform.rotation; } protected virtual void OnDisable() { DestroyImmediate(proceduralMesh); SetSolver(null, false); } protected virtual void Start() { if (Application.isPlaying) Clear(); } protected virtual void OnValidate() { subdivisions.x = Mathf.Max(1, subdivisions.x); subdivisions.y = Mathf.Max(1, subdivisions.y); } public virtual void UpdateContainerShape() { switch (containerShape) { case ContainerShape.Plane: BuildPlaneMesh(); break; case ContainerShape.Volume: BuildVolumeMesh(); break; case ContainerShape.Custom: BuildCustomMesh(); break; } } protected void BuildPlaneMesh() { if (proceduralMesh == null) { proceduralMesh = new Mesh(); proceduralMesh.name = "FluidContainer"; if (m_Filter != null) m_Filter.sharedMesh = proceduralMesh; } // create a new mesh: proceduralMesh.Clear(); subdivisions.x = Mathf.Max(1, subdivisions.x); subdivisions.y = Mathf.Max(1, subdivisions.y); Vector2 quadSize = new Vector2(1.0f / subdivisions.x, 1.0f / subdivisions.y); int vertexCount = (subdivisions.x + 1) * (subdivisions.y + 1); int triangleCount = subdivisions.x * subdivisions.y * 2; if (vertexCount > 65535) proceduralMesh.indexFormat = IndexFormat.UInt32; else proceduralMesh.indexFormat = IndexFormat.UInt16; vertices = new Vector3[vertexCount]; normals = new Vector3[vertexCount]; tangents = new Vector4[vertexCount]; uvs = new Vector2[vertexCount]; triangles = new int[triangleCount * 3]; // generate vertices: // for each row: for (int y = 0; y < subdivisions.y + 1; ++y) { // for each column: for (int x = 0; x < subdivisions.x + 1; ++x) { int v = y * (subdivisions.x + 1) + x; vertices[v] = new Vector3((quadSize.x * x - 0.5f) * size.x, (quadSize.y * y - 0.5f) * size.y, 0); normals[v] = -Vector3.forward; tangents[v] = new Vector4(1, 0, 0, -1); uvs[v] = new Vector3(x / (float)subdivisions.x, y / (float)subdivisions.y); } } // generate triangle faces: // for each row: for (int y = 0; y < subdivisions.y; ++y) { // for each column: for (int x = 0; x < subdivisions.x; ++x) { int face = (y * (subdivisions.x + 1) + x); int t = (y * subdivisions.x + x) * 6; triangles[t] = face + subdivisions.x + 1; triangles[t + 1] = face + 1; triangles[t + 2] = face; triangles[t + 3] = face + subdivisions.x + 2; triangles[t + 4] = face + 1; triangles[t + 5] = face + subdivisions.x + 1; } } proceduralMesh.SetVertices(vertices); proceduralMesh.SetNormals(normals); proceduralMesh.SetTangents(tangents); proceduralMesh.SetUVs(0, uvs); proceduralMesh.SetIndices(triangles, MeshTopology.Triangles, 0); proceduralMesh.RecalculateNormals(); } protected void BuildVolumeMesh() { if (proceduralMesh == null) { proceduralMesh = new Mesh(); proceduralMesh.name = "FluidContainer"; GetComponent().sharedMesh = proceduralMesh; } // create a new mesh: proceduralMesh.Clear(); int vertexCount = 24; int triangleCount = 8; proceduralMesh.indexFormat = IndexFormat.UInt32; tangents = null; uvs = new Vector2[vertexCount]; triangles = new int[triangleCount * 3]; Vector3[] c = new Vector3[8]; float length = size.x; float width = size.y; float height = size.z; c[0] = new Vector3(-length * .5f, -width * .5f, height * .5f); c[1] = new Vector3(length * .5f, -width * .5f, height * .5f); c[2] = new Vector3(length * .5f, -width * .5f, -height * .5f); c[3] = new Vector3(-length * .5f, -width * .5f, -height * .5f); c[4] = new Vector3(-length * .5f, width * .5f, height * .5f); c[5] = new Vector3(length * .5f, width * .5f, height * .5f); c[6] = new Vector3(length * .5f, width * .5f, -height * .5f); c[7] = new Vector3(-length * .5f, width * .5f, -height * .5f); vertices = new Vector3[] { c[0], c[1], c[2], c[3], // Bottom c[7], c[4], c[0], c[3], // Left c[4], c[5], c[1], c[0], // Front c[6], c[7], c[3], c[2], // Back c[5], c[6], c[2], c[1], // Right c[7], c[6], c[5], c[4] // Top }; Vector3 up = -Vector3.up; Vector3 down = -Vector3.down; Vector3 forward = -Vector3.forward; Vector3 back = -Vector3.back; Vector3 left = -Vector3.left; Vector3 right = -Vector3.right; normals = new Vector3[] { down, down, down, down, // Bottom left, left, left, left, // Left forward, forward, forward, forward, // Front back, back, back, back, // Back right, right, right, right, // Right up, up, up, up // Top }; Vector2 uv00 = new Vector2(0f, 0f); Vector2 uv10 = new Vector2(1f, 0f); Vector2 uv01 = new Vector2(0f, 1f); Vector2 uv11 = new Vector2(1f, 1f); uvs = new Vector2[] { uv11, uv01, uv00, uv10, // Bottom uv11, uv01, uv00, uv10, // Left uv11, uv01, uv00, uv10, // Front uv11, uv01, uv00, uv10, // Back uv11, uv01, uv00, uv10, // Right uv11, uv01, uv00, uv10 // Top }; triangles = new int[] { 0, 1, 3, 1, 2, 3, // Bottom 4, 5, 7, 5, 6, 7, // Left 8, 9, 11, 9, 10, 11, // Front 12, 13, 15, 13, 14, 15, // Back 16, 17, 19, 17, 18, 19, // Right 20, 21, 23, 21, 22, 23, // Top }; proceduralMesh.SetVertices(vertices); proceduralMesh.SetNormals(normals); proceduralMesh.SetUVs(0, uvs); proceduralMesh.SetIndices(triangles, MeshTopology.Triangles, 0); } protected void BuildCustomMesh() { if (proceduralMesh != null) DestroyImmediate(proceduralMesh); if (customMesh != null) GetComponent().sharedMesh = customMesh; } private void SetSolver(FluxySolver newSolver, bool setMember) { if (m_Solver != null) m_Solver.UnregisterContainer(this); if (setMember) m_Solver = newSolver; if (m_Solver != null && isActiveAndEnabled) m_Solver.RegisterContainer(this); } public virtual void Clear() { if (m_Solver != null) { int id = m_Solver.GetContainerID(this); m_Solver.ClearContainer(id); } } public Vector3 TransformWorldVectorToUVSpace(in Vector3 vector, in Vector4 uvRect) { if (Mathf.Abs(size.x) < FluxyUtils.epsilon || Mathf.Abs(size.y) < FluxyUtils.epsilon) return Vector3.zero; var v = transform.InverseTransformVector(vector); v.x *= uvRect.z / size.x; v.y *= uvRect.w / size.y; return v; } public Vector3 TransformUVVectorToWorldSpace(in Vector3 vector, in Vector4 uvRect) { if (Mathf.Abs(uvRect.z) < FluxyUtils.epsilon || Mathf.Abs(uvRect.w) < FluxyUtils.epsilon) return Vector3.zero; var v = vector; v.x *= size.x / uvRect.z; v.y *= size.y / uvRect.w; return transform.TransformVector(v); } public Vector3 TransformWorldPointToUVSpace(in Vector3 point, in Vector4 uvRect) { var v = transform.InverseTransformPoint(point); v.x += size.x * 0.5f; v.y += size.y * 0.5f; v.x *= uvRect.z / size.x; v.y *= uvRect.w / size.y; v.x += uvRect.x; v.y += uvRect.y; return v; } public virtual void UpdateTransform() { if (lookAt != null) { if (lookAtMode == LookAtMode.LookAt) transform.rotation = Quaternion.LookRotation(transform.position - lookAt.position, Vector3.up); else transform.rotation = Quaternion.LookRotation(lookAt.forward, lookAt.up); } Shader.SetGlobalVector("_ContainerSize", size); } public virtual void UpdateMaterial(int tile, FluxyStorage.Framebuffer fb) { if (m_Renderer == null || m_Renderer.sharedMaterial == null || fb == null) return; containerRenderer.GetPropertyBlock(propertyBlock); propertyBlock.SetInt("_TileIndex", tile); propertyBlock.SetTexture("_MainTex", fb.stateA); propertyBlock.SetTexture("_Velocity", fb.velocityA); if (lightSource != null && lightSource.isActiveAndEnabled && lightSource.type == LightType.Directional) { m_Renderer.sharedMaterial.EnableKeyword("_LIGHTSOURCE_DIRECTIONAL"); m_Renderer.sharedMaterial.DisableKeyword("_LIGHTSOURCE_POINT"); m_Renderer.sharedMaterial.DisableKeyword("_LIGHTSOURCE_NONE"); propertyBlock.SetVector("_LightVector", transform.InverseTransformDirection(lightSource.transform.forward)); propertyBlock.SetVector("_LightColor", lightSource.color * lightSource.intensity); } else if (lightSource != null && lightSource.isActiveAndEnabled && lightSource.type == LightType.Point) { m_Renderer.sharedMaterial.DisableKeyword("_LIGHTSOURCE_DIRECTIONAL"); m_Renderer.sharedMaterial.EnableKeyword("_LIGHTSOURCE_POINT"); m_Renderer.sharedMaterial.DisableKeyword("_LIGHTSOURCE_NONE"); Vector4 lightVec = transform.InverseTransformPoint(lightSource.transform.position); lightVec.w = 1f / Mathf.Max(lightSource.range * lightSource.range, 0.00001f); propertyBlock.SetVector("_LightVector", lightVec); propertyBlock.SetVector("_LightColor", lightSource.color * lightSource.intensity); } else { m_Renderer.sharedMaterial.DisableKeyword("_LIGHTSOURCE_DIRECTIONAL"); m_Renderer.sharedMaterial.DisableKeyword("_LIGHTSOURCE_POINT"); m_Renderer.sharedMaterial.EnableKeyword("_LIGHTSOURCE_NONE"); propertyBlock.SetVector("_LightColor", Color.white); } containerRenderer.SetPropertyBlock(propertyBlock); } public virtual Vector4 ProjectTarget(in Vector3 targetPosition, Vector2 projectionSize, float aspectRatio, bool scaleWithDistance = true) { var origin = GetProjectionOrigin(targetPosition); Ray ray = new Ray(origin, targetPosition - origin); if (TryGetComponent(out MeshCollider meshCollider) && meshCollider.enabled) { if (meshCollider.Raycast(ray, out RaycastHit hit, Mathf.Infinity)) { float scale = 1; if (scaleWithDistance) { ray = new Ray(origin, (targetPosition + transform.right * 0.01f) - origin); if (meshCollider.Raycast(ray, out RaycastHit secondHit, Mathf.Infinity)) scale = Vector3.Distance(hit.point, secondHit.point) / 0.01f; } return new Vector4(hit.textureCoord.x - 0.5f, hit.textureCoord.y - 0.5f, projectionSize.x * scale * aspectRatio, projectionSize.y * scale); } } else { Plane p = new Plane(transform.forward, transform.position); if (p.Raycast(ray, out float dist)) { var point = ray.GetPoint(dist); var local = transform.InverseTransformPoint(point) / (Vector2)size; float scale = 1; if (scaleWithDistance) { ray = new Ray(origin, (targetPosition + transform.right) - origin); if (p.Raycast(ray, out float dist2)) { var point2 = ray.GetPoint(dist2); var local2 = transform.InverseTransformPoint(point2) / (Vector2)size; scale = Vector2.Distance(local, local2); } } return new Vector4(local.x, local.y, projectionSize.x * scale * aspectRatio, projectionSize.y * scale); } } return Vector4.zero; } private Vector3 GetProjectionOrigin(in Vector3 targetPosition) { // get projection origin position: if (projectFrom != null) return projectFrom.position; else return targetPosition + transform.forward; } public Vector3 UpdateVelocityAndGetAcceleration() { m_Velocity = (transform.position - oldPosition) / Time.deltaTime; Quaternion rotationDelta = transform.rotation * Quaternion.Inverse(oldRotation); m_AngularVelocity = new Vector3(rotationDelta.x, rotationDelta.y, rotationDelta.z) * 2.0f / Time.deltaTime; return (m_Velocity - oldVelocity) / Time.deltaTime; } private void ResetVelocityAndAcceleration() { oldVelocity = m_Velocity; oldRotation = transform.rotation; oldPosition = transform.position; } protected virtual void LateUpdate() { ResetVelocityAndAcceleration(); OnFrameEnded?.Invoke(this); } public Vector3 GetVelocityAt(Vector3 worldPosition) { if (solver != null && (solver.readable & FluxySolver.ReadbackMode.Velocity) != 0) { var fb = solver.framebuffer; if (fb != null) { var rect = solver.GetContainerUVRect(this); var uv = TransformWorldPointToUVSpace(worldPosition, rect); var color = solver.velocityReadbackTexture.GetPixelBilinear(uv.x, uv.y); return TransformUVVectorToWorldSpace(new Vector3(color.r, color.g, 0), rect); } } return Vector3.zero; } public Vector4 GetDensityAt(Vector3 worldPosition) { if (solver != null && (solver.readable & FluxySolver.ReadbackMode.Density) != 0) { var fb = solver.framebuffer; if (fb != null) { var rect = solver.GetContainerUVRect(this); var uv = TransformWorldPointToUVSpace(worldPosition, rect); var color = solver.densityReadbackTexture.GetPixelBilinear(uv.x, uv.y); // no need to transform to world space (unlike velocity vectors) since densities are scalar values. return color; } } return Vector4.zero; } protected virtual void OnDrawGizmosSelected() { if (customMesh == null) { Gizmos.matrix = transform.localToWorldMatrix; Gizmos.DrawWireCube(Vector3.zero, size); } } } }