Progress Bar family — one shared animation core (ProgressBarData + ProgressBarController), several shapes on top.
This page shows a shader-driven build; siblings: Overview · Linear · Radial · Architecture.
This example uses the Progress Bar component with a custom Shader Graph material for enhanced visuals. For creating fully custom progress bar types, see Architecture.
This demo showcases an RPG-style health bar with custom shader effects for enhanced visual appeal.
The RPG Demo demonstrates how to combine Luna's Progress Bar system with custom shaders to create visually stunning health bars commonly seen in RPG games. This approach allows for effects that go beyond what's possible with USS styling alone.
Create a custom UI shader using Shader Graph for the progress bar fill.
Create new Shader Graph: Right-click in Project → Create → Shader Graph → URP → Sprite Unlit (or Canvas for UI)
Add properties to the Blackboard:
Fill (Float, Range 0-1) — Controls fill amountMainColor (Color) — Base fill colorGlowColor (Color) — Edge glow colorGlowIntensity (Float) — Glow strengthBuild the graph:
Example node setup:
UV (X) ──→ Step ──→ Multiply ──→ Base Color
↑
Fill
UV (X) ──→ Subtract (Fill) ──→ Abs ──→ One Minus ──→ Saturate ──→ Multiply (GlowColor) ──→ Add to BaseThe shipped RPGDemo sample (Samples~/Showcase/Components/RPGDemo) does not paint the bar's fill with the material — UI Toolkit USS cannot reference a Material via background-image. Instead, the material lives on a separate orb visual, and the ProgressBar drives the material's shader properties through a custom node updater (see Step 3). The bar itself stays purely a value/animation driver.
If you need direct access to the bar's fill elements (for example, to clear their default background so only the shader visual shows), query them through the component API:
// Strongly-typed access to the fill elements
var fill = progressBar.ProgressElements[0];
// Or query by the actual USS class
var fillByClass = progressBar.Q(className: "ProgressBar__progress-element");The ProgressBar pushes every animated value through an IProgressBarNodeUpdater. Implement one that writes the values into the material's shader properties, then wire it with SetNodeUpdater(). This mirrors the shipped sample's HealthOrbProgressBarNodeUpdater (Samples~/Showcase/Components/RPGDemo/HealthOrb/HealthOrbProgressBarNodeUpdater.cs).
using UnityEngine;
using UnityEngine.UIElements;
using CupkekGames.Luna;
// Routes animated progress/indicator values into shader properties.
// Adjust the property references to match your Shader Graph
// (e.g., "_Fill" if you named the property Fill in Step 1).
public class HealthOrbProgressBarNodeUpdater : IProgressBarNodeUpdater
{
private readonly Material _material;
private readonly string _progressProperty;
private readonly string _indicatorProperty;
public HealthOrbProgressBarNodeUpdater(Material material,
string progressProperty = "_Progress",
string indicatorProperty = "_Progress_Indicator")
{
_material = material;
_progressProperty = progressProperty;
_indicatorProperty = indicatorProperty;
}
public void SetValue(ProgressBarNodeType nodeType, int index, VisualElement element, float value)
{
UpdateValue(nodeType, index, element, value);
}
public void UpdateValue(ProgressBarNodeType nodeType, int index, VisualElement element, float value)
{
if (_material == null) return;
if (nodeType == ProgressBarNodeType.Indicator)
{
_material.SetFloat(_indicatorProperty, value);
}
else
{
_material.SetFloat(_progressProperty, value);
}
}
public void UpdatePosition(ProgressBarNodeType nodeType, int index, VisualElement element, float position) { }
public void SetColor(ProgressBarNodeType nodeType, int index, VisualElement element, Color color) { }
public void OnAnimationStart(ProgressBarNodeType nodeType, int index, VisualElement element, bool positive) { }
public void OnAnimationComplete(ProgressBarNodeType nodeType, int index, VisualElement element, bool positive) { }
}using UnityEngine;
using UnityEngine.UIElements;
using CupkekGames.Luna;
public class RPGHealthBarController : MonoBehaviour
{
[SerializeField] private Material _healthBarMaterial;
private PanelRenderer _panelRenderer;
private ProgressBar _healthBar;
private void Awake()
{
// PanelRenderer delivers the visual tree asynchronously —
// register a reload callback and query elements when it fires.
_panelRenderer = GetComponent<PanelRenderer>();
if (_panelRenderer != null)
{
_panelRenderer.RegisterUIReloadCallback(OnUIReload);
}
}
private void OnUIReload(PanelRenderer renderer, VisualElement root, int version)
{
if (_healthBar != null) return; // sentinel: only init once
_healthBar = root.Q<ProgressBar>("health-bar");
// Route every animated value into the material's shader properties
_healthBar.SetNodeUpdater(new HealthOrbProgressBarNodeUpdater(_healthBarMaterial));
}
private void OnDestroy()
{
if (_panelRenderer != null)
{
_panelRenderer.UnregisterUIReloadCallback(OnUIReload);
}
}
public void TakeDamage(float amount)
{
_healthBar.Progress[0].TargetValue -= amount;
_healthBar.Indicator.TargetValue = _healthBar.Progress[0].TargetValue;
_healthBar.PlayProgress();
_healthBar.PlayIndicator();
}
}MaterialPropertyBlock for per-instance shader properties when you have multiple health bars_Time in the shaderSettings
Theme
Light
Contrast
Material
Dark
Dim
Material Dark
System
Sidebar(Light & Contrast only)
Font Family
DM Sans
Wix
Inclusive Sans
AR One Sans
Direction