© Chris Conlan 2017

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

3. The bmesh Module

Chris Conlan

(1)Bethesda, Maryland, USA

So far, we have talked about ways to create, manage, and transform whole objects. Blender’s default mode is Object Mode, which allows us to select and manipulate one or many objects, typically with transformations that can be appropriately applied to groups of disparate objects, such as rotation and translation.

Blender begins to shine as a 3D art suite when we enter Edit Mode. This mode allows us to select one or many vertices of a single object to perform advanced and detailed transformations. As one would expect, most operations that are intended for Edit Mode cannot be performed in Object Mode and vice versa.

The bmesh module deals almost exclusively in Edit Mode operations. Thus, we will give a proper treatment of the differences between Object Mode and Edit Mode before diving into the functionalities of bmesh.

Edit Mode

To manually enter Edit Mode as a traditional Blender 3D artist would, go to 3D Viewport Header ➤ Interaction Mode Menu ➤ Edit Mode, as pictured in Figure 3-1. Use the same menu for switching back into Object Mode.

A438961_1_En_3_Fig1_HTML.jpg
Figure 3-1. Toggling Between Edit and Object Mode

When switching into Edit Mode, the activated object at that time will be the only object the user can edit for that session of Edit Mode. If the user want to manipulate a different object in Edit Mode, he must switch back to Object Mode to activate the desired object first. Only then, after switching back into Edit Mode with the desired object activated, will he be able to manipulate it. Refer to the section “Selection, Activation, and Specification” in Chapter 2 if the verbiage regarding selection and activation is unclear at this point. Remember that we can always run bpy.context.object in the Interactive Console to check the name of the activated object.

To programmatically switch between Object Mode and Edit Mode, use the two commands in Listing 3-1.

Listing 3-1. Switching Between Object and Edit Mode
# Set mode to Edit Mode
bpy.ops.object.mode_set(mode="EDIT")
# Set mode to Object Mode
bpy.ops.object.mode_set(mode="OBJECT")

Selecting Vertices, Edges, and Planes

To begin manipulating details of single objects, we must be able to select specific parts. We will wrap our mode-setting functions in our ut.py module, then discuss how bmesh is used to select specific parts of an object. In doing so, we will work through a few quirks and version compatibility pitfalls of bmesh and the vertex indexing protocol in Blender.

Switching Between Edit and Object Modes Consistently

Listing 3-2 implements a wrapper function for switching between Object Mode and Edit Mode. We will insert this in the ut.py toolkit we began building in Chapter 2. The only modification we have made to the vanilla bpy.ops method is to deselect all vertices, edges, and planes of the active object when we enter Edit Mode. Currently, Blender’s protocol for determining which parts of the object are selected upon entry in Edit Mode is opaque and unwieldy. We will take the safest and most consistent approach by deselecting every part of the object whenever we enter Edit Mode.

When we enter Object Mode from Edit Mode, Blender simply restores the active and selected objects from when we first entered Edit Mode. This behavior is reliable and understandable, so we will not modify the standard behavior of bpy.ops.object.mode_set(mode = "OBJECT").

Listing 3-2. Wrapper Function for Switching Between Object and Edit Mode
# Place in ut.py

# Function for entering Edit Mode with no vertices selected,
# or entering Object Mode with no additional processes


def mode(mode_name):
    bpy.ops.object.mode_set(mode=mode_name)
    if mode_name == "EDIT":
        bpy.ops.mesh.select_all(action="DESELECT")
Note

If you’re editing a custom module like ut.py multiple times in the same Blender session, make sure to call importlib.reload(ut) on the module to see import the un-cached version into Blender. See Listing 3-3 for an example.

Listing 3-3. Editing Custom Modules, Live Within a Blender Session
# Will use the cached version of ut.py from                  
# your first import of the Blender session
import ut ut.create.cube('myCube')


# Will reload the module from the live script of ut.py
# and create a new cached version for the session
import importlib importlib.reload(ut) ut.create.cube('myCube')


