© Chris Conlan 2017

Chris Conlan, The Blender Python API, 10.1007/978-1-4842-2802-9_6

6. The bgl and blf Modules

Chris Conlan

(1)Bethesda, Maryland, USA

The bgl module is a wrapper for OpenGL functions commonly used by Blender in the 3D Viewport and Blender Game Engine. OpenGL (Open Graphics Library) is an open source low-level API used in innumerable 3D applications to take advantage of hardware-accelerated computing.

The bgl documentation will seem familiar to those reader already familiar with OpenGL. The bgl module itself is meant to mimic to call structure and frame-by-frame rendering style of OpenGL 2.1.

In reading through the bgl documentation, we notice many advanced concepts like buffer operations, face culling, and rasterization. Fortunately for Blender Python programmers, the 3D Viewport manages these operations already. We are more concerned with marking up the 3D Viewport with extra information to help the user understand his models. This chapter focuses primarily on drawing with bgl.

The blf module is a small set of functions for displaying text and drawing fonts. It is closely related to bgl and rarely mentioned in examples without it. Blender Python developers commonly combine the bgl and blf modules to make measurement tools, drawing lines with bgl and displaying their measurements with blf. We do just that in this chapter.

Note that these modules are commonly seen in examples with the bge (Blender Game Engine) module. We will not be working in Blender Game Engine, so these scripts will not run, and attempts to import bge will fail. We restrict our drawing to the 3D Viewport.

Note also that the bgl module is set to be replaced or majorly reconstructed in Blender 2.80+. It is likely this chapter will be the first due for an update after the release of this text.

Instantaneous Drawing

The bgl and blf modules cannot be taught in the same way that other Blender Python modules can. When a line or character is drawn on the 3D Viewport by either of these modules, it is only visible for a single frame. So, we cannot experiment with it in the Interactive Console like we have with other modules. Functions we execute in the Interactive Console may execute without error, but we will not be able to view the results in the 3D Viewport.

To effectively use the bgl and blf modules, we must use them within a handler function that is set to update at every frame change. Thus, we start with a handler example using non-OpenGL concepts.

Handlers Overview

This section gives examples of handlers using bpy.app.handlers. This is not the submodule we will ultimately use when dealing with bgl and blf, but it is instructive for learning about handlers in Blender.

Clock Example

Handlers are functions that are set to run every time an event occurs. To instantiate a handler, we declare a function, then add it to one of the possible lists of handlers in Blender. In Listing 6-1, we create a function that modifies the text of a text mesh with the current time. We then add the function to bpy.app.handlers.scene_update_pre to indicate that we would like it to run right before the 3D Viewport is updated and displayed.

The result is what appears to be a clock in the 3D Viewport. In actuality, it is a text mesh that is updating many times per second. This example is not safe or full-proof, but as long as we keep the object in the scene and named MyTextObj, we can add and edit other objects with the clock running in the background. See Figure 6-1 for the result.

A438961_1_En_6_Fig1_HTML.jpg
Figure 6-1. Result of the Blender clock handler example
Note

The behavior of the clock is not a documented behavior and may change with future releases of Blender. Specifically, Blender intends to change what they qualify as a frame change. Currently, frame changes seem to happen instantaneously and constantly.

The official Blender documentation gives examples where the only parameter passed to the handler is a dummy. Handler functions should be treated like traditional Python lambdas, with the exception that a single dummy argument is required as the first parameter. We pass the function itself rather than the output of the function, and a new unnamed instance of the function is created when it is passed. We cannot easily access this unnamed function after it is created for the handler.

Listing 6-1. Blender Clock Handler Example
import bpy import datetime

# Clear the scene bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete()

# Create an object for our clock bpy.ops.object.text_add(location=(0, 0, 0)) bpy.context.object.name = 'MyTextObj'

# Create a handler function
def tell_time(dummy):
    current_time = datetime.datetime.now().strftime('%H:%M:%S.%f')[:-3]
    bpy.data.objects['MyTextObj'].data.body = current_time


# Add to the list of handler functions "scene_update_pre"
bpy.app.handlers.scene_update_pre.append(tell_time)

Managing Handlers

In the case of bpy.app.handlers, we can edit various lists of functions to manage our handlers. These lists are quite literally Python classes of type list, and we can operate on them as such. We can use list class methods such as append(), pop(), remove(), and clear() to manage our handler functions. See Listing 6-2 for some useful examples.

Listing 6-2. Managing Handler Lists
# Will only work if 'tell_time' is in scope                  
bpy.app.handlers.scene_update_pre.remove(tell_time)
# Useful in development for a clean slate
bpy.app.handlers.scene_update_pre.clear()


# Remove handler at the end of the list and return it
bpy.app.handlers.scene_update_pre.pop()

Types of Handlers

In Listing 6-1, we used bpy.app.handlers.scene_update_pre to modify a mesh according to internal variables before each update. Table 6-1 details types of handlers in bpy.app.handlers as they appear in the official documentation.

Table 6-1. Types of Handlers

Handler

Called On

frame_change_post

After frame change during rendering or playback

frame_change_pre

Before frame change during rendering or playback

render_cancel

Canceling a render job

render_complete

Completing a render job

render_init

Initializing a render job

render_post

After render

render_pre

Before render

render_stats

Printing render statistics

render_write

Directly after frame is written in rendering

load_post

After loading a .blend file

load_pre

Before loading a .blend file

save_post

After saving a .blend file

save_pre

Before saving a .blend file

scene_update_post

After updating scene data (e.g., 3D Viewport)

scene_update_pre

Before updating scene data (e.g. ,3D Viewport)

game_pre

Starting the game engine

game_post

Ending the game engine

There is some functional overlap in Table 6-1, and not every handler behaves how one would expect. For example, using scene_update_post in Listing 6-1 as opposed to scene_update_pre does not work at all. Readers are encouraged to experiment to determine which one fits their needs.

Persistent Handlers

If we want handlers to persist after loading a .blend file, we can add the @persistent decorator. Normally, handlers are freed when loading a .blend file, so certain handlers like bpy.app.handlers.load_post necessitate this decorator. Listing 6-3 uses the @persistent decorator to print file diagnostics after loading a .blend file.

Listing 6-3. Printing File Diagnostics on Load
import bpy
from bpy.app.handlers import persistent


@persistent
def load_diag(dummy):
    obs = bpy.context.scene.objects


     print(' ### File Diagnostics ###')
     print('Objects in Scene:', len(obs))
     for ob in obs:
         print(ob.name, 'of type', ob.type)
bpy.app.handlers.load_post.append(load_diag)


# After reloading startup file:
#
# ### File Diagnostics ###
# Objects in Scene: 3
# Cube of type MESH
# Lamp of type LAMP
# Camera of type CAMERA

Handlers in blf and bgl

Now that we have a basic understanding of handlers, we will detail how to draw with OpenGL tools directly on the 3D Viewport. The handlers used for drawing on the 3D Viewport are not part of bpy.app.handlers, rather they are undocumented member functions of bpy.types.SpaceView3D. To understand these member functions, we have reduced real-world examples of their use by other developers.

Listing 6-4 shows how to use bgl and blf to draw the name of an object on its origin point.

Listing 6-4. Drawing the Name of an Object
import bpy
from bpy_extras import view3d_utils import bgl
import blf


# Color and font size of text
rgb_label = (1, 0.8, 0.1, 1.0)
font_size = 16
font_id = 0


# Wrapper for mapping 3D Viewport to OpenGL 2D region

def gl_pts(context, v):
    return view3d_utils.location_3d_to_region_2d(
        context.region,
        context.space_data.region_3d,
        v)


# Get the active object, find its 2D points, draw the name

def draw_name(context):

    ob = context.object
    v = gl_pts(context, ob.location)


    bgl.glColor4f(*rgb_label)

    blf.size(font_id, font_size, 72)
    blf.position(font_id, v[0], v[1], 0)
    blf.draw(font_id, ob.name)


# Add the handler
# arguments:
# function = draw_name,
# tuple of parameters = (bpy.context,),
# constant1 = 'WINDOW',
# constant2 = 'POST_PIXEL'
bpy.types.SpaceView3D.draw_handler_add(
    draw_name, (bpy.context,), 'WINDOW', 'POST_PIXEL')

Running Listing 6-4 in the text editor will allow you to see the name of the active object drawn on its origin point.

