using System; using UnityEngine; using System.Linq; using System.Collections.Generic; using UnityEngine.ProBuilder; namespace UnityEngine.ProBuilder.MeshOperations { /// /// Contains functions to help with face and edge extrusion. /// public static class ExtrudeElements { /// /// Extrudes a collection of faces. /// /// This is the equivalent of the [Extrude Faces](../manual/Face_Extrude.html) action. /// /// The source mesh. /// The faces to extrude. /// Describes how to extrude the faces (separately or as a group, either from averaged or individual normals). /// The distance to extrude faces. /// An array of the faces created as a result of the extrusion or null if `faces` is null or empty. public static Face[] Extrude(this ProBuilderMesh mesh, IEnumerable faces, ExtrudeMethod method, float distance) { switch (method) { case ExtrudeMethod.IndividualFaces: return ExtrudePerFace(mesh, faces, distance); default: return ExtrudeAsGroups(mesh, faces, method == ExtrudeMethod.FaceNormal, distance); } } /// /// Extrudes a collection of edges. /// /// This is the equivalent of the [Extrude Edges](../manual/Edge_Extrude.html) action. /// /// The source mesh. /// The edges to extrude. /// The distance to extrude. /// True to keep any shared vertices when extruding adjacent edges; false to split the shared vertex. /// True to allow this function to extrude [manifold](../manual/gloss.html#manifold) edges; false to disallow. /// The extruded edges, or null if the action failed due to manifold check or an empty edges parameter. public static Edge[] Extrude(this ProBuilderMesh mesh, IEnumerable edges, float distance, bool extrudeAsGroup, bool enableManifoldExtrude) { if (mesh == null) throw new ArgumentNullException("mesh"); if (edges == null) throw new ArgumentNullException("edges"); SharedVertex[] sharedIndexes = mesh.sharedVerticesInternal; List validEdges = new List(); List edgeFaces = new List(); foreach (Edge e in edges) { int faceCount = 0; Face fa = null; foreach (Face face in mesh.facesInternal) { if (mesh.IndexOf(face.edgesInternal, e) > -1) { fa = face; if (++faceCount > 1) break; } } if (enableManifoldExtrude || faceCount < 2) { validEdges.Add(e); edgeFaces.Add(fa); } } if (validEdges.Count < 1) return null; Vector3[] localVerts = mesh.positionsInternal; if (!mesh.HasArrays(MeshArrays.Normal)) mesh.Refresh(RefreshMask.Normals); IList oNormals = mesh.normals; int[] allEdgeIndexes = new int[validEdges.Count * 2]; int c = 0; for (int i = 0; i < validEdges.Count; i++) { allEdgeIndexes[c++] = validEdges[i].a; allEdgeIndexes[c++] = validEdges[i].b; } List extrudedIndexes = new List(); // used to set the editor selection to the newly created edges List newEdges = new List(); bool hasColors = mesh.HasArrays(MeshArrays.Color); // build out new faces around validEdges for (int i = 0; i < validEdges.Count; i++) { Edge edge = validEdges[i]; Face face = edgeFaces[i]; // Averages the normals using only vertices that are on the edge Vector3 xnorm = extrudeAsGroup ? InternalMeshUtility.AverageNormalWithIndexes(sharedIndexes[mesh.GetSharedVertexHandle(edge.a)], allEdgeIndexes, oNormals) : Math.Normal(mesh, face); Vector3 ynorm = extrudeAsGroup ? InternalMeshUtility.AverageNormalWithIndexes(sharedIndexes[mesh.GetSharedVertexHandle(edge.b)], allEdgeIndexes, oNormals) : Math.Normal(mesh, face); int x_sharedIndex = mesh.GetSharedVertexHandle(edge.a); int y_sharedIndex = mesh.GetSharedVertexHandle(edge.b); var positions = new Vector3[4] { localVerts[edge.a], localVerts[edge.b], localVerts[edge.a] + xnorm.normalized * distance, localVerts[edge.b] + ynorm.normalized * distance }; var colors = hasColors ? new Color[4] { mesh.colorsInternal[edge.a], mesh.colorsInternal[edge.b], mesh.colorsInternal[edge.a], mesh.colorsInternal[edge.b] } : null; Face newFace = mesh.AppendFace( positions, colors, new Vector2[4], new Vector4[4], new Vector4[4], new Face(new int[6] { 2, 1, 0, 2, 3, 1 }, face.submeshIndex, AutoUnwrapSettings.tile, 0, -1, -1, false), new int[4] { x_sharedIndex, y_sharedIndex, -1, -1 }); newEdges.Add(new Edge(newFace.indexesInternal[3], newFace.indexesInternal[4])); extrudedIndexes.Add(new Edge(x_sharedIndex, newFace.indexesInternal[3])); extrudedIndexes.Add(new Edge(y_sharedIndex, newFace.indexesInternal[4])); } // merge extruded vertex indexes with each other if (extrudeAsGroup) { for (int i = 0; i < extrudedIndexes.Count; i++) { int val = extrudedIndexes[i].a; for (int n = 0; n < extrudedIndexes.Count; n++) { if (n == i) continue; if (extrudedIndexes[n].a == val) { mesh.SetVerticesCoincident(new int[] { extrudedIndexes[n].b, extrudedIndexes[i].b }); break; } } } } // todo Should only need to invalidate caches on affected faces foreach (Face f in mesh.facesInternal) f.InvalidateCache(); return newEdges.ToArray(); } /// /// Splits any shared vertices so that this face may be moved independently of the GameObject. /// /// This is the equivalent of the [Detach Faces](../manual/Face_Detach.html) action. /// /// The source mesh. /// The faces to split from the mesh. /// The faces created forming the detached face group. public static List DetachFaces(this ProBuilderMesh mesh, IEnumerable faces) { return DetachFaces(mesh, faces, true); } /// /// Splits any shared vertices so that this face may be moved independently of the GameObject /// and optionally deletes the faces on the source geometry. /// /// This is the equivalent of the [Detach Faces](../manual/Face_Detach.html) action. /// /// The source mesh. /// The faces to split from the mesh. /// True to delete the faces on the source geometry where the faces were detached; false to keep them. /// The faces created forming the detached face group. public static List DetachFaces(this ProBuilderMesh mesh, IEnumerable faces, bool deleteSourceFaces) { if (mesh == null) throw new System.ArgumentNullException("mesh"); if (faces == null) throw new System.ArgumentNullException("faces"); List vertices = new List(mesh.GetVertices()); int sharedIndexOffset = mesh.sharedVerticesInternal.Length; var lookup = mesh.sharedVertexLookup; List detached = new List(); foreach (Face face in faces) { FaceRebuildData data = new FaceRebuildData(); data.vertices = new List(); data.sharedIndexes = new List(); data.face = new Face(face); Dictionary match = new Dictionary(); int[] indexes = new int[face.indexesInternal.Length]; for (int i = 0; i < face.indexesInternal.Length; i++) { int local; if (match.TryGetValue(face.indexesInternal[i], out local)) { indexes[i] = local; } else { local = data.vertices.Count; indexes[i] = local; match.Add(face.indexesInternal[i], local); data.vertices.Add(vertices[face.indexesInternal[i]]); data.sharedIndexes.Add(lookup[face.indexesInternal[i]] + sharedIndexOffset); } } data.face.indexesInternal = indexes.ToArray(); detached.Add(data); } FaceRebuildData.Apply(detached, mesh, vertices); if (deleteSourceFaces) { mesh.DeleteFaces(faces); } mesh.ToMesh(); return detached.Select(x => x.face).ToList(); } /// /// Extrude each face in faces individually along it's normal by distance. /// /// /// /// /// static Face[] ExtrudePerFace(ProBuilderMesh pb, IEnumerable faces, float distance) { Face[] faceArray = faces as Face[] ?? faces.ToArray(); if (!faceArray.Any()) return null; List vertices = new List(pb.GetVertices()); int sharedIndexMax = pb.sharedVerticesInternal.Length; int sharedIndexOffset = 0; int faceIndex = 0; Dictionary lookup = pb.sharedVertexLookup; Dictionary lookupUV = pb.sharedTextureLookup; Dictionary used = new Dictionary(); Face[] newFaces = new Face[faceArray.Sum(x => x.edges.Count)]; foreach (Face face in faceArray) { face.smoothingGroup = Smoothing.smoothingGroupNone; face.textureGroup = -1; Vector3 delta = Math.Normal(pb, face) * distance; Edge[] edges = face.edgesInternal; used.Clear(); for (int i = 0; i < edges.Length; i++) { int vc = vertices.Count; int x = edges[i].a, y = edges[i].b; if (!used.ContainsKey(x)) { used.Add(x, lookup[x]); lookup[x] = sharedIndexMax + (sharedIndexOffset++); } if (!used.ContainsKey(y)) { used.Add(y, lookup[y]); lookup[y] = sharedIndexMax + (sharedIndexOffset++); } lookup.Add(vc + 0, used[x]); lookup.Add(vc + 1, used[y]); lookup.Add(vc + 2, lookup[x]); lookup.Add(vc + 3, lookup[y]); Vertex xx = new Vertex(vertices[x]), yy = new Vertex(vertices[y]); xx.position += delta; yy.position += delta; vertices.Add(new Vertex(vertices[x])); vertices.Add(new Vertex(vertices[y])); vertices.Add(xx); vertices.Add(yy); Face bridge = new Face( new int[6] { vc + 0, vc + 1, vc + 2, vc + 1, vc + 3, vc + 2 }, face.submeshIndex, new AutoUnwrapSettings(face.uv), face.smoothingGroup, -1, -1, false ); newFaces[faceIndex++] = bridge; } for (int i = 0; i < face.distinctIndexesInternal.Length; i++) { vertices[face.distinctIndexesInternal[i]].position += delta; // Break any UV shared connections if (lookupUV != null && lookupUV.ContainsKey(face.distinctIndexesInternal[i])) lookupUV.Remove(face.distinctIndexesInternal[i]); } } pb.SetVertices(vertices); var fc = pb.faceCount; var nc = newFaces.Length; var appended = new Face[fc + nc]; Array.Copy(pb.facesInternal, 0, appended, 0, fc); Array.Copy(newFaces, 0, appended, fc, nc); pb.faces = appended; pb.SetSharedVertices(lookup); pb.SetSharedTextures(lookupUV); return newFaces; } /// /// Extrude faces as groups. /// /// /// /// /// /// static Face[] ExtrudeAsGroups(ProBuilderMesh mesh, IEnumerable faces, bool compensateAngleVertexDistance, float distance) { if (faces == null || !faces.Any()) return null; List vertices = new List(mesh.GetVertices()); int sharedIndexMax = mesh.sharedVerticesInternal.Length; int sharedIndexOffset = 0; Dictionary lookup = mesh.sharedVertexLookup; Dictionary lookupUV = mesh.sharedTextureLookup; List newFaces = new List(); // old triangle index -> old shared index Dictionary oldSharedMap = new Dictionary(); // old shared index -> new shared index Dictionary newSharedMap = new Dictionary(); // bridge face extruded edges, maps vertex index to new extruded vertex position Dictionary delayPosition = new Dictionary(); // used to average the direction of vertices shared by perimeter edges // key[shared index], value[normal count, normal sum] Dictionary>> extrudeMap = new Dictionary>>(); List wings = WingedEdge.GetWingedEdges(mesh, faces, true); List> groups = GetFaceGroups(wings); foreach (HashSet group in groups) { Dictionary perimeter = GetPerimeterEdges(group, lookup); newSharedMap.Clear(); oldSharedMap.Clear(); foreach (var edgeAndFace in perimeter) { EdgeLookup edge = edgeAndFace.Key; Face face = edgeAndFace.Value; int vc = vertices.Count; int x = edge.local.a, y = edge.local.b; if (!oldSharedMap.ContainsKey(x)) { oldSharedMap.Add(x, lookup[x]); int newSharedIndex = -1; if (newSharedMap.TryGetValue(lookup[x], out newSharedIndex)) { lookup[x] = newSharedIndex; } else { newSharedIndex = sharedIndexMax + (sharedIndexOffset++); newSharedMap.Add(lookup[x], newSharedIndex); lookup[x] = newSharedIndex; } } if (!oldSharedMap.ContainsKey(y)) { oldSharedMap.Add(y, lookup[y]); int newSharedIndex = -1; if (newSharedMap.TryGetValue(lookup[y], out newSharedIndex)) { lookup[y] = newSharedIndex; } else { newSharedIndex = sharedIndexMax + (sharedIndexOffset++); newSharedMap.Add(lookup[y], newSharedIndex); lookup[y] = newSharedIndex; } } lookup.Add(vc + 0, oldSharedMap[x]); lookup.Add(vc + 1, oldSharedMap[y]); lookup.Add(vc + 2, lookup[x]); lookup.Add(vc + 3, lookup[y]); delayPosition.Add(vc + 2, x); delayPosition.Add(vc + 3, y); vertices.Add(new Vertex(vertices[x])); vertices.Add(new Vertex(vertices[y])); // extruded edge will be positioned later vertices.Add(null); vertices.Add(null); Face bridge = new Face( new int[6] { vc + 0, vc + 1, vc + 2, vc + 1, vc + 3, vc + 2 }, face.submeshIndex, new AutoUnwrapSettings(face.uv), Smoothing.smoothingGroupNone, -1, -1, false ); newFaces.Add(bridge); } foreach (Face face in group) { // @todo keep together if possible face.textureGroup = -1; Vector3 normal = Math.Normal(mesh, face); for (int i = 0; i < face.distinctIndexesInternal.Length; i++) { int idx = face.distinctIndexesInternal[i]; // If this vertex is on the perimeter but not part of a perimeter edge // move the sharedIndex to match it's new value. if (!oldSharedMap.ContainsKey(idx) && newSharedMap.ContainsKey(lookup[idx])) lookup[idx] = newSharedMap[lookup[idx]]; int com = lookup[idx]; // Break any UV shared connections if (lookupUV != null && lookupUV.ContainsKey(face.distinctIndexesInternal[i])) lookupUV.Remove(face.distinctIndexesInternal[i]); // add the normal to the list of normals for this shared vertex SimpleTuple> dir; if (extrudeMap.TryGetValue(com, out dir)) { dir.item1 += normal; dir.item3.Add(idx); extrudeMap[com] = dir; } else { extrudeMap.Add(com, new SimpleTuple>(normal, normal, new List() { idx })); } } } } foreach (var kvp in extrudeMap) { Vector3 direction = (kvp.Value.item1 / kvp.Value.item3.Count); direction.Normalize(); // If extruding by face normal extend vertices on seams by the hypotenuse float modifier = compensateAngleVertexDistance ? Math.Secant(Vector3.Angle(direction, kvp.Value.item2) * Mathf.Deg2Rad) : 1f; direction.x *= distance * modifier; direction.y *= distance * modifier; direction.z *= distance * modifier; foreach (int i in kvp.Value.item3) { vertices[i].position += direction; } } foreach (var kvp in delayPosition) vertices[kvp.Key] = new Vertex(vertices[kvp.Value]); mesh.SetVertices(vertices); var fc = mesh.faceCount; var nc = newFaces.Count; var appended = new Face[fc + nc]; Array.Copy(mesh.facesInternal, 0, appended, 0, fc); for (int i = fc, c = fc + nc; i < c; i++) appended[i] = newFaces[i - fc]; mesh.faces = appended; mesh.SetSharedVertices(lookup); mesh.SetSharedTextures(lookupUV); return newFaces.ToArray(); } static List> GetFaceGroups(List wings) { HashSet used = new HashSet(); List> groups = new List>(); foreach (WingedEdge wing in wings) { if (used.Add(wing.face)) { HashSet group = new HashSet() { wing.face }; ElementSelection.Flood(wing, group); foreach (Face f in group) used.Add(f); groups.Add(group); } } return groups; } static Dictionary GetPerimeterEdges(HashSet faces, Dictionary lookup) { Dictionary perimeter = new Dictionary(); HashSet used = new HashSet(); foreach (Face face in faces) { foreach (Edge edge in face.edgesInternal) { EdgeLookup e = new EdgeLookup(lookup[edge.a], lookup[edge.b], edge.a, edge.b); if (!used.Add(e)) { if (perimeter.ContainsKey(e)) perimeter.Remove(e); } else { perimeter.Add(e, face); } } } return perimeter; } } }