© Chris Conlan 2017

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

2. The bpy Module

Chris Conlan

(1)Bethesda, Maryland, USA

This chapter introduces and details major components of the bpy module. In doing so, we explain many important behaviors of Blender. We cover selection and activation, creation and deletion, scene management, and code abstraction.

The official documentation for the Blender Python API can be found by selecting a version of Blender at http://www.blender.org/api/ . We are using Blender 2.78c in this text, so our documentation can be found at http://www.blender.org/api/blender_python_api_2_78c_release/ .

Module Overview

We begin by giving some background on each submodule of bpy.

bpy.ops

As implied, this submodule contains operators. These are primarily functions for manipulating objects , similarly to the way Blender artists manipulate objects in the default interface. The submodule can also manipulate the 3D Viewport, renderings, text, and much more.

For manipulating 3D objects, the two most important classes are bpy.ops.object and bpy.ops.mesh. The object class contains functions for manipulating multiple selected objects at the same time as well as many general utilities. The mesh class contains functions for manipulating vertices, edges, and faces of objects one at a time, typically in Edit Mode.

There are currently 71 classes in the bpy.ops submodule, all fairly well-named and well-organized.

Note

Documentation for modules, submodules, and classes can be accessed directly by appending the Pythonic path to the object and .html to the home URL of your version’s Blender documentation. For example, documentation for the bpy.ops.mesh class can be found here: www.blender.org/api/blender_python_api_2_78c_release/bpy.ops.mesh.html .

bpy.context

The bpy. context submodule is used to access objects and areas of Blender by various status criteria. The primary function of this submodule is to give Python developers a means of accessing the current data that a user is working with. If we create a button that permutes all of the selected objects, we can allow the user to select the objects of his choice, then permute all objects in bpy.context.select_objects.

We make frequent use of bpy.context.scene when building add-ons, as it is a required input to certain Blender objects. We can also use bpy.context to access the active objects, toggle between Object Mode and Edit Mode, and accept data from a grease pencil.

bpy.data

This submodule is used to access Blender’s internal data . It can be difficult to interpret documentation on this specific module (the */bpy.data.html page points directly to a separate class), but we will rely heavily on it throughout this text. The bpy.data.objects class contains all of the data determining an object’s shape and position. When we say the the previous submodule bpy.context is great for pointing us to groups of objects, we mean that bpy.context classes will generate references to datablocks of the bpy.data class.

bpy.app

This submodule is not entirely documented, but the information we are confident about thus far can be used to great effect in scripting and add-on development. The sub-submodule bpy.app.handlers is the only one we will concern ourselves with in this text. The handlers submodule contains special functions for triggering custom functions in response to events in Blender. Most commonly used is the frame change handle, which executes some function every time the 3D Viewport is updated (i.e., after a frame change).

bpy.types , bpy.utils , and bpy.props

These modules are discussed in detail in later chapters on add-on development. Readers may presently find the documentation in */bpy.types.html useful for describing classes of objects we are utilizing elsewhere.

bpy.path

This submodule is essentially the same as the os.path submodule that ships natively with Python. It is rarely useful to Blender Python developers outside of the core development team.

Selection, Activation, and Specification

The Blender interface was designed to be intuitive while also providing complex functionality. Certain operations logically apply to single objects where others can logically be used on one or many objects at the same time. To handle these scenarios, Blender developers created three ways to access an object and its data.

  • Selection: One, many, or zero objects can be selected at once. Operations that use selected objects can perform that operation simultaneously on a single object or many objects.

  • Activation: Only a single object can be active at any given time. Operations that work on the active object are typically more specific and drastic, thus cannot be intuitively performed on many things at once.

  • Specification: (Python only) Python scripts can access objects by their names and write directly to their datablocks. While an operation that manipulates selected objects is typically a differential action like translate, rotate, or scale, writing data to specific objects is typically a declarative action like position, orientation, or size.

Selecting an Object

Before continuing, readers are encouraged to create a handful of different objects in the 3D Viewport to use as examples. Go to 3D Viewport Header ➤ Add to see the object creation menu.

