Our cube from the last part is sinking to the bottom of the ocean like a stone. To make it float realistically we have to add a force to it called buoyancy. The main reason I decided to make a boat in Unity was that I read an article in Gamasutra called "Water interaction model for boats in video games." It will explain the math and physics behind what we are going to do in this part, so read it before you begin creating the code. I have also used the same names on the variables as in that article to make it a little easier to follow the code because things will get complicated.
The basic summary of the Gamasutra article is that you have to find out which parts of the boat is below the water. Then you should add a buoyancy force to those parts.
What you need to know is that if you create a 3d object it will consist of triangles. Even the most complicated 3d object, like the ones you see in movies like Shrek, consist of a lot of triangles, but you can't see them because they are so small. So each side in our cube consists of 2 triangles.
In Unity you have to deal with at least 2 arrays to control the triangles. One triangle consists of 3 so-called vertices that each has a coordinate in 3d-space like x,y,z. A vertice is a corner in the triangle. One array will store these. The other array will store in which order the vertices form a triangle. When you build a triangle in Unity you have to store the position of the vertices in the array so they form a clockwise loop through all the corners. If you happen to store them counter-clockwise the triangle will be inside out and you will not see it (you will see it if you move to the other side of the triangle).
So to sum up: The boat (cube) consists of triangles. We need to find out if one of the triangles is submerged. If the entire triangle is below the water then we can just store it and add buoyancy to the entire triangle. But if only a part of the triangle is below the water, then we have to cut it in pieces and store the pieces that are below the water and add buoyancy to those pieces. It will look like this:
To make this work we need the following scripts:
BoatPhysics
ModifyBoatMesh
TriangleData
WaterController
This script will act as the parent to all other scripts we need. This is the only script we have to add to the cube to make it float.
using UnityEngine; using System.Collections; using System.Collections.Generic; namespace BoatTutorial { public class BoatPhysics : MonoBehaviour { //Drags public GameObject underWaterObj; //Script that's doing everything needed with the boat mesh, such as finding out which part is above the water private ModifyBoatMesh modifyBoatMesh; //Mesh for debugging private Mesh underWaterMesh; //The boats rigidbody private Rigidbody boatRB; //The density of the water the boat is traveling in private float rhoWater = 1027f; void Start() { //Get the boat's rigidbody boatRB = gameObject.GetComponent<Rigidbody>(); //Init the script that will modify the boat mesh modifyBoatMesh = new ModifyBoatMesh(gameObject); //Meshes that are below and above the water underWaterMesh = underWaterObj.GetComponent<MeshFilter>().mesh; } void Update() { //Generate the under water mesh modifyBoatMesh.GenerateUnderwaterMesh(); //Display the under water mesh modifyBoatMesh.DisplayMesh(underWaterMesh, "UnderWater Mesh", modifyBoatMesh.underWaterTriangleData); } void FixedUpdate() { //Add forces to the part of the boat that's below the water if (modifyBoatMesh.underWaterTriangleData.Count > 0) { AddUnderWaterForces(); } } //Add all forces that act on the squares below the water void AddUnderWaterForces() { //Get all triangles List<TriangleData> underWaterTriangleData = modifyBoatMesh.underWaterTriangleData; for (int i = 0; i < underWaterTriangleData.Count; i++) { //This triangle TriangleData triangleData = underWaterTriangleData[i]; //Calculate the buoyancy force Vector3 buoyancyForce = BuoyancyForce(rhoWater, triangleData); //Add the force to the boat boatRB.AddForceAtPosition(buoyancyForce, triangleData.center); //Debug //Normal Debug.DrawRay(triangleData.center, triangleData.normal * 3f, Color.white); //Buoyancy Debug.DrawRay(triangleData.center, buoyancyForce.normalized * -3f, Color.blue); } } //The buoyancy force so the boat can float private Vector3 BuoyancyForce(float rho, TriangleData triangleData) { //Buoyancy is a hydrostatic force - it's there even if the water isn't flowing or if the boat stays still // F_buoyancy = rho * g * V // rho - density of the mediaum you are in // g - gravity // V - volume of fluid directly above the curved surface // V = z * S * n // z - distance to surface // S - surface area // n - normal to the surface Vector3 buoyancyForce = rho * Physics.gravity.y * triangleData.distanceToSurface * triangleData.area * triangleData.normal; //The vertical component of the hydrostatic forces don't cancel out but the horizontal do buoyancyForce.x = 0f; buoyancyForce.z = 0f; return buoyancyForce; } } }
This script will cut the cube's triangles into the smaller pieces.
using UnityEngine; using System.Collections; using System.Collections.Generic; namespace BoatTutorial { //Generates the mesh that's below the water public class ModifyBoatMesh { //The boat transform needed to get the global position of a vertice private Transform boatTrans; //Coordinates of all vertices in the original boat Vector3[] boatVertices; //Positions in allVerticesArray, such as 0, 3, 5, to build triangles int[] boatTriangles; //So we only need to make the transformation from local to global once public Vector3[] boatVerticesGlobal; //Find all the distances to water once because some triangles share vertices, so reuse float[] allDistancesToWater; //The triangles belonging to the part of the boat that's under water public List<TriangleData> underWaterTriangleData = new List<TriangleData>(); public ModifyBoatMesh(GameObject boatObj) { //Get the transform boatTrans = boatObj.transform; //Init the arrays and lists boatVertices = boatObj.GetComponent<MeshFilter>().mesh.vertices; boatTriangles = boatObj.GetComponent<MeshFilter>().mesh.triangles; //The boat vertices in global position boatVerticesGlobal = new Vector3[boatVertices.Length]; //Find all the distances to water once because some triangles share vertices, so reuse allDistancesToWater = new float[boatVertices.Length]; } //Generate the underwater mesh public void GenerateUnderwaterMesh() { //Reset underWaterTriangleData.Clear(); //Find all the distances to water once because some triangles share vertices, so reuse for (int j = 0; j < boatVertices.Length; j++) { //The coordinate should be in global position Vector3 globalPos = boatTrans.TransformPoint(boatVertices[j]); //Save the global position so we only need to calculate it once here //And if we want to debug we can convert it back to local boatVerticesGlobal[j] = globalPos; allDistancesToWater[j] = WaterController.current.DistanceToWater(globalPos, Time.time); } //Add the triangles that are below the water AddTriangles(); } //Add all the triangles that's part of the underwater mesh private void AddTriangles() { //List that will store the data we need to sort the vertices based on distance to water List<VertexData> vertexData = new List<VertexData>(); //Add init data that will be replaced vertexData.Add(new VertexData()); vertexData.Add(new VertexData()); vertexData.Add(new VertexData()); //Loop through all the triangles (3 vertices at a time = 1 triangle) int i = 0; while (i < boatTriangles.Length) { //Loop through the 3 vertices for (int x = 0; x < 3; x++) { //Save the data we need vertexData[x].distance = allDistancesToWater[boatTriangles[i]]; vertexData[x].index = x; vertexData[x].globalVertexPos = boatVerticesGlobal[boatTriangles[i]]; i++; } //All vertices are above the water if (vertexData[0].distance > 0f && vertexData[1].distance > 0f && vertexData[2].distance > 0f) { continue; } //Create the triangles that are below the waterline //All vertices are underwater if (vertexData[0].distance < 0f && vertexData[1].distance < 0f && vertexData[2].distance < 0f) { Vector3 p1 = vertexData[0].globalVertexPos; Vector3 p2 = vertexData[1].globalVertexPos; Vector3 p3 = vertexData[2].globalVertexPos; //Save the triangle underWaterTriangleData.Add(new TriangleData(p1, p2, p3)); } //1 or 2 vertices are below the water else { //Sort the vertices vertexData.Sort((x, y) => x.distance.CompareTo(y.distance)); vertexData.Reverse(); //One vertice is above the water, the rest is below if (vertexData[0].distance > 0f && vertexData[1].distance < 0f && vertexData[2].distance < 0f) { AddTrianglesOneAboveWater(vertexData); } //Two vertices are above the water, the other is below else if (vertexData[0].distance > 0f && vertexData[1].distance > 0f && vertexData[2].distance < 0f) { AddTrianglesTwoAboveWater(vertexData); } } } } //Build the new triangles where one of the old vertices is above the water private void AddTrianglesOneAboveWater(List<VertexData> vertexData) { //H is always at position 0 Vector3 H = vertexData[0].globalVertexPos; //Left of H is M //Right of H is L //Find the index of M int M_index = vertexData[0].index - 1; if (M_index < 0) { M_index = 2; } //We also need the heights to water float h_H = vertexData[0].distance; float h_M = 0f; float h_L = 0f; Vector3 M = Vector3.zero; Vector3 L = Vector3.zero; //This means M is at position 1 in the List if (vertexData[1].index == M_index) { M = vertexData[1].globalVertexPos; L = vertexData[2].globalVertexPos; h_M = vertexData[1].distance; h_L = vertexData[2].distance; } else { M = vertexData[2].globalVertexPos; L = vertexData[1].globalVertexPos; h_M = vertexData[2].distance; h_L = vertexData[1].distance; } //Now we can calculate where we should cut the triangle to form 2 new triangles //because the resulting area will always form a square //Point I_M Vector3 MH = H - M; float t_M = -h_M / (h_H - h_M); Vector3 MI_M = t_M * MH; Vector3 I_M = MI_M + M; //Point I_L Vector3 LH = H - L; float t_L = -h_L / (h_H - h_L); Vector3 LI_L = t_L * LH; Vector3 I_L = LI_L + L; //Save the data, such as normal, area, etc //2 triangles below the water underWaterTriangleData.Add(new TriangleData(M, I_M, I_L)); underWaterTriangleData.Add(new TriangleData(M, I_L, L)); } //Build the new triangles where two of the old vertices are above the water private void AddTrianglesTwoAboveWater(List<VertexData> vertexData) { //H and M are above the water //H is after the vertice that's below water, which is L //So we know which one is L because it is last in the sorted list Vector3 L = vertexData[2].globalVertexPos; //Find the index of H int H_index = vertexData[2].index + 1; if (H_index > 2) { H_index = 0; } //We also need the heights to water float h_L = vertexData[2].distance; float h_H = 0f; float h_M = 0f; Vector3 H = Vector3.zero; Vector3 M = Vector3.zero; //This means that H is at position 1 in the list if (vertexData[1].index == H_index) { H = vertexData[1].globalVertexPos; M = vertexData[0].globalVertexPos; h_H = vertexData[1].distance; h_M = vertexData[0].distance; } else { H = vertexData[0].globalVertexPos; M = vertexData[1].globalVertexPos; h_H = vertexData[0].distance; h_M = vertexData[1].distance; } //Now we can find where to cut the triangle //Point J_M Vector3 LM = M - L; float t_M = -h_L / (h_M - h_L); Vector3 LJ_M = t_M * LM; Vector3 J_M = LJ_M + L; //Point J_H Vector3 LH = H - L; float t_H = -h_L / (h_H - h_L); Vector3 LJ_H = t_H * LH; Vector3 J_H = LJ_H + L; //Save the data, such as normal, area, etc //1 triangle below the water underWaterTriangleData.Add(new TriangleData(L, J_H, J_M)); } //Help class to store triangle data so we can sort the distances private class VertexData { //The distance to water from this vertex public float distance; //An index so we can form clockwise triangles public int index; //The global Vector3 position of the vertex public Vector3 globalVertexPos; } //Display the underwater mesh public void DisplayMesh(Mesh mesh, string name, List<TriangleData> triangesData) { List<Vector3> vertices = new List<Vector3>(); List<int> triangles = new List<int>(); //Build the mesh for (int i = 0; i < triangesData.Count; i++) { //From global coordinates to local coordinates Vector3 p1 = boatTrans.InverseTransformPoint(triangesData[i].p1); Vector3 p2 = boatTrans.InverseTransformPoint(triangesData[i].p2); Vector3 p3 = boatTrans.InverseTransformPoint(triangesData[i].p3); vertices.Add(p1); triangles.Add(vertices.Count - 1); vertices.Add(p2); triangles.Add(vertices.Count - 1); vertices.Add(p3); triangles.Add(vertices.Count - 1); } //Remove the old mesh mesh.Clear(); //Give it a name mesh.name = name; //Add the new vertices and triangles mesh.vertices = vertices.ToArray(); mesh.triangles = triangles.ToArray(); mesh.RecalculateBounds(); } } }
To be able to calculate the buoyancy force we have to calculate a few things for each triangle that's below the water, such as area of the triangle, and this script will store all those things. What you can see in the image below is the center of the triangle (where the while lines start) and the normal to the triangle (the white line).
using UnityEngine; using System.Collections; namespace BoatTutorial { //To save space so we don't have to send millions of parameters to each method public struct TriangleData { //The corners of this triangle in global coordinates public Vector3 p1; public Vector3 p2; public Vector3 p3; //The center of the triangle public Vector3 center; //The distance to the surface from the center of the triangle public float distanceToSurface; //The normal to the triangle public Vector3 normal; //The area of the triangle public float area; public TriangleData(Vector3 p1, Vector3 p2, Vector3 p3) { this.p1 = p1; this.p2 = p2; this.p3 = p3; //Center of the triangle this.center = (p1 + p2 + p3) / 3f; //Distance to the surface from the center of the triangle this.distanceToSurface = Mathf.Abs(WaterController.current.DistanceToWater(this.center, Time.time)); //Normal to the triangle this.normal = Vector3.Cross(p2 - p1, p3 - p1).normalized; //Area of the triangle float a = Vector3.Distance(p1, p2); float c = Vector3.Distance(p3, p1); this.area = (a * c * Mathf.Sin(Vector3.Angle(p2 - p1, p3 - p1) * Mathf.Deg2Rad)) / 2f; } } }
Will help us find the distance to water from a coordinate. Add this script to an empty gameobject in your scene. We will need the strange water parameters in the next part of the tutorial where we will create a moving sea, so keep them.
using UnityEngine; using System.Collections; //Controlls the water public class WaterController : MonoBehaviour { public static WaterController current; public bool isMoving; //Wave height and speed public float scale = 0.1f; public float speed = 1.0f; //The width between the waves public float waveDistance = 1f; //Noise parameters public float noiseStrength = 1f; public float noiseWalk = 1f; void Start() { current = this; } //Get the y coordinate from whatever wavetype we are using public float GetWaveYPos(Vector3 position, float timeSinceStart) { //if (isMoving) //{ //return WaveTypes.SinXWave(position, speed, scale, waveDistance, noiseStrength, noiseWalk, timeSinceStart); //} //else //{ //return 0f; //} return 0f; } //Find the distance from a vertice to water //Make sure the position is in global coordinates //Positive if above water //Negative if below water public float DistanceToWater(Vector3 position, float timeSinceStart) { float waterHeight = GetWaveYPos(position, timeSinceStart); float distanceToWater = position.y - waterHeight; return distanceToWater; } }
Now we can finally answer the question: "Will the cube float?" My cube, which weighs 800 kg, is floating. But is it also realistic? To find out we can ask our old friend Archimedes:
Any object, wholly or partially immersed in a fluid, is buoyed up by a force equal to the weight of the fluid displaced by the object.
What Archimedes is saying is that an object floats on water if it can displace a volume of water whose weight is greater than that of the object. So if our cube weighs more than 1000 kg, which is larger than the weight of the volume of displaced water if the density of water is 1000kg/m^3 and the cube's side is 1 m, then it should sink. If you change you cube's mass above/below 1000 kg then you should notice that it will float up/down, so this model is actually good.
This model will work for any mesh you have, so it doesn't have to be a cube. Test to change your object to a cylinder, a sphere, or a more complicated boat. But don't forget to change the mass!