using UnityEngine; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine.ProBuilder; using System; namespace UnityEngine.ProBuilder.MeshOperations { /// /// Functions for beveling edges. /// public static class Bevel { /// /// Apply a bevel to a set of edges. /// /// Target mesh. /// A set of edges to apply bevelling to. /// A value from 0 (bevel not at all) to 1 (bevel entire face). /// The new faces created to form the bevel. public static List BevelEdges(ProBuilderMesh mesh, IList edges, float amount) { if (mesh == null) throw new ArgumentNullException("mesh"); Dictionary lookup = mesh.sharedVertexLookup; List vertices = new List(mesh.GetVertices()); List m_edges = EdgeLookup.GetEdgeLookup(edges, lookup).Distinct().ToList(); List wings = WingedEdge.GetWingedEdges(mesh); List appendFaces = new List(); Dictionary> ignore = new Dictionary>(); HashSet slide = new HashSet(); int beveled = 0; Dictionary>>> holes = new Dictionary>>>(); // 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> spokes = WingedEdge.GetSpokes(wings); HashSet tested_common = new HashSet(); 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(appendFaces.Select(x => x.face)); Dictionary>> sorted = new Dictionary>>(); // 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 matches = wings.Where(x => x.edge.common.Contains(c) && !(ignore.ContainsKey(x.face) && ignore[x.face].Contains(c))); HashSet used = new HashSet(); foreach (WingedEdge match in matches) { if (!used.Add(match.face)) continue; sorted.AddOrAppend(match.face, new SimpleTuple(match, c)); } } // now go through those sorted faces and apply the vertex exploding, keeping track of any holes created foreach (KeyValuePair>> kvp in sorted) { // common index & list of vertices it was split into Dictionary> 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>(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> holesCommonIndexes = new List>(); // offset the indexes of holes and cull any potential holes that are less than 3 indexes (not a hole :) foreach (KeyValuePair>>> 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 holeCommon = new HashSet(); foreach (SimpleTuple> 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 modified = WingedEdge.GetWingedEdges(mesh, appendFaces.Select(x => x.face)); // now go through the holes and create faces for them vertices = new List(mesh.GetVertices()); List holeFaces = new List(); foreach (HashSet 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 v = new List(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 holePath = WingedEdge.SortCommonIndexesByAdjacency(modified, h); if (holePath != null) { List v = new List(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 adjacent = new HashSet(appendFaces.Select(x => x.face)); // and also just the filled holes HashSet newFaces = new HashSet(holeFaces.Select(x => x.face)); newFaces.UnionWith(createdFaces); // now append filled holes to the full list of added faces appendFaces.AddRange(holeFaces); List allNewFaceEdges = WingedEdge.GetWingedEdges(mesh, appendFaces.Select(x => x.face)); for (int i = 0; i < allNewFaceEdges.Count && newFaces.Count > 0; i++) { WingedEdge wing = allNewFaceEdges[i]; if (newFaces.Contains(wing.face)) { newFaces.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 (w.opposite != null && !newFaces.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 GetBridgeFaces( IList vertices, WingedEdge left, WingedEdge right, Dictionary>>> holes) { List faces = new List(); FaceRebuildData rf = new FaceRebuildData(); EdgeLookup a = left.edge; EdgeLookup b = right.edge; rf.vertices = new List() { 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>(rf, new List() { 0, 2 })); holes.AddOrAppend(a.common.b, new SimpleTuple>(rf, new List() { 1, 3 })); return faces; } static void SlideEdge(IList 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; } } }