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);
}
}
}
}