Rounded corners

Rounded corners for nodes. See comments for more information. Designed for Widgets. Please, read instructions before using.

 

Cover image source: https://unsplash.com/photos/hXNGeAFOgT4

Shader code
shader_type canvas_item;
// Shader to round corners of a canvas. The 'radius_scale' is multiplied by
// minimum(width, height)/2.0 to calculate the radius of the corners.
//
// Instructions:
// 1) The node that uses this shader must have signals 'tree_entered' and
// 'item_rect_changed' connected to a callable with the next code:
// material.set_shader_parameter("width", size.x)
// material.set_shader_parameter("height", size.y)
//
// Known issues:
// 1) If used on 'TextureRect', take care of 'expand_mode' and 'stretch_mode',
// because image corners might be outside node rectangle and therefore clipped.
// Corners are rounded, but they are outside node's rectangle.


uniform float radius_scale: hint_range(0.0, 1.0, 0.1) = 0.1;
uniform bool rounded_corner_top_left = true;
uniform bool rounded_corner_top_right = true;
uniform bool rounded_corner_bottom_left = true;
uniform bool rounded_corner_bottom_right = true;
uniform float width = 1.0;
uniform float height = 1.0;


void fragment() {
	vec4 image = texture(TEXTURE, UV);
	vec2 pos = vec2(UV.x*width, UV.y*height);
	float radius = min(width, height)*radius_scale/2.0;
	float dist;
	// Top left corner
	if (rounded_corner_top_left) {
		dist = length(pos - vec2(radius));
		if (dist > radius && pos.x < radius && pos.y < radius) {
			image.a = 0.0;
		}
		// debugging only
//		if (dist < radius){image.r = 1.0;}
	}
	// Top right corner
	if (rounded_corner_top_right) {
		dist = length(pos - vec2(width-radius, radius));
		if (dist > radius && pos.x > width-radius && pos.y < radius) {
			image.a = 0.0;
		}
		// debugging only
//		if (dist < radius){image.r = 1.0;}
	}
	// Bottom left corner
	if (rounded_corner_bottom_left) {
		dist = length(pos - vec2(radius, height-radius));
		if (dist > radius && pos.x < radius && pos.y > height-radius) {
			image.a = 0.0;
		}
		// debugging only
//		if (dist < radius){image.r = 1.0;}
	}
	// Bottom right corner
	if (rounded_corner_bottom_right) {
		dist = length(pos - vec2(width-radius, height-radius));
		if (dist > radius && pos.x > width-radius && pos.y > height-radius) {
			image.a = 0.0;
		}
		// debugging only
//		if (dist < radius){image.r = 1.0;}
	}
	COLOR = image;
}
Tags
corner, Rounded
The shader code and all code snippets in this post are under MIT license and can be used freely. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

Related shaders

Rounded Corners

Rounded Corners (simple)

guest

4 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
cyberlove
cyberlove
4 months ago

Version for constant – anchor size and simple color:

shader_type canvas_item;uniform vec4 color : source_color = vec4(0.18, 0.2, 0.22, 1.0);uniform float radius : hint_range(0.0, 1.0, 0.01) = 0.2;bool IsCornerOut(float X, float Y, float D){	return D > radius && X < radius / 2.0 && Y < radius;}void fragment(){	float K = SCREEN_PIXEL_SIZE.y / SCREEN_PIXEL_SIZE.x;	float dist = 0.0;	vec4 col_mod = color;		if (IsCornerOut(UV.x, UV.y, length(vec2(UV.x * K, UV.y) - vec2(radius))))	{		col_mod.a = 0.0;	}	dist = length(vec2((1.0 - UV.x) * K, UV.y) - vec2(radius));	if (IsCornerOut(1.0 - UV.x, UV.y, length(vec2((1.0 - UV.x) * K, UV.y) - vec2(radius))))	{		col_mod.a = 0.0;	}		if (IsCornerOut(UV.x, 1.0 - UV.y, length(vec2(UV.x * K, 1.0 - UV.y) - vec2(radius))))	{		col_mod.a = 0.0;	}	if (IsCornerOut(1.0 - UV.x, 1.0 - UV.y, length(vec2((1.0 - UV.x) * K, 1.0 - UV.y) - vec2(radius))))	{		col_mod.a = 0.0;	}	COLOR = col_mod;}
cyberlove
cyberlove
4 months ago
Reply to  cyberlove

shader_type canvas_item;
uniform vec4 color : source_color = vec4(0.18, 0.2, 0.22, 1.0);
uniform float radius : hint_range(0.0, 1.0, 0.01) = 0.2;

bool IsCornerOut(float X, float Y, float D)
{
return D > radius && X < radius / 2.0 && Y < radius;
}

