Skip to content

Instantly share code, notes, and snippets.

@yasirkula
Last active November 8, 2024 12:44
Show Gist options
  • Save yasirkula/b90c8278c7d72265b7a7fa4ceab49320 to your computer and use it in GitHub Desktop.
Save yasirkula/b90c8278c7d72265b7a7fa4ceab49320 to your computer and use it in GitHub Desktop.
Create UI image with wave animation in Unity
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
#if UNITY_EDITOR
using UnityEditor;
// Custom Editor to order the variables in the Inspector similar to Image component
[CustomEditor( typeof( WavyImage ) )]
[CanEditMultipleObjects]
public class WavyImageEditor : Editor
{
private SerializedProperty colorProp, spriteProp, preserveAspectProp, waveSpeedIgnoresTimeScaleProp;
private GUIContent spriteLabel;
private List<WavyImage> wavyImages;
private bool inspectingAsset;
internal static bool previewInEditor;
private void OnEnable()
{
Object[] _targets = targets;
wavyImages = new List<WavyImage>( _targets.Length );
for( int i = 0; i < _targets.Length; i++ )
{
WavyImage wavyImage = _targets[i] as WavyImage;
if( wavyImage )
{
wavyImages.Add( wavyImage );
inspectingAsset |= AssetDatabase.Contains( wavyImage );
}
}
colorProp = serializedObject.FindProperty( "m_Color" );
spriteProp = serializedObject.FindProperty( "m_Sprite" );
preserveAspectProp = serializedObject.FindProperty( "m_PreserveAspect" );
waveSpeedIgnoresTimeScaleProp = serializedObject.FindProperty( "m_WaveSpeedIgnoresTimeScale" );
spriteLabel = new GUIContent( "Source Image" );
}
private void OnDisable()
{
if( previewInEditor )
{
previewInEditor = false;
RefreshWavyImages();
}
EditorApplication.update -= PreviewInEditor;
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField( colorProp );
EditorGUILayout.PropertyField( spriteProp, spriteLabel );
if( spriteProp.hasMultipleDifferentValues || spriteProp.objectReferenceValue )
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField( preserveAspectProp );
EditorGUI.indentLevel--;
}
DrawPropertiesExcluding( serializedObject, "m_Script", "m_Color", "m_Sprite", "m_PreserveAspect", "m_WaveSpeedIgnoresTimeScale", "m_OnCullStateChanged" );
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField( waveSpeedIgnoresTimeScaleProp );
EditorGUI.indentLevel--;
serializedObject.ApplyModifiedProperties();
if( !EditorApplication.isPlayingOrWillChangePlaymode && !inspectingAsset )
{
EditorGUILayout.Space();
EditorGUI.BeginChangeCheck();
previewInEditor = GUILayout.Toggle( previewInEditor, "Preview In Editor", GUI.skin.button );
if( EditorGUI.EndChangeCheck() )
{
if( previewInEditor )
{
EditorApplication.update -= PreviewInEditor;
EditorApplication.update += PreviewInEditor;
}
else
{
EditorApplication.update -= PreviewInEditor;
RefreshWavyImages();
EditorApplication.delayCall += RefreshWavyImages; // Preview is sometimes not reset immediately without this call
}
}
}
}
private void PreviewInEditor()
{
if( EditorApplication.isPlayingOrWillChangePlaymode )
{
previewInEditor = false;
EditorApplication.update -= PreviewInEditor;
}
else
RefreshWavyImages();
}
private void RefreshWavyImages()
{
EditorApplication.QueuePlayerLoopUpdate();
}
}
#endif
[RequireComponent( typeof( CanvasRenderer ) )]
[AddComponentMenu( "UI/Wavy Image", 12 )]
public class WavyImage : MaskableGraphic, ILayoutElement
{
[SerializeField]
private Sprite m_Sprite;
public Sprite sprite
{
get { return m_Sprite; }
set
{
if( m_Sprite )
{
if( m_Sprite != value )
{
m_Sprite = value;
SetAllDirty();
TrackImage();
}
}
else if( value )
{
m_Sprite = value;
SetAllDirty();
TrackImage();
}
}
}
[SerializeField]
private bool m_PreserveAspect;
public bool preserveAspect
{
get { return m_PreserveAspect; }
set { if( m_PreserveAspect != value ) { m_PreserveAspect = value; SetVerticesDirty(); } }
}
[Header( "Wave Animation" )]
[Tooltip( "Number of rows and columns in the generated grid. The larger this value is, the smoother the wave animation will be but the calculations will also take more time." )]
[Range( 0, 10 )]
[SerializeField]
private int m_WaveSegments = 3;
public int waveSegments
{
get { return m_WaveSegments; }
set { if( m_WaveSegments != value ) { m_WaveSegments = value; SetVerticesDirty(); } }
}
[Range( -0.5f, 0.5f )]
[SerializeField]
private float m_WaveStrength = 0.15f;
public float waveStrength
{
get { return m_WaveStrength; }
set { if( m_WaveStrength != value ) { m_WaveStrength = value; SetVerticesDirty(); } }
}
[Tooltip( "As this value increases, the wave animation will become more 'chaotic' because vertices will start moving in more random directions." )]
[Range( 0f, 1f )]
[SerializeField]
private float m_WaveDiversity = 0.5f;
private float waveDiversitySqrt; // The visual impact of waveDiversity's square root seems to be more uniform than waveDiversity itself
public float waveDiversity
{
get { return m_WaveDiversity; }
set { if( m_WaveDiversity != value ) { m_WaveDiversity = value; waveDiversitySqrt = Mathf.Sqrt( value ); SetVerticesDirty(); } }
}
[SerializeField]
private float m_WaveSpeed = 0.5f;
public float waveSpeed
{
get { return m_WaveSpeed; }
set { if( m_WaveSpeed != value ) { m_WaveSpeed = value; SetVerticesDirty(); } }
}
[SerializeField]
private bool m_WaveSpeedIgnoresTimeScale = true;
public bool waveSpeedIgnoresTimeScale
{
get { return waveSpeedIgnoresTimeScale; }
set { waveSpeedIgnoresTimeScale = value; }
}
private float waveAnimationTime;
public override Texture mainTexture { get { return m_Sprite ? m_Sprite.texture : s_WhiteTexture; } }
public float pixelsPerUnit
{
get
{
float spritePixelsPerUnit = 100;
if( m_Sprite )
spritePixelsPerUnit = m_Sprite.pixelsPerUnit;
float referencePixelsPerUnit = 100;
if( canvas )
referencePixelsPerUnit = canvas.referencePixelsPerUnit;
return spritePixelsPerUnit / referencePixelsPerUnit;
}
}
public override Material material
{
get
{
if( m_Material != null )
return m_Material;
if( m_Sprite && m_Sprite.associatedAlphaSplitTexture != null )
{
#if UNITY_EDITOR
if( Application.isPlaying )
#endif
return Image.defaultETC1GraphicMaterial;
}
return defaultMaterial;
}
set { base.material = value; }
}
protected override void Awake()
{
base.Awake();
waveDiversitySqrt = Mathf.Sqrt( m_WaveDiversity );
}
protected override void OnEnable()
{
base.OnEnable();
TrackImage();
}
protected override void OnDisable()
{
base.OnDisable();
if( m_Tracked )
UnTrackImage();
}
private void Update()
{
if( m_WaveSegments > 0 && m_WaveSpeed != 0f && m_WaveStrength != 0f )
SetVerticesDirty();
}
protected override void OnPopulateMesh( VertexHelper vh )
{
vh.Clear();
Color32 color32 = color;
Rect rect = GetPixelAdjustedRect();
Vector4 uv;
float bottomLeftX, bottomLeftY, width, height;
if( m_Sprite )
{
uv = DataUtility.GetOuterUV( m_Sprite );
Vector4 padding = DataUtility.GetPadding( m_Sprite );
Vector2 size = m_Sprite.rect.size;
int spriteW = Mathf.RoundToInt( size.x );
int spriteH = Mathf.RoundToInt( size.y );
if( m_PreserveAspect && size.sqrMagnitude > 0f )
{
float spriteRatio = size.x / size.y;
float rectRatio = rect.width / rect.height;
if( spriteRatio > rectRatio )
{
float oldHeight = rect.height;
rect.height = rect.width * ( 1f / spriteRatio );
rect.y += ( oldHeight - rect.height ) * rectTransform.pivot.y;
}
else
{
float oldWidth = rect.width;
rect.width = rect.height * spriteRatio;
rect.x += ( oldWidth - rect.width ) * rectTransform.pivot.x;
}
}
bottomLeftX = rect.x + rect.width * padding.x / spriteW;
bottomLeftY = rect.y + rect.height * padding.y / spriteH;
width = rect.width * ( spriteW - padding.z - padding.x ) / spriteW;
height = rect.height * ( spriteH - padding.w - padding.y ) / spriteH;
}
else
{
uv = Vector4.zero;
bottomLeftX = rect.x;
bottomLeftY = rect.y;
width = rect.width;
height = rect.height;
}
#if UNITY_EDITOR
if( ( !Application.isPlaying && !WavyImageEditor.previewInEditor ) || m_WaveSegments <= 0 )
#else
if( m_WaveSegments <= 0 )
#endif
{
// Generate normal Image
vh.AddVert( new Vector3( bottomLeftX, bottomLeftY, 0f ), color32, new Vector2( uv.x, uv.y ) );
vh.AddVert( new Vector3( bottomLeftX, bottomLeftY + height, 0f ), color32, new Vector2( uv.x, uv.w ) );
vh.AddVert( new Vector3( bottomLeftX + width, bottomLeftY + height, 0f ), color32, new Vector2( uv.z, uv.w ) );
vh.AddVert( new Vector3( bottomLeftX + width, bottomLeftY, 0f ), color32, new Vector2( uv.z, uv.y ) );
vh.AddTriangle( 0, 1, 2 );
vh.AddTriangle( 2, 3, 0 );
}
else
{
// Generate multiple small quads and move their vertices
float invWaveSegments = 1f / m_WaveSegments;
waveAnimationTime += ( m_WaveSpeedIgnoresTimeScale ? Time.unscaledDeltaTime : Time.deltaTime ) * m_WaveSpeed;
float widthOffset = m_WaveStrength * width;
float heightOffset = m_WaveStrength * height;
#if UNITY_EDITOR
waveDiversitySqrt = Mathf.Sqrt( m_WaveDiversity );
#endif
for( int i = 0; i <= m_WaveSegments; i++ )
{
for( int j = 0; j <= m_WaveSegments; j++ )
{
float normalizedX = j * invWaveSegments;
float normalizedY = i * invWaveSegments;
Vector2 _uv = new Vector2( uv.z * normalizedX + uv.x * ( 1f - normalizedX ), uv.w * normalizedY + uv.y * ( 1f - normalizedY ) );
Vector3 _position = new Vector3( bottomLeftX + width * normalizedX, bottomLeftY + height * normalizedY, 0f );
_position.x += ( Mathf.PerlinNoise( normalizedX * waveDiversitySqrt + waveAnimationTime, normalizedY * waveDiversitySqrt ) - 0.5f ) * widthOffset;
_position.y += ( Mathf.PerlinNoise( normalizedX * waveDiversitySqrt, normalizedY * waveDiversitySqrt + waveAnimationTime ) - 0.5f ) * heightOffset;
vh.AddVert( _position, color32, _uv );
}
}
for( int i = 0; i < m_WaveSegments; i++ )
{
int firstTriangle = i * ( m_WaveSegments + 1 );
for( int j = 0; j < m_WaveSegments; j++ )
{
int triangle = firstTriangle + j;
int aboveTriangle = triangle + m_WaveSegments + 1;
vh.AddTriangle( triangle, aboveTriangle, aboveTriangle + 1 );
vh.AddTriangle( aboveTriangle + 1, triangle + 1, triangle );
}
}
}
}
int ILayoutElement.layoutPriority { get { return 0; } }
float ILayoutElement.minWidth { get { return 0f; } }
float ILayoutElement.minHeight { get { return 0f; } }
float ILayoutElement.flexibleWidth { get { return -1; } }
float ILayoutElement.flexibleHeight { get { return -1; } }
float ILayoutElement.preferredWidth { get { return m_Sprite ? ( m_Sprite.rect.size.x / pixelsPerUnit ) : 0f; } }
float ILayoutElement.preferredHeight { get { return m_Sprite ? ( m_Sprite.rect.size.y / pixelsPerUnit ) : 0f; } }
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<WavyImage> m_TrackedTexturelessImages = new List<WavyImage>();
private static bool s_Initialized;
#endif
private void TrackImage()
{
if( m_Sprite != null && m_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-- )
{
WavyImage image = m_TrackedTexturelessImages[i];
if( spriteAtlas.CanBindTo( image.sprite ) )
{
image.SetAllDirty();
m_TrackedTexturelessImages.RemoveAt( i );
}
}
}
#endif
}
@yasirkula
Copy link
Author

yasirkula commented Nov 20, 2021

How To

Simply add this component to an empty object in a canvas. You are strongly recommended to put this object in its own canvas/nested canvas because it will dirty the canvas every frame and cause its entire mesh to be regenerated.

NOTE: Sliced images aren't supported.

WavyImage

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment