Health Bar

Customizable health bar shader, supporting damage trail effect (both health loss and health gain with different colors).

Optional pulse effect at low health.

Compatible with plain ColorRect or any Control node.

 

Here is a sample script in GDScript that controls the shader.  To test it in the editor, create a ColorRect node, set the material to a ShaderMaterial with my shader and attach this script to it:

# By Amy Gilhespy.
# I hereby dedicate this script to the public domain.
# https://creativecommons.org/publicdomain/zero/1.0/

@tool

extends Control

@export var test_in_editor: bool:
	get:
		return _tool
	set(value):
		if value:
			_mat = material.duplicate()
			material = _mat
		_tool = value

@export var delay = 1.0
@export var trail_speed = 50.0
@export var max_hp: float = 100.0
@export var hp: float = 100.0:
	get:
		return _hp
	set(value):
		var last_was_heal = _last_was_heal
		_last_was_heal = value > _hp
		if last_was_heal != _last_was_heal:
			var health = _mat.get_shader_parameter("health")
			_mat.set_shader_parameter("trail", health)
		_hp = value
		_set_health_fract(value / max_hp)

var _delay_expiration: int
var _hp: float = 100.0
var _mat: Material
var _tool: bool
var _last_was_heal: bool

func _set_health_fract(fract: float):
	var now = Time.get_ticks_msec()
	_delay_expiration = now + delay * 1000.0
	_mat.set_shader_parameter("health", fract)

func _init():
	if !Engine.is_editor_hint() || _tool:
		_mat = material.duplicate()
		material = _mat

func _process(delta: float):
	if Engine.is_editor_hint() && !_tool:
		return
	var size = get_rect().size
	_mat.set_shader_parameter("width", size.x)
	_mat.set_shader_parameter("height", size.y)
	var now = Time.get_ticks_msec()
	if now < _delay_expiration:
		return
	var health = _mat.get_shader_parameter("health")
	var trail = _mat.get_shader_parameter("trail")
	var dhealth = health - trail
	if abs(dhealth) * max_hp <= 0.0001:
		return
	if dhealth >= 0.0:
		trail += delta * trail_speed / max_hp
		if trail >= health:
			trail = health
	else:
		trail -= delta * trail_speed / max_hp
		if trail <= health:
			trail = health
	_mat.set_shader_parameter("trail", trail)
Shader code
// By Amy Gilhespy.
// I hereby dedicate this shader to the public domain.
// https://creativecommons.org/publicdomain/zero/1.0/

shader_type canvas_item;

uniform float health = 0.5;
uniform float trail = 0.6;

uniform float width = 800.0;
uniform float height = 20.0;

uniform vec4 health_color: source_color = vec4(-0.0313, 0.58229, 0.08121, 1.0);
uniform vec4 full_health_color: source_color = vec4(-0.03854, 0.50523, 0.20425, 1.0);
uniform vec4 low_health_color: source_color = vec4(0.87165, 0.43797, -0.04402, 1.0);
uniform float low_health_threshold = 0.25;
uniform float low_health_pulse_speed = 2.0;
uniform vec4 damaged_health_color: source_color = vec4(1.02227, 0.36054, 0.36266, 1.0);
uniform vec4 healed_health_color: source_color = vec4(0.87182, 0.98235, 0.90501, 1.0);
uniform vec4 edge_color: source_color = vec4(vec3(4.0), 1.0);
uniform float edge_pixels = 4.0;
uniform vec4 background_color: source_color = vec4(vec3(0.125), 1.0);
uniform vec4 border_color: source_color = vec4(vec3(0.125), 0.5);
uniform float border_pixels = 1.0;

uniform bool round_corners = false;
uniform float skew_pixels = 10.0;

uniform vec4 top_quarter_modulate: source_color = vec4(vec3(2.0), 1.0);
uniform vec4 top_half_modulate: source_color = vec4(vec3(1.0), 1.0);
uniform vec4 bottom_half_modulate: source_color = vec4(vec3(1.0), 1.0);
uniform vec4 bottom_quarter_modulate: source_color = vec4(vec3(0.375), 1.0);