Handlers created with bpy.types.SpaceView3D are not as easily accessible as those in bpy.app.handlers and are persistent by default. Unless we create better controls for flicking these handlers on and off, we will have to restart Blender to detach this handler. In the next section, we place this handler in an add-on that allows us to flick it on and off with a button. Also, we store the handler in a bpy.types.Operator so we will not lose our reference to the function after adding it to the handler.

Note

The draw_handler_add() and draw_handler_remove() functions are currently undocumented in bpy.types.SpaceView3D in Blender’s official documentation. Therefore, we will work with them as best we can based on known functional examples.

Example Add-On

This add-on is a standalone script, so it may be run by copying it into the Text Editor or importing it via the User Preferences. Readers are encouraged to run it via the Text Editor for easy experimentation. See Listing 6-5 for the add-on and Figure 6-2 for a screenshot of the result (in Edit Mode).

A438961_1_En_6_Fig2_HTML.jpg
Figure 6-2. Drawing add-on on a cube in Edit Mode
Listing 6-5. Simple Line and Text Drawing
bl_info = {
    "name": "Simple Line and Text Drawing",
    "author": "Chris Conlan",
    "location": "View3D > Tools > Drawing",
    "version": (1, 0, 0),
    "blender": (2, 7, 8),
    "description": "Minimal add-on for line and text drawing with bgl and blf. "
                   "Adapted from Antonio Vazquez's (antonioya) Archmesh." ,
    "wiki_url": "http://example.com",
    "category": "Development"
}


import bpy
import bmesh
import os


import bpy_extras
import bgl
import blf


# view3d_utils must be imported explicitly
from bpy_extras import view3d_utils


def draw_main(self, context):
    """Main function, toggled by handler"""


    scene = context.scene
    indices = context.scene.gl_measure_indices


    # Set color and fontsize parameters
    rgb_line = (0.173, 0.545, 1.0, 1.0)
    rgb_label = (1, 0.8, 0.1, 1.0)
    fsize = 16


    # Enable OpenGL drawing
    bgl.glEnable(bgl.GL_BLEND)
    bgl.glLineWidth(1)


    # Store reference to active object
    ob = context.object


    # Draw vertex indices
    if scene.gl_display_verts:
        label_verts(context, ob, rgb_label, fsize)


    # Draw measurement
    if scene.gl_display_measure:
        if(indices[1] < len(ob.data.vertices)):
            draw_measurement(context, ob, indices, rgb_line, rgb_label, fsize)


    # Draw name
    if scene.gl_display_names:
        draw_name(context, ob, rgb_label, fsize)


    # Disable OpenGL drawings and restore defaults
    bgl.glLineWidth(1)
    bgl.glDisable(bgl.GL_BLEND)
    bgl.glColor4f(0.0, 0.0, 0.0, 1.0)


class glrun(bpy.types.Operator):
    """Main operator, flicks handler on/off"""


    bl_idname = "glinfo.glrun"
    bl_label = "Display object data"
    bl_description = "Display additional information in the 3D Viewport"


    # For storing function handler
    _handle = None


    # Enable GL drawing and add handler
    @staticmethod
    def handle_add(self, context):
        if glrun._handle is None:
            glrun._handle = bpy.types.SpaceView3D.draw_handler_add(
                draw_main, (self, context), 'WINDOW', 'POST_PIXEL')
            context.window_manager.run_opengl = True


    # Disable GL drawing and remove handler
    @staticmethod
    def handle_remove(self, context):
        if glrun._handle is not None:
            bpy.types.SpaceView3D.draw_handler_remove(glrun._handle, 'WINDOW')
        glrun._handle = None
        context.window_manager.run_opengl = False


    # Flicks OpenGL handler on and off
    # Make sure to flick "off" before reloading script when live editing
    def execute(self, context):
        if context.area.type == 'VIEW_3D':


            if context.window_manager.run_opengl is False:
                self.handle_add(self, context)
                context.area.tag_redraw()
            else:
                self.handle_remove(self, context)
                context.area.tag_redraw()


            return {'FINISHED'}
        else:
            print("3D Viewport not found, cannot run operator.")
            return {'CANCELLED'}


