Particle diffusion

Quoting wikipedia: "diffusion is the net movement of anything, driven by a gradient in concentration". Most typical example is heat diffusion: Imagine a swimming pool filled with cold water. There's a nearby building projecting shadow over it, so that sun only reaches half of the pool. Intuition tells us that there won't be an instant transition between scalding hot and freezing cold water in the middle of the pool. Instead heat will gradually travel (diffuse) trough water and after enough time, the entire pool will be warm.

Diffusion is also the reason why frying pans and pots have protective handles: even though only their bottom is in contact with a heat source, heat diffuses and their entire surface becomes too hot to touch after a while.

In Obi Fluid each particle has 4 floating point values that you can assign any value to, and that diffusion will be applied to during simulation. These values don't have any pre-defined meaning, you can interpret this data as colors, temperature, or any other property. The 4 diffusion channels are exposed as Diffusion Data in the blueprint (see fluid blueprints) and as the solver.userData array in the C# particle API.

During simulation, particles closer than the fluid's smoothing radius will average their 4 diffusion values at a rate defined by the blueprint's diffusion property.

Two particles get closer than their smoothing radius, and their colors gradually average out due to diffusion.

Example: color mixing

Let's say you want fluids of different colors to mix together. One possible approach is to use only one diffusion channel, and interpolate between two colors using the value of this channel. This will only let us mix two colors, but allows us to perform this mixing / interpolation in fancy ways (e.g. using gradients). This is the approach used by the included "FluidMixing" sample scene.

A more general method is to use 3 of the 4 diffusion channels to store color data (red, green and blue, the 4th channel will always be 1) and let diffusion average them. This will let us mix any number of differently colored fluids, in this case we will go with just 2 but this method generalizes to 3, 4, or more.

Let's create 2 fluid blueprints, and initalize their diffusion data to red (1,0,0,1) and yellow (1,1,0,1). Also, we'll set the diffusion rate to a small value, like 0.05. When mixing them, we will get the average of both colors: orange (1,0.5,0,1).

Red fluid
Yellow fluid

Now we need to map the diffusion data in the solver.userData array to particle color. We can do this in LateUpdate(), right before rendering the fluid:

    void LateUpdate()
    {
        for (int i = 0; i < solver.userData.count; ++i)
            solver.colors[i] = solver.userData[i];
    }
								

You can write a component specifically for this, or add this to an existing component you're using. Note all you need is a reference to a ObiSolver.

Once this is done, we place a couple emitters in our scene and assign the red blueprint and the yellow blueprint to them. This is the result:

You can see how color mixes where both fluids meet, giving rise to a nice color gradient. Stirring the fluid would yield a more uniform mix, as more particles come into close proximity of each other.

Example: mapping temperature to viscosity

We could make use of collision callbacks to change the diffusion data of particles in contact with a specific collider, then let diffusion propagate this value to nearby particles.

This is a good way to model heat diffusion: we use one diffusion channel to store temperature, and set a high temperature when particles come into contact with a collider. We can then map temperature to viscosity to get thinner, runny fluid when it's hot, and thicker, more viscous fluid when it's cold. You can see this in action in the included "Raclette" sample scene:

We'll use the first diffusion channel to store temperature. First, we need to subscribe to collision callbacks. Every frame we will check for contacts between colliders and particles, and increase (or decrease) their temperature depending on which collider they are in contact with:

using UnityEngine;
using Obi;

[RequireComponent(typeof(ObiSolver))]
public class Melt : MonoBehaviour {

	public float heat = 0.1f;
	public float cooling = 0.1f;
	public ObiCollider hotCollider = null;
	public ObiCollider coldCollider = null;

 	private ObiSolver solver;

	void Awake(){
		solver = GetComponent<Obi.ObiSolver>();
	}

	void OnEnable () {
		solver.OnCollision += Solver_OnCollision;
	}

	void OnDisable(){
		solver.OnCollision -= Solver_OnCollision;
	}

	void Solver_OnCollision (object sender, Obi.ObiSolver.ObiCollisionEventArgs e)
	{
		var colliderWorld = ObiColliderWorld.GetInstance();

		for (int i = 0;  i < e.contacts.Count; ++i)
		{
			if (e.contacts.Data[i].distance < 0.001f)
			{
				var col = colliderWorld.colliderHandles[e.contacts.Data[i].bodyB].owner;
				if (col != null)
				{
					int k = e.contacts.Data[i].bodyA;

					Vector4 userData = solver.userData[k];
					if (col == hotCollider){
						userData[0] = Mathf.Max(0.05f,userData[0] - heat * Time.fixedDeltaTime);
					}else if (col == coldCollider){
						userData[0] = Mathf.Min(10,userData[0] + cooling * Time.fixedDeltaTime);
					}
					solver.userData[k] = userData;
				}
			}
		}

	}

}

Then, we initialize the contents of the userData array (temperature, in our case) to viscosity when particles are first emitted. We also need to map temperature to viscosity at the end of each frame:

using UnityEngine;
using Obi;

[RequireComponent(typeof(ObiEmitter))]
public class ViscosityFromTemperature : MonoBehaviour
{
	void Awake()
	{
		GetComponent<ObiEmitter>().OnEmitParticle += Emitter_OnEmitParticle;
	}

	void Emitter_OnEmitParticle (ObiEmitter emitter, int particleIndex)
	{
		if (emitter.solver != null)
		{
			int k = emitter.solverIndices[particleIndex];

			Vector4 userData = emitter.solver.userData[k];
			userData[0] = emitter.solver.viscosities[k];
			emitter.solver.userData[k] = userData;
		}
	}

	void LateUpdate()
	{
		for (int i = 0; i < emitter.solverIndices.Length; ++i){
			int k = emitter.solverIndices[i];
			emitter.solver.viscosities[k] = emitter.solver.userData[k][0];
		}
	}
}

Here's the result: the initially thick, viscous fluid falls on a hot plate, becomes thinner and flows to a cold plate, where it becomes thicker again. We also mapped viscosity to color to make it easier to visualize: