We have good news for you!
Recently in Blob, to The Top! we added a subtle circular indicator that shows max power while charging a shoot.
We mentioned it in our latest update post (check it out here; yes it has gifs :D)
This indicator was made using shaders and here is a quick step by step guide how we made it.
To start.. create a new Godot Sprite2D, put there a default icon.svg texture and create a new shader material with a code (not a shader graph!). And now it's time for code!
1. First we need a function to draw a simple circle:
c
float circle(float radius, vec2 uv) {
float len = length(uv);
return len < radius ? 1.0 : 0.0;
}
We can replace ternary operator with built-in step function to simplify it, like that:
c
float circle(float radius, vec2 uv) {
float len = length(uv);
return step(len, radius);
}
2. Now let's make a ring function to draw a ring.
The plan is simple.. we will draw 2 circles and remove inner circle from outer circle. To simplify some math later, instead of just subtraction operation we will use multiplication instead.
c
float ring(float radius, float width, vec2 uv) {
float outer_circle = circle(radius, uv);
float inner_circle = 1.0 - circle(radius - width, uv);
return outer_circle * inner_circle;
}
We can optimize it a little bit by using code from the circle function directly. We can also invert result of step function just by swapping arguments inside.
c
float ring(float radius, float width, vec2 uv) {
float len = length(uv);
float outer_circle = step(len, radius);
float inner_circle = step(radius - width, len);
return outer_circle * inner_circle;
}
Time to test this ring function!

Well.. it works! But you can see it's not very practical. We have to use normalized values and ring isn't even centered.
Let's fix that by modifying UV in the vertex function.
Also for the future use, we will copy value of COLOR variable from the vertex function. It's a Godot specific feature which allows for using value of modulate property (the one from the node) directly inside a shader code within the fragment function. To "transport" value from the vertex function to fragment function we will define a special varying variable.
So in the end our vertex function will look like this:
```c
varying vec4 modulate;
void vertex() {
// Copy value of modulate property to new variable
modulate = COLOR;
// Read a node scale from built-in matrix
vec2 node_scale = vec2(MODEL_MATRIX[0][0], MODEL_MATRIX[1][1]);
// Read texture size used in the Sprite2D node (in this case we used default 128x128 Godot icon)
vec2 tex_size = 1.0 / TEXTURE_PIXEL_SIZE; // = 128x128
// Let's change UV from range 0..1 to -64..64.
UV = (UV * 2.0 - 1.0) * tex_size * node_scale * 0.5; //0..1 -> -1..1 -> -64..64
}
```
And now we can finally use pixels as units! And also shader reacts to modulate property and circle is finally centered.

3. Now it's time to make dashed ring instead of solid one.
First, to make a rotating dashed ring we will need few utility functions:
- A.
get_angle to get angle of the vector in range -PI to PI.
- B.
rotate which will return 2d transformation matrix to rotate any vector by specific angle.
3A. Let's start with get_angle function.
The proper formula would be like this:
c
float get_angle(vec2 vec) {
vec2 base = vec2(1.0, 0.0); // Base vector from which we will calculate angle to `vec`
float a = dot(vec, base);
float b = determinant(mat2(vec, base));
return atan(a, b);
}
This might look like a dark magic but let's simplify some things. We can replace dot and determinant built-in functions with just plane formulas:
c
float get_angle(vec2 vec) {
vec2 base = vec2(1.0, 0.0);
float a = vec.x * base.x + vec.y * base.y;
float b = vec.x * base.y - vec.y * base.x;
return atan(a, b);
}
Isn't it more clear now? But that's not the end. We can replace base.x and base.y with numbers and in the end we will get this simple function:
c
float get_angle(vec2 vec) {
return atan(vec.x, -vec.y);
}
Now when it's finally done..
3B. Let's write a rotate function!
This will return a basic rotation matrix. The theory works like this: if we will multiply any vector by such matrix it will be rotated by a specific angle.. simple enough, right? :D
The final rotate function will look like this:
c
mat2 rotate(float angle) {
float s = sin(angle);
float c = cos(angle);
return mat2(vec2(c,-s),vec2(s,c));
}
Now it's the hardest part.
4. Time to make holes in the ring to create a dashed ring.
The idea is simple:
1. Split space into N*2 squared pizza fragments.
2. Make only each odd fragment visible.
3. Let's combine our ring with pizza.
```c
float pizza(float radius, int n, vec2 uv) {
float deltaangle = TAU / (float(n) * 2.0);
float angle = getangle(uv);
float i = floor(angle / delta_angle);
return float(int(i) % 2);
}
float dashedring(float radius, float width, int n, vec2 uv) {
float pizzashape = pizza(radius, n, uv);
float ringshape = ring(radius, width, uv);
return ringshape * pizzashape;
}
And now to add rotation we can just rotate `UV` by a specific angle using `rotate` function we already prepared.
c
float dashedring(float radius, float width, int n, float rotation, vec2 uv) {
uv *= rotate(rotation);
float pizzashape = pizza(radius, n, uv);
float ringshape = ring(radius, width, uv);
return ringshape * pizzashape;
}
```
Let's test this thing!

We are almost there. The only thing left is to add fading.
We did it by combining two fades like so:

Aaand that's it!
I hope someone will find this helpful :D
Now you can check out the final result in our game here!
If you are interested more in our game, we have good news for you.
After such an amazing and positive feedback (again, thank you sooo much for that :heart:), we have decided to expand the game and release it as a full-fledged title! If there is something you would like to see in the final game, share your thoughts/ideas in the feedback section or under this post. We will see what we can do :grin: