Make a realistic boat in Unity with C#

5. Add resistance forces

So the boat can float with the help of the buoyancy force, but it's still not behaving like a realistic boat would behave. What we need are resistance forces.

You will have to add the following resistance forces: viscous water resistance, pressure drag, slamming, and air resistance. The first three forces originates from the article Water interaction model for boats in video games: Part 2, so you should read that article before you begin.

To make all this work you have to add new scripts and modify old scripts by adding more code. In total you will need the following:

  • BoatPhysics

  • BoatPhysicsMath

  • DebugPhysics

  • ModifyBoatMesh

  • SlammingForceData

  • TriangleData

BoatPhysics

This is a script you've already added, but it has to be modified. The main difference is that we also need to calculate the part of the boat that's above the water so we can add air resistance to it. This process is similar to how we calculated the mesh below the water.

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


//Main controller for all boat physics
public class BoatPhysics : MonoBehaviour 
{
    //Drags
    public GameObject boatMeshObj;
    public GameObject underWaterObj;
    public GameObject aboveWaterObj;

    //Change the center of mass
    public Vector3 centerOfMass;

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

    //Meshes for debugging
    private Mesh underWaterMesh;
    private Mesh aboveWaterMesh;

    //The boats rigidbody
    private Rigidbody boatRB;

    //The density of the water the boat is traveling in
    private float rhoWater = BoatPhysicsMath.RHO_OCEAN_WATER;
    private float rhoAir = BoatPhysicsMath.RHO_AIR;

    void Awake() 
    {
        boatRB = this.GetComponent<Rigidbody>();
    }

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

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

    void Update()
    {
        //Generate the under water and above water meshes
        modifyBoatMesh.GenerateUnderwaterMesh();

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

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

    void FixedUpdate() 
	{
        //Change the center of mass - experimental - move to Start() later
        boatRB.centerOfMass = centerOfMass;

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

        //Add forces to the part of the boat that's above the water
        if (modifyBoatMesh.aboveWaterTriangleData.Count > 0)
        {
            AddAboveWaterForces();
        }
    }

    //Add all forces that act on the squares below the water
    void AddUnderWaterForces()
    {
        //The resistance coefficient - same for all triangles
        float Cf = BoatPhysicsMath.ResistanceCoefficient(
            rhoWater, 
            boatRB.velocity.magnitude,
            modifyBoatMesh.CalculateUnderWaterLength());

        //To calculate the slamming force we need the velocity at each of the original triangles
        List<SlammingForceData> slammingForceData = modifyBoatMesh.slammingForceData;

        CalculateSlammingVelocities(slammingForceData);

        //Need this data for slamming forces
        float boatArea = modifyBoatMesh.boatArea;
        float boatMass = VisbyData.mass; //Replace this line with your boat's total mass

        //To connect the submerged triangles with the original triangles
        List<int> indexOfOriginalTriangle = modifyBoatMesh.indexOfOriginalTriangle;

        //Get all triangles
        List<TriangleData> underWaterTriangleData = modifyBoatMesh.underWaterTriangleData;
       
        for (int i = 0; i < underWaterTriangleData.Count; i++)
        {
            TriangleData triangleData = underWaterTriangleData[i];
            

            //Calculate the forces
            Vector3 forceToAdd = Vector3.zero;

            //Force 1 - The hydrostatic force (buoyancy)
            forceToAdd += BoatPhysicsMath.BuoyancyForce(rhoWater, triangleData);

            //Force 2 - Viscous Water Resistance
            forceToAdd += BoatPhysicsMath.ViscousWaterResistanceForce(rhoWater, triangleData, Cf);

            //Force 3 - Pressure drag
            forceToAdd += BoatPhysicsMath.PressureDragForce(triangleData);

            //Force 4 - Slamming force
            //Which of the original triangles is this triangle a part of
            int originalTriangleIndex = indexOfOriginalTriangle[i];

            SlammingForceData slammingData = slammingForceData[originalTriangleIndex];

            forceToAdd += BoatPhysicsMath.SlammingForce(slammingData, triangleData, boatArea, boatMass);


            //Add the forces to the boat
            boatRB.AddForceAtPosition(forceToAdd, triangleData.center);


            //Debug

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

            //Buoyancy
            //Debug.DrawRay(triangleData.center, BoatPhysicsMath.BuoyancyForce(rhoWater, triangleData).normalized * -3f, Color.blue);

            //Velocity
            //Debug.DrawRay(triangleCenter, triangleVelocityDir * 3f, Color.black);

            //Viscous Water Resistance
            //Debug.DrawRay(triangleCenter, viscousWaterResistanceForce.normalized * 3f, Color.black);
        }
    }



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

        //Loop through all triangles
        for (int i = 0; i < aboveWaterTriangleData.Count; i++)
        {
            TriangleData triangleData = aboveWaterTriangleData[i];


            //Calculate the forces
            Vector3 forceToAdd = Vector3.zero;

            //Force 1 - Air resistance 
			//Replace VisbyData.C_r with your boat's drag coefficient
            forceToAdd += BoatPhysicsMath.AirResistanceForce(rhoAir, triangleData, VisbyData.C_r);

            //Add the forces to the boat
            boatRB.AddForceAtPosition(forceToAdd, triangleData.center);


            //Debug

            //The normal
            //Debug.DrawRay(triangleCenter, triangleNormal * 3f, Color.white);

            //The velocity
            //Debug.DrawRay(triangleCenter, triangleVelocityDir * 3f, Color.black);

            if (triangleData.cosTheta > 0f)
            {
                //Debug.DrawRay(triangleCenter, triangleVelocityDir * 3f, Color.black);
            }

            //Air resistance
            //-3 to show it in the opposite direction to see what's going on
            //Debug.DrawRay(triangleCenter, airResistanceForce.normalized * -3f, Color.blue);
        }
    }



    //Calculate the current velocity at the center of each triangle of the original boat mesh
    private void CalculateSlammingVelocities(List<SlammingForceData> slammingForceData)
    {
        for (int i = 0; i < slammingForceData.Count; i++)
        {
            //Set the new velocity to the old velocity
            slammingForceData[i].previousVelocity = slammingForceData[i].velocity;

            //Center of the triangle in world space
            Vector3 center = transform.TransformPoint(slammingForceData[i].triangleCenter);

            //Get the current velocity at the center of the triangle
            slammingForceData[i].velocity = BoatPhysicsMath.GetTriangleVelocity(boatRB, center);
        }
    }
}

BoatPhysicsMath

To make the code cleaner I decided to add all math to its own script.

using UnityEngine;
using System.Collections;

//Equations that calculates boat physics forces
public static class BoatPhysicsMath
{
    //
    // Constants
    //
    
    //Densities [kg/m^3]

    //Fluid
    public const float RHO_WATER = 1000f;
    public const float RHO_OCEAN_WATER = 1027f;
    public const float RHO_SUNFLOWER_OIL = 920f;
    public const float RHO_MILK = 1035f;
    //Gas
    public const float RHO_AIR = 1.225f;
    public const float RHO_HELIUM = 0.164f;
    //Solid
    public const float RHO_GOLD = 19300f;

    //Drag coefficients
    public const float C_d_flat_plate_perpendicular_to_flow = 1.28f;

    //
    // Math
    //

    //Calculate the velocity at the center of the triangle
    public static Vector3 GetTriangleVelocity(Rigidbody boatRB, Vector3 triangleCenter)
    {
        //The connection formula for velocities (directly translated from Swedish)
        // v_A = v_B + omega_B cross r_BA
        // v_A - velocity in point A
        // v_B - velocity in point B
        // omega_B - angular velocity in point B
        // r_BA - vector between A and B

        Vector3 v_B = boatRB.velocity;

        Vector3 omega_B = boatRB.angularVelocity;

        Vector3 r_BA = triangleCenter - boatRB.worldCenterOfMass;

        Vector3 v_A = v_B + Vector3.Cross(omega_B, r_BA);

        return v_A;
    }

