Outline.cs 7.6 KB

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