3
$\begingroup$

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

enter image description here

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))

shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices)


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


bpy.types.SpaceView3D.draw_handler_add(draw, (), 'WINDOW', 'POST_PIXEL')
$\endgroup$
5
  • 2
    $\begingroup$ Have you tried adding some extra verts to get round corners? $\endgroup$
    – brockmann
    Commented Jul 20, 2021 at 17:38
  • $\begingroup$ I want to control the roundness with a variable. $\endgroup$
    – Karan
    Commented Jul 20, 2021 at 18:06
  • 2
    $\begingroup$ 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. $\endgroup$
    – batFINGER
    Commented Jul 20, 2021 at 18:36
  • $\begingroup$ Then you'll basically need to calculate 1/4th of a circle based on your point number. $\endgroup$
    – brockmann
    Commented Jul 20, 2021 at 19:20
  • 1
    $\begingroup$ What exactly is not perfect using @batFINGER's method? $\endgroup$
    – brockmann
    Commented Oct 21, 2021 at 7:48

3 Answers 3

3
+50
$\begingroup$

enter image description here

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

vertex_shader = '''
    uniform mat4 winMat;

    in vec2 position;
    out vec2 pos;

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

fragment_shader = '''
    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))

shader = gpu.types.GPUShader(vertex_shader, fragment_shader)
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]))
shader.uniform_float("d", 30)

def draw():
    glEnable(GL_BLEND)
    shader.bind()
    shader.uniform_float("winMat", gpu.matrix.get_projection_matrix())
    batch.draw(shader)

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

vertex_shader = '''
    uniform mat4 winMat;

    in vec2 position;
    out vec2 pos;

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

fragment_shader = '''
    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
        self.shader = GPUShader(vertex_shader, fragment_shader)
    def update(self):
        s = self.shader
        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 = self.shader
        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')
$\endgroup$
2
  • $\begingroup$ How to make an instance of the rectangle so that I can render 100 of them? $\endgroup$
    – Karan
    Commented Aug 5, 2022 at 18:41
  • $\begingroup$ You can make a class. I have added to the answer. $\endgroup$
    – X Y
    Commented Aug 6, 2022 at 1:07
5
$\begingroup$

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.

enter image description here

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

enter image description here 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

enter image description here

Result of replacing bmesh triangulate operator with poke

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

Block with rounded edges in python

$\endgroup$
1
  • $\begingroup$ How to remove the edge lines and add an outline? $\endgroup$
    – Karan
    Commented Aug 12, 2022 at 15:34
0
$\begingroup$

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
    def add_arc(cx, cy, start_angle, end_angle, radius):
        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.bind()
    shader.uniform_float("color", (0.1, 0.1, 0.1, 0.7))
    batch.draw(shader)

Parameters:

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

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .