How To: Buoyancy Simulation Using Unity 2D

Introduction

I’ve been in the mood to do some physics simulations. I decided to start small and try to perform some Buoyancy simulation using unity. Do note that this post focuses only on simulating Buoyancy and it’s not the intention of this post to perform a complete simulation. So, let’s see how we can perform some 2D buoyancy simulation in unity. This is what I’ve ended up with

buoyancy simulation using unity
buoyancy simulation using unity

Let’s start by creating 2 scripts: “Buoyancy” and “Water”. Buoyancy script goes on the objects on which Buoyant force should act and Water script is attached to a liquid body. To keep the simulation simple, I’ve decided the circle to have a unit area.
I’ve added the liquid object to layer “Water” and objects that can be collided with to layer “Floor”.
As the liquid is static, we grab the min and max points in the Start().
These are the following variables declared in each of the scripts:

public class Buoyancy : MonoBehaviour
{
    public float density = 750; // Kg/m^3
    public float gravitationForce = 9.8f; // m/s^2;
    
    [HideInInspector]
    public float mass; // in KG
    // 1 Unit = 1 Meter in Unity
    // Forces that will act on body:
    // Gravity : F = Mass * Gravity
    // Buoyancy : B = density * gravity * (Volume to displaced fluid)
    [HideInInspector]
    public Vector3 currentVelocity = Vector3.zero;
    [HideInInspector]
    public Vector3 oldVelocity = Vector3.zero;
    Vector3 externalForces = Vector3.zero;
    // Start is called before the first frame update
    void Start()
    {
        mass = 1 * density; // Considering unit volume
    }
}
public class Water : MonoBehaviour
{
    public float density = 997.0f; // KG/M^3
    public float stickiness = 0.15f;
    Vector3 minPoint = Vector3.zero;
    Vector3 maxPoint = Vector3.zero;
    // Start is called before the first frame update
    void Start()
    {
        BoxCollider2D collider = GetComponent<BoxCollider2D>();
        minPoint = collider.bounds.min;
        maxPoint = collider.bounds.max;
    }
}

Setup

In our simulation, there are going to be 2 forces acting on the body. One is the gravitational force(which is independent of mass) and buoyant force is the other one.
We gather all the external forces that are acting on the body in the previous frame(using “RegisterExternalForce“) and add that to current acceleration of the body(A += F/M).
It’s trivial to compute velocity if we have acceleration. We therefore use the computed velocity to update the position of the object in the “Update” loop (Disp = Vel * DeltaT).

void FixedUpdate()
    {
        SpriteRenderer renderer = GetComponent<SpriteRenderer>();
        Vector3 acceleration = Vector3.zero;
        acceleration.y = -gravitationForce;
        acceleration += externalForces / mass;
        currentVelocity = Vector3.zero;
        currentVelocity = oldVelocity + (acceleration * Time.fixedDeltaTime);
        currentVelocity.y = Mathf.Clamp(currentVelocity.y, -gravitationForce, gravitationForce);
        Vector3 pos = gameObject.transform.position + currentVelocity * Time.fixedDeltaTime;
        CircleCollider2D collider = GetComponent<CircleCollider2D>();
        
        Collider2D otherCollider = Physics2D.OverlapCircle(pos, collider.radius, LayerMask.GetMask("Floor"));
        
        if(otherCollider != null)
        {
            currentVelocity = Vector3.zero;
            acceleration = -acceleration;
        }
        float gravity = mass * gravitationForce;
        oldVelocity = currentVelocity;
        
        //This should be reverted to 0 to gather all the forces again
        externalForces = Vector3.zero;
    }
    public void RegisterForce(Vector3 externalForce)
    {
        externalForces += externalForce;
    }
    // Update is called once per frame
    void Update()
    {
        gameObject.transform.position += currentVelocity * Time.deltaTime;
    }

Buoyant force is acted on when an object comes into contact with a liquid surface. We use Trigger functions to perform these calculations whenever an object with “Buoyancy” component comes into contact with our trigger.

public class Water : MonoBehaviour
{
    ...
    void OnTriggerEnter2D(Collider2D collider)
    {
        RegisterForceOn(collider);
    }
    void OnTriggerStay2D(Collider2D collider)
    {
        RegisterForceOn(collider);
    }
    void RegisterForceOn(Collider2D collider)
    {
        //Will be populated in next section
    }
}

For unity to trigger collision events, one of the objects being partaking collision should have a rigidbody component. But we don’t want the rigidbody to move the object and perform any simulations. To fit our needs for this simulation, we can set the BodyType in rigidbody2D to “kinematic”.

Buoyant Object Settings
Buoyant Object Settings

Buoyancy Calculation

Now we can finally fill the final and most important function that actually calculates the Buoyancy. Whenever a body comes into contact with our liquid surface, we are going to apply the following forces on it:

  • Buoyant Force: acts in direction opposite to gravitational force [(density of liquid) * (acc. due to gravity) * (volume of liquid displaced)]
  • Drag: The resistance to motion of the object and it’s the force acting in opposite direction to relative motion of the object. It’s defined by (0.5 * liquidDensity * (relative speed of object)^2 * (dragCoefficient) * (Cross sectional Area)). For more information, please refer this wiki page.
  • Arrest Force: This is not conventional, but I had to apply an additional force to stop the objects from bouncing forever. This also acts in direction opposite to current velocity, but the calculated force attempts to halt the object. increasing the “Stickiness” variable reduces the resistance of the liquid. It seemed like a neat little effect, so I left it in. Please do post a comment if there’s a better way to prevent the eternal bouncing 🙂
void RegisterForceOn(Collider2D collider)
    {
        Buoyancy buoyancy = collider.gameObject.GetComponent<Buoyancy>();
        SpriteRenderer renderer = collider.gameObject.GetComponent<SpriteRenderer>();
        if(renderer == null || buoyancy == null)
        {
            return;
        }
        Bounds bounds = renderer.bounds;
        Vector2 castPoint = new Vector2(bounds.center.x, bounds.max.y);
        RaycastHit2D cast;
        int layerMask = 1 << LayerMask.NameToLayer("Water");
        cast = Physics2D.Raycast(castPoint, -Vector2.up, 1.0f, layerMask);
        if(cast.collider == null || cast.collider.gameObject != this.gameObject)
        {
            return;
        } 
        //Object is above water surface
        float volumeSubmerged = 0.0f;
        float totalVolume = bounds.size.x * bounds.size.y;
        
        float width = bounds.size.x;
        if(castPoint.y > maxPoint.y)
        {
            float height = Mathf.Abs(cast.point.y - bounds.min.y);
            volumeSubmerged = width * height;
        }
        else
        {
            float height = bounds.size.y;
            volumeSubmerged = width * height;
        }
        
        float buoyantForce = (density * 9.8f * volumeSubmerged);
       
        float sign = Mathf.Sign(buoyancy.currentVelocity.y) * -1.0f;
        float drag = 0.5f * (density) * (buoyancy.currentVelocity.y * buoyancy.currentVelocity.y) * 0.47f * volumeSubmerged * sign;
        float arrest = stickiness * (1.0f - volumeSubmerged/totalVolume) * buoyancy.mass * -(buoyancy.currentVelocity.y) / Time.fixedDeltaTime;
        //This aims to keep object in liquid
        buoyancy.RegisterForce(new Vector3(0, buoyantForce + drag + arrest, 0));
    }

Well, that’s all for this post. Please do post a comment if you figure out any improvements or better ways to approach this.
Thanks for reading till the end!!


Leave a Reply

Your email address will not be published. Required fields are marked *