305 lines
12 KiB
C#
305 lines
12 KiB
C#
|
using System.Collections.Generic;
|
||
|
using Unity.Mathematics;
|
||
|
using UnityEditor.U2D.Sprites;
|
||
|
using UnityEngine;
|
||
|
|
||
|
namespace UnityEditor.U2D.Animation
|
||
|
{
|
||
|
internal class SpriteOutlineRenderer
|
||
|
{
|
||
|
class OutlineRenderTexture
|
||
|
{
|
||
|
public Texture outlineTexture;
|
||
|
public bool dirty;
|
||
|
}
|
||
|
|
||
|
Material m_OutlineMaterial;
|
||
|
Material m_BitMaskMaterial;
|
||
|
Material m_EdgeOutlineMaterial;
|
||
|
Mesh m_CircleMesh;
|
||
|
Dictionary<string, OutlineRenderTexture> m_OutlineTextureCache = new ();
|
||
|
SkinningEvents m_EventSystem;
|
||
|
|
||
|
static readonly int k_OutlineColorProperty = Shader.PropertyToID("_OutlineColor");
|
||
|
static readonly int k_OutlineSizeProperty = Shader.PropertyToID("_OutlineSize");
|
||
|
static readonly int k_AdjustLinearForGammaProperty = Shader.PropertyToID("_AdjustLinearForGamma");
|
||
|
|
||
|
const int k_ReferenceTextureSize = 1024;
|
||
|
|
||
|
public SpriteOutlineRenderer(SkinningEvents eventSystem)
|
||
|
{
|
||
|
m_EdgeOutlineMaterial = new Material(Shader.Find("Hidden/2D-Animation-SpriteEdgeOutline")) { hideFlags = HideFlags.HideAndDontSave };
|
||
|
m_BitMaskMaterial = new Material(Shader.Find("Hidden/2D-Animation-SpriteBitmask")) { hideFlags = HideFlags.HideAndDontSave };
|
||
|
m_OutlineMaterial = new Material(Shader.Find("Hidden/2D-Animation-SpriteOutline")) { hideFlags = HideFlags.HideAndDontSave };
|
||
|
|
||
|
m_EventSystem = eventSystem;
|
||
|
m_EventSystem.meshPreviewChanged.AddListener(OnMeshPreviewChanged);
|
||
|
m_EventSystem.selectedSpriteChanged.AddListener(OnSelectionChanged);
|
||
|
m_CircleMesh = GenerateCircleMesh();
|
||
|
}
|
||
|
|
||
|
public void Dispose()
|
||
|
{
|
||
|
DestroyMaterialsAndMeshes();
|
||
|
DestroyTextures();
|
||
|
|
||
|
m_EventSystem.meshPreviewChanged.RemoveListener(OnMeshPreviewChanged);
|
||
|
m_EventSystem.selectedSpriteChanged.RemoveListener(OnSelectionChanged);
|
||
|
}
|
||
|
|
||
|
internal void RenderSpriteOutline(ISpriteEditor spriteEditor, SpriteCache sprite)
|
||
|
{
|
||
|
if (spriteEditor == null || sprite == null)
|
||
|
return;
|
||
|
|
||
|
if (SelectionOutlineSettings.selectedSpriteOutlineSize < 0.01f || SelectionOutlineSettings.outlineColor.a < 0.01f)
|
||
|
return;
|
||
|
|
||
|
var mesh = GetMesh(sprite);
|
||
|
if (mesh == null)
|
||
|
return;
|
||
|
|
||
|
UnityEngine.Profiling.Profiler.BeginSample("SpriteOutlineRenderer::RenderSpriteOutline");
|
||
|
|
||
|
var vertices = mesh.vertices;
|
||
|
var edges = sprite.GetMesh().edges;
|
||
|
var multMatrix = Handles.matrix * sprite.GetLocalToWorldMatrixFromMode();
|
||
|
|
||
|
var texture = spriteEditor.GetDataProvider<ITextureDataProvider>().texture;
|
||
|
var outlineSize = SelectionOutlineSettings.selectedSpriteOutlineSize;
|
||
|
var outlineColor = SelectionOutlineSettings.outlineColor;
|
||
|
var adjustForGamma = PlayerSettings.colorSpace == ColorSpace.Linear ? 1.0f : 0.0f;
|
||
|
|
||
|
if (edges != null && edges.Length > 0 && vertices.Length > 0)
|
||
|
{
|
||
|
var finalOutlineSize = outlineSize / spriteEditor.zoomLevel;
|
||
|
DrawEdgeOutline(edges, vertices, multMatrix, finalOutlineSize, outlineColor, adjustForGamma);
|
||
|
}
|
||
|
else // Fallback: Draw using the Sobel filter.
|
||
|
{
|
||
|
var finalOutlineSize = Mathf.Max(texture.width, texture.height) * outlineSize / k_ReferenceTextureSize;
|
||
|
DrawMeshOutline(mesh, sprite, multMatrix, finalOutlineSize, outlineColor, adjustForGamma);
|
||
|
}
|
||
|
|
||
|
UnityEngine.Profiling.Profiler.EndSample();
|
||
|
}
|
||
|
|
||
|
void DrawEdgeOutline(int2[] edges, Vector3[] vertices, Matrix4x4 multMatrix, float outlineSize, Color outlineColor, float adjustForGamma)
|
||
|
{
|
||
|
m_EdgeOutlineMaterial.SetColor(k_OutlineColorProperty, outlineColor);
|
||
|
m_EdgeOutlineMaterial.SetFloat(k_AdjustLinearForGammaProperty, adjustForGamma);
|
||
|
m_EdgeOutlineMaterial.SetPass(0);
|
||
|
|
||
|
var edgeCount = edges.Length;
|
||
|
var vertexCount = vertices.Length;
|
||
|
|
||
|
GL.PushMatrix();
|
||
|
GL.MultMatrix(multMatrix);
|
||
|
GL.Begin(GL.QUADS);
|
||
|
for (var i = 0; i < edgeCount; i++)
|
||
|
{
|
||
|
var currentEdge = edges[i];
|
||
|
if (currentEdge.x < 0 || currentEdge.y < 0 || currentEdge.x >= vertexCount || currentEdge.y >= vertexCount)
|
||
|
continue;
|
||
|
|
||
|
var start = vertices[edges[i].x];
|
||
|
var end = vertices[edges[i].y];
|
||
|
var direction = (end - start).normalized;
|
||
|
var right = Vector3.Cross(Vector3.forward, direction) * outlineSize;
|
||
|
|
||
|
GL.Vertex(start - right);
|
||
|
GL.Vertex(start + right);
|
||
|
GL.Vertex(end + right);
|
||
|
GL.Vertex(end - right);
|
||
|
}
|
||
|
GL.End();
|
||
|
GL.PopMatrix();
|
||
|
|
||
|
for (var i = 0; i < edgeCount; i++)
|
||
|
{
|
||
|
var currentEdge = edges[i];
|
||
|
if (currentEdge.x < 0 || currentEdge.y < 0 || currentEdge.x >= vertexCount || currentEdge.y >= vertexCount)
|
||
|
continue;
|
||
|
|
||
|
var start = vertices[edges[i].x];
|
||
|
var end = vertices[edges[i].y];
|
||
|
|
||
|
Graphics.DrawMeshNow(m_CircleMesh, multMatrix * Matrix4x4.TRS(start, Quaternion.identity, Vector3.one * outlineSize));
|
||
|
Graphics.DrawMeshNow(m_CircleMesh, multMatrix * Matrix4x4.TRS(end, Quaternion.identity, Vector3.one * outlineSize));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void DrawMeshOutline(Mesh mesh, SpriteCache spriteCache, Matrix4x4 multMatrix, float outlineSize, Color outlineColor, float adjustForGamma)
|
||
|
{
|
||
|
TryRegenerateMaskTexture(spriteCache);
|
||
|
|
||
|
m_OutlineMaterial.SetColor(k_OutlineColorProperty, outlineColor);
|
||
|
m_OutlineMaterial.SetFloat(k_AdjustLinearForGammaProperty, adjustForGamma);
|
||
|
m_OutlineMaterial.SetFloat(k_OutlineSizeProperty, outlineSize);
|
||
|
m_OutlineMaterial.SetPass(0);
|
||
|
|
||
|
GL.PushMatrix();
|
||
|
GL.MultMatrix(multMatrix);
|
||
|
|
||
|
var meshBoundsRect = new Rect(mesh.bounds.min.x, mesh.bounds.min.y, mesh.bounds.size.x, mesh.bounds.size.y);
|
||
|
GL.Begin(GL.QUADS);
|
||
|
GL.Color(Color.white);
|
||
|
GL.TexCoord(new Vector3(0, 0, 0));
|
||
|
GL.Vertex3(meshBoundsRect.xMin, meshBoundsRect.yMin, 0);
|
||
|
|
||
|
GL.TexCoord(new Vector3(1, 0, 0));
|
||
|
GL.Vertex3(meshBoundsRect.xMax, meshBoundsRect.yMin, 0);
|
||
|
|
||
|
GL.TexCoord(new Vector3(1, 1, 0));
|
||
|
GL.Vertex3(meshBoundsRect.xMax, meshBoundsRect.yMax, 0);
|
||
|
|
||
|
GL.TexCoord(new Vector3(0, 1, 0));
|
||
|
GL.Vertex3(meshBoundsRect.xMin, meshBoundsRect.yMax, 0);
|
||
|
GL.End();
|
||
|
GL.PopMatrix();
|
||
|
}
|
||
|
|
||
|
Texture GenerateOutlineTexture(SpriteCache spriteCache, RenderTexture reuseRT)
|
||
|
{
|
||
|
if (spriteCache == null)
|
||
|
return null;
|
||
|
|
||
|
var mesh = GetMesh(spriteCache);
|
||
|
if (mesh == null || (int)mesh.bounds.size.x == 0 || (int)mesh.bounds.size.y == 0)
|
||
|
return null;
|
||
|
|
||
|
var bounds = mesh.bounds;
|
||
|
UnityEngine.Profiling.Profiler.BeginSample("SpriteOutlineRenderer::GenerateOutlineTexture");
|
||
|
|
||
|
if (reuseRT == null || reuseRT.width != (int)bounds.size.x || reuseRT.height != (int)bounds.size.y)
|
||
|
{
|
||
|
UnityEngine.Profiling.Profiler.BeginSample("SpriteOutlineRenderer::CreateRT");
|
||
|
if(reuseRT != null)
|
||
|
Object.DestroyImmediate(reuseRT);
|
||
|
reuseRT = new RenderTexture((int)bounds.size.x, (int)bounds.size.y, 24, RenderTextureFormat.ARGBHalf) { filterMode = FilterMode.Bilinear };
|
||
|
UnityEngine.Profiling.Profiler.EndSample();
|
||
|
}
|
||
|
|
||
|
var oldRT = RenderTexture.active;
|
||
|
Graphics.SetRenderTarget(reuseRT);
|
||
|
m_BitMaskMaterial.SetPass(0);
|
||
|
UnityEngine.Profiling.Profiler.BeginSample("SpriteOutlineRenderer::DrawMesh");
|
||
|
GL.Clear(false, true, new Color(0, 0, 0, 0));
|
||
|
GL.PushMatrix();
|
||
|
GL.LoadOrtho();
|
||
|
var h = bounds.size.y * 0.5f;
|
||
|
var w = h * (bounds.size.x / bounds.size.y);
|
||
|
GL.LoadProjectionMatrix(Matrix4x4.Ortho(-w, w, -h, h, -1, 1));
|
||
|
GL.Begin(GL.QUADS);
|
||
|
GL.Color(Color.white);
|
||
|
Graphics.DrawMeshNow(mesh, Matrix4x4.Translate(-bounds.center));
|
||
|
GL.End();
|
||
|
GL.PopMatrix();
|
||
|
|
||
|
Graphics.SetRenderTarget(oldRT);
|
||
|
UnityEngine.Profiling.Profiler.EndSample();
|
||
|
|
||
|
UnityEngine.Profiling.Profiler.EndSample();
|
||
|
return reuseRT;
|
||
|
}
|
||
|
|
||
|
static Mesh GetMesh(SpriteCache sprite)
|
||
|
{
|
||
|
var meshPreview = sprite.GetMeshPreview();
|
||
|
|
||
|
if (meshPreview == null)
|
||
|
return null;
|
||
|
|
||
|
return meshPreview.mesh.vertexCount > 0 ? meshPreview.mesh : meshPreview.defaultMesh;
|
||
|
}
|
||
|
|
||
|
static Mesh GenerateCircleMesh()
|
||
|
{
|
||
|
const int triangleVerts = 9;
|
||
|
var verts = new Vector3[triangleVerts];
|
||
|
for (var i = 1; i < verts.Length; i++)
|
||
|
{
|
||
|
verts[i] = Quaternion.Euler(0, 0, i * 360f / (verts.Length - 1)) * Vector3.up;
|
||
|
}
|
||
|
var indices = new int[(verts.Length - 1) * 3];
|
||
|
var index = 0;
|
||
|
for (var i = 1; i < triangleVerts; i++)
|
||
|
{
|
||
|
indices[index++] = 0;
|
||
|
indices[index++] = i;
|
||
|
indices[index++] = i + 1 < triangleVerts ? i + 1 : 1;
|
||
|
}
|
||
|
|
||
|
return new Mesh { vertices = verts, triangles = indices, hideFlags = HideFlags.HideAndDontSave };
|
||
|
}
|
||
|
|
||
|
void OnMeshPreviewChanged(MeshPreviewCache mesh)
|
||
|
{
|
||
|
AddOrUpdateMaskTexture(mesh.sprite, true);
|
||
|
}
|
||
|
|
||
|
void OnSelectionChanged(SpriteCache spriteCache)
|
||
|
{
|
||
|
AddOrUpdateMaskTexture(spriteCache, false);
|
||
|
}
|
||
|
|
||
|
void DestroyMaterialsAndMeshes()
|
||
|
{
|
||
|
if(m_EdgeOutlineMaterial != null)
|
||
|
Object.DestroyImmediate(m_EdgeOutlineMaterial);
|
||
|
if (m_BitMaskMaterial != null)
|
||
|
Object.DestroyImmediate(m_BitMaskMaterial);
|
||
|
if (m_OutlineMaterial != null)
|
||
|
Object.DestroyImmediate(m_OutlineMaterial);
|
||
|
|
||
|
if(m_CircleMesh != null)
|
||
|
Object.DestroyImmediate(m_CircleMesh);
|
||
|
}
|
||
|
|
||
|
void DestroyTextures()
|
||
|
{
|
||
|
if (m_OutlineTextureCache != null)
|
||
|
{
|
||
|
foreach (var value in m_OutlineTextureCache.Values)
|
||
|
{
|
||
|
if (value != null && value.outlineTexture != null)
|
||
|
Object.DestroyImmediate(value.outlineTexture);
|
||
|
}
|
||
|
m_OutlineTextureCache.Clear();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void AddOrUpdateMaskTexture(SpriteCache sprite, bool regenerate)
|
||
|
{
|
||
|
if (m_OutlineTextureCache != null && sprite != null)
|
||
|
{
|
||
|
if (!m_OutlineTextureCache.ContainsKey(sprite.id))
|
||
|
m_OutlineTextureCache.Add(sprite.id, new OutlineRenderTexture() {dirty = true});
|
||
|
|
||
|
var outlineTextureCache = m_OutlineTextureCache[sprite.id];
|
||
|
outlineTextureCache.dirty |= regenerate;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void TryRegenerateMaskTexture(SpriteCache sprite)
|
||
|
{
|
||
|
var selectedSprite = sprite.skinningCache.selectedSprite;
|
||
|
|
||
|
var outlineTextureCache = m_OutlineTextureCache[sprite.id];
|
||
|
if (sprite == selectedSprite)
|
||
|
{
|
||
|
if (outlineTextureCache.dirty || outlineTextureCache.outlineTexture == null)
|
||
|
{
|
||
|
outlineTextureCache.outlineTexture = GenerateOutlineTexture(sprite, (RenderTexture)outlineTextureCache.outlineTexture);
|
||
|
if (outlineTextureCache.outlineTexture != null)
|
||
|
{
|
||
|
outlineTextureCache.outlineTexture.hideFlags = HideFlags.HideAndDontSave;
|
||
|
outlineTextureCache.dirty = false;
|
||
|
}
|
||
|
}
|
||
|
m_OutlineMaterial.mainTexture = outlineTextureCache.outlineTexture;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|