When we click around in the 3D Viewport with a right-click, objects highlight and unhighlight. When we hold the Shift key and click around, we are able to highlight multiple objects at once. These highlights in the 3D Viewport represent the selected objects. To list the selected objects, type the code in Listing 2-1 into the Interactive Console.

Listing 2-1. Getting a List of Selected Objects
# Outputs bpy.data.objects datablocks                
bpy.context.selected_objects

As we alluded to earlier, the bpy.context submodule is great for fetching lists of objects based on their state within Blender. In this case, we fetched all the selected objects.

# Example output of Listing 2.1, list of bpy.data.objects datablocks                
[bpy.data.objects['Sphere'], bpy.data.objects['Circle'], bpy.data.objects['Cube']]

In this case, a sphere named Sphere, a circle named Circle, and a cube named Cube were all selected in the 3D Viewport. We were returned a Python list of bpy.data.objects datablocks. Given the knowledge that all datablocks of this type have a name value, we can loop through the results of Listing 2-1 to access the names of the selected objects. See Listing 2-2, where we grab both the names and positions of the selected objects.

Listing 2-2. Getting a List of Selected Objects
# Return the names of selected objects                  
[k.name for k in bpy.context.selected_objects]


# Return the locations of selected objects
# (location of origin assuming no pending transformations)
[k.location for k in bpy.context.selected_objects]

Now that we know how to manually select objects, we need to automatically select objects based on some criteria. The requisite functions are in bpy.ops. Listing 2-3 creates a function that takes an object name as an argument and selects it, clearing all other selections by default. If the user specifies additive = True, the function will not clear other selections beforehand.

Listing 2-3. Programmatically Selecting Objects
import bpy

def mySelector(objName, additive=False):

    # By default, clear other selections
    if not additive:
      bpy.ops.object.select_all(action='DESELECT')


    # Set the 'select' property of the datablock to True
    bpy.data.objects[objName].select = True


# Select only 'Cube'
mySelector('Cube')


# Select 'Sphere', keeping other selections
mySelector('Sphere', additive=True)


# Translate selected objects 1 unit along the x-axis
bpy.ops.transform.translate(value=(1, 0, 0))
Note

To easily view the names of objects without Python scripting, navigate to the Properties window and select the orange cube icon. Now, active objects will show their name near the top of this subwindow, as is the case in Figure 2-1. Also, the bottom-left corner of the 3D Viewport will display the name of the active object. We discuss activation in the next subsection of this chapter.

A438961_1_En_2_Fig1_HTML.jpg
Figure 2-1. Checking object names in the Blender interface

Activating an Object

Activation, like selection, is an object state in Blender. Unlike selection, only one object can be active at any given time. This state is generally used for vertex, edge, and face manipulation of single objects. This state also has a close relationship with Edit Mode, which we discuss in detail later in this chapter.

When we left-click around the 3D Viewport, any object that we click will be highlighted. When we highlight a single object in this manner, Blender both selects and activates that object. If we hold Shift and left-click around the 3D Viewport, only the first object we click will be active.

Note the area of the Properties window pictured in Figure 2-1, where the name of the active object is displayed. Objects can also be activated via the menu at the bottom of Figure 2-1.

To access the active object in Python, type Listing 2-4 in the Interactive Console. Notice there are two equivalent bpy.context classes for accessing the active object. Just as with selected objects, we are returned a bpy.data.objects datablock, which we can operate on directly.

Listing 2-4. Accessing the Active Object
# Returns bpy.data.objects datablock                  
bpy.context.object


# Longer synonym for the above line
bpy.context.active_object


# Accessing the 'name' and 'location' values of the datablock
bpy.context.object.name
bpy.context.object.location

Listing 2-5 is an analogue to Listing 2-3 for activation. Since only one object can be active at any given time, the activation function is much simpler. We pass a bpy.data.objects datablock to a scene property that handles internal data on activation. Because Blender only allows a single object to be active, we can make a single assignment to bpy.context.scene and allow Blender’s internal engine to sort out de-activation of other objects.

Listing 2-5. Programmatically Activating an Object
import bpy

def myActivator(objName):

  # Pass bpy.data.objects datablock to scene class
  bpy.context.scene.objects.active = bpy.data.objects[objName]


# Activate the object named 'Sphere'
myActivator('Sphere')


# Verify the 'Sphere' was activated
print("Active object:", bpy.context.object.name)


# Selected objects were unaffected
print("Selected objects:", bpy.context.selected_objects)
Note

When we introduce listings intended for use in the Text Editor rather than the Interactive Console (typically multi-line programs), we always import bpy. The bpy module is imported by default in the Interactive Console, but each run of a script in the Text Editor is an independent session that does not import bpy by default. Additionally, when we want to view output of a program in the Interactive Console, we will simply type the object we want to view information on. When we want to view output from the Text Editor, we use printing functions to send the output to the terminal with which Blender was opened. Otherwise, we would be unable to see output other than warnings and errors from the Text Editor scripts.

Specifying an Object (Accessing by Name)

This section details how to return bpy.data.objects datablocks by specifying the name of the object. Listing 2-6 shows how to access the bpy.data.objects datablock for an object given its name. Based on our discussion up to this point, Listing 2-6 may seem trivial. This circular nature of datablock referencing has a very important purpose.

Listing 2-6. Accessing an Object by Specification
# bpy.data.objects datablock for an object named 'Cube'                  
bpy.data.objects['Cube']


# bpy.data.objects datablock for an object named 'eyeballSphere'
bpy.data.objects['eyeballSphere']

Listing 2-7 is an analogue to Listings 2-3 and 2-5, but applies to specification. The goal of mySelector() and myActivator() were to return the datablock or datablocks of objects with a given state. In this case, mySpecifier() trivially returns the datablock.

Listing 2-7. Programmatically Accessing an Object by Specification
import bpy                                                

def mySpecifier(objName):
    # Return the datablock
    return bpy.data.objects[objName]


# Store a reference to the datablock
myCube = mySpecifier('Cube')


# Output the location of the origin
print(myCube.location)


# Works exactly the same as above
myCube = bpy.data.objects['Cube']
print(myCube.location)

Pseudo-Circular Referencing and Abstraction

The bpy.data.objects datablocks have a very interesting property that highlights many of the wise architecting decisions made for the Blender Python API. With the goal of promoting modularity, extensibility, and liberal abstraction, bpy.data.objects datablocks were built to nest infinitely. We refer to this as pseudo-circular referencing because, while references are circular, they occur within rather than between objects, making the concept distinct from circular referencing.

See Listing 2-8 for trivial examples of datablocks making pseudo-circular references.

Listing 2-8. Pseudo-Circular Referencing
# Each line will return the same object type and memory address                
bpy.data
bpy.data.objects.data
bpy.data.objects.data.objects.data
bpy.data.objects.data.objects.data.objects.data


# References to the same object can be made across datablock types
bpy.data.meshes.data
bpy.data.meshes.data.objects.data
bpy.data.meshes.data.objects.data.scenes.data.worlds.data.materials.data


# Different types of datablocks also nest
# Each of these lines returns the bpy.data.meshes datablock for 'Cube'
bpy.data.meshes['Cube']
bpy.data.objects['Cube'].data
bpy.data.objects['Cube'].data.vertices.data
bpy.data.objects['Cube'].data.vertices.data.edges.data.materials.data

Listing 2-8 showcases a powerful feature of the Blender Python API. When we append .data to an object, it returns a reference to the parent datablock. This behavior comes with some restrictions. For example, we cannot append .data.data to move from a bpy.data.meshes[] datablock to the bpy.data datablock. Nonetheless, this behavior will help us build clean and readable codebases that are naturally modular.

We will create tools in this text that enable us to build and manipulate objects in Blender without directly calling the bpy module. While pseudo-circular referencing seems trivial as we present it in Listing 2-8, readers will see that it often happens implicitly in toolkits when abstracting the bpy module.

Transformations with bpy

This section discusses major components of the bpy.ops.transorm class and its analogues elsewhere. It naturally expands on the theme of abstraction and introduces some helpful Blender Python tricks.

Listing 2-9 is a minimal set of tools for creating, selecting, and transforming objects. The bottom of the script runs some example transformations. Figure 2-2 shows the output from a test run of the minimal toolkit in the 3D Viewport.

A438961_1_En_2_Fig2_HTML.jpg
Figure 2-2. Minimal toolkit test
Listing 2-9. Minimal Toolkit for Creation and Transformation (ut.py)
import bpy

# Selecting objects by name
def select(objName):
    bpy.ops.object.select_all(action='DESELECT')
    bpy.data.objects[objName].select = True


# Activating objects by name
def activate(objName):
    bpy.context.scene.objects.active = bpy.data.objects[objName]


class sel:
    """Function Class for operating on SELECTED objects"""


    # Differential
    def translate(v):
        bpy.ops.transform.translate(
            value=v, constraint_axis=(True, True, True))


    # Differential
    def scale(v):
        bpy.ops.transform.resize(value=v, constraint_axis=(True, True, True))


    # Differential
    def rotate_x(v):
        bpy.ops.transform.rotate(value=v, axis=(1, 0, 0))


    # Differential
    def rotate_y(v):
        bpy.ops.transform.rotate(value=v, axis=(0, 1, 0))


    # Differential
    def rotate_z(v):
        bpy.ops.transform.rotate(value=v, axis=(0, 0, 1))


class act:
    """Function Class for operating on ACTIVE objects"""


    # Declarative
    def location(v):
        bpy.context.object.location = v


    # Declarative
    def scale(v):
        bpy.context.object.scale = v


    # Declarative
    def rotation(v):
        bpy.context.object.rotation_euler = v


    # Rename the active object
    def rename(objName):
        bpy.context.object.name = objName


class spec:
    """Function Class for operating on SPECIFIED objects"""


    # Declarative
    def scale(objName, v):
        bpy.data.objects[objName].scale = v


    # Declarative
    def location(objName, v):
        bpy.data.objects[objName].location = v


    # Declarative
    def rotation(objName, v):
        bpy.data.objects[objName].rotation_euler = v


class create:
    """Function Class for CREATING Objects"""


    def cube(objName):
        bpy.ops.mesh.primitive_cube_add(radius=0.5, location=(0, 0, 0))
        act.rename(objName)


    def sphere(objName):
        bpy.ops.mesh.primitive_uv_sphere_add(size=0.5, location=(0, 0, 0))
        act.rename(objName)


    def cone(objName):
        bpy.ops.mesh.primitive_cone_add(radius1=0.5, location=(0, 0, 0))
        act.rename(objName)


# Delete an object by name
def delete(objName):


    select(objName)
    bpy.ops.object.delete(use_global=False)


# Delete all objects
def delete_all():


    if(len(bpy.data.objects) != 0):
        bpy.ops.object.select_all(action='SELECT')
        bpy.ops.object.delete(use_global=False)


if __name__ == "__main__":

    # Create a cube
    create.cube('PerfectCube')


    # Differential transformations combine
    sel.translate((0, 1, 2))


    sel.scale((1, 1, 2))
    sel.scale((0.5, 1, 1))


    sel.rotate_x(3.1415 / 8)
    sel.rotate_x(3.1415 / 7)


    sel.rotate_z(3.1415 / 3)

    # Create a cone
    create.cone('PointyCone')


    # Declarative transformations overwrite
    act.location((-2, -2, 0))
    spec.scale('PointyCone', (1.5, 2.5, 2))


    # Create a Sphere
    create.sphere('SmoothSphere')


    # Declarative transformations overwrite
    spec.location('SmoothSphere', (2, 0, 0))
    act.rotation((0, 0, 3.1415 / 3))
    act.scale((1, 3, 1))

Notice the comment tags differential and declarative. There are a handful of ways to rotate, scale, and translate objects in Blender Python, but it is important to remember which functions dictate a form (declarative) and which functions modify a form (differential). Thankfully, the verbiage of the bpy functions and class values are fairly intuitive. For example, rotate is a verb, therefore differential, and rotation is a noun, therefore declarative.

