# How to make rounded corners for 2d rectangle with a custom shader?

Does anyone know how to make rounded corners for a 2d rectangle with a custom shader?

import bpy
import gpu
from gpu_extras.batch import batch_for_shader

vertices = (
(100, 100), (300, 100),
(100, 200), (300, 200))

indices = (
(0, 1, 2), (2, 1, 3))

batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices)

def draw():
shader.uniform_float("color", (0, 0.5, 0.5, 1.0))

bpy.types.SpaceView3D.draw_handler_add(draw, (), 'WINDOW', 'POST_PIXEL')

• Have you tried adding some extra verts to get round corners? Commented Jul 20, 2021 at 17:38
• I want to control the roundness with a variable. Commented Jul 20, 2021 at 18:06
• blender.stackexchange.com/questions/129188/… Basically can make a 2d mesh (or bmesh) , bevel to make round corners triangulate it and use coordinates as above. Commented Jul 20, 2021 at 18:36
• Then you'll basically need to calculate 1/4th of a circle based on your point number. Commented Jul 20, 2021 at 19:20
• What exactly is not perfect using @batFINGER's method? Commented Oct 21, 2021 at 7:48

#### It works, but you need to find a way to do the Anti-Aliasing

import bpy, gpu
from mathutils import Matrix
from gpu_extras.batch import batch_for_shader
from bgl import glEnable, GL_BLEND

uniform mat4 winMat;

in vec2 position;
out vec2 pos;

void main(){
pos = position;
gl_Position = winMat * vec4(position, 0.0, 1.0);
}
'''

uniform vec4 color;
uniform vec4 LRBT;
uniform float d;

in vec2 pos;
out vec4 FragColor;

float c;
float dL;
float dR;
float dB;
float dT;

void main()
{
c = color[3];

if (pos[0] < LRBT[0] + d){
if (pos[1] > LRBT[3] - d){

}
}
dL = 1 - (LRBT[0] + d - pos[0]) / d;
dT = 1 - (pos[1] - LRBT[3] + d ) / d;
dB = 1 - (LRBT[2] + d - pos[1]) / d;
dR = 1 - (pos[0] - LRBT[1] + d) / d;
c *= dL * dT * dR * dT * dR * dB * dL * dB;

if (c > 0.1){c = 1.0;} else {c = 0.0;}

if (pos[0] <= LRBT[0] + 1){c = 0.0;
} else {
if (pos[0] >= LRBT[1] - 1){c = 0.0;
} else {
if (pos[1] <= LRBT[2] + 1){c = 0.0;
} else {
if (pos[1] >= LRBT[3] - 1){c = 0.0;
}
}
}
}

FragColor = vec4(color[0], color[1], color[2], c);
}
'''

vertices = (
(100, 100), (300, 100),
(100, 200), (300, 200))

indices = (
(0, 1, 2), (2, 1, 3))

batch = batch_for_shader(shader, 'TRIS', {"position": vertices}, indices=indices)
shader.uniform_float("color", (0.5, 0.5, 0.5, 1.0))
v = vertices
shader.uniform_float("LRBT", (v[0][0], v[1][0], v[0][1], v[2][1]))

def draw():
glEnable(GL_BLEND)

bpy.types.SpaceView3D.draw_handler_add(draw, (), 'WINDOW', 'POST_PIXEL')


#### Make a class

import bpy, gpu
from mathutils import Matrix
from gpu_extras.batch import batch_for_shader
from bgl import glEnable, GL_BLEND
from gpu.types import GPUShader
from gpu.matrix import get_projection_matrix

uniform mat4 winMat;

in vec2 position;
out vec2 pos;

void main(){
pos = position;
gl_Position = winMat * vec4(position, 0.0, 1.0);
}
'''

uniform vec4 color;
uniform vec4 LRBT;
uniform float d;

in vec2 pos;
out vec4 FragColor;

float c;
float dL;
float dR;
float dB;
float dT;

void main()
{
c = color[3];

if (pos[0] < LRBT[0] + d){
if (pos[1] > LRBT[3] - d){

}
}
dL = 1 - (LRBT[0] + d - pos[0]) / d;
dT = 1 - (pos[1] - LRBT[3] + d ) / d;
dB = 1 - (LRBT[2] + d - pos[1]) / d;
dR = 1 - (pos[0] - LRBT[1] + d) / d;
c *= dL * dT * dR * dT * dR * dB * dL * dB;

if (c > 0.1){c = 1.0;} else {c = 0.0;}

if (pos[0] <= LRBT[0] + 1){c = 0.0;
} else {
if (pos[0] >= LRBT[1] - 1){c = 0.0;
} else {
if (pos[1] <= LRBT[2] + 1){c = 0.0;
} else {
if (pos[1] >= LRBT[3] - 1){c = 0.0;
}
}
}
}

FragColor = vec4(color[0], color[1], color[2], c);
}
'''