    //Calculate the area of a triangle with three coordinates
    public static float GetTriangleArea(Vector3 p1, Vector3 p2, Vector3 p3)
    {
        //Alternative 1 - Heron's formula
        float a = Vector3.Distance(p1, p2);
        //float b = Vector3.Distance(vertice_2_pos, vertice_3_pos);
        float c = Vector3.Distance(p3, p1);

        //float s = (a + b + c) / 2f;

        //float areaHeron = Mathf.Sqrt(s * (s-a) * (s-b) * (s-c));

        //Alternative 2 - Sinus
        float areaSin = (a * c * Mathf.Sin(Vector3.Angle(p2 - p1, p3 - p1) * Mathf.Deg2Rad)) / 2f;

        float area = areaSin;

        return area;
    }

    //
    // Buoyancy from http://www.gamasutra.com/view/news/237528/Water_interaction_model_for_boats_in_video_games.php
    //

    //The buoyancy force so the boat can float
    public static 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;

        //Check that the force is valid, such as not NaN to not break the physics model
        buoyancyForce = CheckForceIsValid(buoyancyForce, "Buoyancy");

        return buoyancyForce;
    }

    //
    // Resistance forces from http://www.gamasutra.com/view/news/263237/Water_interaction_model_for_boats_in_video_games_Part_2.php
    //

    //Force 1 - Viscous Water Resistance (Frictional Drag)
    public static Vector3 ViscousWaterResistanceForce(float rho, TriangleData triangleData, float Cf)
    {
        //Viscous resistance occurs when water sticks to the boat's surface and the boat has to drag that water with it

        // F = 0.5 * rho * v^2 * S * Cf
        // rho - density of the medium you have
        // v - speed
        // S - surface area
        // Cf - Coefficient of frictional resistance

        //We need the tangential velocity 
        //Projection of the velocity on the plane with the normal normalvec
        //http://www.euclideanspace.com/maths/geometry/elements/plane/lineOnPlane/
        Vector3 B = triangleData.normal;
        Vector3 A = triangleData.velocity;

        Vector3 velocityTangent = Vector3.Cross(B, (Vector3.Cross(A, B) / B.magnitude)) / B.magnitude;

        //The direction of the tangential velocity (-1 to get the flow which is in the opposite direction)
        Vector3 tangentialDirection = velocityTangent.normalized * -1f;

        //Debug.DrawRay(triangleCenter, tangentialDirection * 3f, Color.black);
        //Debug.DrawRay(triangleCenter, velocityVec.normalized * 3f, Color.blue);
        //Debug.DrawRay(triangleCenter, normal * 3f, Color.white);

        //The speed of the triangle as if it was in the tangent's direction
        //So we end up with the same speed as in the center of the triangle but in the direction of the flow
        Vector3 v_f_vec = triangleData.velocity.magnitude * tangentialDirection;

        //The final resistance force
        Vector3 viscousWaterResistanceForce = 0.5f * rho * v_f_vec.magnitude * v_f_vec * triangleData.area * Cf;

        viscousWaterResistanceForce = CheckForceIsValid(viscousWaterResistanceForce, "Viscous Water Resistance");

        return viscousWaterResistanceForce;
    }

    //The Coefficient of frictional resistance - belongs to Viscous Water Resistance but is same for all so calculate once
    public static float ResistanceCoefficient(float rho, float velocity, float length)
    {
        //Reynolds number

        // Rn = (V * L) / nu
        // V - speed of the body
        // L - length of the sumbmerged body
        // nu - viscosity of the fluid [m^2 / s]

        //Viscocity depends on the temperature, but at 20 degrees celcius:
        float nu = 0.000001f;
        //At 30 degrees celcius: nu = 0.0000008f; so no big difference

        //Reynolds number
        float Rn = (velocity * length) / nu;

        //The resistance coefficient
        float Cf = 0.075f / Mathf.Pow((Mathf.Log10(Rn) - 2f), 2f);

        return Cf;
    }

    //Force 2 - Pressure Drag Force
    public static Vector3 PressureDragForce(TriangleData triangleData)
    {
        //Modify for different turning behavior and planing forces
        //f_p and f_S - falloff power, should be smaller than 1
        //C - coefficients to modify 

        float velocity = triangleData.velocity.magnitude;

        //A reference speed used when modifying the parameters
        float velocityReference = velocity;

        velocity = velocity / velocityReference;

        Vector3 pressureDragForce = Vector3.zero;

        if (triangleData.cosTheta > 0f)
        {
            //float C_PD1 = 10f;
            //float C_PD2 = 10f;
            //float f_P = 0.5f;

            //To change the variables real-time - add the finished values later
            float C_PD1 = DebugPhysics.current.C_PD1;
            float C_PD2 = DebugPhysics.current.C_PD2;
            float f_P = DebugPhysics.current.f_P;

            pressureDragForce = -(C_PD1 * velocity + C_PD2 * (velocity * velocity)) * triangleData.area * Mathf.Pow(triangleData.cosTheta, f_P) * triangleData.normal;
        }
        else
        {
            //float C_SD1 = 10f;
            //float C_SD2 = 10f;
            //float f_S = 0.5f;

            //To change the variables real-time - add the finished values later
            float C_SD1 = DebugPhysics.current.C_SD1;
            float C_SD2 = DebugPhysics.current.C_SD2;
            float f_S = DebugPhysics.current.f_S;

            pressureDragForce = (C_SD1 * velocity + C_SD2 * (velocity * velocity)) * triangleData.area * Mathf.Pow(Mathf.Abs(triangleData.cosTheta), f_S) * triangleData.normal;
        }

        pressureDragForce = CheckForceIsValid(pressureDragForce, "Pressure drag");

        return pressureDragForce;
    }

    //Force 3 - Slamming Force (Water Entry Force)
    public static Vector3 SlammingForce(SlammingForceData slammingData, TriangleData triangleData, float boatArea, float boatMass)
    {
        //To capture the response of the fluid to sudden accelerations or penetrations

        //Add slamming if the normal is in the same direction as the velocity (the triangle is not receding from the water)
        //Also make sure thea area is not 0, which it sometimes is for some reason
        if (triangleData.cosTheta < 0f || slammingData.originalArea <= 0f)
        {
            return Vector3.zero;
        }
        
        
        //Step 1 - Calculate acceleration
        //Volume of water swept per second
        Vector3 dV = slammingData.submergedArea * slammingData.velocity;
        Vector3 dV_previous = slammingData.previousSubmergedArea * slammingData.previousVelocity;

        //Calculate the acceleration of the center point of the original triangle (not the current underwater triangle)
        //But the triangle the underwater triangle is a part of
        Vector3 accVec = (dV - dV_previous) / (slammingData.originalArea * Time.fixedDeltaTime);

        //The magnitude of the acceleration
        float acc = accVec.magnitude;

        //Debug.Log(slammingForceData.originalArea);

        //Step 2 - Calculate slamming force
        // F = clamp(acc / acc_max, 0, 1)^p * cos(theta) * F_stop
        // p - power to ramp up slamming force - should be 2 or more

        // F_stop = m * v * (2A / S)
        // m - mass of the entire boat
        // v - velocity
        // A - this triangle's area
        // S - total surface area of the entire boat

        Vector3 F_stop = boatMass * triangleData.velocity * ((2f * triangleData.area) / boatArea);

        //float p = DebugPhysics.current.p;

        //float acc_max = DebugPhysics.current.acc_max;

        float p = 2f;

        float acc_max = acc;

        float slammingCheat = DebugPhysics.current.slammingCheat;

        Vector3 slammingForce = Mathf.Pow(Mathf.Clamp01(acc / acc_max), p) * triangleData.cosTheta * F_stop * slammingCheat;

        //Vector3 slammingForce = Vector3.zero;

        //Debug.Log(slammingForce);

        //The force acts in the opposite direction
        slammingForce *= -1f;

        slammingForce = CheckForceIsValid(slammingForce, "Slamming");

        return slammingForce;   
        
    }

    //
    // Resistance forces from the book "Physics for Game Developers"
    //

    //Force 1 - Frictional drag - same as "Viscous Water Resistance" above, so empty
    //FrictionalDrag()
	
    //Force 2 - Residual resistance - similar to "Pressure Drag Forces" above
    public static float ResidualResistanceForce()
    {
        // R_r = R_pressure + R_wave = 0.5 * rho * v * v * S * C_r
        // rho - water density
        // v - speed of ship
        // S - surface area of the underwater portion of the hull
        // C_r - coefficient of residual resistance - increases as the displacement and speed increases

        //Coefficient of residual resistance
        //float C_r = 0.002f; //0.001 to 0.003

        //Final residual resistance
        //float residualResistanceForce = 0.5f * rho * v * v * S * C_r; 

        //return residualResistanceForce;

        float residualResistanceForce = 0f;

        return residualResistanceForce;
    }

	//Force 3 - Air resistance on the part of the ship above the water (typically 4 to 8 percent of total resistance)
	public static Vector3 AirResistanceForce(float rho, TriangleData triangleData, float C_air)
    {
        // R_air = 0.5 * rho * v^2 * A_p * C_air
        // rho - air density
        // v - speed of ship
        // A_p - projected transverse profile area of ship
        // C_r - coefficient of air resistance (drag coefficient)

        //Only add air resistance if normal is pointing in the same direction as the velocity
        if (triangleData.cosTheta < 0f)
        {
            return Vector3.zero;
        }

        //Find air resistance force
        Vector3 airResistanceForce = 0.5f * rho * triangleData.velocity.magnitude * triangleData.velocity * triangleData.area * C_air;

        //Acting in the opposite side of the velocity
        airResistanceForce *= -1f;

        airResistanceForce = CheckForceIsValid(airResistanceForce, "Air resistance");

        return airResistanceForce;
	}

    //Check that a force is not NaN
    private static Vector3 CheckForceIsValid(Vector3 force, string forceName)
    {
        if (!float.IsNaN(force.x + force.y + force.z))
        {
            return force;
        }
        else
        {
            Debug.Log(forceName += " force is NaN");

            return Vector3.zero;
        }
    }
}

