Make a realistic boat in Unity with C#

3. Add buoyancy so the boat can float

Introduction

Our cube from the last part is sinking to the bottom of the ocean like a stone. To make it float realistically we have to add a force to it called buoyancy. The main reason I decided to make a boat in Unity was that I read an article in Gamasutra called "Water interaction model for boats in video games." It will explain the math and physics behind what we are going to do in this part, so read it before you begin creating the code. I have also used the same names on the variables as in that article to make it a little easier to follow the code because things will get complicated.

The basic summary of the Gamasutra article is that you have to find out which parts of the boat is below the water. Then you should add a buoyancy force to those parts.

What you need to know is that if you create a 3d object it will consist of triangles. Even the most complicated 3d object, like the ones you see in movies like Shrek, consist of a lot of triangles, but you can't see them because they are so small. So each side in our cube consists of 2 triangles.

In Unity you have to deal with at least 2 arrays to control the triangles. One triangle consists of 3 so-called vertices that each has a coordinate in 3d-space like x,y,z. A vertice is a corner in the triangle. One array will store these. The other array will store in which order the vertices form a triangle. When you build a triangle in Unity you have to store the position of the vertices in the array so they form a clockwise loop through all the corners. If you happen to store them counter-clockwise the triangle will be inside out and you will not see it (you will see it if you move to the other side of the triangle).

So to sum up: The boat (cube) consists of triangles. We need to find out if one of the triangles is submerged. If the entire triangle is below the water then we can just store it and add buoyancy to the entire triangle. But if only a part of the triangle is below the water, then we have to cut it in pieces and store the pieces that are below the water and add buoyancy to those pieces. It will look like this:

Unity boat tutorial scene

To make this work we need the following scripts:

  • BoatPhysics

  • ModifyBoatMesh

  • TriangleData

  • WaterController

BoatPhysics

This script will act as the parent to all other scripts we need. This is the only script we have to add to the cube to make it float.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

namespace BoatTutorial
{
    public class BoatPhysics : MonoBehaviour
    {
        //Drags
        public GameObject underWaterObj;

        //Script that's doing everything needed with the boat mesh, such as finding out which part is above the water
        private ModifyBoatMesh modifyBoatMesh;

        //Mesh for debugging
        private Mesh underWaterMesh;

        //The boats rigidbody
        private Rigidbody boatRB;

        //The density of the water the boat is traveling in
        private float rhoWater = 1027f;

        void Start()
        {
            //Get the boat's rigidbody
            boatRB = gameObject.GetComponent<Rigidbody>();

            //Init the script that will modify the boat mesh
            modifyBoatMesh = new ModifyBoatMesh(gameObject);

            //Meshes that are below and above the water
            underWaterMesh = underWaterObj.GetComponent<MeshFilter>().mesh;
        }

        void Update()
        {
            //Generate the under water mesh
            modifyBoatMesh.GenerateUnderwaterMesh();

            //Display the under water mesh
            modifyBoatMesh.DisplayMesh(underWaterMesh, "UnderWater Mesh", modifyBoatMesh.underWaterTriangleData);
        }

        void FixedUpdate()
        {
            //Add forces to the part of the boat that's below the water
            if (modifyBoatMesh.underWaterTriangleData.Count > 0)
            {
                AddUnderWaterForces();
            }
        }

        //Add all forces that act on the squares below the water
        void AddUnderWaterForces()
        {
            //Get all triangles
            List<TriangleData> underWaterTriangleData = modifyBoatMesh.underWaterTriangleData;

            for (int i = 0; i < underWaterTriangleData.Count; i++)
            {
                //This triangle
                TriangleData triangleData = underWaterTriangleData[i];

                //Calculate the buoyancy force
                Vector3 buoyancyForce = BuoyancyForce(rhoWater, triangleData);

                //Add the force to the boat
                boatRB.AddForceAtPosition(buoyancyForce, triangleData.center);


                //Debug

                //Normal
                Debug.DrawRay(triangleData.center, triangleData.normal * 3f, Color.white);

                //Buoyancy
                Debug.DrawRay(triangleData.center, buoyancyForce.normalized * -3f, Color.blue);
            }
        }

        //The buoyancy force so the boat can float
        private Vector3 BuoyancyForce(float rho, TriangleData triangleData)
        {
            //Buoyancy is a hydrostatic force - it's there even if the water isn't flowing or if the boat stays still

            // F_buoyancy = rho * g * V
            // rho - density of the mediaum you are in
            // g - gravity
            // V - volume of fluid directly above the curved surface 

            // V = z * S * n 
            // z - distance to surface
            // S - surface area
            // n - normal to the surface
            Vector3 buoyancyForce = rho * Physics.gravity.y * triangleData.distanceToSurface * triangleData.area * triangleData.normal;

            //The vertical component of the hydrostatic forces don't cancel out but the horizontal do
            buoyancyForce.x = 0f;
            buoyancyForce.z = 0f;

            return buoyancyForce;
        }
    }
}

ModifyBoatMesh

This script will cut the cube's triangles into the smaller pieces.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

namespace BoatTutorial
{
    //Generates the mesh that's below the water
    public class ModifyBoatMesh
    {
        //The boat transform needed to get the global position of a vertice
        private Transform boatTrans;
        //Coordinates of all vertices in the original boat
        Vector3[] boatVertices;
        //Positions in allVerticesArray, such as 0, 3, 5, to build triangles
        int[] boatTriangles;

        //So we only need to make the transformation from local to global once
        public Vector3[] boatVerticesGlobal;
        //Find all the distances to water once because some triangles share vertices, so reuse
        float[] allDistancesToWater;

        //The triangles belonging to the part of the boat that's under water
        public List<TriangleData> underWaterTriangleData = new List<TriangleData>();

        public ModifyBoatMesh(GameObject boatObj)
        {
            //Get the transform
            boatTrans = boatObj.transform;

            //Init the arrays and lists
            boatVertices = boatObj.GetComponent<MeshFilter>().mesh.vertices;
            boatTriangles = boatObj.GetComponent<MeshFilter>().mesh.triangles;

            //The boat vertices in global position
            boatVerticesGlobal = new Vector3[boatVertices.Length];
            //Find all the distances to water once because some triangles share vertices, so reuse
            allDistancesToWater = new float[boatVertices.Length];
        }

        //Generate the underwater mesh
        public void GenerateUnderwaterMesh()
        {
            //Reset
            underWaterTriangleData.Clear();

            //Find all the distances to water once because some triangles share vertices, so reuse
            for (int j = 0; j < boatVertices.Length; j++)
            {
                //The coordinate should be in global position
                Vector3 globalPos = boatTrans.TransformPoint(boatVertices[j]);

                //Save the global position so we only need to calculate it once here
                //And if we want to debug we can convert it back to local
                boatVerticesGlobal[j] = globalPos;

                allDistancesToWater[j] = WaterController.current.DistanceToWater(globalPos, Time.time);
            }

            //Add the triangles that are below the water
            AddTriangles();
        }

        //Add all the triangles that's part of the underwater mesh
        private void AddTriangles()
        {
            //List that will store the data we need to sort the vertices based on distance to water
            List<VertexData> vertexData = new List<VertexData>();

            //Add init data that will be replaced
            vertexData.Add(new VertexData());
            vertexData.Add(new VertexData());
            vertexData.Add(new VertexData());


            //Loop through all the triangles (3 vertices at a time = 1 triangle)
            int i = 0;
            while (i < boatTriangles.Length)
            {
                //Loop through the 3 vertices
                for (int x = 0; x < 3; x++)
                {
                    //Save the data we need
                    vertexData[x].distance = allDistancesToWater[boatTriangles[i]];

                    vertexData[x].index = x;

                    vertexData[x].globalVertexPos = boatVerticesGlobal[boatTriangles[i]];

                    i++;
                }


                //All vertices are above the water
                if (vertexData[0].distance > 0f && vertexData[1].distance > 0f && vertexData[2].distance > 0f)
                {
                    continue;
                }


                //Create the triangles that are below the waterline

                //All vertices are underwater
                if (vertexData[0].distance < 0f && vertexData[1].distance < 0f && vertexData[2].distance < 0f)
                {
                    Vector3 p1 = vertexData[0].globalVertexPos;
                    Vector3 p2 = vertexData[1].globalVertexPos;
                    Vector3 p3 = vertexData[2].globalVertexPos;

                    //Save the triangle
                    underWaterTriangleData.Add(new TriangleData(p1, p2, p3));
                }
                //1 or 2 vertices are below the water
                else
                {
                    //Sort the vertices
                    vertexData.Sort((x, y) => x.distance.CompareTo(y.distance));

                    vertexData.Reverse();

                    //One vertice is above the water, the rest is below
                    if (vertexData[0].distance > 0f && vertexData[1].distance < 0f && vertexData[2].distance < 0f)
                    {
                        AddTrianglesOneAboveWater(vertexData);
                    }
                    //Two vertices are above the water, the other is below
                    else if (vertexData[0].distance > 0f && vertexData[1].distance > 0f && vertexData[2].distance < 0f)
                    {
                        AddTrianglesTwoAboveWater(vertexData);
                    }
                }
            }
        }

