using UnityEngine; using System.Linq; using System.Collections.Generic; namespace UnityEngine.ProBuilder { /// /// Provides utilities for working with smoothing groups. ProBuilder uses smoothing groups to define hard and soft edges. /// To calculate vertex normals, ProBuilder performs these tasks: /// /// 1. Calculates the normals for every face. /// 2. Applies the results of those calculations to each vertex on the face. /// 3. Averages each vertex normal with coincident vertices belonging to the same smoothing group. /// /// public static class Smoothing { /// /// Faces with smoothingGroup = 0 are hard edges. Historically negative values were sometimes also written as hard edges. /// internal const int smoothingGroupNone = 0; /// /// Smoothing groups 1-24 are smooth. /// internal const int smoothRangeMin = 1; /// /// Smoothing groups 1-24 are smooth. /// internal const int smoothRangeMax = 24; /// /// Smoothing groups 25-42 are hard. Note that this is obsolete, and generally hard faces should be marked smoothingGroupNone. /// internal const int hardRangeMin = 25; /// /// Smoothing groups 25-42 are hard. Note that this is soon to be obsolete, and generally hard faces should be marked smoothingGroupNone. /// internal const int hardRangeMax = 42; /// /// Returns the first available unused smoothing group. /// /// The target mesh. /// An unused smoothing group. public static int GetUnusedSmoothingGroup(ProBuilderMesh mesh) { if (mesh == null) throw new System.ArgumentNullException("mesh"); return GetNextUnusedSmoothingGroup(smoothRangeMin, new HashSet(mesh.facesInternal.Select(x => x.smoothingGroup))); } /// /// Get the first available smooth group after a specified index. /// /// /// /// static int GetNextUnusedSmoothingGroup(int start, HashSet used) { while (used.Contains(start) && start < int.MaxValue - 1) { start++; if (start > smoothRangeMax && start < hardRangeMax) start = hardRangeMax + 1; } return start; } /// /// Tests whether the specified smoothing group is smooth. /// /// The ID of the smoothing group to test. /// True if the smoothing group value is smoothed; false otherwise. public static bool IsSmooth(int index) { return (index > smoothingGroupNone && (index < hardRangeMin || index > hardRangeMax)); } /// /// Generates smoothing groups for a set of faces by comparing adjacent faces with normal differences less than `angleThreshold` (in degrees). /// /// The mesh to apply new smoothing groups to. /// Faces to inspect for smoothing. /// Set the maximum value to consider the shared edge smooth. This value is an angle in degrees that represents the difference between adjacent face normals. public static void ApplySmoothingGroups(ProBuilderMesh mesh, IEnumerable faces, float angleThreshold) { ApplySmoothingGroups(mesh, faces, angleThreshold, null); } internal static void ApplySmoothingGroups(ProBuilderMesh mesh, IEnumerable faces, float angleThreshold, Vector3[] normals) { if (mesh == null || faces == null) throw new System.ArgumentNullException("mesh"); // Reset the selected faces to no smoothing group bool anySmoothed = false; foreach (Face face in faces) { if (face.smoothingGroup != smoothingGroupNone) anySmoothed = true; face.smoothingGroup = Smoothing.smoothingGroupNone; } // if a set of normals was not supplied, get a new set of normals // with no prior smoothing groups applied. if (normals == null) { if (anySmoothed) mesh.mesh.normals = null; normals = mesh.GetNormals(); } float threshold = Mathf.Abs(Mathf.Cos(Mathf.Clamp(angleThreshold, 0f, 89.999f) * Mathf.Deg2Rad)); HashSet used = new HashSet(mesh.facesInternal.Select(x => x.smoothingGroup)); int group = GetNextUnusedSmoothingGroup(1, used); HashSet processed = new HashSet(); List wings = WingedEdge.GetWingedEdges(mesh, faces, true); try { foreach (WingedEdge wing in wings) { // Already part of a group if (!processed.Add(wing.face)) continue; wing.face.smoothingGroup = group; if(FindSoftEdgesRecursive(normals, wing, threshold, processed)) { used.Add(group); group = GetNextUnusedSmoothingGroup(group, used); } else { wing.face.smoothingGroup = Smoothing.smoothingGroupNone; } } } catch { Debug.LogWarning("Smoothing has been aborted: Too many edges in the analyzed mesh"); } } // Walk the perimiter of a wing looking for compatibly smooth connections. Returns true if any match was found, false if not. static bool FindSoftEdgesRecursive(Vector3[] normals, WingedEdge wing, float angleThreshold, HashSet processed) { bool foundSmoothEdge = false; using (var it = new WingedEdgeEnumerator(wing)) { while (it.MoveNext()) { var border = it.Current; if (border.opposite == null) continue; if (border.opposite.face.smoothingGroup == Smoothing.smoothingGroupNone && IsSoftEdge(normals, border.edge, border.opposite.edge, angleThreshold)) { if (processed.Add(border.opposite.face)) { foundSmoothEdge = true; border.opposite.face.smoothingGroup = wing.face.smoothingGroup; FindSoftEdgesRecursive(normals, border.opposite, angleThreshold, processed); } } } } return foundSmoothEdge; } static bool IsSoftEdge(Vector3[] normals, EdgeLookup left, EdgeLookup right, float threshold) { Vector3 lx = normals[left.local.a]; Vector3 ly = normals[left.local.b]; Vector3 rx = normals[right.common.a == left.common.a ? right.local.a : right.local.b]; Vector3 ry = normals[right.common.b == left.common.b ? right.local.b : right.local.a]; lx.Normalize(); ly.Normalize(); rx.Normalize(); ry.Normalize(); return Mathf.Abs(Vector3.Dot(lx, rx)) > threshold && Mathf.Abs(Vector3.Dot(ly, ry)) > threshold; } } }