DebugPhysics

If you read the article on Gamasutra you learned that all these forces are not 100 percent realistic, so you have to tweak the parameters to make it look good. So that's what this script is doing.

using UnityEngine;
using System.Collections;

//To be able to change the different physics parameters real time
public class DebugPhysics : MonoBehaviour 
{
    public static DebugPhysics current;

    //Force 2 - Pressure Drag Force
    [Header("Force 2 - Pressure Drag Force")]
    public float velocityReference;

    [Header("Pressure Drag")]
    public float C_PD1 = 10f;
    public float C_PD2 = 10f;
    public float f_P = 0.5f;

    [Header("Suction Drag")]
    public float C_SD1 = 10f;
    public float C_SD2 = 10f;
    public float f_S = 0.5f;

    //Force 3 - Slamming Force
    [Header("Force 3 - Slamming Force")]
    //Power used to ramp up slamming force
    public float p = 2f;
    public float acc_max;
    public float slammingCheat;

    void Start() 
	{
        current = this;
	}
}

ModifyBoatMesh

The script that previously generated the underwater mesh has grown exponentially in complexity. We now have to calculate the above water mesh and special data for the slamming force.

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

//Generates the mesh that's below and above 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;
    //The boats rigidbody
    private Rigidbody boatRB;

    //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 part of the boat that's under water - needed for calculations of length / volume
    private Mesh underWaterMesh;
    public List<TriangleData> underWaterTriangleData = new List<TriangleData>();

    //The part of the boat that's above water
    public List<TriangleData> aboveWaterTriangleData = new List<TriangleData>();

    //To approximate the underwater volume/length we need a mesh collider
    private MeshCollider underWaterMeshCollider;

    //Slamming resistance forces
    //Data that belongs to one triangle in the original boat mesh
    public List<SlammingForceData> slammingForceData = new List<SlammingForceData>();
    //To connect the submerged triangles with the original triangles
    public List<int> indexOfOriginalTriangle = new List<int>();
    //The total area of the entire boat
    public float boatArea;

    float timeSinceStart;

    public ModifyBoatMesh(GameObject boatObj, GameObject underWaterObj, GameObject aboveWaterObj, Rigidbody boatRB) 
    {
        //Get the transform
        boatTrans = boatObj.transform;

        //Get the rigid body
        this.boatRB = boatRB;

        //Get the meshcollider
        underWaterMeshCollider = underWaterObj.GetComponent<MeshCollider>();

        //Save the mesh
        underWaterMesh = underWaterObj.GetComponent<MeshFilter>().mesh;

        //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];

        //Setup the slamming force data
        for (int i = 0; i < (boatTriangles.Length / 3); i++)
        {
            slammingForceData.Add(new SlammingForceData());
        }

        //Calculate the area of the original triangles and the total area of the entire boat
        CalculateOriginalTrianglesArea();
    }

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

        //Switch the submerged triangle area with the one in the previous time step
        for (int j = 0; j < slammingForceData.Count; j++)
        {
            slammingForceData[j].previousSubmergedArea = slammingForceData[j].submergedArea;
        }

        indexOfOriginalTriangle.Clear();

        //Make sure we find the distance to water with the same time
        timeSinceStart = Time.time;


        //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, timeSinceStart);
        }

        //Add the triangles
        AddTriangles();
    }



    //Add all the triangles that's part of the underwater and abovewater meshes
    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 fake 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;
        int triangleCounter = 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)
            {
                Vector3 p1 = vertexData[0].globalVertexPos;
                Vector3 p2 = vertexData[1].globalVertexPos;
                Vector3 p3 = vertexData[2].globalVertexPos;

                //Save the triangle
                aboveWaterTriangleData.Add(new TriangleData(p1, p2, p3, boatRB, timeSinceStart));

                slammingForceData[triangleCounter].submergedArea = 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, boatRB, timeSinceStart));

                //We have already calculated the area of this triangle
                slammingForceData[triangleCounter].submergedArea = slammingForceData[triangleCounter].originalArea;

                indexOfOriginalTriangle.Add(triangleCounter);
            }
            //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, triangleCounter);
                }
                //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, triangleCounter);
                }
            }

            triangleCounter += 1;
        }
    }



    //Build the new triangles where one of the old vertices is above the water
    private void AddTrianglesOneAboveWater(List<VertexData> vertexData, int triangleCounter)
    {
        //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, boatRB, timeSinceStart));
        underWaterTriangleData.Add(new TriangleData(M, I_L, L, boatRB, timeSinceStart));
        //1 triangle above the water
        aboveWaterTriangleData.Add(new TriangleData(I_M, H, I_L, boatRB, timeSinceStart));

        //Calculate the total submerged area
        float totalArea = BoatPhysicsMath.GetTriangleArea(M, I_M, I_L) + BoatPhysicsMath.GetTriangleArea(M, I_L, L);

        slammingForceData[triangleCounter].submergedArea = totalArea;

        indexOfOriginalTriangle.Add(triangleCounter);
        //Add 2 times because 2 submerged triangles need to connect to the same original triangle
        indexOfOriginalTriangle.Add(triangleCounter);
    }



    //Build the new triangles where two of the old vertices are above the water
    private void AddTrianglesTwoAboveWater(List<VertexData> vertexData, int triangleCounter)
    {
        //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 above the water
        underWaterTriangleData.Add(new TriangleData(L, J_H, J_M, boatRB, timeSinceStart));
        //2 triangles below the water
        aboveWaterTriangleData.Add(new TriangleData(J_H, H, J_M, boatRB, timeSinceStart));
        aboveWaterTriangleData.Add(new TriangleData(J_M, H, M, boatRB, timeSinceStart));

        //Calculate the submerged area
        slammingForceData[triangleCounter].submergedArea = BoatPhysicsMath.GetTriangleArea(L, J_H, J_M);

        indexOfOriginalTriangle.Add(triangleCounter);
    }



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



    //Display the underwater or abovewater 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();

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

    //Calculate the length of the mesh that's below the water
    public float CalculateUnderWaterLength()
    {
        //Approximate the length as the length of the underwater mesh
        float underWaterLength = underWaterMesh.bounds.size.z;

        //Debug.Log(underWaterMesh.bounds.size.z);

        return underWaterLength;
    }

    //Calculate the area of each triangle in the boat mesh and store them in an array
    private void CalculateOriginalTrianglesArea()
    {
        //Loop through all the triangles (3 vertices at a time = 1 triangle)
        int i = 0;
        int triangleCounter = 0;
        while (i < boatTriangles.Length)
        {
            Vector3 p1 = boatVertices[boatTriangles[i]];

            i++;

            Vector3 p2 = boatVertices[boatTriangles[i]];

            i++;

            Vector3 p3 = boatVertices[boatTriangles[i]];

            i++;

            //Calculate the area of the triangle
            float triangleArea = BoatPhysicsMath.GetTriangleArea(p1, p2, p3);
       
            //Store the area in a list
            slammingForceData[triangleCounter].originalArea = triangleArea;

            //The total area
            boatArea += triangleArea;

            triangleCounter += 1;
        }
    }
}