vec4 get_health_color() {
	if (health >= 1.0) {
		return full_health_color;
	} else if (health < low_health_threshold) {
		return mix(health_color, low_health_color, sin(TIME * low_health_pulse_speed) * 0.5 + 0.5);
	}
	return health_color;
}

vec4 get_border_color(vec2 uv, float hx) {
	if (uv.x < -hx || uv.x > 1.0 + hx) {
		return vec4(vec3(1.0), 0.0);
	}
	return border_color;
}

void fragment() {
	float aspect = width / height;
	float horizontal_border_u = border_pixels / width;
	float vertical_border_v = border_pixels / height;
	float edge_width = edge_pixels / width;
	vec2 uv0 = UV;
	//vec2 px0 = vec2((uv0.x - 0.5) * width, (uv0.y - 0.5) * height);
	float non_border_width = width - 2.0 * border_pixels - abs(skew_pixels);
	float non_border_height = height - 2.0 * border_pixels;
	float skew = skew_pixels >= 0.0 ? 1.0 - uv0.y : uv0.y;
	vec2 uv = vec2(
		(uv0.x - horizontal_border_u - skew * abs(skew_pixels) / non_border_width) / (1.0 - 2.0 * horizontal_border_u - abs(skew_pixels) / non_border_width),
		(uv0.y - vertical_border_v) / (1.0 - 2.0 * vertical_border_v));
	vec4 color = vec4(vec3(1.0), 0.0);
	bool is_background = false;
	bool is_edge = false;
	if (trail <= health) {
		if (uv.x <= trail) {
			color = get_health_color();
			if (uv.x > health - edge_width) {
				is_edge = true;
			}
		} else if (uv.x <= health) {
			color = healed_health_color;
			if (uv.x > health - edge_width) {
				is_edge = true;
			}
		} else {
			is_background = true;
			color = background_color;
		}
	} else {
		if (uv.x <= health) {
			color = get_health_color();
			if (uv.x > health - edge_width) {
				is_edge = true;
			}
		} else if (uv.x <= trail) {
			color = damaged_health_color;
		} else {
			is_background = true;
			color = background_color;
		}
	}
	if (!is_background) {
		if (uv.y >= 0.5) {
			color *= bottom_half_modulate;
			if (uv.y >= 0.75) {
				color *= bottom_quarter_modulate;
			}
		} else {
			color *= top_half_modulate;
			if (uv.y < 0.25) {
				color *= top_quarter_modulate;
			}
		}
	}
	if (is_edge) {
		color = edge_color;
		if (round_corners) {
			vec2 px = vec2((uv.x - 0.5) * non_border_width, (uv.y - 0.5) * non_border_height);
			float r = 0.5 * non_border_height;
			float y = px.y;
			float x = sqrt(max(0.0, r * r - y * y));
			if (px.x + non_border_width * 0.5 < r - x) {
				color = get_border_color(uv, horizontal_border_u);
			} else if (px.x > non_border_width * 0.5 - (r - x)) {
				color = get_border_color(uv, horizontal_border_u);
			}
		}
	} else if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
		color = get_border_color(uv, horizontal_border_u);
	} else if (round_corners) {
		vec2 px = vec2((uv.x - 0.5) * non_border_width, (uv.y - 0.5) * non_border_height);
		float r = 0.5 * non_border_height;
		float y = px.y;
		float x = sqrt(max(0.0, r * r - y * y));
		if (px.x + non_border_width * 0.5 < r - x) {
			color = get_border_color(uv, horizontal_border_u);
		} else if (px.x > non_border_width * 0.5 - (r - x)) {
			color = get_border_color(uv, horizontal_border_u);
		}
	}
	COLOR *= color;
}
Live Preview
Tags
bar, health, Health Bar, HUD, ui
The shader code and all code snippets in this post are under CC0 license and can be used freely without the author's permission. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from amy.gilhespy

Related shaders

guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments