310 lines
8.3 KiB
C#
310 lines
8.3 KiB
C#
|
//
|
|||
|
// Outline.cs
|
|||
|
// QuickOutline
|
|||
|
//
|
|||
|
// Created by Chris Nolet on 3/30/18.
|
|||
|
// Copyright © 2018 Chris Nolet. All rights reserved.
|
|||
|
//
|
|||
|
|
|||
|
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);
|
|||
|
|
|||
|
// Combine submeshes
|
|||
|
var renderer = meshFilter.GetComponent<Renderer>();
|
|||
|
|
|||
|
if (renderer != null) {
|
|||
|
CombineSubmeshes(meshFilter.sharedMesh, renderer.sharedMaterials);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Clear UV3 on skinned mesh renderers
|
|||
|
foreach (var skinnedMeshRenderer in GetComponentsInChildren<SkinnedMeshRenderer>()) {
|
|||
|
|
|||
|
// Skip if UV3 has already been reset
|
|||
|
if (!registeredMeshes.Add(skinnedMeshRenderer.sharedMesh)) {
|
|||
|
continue;
|
|||
|
}
|
|||
|
|
|||
|
// Clear UV3
|
|||
|
skinnedMeshRenderer.sharedMesh.uv4 = new Vector2[skinnedMeshRenderer.sharedMesh.vertexCount];
|
|||
|
|
|||
|
// Combine submeshes
|
|||
|
CombineSubmeshes(skinnedMeshRenderer.sharedMesh, skinnedMeshRenderer.sharedMaterials);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
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 += smoothNormals[pair.Value];
|
|||
|
}
|
|||
|
|
|||
|
smoothNormal.Normalize();
|
|||
|
|
|||
|
// Assign smooth normal to each vertex
|
|||
|
foreach (var pair in group) {
|
|||
|
smoothNormals[pair.Value] = smoothNormal;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return smoothNormals;
|
|||
|
}
|
|||
|
|
|||
|
void CombineSubmeshes(Mesh mesh, Material[] materials) {
|
|||
|
|
|||
|
// Skip meshes with a single submesh
|
|||
|
if (mesh.subMeshCount == 1) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// Skip if submesh count exceeds material count
|
|||
|
if (mesh.subMeshCount > materials.Length) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// Append combined submesh
|
|||
|
mesh.subMeshCount++;
|
|||
|
mesh.SetTriangles(mesh.triangles, mesh.subMeshCount - 1);
|
|||
|
}
|
|||
|
|
|||
|
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", 0f);
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|