-
-
Save yasirkula/391fa12bc173acdf5ac48c466f180708 to your computer and use it in GitHub Desktop.
using System; | |
using System.Collections.Generic; | |
using UnityEngine; | |
using UnityEngine.UI; | |
#if UNITY_2017_4 || UNITY_2018_2_OR_NEWER | |
using UnityEngine.U2D; | |
#endif | |
using Sprites = UnityEngine.Sprites; | |
#if UNITY_EDITOR | |
using UnityEditor; | |
// Custom Editor to order the variables in the Inspector similar to Image component | |
[CustomEditor( typeof( SlicedFilledImage ) ), CanEditMultipleObjects] | |
public class SlicedFilledImageEditor : Editor | |
{ | |
private SerializedProperty spriteProp, colorProp; | |
private GUIContent spriteLabel; | |
private void OnEnable() | |
{ | |
spriteProp = serializedObject.FindProperty( "m_Sprite" ); | |
colorProp = serializedObject.FindProperty( "m_Color" ); | |
spriteLabel = new GUIContent( "Source Image" ); | |
} | |
public override void OnInspectorGUI() | |
{ | |
serializedObject.Update(); | |
EditorGUILayout.PropertyField( spriteProp, spriteLabel ); | |
EditorGUILayout.PropertyField( colorProp ); | |
DrawPropertiesExcluding( serializedObject, "m_Script", "m_Sprite", "m_Color", "m_OnCullStateChanged" ); | |
serializedObject.ApplyModifiedProperties(); | |
} | |
} | |
#endif | |
// Credit: https://bitbucket.org/Unity-Technologies/ui/src/2018.4/UnityEngine.UI/UI/Core/Image.cs | |
[RequireComponent( typeof( CanvasRenderer ) )] | |
[AddComponentMenu( "UI/Sliced Filled Image", 11 )] | |
public class SlicedFilledImage : MaskableGraphic, ISerializationCallbackReceiver, ILayoutElement, ICanvasRaycastFilter | |
{ | |
private static class SetPropertyUtility | |
{ | |
public static bool SetStruct<T>( ref T currentValue, T newValue ) where T : struct | |
{ | |
if( EqualityComparer<T>.Default.Equals( currentValue, newValue ) ) | |
return false; | |
currentValue = newValue; | |
return true; | |
} | |
public static bool SetClass<T>( ref T currentValue, T newValue ) where T : class | |
{ | |
if( ( currentValue == null && newValue == null ) || ( currentValue != null && currentValue.Equals( newValue ) ) ) | |
return false; | |
currentValue = newValue; | |
return true; | |
} | |
} | |
public enum FillDirection { Right = 0, Left = 1, Up = 2, Down = 3 } | |
private static readonly Vector3[] s_Vertices = new Vector3[4]; | |
private static readonly Vector2[] s_UVs = new Vector2[4]; | |
private static readonly Vector2[] s_SlicedVertices = new Vector2[4]; | |
private static readonly Vector2[] s_SlicedUVs = new Vector2[4]; | |
#pragma warning disable 1692 | |
#pragma warning disable IDE1006 // Suppress 'Naming rule violation' warnings | |
#pragma warning disable 0649 | |
[SerializeField] | |
private Sprite m_Sprite; | |
public Sprite sprite | |
{ | |
get { return m_Sprite; } | |
set | |
{ | |
if( SetPropertyUtility.SetClass( ref m_Sprite, value ) ) | |
{ | |
SetAllDirty(); | |
TrackImage(); | |
} | |
} | |
} | |
[SerializeField] | |
private FillDirection m_FillDirection; | |
public FillDirection fillDirection | |
{ | |
get { return m_FillDirection; } | |
set | |
{ | |
if( SetPropertyUtility.SetStruct( ref m_FillDirection, value ) ) | |
SetVerticesDirty(); | |
} | |
} | |
[Range( 0, 1 )] | |
[SerializeField] | |
private float m_FillAmount = 1f; | |
public float fillAmount | |
{ | |
get { return m_FillAmount; } | |
set | |
{ | |
if( SetPropertyUtility.SetStruct( ref m_FillAmount, Mathf.Clamp01( value ) ) ) | |
SetVerticesDirty(); | |
} | |
} | |
[SerializeField] | |
private bool m_FillCenter = true; | |
public bool fillCenter | |
{ | |
get { return m_FillCenter; } | |
set | |
{ | |
if( SetPropertyUtility.SetStruct( ref m_FillCenter, value ) ) | |
SetVerticesDirty(); | |
} | |
} | |
[SerializeField] | |
private float m_PixelsPerUnitMultiplier = 1f; | |
public float pixelsPerUnitMultiplier | |
{ | |
get { return m_PixelsPerUnitMultiplier; } | |
set { m_PixelsPerUnitMultiplier = Mathf.Max( 0.01f, value ); } | |
} | |
public float pixelsPerUnit | |
{ | |
get | |
{ | |
float spritePixelsPerUnit = 100; | |
if( activeSprite ) | |
spritePixelsPerUnit = activeSprite.pixelsPerUnit; | |
float referencePixelsPerUnit = 100; | |
if( canvas ) | |
referencePixelsPerUnit = canvas.referencePixelsPerUnit; | |
return m_PixelsPerUnitMultiplier * spritePixelsPerUnit / referencePixelsPerUnit; | |
} | |
} | |
#pragma warning restore 0649 | |
[NonSerialized] | |
private Sprite m_OverrideSprite; | |
public Sprite overrideSprite | |
{ | |
get { return activeSprite; } | |
set | |
{ | |
if( SetPropertyUtility.SetClass( ref m_OverrideSprite, value ) ) | |
{ | |
SetAllDirty(); | |
TrackImage(); | |
} | |
} | |
} | |
private Sprite activeSprite { get { return m_OverrideSprite != null ? m_OverrideSprite : m_Sprite; } } | |
public override Texture mainTexture | |
{ | |
get | |
{ | |
if( activeSprite != null ) | |
return activeSprite.texture; | |
return material != null && material.mainTexture != null ? material.mainTexture : s_WhiteTexture; | |
} | |
} | |
public bool hasBorder | |
{ | |
get | |
{ | |
if( activeSprite != null ) | |
{ | |
Vector4 v = activeSprite.border; | |
return v.sqrMagnitude > 0f; | |
} | |
return false; | |
} | |
} | |
public override Material material | |
{ | |
get | |
{ | |
if( m_Material != null ) | |
return m_Material; | |
if( activeSprite && activeSprite.associatedAlphaSplitTexture != null ) | |
{ | |
#if UNITY_EDITOR | |
if( Application.isPlaying ) | |
#endif | |
return Image.defaultETC1GraphicMaterial; | |
} | |
return defaultMaterial; | |
} | |
set { base.material = value; } | |
} | |
public float alphaHitTestMinimumThreshold { get; set; } | |
#pragma warning restore IDE1006 | |
#pragma warning restore 1692 | |
protected SlicedFilledImage() | |
{ | |
useLegacyMeshGeneration = false; | |
} | |
protected override void OnEnable() | |
{ | |
base.OnEnable(); | |
TrackImage(); | |
} | |
protected override void OnDisable() | |
{ | |
base.OnDisable(); | |
if( m_Tracked ) | |
UnTrackImage(); | |
} | |
#if UNITY_EDITOR | |
protected override void OnValidate() | |
{ | |
base.OnValidate(); | |
m_PixelsPerUnitMultiplier = Mathf.Max( 0.01f, m_PixelsPerUnitMultiplier ); | |
} | |
#endif | |
protected override void OnPopulateMesh( VertexHelper vh ) | |
{ | |
if( activeSprite == null ) | |
{ | |
base.OnPopulateMesh( vh ); | |
return; | |
} | |
GenerateSlicedFilledSprite( vh ); | |
} | |
/// <summary> | |
/// Update the renderer's material. | |
/// </summary> | |
protected override void UpdateMaterial() | |
{ | |
base.UpdateMaterial(); | |
// Check if this sprite has an associated alpha texture (generated when splitting RGBA = RGB + A as two textures without alpha) | |
if( activeSprite == null ) | |
{ | |
canvasRenderer.SetAlphaTexture( null ); | |
return; | |
} | |
Texture2D alphaTex = activeSprite.associatedAlphaSplitTexture; | |
if( alphaTex != null ) | |
canvasRenderer.SetAlphaTexture( alphaTex ); | |
} | |
private void GenerateSlicedFilledSprite( VertexHelper vh ) | |
{ | |
vh.Clear(); | |
if( m_FillAmount < 0.001f ) | |
return; | |
Rect rect = GetPixelAdjustedRect(); | |
Vector4 outer = Sprites.DataUtility.GetOuterUV( activeSprite ); | |
Vector4 padding = Sprites.DataUtility.GetPadding( activeSprite ); | |
if( !hasBorder ) | |
{ | |
Vector2 size = activeSprite.rect.size; | |
int spriteW = Mathf.RoundToInt( size.x ); | |
int spriteH = Mathf.RoundToInt( size.y ); | |
// Image's dimensions used for drawing. X = left, Y = bottom, Z = right, W = top. | |
Vector4 vertices = new Vector4( | |
rect.x + rect.width * ( padding.x / spriteW ), | |
rect.y + rect.height * ( padding.y / spriteH ), | |
rect.x + rect.width * ( ( spriteW - padding.z ) / spriteW ), | |
rect.y + rect.height * ( ( spriteH - padding.w ) / spriteH ) ); | |
GenerateFilledSprite( vh, vertices, outer, m_FillAmount ); | |
return; | |
} | |
Vector4 inner = Sprites.DataUtility.GetInnerUV( activeSprite ); | |
Vector4 border = GetAdjustedBorders( activeSprite.border / pixelsPerUnit, rect ); | |
padding = padding / pixelsPerUnit; | |
s_SlicedVertices[0] = new Vector2( padding.x, padding.y ); | |
s_SlicedVertices[3] = new Vector2( rect.width - padding.z, rect.height - padding.w ); | |
s_SlicedVertices[1].x = border.x; | |
s_SlicedVertices[1].y = border.y; | |
s_SlicedVertices[2].x = rect.width - border.z; | |
s_SlicedVertices[2].y = rect.height - border.w; | |
for( int i = 0; i < 4; ++i ) | |
{ | |
s_SlicedVertices[i].x += rect.x; | |
s_SlicedVertices[i].y += rect.y; | |
} | |
s_SlicedUVs[0] = new Vector2( outer.x, outer.y ); | |
s_SlicedUVs[1] = new Vector2( inner.x, inner.y ); | |
s_SlicedUVs[2] = new Vector2( inner.z, inner.w ); | |
s_SlicedUVs[3] = new Vector2( outer.z, outer.w ); | |
float rectStartPos; | |
float _1OverTotalSize; | |
if( m_FillDirection == FillDirection.Left || m_FillDirection == FillDirection.Right ) | |
{ | |
rectStartPos = s_SlicedVertices[0].x; | |
float totalSize = ( s_SlicedVertices[3].x - s_SlicedVertices[0].x ); | |
_1OverTotalSize = totalSize > 0f ? 1f / totalSize : 1f; | |
} | |
else | |
{ | |
rectStartPos = s_SlicedVertices[0].y; | |
float totalSize = ( s_SlicedVertices[3].y - s_SlicedVertices[0].y ); | |
_1OverTotalSize = totalSize > 0f ? 1f / totalSize : 1f; | |
} | |
for( int x = 0; x < 3; x++ ) | |
{ | |
int x2 = x + 1; | |
for( int y = 0; y < 3; y++ ) | |
{ | |
if( !m_FillCenter && x == 1 && y == 1 ) | |
continue; | |
int y2 = y + 1; | |
float sliceStart, sliceEnd; | |
switch( m_FillDirection ) | |
{ | |
case FillDirection.Right: | |
sliceStart = ( s_SlicedVertices[x].x - rectStartPos ) * _1OverTotalSize; | |
sliceEnd = ( s_SlicedVertices[x2].x - rectStartPos ) * _1OverTotalSize; | |
break; | |
case FillDirection.Up: | |
sliceStart = ( s_SlicedVertices[y].y - rectStartPos ) * _1OverTotalSize; | |
sliceEnd = ( s_SlicedVertices[y2].y - rectStartPos ) * _1OverTotalSize; | |
break; | |
case FillDirection.Left: | |
sliceStart = 1f - ( s_SlicedVertices[x2].x - rectStartPos ) * _1OverTotalSize; | |
sliceEnd = 1f - ( s_SlicedVertices[x].x - rectStartPos ) * _1OverTotalSize; | |
break; | |
case FillDirection.Down: | |
sliceStart = 1f - ( s_SlicedVertices[y2].y - rectStartPos ) * _1OverTotalSize; | |
sliceEnd = 1f - ( s_SlicedVertices[y].y - rectStartPos ) * _1OverTotalSize; | |
break; | |
default: // Just there to get rid of the "Use of unassigned local variable" compiler error | |
sliceStart = sliceEnd = 0f; | |
break; | |
} | |
if( sliceStart >= m_FillAmount ) | |
continue; | |
Vector4 vertices = new Vector4( s_SlicedVertices[x].x, s_SlicedVertices[y].y, s_SlicedVertices[x2].x, s_SlicedVertices[y2].y ); | |
Vector4 uvs = new Vector4( s_SlicedUVs[x].x, s_SlicedUVs[y].y, s_SlicedUVs[x2].x, s_SlicedUVs[y2].y ); | |
float fillAmount = ( m_FillAmount - sliceStart ) / ( sliceEnd - sliceStart ); | |
GenerateFilledSprite( vh, vertices, uvs, fillAmount ); | |
} | |
} | |
} | |
private Vector4 GetAdjustedBorders( Vector4 border, Rect adjustedRect ) | |
{ | |
Rect originalRect = rectTransform.rect; | |
for( int axis = 0; axis <= 1; axis++ ) | |
{ | |
float borderScaleRatio; | |
// The adjusted rect (adjusted for pixel correctness) may be slightly larger than the original rect. | |
// Adjust the border to match the adjustedRect to avoid small gaps between borders (case 833201). | |
if( originalRect.size[axis] != 0 ) | |
{ | |
borderScaleRatio = adjustedRect.size[axis] / originalRect.size[axis]; | |
border[axis] *= borderScaleRatio; | |
border[axis + 2] *= borderScaleRatio; | |
} | |
// If the rect is smaller than the combined borders, then there's not room for the borders at their normal size. | |
// In order to avoid artefacts with overlapping borders, we scale the borders down to fit. | |
float combinedBorders = border[axis] + border[axis + 2]; | |
if( adjustedRect.size[axis] < combinedBorders && combinedBorders != 0 ) | |
{ | |
borderScaleRatio = adjustedRect.size[axis] / combinedBorders; | |
border[axis] *= borderScaleRatio; | |
border[axis + 2] *= borderScaleRatio; | |
} | |
} | |
return border; | |
} | |
private void GenerateFilledSprite( VertexHelper vh, Vector4 vertices, Vector4 uvs, float fillAmount ) | |
{ | |
if( m_FillAmount < 0.001f ) | |
return; | |
float uvLeft = uvs.x; | |
float uvBottom = uvs.y; | |
float uvRight = uvs.z; | |
float uvTop = uvs.w; | |
if( fillAmount < 1f ) | |
{ | |
if( m_FillDirection == FillDirection.Left || m_FillDirection == FillDirection.Right ) | |
{ | |
if( m_FillDirection == FillDirection.Left ) | |
{ | |
vertices.x = vertices.z - ( vertices.z - vertices.x ) * fillAmount; | |
uvLeft = uvRight - ( uvRight - uvLeft ) * fillAmount; | |
} | |
else | |
{ | |
vertices.z = vertices.x + ( vertices.z - vertices.x ) * fillAmount; | |
uvRight = uvLeft + ( uvRight - uvLeft ) * fillAmount; | |
} | |
} | |
else | |
{ | |
if( m_FillDirection == FillDirection.Down ) | |
{ | |
vertices.y = vertices.w - ( vertices.w - vertices.y ) * fillAmount; | |
uvBottom = uvTop - ( uvTop - uvBottom ) * fillAmount; | |
} | |
else | |
{ | |
vertices.w = vertices.y + ( vertices.w - vertices.y ) * fillAmount; | |
uvTop = uvBottom + ( uvTop - uvBottom ) * fillAmount; | |
} | |
} | |
} | |
s_Vertices[0] = new Vector3( vertices.x, vertices.y ); | |
s_Vertices[1] = new Vector3( vertices.x, vertices.w ); | |
s_Vertices[2] = new Vector3( vertices.z, vertices.w ); | |
s_Vertices[3] = new Vector3( vertices.z, vertices.y ); | |
s_UVs[0] = new Vector2( uvLeft, uvBottom ); | |
s_UVs[1] = new Vector2( uvLeft, uvTop ); | |
s_UVs[2] = new Vector2( uvRight, uvTop ); | |
s_UVs[3] = new Vector2( uvRight, uvBottom ); | |
int startIndex = vh.currentVertCount; | |
for( int i = 0; i < 4; i++ ) | |
vh.AddVert( s_Vertices[i], color, s_UVs[i] ); | |
vh.AddTriangle( startIndex, startIndex + 1, startIndex + 2 ); | |
vh.AddTriangle( startIndex + 2, startIndex + 3, startIndex ); | |
} | |
int ILayoutElement.layoutPriority { get { return 0; } } | |
float ILayoutElement.minWidth { get { return 0; } } | |
float ILayoutElement.minHeight { get { return 0; } } | |
float ILayoutElement.flexibleWidth { get { return -1; } } | |
float ILayoutElement.flexibleHeight { get { return -1; } } | |
float ILayoutElement.preferredWidth | |
{ | |
get | |
{ | |
if( activeSprite == null ) | |
return 0; | |
return Sprites.DataUtility.GetMinSize( activeSprite ).x / pixelsPerUnit; | |
} | |
} | |
float ILayoutElement.preferredHeight | |
{ | |
get | |
{ | |
if( activeSprite == null ) | |
return 0; | |
return Sprites.DataUtility.GetMinSize( activeSprite ).y / pixelsPerUnit; | |
} | |
} | |
void ILayoutElement.CalculateLayoutInputHorizontal() { } | |
void ILayoutElement.CalculateLayoutInputVertical() { } | |
bool ICanvasRaycastFilter.IsRaycastLocationValid( Vector2 screenPoint, Camera eventCamera ) | |
{ | |
if( alphaHitTestMinimumThreshold <= 0 ) | |
return true; | |
if( alphaHitTestMinimumThreshold > 1 ) | |
return false; | |
if( activeSprite == null ) | |
return true; | |
Vector2 local; | |
if( !RectTransformUtility.ScreenPointToLocalPointInRectangle( rectTransform, screenPoint, eventCamera, out local ) ) | |
return false; | |
Rect rect = GetPixelAdjustedRect(); | |
// Convert to have lower left corner as reference point. | |
local.x += rectTransform.pivot.x * rect.width; | |
local.y += rectTransform.pivot.y * rect.height; | |
Rect spriteRect = activeSprite.rect; | |
Vector4 border = activeSprite.border; | |
Vector4 adjustedBorder = GetAdjustedBorders( border / pixelsPerUnit, rect ); | |
for( int i = 0; i < 2; i++ ) | |
{ | |
if( local[i] <= adjustedBorder[i] ) | |
continue; | |
if( rect.size[i] - local[i] <= adjustedBorder[i + 2] ) | |
{ | |
local[i] -= ( rect.size[i] - spriteRect.size[i] ); | |
continue; | |
} | |
float lerp = Mathf.InverseLerp( adjustedBorder[i], rect.size[i] - adjustedBorder[i + 2], local[i] ); | |
local[i] = Mathf.Lerp( border[i], spriteRect.size[i] - border[i + 2], lerp ); | |
} | |
// Normalize local coordinates. | |
Rect textureRect = activeSprite.textureRect; | |
Vector2 normalized = new Vector2( local.x / textureRect.width, local.y / textureRect.height ); | |
// Convert to texture space. | |
float x = Mathf.Lerp( textureRect.x, textureRect.xMax, normalized.x ) / activeSprite.texture.width; | |
float y = Mathf.Lerp( textureRect.y, textureRect.yMax, normalized.y ) / activeSprite.texture.height; | |
switch( m_FillDirection ) | |
{ | |
case FillDirection.Right: | |
if( x > m_FillAmount ) | |
return false; | |
break; | |
case FillDirection.Left: | |
if( 1f - x > m_FillAmount ) | |
return false; | |
break; | |
case FillDirection.Up: | |
if( y > m_FillAmount ) | |
return false; | |
break; | |
case FillDirection.Down: | |
if( 1f - y > m_FillAmount ) | |
return false; | |
break; | |
} | |
try | |
{ | |
return activeSprite.texture.GetPixelBilinear( x, y ).a >= alphaHitTestMinimumThreshold; | |
} | |
catch( UnityException e ) | |
{ | |
Debug.LogError( "Using alphaHitTestMinimumThreshold greater than 0 on Image whose sprite texture cannot be read. " + e.Message + " Also make sure to disable sprite packing for this sprite.", this ); | |
return true; | |
} | |
} | |
void ISerializationCallbackReceiver.OnBeforeSerialize() { } | |
void ISerializationCallbackReceiver.OnAfterDeserialize() | |
{ | |
m_FillAmount = Mathf.Clamp01( m_FillAmount ); | |
} | |
// Whether this is being tracked for Atlas Binding | |
private bool m_Tracked = false; | |
#if UNITY_2017_4 || UNITY_2018_2_OR_NEWER | |
private static List<SlicedFilledImage> m_TrackedTexturelessImages = new List<SlicedFilledImage>(); | |
private static bool s_Initialized; | |
#endif | |
private void TrackImage() | |
{ | |
if( activeSprite != null && activeSprite.texture == null ) | |
{ | |
#if UNITY_2017_4 || UNITY_2018_2_OR_NEWER | |
if( !s_Initialized ) | |
{ | |
SpriteAtlasManager.atlasRegistered += RebuildImage; | |
s_Initialized = true; | |
} | |
m_TrackedTexturelessImages.Add( this ); | |
#endif | |
m_Tracked = true; | |
} | |
} | |
private void UnTrackImage() | |
{ | |
#if UNITY_2017_4 || UNITY_2018_2_OR_NEWER | |
m_TrackedTexturelessImages.Remove( this ); | |
#endif | |
m_Tracked = false; | |
} | |
#if UNITY_2017_4 || UNITY_2018_2_OR_NEWER | |
private static void RebuildImage( SpriteAtlas spriteAtlas ) | |
{ | |
for( int i = m_TrackedTexturelessImages.Count - 1; i >= 0; i-- ) | |
{ | |
SlicedFilledImage image = m_TrackedTexturelessImages[i]; | |
if( spriteAtlas.CanBindTo( image.activeSprite ) ) | |
{ | |
image.SetAllDirty(); | |
m_TrackedTexturelessImages.RemoveAt( i ); | |
} | |
} | |
} | |
#endif | |
} |
This is exactly what I needed and why on earth isn't it included in unity. Some of the glaring omissions in unity are insane.
Thank you.
This looks great but when I add it nothing appears. I played with all the settings but nothing. I have an object with just this component and a canvas renderer as an object within my canvas. Any ideas? Does the object need some other component as well? Maybe it doesn't work anymore in Unity 2021.3.3f1 ?
@simonpaulkeating It works on 2021.3.1f1. You must assign a sprite to Source Image if it's null.
Hi, I got it working in the end Thanks. I just re-created it from scratch and it worked. Not sure why my first version didn't work. Thanks again! Great Work!
Just wanted to say, you're a legend. Thank you!
Came from official unity forum, excellent script, thanks for sharing and your spirit of open-sourced.
Thank you 🙂
Thank you 👍
Works like a charm, thank you.
Thank you so much, you saved my day!
Just <3 Thanks ton
Thanks, this is realy helpful