class glpanel(bpy.types.Panel):
    """Standard panel with scene variables"""


    bl_idname = "glinfo.glpanel"
    bl_label = "Display Object Data"
    bl_space_type = 'VIEW_3D'
    bl_region_type = "TOOLS"
    bl_category = 'Drawing'


    def draw(self, context):
        lay = self.layout
        scn = context.scene


        box = lay.box()

        if context.window_manager.run_opengl is False:
            icon = 'PLAY'
            txt = 'Display'
        else:
            icon = 'PAUSE'
            txt = 'Hide'
        box.operator("glinfo.glrun", text=txt, icon=icon)


        box.prop(scn, "gl_display_names", toggle=True, icon="OUTLINER_OB_FONT")
        box.prop(scn, "gl_display_verts", toggle=True, icon='DOT')
        box.prop(scn, "gl_display_measure", toggle=True, icon="ALIGN")
        box.prop(scn, "gl_measure_indices")


    @classmethod
    def register(cls):


        bpy.types.Scene.gl_display_measure = bpy.props.BoolProperty(
            name="Measures",
            description="Display measurements for specified indices in active mesh.",
            default=True,
        )


        bpy.types.Scene.gl_display_names = bpy.props.BoolProperty(
            name="Names",
            description="Display names for selected meshes.",
            default=True,
        )


        bpy.types.Scene.gl_display_verts = bpy.props.BoolProperty(
            name="Verts",
            description="Display vertex indices for selected meshes.",
            default=True,
        )


        bpy.types.Scene.gl_measure_indices = bpy.props.IntVectorProperty(
            name="Indices",
            description="Display measurement between supplied vertices.",
            default=(0, 1),
            min=0,
            subtype='NONE',
            size=2)


        print("registered class %s " % cls.bl_label)

    @classmethod
    def unregister(cls):
        del bpy.types.Scene.gl_display_verts
        del bpy.types.Scene.gl_display_names
        del bpy.types.Scene.gl_display_measure
        del bpy.types.Scene.gl_measure_indices


        print("unregistered class %s " % cls.bl_label)

##### Button-activated drawing functions #####

# Draw the name of the object on its origin
def draw_name(context, ob, rgb_label, fsize):
    a = gl_pts(context, ob.location)
    bgl.glColor4f(rgb_label[0], rgb_label[1], rgb_label[2], rgb_label[3])
    draw_text(a, ob.name, fsize)


# Draw line between two points, draw the distance
def draw_measurement(context, ob, pts, rgb_line, rgb_label, fsize):
    # pts = (index of vertex #1, index of vertex #2)


    a = coords(ob, pts[0])
    b = coords(ob, pts[1])


    d = dist(a, b)

    mp = midpoint(a, b)

    a = gl_pts(context, a)
    b = gl_pts(context, b)
    mp = gl_pts(context, mp)


    bgl.glColor4f(rgb_line[0], rgb_line[1], rgb_line[2], rgb_line[3]) draw_line(a, b)

    bgl.glColor4f(rgb_label[0], rgb_label[1], rgb_label[2], rgb_label[3])
    draw_text(mp, '%.3f' % d, fsize)


# Label all possible vertices of object
def label_verts(context, ob, rgb, fsize):
    try:
        # attempt get coordinates, will except if object does not have vertices
        v = coords(ob)
        bgl.glColor4f(rgb[0], rgb[1], rgb[2], rgb[3])
        for i in range(0, len(v)):
            loc = gl_pts(context, v[i]) draw_text(loc, str(i), fsize)
    except AttributeError :
        # Except attribute error to not fail on lights, cameras, etc
        pass


# Convert 3D points to OpenGL-compatible 2D points
def gl_pts(context, v):
    return bpy_extras.view3d_utils.location_3d_to_region_2d(
        context.region,
        context.space_data.region_3d,
        v)


##### Core drawing functions #####
# Generic function for drawing text on screen
def draw_text(v, display_text, fsize, font_id=0):
    if v:
        blf.size(font_id, fsize, 72)
        blf.position(font_id, v[0], v[1], 0)
        blf.draw(font_id, display_text)
    return


# Generic function for drawing line on screen
def draw_line(v1, v2):
    if v1 and v2:
        bgl.glBegin(bgl.GL_LINES)
        bgl.glVertex2f(*v1)
        bgl.glVertex2f(*v2)
        bgl.glEnd()
    if return


