Anyway to BGL draw an outline of a circle in 2.8?

I’d like to have a circle outlined with a color using bgl in Blender 2.8

The circle will be moving, so viewport updating will be nice.

A circle can be drawn using a preset. Just add the function to a draw handler.

from gpu_extras.presets import draw_circle_2d

pos = 200, 200
color = 1, 0, 0, 1
radius = 50

def draw():
    draw_circle_2d(pos, color, radius)

See here and here for more info.

Can this outline an existing circle?
I add the circle(mesh) & would like to draw over it.

No, that would require you getting the vertex coordinates of the actual mesh circle.

Assuming you know how to get those coordinates, you just plop them in a gpu batch. One of the links has examples of how a batch is made and drawn.

Yea I will have to look into it since I have no experience with the GPU shader api.

Okay, so I create a batch & now I’m having trouble getting the vertices of the circle(mesh). Also, it doesn’t seem to draw exactly where the circle(mesh) is located. The circle(mesh) will be moved around, rotation & everything. All I need is the outline to match the location & rotation.

def create_batch(self):
    
    
    obj = bpy.data.objects['Circle']#bpy.context.active_object
   

    vdata = obj.data.vertices

    vertices = [obj.matrix_world @ vert.co for vert in vdata] 

    plain_verts = [vert.to_tuple() for vert in vertices]
  
    self.shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
    self.batch = batch_for_shader(self.shader, 'LINE_STRIP', {"pos": plain_verts})

This example draws a yellow outline, assuming the mesh is a circle.
Here I’m using bmesh, but you can use ob.data if you don’t need to be in edit mode.
The coords need to be fetched from the mesh for each redraw if you want it to move.

import bpy, gpu, bgl
import bmesh
from gpu_extras.batch import batch_for_shader
from struct import pack

ob = bpy.context.object
mat = ob.matrix_world

if bpy.context.mode != 'EDIT_MESH':
    bpy.ops.object.editmode_toggle()
bm = bmesh.from_edit_mesh(ob.data)

def get_coords():
    return [mat @ v.co for v in bm.verts]

r, g, b, a = 1.0, 1.0, 0.0, 1.0
shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")

def draw():
    bgl.glLineWidth(3)
    batch = batch_for_shader(shader, 'LINE_LOOP', {"pos": get_coords()})
    shader.bind()
    color = shader.uniform_from_name("color")
    shader.uniform_vector_float(color, pack("4f", r, g, b, a), 4)
    batch.draw(shader)

if __name__ == "__main__":
    st = bpy.types.SpaceView3D
    st.draw_handler_add(draw, (), 'WINDOW', 'POST_VIEW')

    for area in bpy.context.screen.areas:
        if area.type == 'VIEW_3D':
            area.tag_redraw()

1 Like

Awesome! I was wondering,
Is there no way of making this work while in object mode?

Just change the function to grab coords from ob.data.vertices

def get_coords():
    return [mat @ v.co for v in ob.data.vertices]
1 Like

Thank you so much for your help. I really appreciate it. I still have a lot to study!
Are there any other links you’d recommend for better understanding the GPU shader api?

After following this tutorial by Jayanam: https://www.youtube.com/watch?v=EgrgEoNFNsA

I was trying to get this to work as a modal operator & failed miserably…

I believe it may be because of the draw_callback & passing the args to the handler, but can’t seem to find a solution for this… could it be where I placed tag_redraw?

import bpy, gpu, bgl
import bmesh
from gpu_extras.batch import batch_for_shader
from struct import pack



class circle_outline(bpy.types.Operator):
    bl_idname = "object.circle_outline"
    bl_label = "Outline active circle"
    bl_options = {"REGISTER", "UNDO"}
    


    def __init__(self):
        self.draw_handle = None
        self.draw_event = None
        
        self.widgets = []

    def invoke(self, context, event):
        
        self.create_batch()
        
        args = (self, context)
        
        self.register_handlers(args,context)
        for area in bpy.context.screen.areas:
            if area.type == 'VIEW_3D':
                area.tag_redraw()
        
        return {'RUNNING_MODAL'}
    
    def register_handlers(self, args,context):
        
        self.draw_handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback, args, "WINDOW", "POST_VIEW")
        
        self.draw_event = context.window_manager.event_timer_add(0.1, window=context.window)
    
    def unregister_handlers(self, context):
        
        context.window_manager.event_timer_remove(self.draw_event)
        
        bpy.types.SpaceView3D.draw_handler_remove(self.draw_handle, "WINDOW")
    
        self.draw_handle = None
        self.draw_event = None
        
        
    def modal(self, context, event):
                
        if event.type == "ESC":
            
            self.unregister_handlers(context)
            
        return {"CANCELLED"}
    
        
    
        return {"PASS_THROUGH"}


    def finish(self):
        self.unregister_handlers(context)
        return {"FINISHED"}
    
    def create_batch(self):
        #ob = bpy.context.object
        
        obj = bpy.context.active_object
        

        mat = obj.matrix_world
        
        def get_coords():
            return [mat @ v.co for v in obj.data.vertices]##return [mat @ v.co for v in bm.verts]
        
        
        r, g, b, a = 1.0, 1.0, 0.0, 1.0
        
        self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
        
        self.batch = batch_for_shader(self.shader, 'LINE_LOOP', {"pos": get_coords()})
        
    def draw_callback(self, op, context):
        bgl.glLineWidth(3)
        self.shader.bind()
        color = self.shader.uniform_from_name("color")
        self.shader.uniform_vector_float(color, pack("4f", r, g, b, a), 4)
        self.batch.draw(self.shader)
                
        
    
        
        
        
bpy.utils.register_class(circle_outline)

Try this more simplified example:

import bpy, gpu, bgl
from gpu_extras.batch import batch_for_shader
from struct import pack


class circle_outline(bpy.types.Operator):
    bl_idname = "object.circle_outline"
    bl_label = "Outline active circle"
    bl_options = {"REGISTER", "UNDO"}

    def end(self, context):
        context.space_data.draw_handler_remove(self._handle, "WINDOW")
        context.area.tag_redraw()
        return {'FINISHED'}

    def invoke(self, context, event):
        obj = context.object
        shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
        color_pos = shader.uniform_from_name("color")
        color = 1.0, 1.0, 0.0, 1.0

        def get_ob_coords():
            return [obj.matrix_world @ v.co for v in obj.data.vertices]
        
        def draw():
            bgl.glLineWidth(3)
            shader.bind()
            shader.uniform_vector_float(color_pos, pack("4f", *color), 4)
            batch = batch_for_shader(shader, 'LINE_LOOP', {"pos": get_ob_coords()})
            batch.draw(shader)

        context.window_manager.modal_handler_add(self)
        self._handle = context.space_data.draw_handler_add(
            draw, (), 'WINDOW', 'POST_VIEW')

        context.area.tag_redraw()
        return {'RUNNING_MODAL'}
    
    def modal(self, context, event):
        if event.type == "ESC" or not context.object:
            return self.end(context)
        return {"PASS_THROUGH"}

if __name__ == "__main__":
    bpy.utils.register_class(circle_outline)
1 Like

works great!

When you access the vertices in object mode, is it limited to only vertices?

I’m asking this because I’ve been trying to do the same with this:

import bpy
import gpu
from gpu_extras.batch import batch_for_shader

import bmesh

data = bpy.context.active_object.data
bm = bmesh.from_edit_mesh(data)

faces = [f for f in bm.faces if f.select == True]

dbm = bmesh.new()
for face in faces:
    dbm.faces.new((dbm.verts.new(v.co, v) for v in face.verts), face)
dbm.verts.index_update()

vertices = [v.co for v in dbm.verts]
faceindices = [(loop.vert.index for loop in looptris) for looptris in dbm.calc_loop_triangles()]
face_colors = [(0.9, 0.25, 0.25, 1) for _ in range(len(dbm.verts))]

edgeindices = [(v.index for v in e.verts) for e in dbm.edges]
edges_colors = [(0.0, 1.0, 0.25, 1) for _ in range(len(dbm.verts))]



shader = gpu.shader.from_builtin('3D_SMOOTH_COLOR')
batch1 = batch_for_shader(
    shader, 'TRIS',
    {"pos": vertices, "color": face_colors},
    indices=faceindices,
)
batch2 = batch_for_shader(
    shader, 'LINES',
    {"pos": vertices, "color": edges_colors},
    indices=edgeindices,
)


def draw():
    batch1.draw(shader)
    batch2.draw(shader)


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

You can access faces (data.polygons) and edges from object mode, too. The only difference is that faces and edges reference vertices by their index (not the vertices themselves). This is useful for drawing using an index buffer.

Just tested the code. Works just fine. What is the goal - to outline the shape instead of drawing each edge?

The goal is to outline only the selected edge loops & faces when changing selection as well. Also noticed that redraw doesnt update draw when moving the faces or edges.

For real time update you’ll need to use bmesh which makes drawing a little slower.
Contrary to what ob.data.calc_loop_triangles() says, it doesn’t work in edit mode.

Here’s a bmesh version that works on selection. Note that all the data fetching happens in the draw function.

import bpy
import gpu
from gpu_extras.batch import batch_for_shader
import bmesh

shader = gpu.shader.from_builtin('3D_SMOOTH_COLOR')
ob = bpy.context.object

def draw():
    bm = bmesh.from_edit_mesh(ob.data)
    verts = [v.co[:] for v in bm.verts]

    face_colors = ((0.9, 0.25, 0.25, 1),) * len(verts)
    edges_colors = ((0.0, 1.0, 0.25, 1),) * len(verts)

    faces = set(f.index for f in bm.faces if f.select)
    face_tri_indices = [[loop.vert.index for loop in looptris]
                        for looptris in bm.calc_loop_triangles()
                        if looptris[0].face.select]

    batch1 = batch_for_shader(
        shader, 'TRIS',
        {"pos": verts, "color": face_colors},
        indices=face_tri_indices)


    edge_indices = [(v.index for v in e.verts) for e in bm.edges if e.select]
    
    batch2 = batch_for_shader(
        shader, 'LINES',
        {"pos": verts, "color": edges_colors},
        indices=edge_indices)

    batch1.draw(shader)
    batch2.draw(shader)

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

Okay that makes sense.
I noticed that defining a draw_callback with args being passed can start & end a handler.

Do you know if it can outline objects with a subdivision modifier? It doesn’t seem to take the subdivision modifier into account when displaying in edit mode.

Do you think it’s possible to detect the vertex count, so if the selection has 4 vertices, outline it, otherwise don’t.

Edit: I was able to find a solution for this:

edge_indices = [(v.index for v in e.verts) for e in bm.edges if (e.select and selected_verts == 5)]

By “outline objects”, you mean draw an actual outline of the object’s silhouette, or just show the subdivided wire underneath?
The former is generally not cheap and requres and a custom shader.
If it’s the latter obj.show_wire already does that.

Generic selection, eg. whether a selection is made, can be tested using these (updates in edit mode):
obj.data.total_vert_sel
obj.data.total_edge_sel
obj.data.total_face_sel

If you need to find selected geometry and draw them, you still need to loop through the whole mesh and testing agains elem.select where elem is either a vert, edge or face. This already was done in the code above.

Yea I’d like to show the subdivided wire, I dont think this is possible since I am grabbing obj.data that hasn’t been applied since the modifier is still there. Only if I apply the modifiers, it will draw the edges outline. I was hoping it could work without having to apply.