123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279 |
- //
- // Outline.cs
- // QuickOutline
- //
- // Created by Chris Nolet on 3/30/18.
- // Copyright © 2018 Chris Nolet. All rights reserved.
- //
- // Extension Asset: One license required for each individual user
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using UnityEngine;
- [DisallowMultipleComponent]
- public class Outline : MonoBehaviour {
- private static HashSet<Mesh> registeredMeshes = new HashSet<Mesh>();
- public enum Mode {
- OutlineAll,
- OutlineVisible,
- OutlineHidden,
- OutlineAndSilhouette,
- SilhouetteOnly
- }
- public Mode OutlineMode {
- get { return outlineMode; }
- set {
- outlineMode = value;
- needsUpdate = true;
- }
- }
- public Color OutlineColor {
- get { return outlineColor; }
- set {
- outlineColor = value;
- needsUpdate = true;
- }
- }
- public float OutlineWidth {
- get { return outlineWidth; }
- set {
- outlineWidth = value;
- needsUpdate = true;
- }
- }
- [Serializable]
- private class ListVector3 {
- public List<Vector3> data;
- }
- [SerializeField]
- private Mode outlineMode;
- [SerializeField]
- private Color outlineColor = Color.white;
- [SerializeField, Range(0f, 10f)]
- private float outlineWidth = 2f;
- [Header("Optional")]
- [SerializeField, Tooltip("Precompute enabled: Per-vertex calculations are performed in the editor and serialized with the object. "
- + "Precompute disabled: Per-vertex calculations are performed at runtime in Awake(). This may cause a pause for large meshes.")]
- private bool precomputeOutline;
- [SerializeField, HideInInspector]
- private List<Mesh> bakeKeys = new List<Mesh>();
- [SerializeField, HideInInspector]
- private List<ListVector3> bakeValues = new List<ListVector3>();
- private Renderer[] renderers;
- private Material outlineMaskMaterial;
- private Material outlineFillMaterial;
- private bool needsUpdate;
- void Awake() {
- // Cache renderers
- renderers = GetComponentsInChildren<Renderer>();
- // Instantiate outline materials
- outlineMaskMaterial = Instantiate(Resources.Load<Material>(@"Materials/OutlineMask"));
- outlineFillMaterial = Instantiate(Resources.Load<Material>(@"Materials/OutlineFill"));
- outlineMaskMaterial.name = "OutlineMask (Instance)";
- outlineFillMaterial.name = "OutlineFill (Instance)";
- // Retrieve or generate smooth normals
- LoadSmoothNormals();
- // Apply material properties immediately
- needsUpdate = true;
- }
- void OnEnable() {
- foreach (var renderer in renderers) {
- // Append outline shaders
- var materials = renderer.sharedMaterials.ToList();
- materials.Add(outlineMaskMaterial);
- materials.Add(outlineFillMaterial);
- renderer.materials = materials.ToArray();
- }
- }
- void OnValidate() {
- // Update material properties
- needsUpdate = true;
- // Clear cache when baking is disabled or corrupted
- if (!precomputeOutline && bakeKeys.Count != 0 || bakeKeys.Count != bakeValues.Count) {
- bakeKeys.Clear();
- bakeValues.Clear();
- }
- // Generate smooth normals when baking is enabled
- if (precomputeOutline && bakeKeys.Count == 0) {
- Bake();
- }
- }
- void Update() {
- if (needsUpdate) {
- needsUpdate = false;
- UpdateMaterialProperties();
- }
- }
- void OnDisable() {
- foreach (var renderer in renderers) {
- // Remove outline shaders
- var materials = renderer.sharedMaterials.ToList();
- materials.Remove(outlineMaskMaterial);
- materials.Remove(outlineFillMaterial);
- renderer.materials = materials.ToArray();
- }
- }
- void OnDestroy() {
- // Destroy material instances
- Destroy(outlineMaskMaterial);
- Destroy(outlineFillMaterial);
- }
- void Bake() {
- // Generate smooth normals for each mesh
- var bakedMeshes = new HashSet<Mesh>();
- foreach (var meshFilter in GetComponentsInChildren<MeshFilter>()) {
- // Skip duplicates
- if (!bakedMeshes.Add(meshFilter.sharedMesh)) {
- continue;
- }
- // Serialize smooth normals
- var smoothNormals = SmoothNormals(meshFilter.sharedMesh);
- bakeKeys.Add(meshFilter.sharedMesh);
- bakeValues.Add(new ListVector3() { data = smoothNormals });
- }
- }
- void LoadSmoothNormals() {
- // Retrieve or generate smooth normals
- foreach (var meshFilter in GetComponentsInChildren<MeshFilter>()) {
- // Skip if smooth normals have already been adopted
- if (!registeredMeshes.Add(meshFilter.sharedMesh)) {
- continue;
- }
- // Retrieve or generate smooth normals
- var index = bakeKeys.IndexOf(meshFilter.sharedMesh);
- var smoothNormals = (index >= 0) ? bakeValues[index].data : SmoothNormals(meshFilter.sharedMesh);
- // Store smooth normals in UV3
- meshFilter.sharedMesh.SetUVs(3, smoothNormals);
- }
- // Clear UV3 on skinned mesh renderers
- foreach (var skinnedMeshRenderer in GetComponentsInChildren<SkinnedMeshRenderer>()) {
- if (registeredMeshes.Add(skinnedMeshRenderer.sharedMesh)) {
- skinnedMeshRenderer.sharedMesh.uv4 = new Vector2[skinnedMeshRenderer.sharedMesh.vertexCount];
- }
- }
- }
- List<Vector3> SmoothNormals(Mesh mesh) {
- // Group vertices by location
- var groups = mesh.vertices.Select((vertex, index) => new KeyValuePair<Vector3, int>(vertex, index)).GroupBy(pair => pair.Key);
- // Copy normals to a new list
- var smoothNormals = new List<Vector3>(mesh.normals);
- // Average normals for grouped vertices
- foreach (var group in groups) {
- // Skip single vertices
- if (group.Count() == 1) {
- continue;
- }
- // Calculate the average normal
- var smoothNormal = Vector3.zero;
- foreach (var pair in group) {
- smoothNormal += mesh.normals[pair.Value];
- }
- smoothNormal.Normalize();
- // Assign smooth normal to each vertex
- foreach (var pair in group) {
- smoothNormals[pair.Value] = smoothNormal;
- }
- }
- return smoothNormals;
- }
- void UpdateMaterialProperties() {
- // Apply properties according to mode
- outlineFillMaterial.SetColor("_OutlineColor", outlineColor);
- switch (outlineMode) {
- case Mode.OutlineAll:
- outlineMaskMaterial.SetFloat("_ZTest", (float)UnityEngine.Rendering.CompareFunction.Always);
- outlineFillMaterial.SetFloat("_ZTest", (float)UnityEngine.Rendering.CompareFunction.Always);
- outlineFillMaterial.SetFloat("_OutlineWidth", outlineWidth);
- break;
- case Mode.OutlineVisible:
- outlineMaskMaterial.SetFloat("_ZTest", (float)UnityEngine.Rendering.CompareFunction.Always);
- outlineFillMaterial.SetFloat("_ZTest", (float)UnityEngine.Rendering.CompareFunction.LessEqual);
- outlineFillMaterial.SetFloat("_OutlineWidth", outlineWidth);
- break;
- case Mode.OutlineHidden:
- outlineMaskMaterial.SetFloat("_ZTest", (float)UnityEngine.Rendering.CompareFunction.Always);
- outlineFillMaterial.SetFloat("_ZTest", (float)UnityEngine.Rendering.CompareFunction.Greater);
- outlineFillMaterial.SetFloat("_OutlineWidth", outlineWidth);
- break;
- case Mode.OutlineAndSilhouette:
- outlineMaskMaterial.SetFloat("_ZTest", (float)UnityEngine.Rendering.CompareFunction.LessEqual);
- outlineFillMaterial.SetFloat("_ZTest", (float)UnityEngine.Rendering.CompareFunction.Always);
- outlineFillMaterial.SetFloat("_OutlineWidth", outlineWidth);
- break;
- case Mode.SilhouetteOnly:
- outlineMaskMaterial.SetFloat("_ZTest", (float)UnityEngine.Rendering.CompareFunction.LessEqual);
- outlineFillMaterial.SetFloat("_ZTest", (float)UnityEngine.Rendering.CompareFunction.Greater);
- outlineFillMaterial.SetFloat("_OutlineWidth", 0);
- break;
- }
- }
- }
|