I am slowly getting better at writing shader code. Here's a "unit selected" shader I'd like to share:
https://godotshaders.com/shader/unit-selected-oscillating-circle/

If anyone has ideas on how to improve upon it I'd be interested in how.

shader_type canvas_item;

uniform float ring_radius : hint_range(0.1, 0.5, 0.01) = 0.4;
uniform float thickness_scalar : hint_range(0.0, 0.99, 0.05) = 0.7;
uniform float oscillation_scalar : hint_range(0.0, 0.25, 0.005) = 0.025;
uniform float speed : hint_range(0.0, 50.0, 0.1) = 1.0;
uniform vec4 main_color : hint_color = vec4(1.0,1.0,1.0,1.0);
uniform vec4 lerp_color : hint_color = vec4(1.0,1.0,1.0,1.0);

float range_lerp(float value, float min1, float min2, float max1, float max2){
	return min2 + (max2 - min2) * ((value - min1) / (max1 - min1));
}

void fragment() {
	// Calculate the distance between the current pixel and the center of the unit
	float dist = distance(UV, vec2(0.5, 0.5));

	// Add a slight oscillation to the size of the ring
	float o = cos(TIME * speed);
	float ring_size = ring_radius + o * oscillation_scalar;
	
	// Solve for ring alpha channel
	float alpha = 0.0;
	if (dist < ring_size){
		alpha = clamp(range_lerp(dist, thickness_scalar * ring_size, 0.0, ring_size, 1.0), 0.0, 1.0);
	}
	
	// Solve w mix amount for optional color lerping
	float w = range_lerp(o, -1.0, 1.0, 1.0, 0.0);
	
	// Output the final color
	COLOR = vec4(mix(main_color.rgb, lerp_color.rgb, w), alpha );
}
  • xyz replied to this.

    Erich_L Keep the thickness constant (i.e. independent of the current ring size), to avoid the thing looking like just plainly scaled bitmap.

      xyz I'm confused, I've been staring at the gif for awhile, doesn't it seem to have the same thickness throughout?

      • xyz replied to this.

        I might take a look. I just change color of the hex right now which works pretty good.

        Erich_L I'm confused, I've been staring at the gif for awhile, doesn't it seem to have the same thickness throughout?

        No, the thickness oscillates. It says so in the code. The range you're remapping into alpha is: ring_size - ring_size * thickness_scalar, so it follows that thickness is: ring_size*(1-thickness_scalar). It's proportional to ring_size, which oscillates.

        You also don't need the whole lerp/if shtick: Do it the shader way:

        float alpha = step(dist, ring_size) * smoothstep(ring_size * (1.0-thickness_scalar), ring_size, dist);

        Or as I'd prefer it, with constant thickness:

        float alpha = step(dist, ring_size) * smoothstep(ring_size-thickness_scalar, ring_size, dist);

        You can also add inner_hardness uniform that varies in 0-1 range to make the gradient zone customizable. The thing then becomes:

        float alpha = step(dist, ring_size) * smoothstep(ring_size-thickness_scalar, ring_size-(thickness_scalar*inner_hardness), dist);

        Alright, enough nitpicking for today 🫠

          xyz I see! I chose as my favorite line:

          	float alpha = step(dist, ring_size) * smoothstep(ring_size * (1.0 - thickness_scalar), ring_size, dist);

          Using step and smoothstep, wow, what an invaluable pair of helpers for shader programming. I always hate if statements I can't get rid of.

          I also chose to add a "spring" effect when the unit is selected springing a ring size scalar from 0 to 1. Code with spring effect:

          Click to reveal Click to hide
          shader_type canvas_item;
          
          // reset this from code back to zero to set the "spring"
          uniform float reset : hint_range(0.0, 1.0, 1.0) = 0.0;
          
          uniform float ring_radius : hint_range(0.1, 0.5, 0.01) = 0.4;
          uniform float thickness_scalar : hint_range(0.0, 0.99, 0.05) = 0.7;
          uniform float oscillation_scalar : hint_range(0.0, 0.25, 0.005) = 0.025;
          uniform float speed : hint_range(0.0, 50.0, 0.1) = 1.0;
          uniform vec4 main_color : hint_color = vec4(1.0,1.0,1.0,1.0);
          uniform vec4 lerp_color : hint_color = vec4(1.0,1.0,1.0,1.0);
          
          float range_lerp(float value, float min1, float min2, float max1, float max2){
          	return min2 + (max2 - min2) * ((value - min1) / (max1 - min1));
          }
          
          void fragment() {
          	// Calculate the distance between the current pixel and the center of the unit
          	float dist = distance(UV, vec2(0.5, 0.5));
          
          	// Add a slight oscillation to the size of the ring
          	float o = cos(TIME * speed);
          	float ring_size = reset * ring_radius + o * oscillation_scalar;
          	
          	float inner_hardness = 0.0;
          	
          	// Solve for ring alpha channel
          	float alpha = step(dist, ring_size) * smoothstep(ring_size * (1.0 - thickness_scalar), ring_size, dist);
          	
          	// Solve w mix amount for optional color lerping
          	float w = range_lerp(o, -1.0, 1.0, 1.0, 0.0);
          	
          	// Output the final color
          	COLOR = vec4(mix(main_color.rgb, lerp_color.rgb, w), alpha );
          }

          Which would require this gdscript to interpolate the spring variable named reset:

          Click to reveal Click to hide
          	var tween := Tween.new()
          	add_child(tween)
          	tween.interpolate_property(node.material, "shader_param/reset", 0.0, 1.0, 0.2, Tween.TRANS_CUBIC, Tween.EASE_OUT)
          	tween.connect("tween_all_completed", tween, "queue_free")
          	tween.start()
          • xyz replied to this.

            Erich_L step() is basically an if in disguise 😃
            smoothstep() is cubic interpolation, which tends to look better than linear interpolation in visual gradients.

            Btw you also don't need that range_lerp() for brining the cosine into 0-1 range. You can simply do:

            float w = o * .5 + .5;

            And optimize further to:

            float w = fma(o, .5, .5);

            You've now optimized away at least 5 gpu instructions (4 additions and 2 multiplications, replaced with a single gpu instruction fma).

            It's not a big deal in such a simple shader but good to be aware of for shaders with more involved calculations.

              wish u were working on "normord" 😉

                DJM It's all good shader learning is critical!

                xyz ah yep I see that. My brain uses range_lerp as a crutch. Thanks a ton!

                DJM wish u were working on "normord" 😉

                Hey, strictly speaking, I was working on it, although only for a single afternoon. I expect at least a mention in the credits when the game makes it big on Steam 😉. You're saying you have more of them unwieldy sprites that need to be taken care of? 🙂

                I wouldn't interfere with @Erich_L 's shader code that much if I didn't know he enjoys it. Plus it's a publicly released piece of code. Using fmas will make the coder look like they know what they're doing 🙂

                • DJM replied to this.

                  xyz u will get a mention for sure. and yeah, i still need a better particle system in godot