# This is what the header of your main script
# should look like when editing custom modules
import ut
import importlib importlib.reload(ut)


# Code using ut.py ...

Instantiating a bmesh Object

In Blender, bmesh objects are fairly heavy-handed and computationally expensive when compared to other core data structures. To maintain efficiency, Blender gives much of the data and instance management work to the user to manage via the API. We will continue to see examples of this as we explore the bmesh module. See Listing 3-4 for an example of instantiating a bmesh object. In general, instantiating a bmesh object requires us to pass a bpy.data.meshes datablock to bmesh.from_edit_mesh() while in Edit Mode.

Listing 3-4. Instantiating a bmesh Object
import bpy import bmesh

# Must start in object mode
# Script will fail if scene is empty
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()


# Create a cube and enter Edit Mode
bpy.ops.mesh.primitive_cube_add(radius=1, location=(0, 0, 0))
bpy.ops.object.mode_set(mode='EDIT')


# Store a reference to the mesh datablock
mesh_datablock = bpy.context.object.data


# Create the bmesh object (named bm) to operate on
bm = bmesh.from_edit_mesh(mesh_datablock)


# Print the bmesh object
print(bm)

If we try running these commands in the Interactive Console, we may get a different result. Instances of bmesh objects are not persistent. Unless Blender detects that it is being actively used, the bmesh object will dereference the mesh datablock, garbage collect internal data, and return <BMesh dead at some_memory_address>. This is a desirable behavior given the space and compute power required to maintain a bmesh object, but it does require programmers to execute extra commands to keep it alive. We will encounter these commands as we build functions for selecting specific parts of 3D objects.

Selecting Parts of a 3D Object

To select parts of a bmesh object, we manipulate the select Booleans of each BMesh.verts, BMesh.edges, and BMesh.faces object. Listing 3-5 gives an example of selecting parts of a cube.

Notice the numerous calls to ensure_lookup_table() in Listing 3-5. We use these functions to remind Blender to keep certain parts of the BMesh object from being garbage-collected between operations. These functions take up minimal processing power, so we can call them liberally without much consequence. It is better to over-call them than to under-call them, because debugging this error:

ReferenceError: BMesh data of type BMesh has been removed

Can be nightmarish in large codebases with no protocol for ensure_lookup_table().

Listing 3-5. Selecting Parts of 3D Objects
import bpy
import bmesh


# Must start in object mode
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()


# Create a cube and enter Edit Mode
bpy.ops.mesh.primitive_cube_add(radius=1, location=(0, 0, 0))
bpy.ops.object.mode_set(mode='EDIT')


# Set to "Face Mode" for easier visualization
bpy.ops.mesh.select_mode(type = "FACE")


# Register bmesh object and select various parts
bm = bmesh.from_edit_mesh(bpy.context.object.data)


# Deselect all verts, edges, faces
bpy.ops.mesh.select_all(action="DESELECT")


# Select a face
bm.faces.ensure_lookup_table()
bm.faces[0].select = True


# Select an edge
bm.edges.ensure_lookup_table()
bm.edges[7].select = True


# Select a vertex
bm.verts.ensure_lookup_table()
bm.verts[5].select = True

Readers will notice that we run bpy.ops.mesh.select_mode(type = "FACE"). This concept has not been covered up to this point but is important to understand to properly use advanced Edit Mode functions. Typically, Blender artists click one of the three options in 3D Viewport Header, as shown in Figure 3-2. The buttons in Figure 3-2 correspond to the VERT, EDGE, and FACE arguments in bpy.ops.mesh.select_mode(). Right now, this will only affect how we visualize selections in Edit Mode. We select FACE for this example because it is the best mode for visualizing all three types simultaneously. Later in the chapter, we will discuss some functions in Edit Mode whose behavior will change depending on this selection.

A438961_1_En_3_Fig2_HTML.jpg
Figure 3-2. Toggling various selection modes

Edit Mode Transformations

This section discusses simple transformations like translation and rotation in Edit Mode, as well as advanced transformations like randomization, extrusion, and subdivision.

Basic Transformations

Conveniently enough, we can use the same functions we used for Object Mode transformations in Chapter 2 to operate on individual parts of a 3D object. We will give some examples Listing 3-6 using the bpy.ops submodule introduced in Listing 2-9. See Figure 3-3 for output of slightly deformed cubes.

A438961_1_En_3_Fig3_HTML.jpg
Figure 3-3. Deforming cubes with edit mode operations
Listing 3-6. Basic Transformations in Edit Mode
import bpy
import bmesh


# Must start in object mode
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()


# Create a cube and rotate a face around the y-axis
bpy.ops.mesh.primitive_cube_add(radius=0.5, location=(-3, 0, 0)) bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action="DESELECT")


# Set to face mode for transformations
bpy.ops.mesh.select_mode(type = "FACE")


bm = bmesh.from_edit_mesh(bpy.context.object.data)
bm.faces.ensure_lookup_table()
bm.faces[1].select = True
bpy.ops.transform.rotate(value = 0.3, axis = (0, 1, 0))


bpy.ops.object.mode_set(mode='OBJECT')

# Create a cube and pull an edge along the y-axis
bpy.ops.mesh.primitive_cube_add(radius=0.5, location=(0, 0, 0)) bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action="DESELECT")


bm = bmesh.from_edit_mesh(bpy.context.object.data)
bm.edges.ensure_lookup_table()
bm.edges[4].select = True
bpy.ops.transform.translate(value = (0, 0.5, 0))


bpy.ops.object.mode_set(mode='OBJECT')

# Create a cube and pull a vertex 1 unit
# along the y and z axes
# Create a cube and pull an edge along the y-axis
bpy.ops.mesh.primitive_cube_add(radius=0.5, location=(3, 0, 0)) bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action="DESELECT")


bm = bmesh.from_edit_mesh(bpy.context.object.data)
bm.verts.ensure_lookup_table()
bm.verts[3].select = True
bpy.ops.transform.translate(value = (0, 1, 1))


bpy.ops.object.mode_set(mode='OBJECT')

Advanced Transformations

We could not hope to cover all of the tools included in Blender for editing meshes, so we will cover a handful in this section and flush out more using examples at the end of the chapter. Listing 3-7 implements the extrude, subdivide, and randomize operators . See Figure 3-4 for the intended output.

A438961_1_En_3_Fig4_HTML.jpg
Figure 3-4. Extrude, Subdivide, and Randomize Operators
Listing 3-7. Extrude, Subdivide, and Randomize Operators
import bpy import bmesh

# Will fail if scene is empty
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()


# Create a cube and extrude the top face away from it bpy.ops.mesh.primitive_cube_add(radius=0.5, location=(-3, 0, 0)) bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action="DESELECT")


# Set to face mode for transformations
bpy.ops.mesh.select_mode(type = "FACE")


bm = bmesh.from_edit_mesh(bpy.context.object.data)
bm.faces.ensure_lookup_table()
bm.faces[5].select = True
bpy.ops.mesh.extrude_region_move(TRANSFORM_OT_translate =
      {"value": (0.3, 0.3, 0.3),
       "constraint_axis": (True, True, True),
       "constraint_orientation" :'NORMAL'})


bpy.ops.object.mode_set(mode='OBJECT')

# Create a cube and subdivide the top face
bpy.ops.mesh.primitive_cube_add(radius=0.5, location=(0, 0, 0))
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action="DESELECT")


bm = bmesh.from_edit_mesh(bpy.context.object.data)
bm.faces.ensure_lookup_table()
bm.faces[5].select = True
bpy.ops.mesh.subdivide(number_cuts = 1)


bpy.ops.mesh.select_all(action="DESELECT")
bm.faces.ensure_lookup_table()
bm.faces[5].select = True
bm.faces[7].select = True
bpy.ops.transform.translate(value = (0, 0, 0.5))
bpy.ops.object.mode_set(mode='OBJECT')


