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.
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):
Whenever I post a gif at least half the questions I get are about my water. It's a flipped mesh plus a foam skirt. Oldest trick in the book pic.twitter.com/ZOzwoB0GDz
— Oskar Stålberg (@OskSta) April 10, 2017
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):
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.
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:
Now we can create the final foam mesh:
What's going on in the method that's creating the foam mesh is this:
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; } }
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.
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.
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: