// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; namespace Microsoft.MixedReality.GraphicsTools { /// /// Component which can be used to render an outline around a mesh renderer. Enabling this component introduces an additional render pass(es) /// of the object being outlined, but is designed to run performantly on mobile Mixed Reality devices and does not utilize any post processes. /// This behavior is designed to be used in conjunction with the Graphics Tools/Standard shader. Limitations of this effect include it not working well /// on objects which are not watertight (or required to be two sided) and depth sorting issues may occur on overlapping objects. /// [DisallowMultipleComponent, RequireComponent(typeof(Renderer)), AddComponentMenu("Scripts/GraphicsTools/MeshOutline")] public class MeshOutline : BaseMeshOutline { private const string vertexExtrusionKeyword = "_VERTEX_EXTRUSION"; private const string vertexExtrusionSmoothNormalsKeyword = "_VERTEX_EXTRUSION_SMOOTH_NORMALS"; private Renderer baseRenderer = null; private int stencilReferenceID = Shader.PropertyToID("_StencilReference"); private int vertexExtrusionValueID = Shader.PropertyToID("_VertexExtrusionValue"); private Material[] defaultMaterials = null; private MeshSmoother createdMeshSmoother = null; #region MonoBehaviour Implementation /// /// Gathers initial render state. /// private void Awake() { if (GetComponent() == null && GetComponent() == null) { Debug.LogWarning($"{this.GetType()} is not supported on this type of renderer."); } baseRenderer = GetComponent(); defaultMaterials = baseRenderer.sharedMaterials; } /// /// Enables the outline. /// private void OnEnable() { ApplyOutlineMaterial(); } /// /// Resets the renderer materials to the default settings. /// private void OnDisable() { baseRenderer.materials = defaultMaterials; } /// /// Removes any components this component has created. /// private void OnDestroy() { Destroy(createdMeshSmoother); } #endregion MonoBehaviour Implementation #region BaseMeshOutline Implementation /// /// Prepares and applies the current outline material to the MeshRenderer/SkinnedMeshRenderer. /// public override void ApplyOutlineMaterial() { if (enabled == false || gameObject.activeInHierarchy == false || baseRenderer == null || outlineMaterial == null) { return; } Debug.AssertFormat(outlineMaterial.IsKeywordEnabled(vertexExtrusionKeyword), "The material \"{0}\" does not have vertex extrusion enabled, an outline might not be rendered.", outlineMaterial.name); if (UseStencilOutline) { if (stencilWriteMaterial == null) { Debug.LogError("ApplyOutlineMaterial failed due to missing stencil write material."); return; } // Ensure that the stencil material renders after the default materials and the outline material after the stencil material. if (AutoAssignRenderQueue) { int maxRenderQueue = Mathf.Max(GetMaxRenderQueue(defaultMaterials), (int)RenderQueue.Overlay); stencilWriteMaterial.renderQueue = maxRenderQueue + 1; outlineMaterial.renderQueue = maxRenderQueue + 2; } } else { // Ensure that the outline material always renders before the default materials. if (AutoAssignRenderQueue) { outlineMaterial.renderQueue = GetMinRenderQueue(defaultMaterials) - 1; } } // If smooth normals are requested, make sure the mesh has smooth normals. if (outlineMaterial.IsKeywordEnabled(vertexExtrusionSmoothNormalsKeyword)) { var meshSmoother = (createdMeshSmoother == null) ? gameObject.GetComponent() : createdMeshSmoother; if (meshSmoother == null) { createdMeshSmoother = gameObject.AddComponent(); meshSmoother = createdMeshSmoother; } meshSmoother.SmoothNormals(); } ApplyOutlineWidth(); ApplyStencilReference(); // Add the outline material as another material pass. var materials = new Material[UseStencilOutline ? defaultMaterials.Length + 2 : defaultMaterials.Length + 1]; defaultMaterials.CopyTo(materials, 0); int nextFreeIdx = defaultMaterials.Length; if (UseStencilOutline) { materials[nextFreeIdx++] = stencilWriteMaterial; } materials[nextFreeIdx] = outlineMaterial; HandleMultipleSubMeshes(); baseRenderer.materials = materials; } /// /// Updates the current vertex extrusion value used by the materials. /// public override void ApplyOutlineWidth() { if (outlineMaterial != null) { outlineMaterial.SetFloat(vertexExtrusionValueID, outlineOffset + outlineWidth); } if (stencilWriteMaterial != null) { // Clamp to ensure we don't get z-fighting. stencilWriteMaterial.SetFloat(vertexExtrusionValueID, Mathf.Max(0.00001f, outlineOffset)); } } /// /// Updates the current stencil ID used by the materials. /// public override void ApplyStencilReference() { if (outlineMaterial != null) { outlineMaterial.SetFloat(stencilReferenceID, stencilReference); } if (stencilWriteMaterial != null) { stencilWriteMaterial.SetFloat(stencilReferenceID, stencilReference); } } #endregion BaseMeshOutline Implementation /// /// Makes this BaseMeshOutline's settings match the other BaseMeshOutline. /// public void CopyFrom(BaseMeshOutline other) { outlineWidth = other.OutlineWidth; outlineMaterial = other.OutlineMaterial; autoAssignRenderQueue = other.AutoAssignRenderQueue; useStencilOutline = other.UseStencilOutline; stencilWriteMaterial = other.StencilWriteMaterial; outlineOffset = other.OutlineOffset; stencilReference = other.StencilReference; ApplyOutlineMaterial(); } /// /// Searches for the minimum render queue value in a list of materials. /// private static int GetMinRenderQueue(Material[] materials) { var min = int.MaxValue; foreach (var material in materials) { if (material != null) { min = Mathf.Min(min, material.renderQueue); } } if (min == int.MaxValue) { min = (int)RenderQueue.Background; } return min; } /// /// Searches for the maximum render queue value in a list of materials. /// private static int GetMaxRenderQueue(Material[] materials) { var max = int.MinValue; foreach (var material in materials) { if (material != null) { max = Mathf.Max(max, material.renderQueue); } } if (max == int.MinValue) { max = (int)RenderQueue.Overlay; } return max; } /// /// Creates and adds a SubMeshDescriptor which covers the entire mesh. /// This ensures that the outline material is applied to the entire mesh. /// private void HandleMultipleSubMeshes() { Mesh mesh = null; if (baseRenderer is SkinnedMeshRenderer skinnedMeshRenderer) { mesh = skinnedMeshRenderer.sharedMesh; } else if (baseRenderer is MeshRenderer meshRenderer) { mesh = meshRenderer.GetComponent().sharedMesh; } if (mesh != null) { var meshSubMeshCount = mesh.subMeshCount; if (meshSubMeshCount > 1) { var subMeshes = new SubMeshDescriptor[meshSubMeshCount + 1]; for (int i = 0; i < meshSubMeshCount; i++) { var subMesh = mesh.GetSubMesh(i); subMeshes[i] = subMesh; } var lastMesh = subMeshes[meshSubMeshCount - 1]; subMeshes[meshSubMeshCount] = new SubMeshDescriptor(0, lastMesh.indexStart + lastMesh.indexCount); mesh.SetSubMeshes(subMeshes, MeshUpdateFlags.DontRecalculateBounds); } } } } }