Skip to content

Instantly share code, notes, and snippets.

@b3x206
Last active October 21, 2025 17:25
Show Gist options
  • Select an option

  • Save b3x206/ccb501fc54c2d2bd50ad0e1cb376d360 to your computer and use it in GitHub Desktop.

Select an option

Save b3x206/ccb501fc54c2d2bd50ad0e1cb376d360 to your computer and use it in GitHub Desktop.

Revisions

  1. b3x206 revised this gist Oct 21, 2025. 1 changed file with 7 additions and 1 deletion.
    8 changes: 7 additions & 1 deletion MeshProgressBar.cs
    Original file line number Diff line number Diff line change
    @@ -1,8 +1,14 @@
    // You can freely use this script to do anything with it. But attributions and improvements are always welcome :)
    using UnityEngine;
    using System;
    using System.Collections.Generic;

    // This is free and unencumbered software released into the public domain.
    // Anyone is free to copy, modify, publish, use, compile, sell, or
    // distribute this software, either in source code form or as a compiled
    // binary, for any purpose, commercial or non-commercial, and by any
    // means.
    // (see https://unlicense.org)

    /// <summary>
    /// Mesh based progress bar that can be also used as an health bar.
    /// <br>It creates a mesh with 2 submeshes to have a foreground[0] and a background[1]</br>
  2. b3x206 revised this gist Sep 27, 2024. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions MeshProgressBar.cs
    Original file line number Diff line number Diff line change
    @@ -8,6 +8,7 @@
    /// <br>It creates a mesh with 2 submeshes to have a foreground[0] and a background[1]</br>
    /// <br>The UV's of the ProgressBar are mapped between 0-1, per material. This means that it always fits the texture inside.</br>
    /// </summary>
    /// <example>
    /// <![CDATA[
    /// // Setup 'MeshProgressBar' by adding it into an object and then assigning materials to the 0 and 1 index of the 'MeshRenderer'.
    /// using UnityEngine;
  3. b3x206 created this gist Aug 25, 2024.
    420 changes: 420 additions & 0 deletions MeshProgressBar.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,420 @@
    // You can freely use this script to do anything with it. But attributions and improvements are always welcome :)
    using UnityEngine;
    using System;
    using System.Collections.Generic;

    /// <summary>
    /// Mesh based progress bar that can be also used as an health bar.
    /// <br>It creates a mesh with 2 submeshes to have a foreground[0] and a background[1]</br>
    /// <br>The UV's of the ProgressBar are mapped between 0-1, per material. This means that it always fits the texture inside.</br>
    /// </summary>
    /// <![CDATA[
    /// // Setup 'MeshProgressBar' by adding it into an object and then assigning materials to the 0 and 1 index of the 'MeshRenderer'.
    /// using UnityEngine;
    ///
    /// public class SampleScript : MonoBehaviour
    /// {
    /// public int startingHealth = 100;
    /// public MeshProgressBar healthBar;
    /// public int Health { get; private set; }
    ///
    /// private void Start()
    /// {
    /// Health = startingHealth;
    /// }
    ///
    /// private void Update()
    /// {
    /// if (Input.GetKeyDown(KeyCode.Space))
    /// {
    /// Damage(Random.Range(5, 15));
    /// }
    /// }
    ///
    /// public void Damage(int damage)
    /// {
    /// Health -= damage;
    ///
    /// // 'Progress' is clamped between 0-1, but is a floating point number.
    /// // Casting to (float) is required to not do integer division. (which floors the number)
    /// healthBar.Progress = (float)Health / startingHealth;
    /// }
    /// }
    /// ]]>
    /// </example>
    [ExecuteAlways, RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
    public class MeshProgressBar : MonoBehaviour
    {
    // taken from BXFW to make this script standalone
    public enum TransformAxis
    {
    None = 0,

    XAxis = 1 << 0,
    YAxis = 1 << 1,
    ZAxis = 1 << 2,

    XYZAxis = XAxis | YAxis | ZAxis,
    }

    /// <summary>
    /// Generates a quad for the bar meshes.
    /// <br>The given 1, 3, 4 and 6'th vertex of the mesh control the center divider.</br>
    /// <br>The foreground submesh is the 0'th index and the background submesh is in the 1'st index.</br>
    /// </summary>
    protected static Mesh GenerateSplitQuad()
    {
    // Don't combine the center into a single vertex
    // That combination will cause the UV's being split weirdly

    Mesh mesh = new Mesh();

    float width = 1f;
    float height = 1f;

    float halfWidth = width / 2f;
    float halfHeight = height / 2f;

    // 2 - 3, 6 - 7
    // | | | |
    // 0 - 1, 4 - 5
    Vector3[] vertices = new Vector3[8]
    {
    // left
    new Vector3(-halfWidth, -halfHeight, 0f),
    new Vector3(0f, -halfHeight, 0f),
    new Vector3(-halfWidth, height - halfHeight, 0f),
    new Vector3(0f, height - halfHeight, 0f),
    // right
    new Vector3(0f, -halfHeight, 0f),
    new Vector3(width - halfWidth, -halfHeight, 0f),
    new Vector3(0f, height - halfHeight, 0f),
    new Vector3(width - halfWidth, height - halfHeight, 0f),
    };
    mesh.vertices = vertices;

    mesh.subMeshCount = 2;
    mesh.SetTriangles(new int[6]
    {
    // lower left triangle
    0, 2, 1,
    // upper right triangle
    2, 3, 1
    }, 0);
    mesh.SetTriangles(new int[6]
    {
    // lower left
    4, 6, 5,
    // upper right
    6, 7, 5
    }, 1);

    Vector3[] normals = new Vector3[8]
    {
    -Vector3.forward,
    -Vector3.forward,
    -Vector3.forward,
    -Vector3.forward,
    -Vector3.forward,
    -Vector3.forward,
    -Vector3.forward,
    -Vector3.forward
    };
    mesh.normals = normals;

    Vector2[] uv = new Vector2[8]
    {
    // left
    new Vector2(0f, 0f),
    new Vector2(1f, 0f),
    new Vector2(0f, 1f),
    new Vector2(1f, 1f),
    // right
    new Vector2(0f, 0f),
    new Vector2(1f, 0f),
    new Vector2(0f, 1f),
    new Vector2(1f, 1f)
    };
    mesh.uv = uv;
    mesh.name = "PBarMesh";

    return mesh;
    }

    private MeshFilter m_MeshFilter;
    /// <summary>
    /// Mesh filter attached to this progress bar.
    /// </summary>
    public MeshFilter MeshFilter
    {
    get
    {
    if (m_MeshFilter == null)
    {
    m_MeshFilter = GetComponent<MeshFilter>();
    }

    return m_MeshFilter;
    }
    }
    private Renderer m_Renderer;
    /// <summary>
    /// Renderer attached to this progress bar.
    /// </summary>
    public Renderer Renderer
    {
    get
    {
    if (m_Renderer == null)
    {
    if (!TryGetComponent(out m_Renderer))
    {
    return null;
    }
    }

    CheckRendererMaterialsSize(m_Renderer);
    return m_Renderer;
    }
    }
    private void CheckRendererMaterialsSize(Renderer r)
    {
    bool hasAtleastTwoMaterials;
    bool hasAtleastOneMaterial;

    #if UNITY_EDITOR
    // 'sharedMaterials'
    if (!Application.isPlaying)
    {
    hasAtleastTwoMaterials = r.sharedMaterials.Length >= 2;
    hasAtleastOneMaterial = hasAtleastTwoMaterials || r.sharedMaterials.Length >= 1;

    if (r.sharedMaterials.Length != 2)
    {
    Material mat1 = null, mat2 = null;

    if (hasAtleastTwoMaterials)
    {
    mat1 = r.sharedMaterials[0];
    mat2 = r.sharedMaterials[1];
    }
    else if (hasAtleastOneMaterial)
    {
    mat1 = r.sharedMaterials[0];
    }

    // nullify the 'r.sharedMaterials' first
    // to avoid damaging the materials itself
    // This is done because unity's Renderer material management is footgun
    for (int i = 2; i < r.sharedMaterials.Length; i++)
    {
    r.sharedMaterials[i] = null;
    }

    // finally set the shared materials array after no materials were verified to be not damagable irreversibily
    // (it is irreversible if the user wasn't using version control)
    r.sharedMaterials = new Material[2]
    {
    mat1,
    mat2
    };
    }

    return;
    }
    #endif
    hasAtleastTwoMaterials = r.materials.Length >= 2;
    hasAtleastOneMaterial = hasAtleastTwoMaterials || r.materials.Length >= 1;

    // This one can be done as is, the materials will be instanced and be not damaged
    if (r.materials.Length != 2)
    {
    r.materials = new Material[2]
    {
    hasAtleastOneMaterial ? r.materials[0] : null,
    hasAtleastTwoMaterials ? r.materials[1] : null
    };
    }
    }

    /// <summary>
    /// The progress bar mesh.
    /// <br>Depending on the current runtime state, this is the <see cref="MeshFilter.sharedMesh"/>
    /// or the <see cref="MeshFilter.mesh"/> with the quad always assigned.</br>
    /// </summary>
    public Mesh ProgressMesh
    {
    get
    {
    if (MeshFilter.sharedMesh == null)
    {
    MeshFilter.sharedMesh = GenerateSplitQuad();
    }

    #if UNITY_EDITOR
    return Application.isPlaying ? MeshFilter.mesh : MeshFilter.sharedMesh;
    #else
    return MeshFilter.mesh;
    #endif
    }
    }
    private Camera m_BillboardCamera;
    /// <summary>
    /// The camera to billboard towards.
    /// </summary>
    public Camera BillboardCamera
    {
    get
    {
    if (m_BillboardCamera == null)
    {
    m_BillboardCamera = Camera.main;
    }

    return m_BillboardCamera;
    }
    }
    /// <summary>
    /// Foreground material applied to this progress bar.
    /// <br>Depending on which state of the runtime this material is accessed on, it can be the sharedMaterial or the material.</br>
    /// </summary>
    public Material ForegroundMaterial
    {
    get
    {
    #if UNITY_EDITOR
    return Application.isPlaying ? Renderer.materials[0] : Renderer.sharedMaterials[0];
    #else
    return Renderer.materials[0];
    #endif
    }
    }
    /// <summary>
    /// Background material applied to this progress bar.
    /// <br>Depending on which state of the runtime this material is accessed on, it can be the sharedMaterial or the material.</br>
    /// </summary>
    public Material BackgroundMaterial
    {
    get
    {
    #if UNITY_EDITOR
    return Application.isPlaying ? Renderer.materials[1] : Renderer.sharedMaterials[1];
    #else
    return Renderer.materials[1];
    #endif
    }
    }

    private readonly List<Vector3> m_VertsCache = new List<Vector3>(8);
    protected void SetProgressVertices(float progress)
    {
    // The given vertices, 1, 3, 4 and 6 has to be moved.
    ProgressMesh.GetVertices(m_VertsCache);
    float initialLeftX = m_VertsCache[0].x;
    m_VertsCache[1] = new Vector3(initialLeftX + progress, m_VertsCache[1].y, m_VertsCache[1].z);
    m_VertsCache[3] = new Vector3(initialLeftX + progress, m_VertsCache[3].y, m_VertsCache[3].z);
    m_VertsCache[4] = new Vector3(initialLeftX + progress, m_VertsCache[4].y, m_VertsCache[4].z);
    m_VertsCache[6] = new Vector3(initialLeftX + progress, m_VertsCache[6].y, m_VertsCache[6].z);
    ProgressMesh.SetVertices(m_VertsCache);
    }
    /// <summary>
    /// Translates the mesh's vertices / rendering matrix to always face towards camera.
    /// </summary>
    protected void UpdateBillboard()
    {
    Camera targetCamera;
    #if UNITY_EDITOR
    // do nothing in prefab scene, as prefab scenes when changed, it needs to be set dirty in 73 different ways
    if (UnityEditor.SceneManagement.PrefabStageUtility.GetCurrentPrefabStage() != null)
    {
    return;
    }

    if (!Application.isPlaying)
    {
    targetCamera = UnityEditor.SceneView.lastActiveSceneView.camera;
    }
    else
    #endif
    {
    targetCamera = BillboardCamera;
    }

    if (targetCamera == null)
    {
    return;
    }

    // Since the mesh vertex positions are not transformed, we can always rotate it
    // However this will mutate the mostly immutable mesh, which is bad.
    // So update this transform as unity renderer is a black box and using Graphics.DrawMesh usually doesn't work as intended.
    // --
    // Note : we could serialize the 'm_VertsCache' and transform the mesh directly, but this will do for now.
    // Even though it is changing a serialized value, we are going to change a serialized value anyways (the sharedMesh, that is)
    Vector3 targetRotationEuler = Quaternion.LookRotation(transform.position - targetCamera.transform.position, Vector3.forward).eulerAngles;
    if ((billboardAxis & TransformAxis.XAxis) != TransformAxis.XAxis)
    {
    targetRotationEuler.x = 0f;
    }
    if ((billboardAxis & TransformAxis.YAxis) != TransformAxis.YAxis)
    {
    targetRotationEuler.y = 0f;
    }
    if ((billboardAxis & TransformAxis.ZAxis) != TransformAxis.ZAxis)
    {
    targetRotationEuler.z = 0f;
    }
    transform.rotation = Quaternion.Euler(targetRotationEuler);
    }

    [SerializeField, Range(0f, 1f)] private float m_Progress = 0f;
    /// <summary>
    /// The progress elapsed for this progress bar.
    /// <br>This value goes between 0-&gt;1. Setting this value updates the vertices of the mesh to it's progressed state.</br>
    /// </summary>
    public float Progress
    {
    get
    {
    return m_Progress;
    }
    set
    {
    m_Progress = Mathf.Clamp01(value);
    SetProgressVertices(m_Progress);
    }
    }
    /// <summary>
    /// If this is set to any value other than <see cref="TransformAxis.None"/>,
    /// the meshes are transformed to look at the given primary scene camera.
    /// </summary>
    public TransformAxis billboardAxis = TransformAxis.YAxis;

    protected virtual void Awake()
    {
    // Prefab instances delete the mesh because unity serializer
    if (MeshFilter.sharedMesh == null)
    {
    MeshFilter.sharedMesh = GenerateSplitQuad();
    }
    }

    protected virtual void Update()
    {
    #if UNITY_EDITOR
    if (!Application.isPlaying)
    {
    // Doing this on the 'OnValidate' outputs 190237813 warnings to the console
    // because unity wants to do that.
    SetProgressVertices(m_Progress);
    }
    #endif
    // Do billboarding
    UpdateBillboard();
    }

    protected virtual void Reset()
    {
    MeshFilter.sharedMesh = null;
    }
    }