Make a realistic boat in Unity with C#

7. Add foam

As the boat is interacting with the water, it will create various types of foams. Here we will create the foam that's around the boat and bow splashes as the bow of the boat is moving down into the water.

Foam around the boat

As usual there are more than one way to create the foam that's around the boat as it is moving in the water. One way to add the foam is to create a foam skirt, which is a skirt added just where the boat is intersecting with the water. I got the inspiration to the foam skirt from this tweet (we will also add the flipped mesh because it's just a few lines of code):

To create the foam skirt, we need the vertices where the boat is intersecting with the water. In the BoatPhysics script you need to create the list where we store these vertices. You also need to add new GameObjects which are the same as the objects used to display the mesh that's below the water, so we can display both the foam mesh and the mirror mesh.

//For water reflection
public GameObject underWaterMirrorObj;
//The foam
public GameObject foamSkirtObj;

//A list with all vertices that are at the intersection point between the air and water
private List<Vector3> intersectionVertices = new List<Vector3>();

//Script that displays extra meshes, generate a mirrored mesh and a foam skirt
private GenerateExtraBoatMeshes generateExtraMeshes;

void Start() 
{
	//Init the script that will modify the boat mesh
	modifyBoatMesh = new ModifyBoatMesh(boatMeshObj, underWaterObj, aboveWaterObj, boatRB);

	generateExtraMeshes = new GenerateExtraBoatMeshes(boatMeshObj);

	//Meshes that are below and above the water
	underWaterMesh = underWaterObj.GetComponent<MeshFilter>().mesh;
	aboveWaterMesh = aboveWaterObj.GetComponent<MeshFilter>().mesh;
	
	underWaterMirrorMesh = underWaterMirrorObj.GetComponent<MeshFilter>().mesh;
	foamMesh = foamSkirtObj.GetComponent<MeshFilter>().mesh;
}

//And the update method now looks like this
void Update()
{
	//Generate the under water and above water meshes
	modifyBoatMesh.GenerateUnderwaterMesh(intersectionVertices);

	//Display the under water mesh - is always needed to get the underwater length for forces calculations
	generateExtraMeshes.DisplayMesh(underWaterMesh, "UnderWater Mesh", modifyBoatMesh.underWaterTriangleData);

	//Display the above water mesh
	generateExtraMeshes.DisplayMesh(aboveWaterMesh, "AboveWater Mesh", modifyBoatMesh.aboveWaterTriangleData);

	//Display the mesh that's the mirror
	generateExtraMeshes.DisplayMirrorMesh(underWaterMirrorMesh, "UnderwaterWater Mirror Mesh", modifyBoatMesh.aboveWaterTriangleData);

	//Generate the foam skirt
	generateExtraMeshes.GenerateFoamSkirt(foamMesh, "Foam skirt", intersectionVertices);
}

In the ModifyBoatMesh script you need to add the vertices to this list both in the AddTrianglesOneAboveWater method and in the AddTrianglesTwoAboveWater method. Also don't forget to clear the list each time in the beginning of the GenerateUnderwaterMesh method.

private void AddTrianglesOneAboveWater()
{
	intersectionVertices.Add(I_M);
	intersectionVertices.Add(I_L);
}

private void AddTrianglesTwoAboveWater()
{
	intersectionVertices.Add(J_H);
	intersectionVertices.Add(J_M);
}

We now have a list with all the vertices where the boat is intersecting with the water. The problem now is that they are not sorted so it's impossible to create a single mesh from these vertices. Why? Because to create the foam mesh we have to move the vertices along the average normal between two sides. It looks like this (the green line shows in which order the vertices were added to the list, the higher line the later they were added):

Vertices before the sort

To sort the vertices we are going to use a method called convex hull. I've written a separate tutorial on that subject here: Find the convex hull of random points.

Vertices after convex hull

As you can see from the image above, we have lost a lot of vertices. The reason is that when we cut the boat, the resulting shape is not always convex even though the original boat hull was convex. So we need to add vertices to make the foam more smooth:

Vertices after adding more

Now we can create the final foam mesh:

The final foam skirt

What's going on in the method that's creating the foam mesh is this:

How the foam mesh was created

Let's say that we are at the section where the vertices are called TL and TR. To get the BR vertex we have to move the TR vertex along a normal, and this normal is the average of the normal this section and the section to the right of it.