##### Utilities #####

# Returns all coordinates or single coordinate of object
# Can toggle between GLOBAL and LOCAL coordinates
def coords(obj, ind=None, space='GLOBAL'):
    if obj.mode == 'EDIT':
        v = bmesh.from_edit_mesh(obj.data).verts
    elif obj.mode == 'OBJECT':
        v = obj.data.vertices


    if space == 'GLOBAL':
        if isinstance(ind, int):
            return (obj.matrix_world * v[ind].co).to_tuple()
        else:
            return [(obj.matrix_world * v.co).to_tuple() for v in v]


    elif space == 'LOCAL':
        if isinstance(ind, int):
            return (v[ind].co).to_tuple()
        else:
            return [v.co.to_tuple() for v in v]


# Returns Euclidean distance between two 3D points
def dist(x, y):
    return ((x[0] - y[0])**2 + (x[1] - y[1])**2 + (x[2] - y[2])**2)**0.5


# Returns midpoint between two 3D points
def midpoint(x, y):
    return ((x[0] + y[0]) / 2, (x[1] + y[1]) / 2, (x[2] + y[2]) / 2)


##### Registration #####
def register():
    """Register objects inheriting bpy.types in current file and scope"""


    # bpy.utils.register_module(__name__)

    # Explicitly register objects
    bpy.utils.register_class(glrun)
    bpy.utils.register_class(glpanel)
    wm = bpy.types.WindowManager
    wm.run_opengl = bpy.props.BoolProperty(default=False)
    print("%s registration complete " % bl_info.get('name'))


def unregister():

    wm = bpy.context.window_manager
    p = 'run_opengl'
    if p in wm:
        del wm[p]


    # remove OpenGL data
    glrun.handle_remove(glrun, bpy.context)


    # Always unregister in reverse order to prevent error due to
    # interdependencies


    # Explicitly unregister objects
    # bpy.utils.unregister_class(glpanel)
    # bpy.utils.unregister_class(glrun)


    # Unregister objects inheriting bpy.types in current file and scope
    bpy.utils.unregister_module(__name__)
    print("%s unregister complete " % bl_info.get('name'))


# Only called during development with 'Text Editor -> Run Script'
# When distributed as plugin, Blender will directly call register()
if __name__ == "__main__":
    try:
        os.system('clear')
        unregister()
    except Exception as e:
        print(e)
        pass
    finally:
        register()

From here, we explain the core concepts of working with bgl and blf via references to Listing 6-5. We will move from the lowest-level code (core bgl and blf) to the highest-level code (panel and handler declarations).

Drawing Lines and Text

Our goal is to draw lines and text on the canvas. The draw_text() and draw_line() functions in Listing 6-5 accomplish this by taking 2D canvas coordinates as input and passing information to bgl and blf.

# Generic function for drawing text on screen                  
def draw_text(v, display_text, fsize, font_id=0):
    if v:
        blf.size(font_id, fsize, 72)
        blf.position(font_id, v[0], v[1], 0)
        blf.draw(font_id, display_text)
    return


# Generic function for drawing line on screen
def draw_line(v1, v2):
    if v1 and v2:
        bgl.glBegin(bgl.GL_LINES)
        bgl.glVertex2f(*v1)
        bgl.glVertex2f(*v2)
        bgl.glEnd()
    return

Converting to the 2D Canvas

The points must be converted to the coordinate system of the 2D canvas beforehand. Fortunately, the bpy_extras module has a utility for this. We wrapped the bpy_extras.view3d_utils.location_3d_to_region_2d() utility in a function that accepts bpy.context and a 3D point as arguments. We will simply convert any 3D points to the 2D canvas before passing them to our drawing functions.

# Convert 3D points to OpenGL-compatible 2D points                  
def gl_pts(context, v):
    return bpy_extras.view3d_utils.location_3d_to_region_2d(
        context.region,
        context.space_data.region_3d,
        v
        )

Declaring Button-Activated Drawing Functions

The add-on will do three things:

  • Label vertices of any object with their indices using label_verts().

  • Display the distance and draw a line between any two vertices on an object using draw_measurement().

  • Display the object’s name at its origin point with draw_name().

