35
\$\begingroup\$

The issue of the traditional rectangular or heart-based health bar is something well-understood and easily solved. But what is the accepted solution for more "creatively-shaped" health bars, like the mockup I've made below?

Empty health Full health, partial health

The obvious way to do this would be to have "in-between" sprites, so the third image would be its own sprite, along with many other transitions for different levels of health. But that seems really inelegant and the number of in-betweens needed to create a smooth transition between health states would be large.

The only other idea I can come up with is to overlay the "full health" sprite on top of the "empty health" sprite and pre-bake sets of pixel coordinates to represent each tick of health and then make them opaque or transparent as health goes up or down.

I intend this question to be language and platform agnostic but feel free to provide a concrete sample in any language/library/framework if it helps the answer.

\$\endgroup\$
  • 5
    \$\begingroup\$ "N̶a̶n̶o̶m̶a̶c̶h̶i̶n̶e̶s̶ Shaders, son" \$\endgroup\$ – Mukesh Ingham Feb 27 '16 at 18:01
  • \$\begingroup\$ I feel like that transition could be defined mathematically, from there you can have the computer calculate the pixel coordinates to show/hide rather than prebaking them \$\endgroup\$ – Richard Tingle Feb 27 '16 at 23:47
  • \$\begingroup\$ How is the heart-shaped health bar "easily solved"? \$\endgroup\$ – MooseBoys Feb 28 '16 at 0:54
  • 1
    \$\begingroup\$ @MooseBoys I was talking about heart-based health, not heart-shaped. Think the Legend of Zelda games. Sorry for not being more clear. \$\endgroup\$ – msd7734 Feb 28 '16 at 2:40
  • \$\begingroup\$ @RichardTingle, good idea :) \$\endgroup\$ – Jon Feb 28 '16 at 7:48
48
\$\begingroup\$

There is a very simple way achieving this with shaders, you need three* textures: empty health bar, health bar texture and mask with gradient of health distribution with one extreme(e.g. darkest not black or most transparent alpha value) on one end and the other on other end. It is best shown on image, I am currently unable do draw curved gradient only linear one - but I am sure your artists wont have any trouble: enter image description here Now, in shader sample both textures and threshold the mask value, discarding all the pixels which are not brighter than input threshold (based on health % shader input).
You could use mathematical formula for masking instead of a texture, but if you did that, you would be still very limited to shapes you are able(=simple ones) to create with said formulas - and finding one is not trivial.
*see comments

\$\endgroup\$
  • 9
    \$\begingroup\$ In a lot of cases (including the OP's example) using a separate texture for color is overkill. In most cases you could just remap the grayscale mask gradient to colors specified as uniforms to the shader, while still discarding any mask values below a certain threshold. \$\endgroup\$ – bcrist Feb 28 '16 at 2:22
  • 1
    \$\begingroup\$ You don't actually need darkest not black, you can use the alpha channel to screen out pixels you don't want. \$\endgroup\$ – Jack Aidley Feb 28 '16 at 11:04
  • 1
    \$\begingroup\$ @JackAidley sure that would be possible, but I wanted the solution as much flexible as possible - in most AAA titles you dont have hp just "red" but it is textured. \$\endgroup\$ – wondra Feb 28 '16 at 11:49
  • 1
    \$\begingroup\$ @bcrist good point, I considered suggesting it as an optimization. But I could not draw it properly so I decided to keep it as simple as possible and leave optimization for implementation. \$\endgroup\$ – wondra Feb 28 '16 at 11:50
  • \$\begingroup\$ @wondra For many cases including the example look in the OP you could use a procedural gradient, or a one-dimensional color lookup (with the mask level as input), rather than a texture. \$\endgroup\$ – Random832 Feb 29 '16 at 16:56
34
\$\begingroup\$

Implemented mathematically, as a pixel shader:

On the CPU, calculate HealthDirection and its' dot product with V2 enter image description here

On the GPU, calculate a normalized direction away from the centerpoint for each pixel. Compare each dot product to HealthDirections' to choose whether to shade "background", or in color. enter image description here

If you "inverted" this algorithm and worked from the other direction, you could "un-slerp" each pixel's direction back into a percentage and compare them each to HealthPercent, instead. With each pixel's percentage calculated, peforming the fade from red to green on the GPU becomes trivial.

Draw the bar first applying the algorithm to a quad; output colors between rangeMin and rangeMax (could be baked) and clip() or output transparent every where else. Next, draw the (modified) sprite over it, using alpha. I traced your bar in CAD, flood-filled it, then "magic wanded" the solid color shape in Paint to delete it. Mine looks like crap due to mixing AutoCAD and Paint, by hand; assuming you blend the inner edges to transparent, yours will look perfect.

The white border is the limits of the quad. It and the red X's are for reference only:

enter image description here

The "sprite with hole" does not need to be rendered at the same time as the health bar. They should get instanced and drawn all at once, after all the health bars are complete. Since the algorithm is self-contained in the shader, you could also add instancing to it and then, also, draw all of the healthbars with one instanced draw call. Drawing every healthbar on the screen should take 2 DrawInstanced(...) calls and be "crazy fast".

\$\endgroup\$
  • 1
    \$\begingroup\$ If you're going to mask off in a circular fashion like this, you can implement it more efficiently by simply altering the triangles you plot for a circular fade. \$\endgroup\$ – Jack Aidley Feb 28 '16 at 11:03
  • \$\begingroup\$ @JackAidley, please be more specific, because I don't consider this masking; the progress bar is fully rendered, in place, on only two triangles, with no sampling, after the first pass. The sprite just happens to have a hole in it and "pretties" the whole thing up at the end. (It is a mask of sorts, but not as an integral part of the algorithm?) Thanks \$\endgroup\$ – Jon Feb 28 '16 at 19:12
  • \$\begingroup\$ What you're doing is equivalent to drawing a masked version coloured of the coloured bar over a background sprite that already has the grey colour in place. Because your method requires a custom shader and writes to every pixel you end up with a slower method than drawing only the desired part of the shape. \$\endgroup\$ – Jack Aidley Feb 28 '16 at 20:29
  • \$\begingroup\$ @JackAidley, but there's no background sprite? The rainbow is drawn using only four float3 world positions; no UV's, no "background sprite". Also, you are concerned that the pixel shader is going to get bogged down working on some tiny quads? Also, RangeMin and RangeMax clip() most of those fragments, instantly. \$\endgroup\$ – Jon Feb 28 '16 at 20:49
  • \$\begingroup\$ You will require a background sprite in order to get the borders as shown in the original image; if you are omitting these then, yes, the methods are different. \$\endgroup\$ – Jack Aidley Feb 28 '16 at 21:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.