Drawing very thick lines with bgl

I have a drawing addon where it samples the mouse position when the left mouse is clicked and creates points, the 3d part of it works fine but when it comes to drawing the points with BGL there are a couple of issues

I can’t render it as a connected curve/line because BGL has a clamp on the thickness, I can’t make it go wider than a few pixels and i need unlimited size because someone might make the brush very big

I’m currently rendering each point as a circle which lets me draw them at any size, but comes with the downside of gaps in between the points. If the user moves their mouse really fast there are fewer points to sample so you get gaps. I know how to resample the points into a connected bezier curve then resample the curve into a ton of points and render them as circles again but this sounds like overkill

I’m wondering if maybe something came out with 3.0 that lets me go ahead and draw very thick lines, or if there’s a better way to approach this situation

I’m not aware of any changes in 3.0 that would unclamp gl lines, doubly so since BGL is actually being deprecated (originally the target was 3.0 but it has been pushed to 3.1). why not just draw a polygon, or write a custom ‘line’ fragment shader where you can specify any thickness you want?

Thanks for the quick reply! Ah man I’m going to have to redo all the BGL stuff for 3.1 ? I will start looking into learning openGL. I just haven’t seen any examples of that stuff in blender yet, do you have any premade snippets by any chance? re: the custom fragment shader. I’ll do some googling though, thank you for getting me started!

I was having some difficulties with the math of converting the line into a polygon but just found this, https://blog.mapbox.com/drawing-antialiased-lines-with-opengl-8766f34192dc so I will see what I can do. I guess I don’t have to move the points to actually create the polygon, this guy just duplicates them and makes 2 tris so thats easy enough.

I dug into my old doodles folder from a few years back when I was obsessed with anti-aliased circles. I’ve updated it to use the gpu module and added an operator to draw (linearly) interpolated mouse paths.You can find parametric SDF functions for GLSL scattered around the web, that better suit your fragment shader.

Usage: Run in text editor. Draw with LMB, change size with mouse wheel, clear with RMB, exit with Esc.

import bpy
import gpu
from gpu.types import GPUVertBuf, GPUBatch
from gpu_extras.batch import batch_for_shader
from gpu.types import GPUShader
from mathutils import Vector

# Generic
vertex_shader = """
    uniform mat4 ModelViewProjectionMatrix;
    in vec2 pos;
    void main()
    {
      gl_Position = ModelViewProjectionMatrix * vec4(pos, 0.0, 1.0);
    }
"""

def frag_circle(r, g, b, a, sharpness):
    return """
        vec4 col = vec4({}, {}, {}, {});
        vec4 add_col = vec4(1, 1, 1, 0);
        out vec4 out_col;
        void main()
        {{
            float r, e, a;
            vec2 cxy = 2.0 * gl_PointCoord - 1.0; // coord

            r = dot(cxy, cxy) + 0.5; // radius
            e = fwidth(r); // edge blur
            a = 1.0 - smoothstep(1.0 - e, 1.0 + e, r);

            out_col = add_col + (a / col) * {s};
        }}""".format(r, g, b, a, s=max(0, sharpness))


point_size = 12
sharpness = 1.0
fragment_shader = frag_circle(1.0, 1.0, 0.0, 1.0, sharpness)
shader = GPUShader(vertex_shader, fragment_shader)
fmt = shader.format_calc()


class TEXT_OT_draw(bpy.types.Operator):
    bl_idname = "text.draw"
    bl_label = "Draw"
    active = False
    
    def draw_handler(self):
        if not self.path:
            return

        state = gpu.state.blend_get()
        gpu.state.blend_set("ALPHA")
        gpu.state.point_size_set(self.point_size)
        batch_for_shader(shader, 'POINTS', {"pos": self.path}).draw(shader)
        gpu.state.blend_set(state)

    def invoke(self, context, event):
        context.window_manager.modal_handler_add(self)
        self.handle = context.space_data.draw_handler_add(
            self.draw_handler, (), 'WINDOW', 'POST_PIXEL')
        self.path = []
        self.point_size = point_size
        return {'RUNNING_MODAL'}

    def modal(self, context, event):
        if event.type == 'LEFTMOUSE':
            self.active = event.value == 'PRESS'
            if self.active:
                point = Vector((event.mouse_region_x, event.mouse_region_y))
                self.path.append(point)
            context.area.tag_redraw()

        elif event.type == 'MOUSEMOVE' and self.active:
            point = Vector((event.mouse_region_x, event.mouse_region_y))
            
            # Interpolate distance
            point_prev = self.path[-1]
            dist = (point - point_prev).length
            step = 2
            while dist > step:
                point_prev = point_prev.lerp(point, (step / dist))
                self.path.append(point_prev)
                dist -= step

            self.path.append(point)
            context.area.tag_redraw()

        elif event.type == 'ESC':
            context.space_data.draw_handler_remove(self.handle, 'WINDOW')
            context.area.tag_redraw()
            return {'CANCELLED'}
        
        elif event.type == 'RIGHTMOUSE':
            self.path.clear()
            context.area.tag_redraw()

        # Change point size
        elif event.type == 'WHEELUPMOUSE':
            self.point_size *= 1.25
            context.area.tag_redraw()
        elif event.type == 'WHEELDOWNMOUSE':
            self.point_size = max(1, self.point_size / 1.25)
            context.area.tag_redraw()
        return {'RUNNING_MODAL'}

if __name__ == "__main__":
    bpy.utils.register_class(TEXT_OT_draw)
    bpy.ops.text.draw('INVOKE_DEFAULT')

Thank you very much! This is perfect :slight_smile: