Parameterized objects in Blender, a Python tutorrial

Writing new Blender operators in Python is quite simple and adding all sorts of parameters to customize that mesh is also not difficult but once a mesh is added to the scene and you have performed other actions it is no longer possible to tweak those settings and alter the mesh. You can of course delete the object and execute the operator anew but with more than one object the operator would remember its last settings which are not necessarily the ones you used to create the object you want to change. It would be much more convenient to have the options available for tweaking directly when you select an object.
The way to achieve this is to store the values for these options not as part of the operator but as part of the object and create an operator that checks for these object-bound properties and acts on their value.
Fortunately it's rather easy to do this in Blender and the code snippet below show how this can be done: it implements a very simple Spokes object, i.e. an object with a configurable number of arms, like in the picture below.

The number of arms is stored as an object property and the code provides both an operator to add a Spokes object and a panel that is installed in the modifier context to change the number of spokes. There is nothing special about placing the panel with the modifiers, you could position it elsewhere, but because these properties are persistent and changing them is non destructive it feels to me a bit like a modifier.
bpy.types.Object.reg = StringProperty()

bpy.types.Object.numberofspokes = IntProperty(name="Number of spokes",
         description="Number of spokes",
         default=6,
         min=2,
         soft_max=50,
         update=updateMesh)

class Spokes(bpy.types.Panel):
 bl_idname = "Spokes"
 bl_label = "Spokes"
 bl_space_type = "PROPERTIES"
 bl_region_type = "WINDOW"
 bl_context = "modifier"
 bl_options = {'DEFAULT_CLOSED'}

 def draw(self, context):
  layout = self.layout
  if bpy.context.mode == 'EDIT_MESH':
   layout.label('Spokes doesn\'t work in the EDIT-Mode.')
  else:
   o = context.object
   if 'reg' in o:
    if o['reg'] == 'Spokes':
     box = layout.box()
     box.prop(o, 'numberofspokes')
    else:
     layout.operator('mesh.spokes_convert')
   else:
    layout.operator('mesh.spokes_convert')

class SpokesAdd(bpy.types.Operator):
 bl_idname = "mesh.spokes_add"
 bl_label = "Spokes"
 bl_options = {'REGISTER', 'UNDO'}

 @classmethod
 def poll(self, context):
  return context.mode == 'OBJECT'

 def execute(self, context):
  bpy.ops.mesh.primitive_cube_add()
  context.active_object.name = "Spokes"
  bpy.ops.mesh.spokes_convert('INVOKE_DEFAULT')
  return {'FINISHED'}

class SpokesConvert(bpy.types.Operator):
 bl_idname = 'mesh.spokes_convert'
 bl_label = 'Convert to Spokes object'
 bl_options = {"UNDO"}

 def invoke(self, context, event):
  o = context.object
  o.reg = 'Spokes'
  o.numberofspokes = 6 # assigning something forces call to updateMesh()
  return {"FINISHED"}
The first lines in the code snippet show the property definitions while line 10 and 33 are where code for the modifier panel and the operators start respectively. Note that the new properties are added to the bpy.types.object. Because Python is such a dynamic language we can add properties to existing class objects.

The nitty gritty

There are a few things to note. When defining a new object property this custom property is available on all Blender objects. It is therefore not sufficient to check whether the numberofspokes property exists to see if we want to show a panel (in the draw() function at line 24). We could check if it contained a valid integer (because other objects that were not created with the Spokes operator would have the property but it wouldn't have been initialized) but it is much easier to create a second object property (a string property called reg in this example) that holds a value that identifies it as a Spokes object. This property could be reused and hold different values for other kinds of parameterized objects. The panel is part of the modifier context and looks like this:


The class that implements the panel doesn't have any code to actually change a Spokes object, all it does is display the numberofspokes property. This property however is defined with a reference to the updateMesh() function (line 8, the actual definition of tbe function is not shown) via its update argument, so every time the numberofspokes option is adjusted this function gets called to update the mesh.
The draw() function also shows a message when we are in edit mode and offers to convert an object to a Spokes object if it isn't already. The reason to have two operators is that we would like to have the option to create a Spokes object from scratch (from the add->mesh menu) and to convert existing objects. For the first option we provide the SpokesAdd operator, which merely creates a cube object and calls the SpokesConvert operator.
All that the SpokesConvert operator does is to assign values the object properties which will trigger a call to the updateMesh() function that will change the mesh data.
Note that custom properties that we defined are also available in the context panel (the one you get when pressing ctrl-n in the 3d view) but unlike the panel we defined we have no control over how to display these properties. They are however 'live', changing the numberofspokes property here will just as surely alter your object as when you alter it in the modifier panel.

Summary

To create parameterized objects you need to:
  • add properties to the bpy.types.Object class
  • make sure one of these properties can be used to identify an object as being a parameterized object
  • create an operator to add a new object like always but make sure it sets these object properties as well
  • create a way (a panel for example) to let the user alter the new object properties
  • create a function to actually alter the mesh when a property is changed

Code availability

The full code for a working add-on is available on GitHub






2 comments:

  1. Very interesting idea - I would say that these properties don't belong under modifiers - better suited to object or maybe mesh properties, you are trying to make the properties a part of the object definition.

    ReplyDelete
  2. Paste it into blenders text editor and click the run script button in the header.

    ReplyDelete