using System.Collections.Generic; using System.Linq; using UnityEngine; namespace FIMSpace.Generating { public static partial class FMeshUtils { #region Weld V2 /// /// Vertex welding algorithm thanks to: ParadoxSolutions on the unity community forums /// https://forum.unity.com/threads/mesh-combine-that-preserves-shared-vertices.966650/#post-6294322 /// public static Mesh Weld2(this Mesh mesh, float bucketStep) { Vector3[] oldVertices = mesh.vertices; Vector3[] newVertices = new Vector3[oldVertices.Length]; Vector3[] newNormals = new Vector3[oldVertices.Length]; Color[] newColors = new Color[oldVertices.Length]; bool hasColors = mesh.colors.Length > 0; int[] old2new = new int[oldVertices.Length]; int newSize = 0; // UV Preserve Required Operations Dictionary uvCollect = new Dictionary(); for (int v = 0; v < mesh.vertices.Length; v++) { Vector3 vPos = mesh.vertices[v]; if (uvCollect.ContainsKey(vPos) == false) uvCollect.Add(vPos, mesh.uv[v]); } // Find AABB Vector3 min = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue); Vector3 max = new Vector3(float.MinValue, float.MinValue, float.MinValue); for (int i = 0; i < oldVertices.Length; i++) { if (oldVertices[i].x < min.x) min.x = oldVertices[i].x; if (oldVertices[i].y < min.y) min.y = oldVertices[i].y; if (oldVertices[i].z < min.z) min.z = oldVertices[i].z; if (oldVertices[i].x > max.x) max.x = oldVertices[i].x; if (oldVertices[i].y > max.y) max.y = oldVertices[i].y; if (oldVertices[i].z > max.z) max.z = oldVertices[i].z; } min -= Vector3.one * 0.111111f; max += Vector3.one * 0.899999f; // Make cubic buckets, each with dimensions "bucketStep" int bucketSizeX = Mathf.FloorToInt((max.x - min.x) / bucketStep) + 1; int bucketSizeY = Mathf.FloorToInt((max.y - min.y) / bucketStep) + 1; int bucketSizeZ = Mathf.FloorToInt((max.z - min.z) / bucketStep) + 1; List[,,] buckets = new List[bucketSizeX, bucketSizeY, bucketSizeZ]; // Make new vertices for (int i = 0; i < oldVertices.Length; i++) { // Determine which bucket it belongs to int x = Mathf.FloorToInt((oldVertices[i].x - min.x) / bucketStep); int y = Mathf.FloorToInt((oldVertices[i].y - min.y) / bucketStep); int z = Mathf.FloorToInt((oldVertices[i].z - min.z) / bucketStep); // Check to see if it's already been added if (buckets[x, y, z] == null) buckets[x, y, z] = new List(); // Make buckets lazily for (int j = 0; j < buckets[x, y, z].Count; j++) { Vector3 to = newVertices[buckets[x, y, z][j]] - oldVertices[i]; if (Vector3.SqrMagnitude(to) < 0.001f) { old2new[i] = buckets[x, y, z][j]; goto skip; // Skip to next old vertex if this one is already there } } // Add new vertex newVertices[newSize] = oldVertices[i]; if (hasColors) newColors[newSize] = mesh.colors[i]; newNormals[newSize] = mesh.normals[i]; buckets[x, y, z].Add(newSize); old2new[i] = newSize; newSize++; skip:; } // Make new triangles int[] oldTris = mesh.triangles; int[] newTris = new int[oldTris.Length]; for (int i = 0; i < oldTris.Length; i++) { newTris[i] = old2new[oldTris[i]]; } Vector3[] finalVertices = new Vector3[newSize]; Color[] finalCols = new Color[newSize]; Vector3[] finalNorms = new Vector3[newSize]; Vector2[] finalUvs = new Vector2[newSize]; for (int i = 0; i < newSize; i++) { finalVertices[i] = newVertices[i]; finalNorms[i] = newNormals[i]; finalCols[i] = newColors[i]; } // UV Preserve Operation for (int i = 0; i < newSize; i++) { finalUvs[i] = uvCollect[finalVertices[i]]; } mesh.Clear(); FMeshUtils.SetVerticesUnity2018(mesh, finalVertices); FMeshUtils.SetTrianglesUnity2018(mesh, newTris); FMeshUtils.SetNormalsUnity2018(mesh, finalNorms); if (hasColors) mesh.SetColorsUnity2018(finalCols.ToList()); mesh.SetUVUnity2018(finalUvs); mesh.RecalculateTangents(); return mesh; } #endregion /// /// Thanks to Tolin Simpson! /// https://github.com/TolinSimpson /// For great vertex velding algorithm. /// I did few changes to support UV transport and Vertex Color /// #region UV Packer public static class UVPacker { internal class Node { public Node Child1; public Node Child2; public Node Child3; public float X; public float Y; public float Width; public float Height; public bool Used; } public class Box { public float X; public float Y; public float Width; public float Height; public float ShiftX; public float ShiftY; public float Side; public MeshExtended Extended; public Vector2[] PackedUVs; } static Box UVBox(MeshExtended extended, float gap) { // find the minimum and maximum values for X and Y. Vector2[] uvs = extended.UVs; float xMin = Mathf.Infinity; float xMax = -Mathf.Infinity; float yMin = Mathf.Infinity; float yMax = -Mathf.Infinity; foreach (Vector2 v2 in uvs) { if (v2.x < xMin) { xMin = v2.x; } if (v2.x > xMax) { xMax = v2.x; } if (v2.y < yMin) { yMin = v2.y; } if (v2.y > yMax) { yMax = v2.y; } } // calculate the with and height plus a small gap definded by the user. float wid = xMax - xMin + gap; float hgt = yMax - yMin + gap; // calculate the box size base on the 3D size. Vector3 size = extended.Size; float sid = size.x * size.y * size.z; return new Box { Height = hgt, Width = wid, Side = sid, Extended = extended, ShiftX = xMin, ShiftY = yMin }; } static Node FindNode(Node node, float width, float height) { // Find an empty node with enough space to fit the box. if (node.Used) { Node next = node.Child1 == null ? null : FindNode(node.Child1, width, height); if (next != null) { return next; } next = node.Child2 == null ? null : FindNode(node.Child2, width, height); if (next != null) { return next; } return node.Child3 == null ? null : FindNode(node.Child3, width, height); } if (width <= node.Width && height <= node.Height) { return node; } return null; } static void SplitNode(Node node, Box box) { float x = node.X; float y = node.Y; box.X = x; box.Y = y; // calculate the space left to the right and upwards. float dw = node.Width - box.Side; float dl = node.Height - box.Side; // adjust the node size to the box size. node.Used = true; node.Width = box.Side; node.Height = box.Side; if (dw > 0) { // add a node to the right is there's unused horizontal space. node.Child1 = new Node { X = x + node.Width, Y = y, Width = dw, Height = node.Height }; } if (dl > 0) { // add a node above if there's unused vertical space. node.Child2 = new Node { X = x, Y = y + node.Height, Width = node.Width + dw, Height = dl }; } } static Node AttachNode(Node root, Box box) { // Mo empty space was found, so create some. Node used, empty, parent; float x, y; // chose where to add the new space, in such a way that over time the resulting container becomes closer to a square. if (root.Width > root.Height) { // calulate the space that wil be left unused if the node is added to the right. float dw = Mathf.Abs(root.Width - box.Side); x = 0; y = root.Height; // Create a node for the empty space. if (root.Width < box.Side) { empty = new Node { X = root.Width, Y = 0, Width = dw, Height = root.Height }; } else { empty = new Node { X = box.Side, Y = y, Width = dw, Height = box.Side }; } // Create a node for the box. used = new Node { X = 0, Y = y, Width = box.Side, Height = box.Side, Used = true }; // and add a new parent. parent = new Node { X = 0, Y = 0, Width = root.Width, Height = root.Height + box.Side, Used = true, Child1 = root, Child2 = used, Child3 = empty }; } else { // calulate the space that wil be left unused if the node is added above. float dl = Mathf.Abs(root.Height - box.Side); x = root.Width; y = 0; // Create a node for the empty space. if (root.Height < box.Side) { empty = new Node { X = 0, Y = root.Height, Width = root.Width, Height = dl }; } else { empty = new Node { X = x, Y = box.Side, Width = box.Side, Height = dl }; } // Create a node for the box. used = new Node { X = x, Y = 0, Width = box.Side, Height = box.Side, Used = true }; // and add a new parent. parent = new Node { X = 0, Y = 0, Width = root.Width + box.Side, Height = root.Height, Used = true, Child1 = root, Child2 = used, Child3 = empty }; } box.X = x; box.Y = y; return parent; } static void AdjustUVs(Box box) { // make sure the UV fit in the 0-1 space (inside the box) Vector2[] uv = box.Extended.UVs; int count = uv.Length; float sqx = box.Side / box.Width; float sqy = box.Side / box.Height; float x = box.X; float y = box.Y; float shiftx = box.ShiftX; float shifty = box.ShiftY; Vector2[] packed = new Vector2[count]; for (int u = 0; u < count; u++) { packed[u].x = x + (uv[u].x - shiftx) * sqx; packed[u].y = y + (uv[u].y - shifty) * sqy; } box.PackedUVs = packed; } static public Box Pack(MeshExtended extended, float gap) { Box boxs = UVBox(extended, gap); boxs.Side = 1f; // make a root node based on the first (biggest) box. Node root = new Node { Height = boxs.Side, Width = boxs.Side }; Node node = FindNode(root, boxs.Side, boxs.Side); if (node == null) { // if there's not enough space then add extra space. root = AttachNode(root, boxs); } else { // split the node to take only the necesary space. SplitNode(node, boxs); } // once we have all the boxes at their correspinding positions and scales, let's ajust the UVs to fit each box. AdjustUVs(boxs); return boxs; } } #endregion #region Combine Welded struct WeldHelperVert { public Color c; public Vector2 u; public WeldHelperVert(Color col, Vector2 uv) { c = col; u = uv; } } public static Mesh Weld(Mesh mesh, float gap) { MeshExtended extended = new MeshExtended(); extended.Prepare(mesh); UVPacker.Box boxes = UVPacker.Pack(extended, gap); #region Prepare a new mesh Mesh combined = new Mesh(); List combinedVertices = new List(); List combinedUVs = new List(); List combinedNormals = new List(); List combinedColors = new List(); List combinedTris = new List(); int triOffset = 0; #endregion // once welded and with the UVs packed we can just add items to lists. MeshExtended extmesh = boxes.Extended; Vector3[] vertices = extmesh.Vertices; Vector2[] uvs = boxes.PackedUVs; Vector3[] normals = extmesh.Normals; Dictionary welds = new Dictionary(); for (int i = 0; i < extmesh.Vertices.Length; i++) { Vector3 vertPos = vertices[i]; if (!welds.ContainsKey(vertPos)) { Color col = Color.white; if (i < extmesh.Colors.Length) col = extmesh.Colors[i]; welds.Add(vertPos, new WeldHelperVert(col, extmesh.UVs[i])); } } int[] triangles = extmesh.Triangles; int vertexCount = vertices.Length; int triCount = triangles.Length; for (int v = 0; v < vertexCount; v++) { combinedVertices.Add(vertices[v]); combinedUVs.Add(uvs[v]); combinedNormals.Add(normals[v]); var weldHelp = welds[vertices[v]]; combinedColors.Add(weldHelp.c); } for (int t = 0; t < triCount; t++) combinedTris.Add(triangles[t] + triOffset); // Create the new mesh combined.SetVertices(combinedVertices); combined.SetUVs(0, combinedUVs); combined.SetNormals(combinedNormals); combined.SetColors(combinedColors); combined.SetTriangles(combinedTris, 0); combined.RecalculateTangents(); return combined; } #endregion #region Mesh Extended /// /// Extended Mesh class for use in Mesh combining. /// public class MeshExtended { public enum SizeMethod { Bounds, WorldScale } class VertexUV { public Vector3 Position; public Vector2 UV; } public Vector3[] Vertices; public Color[] Colors; public Vector2[] UVs; public Vector3[] Normals; public int[] Triangles; public Vector3 Size; /// /// Finds vertices to weld. /// static int FindWeld(List list, Vector3 vertex, Vector2 uv) { // Find out if there's is a suitable vertext to weld. return list.FindIndex(e => { Vector3 p = e.Position; Vector3 u = e.UV; return p.x == vertex.x && p.y == vertex.y && p.z == vertex.z && u.x == uv.x && u.y == uv.y; // Notice we take into account the UVs, otherwise we will lose texture info. }); } /// /// Prepares a mesh to be combined. /// public void Prepare(Mesh mesh) { #region Get data from the mesh Vector3[] vertices = mesh.vertices; Vector2[] uvs = mesh.uv; Vector3[] normals = mesh.normals; Color[] colors = mesh.colors; int[] triangles = mesh.triangles; int vertexCount = vertices.Length; int triCount = triangles.Length; List wVertices = new List(); List wNormals = new List(); List wColor = new List(); List wTriangles = new List(); #endregion for (int v = 0; v < vertexCount; v++) { Vector3 currentVertex = vertices[v]; Vector2 currentUV = uvs[v]; int index = FindWeld(wVertices, currentVertex, currentUV); if (index == -1) { // no weld point found, so add the vertex and UV to the list. wVertices.Add(new VertexUV { Position = currentVertex, UV = currentUV }); wNormals.Add(normals[v]); if (colors.Length > 0) wColor.Add(colors[v]); index = wVertices.Count - 1; } // the value of v and index will be always equal unless a weld point was found. if (v != index) for (int t = 0; t < triCount; t++) { // if there's a weld, we need to update the triangles. if (triangles[t] == v) { triangles[t] = index; } } } // wait untill all the vertex are processed to update the triangle's list. for (int t = 0; t < triCount; t++) { wTriangles.Add(triangles[t]); } // Save mesh data. Normals = wNormals.ToArray(); Colors = wColor.ToArray(); Triangles = wTriangles.ToArray(); // Calculate the UV scale. Size = mesh.bounds.size; //Size = Vector3.one; // Save vertices and UVs int pcount = wVertices.Count; Vertices = new Vector3[pcount]; UVs = new Vector2[pcount]; for (int v = 0; v < pcount; v++) { // convert local mesh coordinates to world space. Vector3 worldPos = wVertices[v].Position; // convert world space to the root's local space. Vertices[v] = worldPos; UVs[v] = wVertices[v].UV; } } } #endregion } }