PART 2: Drawing curved health bars in GameMaker using Shaders.

This is the 3rd or 4th time I’ve found myself writing a guide on a different way to draw a curved health bar, but this is definitely my favourite and most performant way. Also It allows for any shaped progress bar and is the easiest way to visualise how these masks and thresholds work.

Curved Health Bars and Progress Bars in GameMaker

This will also work for pie charts, curved progress bars, download bars, and other complex shaped visual progress indicators.

This will work by having 3 images; one of the filled progress bar, one of the empty progress bar, and a greyscale image that says when a pixel should flip from one image to the other.

Using shaders to threshold images in GameMaker.

We are going to do this all in a GLSL shader and GameMaker so you may need to read the article before to check how to send these images into the shader.

Progress bar switching between different images.

Lets jump straight into the code:

CREATE EVENT:
shader_name = shader_health_bar

in_image_0 = spr_bar_threshold
in_image_1 = spr_bar_1
in_image_2 = spr_bar_2

	

var i = 0;
shader_params[i++] = shader_get_uniform(shader_name, "u_image_0_uvs");
shader_params[i++] = shader_get_uniform(shader_name, "u_image_1_uvs");
shader_params[i++] = shader_get_uniform(shader_name, "u_image_2_uvs");
shader_params[i++] = shader_get_uniform(shader_name, "u_value");

i = 0;
shader_sampler[i] = shader_get_sampler_index(shader_name, "s_imageIn0");
shader_image[i] = sprite_get_texture(in_image_0, 0);
var _uvs0 = sprite_get_uvs(in_image_0, 0);
shader_image_uvs[i] = [_uvs0[0], _uvs0[1], _uvs0[2], _uvs0[3], sprite_get_width(in_image_0), sprite_get_height(in_image_0)];
i++;

shader_sampler[i] = shader_get_sampler_index(shader_name, "s_imageIn1");
shader_image[i] = sprite_get_texture(in_image_1, 0);
var _uvs1 = sprite_get_uvs(in_image_1, 0);
shader_image_uvs[i] = [_uvs1[0], _uvs1[1], _uvs1[2], _uvs1[3], sprite_get_width(in_image_1), sprite_get_height(in_image_1)];
i++;

shader_sampler[i] = shader_get_sampler_index(shader_name, "s_imageIn2");
shader_image[i] = sprite_get_texture(in_image_2, 0);
var _uvs2 = sprite_get_uvs(in_image_2, 0);
shader_image_uvs[i] = [_uvs2[0], _uvs2[1], _uvs2[2], _uvs2[3], sprite_get_width(in_image_2), sprite_get_height(in_image_2)];
i++;
        
DRAW EVENT:

var _value = 0.5    // THIS IS THE AMOUNT AS A DECIMAL 0-1

shader_set(shader_name);
		
		
i = 0;
shader_set_uniform_f_array(shader_params[i], shader_image_uvs[i]); i++
shader_set_uniform_f_array(shader_params[i], shader_image_uvs[i]); i++
shader_set_uniform_f_array(shader_params[i], shader_image_uvs[i]); i++
shader_set_uniform_f(shader_params[i], _value); i++


i = 1;
texture_set_stage(shader_sampler[i], shader_image[i]); i++
texture_set_stage(shader_sampler[i], shader_image[i]); i++

draw_sprite(in_image_0,0,1000,230)


shader_reset();
        
SHADER CODE:
varying vec2 v_vTexcoord;
varying vec4 v_vColour;



uniform float u_image_0_uvs[6]; // Left, Top, Right, Bottom, Width, Height
uniform float u_image_1_uvs[6]; // Left, Top, Right, Bottom, Width, Height
uniform float u_image_2_uvs[6]; // Left, Top, Right, Bottom, Width, Height
uniform float u_value;
	
	
uniform sampler2D s_imageIn1; // Texture sampler for image 1
uniform sampler2D s_imageIn2; // Texture sampler for image 2

float lerp_long(float _input_min, float _input_max, float _output_min, float _output_max, float _amount) {
    return _output_min + (_amount - _input_min) * (_output_max - _output_min) / (_input_max - _input_min);
}