To make it work you need two scripts:

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

//Display extra meshes, generate a mirrored mesh and a foam skirt
public class GenerateExtraBoatMeshes
{
    //The boat transform needed to get the global position of a vertex
    private Transform boatTrans;

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

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

        //Build the mesh
        for (int i = 0; i < trianglesData.Count; i++)
        {
            //From global coordinates to local coordinates
            Vector3 p1 = boatTrans.InverseTransformPoint(trianglesData[i].p1);
            Vector3 p2 = boatTrans.InverseTransformPoint(trianglesData[i].p2);
            Vector3 p3 = boatTrans.InverseTransformPoint(trianglesData[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();

        //Important to recalculate bounds because we need the bounds to calculate the length of the underwater mesh
        mesh.RecalculateBounds();
    }

    //Display the mesh that's the mirror
    public void DisplayMirrorMesh(Mesh mesh, string name, List<TriangleData> trianglesData)
    {
        //Move the vertices based on distance to water
        float timeSinceStart = Time.time;

        for (int i = 0; i < trianglesData.Count; i++)
        {
            TriangleData thisTriangle = trianglesData[i];

            //The vertices in TriangleData are global
            thisTriangle.p1.y -= WaterController.current.DistanceToWater(thisTriangle.p1, timeSinceStart) * 2f;
            thisTriangle.p2.y -= WaterController.current.DistanceToWater(thisTriangle.p2, timeSinceStart) * 2f;
            thisTriangle.p3.y -= WaterController.current.DistanceToWater(thisTriangle.p3, timeSinceStart) * 2f;

            //Flip the triangle because it will be inside out when we mirror the mesh
            Vector3 tmp = thisTriangle.p2;

            thisTriangle.p2 = thisTriangle.p3;

            thisTriangle.p3 = tmp;

            trianglesData[i] = thisTriangle;
        }

        DisplayMesh(mesh, name, trianglesData);
    }

    //Generate the foam skirt
    //intersectionVertices are in global pos
    public void GenerateFoamSkirt(Mesh mesh, string name, List<Vector3> intersectionVertices)
    {
        //Step 1. Clean the vertices that are close together
        List<Vector3> cleanedVertices = CleanVertices(intersectionVertices);

        //Display in which order the vertices have been added to the list
        //DisplayVerticesOrderHeight(cleanedVertices, Color.green);


        //Step 2. Sort the vertices
        List<Vector3> sortedVertices = ConvexHull.SortVerticesConvexHull(cleanedVertices);

        //DisplayVerticesOrder(sortedVertices, Color.blue);

        //DisplayVerticesOrderHeight(sortedVertices, Color.green);


        //Step 3. Add more vertices by splitting sections that are too far away to get a smoother foam
        List<Vector3> finalVertices = AddVertices(sortedVertices);

        //DisplayVerticesOrder(sortedVertices, Color.blue);

        DisplayVerticesOrderHeight(finalVertices, Color.green);


        //Step 4. Create the foam mesh
        CreateFoamMesh(finalVertices, mesh, name);
    }

    //Clean vertices that are close together
    private List<Vector3> CleanVertices(List<Vector3> intersectionVertices)
    {
        List<Vector3> cleanedVertices = new List<Vector3>();

        //Debug.Log("Before cleaning: " + intersectionVertices.Count);

        for (int i = 0; i < intersectionVertices.Count; i++)
        {
            bool hasFoundNearbyVertice = false;

            for (int j = 0; j < cleanedVertices.Count; j++)
            {
                //Is the list already including a vertice at a similar position)
                if (Vector3.SqrMagnitude(cleanedVertices[j] - intersectionVertices[i]) < 0.1f)
                {
                    hasFoundNearbyVertice = true;

                    break;
                }
            }

            if (!hasFoundNearbyVertice)
            {
                cleanedVertices.Add(intersectionVertices[i]);
            }
        }

        //Debug.Log("After cleaning: " + cleanedVertices.Count);

        return cleanedVertices;
    }

    //Add more vertices by splitting sections that are too far away to get a smoother foam
    private List<Vector3> AddVertices(List<Vector3> sortedVertices)
    {
        List<Vector3> finalVertices = new List<Vector3>();

        float distBetweenNewVertices = 4f;

        for (int i = 0; i < sortedVertices.Count; i++)
        {
            int lastVertPos = i - 1;

            if (lastVertPos < 0)
            {
                lastVertPos = sortedVertices.Count - 1;
            }

            Vector3 lastVert = sortedVertices[lastVertPos];
            Vector3 thisVert = sortedVertices[i];

            float distance = Vector3.Magnitude(thisVert - lastVert);

            Vector3 dir = Vector3.Normalize((thisVert - lastVert));

            //How many new vertices can we fit between?
            int newVertices = Mathf.FloorToInt(distance / distBetweenNewVertices);

            //Add the new vertices
            finalVertices.Add(lastVert);

            for (int j = 1; j < newVertices; j++)
            {
                Vector3 newVert = lastVert + j * dir * distBetweenNewVertices;

                finalVertices.Add(newVert);
            }
        }

        //Add the last vertex
        finalVertices.Add(sortedVertices[sortedVertices.Count - 1]);

        //Make sure all vertices are above the water so we can see the foam
        float timeSinceStart = Time.time;

        for (int i = 0; i < finalVertices.Count; i++)
        {
            Vector3 thisVertice = finalVertices[i];

            thisVertice.y = WaterController.current.GetWaveYPos(thisVertice, timeSinceStart);

            thisVertice.y += 0.1f;

            finalVertices[i] = thisVertice;
        }

        return finalVertices;
    }

    //Create the foam mesh
    private void CreateFoamMesh(List<Vector3> finalVertices, Mesh mesh, string name)
    {
        List<Vector3> vertices = new List<Vector3>();
        List<int> triangles = new List<int>();
        List<Vector2> uvs = new List<Vector2>();

        //How far from the boat is the foam extruded?
        float foamSize = 2f;

        float timeSinceStart = Time.time;

        //
        //Init by calculating the left normal, the normal, and the average left normal because we can reuse them
        //
        Vector3 TL = finalVertices[finalVertices.Count - 1];
        Vector3 TR = finalVertices[0];

        //To get the other corners we need the average "outgoing" normal from both sides of a vertex
        //This side
        Vector3 vecBetween = Vector3.Normalize(TR - TL);

        Vector3 normal = new Vector3(vecBetween.z, 0f, -vecBetween.x);

        //Left side
        Vector3 vecBetweenLeft = Vector3.Normalize(TL - finalVertices[finalVertices.Count - 2]);

        Vector3 normalLeft = new Vector3(vecBetweenLeft.z, 0f, -vecBetweenLeft.x);

        Vector3 averageNormalLeft = Vector3.Normalize((normalLeft + normal) * 0.5f);


        //Move the vertex along the average normal
        Vector3 BL = TL + averageNormalLeft * foamSize;

        //Move the outer part of the foam with the wave
        BL.y = WaterController.current.GetWaveYPos(BL, timeSinceStart);

        //From global coordinates to local coordinates
        Vector3 TL_local = boatTrans.InverseTransformPoint(TL);
        Vector3 BL_local = boatTrans.InverseTransformPoint(BL);

        vertices.Add(TL_local);
        vertices.Add(BL_local);

        uvs.Add(new Vector2(0f, 0f));
        uvs.Add(new Vector2(0f, 1f));

        //Main loop
        for (int i = 0; i < finalVertices.Count; i++)
        {
            //Right side 
            int rightPos = i + 1;
            if (rightPos > finalVertices.Count - 1)
            {
                rightPos = 0;
            }

            Vector3 vecBetweenRight = Vector3.Normalize(finalVertices[rightPos] - TR);

            Vector3 normalRight = new Vector3(vecBetweenRight.z, 0f, -vecBetweenRight.x);

            Vector3 averageNormalRight = Vector3.Normalize((normalRight + normal) * 0.5f);

            //Move the vertex along the average normal
            Vector3 BR = TR + averageNormalRight * foamSize;

            //Move the outer part of the foam with the wave
            BR.y = WaterController.current.GetWaveYPos(BR, timeSinceStart);

            //From global coordinates to local coordinates
            Vector3 TR_local = boatTrans.InverseTransformPoint(TR);
            Vector3 BR_local = boatTrans.InverseTransformPoint(BR);

            vertices.Add(TR_local);
            vertices.Add(BR_local);

            uvs.Add(new Vector2(1f, 0f));
            uvs.Add(new Vector2(1f, 1f));

            //
            // Build the two triangles
            //
            //Added in the order TL - BL - TR - BR
            //TL - BR - BL
            triangles.Add(vertices.Count - 4);
            triangles.Add(vertices.Count - 1);
            triangles.Add(vertices.Count - 3);
            //TL - TR - BR
            triangles.Add(vertices.Count - 4);
            triangles.Add(vertices.Count - 2);
            triangles.Add(vertices.Count - 1);

            //
            // Update for the next iteration
            //
            //Update the normal  and the corners for the next iteration
            normalLeft = normal;

            normal = normalRight;

            averageNormalLeft = averageNormalRight;

            TL = TR;
            TR = finalVertices[rightPos];
        }

        //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.uv = uvs.ToArray();

        mesh.RecalculateBounds();

        mesh.RecalculateNormals();
    }

    //Display in which order the vertices have been added to a list by
    //connecting them with a line
    private void DisplayVerticesOrder(List<Vector3> verticesList, Color color)
    {
        //A line connecting all vertices
        float height = 0.5f;
        for (int i = 0; i < verticesList.Count; i++)
        {
            Vector3 start = verticesList[i] + Vector3.up * height;

            //Connect the end with the start
            int endPos = i + 1;

            if (i == verticesList.Count - 1)
            {
                endPos = 0;
            }
            
            Vector3 end = verticesList[endPos] + Vector3.up * height;

            Debug.DrawLine(start, end, color);
        }
    }

    //Display in which order the vertices have been added to a list by
    //drawing a line from their coordinates, and the height is based on thir position in the list
    private void DisplayVerticesOrderHeight(List<Vector3> verticesList, Color color)
    {
        float length = 0.1f;
        for (int i = 0; i < verticesList.Count; i++)
        {
            Debug.DrawRay(verticesList[i], Vector3.up * length, color);

            //So we can see the sorting order
            length += 0.2f;
        }
    }
}

...and:

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

//Generate a convex hull in 2d space with different algorithms
public class ConvexHull 
{
    //Sort vertices based on a convex hull algorithm
    //Will remove the vertices that are not on the convex hull
    public static List<Vector3> SortVerticesConvexHull(List<Vector3> unSortedList)
    {
        List<Vector3> sortedList = new List<Vector3>();

        //Graham's scan algorithm

        //
        // Init
        //
        //Find the vertice with the smallest x coordinate

        //Init with just the first in the list
        float smallestValue = unSortedList[0].x;
        int smallestIndex = 0;

        for (int i = 1; i < unSortedList.Count; i++)
        {
            if (unSortedList[i].x < smallestValue)
            {
                smallestValue = unSortedList[i].x;

                smallestIndex = i;
            }
            //If they are the same, choose the one with the smallest z value
            else if (unSortedList[i].x == smallestValue)
            {
                if (unSortedList[i].z < unSortedList[smallestIndex].z)
                {
                    smallestIndex = i;
                }
            }
        }


        //Remove the smallest value from the list and add it as the first
        //coordinate on the convex hull
        sortedList.Add(unSortedList[smallestIndex]);

        unSortedList.RemoveAt(smallestIndex);


        //Sort the unsorted vertices based on angle
        Vector3 firstPoint = sortedList[0];
        //Important that everything is in 2d space
        firstPoint.y = 0f;

        //Will sort from smallest to highest angle
        unSortedList = unSortedList.OrderBy(n => GetAngle(new Vector3(n.x, 0f, n.z) - firstPoint)).ToList();

        //Reverse because it's faster to remove vertices from the end
        unSortedList.Reverse();

        //The vertice with the smallest angle is also on the convex hull so add it
        sortedList.Add(unSortedList[unSortedList.Count - 1]);

        unSortedList.RemoveAt(unSortedList.Count - 1);


        //
        //Main algorithm
        //
        //To avoid infinite loop
        int safety = 0;
        while (unSortedList.Count > 0 && safety < 1000)
        {
            safety += 1;

            //Is this a clockwise or a counter-clockwise triangle
            Vector3 a = sortedList[sortedList.Count - 2];
            Vector3 b = sortedList[sortedList.Count - 1];

            Vector3 c = unSortedList[unSortedList.Count - 1];

            unSortedList.RemoveAt(unSortedList.Count - 1);

            sortedList.Add(c);

            //Need to back track in case we messed up at an earlier point
            while (isClockWise(a, b, c) && safety < 1000)
            {
                //Remove the next to last one because we know it aint on the hull
                sortedList.RemoveAt(sortedList.Count - 2);

                a = sortedList[sortedList.Count - 3];
                b = sortedList[sortedList.Count - 2];
                c = sortedList[sortedList.Count - 1];

                safety += 1;
            }
        }


        return sortedList;
    }

    //Is a triangle in 2d space clockwise or counter-clockwise
    //https://www.youtube.com/watch?v=0HZaRu5IupM
    private static bool isClockWise(Vector3 a, Vector3 b, Vector3 c)
    {
        float signedArea = (b.x - a.x) * (c.z - a.z) - (b.z - a.z) * (c.x - a.x);

        if (signedArea > 0f)
        {
            return false;
        }
        //Is also returning true if all points are on one line
        //Which is good because then we can remove one of them because is not needed
        else
        {
            return true;
        }
    }

    //This returns the angle with some measurement
    private static float GetAngle(Vector3 vec)
    {
        //Angle between the vector and x-axis
        //Vector3.Angle has too low precision so is not working!
        float angle = Mathf.Atan2(vec.z, vec.x);

        return angle;
    }
}

Bow splashes

To create the bow splashes we are going to use the same technique as used in the game Assassin’s Creed III: The tech behind (or beneath) the action. The idea is that we have the spheres that's moving along a line, which is aligned with the boat's hull. If the sphere is moving up, then a particle system is emitting foam.

Inspiration from the game assasins creed

To make this work in unity, you need an empty gameobject called "Bow Splash" which should be a child to the boat. Then as children to this gameobject, you should add a sphere and two more empty gameobjects. The two empty gameobjects should be aligned with the boat's hull where you want the bow splashes to appear. As child to the sphere you should add a particle system and tweak it so it looks like it's emitting foam. The blue line in the image below is going between the two empty gameobjects.

Bow splash setup in unity

Then you add the following script to the "Bow Splash" gameobject.

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

//Add bow splases to the boat
public class BowSplash : MonoBehaviour 
{
    public Transform sphere;
    //The splashTop and splashBottom are empty gameobjects determining when the angle of the foam and when it should stop
    public Transform splashTop;
    public Transform splashBottom;
    public ParticleSystem splashParticleSystem;

    private Vector3 lastPos;
	
	
	void Update() 
	{
        //Debug by drawing a line between the top and bottom
        Debug.DrawLine(splashBottom.position, splashTop.position, Color.blue);

        //What's the position of the sphere
        //Positive if above water, negative if below water
        float bottomDistToWater = WaterController.current.DistanceToWater(splashBottom.position, Time.time);

        float topDistToWater = WaterController.current.DistanceToWater(splashTop.position, Time.time);

        //Only add foam if one is above water and the other is below
        if (topDistToWater > 0f && bottomDistToWater < 0f)
        {
            //Cut in the same way as in http://www.gamasutra.com/view/news/237528/Water_interaction_model_for_boats_in_video_games.php
            //Figure 7:
            Vector3 H = splashTop.position;
            Vector3 M = splashBottom.position;

            float h_M = bottomDistToWater;
            float h_H = topDistToWater;

            Vector3 MH = H - M;

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

            Vector3 MI_M = t_M * MH;

            //This is the position where the water is intersecting with the line
            Vector3 I_M = MI_M + M;

            //Move the sphere to this position
            sphere.position = I_M;

            //Add foam if the boat is moving down into the water
            if (I_M.y < lastPos.y)
            {
                //Align the ps along the line
                splashParticleSystem.transform.LookAt(splashTop.position);

                if (!splashParticleSystem.isPlaying)
                {
                    //Debug.Log("Add foam");

                    splashParticleSystem.Play();
                }
            }
            else
            {
                splashParticleSystem.Stop();
            }

            lastPos = I_M;
        }
        else
        {
            splashParticleSystem.Stop();
        }
    }
}

If everything is working, it should look like this:

Final foam in Unity