box_indices = ((0, 1, 2), (2, 1, 3))

class Rbox:
__slots__ = "L", "R", "B", "T", "d", "color", "batch", "shader"
def __init__(self, L, R, B, T, d, color): # left, right, bottom, top, depth
self.L = L
self.R = R
self.B = B
self.T = T
self.d = d
self.color = color
def update(self):
self.batch = batch_for_shader(s, 'TRIS', {"position": (
(self.L, self.B), (self.R, self.B),
(self.L, self.T), (self.R, self.T))}, indices=box_indices)
def draw(self):
s.bind()
s.uniform_float("color", self.color)
s.uniform_float("LRBT", (self.L, self.R, self.B, self.T))
s.uniform_float("d", self.d)
s.uniform_float("winMat", get_projection_matrix())
self.batch.draw(s)
def move(self, dx, dy):
self.L += dx
self.R += dx
self.B += dy
self.T += dy
def move_update(self, dx, dy):
self.move(dx, dy)
self.update()

Rboxes = [
Rbox(100, 300, 100, 200, 30, (0.9, 0.9, 0.9, 1.0)),
Rbox(100, 300, 300, 400, 30, (0.1, 0.1, 0.1, 1.0)),
]
for r in Rboxes: r.update()

def draw():
glEnable(GL_BLEND)
for r in Rboxes: r.draw()

bpy.types.SpaceView3D.draw_handler_add(draw, (), 'WINDOW', 'POST_PIXEL')

• How to make an instance of the rectangle so that I can render 100 of them? Commented Aug 5, 2022 at 18:41
• You can make a class. I have added to the answer.
– X Y
Commented Aug 6, 2022 at 1:07

Make a mesh.-

Your example has 4 vertices and two faces making a rectangle of two triangles, going to need a bit more geometry than that to have rounded corners. The same method applies here to making a mesh.

Given blender has a built in tool to make meshes, let's make a plane with bmesh, bevel the corners, triangulate it, and scale to suit.

Bevel & triangulate.

Test script, makes a 2 x 1 rounded corner rectangle. The corner radius is 0.2 and 5 segments create the bevel. The bmesh is later translated and scaled to match our bgl coordinates. Moved to 100, 100 and scaled by 100 (Could also be saved to a mesh. See other bgl manual examples re using mesh coordinates)

import bmesh
import bpy
from mathutils import Matrix

dim = (2, 1)
cnr = 0.2

bm = bmesh.new()
bmesh.ops.create_grid(
bm,
size=0.5,
matrix=Matrix.Diagonal((dim[0], dim[1], 1, 1)) @
Matrix.Translation((0.5, 0.5, 0)),
)
bmesh.ops.bevel(
bm,
geom=bm.verts,
#loop_slide=True,
affect='VERTICES',
profile=0.5, # round
offset=cnr,
segments=5,
)

bmesh.ops.triangulate(
bm,
faces=bm.faces,
)

# scale by 100
bm.transform(
Matrix.Diagonal([100] * 4)
)
# translate by 100, 100
bm.transform(
Matrix.Translation((100, 100, 0))
)

print("vertices = (")
for v in bm.verts:
print(f"\t{v.co.xy[:]},")
print(")")
print("faces = (")
for f in bm.faces:
print(f"\t{[v.index for v in f.verts]},")
bm.to_mesh(bpy.context.object.data)
print(")")


Produces

Color (0,1, 0.5, 1.0) rounded corner result of using data below (faces for indices), overlayed on pre-run rectangle of question script

