Skip to content

Instantly share code, notes, and snippets.

@Okay-Roman
Last active June 23, 2026 13:03
Show Gist options
  • Select an option

  • Save Okay-Roman/8ba84316968cd3aac72f3984ad5a6251 to your computer and use it in GitHub Desktop.

Select an option

Save Okay-Roman/8ba84316968cd3aac72f3984ad5a6251 to your computer and use it in GitHub Desktop.
A radial fill element that can be used in Unity UI Toolkit, makes use of MeshWriteData to create a mesh which will only be visible within the confines of its parent due to the overflow attribute.
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.UIElements;
namespace NS.RomanLib
{
public class RadialFillElement: VisualElement, INotifyValueChanged<float>
{
protected float m_value = float.NaN;
public void SetValueWithoutNotify(float newValue)
{
m_value = newValue;
radialFill.MarkDirtyRepaint();
}
public float value
{
get
{
m_value = Mathf.Clamp(m_value, 0, 1);
return m_value;
}
set
{
if (EqualityComparer<float>.Default.Equals(this.m_value, value))
return;
if (this.panel != null)
{
using (ChangeEvent<float> pooled = ChangeEvent<float>.GetPooled(this.m_value, value))
{
pooled.target = (IEventHandler) this;
this.SetValueWithoutNotify(value);
this.SendEvent((EventBase)pooled);
}
}
else
{
this.SetValueWithoutNotify(value);
}
}
}
public float width { get; set; }
public float height { get; set; }
public Color fillColor { get; set; }
public float angleOffset { get; set; }
public string overlayImagePath { get; set; }
public enum FillDirection
{
Clockwise,
AntiClockwise
}
public FillDirection fillDirection { get; set; }
private float m_overlayImageScale;
public float overlayImageScale
{
get
{
m_overlayImageScale = Mathf.Clamp(m_overlayImageScale, 0, 1);
return m_overlayImageScale;
}
set => m_overlayImageScale = value;
}
private float radius => (width > height) ? width / 2 : height / 2;
public VisualElement radialFill;
public VisualElement overlayImage;
public new class UxmlFactory : UxmlFactory<RadialFillElement, UxmlTraits> { }
public new class UxmlTraits : VisualElement.UxmlTraits
{
UxmlFloatAttributeDescription m_value = new UxmlFloatAttributeDescription() { name = "value", defaultValue = 1f};
UxmlFloatAttributeDescription m_width = new UxmlFloatAttributeDescription() { name = "width", defaultValue = 20f};
UxmlFloatAttributeDescription m_height = new UxmlFloatAttributeDescription() { name = "height", defaultValue = 20f};
UxmlFloatAttributeDescription m_angleOffset = new UxmlFloatAttributeDescription() { name = "angle-offset", defaultValue = 0};
UxmlColorAttributeDescription m_fillColor = new UxmlColorAttributeDescription() { name = "fill-color", defaultValue = Color.white};
UxmlStringAttributeDescription m_overlayImagePath = new UxmlStringAttributeDescription() { name = "overlay-image-path", defaultValue = ""};
UxmlFloatAttributeDescription m_overlayImageScale = new UxmlFloatAttributeDescription() { name = "overlay-image-scale", defaultValue = 1f};
UxmlEnumAttributeDescription<FillDirection> m_fillDirection = new UxmlEnumAttributeDescription<FillDirection>() { name = "fill-direction", defaultValue = 0 };
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
{
get { yield break; }
}
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
{
base.Init(ve, bag, cc);
var ate = ve as RadialFillElement;
// Assigning uxml attributes to c# properties
ate.value = m_value.GetValueFromBag(bag, cc);
ate.width = m_width.GetValueFromBag(bag, cc);
ate.height = m_height.GetValueFromBag(bag, cc);
ate.fillColor = m_fillColor.GetValueFromBag(bag, cc);
ate.overlayImagePath = m_overlayImagePath.GetValueFromBag(bag, cc);
ate.overlayImageScale = m_overlayImageScale.GetValueFromBag(bag, cc);
ate.angleOffset = m_angleOffset.GetValueFromBag(bag, cc);
ate.fillDirection = m_fillDirection.GetValueFromBag(bag, cc);
// Creating the hierarchy for the radial fill element
ate.name = "radial-fill-element";
ate.Clear();
VisualElement radialBoundary = new VisualElement() { name = "radial-boundary" };
radialBoundary.Add(ate.radialFill);
ate.radialFill.Add(ate.overlayImage);
ate.radialFill.style.flexGrow = 1;
ate.overlayImage.style.flexGrow = 1;
ate.overlayImage.style.scale = new Scale(new Vector2(ate.overlayImageScale, ate.overlayImageScale));
ate.style.height = ate.height;
ate.style.width = ate.width;
radialBoundary.style.height = ate.height;
radialBoundary.style.width = ate.width;
radialBoundary.style.overflow = Overflow.Hidden;
ate.overlayImage.style.backgroundImage = null;
#if UNITY_EDITOR
Texture2D tex = UnityEditor.AssetDatabase.LoadAssetAtPath<Texture2D>(ate.overlayImagePath);
if (tex != null)
{
ate.overlayImage.style.backgroundImage = tex;
}
#endif
// Angle Offset determines the rotation of the radialFill VE, overlayImage will use the inverse of this
// rotation so the image remains upright
ate.radialFill.transform.rotation = Quaternion.Euler(0, 0, ate.angleOffset);
ate.overlayImage.transform.rotation = Quaternion.Euler(0, 0, -ate.angleOffset);
ate.Add(radialBoundary);
}
}
public RadialFillElement() : base()
{
radialFill = new VisualElement() { name = "radial-fill" };
overlayImage = new VisualElement() { name = "overlay-image" };
radialFill.generateVisualContent += OnGenerateVisualContent;
}
public void AngleUpdate(ChangeEvent<float> evt)
{
radialFill?.MarkDirtyRepaint();
}
public void OnGenerateVisualContent(MeshGenerationContext mgc)
{
// default draw 1 triangle
int triCount = 3;
int indiceCount = 3;
m_value = Mathf.Clamp(m_value, 0, 360);
if (m_value * 360 < 240)
{
// Draw only 2 triangles
if (value * 360 > 120)
{
triCount = 4;
indiceCount = 6;
}
}
// Draw 3 triangles
else
{
triCount = 4;
indiceCount = 9;
if (m_value < 1)
{
triCount = 5;
indiceCount = 9;
}
}
// Create our MeshWriteData object, allocate the least amount of vertices and triangle indices required
MeshWriteData mwd = mgc.Allocate(triCount, indiceCount);
Vector3 origin = new Vector3((float)width/2, (float)height/2, 0);
float diameter = 4 * radius;
float degrees = ((m_value * 360) - 90)/Mathf.Rad2Deg;
//First two vertex are mandatory for 1 triangle
mwd.SetNextVertex(new Vertex(){position = origin + new Vector3(0 * diameter, 0 * diameter, Vertex.nearZ), tint = fillColor});
mwd.SetNextVertex(new Vertex(){position = origin + new Vector3(0 * diameter, -1 * diameter, Vertex.nearZ), tint = fillColor});
float direction = 1;
if (fillDirection == FillDirection.AntiClockwise)
{
direction = -1;
}
mwd.SetNextIndex(0);
mwd.SetNextIndex((fillDirection == FillDirection.AntiClockwise) ? (ushort)2 : (ushort)1);
if (m_value * 360 <= 120)
{
mwd.SetNextVertex(new Vertex(){position = origin + new Vector3(Mathf.Cos(degrees) * diameter * direction, Mathf.Sin(degrees) * diameter, Vertex.nearZ), tint = fillColor});
mwd.SetNextIndex((fillDirection == FillDirection.AntiClockwise) ? (ushort)1 : (ushort)2);
}
if (m_value * 360 > 120 && m_value * 360 <= 240)
{
mwd.SetNextVertex(new Vertex(){position = origin + new Vector3(Mathf.Cos(30/Mathf.Rad2Deg) * diameter * direction, Mathf.Sin(30/Mathf.Rad2Deg) * diameter, Vertex.nearZ), tint = fillColor});
mwd.SetNextIndex((fillDirection == FillDirection.AntiClockwise) ? (ushort)1 : (ushort)2);
mwd.SetNextVertex(new Vertex(){position = origin + new Vector3(Mathf.Cos(degrees) * diameter * direction, Mathf.Sin(degrees) * diameter, Vertex.nearZ), tint = fillColor});
mwd.SetNextIndex(0);
mwd.SetNextIndex((fillDirection == FillDirection.AntiClockwise) ? (ushort)3 : (ushort)2);
mwd.SetNextIndex((fillDirection == FillDirection.AntiClockwise) ? (ushort)2 : (ushort)3);
}
if (m_value * 360 > 240)
{
mwd.SetNextVertex(new Vertex(){position = origin + new Vector3(Mathf.Cos(30/Mathf.Rad2Deg) * diameter * direction, Mathf.Sin(30/Mathf.Rad2Deg) * diameter, Vertex.nearZ), tint = fillColor});
mwd.SetNextIndex((fillDirection == FillDirection.AntiClockwise) ? (ushort)1 : (ushort)2);
mwd.SetNextVertex(new Vertex(){position = origin + new Vector3(Mathf.Cos(150/Mathf.Rad2Deg) * diameter * direction, Mathf.Sin(150/Mathf.Rad2Deg) * diameter, Vertex.nearZ), tint = fillColor});
mwd.SetNextIndex(0);
mwd.SetNextIndex((fillDirection == FillDirection.AntiClockwise) ? (ushort)3 : (ushort)2);
mwd.SetNextIndex((fillDirection == FillDirection.AntiClockwise) ? (ushort)2 : (ushort)3);
if (m_value * 360 >= 360)
{
mwd.SetNextIndex(0);
mwd.SetNextIndex((fillDirection == FillDirection.AntiClockwise) ? (ushort)1 : (ushort)3);
mwd.SetNextIndex((fillDirection == FillDirection.AntiClockwise) ? (ushort)3 : (ushort)1);
}
else
{
mwd.SetNextVertex(new Vertex() { position = origin + new Vector3(Mathf.Cos(degrees) * diameter * direction, Mathf.Sin(degrees) * diameter, Vertex.nearZ), tint = fillColor });
mwd.SetNextIndex(0);
mwd.SetNextIndex((fillDirection == FillDirection.AntiClockwise) ? (ushort)4 : (ushort)3);
mwd.SetNextIndex((fillDirection == FillDirection.AntiClockwise) ? (ushort)3 : (ushort)4);
}
}
}
}
}
@drkhodakarami

Copy link
Copy Markdown

This is old and full of depricated method calls, would be nice if you update this solution. Thanks

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