Listing 2-9, which we will call ut.py, is a good starting point for a custom utility class.

In this book, we are interested in teaching the Blender Python API, not the author’s ut.py module. While the ut.py module is a good reference and teaching tool, we will refrain from using its single-line function calls in future chapters. While those function calls may solve our problems in the short-term, they obscure class structures and parameters we would otherwise like to reinforce through repetition.

For now, we will do some cool visualizations with ut.py. In future chapters, we will add bulky and meaningful utility functions to it while treating the single-line functions as placeholders.

Visualizing Multivariate Data with the Minimal Toolkit

In this section, we visualize multivariate data with the toolkit in Listing 2-9. Before we begin, give this toolkit a Python filename of ut.py using the bar at the base of the Text Editor. Now, click the plus sign in the base of the Text Editor to create a new script. The file ut.py is now a linked script within the Blender Python environment, and we can import it into other scripts within the environment.

We will be visualizing the famous Fisher’s Iris data set. This data set has five columns of data. The first four columns are numeric values describing dimensions of flower, and the final column is a categorical value describing the type of flower. There are three types of flowers in this data set: setosa , versicolor, and virginica .

Listing 2-10 serves as the header code for this example. It imports the necessary modules: our toolkit ut, the csv module , and urllib.request. We will fetch the data from a file repository with urllib, then parse it with csv. It is not necessary to understand all the code in Listing 2-10 to profit from this example.

Listing 2-10. Reading in iris.csv for the Exercise
import ut
import csv
import urllib.request


###################
# Reading in Data #
###################


# Read iris.csv from file repository
url_str = 'http://blender.chrisconlan.com/iris.csv'
iris_csv = urllib.request.urlopen(url_str)
iris_ob = csv.reader(iris_csv.read().decode('utf-8').splitlines())


# Store header as list, and data as list of lists
iris_header = []
iris_data = []


for v in iris_ob:
    if not iris_header:
        iris_header = v
    else:
        v = [float(v[0]),
             float(v[1]),
             float(v[2]),
             float(v[3]),
             str(v[4])]
        iris_data.append(v)

Visualizing Three Dimensions of Data

Since Blender is a 3D modeling suite, it seems most logical to visualize three dimensions of data. Listing 2-11 places a sphere at the (x, y, z) values of the 3D Viewport specified by the sepal length, sepal width, and petal length of each observation.

Listing 2-11. Visualizing Three Dimensions of Data
# Columns:                  
# 'Sepal.Length', 'Sepal.Width',
# 'Petal.Length', 'Petal.Width', 'Species'


# Visualize 3 dimensions
# Sepal.Length, Sepal.Width, and 'Petal.Length'


# Clear scene
ut.delete_all()


# Place data
for i in range(0, len(iris_data)):
    ut.create.sphere('row-' + str(i))
    v = iris_data[i]
    ut.act.scale((0.25, 0.25, 0.25))
    ut.act.location((v[0], v[1], v[2]))

The resultant set of spheres appear in the 3D Viewport, as shown in Figure 2-3. Obviously, the 2D picture printed in this text does not do this model justice. Using Blender’s mouse and keyboard movement tools, users can explore this data very intuitively.

A438961_1_En_2_Fig3_HTML.jpg
Figure 2-3. Visualizing Three Dimensions of Iris Data

Visualizing Four Dimensions of Data

Fortunately, there are more than three ways we can parameterize objects using Blender Python. To account for the final numeric variable, petal width, we will scale the spheres by the petal width. This will allow us to visualize and understand four dimensions of data within Blender. Listing 2-12 is a slight modification of the prior.

Listing 2-12. Visualizing Four Dimensions of Data
# Columns:                  

# 'Sepal.Length', 'Sepal.Width',
# 'Petal.Length', 'Petal.Width', 'Species'


# Visualize 4 dimensions
# Sepal.Length, Sepal.Width, 'Petal.Length',
# and scale the object by a factor of 'Petal.Width'


# Clear scene
ut.delete_all()