# Create a cube and add a random offset to each vertex
bpy.ops.mesh.primitive_cube_add(radius=0.5, location=(3, 0, 0))
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.transform.vertex_random(offset = 0.5)


bpy.ops.object.mode_set(mode='OBJECT')

Note on Indexing and Cross-Compatibility

Readers may have noticed that the indices of vertices, edges, and faces in 3D objects are arranged in no particular order. In the example scripts thus far, the author had manually located the indices in advance rather than discover them programmatically. For example, when manipulating the tops of cubes in Listing 3-7, the author determined in advance that ut.act.select_face(bm, 5) would select the face on the top side of the cube. This was determined through trial-and-error testing.

Using trial-and-error tests to discover the index number of a part of an object is an acceptable practice in general, but suffers from a number of disadvantages. Within any given version of Blender, indexing semantics should be considered replicable but untamable.

  • Default indices of objects vary wildly across different versions of Blender. The author has noted major compatibility issues in add-ons relying on hardcoded indices across different versions of Blender. Major differences were noted between version 2.77 and version 2.78 in add-ons relying on hardcoded indices.

  • Behavior of indexing after certain transformations is very unwieldy. See Figure 3-5 for an example of the vertex indices of a default plane, a plane after three insets, and a plain after two subdivisions. The indices in these planes conform to no particular logical pattern. Variance among transformations is another source of cross-version incompatibility.

    A438961_1_En_3_Fig5_HTML.jpg
    Figure 3-5. Default, inset, and subdivided planes with vertex indices labeled
  • Add-ons using hardcoded indices are very limited in user-interaction possibilities. An add-on that uses hardcoded indices can run successively, but can very rarely if ever engage in back-and-forth interaction with the user .

The workaround to this issue is selection by characteristic. To select a vertex by a characteristic, we loop through each vertex in the object and run bm.verts[i].select = True on vertices that meet a criteria. The same holds for edges and faces. On paper, this method looks very computationally expensive and algorithmically complex, but you will find it is surprisingly fast and modular. Plugins that use pure selection by characteristic can often run successfully on many versions of Blender simultaneously. Unfortunately, implementing this opens up a conceptual can of worms in Blender regarding local and global coordinate systems. We flush this out as well in the next section.

Global and Local Coordinates

Blender stores many sets of coordinate data for each part of each object. In most cases, we will only be concerned with two sets of coordinates: global coordinates G and local coordinates L. When we perform transformations on objects, Blender stores these transformations as part of a transformation matrix, T. Blender will, at some point, apply the transformation matrix to the local coordinates. After Blender applies the transformation matrix, the local coordinates will be equal to the global coordinates, and the transformation matrix will be the identity matrix.

Within the 3D Viewport, we view global coordinates G = T * L always.

We can control when Blender applies transformations with bpy.ops.object.transform_apply(). This will not change the appearance of the objects, rather it will set L equal to G and set T equal to the identity.

We can use this to our advantage to easily select specific parts of objects. If we delay execution of bpy.ops.object.transform_apply() by not running it and not exiting Edit Mode, we can maintain two data sets G and L. In practice, G is very useful for positioning objects relative to others, and L is very easy to loop through to fetch indices.

See Listing 3-8 for functions to access global and local coordinates of an object. Given the bpy.data.meshes[].vertices datablock as v, v.co gives the local coordinates and bpy.data.objects[].matrix_world * v.co gives the global coordinates. Thankfully, this datablock can be accessed in both Object Mode and Edit Mode. We will build mode-independent functions for accessing these coordinates. See Listing 3-8 for functions that fetch each set of coordinates independent of the mode.

These functions sacrifice some clarity in exchange for brevity and efficiency. In this code, v is a list of tuples that represents our matrix L, and obj.matrix_world is a Python matrix that represents our transformation matrix T.

Listing 3-8. Fetching Global and Local Coordinates
def coords(objName, space='GLOBAL'):

     # Store reference to the bpy.data.objects datablock
     obj = bpy.data.objects[objName]


     # Store reference to bpy.data.objects[].meshes datablock
     if obj.mode == 'EDIT':
         v = bmesh.from_edit_mesh(obj.data).verts
     elif obj.mode == 'OBJECT':
         v = obj.data.vertices


     if space == 'GLOBAL':
          # Return T * L as list of tuples
          return [(obj.matrix_world * v.co).to_tuple() for v in v]
     elif space == 'LOCAL':
          # Return L as list of tuples
          return [v.co.to_tuple() for v in v]


class sel:

     # Add this to the ut.sel class, for use in object mode
     def transform_apply():
         bpy.ops.object.transform_apply(
             location=True, rotation=True, scale=True)

See Listing 3-9 for an example of the behavior of local and global coordinates. We print the first two coordinate triples of a cube before transformation, immediately after transformation, and after transform_apply(). This makes sense on paper and in the code editor. Running Listing 3-9 in the Interactive Console line-by-line highlights the interesting behavior of transform_apply(). After translating the cube, readers will see the cube move, but the local coordinates will remain the same. After running transform_apply(), the cube will not move, but the local coordinates will update to match the global coordinates.

Listing 3-9. Behavior of Global and Local Coordinates and Transform Apply
import ut
import importlib
importlib.reload(ut)


import bpy

# Will fail if scene is empty
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()


bpy.ops.mesh.primitive_cube_add(radius=0.5, location=(0, 0, 0))
bpy.context.object.name = 'Cube-1'


# Check global and local coordinates
print(' Before transform:')
print('Global:', ut.coords('Cube-1', 'GLOBAL')[0:2])
print('Local: ', ut.coords('Cube-1', 'LOCAL')[0:2])


# Translate it along x = y = z
# See the cube move in the 3D viewport
bpy.ops.transform.translate(value = (3, 3, 3))


# Check global and local coordinates
print(' After transform, unapplied:')
print('Global: ', ut.coords('Cube-1', 'GLOBAL')[0:2])
print('Local: ', ut.coords('Cube-1', 'LOCAL')[0:2])


# Apply transformation
# Nothing changes in 3D viewport
ut.sel.transform_apply()


# Check global and local coordinates
print(' After transform, applied:')
print('Global: ', ut.coords('Cube-1', 'GLOBAL')[0:2])
print('Local: ', ut.coords('Cube-1', 'LOCAL')[0:2])


############################ Output ###########################
# Before transform:
# Global: [(-0.5, -0.5, -0.5), (-0.5, -0.5, 0.5)]
# Local: [(-0.5, -0.5, -0.5), (-0.5, -0.5, 0.5)]
#
# After transform, unapplied:
# Global: [(2.5, 2.5, 2.5), (2.5, 2.5, 3.5)]
# Local: [(-0.5, -0.5, -0.5), (-0.5, -0.5, 0.5)]
#
# After transform, applied:
# Global: [(2.5, 2.5, 2.5), (2.5, 2.5, 3.5)]
# Local: [(2.5, 2.5, 2.5), (2.5, 2.5, 3.5)]
###############################################################

In the next section, we use this concept to combat the issue presented in Figure 3-5 and unlock the full power of Edit Mode in Blender.

Selecting Vertices, Edges, and Faces by Location

See Listing 3-10 for two functions that work together to facilitate selection of vertices, edges, and faces per their location in global and local coordinate systems. The function we specify as ut.act.select_by_loc() looks and is very complex, but does not use any Blender concepts that we have not introduced up to this point. The author believes this function should be included as part of the bmesh module because it is so widely applicable.

Listing 3-10. Function for Selecting Pieces of Objects by Location
# Add in body of script, outside any class declarations                
def in_bbox(lbound, ubound, v, buffer=0.0001):
    return lbound[0] - buffer <= v[0] <= ubound[0] + buffer and
        lbound[1] - buffer <= v[1] <= ubound[1] + buffer and
        lbound[2] - buffer <= v[2] <= ubound[2] + buffer


class act:

    # Add to ut.act class
    def select_by_loc(lbound=(0, 0, 0), ubound=(0, 0, 0),
                      select_mode='VERT', coords='GLOBAL'):


    # Set selection mode, VERT, EDGE, or FACE
    selection_mode(select_mode)


    # Grab the transformation matrix
    world = bpy.context.object.matrix_world


    # Instantiate a bmesh object and ensure lookup table
    # Running bm.faces.ensure_lookup_table() works for all parts
    bm = bmesh.from_edit_mesh(bpy.context.object.data)
    bm.faces.ensure_lookup_table()


    # Initialize list of vertices and list of parts to be selected
    verts = []
    to_select = []
    # For VERT, EDGE, or FACE ...
    # 1. Grab list of global or local coordinates
    # 2. Test if the piece is entirely within the rectangular
    #    prism defined by lbound and ubound
    # 3. Select each piece that returned True and deselect
    #    each piece that returned False in Step 2


    if select_mode == 'VERT':
        if coords == 'GLOBAL':
            [verts.append((world * v.co).to_tuple()) for v in bm.verts]
        elif coords == 'LOCAL':
            [verts.append(v.co.to_tuple()) for v in bm.verts]


        [to_select.append(in_bbox(lbound, ubound, v)) for v in verts]
        for vertObj, select in zip(bm.verts, to_select):
            vertObj.select = select


    if select_mode == 'EDGE':
        if coords == 'GLOBAL':
            [verts.append([(world * v.co).to_tuple()
                              for v in e.verts]) for e in bm.edges]
        elif coords == 'LOCAL':
            [verts.append([v.co.to_tuple() for v in e.verts])
             for e in bm.edges]


        [to_select.append(all(in_bbox(lbound, ubound, v)
                              for v in e)) for e in verts]
        for edgeObj, select in zip(bm.edges, to_select):
            edgeObj.select = select


    if select_mode == 'FACE':
        if coords == 'GLOBAL':
            [verts.append([(world * v.co).to_tuple()
                           for v in f.verts]) for f in bm.faces]
        elif coords == 'LOCAL':
            [verts.append([v.co.to_tuple() for v in f.verts])
             for f in bm.faces]


        [to_select.append(all(in_bbox(lbound, ubound, v)
                              for v in f)) for f in verts]
        for faceObj, select in zip(bm.faces, to_select):
            faceObj.select = select

Listing 3-11 gives an example of using ut.act.select_by_loc() to select pieces of a sphere and transform them. Remember that the first two arguments to this function are the lowest corner and highest corner of a rectangular prism in the 3D. If the entire piece (vertex, edge, face) falls within the rectangular prism, it will be selected.

Listing 3-11. Selecting and Transforming Pieces of a Sphere
import ut
import importlib
importlib.reload(ut)


import bpy

# Will fail if scene is empty
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()


bpy.ops.mesh.primitive_uv_sphere_add(size=0.5, location=(0, 0, 0))
bpy.ops.transform.resize(value = (5, 5, 5))
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='DESELECT')


# Selects upper right quadrant of sphere
ut.act.select_by_loc((0, 0, 0), (1, 1, 1), 'VERT', 'LOCAL')


# Selects nothing
ut.act.select_by_loc((0, 0, 0), (1, 1, 1), 'VERT', 'GLOBAL')


# Selects upper right quadrant of sphere
ut.act.select_by_loc((0, 0, 0), (5, 5, 5), 'VERT', 'LOCAL')


# Mess with it
bpy.ops.transform.translate(value = (1, 1,1))
bpy.ops.transform.resize(value = (2, 2, 2))


# Selects lower half of sphere
ut.act.select_by_loc((-5, -5, -5), (5, 5, -0.5), 'EDGE', 'GLOBAL')


