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
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); } } }
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; } } }
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; } }
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; } } }
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; }
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!