# Place data
for i in range(0, len(iris_data)):
    ut.create.sphere('row-' + str(i))
    v = iris_data[i]
    scale_factor = 0.2
    ut.act.scale((v[3] * scale_factor,) * 3)
    ut.act.location((v[0], v[1], v[2]))

The resultant set of spheres appear in the 3D Viewport, as shown in Figure 2-4. It is very apparent that the lower group of spheres has a very small sepal width. Figure 2-5 zooms in on this cluster of data.

A438961_1_En_2_Fig4_HTML.jpg
Figure 2-4. Visualizing Four Dimensions of Iris Data
A438961_1_En_2_Fig5_HTML.jpg
Figure 2-5. Visualizing Four Dimensions of Iris Data Pt. 2

Visualizing Five Dimensions of Data

From what we have seen up to this point, there exist at least two very distinct clusters within this data. We will dig into the flower species data to look for a relationship. To easily distinguish between types of flowers within the 3D Viewport, we can assign each flower type a geometric shape. See Listing 2-13.

Listing 2-13. Visualizing Five Dimensions of Data
# Columns:                  
# 'Sepal.Length', 'Sepal.Width',
# 'Petal.Length', 'Petal.Width', 'Species'


# Visualize 5 dimensions
# Sepal.Length, Sepal.Width, 'Petal.Length',
# and scale the object by a factor of 'Petal.Width'
# setosa = sphere, versicolor = cube, virginica = cone


# Clear scene
ut.delete_all()


# Place data
for i in range(0, len(iris_data)):
    v = iris_data[i]


    if v[4] == 'setosa':
        ut.create.sphere('setosa-' + str(i))
    if v[4] == 'versicolor':
        ut.create.cube('versicolor-' + str(i))
    if v[4] == 'virginica':
        ut.create.cone('virginica-' + str(i))


    scale_factor = 0.2
    ut.act.scale((v[3] * scale_factor,) * 3)
    ut.act.location((v[0], v[1], v[2]))

The resultant output in the 3D Viewport (Figure 2-6) sheds light on the relationship between dimensions and species within the data. We see many cones, virginica flowers, at the peak of the larger cluster, and we see many cubes, versicolor flowers, at the bottom of that larger cluster. There is some overlap between the dimensions of these two species. The spheres, setosa flowers, make up the completely separated cluster of flowers with smaller dimensions.

A438961_1_En_2_Fig6_HTML.jpg
Figure 2-6. Visualizing Five Dimensions of Iris Data

Discussion

With fewer than 200 lines code, we have built a powerful proof-of-concept for an interactive multivariate data visualization software. Concepts like this can be extended with advanced API functions we have yet to cover, including texturing, GUI development, and vertex-level operations. At present, our example software can use improvement on the following fronts:

  • No ability to scale data for the visualizer. The iris data worked nicely because the numeric values were conveniently in the range of (0, 0, 0) ± 10, which is about how many Blender units are easily viewable by default.

  • We could investigate a better system for scaling objects such that they best represent the data. For example, the volume of a sphere is proportional to the cube of the radius, so we may consider passing the cubic root of the data value as radius to the scale() function. The argument can be made that this creates a more intuitive visualization. The same argument can be made for taking the square root of the data value, because the area covered by a sphere in the 3D Viewport is proportional to the square of its radius.

  • In our five-dimensional visualization, it would be more intuitive to change the colors of the spheres rather than assign a shape to each species.

  • Our method of reading in data is static and GUI-less. An add-on developer would naturally like to apply this methodology to any data set, giving the user comprehensive controls over what he views and how he does so.

Note that, via ut.py, the main script was able to manipulate models in Blender without calling or importing bpy. This is not a recommended practice by any means, but it is exemplar of how the Blender Python environment treats bpy as a global collection of functions and data.

Conclusion

This chapter has introduced a lot of important high-level concepts about the Blender Python API, as well as detailed core functions of the bpy module. In the next chapter, we discuss Edit Mode and the bmesh module in detail. By the end of Chapter 3, users should be able to create any shape using the API. As we introduce more complicated and interdependent processes, abstraction will become both more important and more laborious.

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

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