SlammingForceData

This is a little class we need if we are going to calculate the slamming force.

using UnityEngine;
using System.Collections;

//Data that belongs to one triangle in the original boat mesh
//and is needed to calculate the slamming force
public class SlammingForceData 
{
    //The area of the original triangles - calculate once in the beginning because always the same
    public float originalArea;
    //How much area of a triangle in the whole boat is submerged
    public float submergedArea;
    //Same as above but previous time step
    public float previousSubmergedArea;
    //Need to save the center of the triangle to calculate the velocity
    public Vector3 triangleCenter;
    //Velocity
    public Vector3 velocity;
    //Same as above but previous time step
    public Vector3 previousVelocity;
}

TriangleData

And finally the modified TriangleData struct.

using UnityEngine;
using System.Collections;

//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;

    //The velocity of the triangle at the center
    public Vector3 velocity;

    //The velocity normalized
    public Vector3 velocityDir;

    //The angle between the normal and the velocity
    //Negative if pointing in the opposite direction
    //Positive if pointing in the same direction
    public float cosTheta;
	
    public TriangleData(Vector3 p1, Vector3 p2, Vector3 p3, Rigidbody boatRB, float timeSinceStart)
    {
        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, timeSinceStart));

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

        //Area of the triangle
        this.area = BoatPhysicsMath.GetTriangleArea(p1, p2, p3);

        //Velocity vector of the triangle at the center
        this.velocity = BoatPhysicsMath.GetTriangleVelocity(boatRB, this.center);

        //Velocity direction
        this.velocityDir = this.velocity.normalized;

        //Angle between the normal and the velocity
        //Negative if pointing in the opposite direction
        //Positive if pointing in the same direction
        this.cosTheta = Vector3.Dot(this.velocityDir, this.normal);
    }
}

Phew! If you now press play, tweak the parameters, you should now have a boat that's behaving more realistically!