This is a post about turning this
into this:
(click to interact)Click/drag to adjust %
A common approach
When faced with a task of drawing a circular bar, I think most people's first guess would be to draw a strip of triangles, just like this:
(click to interact)Click/drag to adjust % and side count
Or maybe like this if you're feeling sophisticated:(click to interact)Click/drag to adjust % and side count
The more triangles you add, the smoother the circle gets.But it takes a bunch of triangles to get a reasonably smooth circle, even at low resolution!
And what can you do about that? Well,
A faster approach
The fastest way to draw a circle is to not draw a circle.
Not in the sense that you shouldn't be doing this,
but that we could slice up a picture texture of a circle
instead of trying to approximate it with a bunch of triangles.
Like this:
(click to interact)Click/drag to adjust %
The implementation is much like my 2013 tutorial, but with a texture applied:
function draw_texture_radial(_tex, _value, _x1, _y1, _x2, _y2, _color, _alpha) { if (_value <= 0) exit; if (_value >= 1) { draw_primitive_begin_texture(pr_trianglelist, _tex); draw_vertex_texture_color(_x1, _y1, 0, 0, _color, _alpha); repeat (2) { draw_vertex_texture_color(_x2, _y1, 1, 0, _color, _alpha); draw_vertex_texture_color(_x1, _y2, 0, 1, _color, _alpha); } draw_vertex_texture_color(_x2, _y2, 1, 1, _color, _alpha); draw_primitive_end(); exit; } // middle point: var _mx = (_x1 + _x2) / 2; var _my = (_y1 + _y2) / 2; draw_primitive_begin_texture(pr_trianglelist, _tex); draw_vertex_texture_color(_mx, _my, 0.5, 0.5, _color, _alpha); draw_vertex_texture_color(_mx, _y1, 0.5, 0, _color, _alpha); // corners, each of these finishes the last triangle and starts a new one: if (_value >= 1/8) { draw_vertex_texture_color(_x2, _y1, 1, 0, _color, _alpha); // draw_vertex_texture_color(_mx, _my, 0.5, 0.5, _color, _alpha); draw_vertex_texture_color(_x2, _y1, 1, 0, _color, _alpha); } if (_value >= 3/8) { draw_vertex_texture_color(_x2, _y2, 1, 1, _color, _alpha); // draw_vertex_texture_color(_mx, _my, 0.5, 0.5, _color, _alpha); draw_vertex_texture_color(_x2, _y2, 1, 1, _color, _alpha); } if (_value >= 5/8) { draw_vertex_texture_color(_x1, _y2, 0, 1, _color, _alpha); // draw_vertex_texture_color(_mx, _my, 0.5, 0.5, _color, _alpha); draw_vertex_texture_color(_x1, _y2, 0, 1, _color, _alpha); } if (_value >= 7/8) { draw_vertex_texture_color(_x1, _y1, 0, 0, _color, _alpha); // draw_vertex_texture_color(_mx, _my, 0.5, 0.5, _color, _alpha); draw_vertex_texture_color(_x1, _y1, 0, 0, _color, _alpha); } // final vertex (towards value-angle): var _dir = pi * (_value * 2 - 0.5); var _dx = cos(_dir); var _dy = sin(_dir); // normalize: var _dmax = max(abs(_dx), abs(_dy)); if (_dmax < 1) { _dx /= _dmax; _dy /= _dmax; } // _dx = (1 + _dx) / 2; _dy = (1 + _dy) / 2; draw_vertex_texture_color( lerp(_x1, _x2, _dx), lerp(_y1, _y2, _dy), _dx, _dy, _color, _alpha ); draw_primitive_end(); }
One thing that's new here is that instead of using pr_trianglefan
(which breaks the drawing batch and doesn't exist on some platforms)
the function simulates its behaviour.
And here's a convenience function to draw sprites with this:
function draw_sprite_radial(_sprite, _subimg, _value, _x, _y, _xscale, _yscale, _color, _alpha, _uncrop = true) { var _x1, _y1, _x2, _y2; if (_uncrop) { var _ox = sprite_get_xoffset(_sprite); var _oy = sprite_get_yoffset(_sprite); _x1 = _x + _xscale * (sprite_get_bbox_left(_sprite) - _ox); _x2 = _x + _xscale * (sprite_get_bbox_right(_sprite) + 1 - _ox); _y1 = _y + _yscale * (sprite_get_bbox_top(_sprite) - _oy); _y2 = _y + _yscale * (sprite_get_bbox_bottom(_sprite) + 1 - _oy); } else { _x1 = _x - _xscale * sprite_get_xoffset(_sprite); _x2 = _x1 + _xscale * sprite_get_width(_sprite); _y1 = _y - _yscale * sprite_get_yoffset(_sprite); _y2 = _y1 + _yscale * sprite_get_height(_sprite); } draw_texture_radial(sprite_get_texture(_sprite, _subimg), _value, _x1, _y1, _x2, _y2, _color, _alpha); }
The last argument indicates whether the sprite is cropped and should be measured to avoid drawing the polygon bigger than it should be.
There's no quick way to do this for sprites with different-sized frames so you'll want to move those to a texture group with "Automatically Crop" flag disabled or make each frame a separate sprite.
A shader approach
Breaks the batch but probably faster if you need to draw a lot of these at once.
GLSL ES vertex code (for sh_radial
):
attribute vec3 in_Position; attribute vec4 in_Colour; attribute vec2 in_TextureCoord; varying vec2 v_vTexcoord; varying vec4 v_vColour; varying vec2 v_vRelative; varying float v_fill; #define pi 3.141592653589793 void main() { vec4 object_space_pos = vec4( in_Position.x, in_Position.y, 0.0, 1.0); gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos; v_vTexcoord = in_TextureCoord; v_vColour = in_Colour; // corner IDs are clockwise but we want 0..1 on two axes float ry = mod(in_Colour.b * 255.0, 2.0); float rz = mod(in_Colour.r * 255.0, 2.0); float rx = ry + (1.0 - ry * 2.0) * rz; v_vRelative = vec2(rx, ry); v_fill = in_Position.z * (pi * 2.0); }
GLSL ES fragment code:
varying vec2 v_vTexcoord; varying vec4 v_vColour; varying vec2 v_vRelative; varying float v_fill; #define pi 3.141592653589793 void main() { vec2 r = v_vRelative - vec2(0.5, 0.5); float a = pi - atan(r.x, r.y); if (a < v_fill) { gl_FragColor = v_vColour * texture2D(gm_BaseTexture, v_vTexcoord); } else { discard; } }
Helper scripts:
function draw_radial_begin() { shader_set(sh_radial); shader_enable_corner_id(true); return gpu_get_depth(); } function draw_radial_end(_depth) { shader_reset(); shader_enable_corner_id(false); gpu_set_depth(_depth); }
This works as following:
- The vertex shader gets corner IDs from red/blue channels
(as per
shader_enable_corner_id
) and uses these to set upv_vRelative
.
This way the fragment shader gets to know which part of the polygon we're at, regardless of its position, UVs, or cropping. - Now that we can specify
Z
coordinate for 2d primitives usinggpu_set_depth
, we can use this to pass in additional information (in this case, fill %). - The fragment shader finds out the angle between the middle of the polygon and the current point, and discards the pixel if it's beyond the intended range.
You can use it as following:
var _old_depth = draw_radial_begin(); gpu_set_depth(0.71); // <- progress/fill in 0..1 range draw_sprite_ext(spr_progress, 0, x, y, 1, 1, 0, c_white, 1); gpu_set_depth(0.35); draw_sprite_ext(spr_progress, 0, x + 200, y, 1, 1, 0, c_white, 1); draw_radial_end(_old_depth);
Alternative
If you want to use this in 3d or are using LTS
(which doesn't have gpu_set_depth
yet),
you can replace the vertex shader with this one:
attribute vec3 in_Position; attribute vec4 in_Colour; attribute vec2 in_TextureCoord; varying vec2 v_vTexcoord; varying vec4 v_vColour; varying vec2 v_vRelative; varying float v_fill; #define pi 3.141592653589793 void main() { vec4 object_space_pos = vec4( in_Position.x, in_Position.y, in_Position.z, 1.0); gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos; v_vTexcoord = in_TextureCoord; v_vColour = vec4(in_Colour.rgb, 1.0); // corner IDs are clockwise but we want 0..1 on two axes float ry = mod(in_Colour.b * 255.0, 2.0); float rz = mod(in_Colour.r * 255.0, 2.0); float rx = ry + (1.0 - ry * 2.0) * rz; v_vRelative = vec2(rx, ry); v_fill = in_Colour.a * (pi * 2.0); }
It takes the fill % from the alpha multiplier instead, so you can remove depth-related lines from the helper scripts and use this like so:
draw_radial_begin(); draw_sprite_ext(spr_progress, 0, x, y, 1, 1, 0, c_white, 0.71); draw_sprite_ext(spr_progress, 0, x + 200, y, 1, 1, 0, c_white, 0.35); draw_radial_end();
And if you wanted both depth and alpha at once, you would have to do this properly - by defining a vertex with an additional attribute for fill % and building your own quads to pass to the shader.
In other tools
To me this seems like a kind of thing that's fine to do user-side as it isn't performance-critical or likely to be used in most projects, but you can find it in some places:
- Unity: Image.FillMethod
- Godot: TextureProgressBar
- Unreal Engine: people are making
shaders
just like mine.
Although this one seems to have a little more going on..?
Downloads
A test project with scripts and shaders from this post can be found on GitHub.
Thanks for reading!