void fragment()
{

float K = SCREEN_PIXEL_SIZE.y / SCREEN_PIXEL_SIZE.x;
float dist = 0.0;
vec4 col_mod = color;

if (IsCornerOut(UV.x, UV.y, length(vec2(UV.x * K, UV.y) – vec2(radius))))
{
col_mod.a = 0.0;
}

if (IsCornerOut(1.0 – UV.x, UV.y, length(vec2((1.0 – UV.x) * K, UV.y) – vec2(radius)))) {
col_mod.a = 0.0;
}

if (IsCornerOut(UV.x, 1.0 – UV.y, length(vec2(UV.x * K, 1.0 – UV.y) – vec2(radius))))
{
col_mod.a = 0.0;
}

if (IsCornerOut(1.0 – UV.x, 1.0 – UV.y, length(vec2((1.0 – UV.x) * K, 1.0 – UV.y) – vec2(radius))))
{
col_mod.a = 0.0;
}

COLOR = col_mod;
}
1:1 anchor delta size relations needed, and it will work with different screen size.

Last edited 4 months ago by cyberlove
HyperGameDev
3 months ago

Here’s a version I made with an adjustable border to go around the control node. For me, setting the overall width and height parameters to 1/100th of their size looked good but YMMV!

shader_type canvas_item;

uniform sampler2D source_texture;
uniform float radius_scale : hint_range(0.0, 1.0, 0.1) = 0.1;
uniform bool rounded_corner_top_left = true;
uniform bool rounded_corner_top_right = true;
uniform bool rounded_corner_bottom_left = true;
uniform bool rounded_corner_bottom_right = true;

uniform float width = 1.0;
uniform float height = 1.0;

uniform vec4 border_color : source_color = vec4(1.0);
uniform float border_thickness : hint_range(0.0, 1.0, 0.001) = 1.0;

float corner_distance(vec2 pos, vec2 center, bool enabled) {
  if (!enabled) return -1.0;
  return length(pos - center);
}

void fragment() {
  vec2 pos = vec2(UV.x * width, UV.y * height);
  float radius = min(width, height) * radius_scale / 2.0;
  vec4 tex_color = texture(source_texture, UV);

  float inner = radius - border_thickness;
  bool mask = false;
  bool border = false;

  // Corner masks.
  if (rounded_corner_top_left && pos.x < radius && pos.y < radius)
    mask = corner_distance(pos, vec2(radius), true) > radius;
  else if (rounded_corner_top_right && pos.x > width - radius && pos.y < radius)
    mask = corner_distance(pos, vec2(width - radius, radius), true) > radius;
  else if (rounded_corner_bottom_left && pos.x < radius && pos.y > height - radius)
    mask = corner_distance(pos, vec2(radius, height - radius), true) > radius;
  else if (rounded_corner_bottom_right && pos.x > width - radius && pos.y > height - radius)
    mask = corner_distance(pos, vec2(width - radius, height - radius), true) > radius;

  // Border logic.
  if (!mask) {
    float d = -1.0;
    if (rounded_corner_top_left && pos.x < radius && pos.y < radius)
      d = corner_distance(pos, vec2(radius), true);
    else if (rounded_corner_top_right && pos.x > width - radius && pos.y < radius)
      d = corner_distance(pos, vec2(width - radius, radius), true);
    else if (rounded_corner_bottom_left && pos.x < radius && pos.y > height - radius)
      d = corner_distance(pos, vec2(radius, height - radius), true);
    else if (rounded_corner_bottom_right && pos.x > width - radius && pos.y > height - radius)
      d = corner_distance(pos, vec2(width - radius, height - radius), true);

    if (d >= inner && d <= radius) {
      border = true;
    } else if (
      (pos.x < border_thickness || pos.x > width - border_thickness) ||
      (pos.y < border_thickness || pos.y > height - border_thickness)
    ) {
      border = true;
    }
  }

  if (mask) {
    discard;
  } else if (border) {
    COLOR = border_color;
  } else {
    COLOR = tex_color;
  }
}
HyperGameDev
3 months ago
Reply to  HyperGameDev

And per acgc99’s suggestion, here’s the .gdscript I added to the control nodes to ensure they dynamically resized when changed (the @tool at the top is optional):

@tool

extends Control

@onready var shader_material: ShaderMaterial = get_material()

var scale_factor: float = 100.

func _ready():
	connect("tree_entered", _on_update_shader_params)
	connect("item_rect_changed", _on_update_shader_params)

func _on_update_shader_params():
	var rect_size = get_rect().size
	shader_material.set_shader_parameter("width", rect_size.x/scale_factor)
	shader_material.set_shader_parameter("height", rect_size.y/scale_factor)