// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; namespace Microsoft.MixedReality.Toolkit.Utilities { /// /// Component which can be used to automatically generate smoothed normals on a mesh and pack /// those normals into a UV set. Smoothed normals can be used for a variety of effects including /// extruding disjoint meshes along a vertex normal. This behavior is designed to be used in conjunction /// with the MRTK/Standard shader which assumes smoothed normals are packed into the 3rd UV set. /// [RequireComponent(typeof(MeshFilter))] [HelpURL("https://docs.microsoft.com/windows/mixed-reality/mrtk-unity/features/rendering/mrtk-standard-shader#mesh-outlines")] [AddComponentMenu("Scripts/MRTK/Core/MeshSmoother")] public class MeshSmoother : MonoBehaviour { private const int smoothNormalUVChannel = 2; [Tooltip("Should this component automatically smooth normals on awake?")] [SerializeField] private bool smoothNormalsOnAwake = false; private MeshFilter meshFilter = null; /// /// Helper class to track mesh references. /// private class MeshReference { public Mesh Mesh; private int referenceCount; public MeshReference(Mesh mesh) { Mesh = mesh; referenceCount = 1; } public void Increment() { ++referenceCount; } public void Decrement() { --referenceCount; } public bool IsReferenced() { return referenceCount > 0; } } private static Dictionary processedMeshes = new Dictionary(); /// /// Performs normal smoothing on the current mesh filter associated with this component synchronously. /// This method will not try and re-smooth meshes which have already been smoothed. /// public void SmoothNormals() { Mesh mesh; // No need to do any smoothing if this mesh has already been processed. if (AcquirePreprocessedMesh(out mesh)) { return; } var result = CalculateSmoothNormals(mesh.vertices, mesh.normals); mesh.SetUVs(smoothNormalUVChannel, result); } /// /// Performs normal smoothing on the current mesh filter associated with this component asynchronously. /// This method will not try and re-smooth meshes which have already been smoothed. /// /// A task which will complete once normal smoothing is finished. public Task SmoothNormalsAsync() { Mesh mesh; // No need to do any smoothing if this mesh has already been processed. if (AcquirePreprocessedMesh(out mesh)) { return Task.CompletedTask; } // Create a copy of the vertices and normals and apply the smoothing in an async task. var vertices = mesh.vertices; var normals = mesh.normals; var asyncTask = Task.Run(() => CalculateSmoothNormals(vertices, normals)); // Once the async task is complete, apply the smoothed normals to the mesh on the main thread. return asyncTask.ContinueWith((i) => { mesh.SetUVs(smoothNormalUVChannel, i.Result); }, TaskScheduler.FromCurrentSynchronizationContext()); } #region MonoBehaviour Implementation /// /// Applies smoothing asynchronously if specified by the inspector property. /// private void Awake() { meshFilter = GetComponent(); if (smoothNormalsOnAwake) { SmoothNormalsAsync(); } } /// /// Clean up any meshes which were created if no longer referenced. /// private void OnDestroy() { MeshReference meshReference; var sharedMesh = meshFilter.sharedMesh; if (sharedMesh != null && processedMeshes.TryGetValue(sharedMesh, out meshReference)) { meshReference.Decrement(); if (!meshReference.IsReferenced()) { Destroy(meshReference.Mesh); processedMeshes.Remove(sharedMesh); } } } #endregion MonoBehaviour Implementation /// /// Safely acquires a mesh for processing. Checks for meshes which have already been processed and increments reference counts. /// /// A reference to the mesh which was already processed or is ready to be processed. /// True if the mesh was already processed, false otherwise. private bool AcquirePreprocessedMesh(out Mesh mesh) { var sharedMesh = meshFilter.sharedMesh; MeshReference meshReference; // If this mesh has already been processed, apply the preprocessed mesh and increment the reference count. if (sharedMesh != null && processedMeshes.TryGetValue(sharedMesh, out meshReference)) { meshReference.Increment(); mesh = meshReference.Mesh; meshFilter.mesh = mesh; return true; } // Clone the mesh, and create a mesh reference which can be keyed off either the original mesh or cloned mesh. mesh = meshFilter.mesh; meshReference = new MeshReference(mesh); processedMeshes[mesh] = meshReference; if (sharedMesh != null) { processedMeshes[sharedMesh] = meshReference; } return false; } /// /// This method groups vertices in a mesh that share the same location in space then averages the normals of those vertices. /// For example, if you imagine the 3 vertices that make up one corner of a cube. Normally there will be 3 normals facing in the direction /// of each face that touches that corner. This method will take those 3 normals and average them into a normal that points in the /// direction from the center of the cube to the corner of the cube. /// /// A list of vertices that represent a mesh. /// A list of normals that correspond to each vertex passed in via the vertices param. /// A list of normals which are smoothed, or averaged, based on share vertex position. private static List CalculateSmoothNormals(Vector3[] vertices, Vector3[] normals) { var watch = System.Diagnostics.Stopwatch.StartNew(); // Group all vertices that share the same location in space. var groupedVerticies = new Dictionary>>(); for (int i = 0; i < vertices.Length; ++i) { var vertex = vertices[i]; List> group; if (!groupedVerticies.TryGetValue(vertex, out group)) { group = new List>(); groupedVerticies[vertex] = group; } group.Add(new KeyValuePair(i, vertex)); } var smoothNormals = new List(normals); // If we don't hit the degenerate case of each vertex is its own group (no vertices shared a location), average the normals of each group. if (groupedVerticies.Count != vertices.Length) { foreach (var group in groupedVerticies) { var smoothingGroup = group.Value; // No need to smooth a group of one. if (smoothingGroup.Count != 1) { var smoothedNormal = Vector3.zero; foreach (var vertex in smoothingGroup) { smoothedNormal += normals[vertex.Key]; } smoothedNormal.Normalize(); foreach (var vertex in smoothingGroup) { smoothNormals[vertex.Key] = smoothedNormal; } } } } Debug.LogFormat("CalculateSmoothNormals took {0} ms on {1} vertices.", watch.ElapsedMilliseconds, vertices.Length); return smoothNormals; } } }