Outline.cs 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. //
  2. // Outline.cs
  3. // QuickOutline
  4. //
  5. // Created by Chris Nolet on 3/30/18.
  6. // Copyright © 2018 Chris Nolet. All rights reserved.
  7. //
  8. using System;
  9. using System.Collections.Generic;
  10. using System.Linq;
  11. using UnityEngine;
  12. [DisallowMultipleComponent]
  13. public class Outline : MonoBehaviour {
  14. private static HashSet<Mesh> registeredMeshes = new HashSet<Mesh>();
  15. public enum Mode {
  16. OutlineAll,
  17. OutlineVisible,
  18. OutlineHidden,
  19. OutlineAndSilhouette,
  20. SilhouetteOnly
  21. }
  22. public Mode OutlineMode {
  23. get { return outlineMode; }
  24. set {
  25. outlineMode = value;
  26. needsUpdate = true;
  27. }
  28. }
  29. public Color OutlineColor {
  30. get { return outlineColor; }
  31. set {
  32. outlineColor = value;
  33. needsUpdate = true;
  34. }
  35. }
  36. public float OutlineWidth {
  37. get { return outlineWidth; }
  38. set {
  39. outlineWidth = value;
  40. needsUpdate = true;
  41. }
  42. }
  43. [Serializable]
  44. private class ListVector3 {
  45. public List<Vector3> data;
  46. }
  47. [SerializeField]
  48. private Mode outlineMode;
  49. [SerializeField]
  50. private Color outlineColor = Color.white;
  51. [SerializeField, Range(0f, 10f)]
  52. private float outlineWidth = 2f;
  53. [Header("Optional")]
  54. [SerializeField, Tooltip("Precompute enabled: Per-vertex calculations are performed in the editor and serialized with the object. "
  55. + "Precompute disabled: Per-vertex calculations are performed at runtime in Awake(). This may cause a pause for large meshes.")]
  56. private bool precomputeOutline;
  57. [SerializeField, HideInInspector]
  58. private List<Mesh> bakeKeys = new List<Mesh>();
  59. [SerializeField, HideInInspector]
  60. private List<ListVector3> bakeValues = new List<ListVector3>();
  61. private Renderer[] renderers;
  62. private Material outlineMaskMaterial;
  63. private Material outlineFillMaterial;
  64. private bool needsUpdate;
  65. void Awake() {
  66. // Cache renderers
  67. renderers = GetComponentsInChildren<Renderer>();
  68. // Instantiate outline materials
  69. outlineMaskMaterial = Instantiate(Resources.Load<Material>(@"Materials/OutlineMask"));
  70. outlineFillMaterial = Instantiate(Resources.Load<Material>(@"Materials/OutlineFill"));
  71. outlineMaskMaterial.name = "OutlineMask (Instance)";
  72. outlineFillMaterial.name = "OutlineFill (Instance)";
  73. // Retrieve or generate smooth normals
  74. LoadSmoothNormals();
  75. // Apply material properties immediately
  76. needsUpdate = true;
  77. }
  78. void OnEnable() {
  79. foreach (var renderer in renderers) {
  80. // Append outline shaders
  81. var materials = renderer.sharedMaterials.ToList();
  82. materials.Add(outlineMaskMaterial);
  83. materials.Add(outlineFillMaterial);
  84. renderer.materials = materials.ToArray();
  85. }
  86. }
  87. void OnValidate() {
  88. // Update material properties
  89. needsUpdate = true;
  90. // Clear cache when baking is disabled or corrupted
  91. if (!precomputeOutline && bakeKeys.Count != 0 || bakeKeys.Count != bakeValues.Count) {
  92. bakeKeys.Clear();
  93. bakeValues.Clear();
  94. }
  95. // Generate smooth normals when baking is enabled
  96. if (precomputeOutline && bakeKeys.Count == 0) {
  97. Bake();
  98. }
  99. }
  100. void Update() {
  101. if (needsUpdate) {
  102. needsUpdate = false;
  103. UpdateMaterialProperties();
  104. }
  105. }
  106. void OnDisable() {
  107. foreach (var renderer in renderers) {
  108. // Remove outline shaders
  109. var materials = renderer.sharedMaterials.ToList();
  110. materials.Remove(outlineMaskMaterial);
  111. materials.Remove(outlineFillMaterial);
  112. renderer.materials = materials.ToArray();
  113. }
  114. }
  115. void OnDestroy() {
  116. // Destroy material instances
  117. Destroy(outlineMaskMaterial);
  118. Destroy(outlineFillMaterial);
  119. }
  120. void Bake() {
  121. // Generate smooth normals for each mesh
  122. var bakedMeshes = new HashSet<Mesh>();
  123. foreach (var meshFilter in GetComponentsInChildren<MeshFilter>()) {
  124. // Skip duplicates
  125. if (!bakedMeshes.Add(meshFilter.sharedMesh)) {
  126. continue;
  127. }
  128. // Serialize smooth normals
  129. var smoothNormals = SmoothNormals(meshFilter.sharedMesh);
  130. bakeKeys.Add(meshFilter.sharedMesh);
  131. bakeValues.Add(new ListVector3() { data = smoothNormals });
  132. }
  133. }
  134. void LoadSmoothNormals() {
  135. // Retrieve or generate smooth normals
  136. foreach (var meshFilter in GetComponentsInChildren<MeshFilter>()) {
  137. // Skip if smooth normals have already been adopted
  138. if (!registeredMeshes.Add(meshFilter.sharedMesh)) {
  139. continue;
  140. }
  141. // Retrieve or generate smooth normals
  142. var index = bakeKeys.IndexOf(meshFilter.sharedMesh);
  143. var smoothNormals = (index >= 0) ? bakeValues[index].data : SmoothNormals(meshFilter.sharedMesh);
  144. // Store smooth normals in UV3
  145. meshFilter.sharedMesh.SetUVs(3, smoothNormals);
  146. }
  147. // Clear UV3 on skinned mesh renderers
  148. foreach (var skinnedMeshRenderer in GetComponentsInChildren<SkinnedMeshRenderer>()) {
  149. if (registeredMeshes.Add(skinnedMeshRenderer.sharedMesh)) {
  150. skinnedMeshRenderer.sharedMesh.uv4 = new Vector2[skinnedMeshRenderer.sharedMesh.vertexCount];
  151. }
  152. }
  153. }
  154. List<Vector3> SmoothNormals(Mesh mesh) {
  155. // Group vertices by location
  156. var groups = mesh.vertices.Select((vertex, index) => new KeyValuePair<Vector3, int>(vertex, index)).GroupBy(pair => pair.Key);
  157. // Copy normals to a new list
  158. var smoothNormals = new List<Vector3>(mesh.normals);
  159. // Average normals for grouped vertices
  160. foreach (var group in groups) {
  161. // Skip single vertices
  162. if (group.Count() == 1) {
  163. continue;
  164. }
  165. // Calculate the average normal
  166. var smoothNormal = Vector3.zero;
  167. foreach (var pair in group) {
  168. smoothNormal += mesh.normals[pair.Value];
  169. }
  170. smoothNormal.Normalize();
  171. // Assign smooth normal to each vertex
  172. foreach (var pair in group) {
  173. smoothNormals[pair.Value] = smoothNormal;
  174. }
  175. }
  176. return smoothNormals;
  177. }
  178. void UpdateMaterialProperties() {
  179. // Apply properties according to mode
  180. outlineFillMaterial.SetColor("_OutlineColor", outlineColor);
  181. switch (outlineMode) {
  182. case Mode.OutlineAll:
  183. outlineMaskMaterial.SetFloat("_ZTest", (float)UnityEngine.Rendering.CompareFunction.Always);
  184. outlineFillMaterial.SetFloat("_ZTest", (float)UnityEngine.Rendering.CompareFunction.Always);
  185. outlineFillMaterial.SetFloat("_OutlineWidth", outlineWidth);
  186. break;
  187. case Mode.OutlineVisible:
  188. outlineMaskMaterial.SetFloat("_ZTest", (float)UnityEngine.Rendering.CompareFunction.Always);
  189. outlineFillMaterial.SetFloat("_ZTest", (float)UnityEngine.Rendering.CompareFunction.LessEqual);
  190. outlineFillMaterial.SetFloat("_OutlineWidth", outlineWidth);
  191. break;
  192. case Mode.OutlineHidden:
  193. outlineMaskMaterial.SetFloat("_ZTest", (float)UnityEngine.Rendering.CompareFunction.Always);
  194. outlineFillMaterial.SetFloat("_ZTest", (float)UnityEngine.Rendering.CompareFunction.Greater);
  195. outlineFillMaterial.SetFloat("_OutlineWidth", outlineWidth);
  196. break;
  197. case Mode.OutlineAndSilhouette:
  198. outlineMaskMaterial.SetFloat("_ZTest", (float)UnityEngine.Rendering.CompareFunction.LessEqual);
  199. outlineFillMaterial.SetFloat("_ZTest", (float)UnityEngine.Rendering.CompareFunction.Always);
  200. outlineFillMaterial.SetFloat("_OutlineWidth", outlineWidth);
  201. break;
  202. case Mode.SilhouetteOnly:
  203. outlineMaskMaterial.SetFloat("_ZTest", (float)UnityEngine.Rendering.CompareFunction.LessEqual);
  204. outlineFillMaterial.SetFloat("_ZTest", (float)UnityEngine.Rendering.CompareFunction.Greater);
  205. outlineFillMaterial.SetFloat("_OutlineWidth", 0);
  206. break;
  207. }
  208. }
  209. }