vertices = (
(120.0, 100.0),
(100.0, 120.0),
(113.81965637207031, 100.97886657714844),
(108.24429321289062, 103.81965637207031),
(103.81965637207031, 108.24429321289062),
(100.97886657714844, 113.81965637207031),
(300.0, 120.0),
(280.0, 100.0),
(299.0211181640625, 113.81965637207031),
(296.18035888671875, 108.24429321289062),
(291.7557067871094, 103.81965637207031),
(286.18035888671875, 100.97886657714844),
(120.0, 200.0),
(100.0, 180.0),
(113.81965637207031, 199.0211181640625),
(108.24429321289062, 196.18032836914062),
(103.81965637207031, 191.75570678710938),
(100.97886657714844, 186.18032836914062),
(280.0, 200.0),
(300.0, 180.0),
(286.18035888671875, 199.0211181640625),
(291.7557067871094, 196.18032836914062),
(296.18035888671875, 191.75570678710938),
(299.0211181640625, 186.18032836914062),
)
faces = (
[13, 1, 0],
[19, 21, 18],
[1, 5, 0],
[5, 4, 3],
[3, 2, 0],
[0, 7, 18],
[7, 11, 6],
[18, 7, 6],
[11, 10, 9],
[9, 8, 11],
[8, 6, 11],
[6, 19, 18],
[19, 23, 21],
[23, 22, 21],
[21, 20, 18],
[18, 12, 0],
[12, 14, 13],
[0, 12, 13],
[14, 15, 13],
[15, 16, 13],
[16, 17, 13],
[5, 3, 0],
)


Triangulate with Poke instead

Result of replacing bmesh triangulate operator with poke

bmesh.ops.poke(
bm,
faces=bm.faces,
)


Block with rounded edges in python

• How to remove the edge lines and add an outline? Commented Aug 12, 2022 at 15:34

Though, this is not a custom shader, still might be useful to someone.

Calculating vertices is easy:

def draw_rounded_box(self, x, y, width, height, radius):
segments = 16  # Number of segments for each rounded corner
vertices = []
indices = []

# Function to add arc vertices
angle_step = (end_angle - start_angle) / segments
start_index = len(vertices)  # Remember starting index
for i in range(segments + 1):
angle = start_angle + angle_step * i
vertices.append((cx + math.cos(angle) * radius, cy + math.sin(angle) * radius))
return start_index  # Return starting index of added vertices

# Bottom left corner
start_bottom_left = add_arc(x + radius, y + radius, math.pi, 3 * math.pi / 2, radius)
# Bottom right corner
start_bottom_right = add_arc(x + width - radius, y + radius, 3 * math.pi / 2, 2 * math.pi, radius)
# Top right corner
start_top_right = add_arc(x + width - radius, y + height - radius, 0, math.pi / 2, radius)
# Top left corner
start_top_left = add_arc(x + radius, y + height - radius, math.pi / 2, math.pi, radius)

# Add vertices for the straight edges, avoiding duplicates
vertices.extend([
vertices[start_bottom_left],    # Bottom left corner
vertices[start_bottom_left + segments],  # Bottom left edge
vertices[start_bottom_right],   # Bottom right corner
vertices[start_bottom_right + segments],  # Bottom right edge
vertices[start_top_right],      # Top right corner
vertices[start_top_right + segments],    # Top right edge
vertices[start_top_left],       # Top left corner
vertices[start_top_left + segments],     # Top left edge
(x + radius, y),                # Bottom edge start
(x + width - radius, y),        # Bottom edge end
(x + width, y + radius),        # Right edge top
(x + width, y + height - radius),  # Right edge bottom
(x + width - radius, y + height),   # Top edge right
(x + radius, y + height),      # Top edge left
(x, y + height - radius),      # Left edge bottom
(x, y + radius)                # Left edge top
])


The problem is making out triangles to cover the area efficiently.

The naive approach:

    # Calculate indices dynamically
def calculate_indices():
num_vertices = len(vertices)
for i in range(0, num_vertices, 2):
v1 = i
v2 = i + 1
v3 = (i + 3) % num_vertices
v4 = (i + 2) % num_vertices
indices.extend([(v1, v2, v3), (v1, v3, v4)])


It will result in the outline of rounded corners, but will not fill them.

So we need to draw each triangle to the center:

    # Calculate center of the rectangle
center_x = x + width / 2
center_y = y + height / 2
middle_vertex_index = len(vertices)  # Index of the middle vertex
vertices.append((center_x, center_y))  # Append middle vertex

# Calculate indices dynamically
def calculate_indices():
num_vertices = len(vertices)
for i in range(0, num_vertices - 2, 2):
v1 = i
v2 = i + 1
v3 = (i + 2) % num_vertices  # Next vertex in line
indices.extend([
(v1, v3, middle_vertex_index)
])

calculate_indices()


The last step is the same as in non-rounded triangle:

    shader = gpu.shader.from_builtin('UNIFORM_COLOR')

batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices)
shader.uniform_float("color", (0.1, 0.1, 0.1, 0.7))


Parameters:

• segments will influence the smoothness of rounded corners
• radius is a roundness of rectangle corner
• x, y, width, height are self-explanatory