/// <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); }
static void CollectAdjacentFaces(WingedEdge wing, HashSet <Face> filter, List <Face> group) { if (!filter.Add(wing.face)) { return; } group.Add(wing.face); var enumerator = new WingedEdgeEnumerator(wing); while (enumerator.MoveNext()) { var opposite = enumerator.Current.opposite; if (opposite == null) { continue; } CollectAdjacentFaces(opposite, filter, group); } }
static Face GetBestQuadConnection(WingedEdge wing, Dictionary <EdgeLookup, float> connections) { float score = 0f; Face face = null; using (var it = new WingedEdgeEnumerator(wing)) { while (it.MoveNext()) { var border = it.Current; float s = 0f; if (connections.TryGetValue(border.edge, out s) && s > score) { score = connections[border.edge]; face = border.opposite.face; } } } return(face); }
/// <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 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(); } }
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); }