        //Build the new triangles where one of the old vertices is above the water
        private void AddTrianglesOneAboveWater(List<VertexData> vertexData)
        {
            //H is always at position 0
            Vector3 H = vertexData[0].globalVertexPos;

            //Left of H is M
            //Right of H is L

            //Find the index of M
            int M_index = vertexData[0].index - 1;
            if (M_index < 0)
            {
                M_index = 2;
            }

            //We also need the heights to water
            float h_H = vertexData[0].distance;
            float h_M = 0f;
            float h_L = 0f;

            Vector3 M = Vector3.zero;
            Vector3 L = Vector3.zero;

            //This means M is at position 1 in the List
            if (vertexData[1].index == M_index)
            {
                M = vertexData[1].globalVertexPos;
                L = vertexData[2].globalVertexPos;

                h_M = vertexData[1].distance;
                h_L = vertexData[2].distance;
            }
            else
            {
                M = vertexData[2].globalVertexPos;
                L = vertexData[1].globalVertexPos;

                h_M = vertexData[2].distance;
                h_L = vertexData[1].distance;
            }

			
            //Now we can calculate where we should cut the triangle to form 2 new triangles
            //because the resulting area will always form a square

            //Point I_M
            Vector3 MH = H - M;

            float t_M = -h_M / (h_H - h_M);

            Vector3 MI_M = t_M * MH;

            Vector3 I_M = MI_M + M;


            //Point I_L
            Vector3 LH = H - L;

            float t_L = -h_L / (h_H - h_L);

            Vector3 LI_L = t_L * LH;

            Vector3 I_L = LI_L + L;


            //Save the data, such as normal, area, etc      
            //2 triangles below the water  
            underWaterTriangleData.Add(new TriangleData(M, I_M, I_L));
            underWaterTriangleData.Add(new TriangleData(M, I_L, L));
        }

        //Build the new triangles where two of the old vertices are above the water
        private void AddTrianglesTwoAboveWater(List<VertexData> vertexData)
        {
            //H and M are above the water
            //H is after the vertice that's below water, which is L
            //So we know which one is L because it is last in the sorted list
            Vector3 L = vertexData[2].globalVertexPos;

            //Find the index of H
            int H_index = vertexData[2].index + 1;
            if (H_index > 2)
            {
                H_index = 0;
            }


            //We also need the heights to water
            float h_L = vertexData[2].distance;
            float h_H = 0f;
            float h_M = 0f;

            Vector3 H = Vector3.zero;
            Vector3 M = Vector3.zero;

            //This means that H is at position 1 in the list
            if (vertexData[1].index == H_index)
            {
                H = vertexData[1].globalVertexPos;
                M = vertexData[0].globalVertexPos;

                h_H = vertexData[1].distance;
                h_M = vertexData[0].distance;
            }
            else
            {
                H = vertexData[0].globalVertexPos;
                M = vertexData[1].globalVertexPos;

                h_H = vertexData[0].distance;
                h_M = vertexData[1].distance;
            }


            //Now we can find where to cut the triangle

            //Point J_M
            Vector3 LM = M - L;

            float t_M = -h_L / (h_M - h_L);

            Vector3 LJ_M = t_M * LM;

            Vector3 J_M = LJ_M + L;


            //Point J_H
            Vector3 LH = H - L;

            float t_H = -h_L / (h_H - h_L);

            Vector3 LJ_H = t_H * LH;

            Vector3 J_H = LJ_H + L;


            //Save the data, such as normal, area, etc
            //1 triangle below the water
            underWaterTriangleData.Add(new TriangleData(L, J_H, J_M));
        }

        //Help class to store triangle data so we can sort the distances
        private class VertexData
        {
            //The distance to water from this vertex
            public float distance;
            //An index so we can form clockwise triangles
            public int index;
            //The global Vector3 position of the vertex
            public Vector3 globalVertexPos;
        }