# Mess with it
bpy.ops.transform.translate(value = (0, 0, 3))
bpy.ops.transform.resize(value = (0.1, 0.1, 0.1))


bpy.ops.object.mode_set(mode='OBJECT')

Checkpoint and Examples

Up to this point we have made a lot of additions to ut.py. For an up-to-date version with all of the additions we have made thus far in the book, visit blender.chrisconlan.com/ut_ch03.py.

Given this version of ut.py, we will try some fun examples. See Listing 3-12 for a random shape growth algorithm. A brief algorithm randomly (and sloppily) selects a chunk of space in which the object resides, then extrudes the selected portion along the vertical normal of the selected surface. To extrude along the vertical normal of a surface, we simply run ut.act.extrude((0, 0, 1)), since this function uses the local orientation of the surface by default.

The algorithm lets us build both elegant and wacky shapes. The type of result is mostly dependent on which shape we supply in the ut.create call near the top of the script. See Figures 3-6 and 3-7 for examples of Listing 3-12 with a cube and sphere, respectively.

A438961_1_En_3_Fig6_HTML.jpg
Figure 3-6. Random cube extrusion with 500 iterations
A438961_1_En_3_Fig7_HTML.jpg
Figure 3-7. Random sphere extrusion with 1000 iterations
Listing 3-12. Random Shape Growth
import ut
import importlib importlib.reload(ut)
import bpy


from random import randint
from math import floor


# Must start in object mode
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()


# Create a cube
bpy.ops.mesh.primitive_cube_add(radius=0.5, location=(0, 0, 0))
bpy.context.object.name = 'Cube-1'


bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action="DESELECT")


for i in range(0, 100):

    # Grab the local coordinates
    coords = ut.coords('Cube-1', 'LOCAL')


    # Find the bounding box for the object
    lower_bbox = [floor(min([v[i] for v in coords])) for i in [0, 1, 2]]
    upper_bbox = [floor(max([v[i] for v in coords])) for i in [0, 1, 2]]


    # Select a random face 2x2x1 units wide, snapped to integer coordinates
    lower_sel = [randint(l, u) for l, u in zip(lower_bbox, upper_bbox)]
    upper_sel = [l + 2 for l in lower_sel]
    upper_sel[randint(0, 2)] -= 1


    ut.act.select_by_loc(lower_sel, upper_sel, 'FACE', 'LOCAL')

    # Extrude the surface along it aggregate vertical normal
    bpy.ops.mesh.extrude_region_move(TRANSFORM_OT_translate =
          {"value": (0, 0, 1),
           "constraint_axis": (True, True, True),
           "constraint_orientation" :'NORMAL'})

While these examples may seem trivial, they illustrate the power of automating Edit Mode operations in Blender. While the brief algorithm in Listing 3-12 can make fascinating shapes, the concepts within can be used to create entire CAD systems in Blender given the right domain-specific knowledge. Great examples include:

  • Models of commercial buildings

  • Models of mathematical surfaces

  • Atomic and chemical models

All of these can be achieved with the concepts discussed in this chapter. As it stands, our toolkit is not very case-specific. There are a lot of areas where it can be improved to accommodate modeling needs of different disciplines and applications. Notable ways to customize and improve our toolkit include :

  • Creating ut.act.select_by_loc() functions that support selection regions other than rectangular prisms. There is potential use for cylindrical, spherical, two-dimensional, and one-dimensional selection surfaces.

  • Creating additional ut.create functions and case-specific automated naming schema for them.

  • Adding additional edit mode operations to ut.act in the same way we have added ut.act.extrude and ut.act.subdivide. There is ample opportunity to explore and further parameterize these functions.

  • Adding LOCAL, NORMAL, and GIMBAL axis operations to ut.sel. Thus far, we have been using the default of GLOBAL. For example, translation, rotation, and scaling can all be performed along these axes.

Conclusion

In the next chapters, we talk about basic rendering concepts required for effective add-on development in Blender.

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

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