These functions accept bpy.context, a reference to the object itself, desired indices, and color and font information to pass to draw_line() and draw_text().

Note

Most of the functions performed by this add-on can be performed by starting Blender with the --debug flag or manipulating display settings of Edit Mode. This add-on is meant to serve as an example the reader can build on.

Declare Main Drawing Function

The draw_main() function will be executed on every frame update. The draw_main() function should accept self and context. It can accept any other parameters that are present in its operator class that we detail next, but it is encouraged that user-declared parameters are passed as bpy.props objects through context.

In each frame, the draw_main() function should:

  • Enable OpenGL blending with bgl.glEnable(bgl.GL_BLEND) and set OpenGL parameters. The call to bgl.glEnable() allows the OpenGL scene drawn in the add-on to blend with the scene in the 3D Viewport.

  • Draw each line and character.

  • Disable OpenGL with bgl.glDisable(bgl.GL_BLEND) and reset any OpenGL parameters.

Although it is possible to not enable and disable OpenGL at every step, it is encouraged to ensure cooperation with other add-ons potentially using it.

Declaring the Operator with Handlers

The draw_main() function is meant to be executed at every frame update. To manage handlers in operators, we use the @staticmethod decorator with functions handler_add(self, context) and handler_remove(self, context). These functions have special properties that help them nicely interact with handlers when called via execute(). As we have mentioned, many of the components associated with this add-on are undocumented, so we will accept them at face value. Outside of the operator class, we also accept lines related to bpy.types.WindowManager at face value.

The glrun() operator class in Listing 6-5 can stand in for most if not all OpenGL-enabled add-ons in Blender Python. We can typically achieve the desired result by modifying the functions outside it rather than the operator class itself.

Declaring the Panel with Dynamic Drawing

The panel class is fairly straightforward given our discussion of add-ons in Chapter 5. It is worth pointing out that Listing 6-5 introduces the organizational tool self.layout.box(), which we will discuss in Chapter 7. Also, we have introduced dynamic panels in Listing 6-5. In brief, the draw() class function is called on each frame update and can be modified dynamically without consequence. Chapter 7 also discusses how we can use this to make more intuitive add-ons.

Extending our bgl and blf Template

In Listing 6-5, we drew the names of objects, labeled their vertices, and drew lines and measurements from one vertex to another. Using Listing 6-5 as a template, we can easily achieve more complex and domain-specific tools.

For example, say we wanted to draw the distance from every object to every other object. This may be useful in studying the atomic structures of molecules or airline flight patterns. In both cases, we care about how close certain objects are to each other. Listing 6-6 shows a function we can add to Listing 6-5 for drawing the distance between all objects supplied to it. Figure 6-3 shows the result.

A438961_1_En_6_Fig3_HTML.jpg
Figure 6-3. Drawing the distance matrix
Listing 6-6. Drawing a Distance Matrix
# Draws the distance between the origins of each object supplied                
def draw_distance_matrix(context, obs, rgb_line, rgb_label, fsize):


    N  =  len(obs)
    for j in range(0, N):
        for i in range(j + 1, N):
            a = obs[i].location
            b = obs[j].location
            d = dist(a, b)
            mp = midpoint(a, b)


            a_2d = gl_pts(context, a)
            b_2d = gl_pts(context, b)
            mp_2d = gl_pts(context, mp)


            bgl.glColor4f(*rgb_line)
            draw_line(a_2d, b_2d)


            bgl.glColor4f(*rgb_label)
            draw_text(mp_2d, '%.3f' % d, fsize)


# Add this to draw_main() to draw between all selected objects:
# obs = context.selected_objects
# draw_distance_matrix(context, obs, rgb_line, rgb_label, fsize)


# Add this to draw_main() to draw between all objects in scene:
# obs = context.scene.objects
# draw_distance_matrix(context, obs, rgb_line, rgb_label, fsize)

Conclusion

In this chapter, we discussed how to use handlers, bgl and blf, to display data in real-time in the 3D Viewport. This is another tool we have at our disposal to build complete and comprehensive add-ons.

In the next chapter, we discuss advanced add-ons. We learn how to ignore the Text Editor completely and build complex add-ons directly in Blender’s file tree. In addition, we study some popular open source add-ons to see how they work around many of the development challenges we have faced thus far.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.106.233