        //Display the underwater mesh
        public void DisplayMesh(Mesh mesh, string name, List<TriangleData> triangesData)
        {
            List<Vector3> vertices = new List<Vector3>();
            List<int> triangles = new List<int>();

            //Build the mesh
            for (int i = 0; i < triangesData.Count; i++)
            {
                //From global coordinates to local coordinates
                Vector3 p1 = boatTrans.InverseTransformPoint(triangesData[i].p1);
                Vector3 p2 = boatTrans.InverseTransformPoint(triangesData[i].p2);
                Vector3 p3 = boatTrans.InverseTransformPoint(triangesData[i].p3);

                vertices.Add(p1);
                triangles.Add(vertices.Count - 1);

                vertices.Add(p2);
                triangles.Add(vertices.Count - 1);

                vertices.Add(p3);
                triangles.Add(vertices.Count - 1);
            }

            //Remove the old mesh
            mesh.Clear();

            //Give it a name
            mesh.name = name;

            //Add the new vertices and triangles
            mesh.vertices = vertices.ToArray();

            mesh.triangles = triangles.ToArray();

            mesh.RecalculateBounds();
        }
    }
}

TriangleData

To be able to calculate the buoyancy force we have to calculate a few things for each triangle that's below the water, such as area of the triangle, and this script will store all those things. What you can see in the image below is the center of the triangle (where the while lines start) and the normal to the triangle (the white line).

Unity boat tutorial scene triangle data

using UnityEngine;
using System.Collections;

namespace BoatTutorial
{
    //To save space so we don't have to send millions of parameters to each method
    public struct TriangleData
    {
        //The corners of this triangle in global coordinates
        public Vector3 p1;
        public Vector3 p2;
        public Vector3 p3;

        //The center of the triangle
        public Vector3 center;

        //The distance to the surface from the center of the triangle
        public float distanceToSurface;

        //The normal to the triangle
        public Vector3 normal;

        //The area of the triangle
        public float area;

        public TriangleData(Vector3 p1, Vector3 p2, Vector3 p3)
        {
            this.p1 = p1;
            this.p2 = p2;
            this.p3 = p3;

            //Center of the triangle
            this.center = (p1 + p2 + p3) / 3f;

            //Distance to the surface from the center of the triangle
            this.distanceToSurface = Mathf.Abs(WaterController.current.DistanceToWater(this.center, Time.time));

            //Normal to the triangle
            this.normal = Vector3.Cross(p2 - p1, p3 - p1).normalized;

            //Area of the triangle
            float a = Vector3.Distance(p1, p2);

            float c = Vector3.Distance(p3, p1);

            this.area = (a * c * Mathf.Sin(Vector3.Angle(p2 - p1, p3 - p1) * Mathf.Deg2Rad)) / 2f;
        }
    }
}

WaterController

Will help us find the distance to water from a coordinate. Add this script to an empty gameobject in your scene. We will need the strange water parameters in the next part of the tutorial where we will create a moving sea, so keep them.

using UnityEngine;
using System.Collections;

//Controlls the water
public class WaterController : MonoBehaviour 
{
    public static WaterController current;

    public bool isMoving;

    //Wave height and speed
    public float scale = 0.1f;
    public float speed = 1.0f;
    //The width between the waves
    public float waveDistance = 1f;
    //Noise parameters
    public float noiseStrength = 1f;
    public float noiseWalk = 1f;

    void Start()
    {
        current = this;
    }

    //Get the y coordinate from whatever wavetype we are using
    public float GetWaveYPos(Vector3 position, float timeSinceStart)
    {
        //if (isMoving)
        //{
            //return WaveTypes.SinXWave(position, speed, scale, waveDistance, noiseStrength, noiseWalk, timeSinceStart);
        //}
        //else
        //{
            //return 0f;
        //}
		
		return 0f;
    }

    //Find the distance from a vertice to water
    //Make sure the position is in global coordinates
    //Positive if above water
    //Negative if below water
    public float DistanceToWater(Vector3 position, float timeSinceStart)
    {
        float waterHeight = GetWaveYPos(position, timeSinceStart);

        float distanceToWater = position.y - waterHeight;

        return distanceToWater;
    }
}

Will it float?

Now we can finally answer the question: "Will the cube float?" My cube, which weighs 800 kg, is floating. But is it also realistic? To find out we can ask our old friend Archimedes:

Any object, wholly or partially immersed in a fluid, is buoyed up by a force equal to the weight of the fluid displaced by the object.

What Archimedes is saying is that an object floats on water if it can displace a volume of water whose weight is greater than that of the object. So if our cube weighs more than 1000 kg, which is larger than the weight of the volume of displaced water if the density of water is 1000kg/m^3 and the cube's side is 1 m, then it should sink. If you change you cube's mass above/below 1000 kg then you should notice that it will float up/down, so this model is actually good.

This model will work for any mesh you have, so it doesn't have to be a cube. Test to change your object to a cylinder, a sphere, or a more complicated boat. But don't forget to change the mass!

Unity boat tutorial scene