Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion for package structure #35

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
renamed scripts to runtime
  • Loading branch information
SGoerzen committed Mar 28, 2023
commit e30d6e02ef250a69681b47b70444361d6450bde9
309 changes: 309 additions & 0 deletions QuickOutline/Runtime/Outline.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
//
// 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;
}
}
}
13 changes: 13 additions & 0 deletions QuickOutline/Runtime/Outline.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions QuickOutline/Runtime/QuickOutline.asmdef
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "Outline",
"references": [
""
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
7 changes: 7 additions & 0 deletions QuickOutline/Runtime/QuickOutline.asmdef.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.