void main() {
	    

    // Calculate the UV coordinates for the first image
    vec2 use_pos1 = vec2(
        lerp_long(u_image_0_uvs[0], u_image_0_uvs[2], u_image_1_uvs[0], u_image_1_uvs[2], v_vTexcoord.x),
        lerp_long(u_image_0_uvs[1], u_image_0_uvs[3], u_image_1_uvs[1], u_image_1_uvs[3], v_vTexcoord.y)
    );
	
    vec2 use_pos2 = vec2(
        lerp_long(u_image_0_uvs[0], u_image_0_uvs[2], u_image_2_uvs[0], u_image_2_uvs[2], v_vTexcoord.x),
        lerp_long(u_image_0_uvs[1], u_image_0_uvs[3], u_image_2_uvs[1], u_image_2_uvs[3], v_vTexcoord.y)
    );



    // Sample the textures
    vec4 og_img = texture2D(gm_BaseTexture, v_vTexcoord);

	vec2 _use_pos;	// Which texture do we want to poll?
	
	if (og_img.r < u_value) 
	{
	    _use_pos = use_pos1;
	}
	else
	{
		_use_pos = use_pos2;
	}
	
	gl_FragColor = v_vColour * texture2D(s_imageIn2, _use_pos);

}
        

In the above code there are a few things I want to point out.

Threshold

Using the greyscale image passed in we can pull the value of each pixel out using texture2D(gm_BaseTexture, v_vTexcoord) and rather than using this to draw to the screen we can use this value as data.

In the above shader code our threshold value is put into og_img.r (the .r is there because we only need to poll one channel so I’m checking the red channel). From here we can check if the passed in value (u_value) is greater than this and use it to decide which of the two images we want to check.

Mapping the images

We’ve previously looked at how to map the images depending on which of the two images we want to draw. This code will put those coordinates into _use_pos and when the graphics card looks up what it should draw it will switch between those two images.

How to work out where an image is on the texture page.
Different shaped bars

Using a different mask allows us to change how the bar gets filled in.

Drawing a diagonal health bar in GameMaker

In the above bar I’ve put the threshold mask at a slight angle so it will fill in with a slight diagonal leaning.

How to draw a diagonal health bar

Also I’ve made the bar above have a gradient just to highlight that with two images being put in we really can draw it however we like.

Curved bars

I want to show that using this technique the mask now doesnt have to be a straight line like the one in the top example. We can start doing fun things without needing to worry about drawing complex angles and shapes in our code.

These are some health bars I’ve used in a GameMaker game in the past to show data about a player along with the mask that was used:

Curved health bar in GameMaker

Threshold mask that looked like this:

Curved health bar mask in GameMaker

We can get curved progress bars and pie charts like this one:

How to draw pie chart in GameMaker

Simply by using a threshold like this:

Mask for drawing a pie chart in GameMaker

Excuse my super retro version of Photoshop but this is how I’ve been making the mask for the curved progress bars:

Mask for health bar in GameMaker
Animated and Textured progress bars:

We can also easily get non-straight progress bars by using a slightly textured mask:

Non-straight progress bar in GameMaker

Which uses this mask:

Mask for health bar in GameMaker

We can also add a texture to our thresholds to get some more complex effects:

Drawing complex health bars in GameMaker. Drawing complex health bars in GameMaker. Drawing complex health bars in GameMaker. Drawing complex health bars in GameMaker.

From the images above you can see that there is a very harsh line where we either select one image or the other. We can blend our images together to give a smooth transition, pixels way above the value will still be fully one image, and pixels way below the value will fully be the second image. However pixels close to the value will be a blend of the two images.

Here is the code for that:

float _smooth_range = 0.005;
float _blend_amount = smoothstep(u_value - _smooth_range, u_value + _smooth_range, og_img.r);

vec4 tex1 = texture2D(s_imageIn1, use_pos1);
vec4 tex2 = texture2D(s_imageIn2, use_pos2);

vec4 finalColor = mix(tex1, tex2, _blend_amount);
        

It is hard to see in following images because they are gifs and it kinda butchers the colours, however there is a nice smooth transition between the colours as it fades from one image to the next.

Health bar with an anti aliased edge. Animated health bar in GameMaker.
Blended health bar in GameMaker.

If we put all these techniques together we can get some pretty cool looking progress bars without any extra overhead more than just drawing one single image. No having to calculate any angles on the CPU because its all done in the shader.

Drawing cool health bars in GameMaker
How to draw cool progress bars in GameMaker

Just to show how this smoothed bar looks at different levels I’ve provided an image to see how it impacts the bar even at extreme values:

How to smooth progress bar

Go forth and shade cool progress bars!