using UnityEngine; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine.ProBuilder; using System; namespace UnityEngine.ProBuilder.MeshOperations { /// <summary> /// Functions for beveling edges. /// </summary> public static class Bevel { /// <summary> /// Apply a bevel to a set of edges. /// </summary> /// <param name="mesh">Target mesh.</param> /// <param name="edges">A set of edges to apply bevelling to.</param> /// <param name="amount">A value from 0 (bevel not at all) to 1 (bevel entire face).</param> /// <returns>The new faces created to form the bevel.</returns> public static List<Face> BevelEdges(ProBuilderMesh mesh, IList<Edge> edges, float amount) { if (mesh == null) throw new ArgumentNullException("mesh"); Dictionary<int, int> lookup = mesh.sharedVertexLookup; List<Vertex> vertices = new List<Vertex>(mesh.GetVertices()); List<EdgeLookup> m_edges = EdgeLookup.GetEdgeLookup(edges, lookup).Distinct().ToList(); List<WingedEdge> wings = WingedEdge.GetWingedEdges(mesh); List<FaceRebuildData> appendFaces = new List<FaceRebuildData>(); Dictionary<Face, List<int>> ignore = new Dictionary<Face, List<int>>(); HashSet<int> slide = new HashSet<int>(); int beveled = 0; Dictionary<int, List<SimpleTuple<FaceRebuildData, List<int>>>> holes = new Dictionary<int, List<SimpleTuple<FaceRebuildData, List<int>>>>(); // test every edge that will be moved along to make sure the bevel distance is appropriate. if it's not, adjust the max bevel amount // to suit. Dictionary<int, List<WingedEdge>> spokes = WingedEdge.GetSpokes(wings); HashSet<int> tested_common = new HashSet<int>(); foreach (EdgeLookup e in m_edges) { if (tested_common.Add(e.common.a)) { foreach (WingedEdge w in spokes[e.common.a]) { Edge le = w.edge.local; amount = Mathf.Min(Vector3.Distance(vertices[le.a].position, vertices[le.b].position) - .001f, amount); } } if (tested_common.Add(e.common.b)) { foreach (WingedEdge w in spokes[e.common.b]) { Edge le = w.edge.local; amount = Mathf.Min(Vector3.Distance(vertices[le.a].position, vertices[le.b].position) - .001f, amount); } } } if (amount < .001f) { Log.Info("Bevel Distance > Available Surface"); return null; } // iterate selected edges and move each leading edge back along it's direction // storing information about adjacent faces in the process foreach (EdgeLookup lup in m_edges) { WingedEdge we = wings.FirstOrDefault(x => x.edge.Equals(lup)); if (we == null || we.opposite == null) continue; beveled++; ignore.AddOrAppend(we.face, we.edge.common.a); ignore.AddOrAppend(we.face, we.edge.common.b); ignore.AddOrAppend(we.opposite.face, we.edge.common.a); ignore.AddOrAppend(we.opposite.face, we.edge.common.b); // after initial slides go back and split indirect triangles at the intersecting index into two vertices slide.Add(we.edge.common.a); slide.Add(we.edge.common.b); SlideEdge(vertices, we, amount); SlideEdge(vertices, we.opposite, amount); appendFaces.AddRange(GetBridgeFaces(vertices, we, we.opposite, holes)); } if (beveled < 1) { Log.Info("Cannot Bevel Open Edges"); return null; } // grab the "createdFaces" array now so that the selection returned is just the bridged faces // then add holes later var createdFaces = new List<Face>(appendFaces.Select(x => x.face)); Dictionary<Face, List<SimpleTuple<WingedEdge, int>>> sorted = new Dictionary<Face, List<SimpleTuple<WingedEdge, int>>>(); // sort the adjacent but affected faces into winged edge groups where each group contains a set of // unique winged edges pointing to the same face foreach (int c in slide) { IEnumerable<WingedEdge> matches = wings.Where(x => x.edge.common.Contains(c) && !(ignore.ContainsKey(x.face) && ignore[x.face].Contains(c))); HashSet<Face> used = new HashSet<Face>(); foreach (WingedEdge match in matches) { if (!used.Add(match.face)) continue; sorted.AddOrAppend(match.face, new SimpleTuple<WingedEdge, int>(match, c)); } } // now go through those sorted faces and apply the vertex exploding, keeping track of any holes created foreach (KeyValuePair<Face, List<SimpleTuple<WingedEdge, int>>> kvp in sorted) { // common index & list of vertices it was split into Dictionary<int, List<int>> appended; FaceRebuildData f = VertexEditing.ExplodeVertex(vertices, kvp.Value, amount, out appended); if (f == null) continue; appendFaces.Add(f); foreach (var apv in appended) { // organize holes by new face so that later we can compare the winding of the new face to the hole face // holes are sorted by key: common index value: face, vertex list holes.AddOrAppend(apv.Key, new SimpleTuple<FaceRebuildData, List<int>>(f, apv.Value)); } } FaceRebuildData.Apply(appendFaces, mesh, vertices); int removed = mesh.DeleteFaces(sorted.Keys).Length; mesh.sharedTextures = new SharedVertex[0]; mesh.sharedVertices = SharedVertex.GetSharedVerticesWithPositions(mesh.positionsInternal); // @todo don't rebuild indexes, keep 'em cached SharedVertex[] sharedIndexes = mesh.sharedVerticesInternal; lookup = mesh.sharedVertexLookup; List<HashSet<int>> holesCommonIndexes = new List<HashSet<int>>(); // offset the indexes of holes and cull any potential holes that are less than 3 indexes (not a hole :) foreach (KeyValuePair<int, List<SimpleTuple<FaceRebuildData, List<int>>>> hole in holes) { // less than 3 indexes in hole path; ain't a hole if (hole.Value.Sum(x => x.item2.Count) < 3) continue; HashSet<int> holeCommon = new HashSet<int>(); foreach (SimpleTuple<FaceRebuildData, List<int>> path in hole.Value) { int offset = path.item1.Offset() - removed; for (int i = 0; i < path.item2.Count; i++) holeCommon.Add(lookup[path.item2[i] + offset]); } holesCommonIndexes.Add(holeCommon); } List<WingedEdge> modified = WingedEdge.GetWingedEdges(mesh, appendFaces.Select(x => x.face)); // now go through the holes and create faces for them vertices = new List<Vertex>(mesh.GetVertices()); List<FaceRebuildData> holeFaces = new List<FaceRebuildData>(); foreach (HashSet<int> h in holesCommonIndexes) { // even if a set of hole indexes made it past the initial culling, the distinct part // may have reduced the index count if (h.Count < 3) { continue; } // skip sorting the path if it's just a triangle if (h.Count < 4) { List<Vertex> v = new List<Vertex>(mesh.GetVertices(h.Select(x => sharedIndexes[x][0]).ToList())); holeFaces.Add(AppendElements.FaceWithVertices(v)); } // if this hole has > 3 indexes, it needs a tent pole triangulation, which requires sorting into the perimeter order else { List<int> holePath = WingedEdge.SortCommonIndexesByAdjacency(modified, h); List<Vertex> v = new List<Vertex>(mesh.GetVertices(holePath.Select(x => sharedIndexes[x][0]).ToList())); holeFaces.AddRange(AppendElements.TentCapWithVertices(v)); } } FaceRebuildData.Apply(holeFaces, mesh, vertices); mesh.sharedVertices = SharedVertex.GetSharedVerticesWithPositions(mesh.positionsInternal); // go through new faces and conform hole normals // get a hash of just the adjacent and bridge faces // HashSet<pb_Face> adjacent = new HashSet<pb_Face>(appendFaces.Select(x => x.face)); // and also just the filled holes HashSet<Face> newHoles = new HashSet<Face>(holeFaces.Select(x => x.face)); // now append filled holes to the full list of added faces appendFaces.AddRange(holeFaces); List<WingedEdge> allNewFaceEdges = WingedEdge.GetWingedEdges(mesh, appendFaces.Select(x => x.face)); for (int i = 0; i < allNewFaceEdges.Count && newHoles.Count > 0; i++) { WingedEdge wing = allNewFaceEdges[i]; if (newHoles.Contains(wing.face)) { newHoles.Remove(wing.face); // find first edge whose opposite face isn't a filled hole* then // conform normal by that. // *or is a filled hole but has already been conformed using (var it = new WingedEdgeEnumerator(wing)) { while (it.MoveNext()) { var w = it.Current; if (!newHoles.Contains(w.opposite.face)) { w.face.submeshIndex = w.opposite.face.submeshIndex; w.face.uv = new AutoUnwrapSettings(w.opposite.face.uv); SurfaceTopology.ConformOppositeNormal(w.opposite); break; } } } } } mesh.ToMesh(); return createdFaces; } static readonly int[] k_BridgeIndexesTri = new int[] { 2, 1, 0 }; static List<FaceRebuildData> GetBridgeFaces( IList<Vertex> vertices, WingedEdge left, WingedEdge right, Dictionary<int, List<SimpleTuple<FaceRebuildData, List<int>>>> holes) { List<FaceRebuildData> faces = new List<FaceRebuildData>(); FaceRebuildData rf = new FaceRebuildData(); EdgeLookup a = left.edge; EdgeLookup b = right.edge; rf.vertices = new List<Vertex>() { vertices[a.local.a], vertices[a.local.b], vertices[a.common.a == b.common.a ? b.local.a : b.local.b], vertices[a.common.a == b.common.a ? b.local.b : b.local.a] }; Vector3 an = Math.Normal(vertices, left.face.indexesInternal); Vector3 bn = Math.Normal(rf.vertices, k_BridgeIndexesTri); int[] triangles = new int[] { 2, 1, 0, 2, 3, 1 }; if (Vector3.Dot(an, bn) < 0f) System.Array.Reverse(triangles); rf.face = new Face( triangles, left.face.submeshIndex, AutoUnwrapSettings.tile, -1, -1, -1, false); faces.Add(rf); holes.AddOrAppend(a.common.a, new SimpleTuple<FaceRebuildData, List<int>>(rf, new List<int>() { 0, 2 })); holes.AddOrAppend(a.common.b, new SimpleTuple<FaceRebuildData, List<int>>(rf, new List<int>() { 1, 3 })); return faces; } static void SlideEdge(IList<Vertex> vertices, WingedEdge we, float amount) { we.face.manualUV = true; we.face.textureGroup = -1; Edge slide_x = GetLeadingEdge(we, we.edge.common.a); Edge slide_y = GetLeadingEdge(we, we.edge.common.b); if (!slide_x.IsValid() || !slide_y.IsValid()) return; Vertex x = (vertices[slide_x.a] - vertices[slide_x.b]); x.Normalize(); Vertex y = (vertices[slide_y.a] - vertices[slide_y.b]); y.Normalize(); // need the pb_Vertex value to be modified, not reassigned in this array (which += does) vertices[we.edge.local.a].Add(x * amount); vertices[we.edge.local.b].Add(y * amount); } static Edge GetLeadingEdge(WingedEdge wing, int common) { if (wing.previous.edge.common.a == common) return new Edge(wing.previous.edge.local.b, wing.previous.edge.local.a); else if (wing.previous.edge.common.b == common) return new Edge(wing.previous.edge.local.a, wing.previous.edge.local.b); else if (wing.next.edge.common.a == common) return new Edge(wing.next.edge.local.b, wing.next.edge.local.a); else if (wing.next.edge.common.b == common) return new Edge(wing.next.edge.local.a, wing.next.edge.local.b); return Edge.Empty; } } }