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;
}
