/// <summary> /// Attempts to find edges along an Edge loop. /// /// http://wiki.blender.org/index.php/Doc:2.4/Manual/Modeling/Meshes/Selecting/Edges says: /// First check to see if the selected element connects to only 3 other edges. /// If the edge in question has already been added to the list, the selection ends. /// Of the 3 edges that connect to the current edge, the ones that share a face with the current edge are eliminated /// and the remaining edge is added to the list and is made the current edge. /// </summary> /// <param name="mesh"></param> /// <param name="edges"></param> /// <param name="loop"></param> /// <returns></returns> internal static bool GetEdgeLoop(ProBuilderMesh mesh, IEnumerable <Edge> edges, out Edge[] loop) { List <WingedEdge> wings = WingedEdge.GetWingedEdges(mesh); IEnumerable <EdgeLookup> m_edgeLookup = EdgeLookup.GetEdgeLookup(edges, mesh.sharedVertexLookup); HashSet <EdgeLookup> sources = new HashSet <EdgeLookup>(m_edgeLookup); HashSet <EdgeLookup> used = new HashSet <EdgeLookup>(); for (int i = 0; i < wings.Count; i++) { if (used.Contains(wings[i].edge) || !sources.Contains(wings[i].edge)) { continue; } bool completeLoop = GetEdgeLoopInternal(wings[i], wings[i].edge.common.b, used); // loop didn't close if (!completeLoop) { GetEdgeLoopInternal(wings[i], wings[i].edge.common.a, used); } } loop = used.Select(x => x.local).ToArray(); return(true); }
void ExtrudeEdge() { // fetch a random perimeter edge connected to the last face extruded List <WingedEdge> wings = WingedEdge.GetWingedEdges(m_Mesh); IEnumerable <WingedEdge> sourceWings = wings.Where(x => x.face == m_LastExtrudedFace); List <Edge> nonManifoldEdges = sourceWings.Where(x => x.opposite == null).Select(y => y.edge.local).ToList(); int rand = (int)Random.Range(0, nonManifoldEdges.Count); Edge sourceEdge = nonManifoldEdges[rand]; // get the direction this edge should extrude in var edgeCenter = Math.Average(m_Mesh.positions, new[] { sourceEdge.a, sourceEdge.b }); var faceCenter = Math.Average(m_Mesh.positions, m_LastExtrudedFace.distinctIndexes); Vector3 dir = (edgeCenter - faceCenter).normalized; // this will be populated with the extruded edge Edge[] extrudedEdges; // perform extrusion extrudedEdges = m_Mesh.Extrude(new Edge[] { sourceEdge }, 0f, false, true); // get the last extruded face m_LastExtrudedFace = m_Mesh.faces.Last(); // translate the vertices m_Mesh.TranslateVertices(extrudedEdges, dir * distance); // rebuild mesh with new geometry added by extrude m_Mesh.ToMesh(); // rebuild mesh normals, textures, collisions, etc m_Mesh.Refresh(); }
/// <summary> /// Provided two faces, this method will attempt to project @f2 and align its size, rotation, and position to match /// the shared edge on f1. Returns true on success, false otherwise. /// </summary> /// <param name="mesh"></param> /// <param name="f1"></param> /// <param name="f2"></param> /// <param name="channel"></param> /// <returns></returns> public static bool AutoStitch(ProBuilderMesh mesh, Face f1, Face f2, int channel) { var wings = WingedEdge.GetWingedEdges(mesh, new [] { f1, f2 }); var sharedEdge = wings.FirstOrDefault(x => x.face == f1 && x.opposite != null && x.opposite.face == f2); if (sharedEdge == null) { return(false); } if (f1.manualUV) { f2.manualUV = true; } f1.textureGroup = -1; f2.textureGroup = -1; Projection.PlanarProject(mesh, f2); if (AlignEdges(mesh, f2, sharedEdge.edge.local, sharedEdge.opposite.edge.local, channel)) { if (!f2.manualUV) { UvUnwrapping.SetAutoAndAlignUnwrapParamsToUVs(mesh, new [] { f2 }); } return(true); } return(false); }
public static List <List <Edge> > FindHoles(ProBuilderMesh mesh, HashSet <int> common) { List <List <Edge> > holes = new List <List <Edge> >(); List <WingedEdge> wings = WingedEdge.GetWingedEdges(mesh); foreach (List <WingedEdge> hole in FindHoles(wings, common)) { holes.Add(hole.Select(x => x.edge.local).ToList()); } return(holes); }
/// <summary> /// Find any holes touching one of the passed vertex indexes. /// </summary> /// <param name="mesh"></param> /// <param name="indexes"></param> /// <returns></returns> internal static List <List <Edge> > FindHoles(ProBuilderMesh mesh, IEnumerable <int> indexes) { HashSet <int> common = mesh.GetSharedVertexHandles(indexes); List <List <Edge> > holes = new List <List <Edge> >(); List <WingedEdge> wings = WingedEdge.GetWingedEdges(mesh); foreach (List <WingedEdge> hole in FindHoles(wings, common)) { holes.Add(hole.Select(x => x.edge.local).ToList()); } return(holes); }
/// <summary> /// Grow faces to include any face touching the perimeter edges. /// </summary> /// <param name="mesh">The source mesh.</param> /// <param name="faces">The faces to grow out from.</param> /// <param name="maxAngleDiff">If provided, adjacent faces must have a normal that is within maxAngleDiff (in degrees) difference of the perimeter face.</param> /// <returns>The original faces selection, plus any new faces added as a result the grow operation.</returns> public static HashSet <Face> GrowSelection(ProBuilderMesh mesh, IEnumerable <Face> faces, float maxAngleDiff = -1f) { List <WingedEdge> wings = WingedEdge.GetWingedEdges(mesh, true); HashSet <Face> source = new HashSet <Face>(faces); HashSet <Face> neighboring = new HashSet <Face>(); Vector3 srcNormal = Vector3.zero; bool checkAngle = maxAngleDiff > 0f; for (int i = 0; i < wings.Count; i++) { if (!source.Contains(wings[i].face)) { continue; } if (checkAngle) { srcNormal = Math.Normal(mesh, wings[i].face); } using (var it = new WingedEdgeEnumerator(wings[i])) { while (it.MoveNext()) { var w = it.Current; if (w.opposite != null && !source.Contains(w.opposite.face)) { if (checkAngle) { Vector3 oppNormal = Math.Normal(mesh, w.opposite.face); if (Vector3.Angle(srcNormal, oppNormal) < maxAngleDiff) { neighboring.Add(w.opposite.face); } } else { neighboring.Add(w.opposite.face); } } } } } return(neighboring); }
/// <summary> /// Iterates through face edges and builds a list using the opposite edge, iteratively. /// </summary> /// <param name="pb">The probuilder mesh</param> /// <param name="edges">The edges already selected</param> /// <returns>The new selected edges</returns> internal static IEnumerable <Edge> GetEdgeRingIterative(ProBuilderMesh pb, IEnumerable <Edge> edges) { List <WingedEdge> wings = WingedEdge.GetWingedEdges(pb); List <EdgeLookup> edgeLookup = EdgeLookup.GetEdgeLookup(edges, pb.sharedVertexLookup).ToList(); edgeLookup = edgeLookup.Distinct().ToList(); Dictionary <Edge, WingedEdge> wings_dic = new Dictionary <Edge, WingedEdge>(); for (int i = 0; i < wings.Count; i++) { if (!wings_dic.ContainsKey(wings[i].edge.common)) { wings_dic.Add(wings[i].edge.common, wings[i]); } } HashSet <EdgeLookup> used = new HashSet <EdgeLookup>(); for (int i = 0, c = edgeLookup.Count; i < c; i++) { WingedEdge we; if (!wings_dic.TryGetValue(edgeLookup[i].common, out we)) { continue; } WingedEdge cur = we; if (!used.Contains(cur.edge)) { used.Add(cur.edge); } var next = EdgeRingNext(cur); if (next != null && next.opposite != null && !used.Contains(next.edge)) { used.Add(next.edge); } var prev = EdgeRingNext(cur.opposite); if (prev != null && prev.opposite != null && !used.Contains(prev.edge)) { used.Add(prev.edge); } } return(used.Select(x => x.local)); }
/// <summary> /// Ensure that all adjacent face normals are pointing in a uniform direction. This function supports multiple islands of connected faces, but it may not unify each island the same way. /// </summary> /// <param name="mesh">The mesh that the faces belong to.</param> /// <param name="faces">The faces to make uniform.</param> /// <returns>The state of the action.</returns> public static ActionResult ConformNormals(this ProBuilderMesh mesh, IEnumerable <Face> faces) { List <WingedEdge> wings = WingedEdge.GetWingedEdges(mesh, faces); HashSet <Face> used = new HashSet <Face>(); int count = 0; // this loop adds support for multiple islands of grouped selections for (int i = 0; i < wings.Count; i++) { if (used.Contains(wings[i].face)) { continue; } Dictionary <Face, bool> flags = new Dictionary <Face, bool>(); GetWindingFlags(wings[i], true, flags); int flip = 0; foreach (var kvp in flags) { flip += kvp.Value ? 1 : -1; } bool direction = flip > 0; foreach (var kvp in flags) { if (direction != kvp.Value) { count++; kvp.Key.Reverse(); } } used.UnionWith(flags.Keys); } if (count > 0) { return(new ActionResult(ActionResult.Status.Success, count > 1 ? string.Format("Flipped {0} faces", count) : "Flipped 1 face")); } else { return(new ActionResult(ActionResult.Status.NoChange, "Faces Uniform")); } }
/// <summary> /// Recursively add all faces touching any of the selected faces. /// </summary> /// <param name="mesh">The source mesh.</param> /// <param name="faces">The starting faces.</param> /// <param name="maxAngleDiff">Faces must have a normal that is within maxAngleDiff (in degrees) difference of the perimeter face to be added to the collection.</param> /// <returns>A collection of faces that are connected by shared edges to the original faces.</returns> public static HashSet <Face> FloodSelection(ProBuilderMesh mesh, IList <Face> faces, float maxAngleDiff) { List <WingedEdge> wings = WingedEdge.GetWingedEdges(mesh, true); HashSet <Face> source = new HashSet <Face>(faces); HashSet <Face> flood = new HashSet <Face>(); for (int i = 0; i < wings.Count; i++) { if (!flood.Contains(wings[i].face) && source.Contains(wings[i].face)) { flood.Add(wings[i].face); Flood(mesh, wings[i], maxAngleDiff > 0f ? Math.Normal(mesh, wings[i].face) : Vector3_Zero, maxAngleDiff, flood); } } return(flood); }
static IEnumerable <List <Face> > GetFaceSelectionGroups(ProBuilderMesh mesh) { var wings = WingedEdge.GetWingedEdges(mesh, mesh.selectedFacesInternal, true); var filter = new HashSet <Face>(); var groups = new List <List <Face> >(); foreach (var wing in wings) { var group = new List <Face>() { }; CollectAdjacentFaces(wing, filter, group); if (group.Count > 0) { groups.Add(group); } } return(groups); }
/// <summary> /// Fetch a face loop. /// </summary> /// <param name="mesh">The source mesh.</param> /// <param name="faces">The faces to scan for loops.</param> /// <param name="ring">Toggles between loop and ring. Ring and loop are arbritary with faces, so this parameter just toggles between which gets scanned first.</param> /// <returns>A collection of faces gathered by extending a ring or loop,</returns> public static HashSet <Face> GetFaceLoop(ProBuilderMesh mesh, Face[] faces, bool ring = false) { if (mesh == null) { throw new ArgumentNullException("mesh"); } if (faces == null) { throw new ArgumentNullException("faces"); } HashSet <Face> loops = new HashSet <Face>(); List <WingedEdge> wings = WingedEdge.GetWingedEdges(mesh); foreach (Face face in faces) { loops.UnionWith(GetFaceLoop(wings, face, ring)); } return(loops); }
static IEnumerable <List <Face> > GetFaceSelectionGroups(ProBuilderMesh mesh) { var wings = WingedEdge.GetWingedEdges(mesh, mesh.selectedFacesInternal, true); var filter = new HashSet <Face>(); var groups = new List <List <Face> >(); var groupIdx = -1; var i = -1; foreach (var wing in wings) { var group = new List <Face>() { }; CollectAdjacentFaces(wing, filter, group); if (group.Count > 0) { i++; // Make sure the last selected face is the last in the group var idx = group.IndexOf(mesh.selectedFacesInternal[mesh.selectedFacesInternal.Length - 1]); if (idx != -1 && idx < group.Count) { var item = group[idx]; groupIdx = i; group[idx] = group[group.Count - 1]; group[group.Count - 1] = item; } groups.Add(group); } } // Make sure the last selected face's group is the last in the groups if (groupIdx != -1 && groupIdx < groups.Count) { var item = groups[groupIdx]; groups[groupIdx] = groups[groups.Count - 1]; groups[groups.Count - 1] = item; } return(groups); }
/// <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); }
public void FindSpecialSides() { if (this.pb.faces.Count < 5) { return; } this.topFace = this.pb.faces[0]; var topWingedEdges = WingedEdge.GetWingedEdges(this.pb, new Face[] { this.topFace }, false); // For now, this only works on blocks with a building with 4 sides. if (this.topFace.edges.Count != 4) { return; } this.fourSides = true; float shortestDistance = 0f; // find shortest side: foreach (var wingedEdge in topWingedEdges) { var vertices = this.pb.GetVertices(new List <int>() { wingedEdge.edge.local.a, wingedEdge.edge.local.b }); var edgeLength = Vector3.Distance(vertices[0].position, vertices[1].position); if (shortestDistance == 0 || edgeLength < shortestDistance) { shortestDistance = edgeLength; this.topFaceShortestEdge = wingedEdge.edge.local; this.topFaceShortestEdgeCommon = wingedEdge.edge.common; } } // search the opposite edge (i.e. the edge that shares no corners / vertices with the shortest edge) foreach (var wingedEdge in topWingedEdges) { var edge = wingedEdge.edge.local; if (edge.a != this.topFaceShortestEdge.a && edge.a != this.topFaceShortestEdge.b && edge.b != this.topFaceShortestEdge.a && edge.b != this.topFaceShortestEdge.b) { this.topFaceOppositeEdge = edge; this.topFaceOppositeEdgeCommon = wingedEdge.edge.common; break; } } var wingedEdges = WingedEdge.GetWingedEdges(this.pb, this.pb.faces, false); foreach (var wingedEdge in wingedEdges) { if (wingedEdge.edge.common == this.topFaceShortestEdgeCommon && wingedEdge.face != this.topFace) { this.frontFace = wingedEdge.face; this.frontFaceTopEdge = wingedEdge.edge.local; } if (wingedEdge.edge.common == this.topFaceOppositeEdgeCommon && wingedEdge.face != this.topFace) { this.backFace = wingedEdge.face; this.backFaceTopEdge = wingedEdge.edge.local; } } }
public override void FillHoles() { int filled = 0; foreach (ProBuilderMesh mesh in m_edgeSelection.Meshes) { //MeshSelection selection = new MeshSelection(); //selection.SelectedEdges.Add(mesh, m_edgeSelection.GetEdges(mesh)); //selection.EdgesToVertices(false); HashSet <int> indexes = new HashSet <int>(); for (int i = 0; i < mesh.vertexCount; ++i) { indexes.Add(i); } List <List <Edge> > holes = PBElementSelection.FindHoles(mesh, indexes); mesh.ToMesh(); List <WingedEdge> wings = WingedEdge.GetWingedEdges(mesh); HashSet <Face> appendedFaces = new HashSet <Face>(); // const bool wholePath = false; foreach (List <Edge> hole in holes) { List <int> holeIndexes; Face face; if (!hole.All(e => m_edgeSelection.IsSelected(mesh, e))) { continue; } //if (wholePath) //{ // // if selecting whole path and in edge mode, make sure the path contains // // at least one complete edge from the selection. // if (!hole.Any(x => common.Contains(x.edge.common.a) && // common.Contains(x.edge.common.b))) // continue; // holeIndexes = hole.Select(x => x.edge.local.a).ToList(); // face = AppendElements.CreatePolygon(mesh, holeIndexes, false); //} //else { //IEnumerable<WingedEdge> selected = hole.Where(x => common.Contains(x.edge.common.a)); //holeIndexes = selected.Select(x => x.edge.local.a).ToList(); //holeIndexes = hole.Select(x => x.edge.local.a).ToList(); //face = AppendElements.CreatePolygon(mesh, holeIndexes, true); holeIndexes = hole.Select(x => x.a).ToList(); face = AppendElements.CreatePolygon(mesh, holeIndexes, true); } if (face != null) { filled++; appendedFaces.Add(face); } } mesh.SetSelectedFaces(appendedFaces); wings = WingedEdge.GetWingedEdges(mesh); // make sure the appended faces match the first adjacent face found // both in winding and face properties foreach (var appendedFace in appendedFaces) { var wing = wings.FirstOrDefault(x => x.face == appendedFace); if (wing == null) { continue; } using (var it = new WingedEdgeEnumerator(wing)) { while (it.MoveNext()) { if (it.Current == null) { continue; } var currentWing = it.Current; var oppositeFace = it.Current.opposite != null ? it.Current.opposite.face : null; if (oppositeFace != null && !appendedFaces.Contains(oppositeFace)) { currentWing.face.submeshIndex = oppositeFace.submeshIndex; currentWing.face.uv = new AutoUnwrapSettings(oppositeFace.uv); PBSurfaceTopology.ConformOppositeNormal(currentWing.opposite); break; } } } } mesh.ToMesh(); mesh.Refresh(); //mesh.Optimize(); } }
/// <summary> /// Inserts new edges connecting the passed edges, optionally restricting new edge insertion to faces in faceMask. /// </summary> /// <param name="mesh"></param> /// <param name="edges"></param> /// <param name="addedFaces"></param> /// <param name="connections"></param> /// <param name="returnFaces"></param> /// <param name="returnEdges"></param> /// <param name="faceMask"></param> /// <returns></returns> internal static ActionResult Connect( this ProBuilderMesh mesh, IEnumerable <Edge> edges, out Face[] addedFaces, out Edge[] connections, bool returnFaces = false, bool returnEdges = false, HashSet <Face> faceMask = null) { Dictionary <int, int> lookup = mesh.sharedVertexLookup; Dictionary <int, int> lookupUV = mesh.sharedTextureLookup; HashSet <EdgeLookup> distinctEdges = new HashSet <EdgeLookup>(EdgeLookup.GetEdgeLookup(edges, lookup)); List <WingedEdge> wings = WingedEdge.GetWingedEdges(mesh); // map each edge to a face so that we have a list of all touched faces with their to-be-subdivided edges Dictionary <Face, List <WingedEdge> > touched = new Dictionary <Face, List <WingedEdge> >(); foreach (WingedEdge wing in wings) { if (distinctEdges.Contains(wing.edge)) { List <WingedEdge> faceEdges; if (touched.TryGetValue(wing.face, out faceEdges)) { faceEdges.Add(wing); } else { touched.Add(wing.face, new List <WingedEdge>() { wing }); } } } Dictionary <Face, List <WingedEdge> > affected = new Dictionary <Face, List <WingedEdge> >(); // weed out edges that won't actually connect to other edges (if you don't play ya' can't stay) foreach (KeyValuePair <Face, List <WingedEdge> > kvp in touched) { if (kvp.Value.Count <= 1) { WingedEdge opp = kvp.Value[0].opposite; if (opp == null) { continue; } List <WingedEdge> opp_list; if (!touched.TryGetValue(opp.face, out opp_list)) { continue; } if (opp_list.Count <= 1) { continue; } } affected.Add(kvp.Key, kvp.Value); } List <Vertex> vertices = new List <Vertex>(mesh.GetVertices()); List <ConnectFaceRebuildData> results = new List <ConnectFaceRebuildData>(); // just the faces that where connected with > 1 edge List <Face> connectedFaces = new List <Face>(); HashSet <int> usedTextureGroups = new HashSet <int>(mesh.facesInternal.Select(x => x.textureGroup)); int newTextureGroupIndex = 1; // do the splits foreach (KeyValuePair <Face, List <WingedEdge> > split in affected) { Face face = split.Key; List <WingedEdge> targetEdges = split.Value; int inserts = targetEdges.Count; Vector3 nrm = Math.Normal(vertices, face.indexesInternal); if (inserts == 1 || (faceMask != null && !faceMask.Contains(face))) { ConnectFaceRebuildData c; if (InsertVertices(face, targetEdges, vertices, out c)) { Vector3 fn = Math.Normal(c.faceRebuildData.vertices, c.faceRebuildData.face.indexesInternal); if (Vector3.Dot(nrm, fn) < 0) { c.faceRebuildData.face.Reverse(); } results.Add(c); } } else if (inserts > 1) { List <ConnectFaceRebuildData> res = inserts == 2 ? ConnectEdgesInFace(face, targetEdges[0], targetEdges[1], vertices) : ConnectEdgesInFace(face, targetEdges, vertices); if (face.textureGroup < 0) { while (usedTextureGroups.Contains(newTextureGroupIndex)) { newTextureGroupIndex++; } usedTextureGroups.Add(newTextureGroupIndex); } if (res == null) { connections = null; addedFaces = null; return(new ActionResult(ActionResult.Status.Failure, "Unable to connect faces")); } else { foreach (ConnectFaceRebuildData c in res) { connectedFaces.Add(c.faceRebuildData.face); Vector3 fn = Math.Normal(c.faceRebuildData.vertices, c.faceRebuildData.face.indexesInternal); if (Vector3.Dot(nrm, fn) < 0) { c.faceRebuildData.face.Reverse(); } c.faceRebuildData.face.textureGroup = face.textureGroup < 0 ? newTextureGroupIndex : face.textureGroup; c.faceRebuildData.face.uv = new AutoUnwrapSettings(face.uv); c.faceRebuildData.face.submeshIndex = face.submeshIndex; c.faceRebuildData.face.smoothingGroup = face.smoothingGroup; c.faceRebuildData.face.manualUV = face.manualUV; } results.AddRange(res); } } } FaceRebuildData.Apply(results.Select(x => x.faceRebuildData), mesh, vertices, null); mesh.sharedTextures = new SharedVertex[0]; int removedVertexCount = mesh.DeleteFaces(affected.Keys).Length; mesh.sharedVertices = SharedVertex.GetSharedVerticesWithPositions(mesh.positionsInternal); mesh.ToMesh(); // figure out where the new edges where inserted if (returnEdges) { // offset the newVertexIndexes by whatever the FaceRebuildData did so we can search for the new edges by index var appended = new HashSet <int>(); for (int n = 0; n < results.Count; n++) { for (int i = 0; i < results[n].newVertexIndexes.Count; i++) { appended.Add((results[n].newVertexIndexes[i] + results[n].faceRebuildData.Offset()) - removedVertexCount); } } Dictionary <int, int> lup = mesh.sharedVertexLookup; IEnumerable <Edge> newEdges = results.SelectMany(x => x.faceRebuildData.face.edgesInternal).Where(x => appended.Contains(x.a) && appended.Contains(x.b)); IEnumerable <EdgeLookup> distNewEdges = EdgeLookup.GetEdgeLookup(newEdges, lup); connections = distNewEdges.Distinct().Select(x => x.local).ToArray(); } else { connections = null; } if (returnFaces) { addedFaces = connectedFaces.ToArray(); } else { addedFaces = null; } return(new ActionResult(ActionResult.Status.Success, string.Format("Connected {0} Edges", results.Count / 2))); }
/// <summary> /// Iterates through face edges and builds a list using the opposite edge. /// </summary> /// <param name="pb"></param> /// <param name="edges"></param> /// <returns></returns> internal static IEnumerable <Edge> GetEdgeRing(ProBuilderMesh pb, IEnumerable <Edge> edges) { List <WingedEdge> wings = WingedEdge.GetWingedEdges(pb); List <EdgeLookup> edgeLookup = EdgeLookup.GetEdgeLookup(edges, pb.sharedVertexLookup).ToList(); edgeLookup = edgeLookup.Distinct().ToList(); Dictionary <Edge, WingedEdge> wings_dic = new Dictionary <Edge, WingedEdge>(); for (int i = 0; i < wings.Count; i++) { if (!wings_dic.ContainsKey(wings[i].edge.common)) { wings_dic.Add(wings[i].edge.common, wings[i]); } } HashSet <EdgeLookup> used = new HashSet <EdgeLookup>(); for (int i = 0, c = edgeLookup.Count; i < c; i++) { WingedEdge we; if (!wings_dic.TryGetValue(edgeLookup[i].common, out we) || used.Contains(we.edge)) { continue; } WingedEdge cur = we; while (cur != null) { if (!used.Add(cur.edge)) { break; } cur = EdgeRingNext(cur); if (cur != null && cur.opposite != null) { cur = cur.opposite; } } cur = EdgeRingNext(we.opposite); if (cur != null && cur.opposite != null) { cur = cur.opposite; } // run in both directions while (cur != null) { if (!used.Add(cur.edge)) { break; } cur = EdgeRingNext(cur); if (cur != null && cur.opposite != null) { cur = cur.opposite; } } } return(used.Select(x => x.local)); }
/// <summary> /// Extrude faces as groups. /// </summary> /// <param name="mesh"></param> /// <param name="faces"></param> /// <param name="compensateAngleVertexDistance"></param> /// <param name="distance"></param> /// <returns></returns> static Face[] ExtrudeAsGroups(ProBuilderMesh mesh, IEnumerable <Face> faces, bool compensateAngleVertexDistance, float distance) { if (faces == null || !faces.Any()) { return(null); } List <Vertex> vertices = new List <Vertex>(mesh.GetVertices()); int sharedIndexMax = mesh.sharedVerticesInternal.Length; int sharedIndexOffset = 0; Dictionary <int, int> lookup = mesh.sharedVertexLookup; Dictionary <int, int> lookupUV = mesh.sharedTextureLookup; List <Face> newFaces = new List <Face>(); // old triangle index -> old shared index Dictionary <int, int> oldSharedMap = new Dictionary <int, int>(); // old shared index -> new shared index Dictionary <int, int> newSharedMap = new Dictionary <int, int>(); // bridge face extruded edges, maps vertex index to new extruded vertex position Dictionary <int, int> delayPosition = new Dictionary <int, int>(); // used to average the direction of vertices shared by perimeter edges // key[shared index], value[normal count, normal sum] Dictionary <int, SimpleTuple <Vector3, Vector3, List <int> > > extrudeMap = new Dictionary <int, SimpleTuple <Vector3, Vector3, List <int> > >(); List <WingedEdge> wings = WingedEdge.GetWingedEdges(mesh, faces, true); List <HashSet <Face> > groups = GetFaceGroups(wings); foreach (HashSet <Face> group in groups) { Dictionary <EdgeLookup, Face> 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 <Vector3, Vector3, List <int> > dir; if (extrudeMap.TryGetValue(com, out dir)) { dir.item1 += normal; dir.item3.Add(idx); extrudeMap[com] = dir; } else { extrudeMap.Add(com, new SimpleTuple <Vector3, Vector3, List <int> >(normal, normal, new List <int>() { 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()); }
public static List <Face> ToQuads(this ProBuilderMesh mesh, IList <Face> faces, bool smoothing = true) { HashSet <Face> processed = new HashSet <Face>(); List <WingedEdge> wings = WingedEdge.GetWingedEdges(mesh, faces, true); // build a lookup of the strength of edge connections between triangle faces Dictionary <EdgeLookup, float> connections = new Dictionary <EdgeLookup, float>(); for (int i = 0; i < wings.Count; i++) { using (var it = new WingedEdgeEnumerator(wings[i])) { while (it.MoveNext()) { var border = it.Current; if (border.opposite != null && !connections.ContainsKey(border.edge)) { float score = mesh.GetQuadScore(border, border.opposite); connections.Add(border.edge, score); } } } } List <SimpleTuple <Face, Face> > quads = new List <SimpleTuple <Face, Face> >(); // move through each face and find it's best quad neighbor foreach (WingedEdge face in wings) { if (!processed.Add(face.face)) { continue; } float bestScore = 0f; Face buddy = null; using (var it = new WingedEdgeEnumerator(face)) { while (it.MoveNext()) { var border = it.Current; if (border.opposite != null && processed.Contains(border.opposite.face)) { continue; } float borderScore; // only add it if the opposite face's best score is also this face if (connections.TryGetValue(border.edge, out borderScore) && borderScore > bestScore && face.face == GetBestQuadConnection(border.opposite, connections)) { bestScore = borderScore; buddy = border.opposite.face; } } } if (buddy != null) { processed.Add(buddy); quads.Add(new SimpleTuple <Face, Face>(face.face, buddy)); } } // don't collapse coincident vertices if smoothing is enabled, we need the original normals intact return(MergeElements.MergePairs(mesh, quads, smoothing)); }
/// <summary> /// Import mesh data from a GameObject's MeshFilter.sharedMesh and MeshRenderer.sharedMaterials. /// </summary> /// <param name="originalMesh">The UnityEngine.Mesh to extract attributes from.</param> /// <param name="materials">The materials array corresponding to the originalMesh submeshes.</param> /// <param name="importSettings">Optional settings parameter defines import customization properties.</param> /// <exception cref="NotSupportedException">Import only supports triangle and quad mesh topologies.</exception> public void Import(MeshImportSettings importSettings = null) { if (importSettings == null) { importSettings = k_DefaultImportSettings; } // When importing the mesh is always split into triangles with no vertices shared // between faces. In a later step co-incident vertices are collapsed (eg, before // leaving the Import function). Vertex[] sourceVertices = m_SourceMesh.GetVertices(); List <Vertex> splitVertices = new List <Vertex>(); List <Face> faces = new List <Face>(); // Fill in Faces array with just the position indexes. In the next step we'll // figure out smoothing groups & merging int vertexIndex = 0; int materialCount = m_SourceMaterials != null ? m_SourceMaterials.Length : 0; for (int submeshIndex = 0; submeshIndex < m_SourceMesh.subMeshCount; submeshIndex++) { switch (m_SourceMesh.GetTopology(submeshIndex)) { case MeshTopology.Triangles: { int[] indexes = m_SourceMesh.GetIndices(submeshIndex); for (int tri = 0; tri < indexes.Length; tri += 3) { faces.Add(new Face( new int[] { vertexIndex, vertexIndex + 1, vertexIndex + 2 }, Math.Clamp(submeshIndex, 0, materialCount - 1), AutoUnwrapSettings.tile, Smoothing.smoothingGroupNone, -1, -1, true)); splitVertices.Add(sourceVertices[indexes[tri]]); splitVertices.Add(sourceVertices[indexes[tri + 1]]); splitVertices.Add(sourceVertices[indexes[tri + 2]]); vertexIndex += 3; } } break; case MeshTopology.Quads: { int[] indexes = m_SourceMesh.GetIndices(submeshIndex); for (int quad = 0; quad < indexes.Length; quad += 4) { faces.Add(new Face(new int[] { vertexIndex, vertexIndex + 1, vertexIndex + 2, vertexIndex + 2, vertexIndex + 3, vertexIndex + 0 }, Math.Clamp(submeshIndex, 0, materialCount - 1), AutoUnwrapSettings.tile, Smoothing.smoothingGroupNone, -1, -1, true)); splitVertices.Add(sourceVertices[indexes[quad]]); splitVertices.Add(sourceVertices[indexes[quad + 1]]); splitVertices.Add(sourceVertices[indexes[quad + 2]]); splitVertices.Add(sourceVertices[indexes[quad + 3]]); vertexIndex += 4; } } break; default: throw new NotSupportedException("ProBuilder only supports importing triangle and quad meshes."); } } m_Vertices = splitVertices.ToArray(); m_Destination.Clear(); m_Destination.SetVertices(m_Vertices); m_Destination.faces = faces; m_Destination.sharedVertices = SharedVertex.GetSharedVerticesWithPositions(m_Destination.positionsInternal); m_Destination.sharedTextures = new SharedVertex[0]; HashSet <Face> processed = new HashSet <Face>(); if (importSettings.quads) { List <WingedEdge> wings = WingedEdge.GetWingedEdges(m_Destination, m_Destination.facesInternal, true); // build a lookup of the strength of edge connections between triangle faces Dictionary <EdgeLookup, float> connections = new Dictionary <EdgeLookup, float>(); for (int i = 0; i < wings.Count; i++) { using (var it = new WingedEdgeEnumerator(wings[i])) { while (it.MoveNext()) { var border = it.Current; if (border.opposite != null && !connections.ContainsKey(border.edge)) { float score = GetQuadScore(border, border.opposite); connections.Add(border.edge, score); } } } } List <SimpleTuple <Face, Face> > quads = new List <SimpleTuple <Face, Face> >(); // move through each face and find it's best quad neighbor foreach (WingedEdge face in wings) { if (!processed.Add(face.face)) { continue; } float bestScore = 0f; Face buddy = null; using (var it = new WingedEdgeEnumerator(face)) { while (it.MoveNext()) { var border = it.Current; if (border.opposite != null && processed.Contains(border.opposite.face)) { continue; } float borderScore; // only add it if the opposite face's best score is also this face if (connections.TryGetValue(border.edge, out borderScore) && borderScore > bestScore && face.face == GetBestQuadConnection(border.opposite, connections)) { bestScore = borderScore; buddy = border.opposite.face; } } } if (buddy != null) { processed.Add(buddy); quads.Add(new SimpleTuple <Face, Face>(face.face, buddy)); } } // don't collapse coincident vertices if smoothing is enabled, we need the original normals intact MergeElements.MergePairs(m_Destination, quads, !importSettings.smoothing); } if (importSettings.smoothing) { Smoothing.ApplySmoothingGroups(m_Destination, m_Destination.facesInternal, importSettings.smoothingAngle, m_Vertices.Select(x => x.normal).ToArray()); // After smoothing has been applied go back and weld coincident vertices created by MergePairs. MergeElements.CollapseCoincidentVertices(m_Destination, m_Destination.facesInternal); } }
protected override ActionResult PerformActionImplementation() { if (MeshSelection.selectedObjectCount < 1) { return(ActionResult.NoSelection); } UndoUtility.RecordSelection("Fill Hole"); ActionResult res = new ActionResult(ActionResult.Status.NoChange, "No Holes Found"); int filled = 0; bool wholePath = m_SelectEntirePath; foreach (ProBuilderMesh mesh in MeshSelection.topInternal) { bool selectAll = mesh.selectedIndexesInternal == null || mesh.selectedIndexesInternal.Length < 1; IEnumerable <int> indexes = selectAll ? mesh.facesInternal.SelectMany(x => x.indexes) : mesh.selectedIndexesInternal; mesh.ToMesh(); List <WingedEdge> wings = WingedEdge.GetWingedEdges(mesh); HashSet <int> common = mesh.GetSharedVertexHandles(indexes); List <List <WingedEdge> > holes = ElementSelection.FindHoles(wings, common); HashSet <Face> appendedFaces = new HashSet <Face>(); foreach (List <WingedEdge> hole in holes) { List <int> holeIndexes; Face face; if (wholePath) { // if selecting whole path and in edge mode, make sure the path contains // at least one complete edge from the selection. if (ProBuilderEditor.selectMode == SelectMode.Edge && !hole.Any(x => common.Contains(x.edge.common.a) && common.Contains(x.edge.common.b))) { continue; } holeIndexes = hole.Select(x => x.edge.local.a).ToList(); face = AppendElements.CreatePolygon(mesh, holeIndexes, false); } else { IEnumerable <WingedEdge> selected = hole.Where(x => common.Contains(x.edge.common.a)); holeIndexes = selected.Select(x => x.edge.local.a).ToList(); face = AppendElements.CreatePolygon(mesh, holeIndexes, true); } if (face != null) { filled++; appendedFaces.Add(face); } } mesh.SetSelectedFaces(appendedFaces); wings = WingedEdge.GetWingedEdges(mesh); // make sure the appended faces match the first adjacent face found // both in winding and face properties foreach (var appendedFace in appendedFaces) { var wing = wings.FirstOrDefault(x => x.face == appendedFace); if (wing == null) { continue; } using (var it = new WingedEdgeEnumerator(wing)) { while (it.MoveNext()) { if (it.Current == null) { continue; } var currentWing = it.Current; var oppositeFace = it.Current.opposite != null ? it.Current.opposite.face : null; if (oppositeFace != null && !appendedFaces.Contains(oppositeFace)) { currentWing.face.submeshIndex = oppositeFace.submeshIndex; currentWing.face.uv = new AutoUnwrapSettings(oppositeFace.uv); SurfaceTopology.ConformOppositeNormal(currentWing.opposite); break; } } } } mesh.ToMesh(); mesh.Refresh(); mesh.Optimize(); } ProBuilderEditor.Refresh(); if (filled > 0) { res = new ActionResult(ActionResult.Status.Success, filled > 1 ? string.Format("Filled {0} Holes", filled) : "Fill Hole"); } return(res); }