243 lines
9.0 KiB
C#
243 lines
9.0 KiB
C#
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
using System.Collections.Generic;
|
|
using System.Threading.Tasks;
|
|
using UnityEngine;
|
|
|
|
namespace Microsoft.MixedReality.Toolkit.Utilities
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[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;
|
|
|
|
/// <summary>
|
|
/// Helper class to track mesh references.
|
|
/// </summary>
|
|
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<Mesh, MeshReference> processedMeshes = new Dictionary<Mesh, MeshReference>();
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <returns>A task which will complete once normal smoothing is finished.</returns>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Applies smoothing asynchronously if specified by the inspector property.
|
|
/// </summary>
|
|
private void Awake()
|
|
{
|
|
meshFilter = GetComponent<MeshFilter>();
|
|
|
|
if (smoothNormalsOnAwake)
|
|
{
|
|
SmoothNormalsAsync();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clean up any meshes which were created if no longer referenced.
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Safely acquires a mesh for processing. Checks for meshes which have already been processed and increments reference counts.
|
|
/// </summary>
|
|
/// <param name="mesh">A reference to the mesh which was already processed or is ready to be processed.</param>
|
|
/// <returns>True if the mesh was already processed, false otherwise.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="vertices">A list of vertices that represent a mesh.</param>
|
|
/// <param name="normals">A list of normals that correspond to each vertex passed in via the vertices param.</param>
|
|
/// <returns>A list of normals which are smoothed, or averaged, based on share vertex position.</returns>
|
|
private static List<Vector3> 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<Vector3, List<KeyValuePair<int, Vector3>>>();
|
|
|
|
for (int i = 0; i < vertices.Length; ++i)
|
|
{
|
|
var vertex = vertices[i];
|
|
List<KeyValuePair<int, Vector3>> group;
|
|
|
|
if (!groupedVerticies.TryGetValue(vertex, out group))
|
|
{
|
|
group = new List<KeyValuePair<int, Vector3>>();
|
|
groupedVerticies[vertex] = group;
|
|
}
|
|
|
|
group.Add(new KeyValuePair<int, Vector3>(i, vertex));
|
|
}
|
|
|
|
var smoothNormals = new List<Vector3>(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;
|
|
}
|
|
}
|
|
}
|