Last active
May 26, 2024 05:48
-
-
Save yasirkula/7f34c2e190330da41edcca6b383490ff to your computer and use it in GitHub Desktop.
Create 4-color gradient UI graphics in Unity
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.Generic; | |
using UnityEngine; | |
using UnityEngine.Sprites; | |
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( GradientGraphic ) ), CanEditMultipleObjects] | |
public class GradientGraphicEditor : Editor | |
{ | |
private SerializedProperty spriteProp; | |
private SerializedProperty topLeftColorProp; | |
private SerializedProperty topRightColorProp; | |
private SerializedProperty bottomLeftColorProp; | |
private SerializedProperty bottomRightColorProp; | |
private SerializedProperty gradientSmoothnessProp; | |
private SerializedProperty useSlicedSpriteProp; | |
private SerializedProperty fillCenterProp; | |
private SerializedProperty pixelsPerUnitMultiplierProp; | |
private GUIContent spriteLabel; | |
private void OnEnable() | |
{ | |
spriteProp = serializedObject.FindProperty( "sprite" ); | |
topLeftColorProp = serializedObject.FindProperty( "topLeftColor" ); | |
topRightColorProp = serializedObject.FindProperty( "topRightColor" ); | |
bottomLeftColorProp = serializedObject.FindProperty( "bottomLeftColor" ); | |
bottomRightColorProp = serializedObject.FindProperty( "bottomRightColor" ); | |
gradientSmoothnessProp = serializedObject.FindProperty( "gradientSmoothness" ); | |
useSlicedSpriteProp = serializedObject.FindProperty( "useSlicedSprite" ); | |
fillCenterProp = serializedObject.FindProperty( "fillCenter" ); | |
pixelsPerUnitMultiplierProp = serializedObject.FindProperty( "pixelsPerUnitMultiplier" ); | |
spriteLabel = new GUIContent( "Source Image" ); | |
} | |
public override void OnInspectorGUI() | |
{ | |
bool isSlicedSpriteAssigned = ( (GradientGraphic) target ).hasBorder; | |
serializedObject.Update(); | |
EditorGUILayout.PropertyField( spriteProp, spriteLabel ); | |
EditorGUILayout.PropertyField( topLeftColorProp ); | |
EditorGUILayout.PropertyField( topRightColorProp ); | |
EditorGUILayout.PropertyField( bottomLeftColorProp ); | |
EditorGUILayout.PropertyField( bottomRightColorProp ); | |
EditorGUILayout.PropertyField( gradientSmoothnessProp ); | |
DrawPropertiesExcluding( serializedObject, "m_Script", "sprite", "m_Color", "topLeftColor", "topRightColor", "bottomLeftColor", "bottomRightColor", "gradientSmoothness", "useSlicedSprite", "fillCenter", "pixelsPerUnitMultiplier", "m_OnCullStateChanged" ); | |
if( isSlicedSpriteAssigned ) | |
{ | |
EditorGUILayout.PropertyField( useSlicedSpriteProp ); | |
EditorGUI.indentLevel++; | |
if( useSlicedSpriteProp.boolValue || useSlicedSpriteProp.hasMultipleDifferentValues ) | |
{ | |
EditorGUILayout.PropertyField( fillCenterProp ); | |
EditorGUILayout.PropertyField( pixelsPerUnitMultiplierProp ); | |
} | |
EditorGUI.indentLevel--; | |
} | |
serializedObject.ApplyModifiedProperties(); | |
} | |
} | |
#endif | |
[RequireComponent( typeof( CanvasRenderer ) )] | |
[AddComponentMenu( "UI/Gradient Graphic", 12 )] | |
public class GradientGraphic : MaskableGraphic, ILayoutElement | |
{ | |
private static readonly Vector2[] s_SlicedVertices = new Vector2[4]; | |
private static readonly Vector2[] s_SlicedUVs = new Vector2[4]; | |
public Sprite sprite; | |
public override Texture mainTexture { get { return sprite ? sprite.texture : s_WhiteTexture; } } | |
public Color topLeftColor = Color.white; | |
public Color topRightColor = Color.white; | |
public Color bottomLeftColor = Color.white; | |
public Color bottomRightColor = Color.white; | |
[Range( 1, 5 )] | |
[Tooltip( "Increasing this value will make the gradient smoother by generating more vertices for the UI mesh; increase it only when needed" )] | |
public int gradientSmoothness = 1; | |
public bool useSlicedSprite = true; | |
public bool fillCenter = true; | |
public float pixelsPerUnitMultiplier = 1f; | |
public float pixelsPerUnit | |
{ | |
get | |
{ | |
float spritePixelsPerUnit = 100; | |
if( sprite ) | |
spritePixelsPerUnit = sprite.pixelsPerUnit; | |
float referencePixelsPerUnit = 100; | |
if( canvas ) | |
referencePixelsPerUnit = canvas.referencePixelsPerUnit; | |
return pixelsPerUnitMultiplier * spritePixelsPerUnit / referencePixelsPerUnit; | |
} | |
} | |
public bool hasBorder { get { return sprite && sprite.border.sqrMagnitude > 0f; } } | |
public override Material material | |
{ | |
get | |
{ | |
if( m_Material != null ) | |
return m_Material; | |
if( sprite && sprite.associatedAlphaSplitTexture != null ) | |
{ | |
#if UNITY_EDITOR | |
if( Application.isPlaying ) | |
#endif | |
return Image.defaultETC1GraphicMaterial; | |
} | |
return defaultMaterial; | |
} | |
set { base.material = value; } | |
} | |
protected override void OnEnable() | |
{ | |
base.OnEnable(); | |
TrackImage(); | |
} | |
protected override void OnDisable() | |
{ | |
base.OnDisable(); | |
if( m_Tracked ) | |
UnTrackImage(); | |
} | |
protected override void OnValidate() | |
{ | |
base.OnValidate(); | |
gradientSmoothness = Mathf.Max( gradientSmoothness, 1 ); | |
pixelsPerUnitMultiplier = Mathf.Max( pixelsPerUnitMultiplier, 0.01f ); | |
} | |
protected override void OnPopulateMesh( VertexHelper vh ) | |
{ | |
vh.Clear(); | |
Rect rect = GetPixelAdjustedRect(); | |
float width = rect.width; | |
float height = rect.height; | |
Vector4 uv = sprite ? DataUtility.GetOuterUV( sprite ) : Vector4.zero; | |
Vector2 pivot = rectTransform.pivot; | |
float bottomLeftX = -width * pivot.x; | |
float bottomLeftY = -height * pivot.y; | |
if( useSlicedSprite && hasBorder ) | |
{ | |
// Generate sliced Image | |
Vector4 padding, inner, border; | |
if( sprite ) | |
{ | |
padding = DataUtility.GetPadding( sprite ); | |
inner = DataUtility.GetInnerUV( sprite ); | |
border = sprite.border / pixelsPerUnit; | |
} | |
else | |
{ | |
inner = Vector4.zero; | |
padding = Vector4.zero; | |
border = Vector4.zero; | |
} | |
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 rect to avoid small gaps between borders (case 833201). | |
if( originalRect.size[axis] != 0 ) | |
{ | |
borderScaleRatio = rect.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( rect.size[axis] < combinedBorders && combinedBorders != 0 ) | |
{ | |
borderScaleRatio = rect.size[axis] / combinedBorders; | |
border[axis] *= borderScaleRatio; | |
border[axis + 2] *= borderScaleRatio; | |
} | |
} | |
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( uv.x, uv.y ); | |
s_SlicedUVs[1] = new Vector2( inner.x, inner.y ); | |
s_SlicedUVs[2] = new Vector2( inner.z, inner.w ); | |
s_SlicedUVs[3] = new Vector2( uv.z, uv.w ); | |
float invWidth = 1f / width; | |
float invHeight = 1f / height; | |
for( int x = 0; x < 3; x++ ) | |
{ | |
int x2 = x + 1; | |
for( int y = 0; y < 3; y++ ) | |
{ | |
if( !fillCenter && x == 1 && y == 1 ) | |
continue; | |
int y2 = y + 1; | |
int startIndex = vh.currentVertCount; | |
if( gradientSmoothness <= 1 || x != 1 || y != 1 ) | |
{ | |
Color c1 = GetColorAt( ( s_SlicedVertices[x].x - bottomLeftX ) * invWidth, ( s_SlicedVertices[y].y - bottomLeftY ) * invHeight ); | |
Color c2 = GetColorAt( ( s_SlicedVertices[x].x - bottomLeftX ) * invWidth, ( s_SlicedVertices[y2].y - bottomLeftY ) * invHeight ); | |
Color c3 = GetColorAt( ( s_SlicedVertices[x2].x - bottomLeftX ) * invWidth, ( s_SlicedVertices[y2].y - bottomLeftY ) * invHeight ); | |
Color c4 = GetColorAt( ( s_SlicedVertices[x2].x - bottomLeftX ) * invWidth, ( s_SlicedVertices[y].y - bottomLeftY ) * invHeight ); | |
vh.AddVert( new Vector3( s_SlicedVertices[x].x, s_SlicedVertices[y].y, 0 ), c1, new Vector2( s_SlicedUVs[x].x, s_SlicedUVs[y].y ) ); | |
vh.AddVert( new Vector3( s_SlicedVertices[x].x, s_SlicedVertices[y2].y, 0 ), c2, new Vector2( s_SlicedUVs[x].x, s_SlicedUVs[y2].y ) ); | |
vh.AddVert( new Vector3( s_SlicedVertices[x2].x, s_SlicedVertices[y2].y, 0 ), c3, new Vector2( s_SlicedUVs[x2].x, s_SlicedUVs[y2].y ) ); | |
vh.AddVert( new Vector3( s_SlicedVertices[x2].x, s_SlicedVertices[y].y, 0 ), c4, new Vector2( s_SlicedUVs[x2].x, s_SlicedUVs[y].y ) ); | |
vh.AddTriangle( startIndex, startIndex + 1, startIndex + 2 ); | |
vh.AddTriangle( startIndex + 2, startIndex + 3, startIndex ); | |
} | |
else | |
{ | |
// Generate multiple small quads in the center | |
float invSmoothness = 1f / gradientSmoothness; | |
float _bottomLeftX = s_SlicedVertices[x].x; | |
float _bottomLeftY = s_SlicedVertices[y].y; | |
float _width = s_SlicedVertices[x2].x - _bottomLeftX; | |
float _height = s_SlicedVertices[y2].y - _bottomLeftY; | |
float _bottomLeftUVx = s_SlicedUVs[x].x; | |
float _bottomLeftUVy = s_SlicedUVs[y].y; | |
float _uvWidth = s_SlicedUVs[x2].x - _bottomLeftUVx; | |
float _uvHeight = s_SlicedUVs[y2].y - _bottomLeftUVy; | |
for( int i = 0; i <= gradientSmoothness; i++ ) | |
{ | |
for( int j = 0; j <= gradientSmoothness; j++ ) | |
{ | |
float normalizedX = j * invSmoothness; | |
float normalizedY = i * invSmoothness; | |
Vector3 _position = new Vector3( _bottomLeftX + _width * normalizedX, _bottomLeftY + _height * normalizedY, 0f ); | |
Vector2 _uv = new Vector2( _bottomLeftUVx + _uvWidth * normalizedX, _bottomLeftUVy + _uvHeight * normalizedY ); | |
Color _color = GetColorAt( ( _position.x - bottomLeftX ) * invWidth, ( _position.y - bottomLeftY ) * invHeight ); | |
vh.AddVert( _position, _color, _uv ); | |
} | |
} | |
for( int i = 0; i < gradientSmoothness; i++ ) | |
{ | |
int firstTriangle = startIndex + i * ( gradientSmoothness + 1 ); | |
for( int j = 0; j < gradientSmoothness; j++ ) | |
{ | |
int triangle = firstTriangle + j; | |
int aboveTriangle = triangle + gradientSmoothness + 1; | |
vh.AddTriangle( triangle, aboveTriangle, aboveTriangle + 1 ); | |
vh.AddTriangle( aboveTriangle + 1, triangle + 1, triangle ); | |
} | |
} | |
} | |
} | |
} | |
} | |
else | |
{ | |
// Generate normal Image | |
if( gradientSmoothness <= 1 ) | |
{ | |
// Generate single quad | |
vh.AddVert( new Vector3( bottomLeftX, bottomLeftY, 0f ), bottomLeftColor, new Vector2( uv.x, uv.y ) ); | |
vh.AddVert( new Vector3( bottomLeftX, bottomLeftY + height, 0f ), topLeftColor, new Vector2( uv.x, uv.w ) ); | |
vh.AddVert( new Vector3( bottomLeftX + width, bottomLeftY + height, 0f ), topRightColor, new Vector2( uv.z, uv.w ) ); | |
vh.AddVert( new Vector3( bottomLeftX + width, bottomLeftY, 0f ), bottomRightColor, new Vector2( uv.z, uv.y ) ); | |
vh.AddTriangle( 0, 1, 2 ); | |
vh.AddTriangle( 2, 3, 0 ); | |
} | |
else | |
{ | |
// Generate multiple small quads | |
float invSmoothness = 1f / gradientSmoothness; | |
for( int i = 0; i <= gradientSmoothness; i++ ) | |
{ | |
for( int j = 0; j <= gradientSmoothness; j++ ) | |
{ | |
float normalizedX = j * invSmoothness; | |
float normalizedY = i * invSmoothness; | |
Vector3 _position = new Vector3( bottomLeftX + width * normalizedX, bottomLeftY + height * normalizedY, 0f ); | |
Vector2 _uv = new Vector2( uv.z * normalizedX + uv.x * ( 1f - normalizedX ), uv.w * normalizedY + uv.y * ( 1f - normalizedY ) ); | |
vh.AddVert( _position, GetColorAt( normalizedX, normalizedY ), _uv ); | |
} | |
} | |
for( int i = 0; i < gradientSmoothness; i++ ) | |
{ | |
int firstTriangle = i * ( gradientSmoothness + 1 ); | |
for( int j = 0; j < gradientSmoothness; j++ ) | |
{ | |
int triangle = firstTriangle + j; | |
int aboveTriangle = triangle + gradientSmoothness + 1; | |
vh.AddTriangle( triangle, aboveTriangle, aboveTriangle + 1 ); | |
vh.AddTriangle( aboveTriangle + 1, triangle + 1, triangle ); | |
} | |
} | |
} | |
} | |
} | |
private Color GetColorAt( float x, float y ) | |
{ | |
Color topLerp = topLeftColor * ( 1f - x ) + topRightColor * x; | |
Color bottomLerp = bottomLeftColor * ( 1f - x ) + bottomRightColor * x; | |
return bottomLerp * ( 1f - y ) + topLerp * y; | |
} | |
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( sprite == null ) | |
return 0; | |
return DataUtility.GetMinSize( sprite ).x / pixelsPerUnit; | |
} | |
} | |
float ILayoutElement.preferredHeight | |
{ | |
get | |
{ | |
if( sprite == null ) | |
return 0; | |
return DataUtility.GetMinSize( sprite ).y / pixelsPerUnit; | |
} | |
} | |
void ILayoutElement.CalculateLayoutInputHorizontal() { } | |
void ILayoutElement.CalculateLayoutInputVertical() { } | |
// 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<GradientGraphic> m_TrackedTexturelessImages = new List<GradientGraphic>(); | |
private static bool s_Initialized; | |
#endif | |
private void TrackImage() | |
{ | |
if( sprite != null && sprite.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-- ) | |
{ | |
GradientGraphic image = m_TrackedTexturelessImages[i]; | |
if( spriteAtlas.CanBindTo( image.sprite ) ) | |
{ | |
image.SetAllDirty(); | |
m_TrackedTexturelessImages.RemoveAt( i ); | |
} | |
} | |
} | |
#endif | |
} |
@kadaxo If Gradient Smoothness has no effect, then the only solution I can think of is creating a new UI/Image shader that applies dithering to the colors. I've no dithering experience so I don't know for certain if this is even possible, unfortunately. You should also make sure that "Use 32-bit Display Buffer" is enabled in Player Settings.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Is there any way to make it smoother? There is banding.