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.

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.

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.

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.
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.
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.

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

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

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.
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:

Threshold mask that looked like this:

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

Simply by using a threshold like this:

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

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

Which uses this mask:

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




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.



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.


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:

Go forth and shade cool progress bars!