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 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().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; } } } }