Blender 3D Plugin
Mature Blender 3D plugins are amazing things. Being new to Python and Blender, I often find myself dumbfounded by the power of what people in the community have created. Frankly, some of these packages look kind of amazing, and when the nagging voice of self-doubt or impostor syndrome breaks down, it's easy to think, "If only someone could make something that could do xxx."
Then I remembered that by combining curiosity and stubbornness with good documentation, someone can be anyone, and X can be X, Y, and Z. Even the hard parts can be figured out-especially because all the stubborn and curious people made sure that Blender's Python documentation and the stackexchange is as good as the gee whiz graphs it lets us create.
In the same way that the documentation and modeling that already exists provides a smooth foundation for writing a Blender plugin from scratch, providing an extensible structure for the plugin at the beginning helps show how the various parts of the Python API fit together more clearly. In other words, it makes newly written code better than it would otherwise be, while also making existing code easier to learn.
At the end of this article, we'll create a fully functional and installed plugin that provides a custom UI element to add Standoff to the Blender scene, with interface controls to adjust the diameter and height of the created mesh.
I. Structure of the document
The full directory and file structure that will exist at the end of this article, which we can create using mkdir and touch, is a fill-in-the-blanks game for this example. I'm calling the project DemoRack and setting it to the name of the top-level directory in the folder I'm using for the Python project: it doesn't necessarily have to be anywhere Blender-specific. Here is the file structure of the DemoRack project:
DemoRack |-- |-- <-- will (re)compile via 'zip -r src' |-- src |-- |-- __init__.py |-- |-- standoff_mesh.py <-- from Part 2, not modified in this post |-- |-- standoff_operator.py |-- |-- standoff_panel.py |-- |-- standoff_props.py
Below we briefly describe what these files do:
- : Compiled src, installed files in Blender.
- __init__.py: registers all necessary information and classes for the add-on.
- standoff_mesh.py: module for generating target geometry/mesh data.
- standoff_operator.py: the "do-er" that will be provided to the UI.
- standoff_panel.py: adding the plugin to the UI element will ...... Add.
- standoff_props.py: defines the data objects needed for Panel and Operator.
In short, each new standoff_ module will contain register() and unregister() functions. The __init__ module will import these modules and bundle both types of functions into a single iterator.The Blender Python documentation describes the role these functions play:
II.__init__.py
With that background, and because most of the code in the __init__ module is related to the sys and importlib packages, I'm going to include the main points here without trying to go into the weeds of Python module import.The specific things to look out for with the Blender plugin are the list of module_names, the declaration of the files to be introduced into the register and unregister functions, and the open bl_info dictionary. As described in the official plugin introduction tutorial, bl_info contains all the information that will be found in the Preferences pane:
bl_info is a dictionary containing additional metadata, such as the title, version, and author to be displayed in the Preferences add-on list. It also specifies the minimum Blender version required to run the script; older versions will not display add-ons in the list.
Here is a sample code:
bl_info = { "name": "DemoRack", "description": "Make Mini Rack Units Dynamically", "author": "Jim O'Connor <hello@>", "version": (0, 0, 1), "blender": (2, 90, 1), "category": "3D View" } module_names = [ 'standoff_props', 'standoff_operator', 'standoff_panel' ] import sys import importlib module_full_names = [ f"{__name__}.{module}" for module in module_names ] for module in module_full_names: if module in : ([module]) else: locals()[module] = importlib.import_module(module) setattr(locals()[module], 'module_names', module_full_names) def register(): for module in module_full_names: if module in : if hasattr([module], 'register'): [module].register() def unregister(): for module in module_full_names: if module in : if hasattr([module], 'unregister'): [module].unregister()
III. standoff_props.py
This module will be the most involved of them all, but it also provides the backbone for the others and implements a pattern that can be widely reused. It relies on importing the PropertyGroup type (document) that is used to bind a set of property definitions together,. Once a PropertyGroup is registered in Blender, it provides a bridge between pointers to Python's scriptable data objects and the underlying C-allocated memory that does Blender's heavy lifting.
We need to define, inherit and track 3 properties in the standoff_props.py class:
- metric_diameter: FloatProperty(**kwargs)
- height: FloatProperty(**kwargs)
- mesh: PointerProperty(type=Mesh)
Of these, the first two should be self-explanatory and will have more details about the parameters in the implementation. The 3rd PointerProperty points to an object in memory and requires that the type of that object be specified at definition time and that it be a subclass of PropertyGroup or (i.e. Mesh). This means that any attempt to set the value to an instance of any other data type (in this case, any non ) will raise an error, and any attempt to pass the value to a parameter that expects any other data is also of this type.
In this case, the PointerProperty property of the mesh will be used to save the return value in () and instantiate and modify it using the values stored behind metric_diameter and height. The full definition of these three properties is shown below:
class PG_Standoff(PropertyGroup): metric_diameter: FloatProperty( name="Inner Diameter (Metric)", min=2, max=5, step=50, precision=1, set=prop_methods("SET", "metric_diameter"), get=prop_methods("GET", "metric_diameter"), update=prop_methods("UPDATE")) height: FloatProperty( name="Standoff Height", min=2, max=6, step=25, precision=2, set=prop_methods("SET", "height"), get=prop_methods("GET", "height"), update=prop_methods("UPDATE")) mesh: PointerProperty(type=Mesh)
The individual set, get, and update arguments all point to the return value of the prop_methods function. These values must be functions with parameters (self, value), (self) and (self, context). This closure factory may seem extra complex, but it will significantly reduce duplication and provide greater flexibility in interacting with the PropertyGroup's data properties.
An important difference to understand is that the update function is called whenever a property changes - it is not called as a way of updating a property that defines it. Instead, it provides a way to communicate changes to a particular property to the rest of the program; this must be used carefully to avoid side effects, and because there is no check to avoid infinite recursion.
Another thing to note is that the use of set and get functions means that any default values must be set via explicit set calls (rather than kwarg), but this also provides the opportunity to hook up the on_load method if necessary. Finally, the prop_methods function must be defined before any PropertyGroup class that calls it.
The skeleton code for the prop_methods function is shown below:
def prop_methods(call, prop=None): def getter(self): # getter function must check if prop attr has a value yet # if no value, will throw error, so must set default # can hook on load here # and either way, return self[prop] value def setter(self, value): self[prop] = value def updater(self, context): (context) methods = { "GET": getter, "SET": setter, "UPDATE": updater } return methods[call]
The complete implementation is shown below:
def prop_methods(call, prop=None): def getter(self): try: value = self[prop] except: set_default = prop_methods("SET", prop) set_default(self, [prop]) if hasattr(self, "on_load"): self.on_load() value = self[prop] finally: return value def setter(self, value): self[prop] = value def updater(self, context): (context) methods = { "GET": getter, "SET": setter, "UPDATE": updater, } return methods[call]
To do this, any class with a call to the prop_methods attribute requires a dictionary object named defaults and a method named update (which is provided by the function, but not the on_load method). In the PG_Standoff class, these calls will be used for the return value of append() and whenever the metric_diameter or height attributes are modified.The rest of the PG_Standoff course can be written as:
class PG_Standoff(PropertyGroup): # ... defaults = { "metric_diameter": 2.5, "height": 3 } standoff = Standoff() def on_load(self): if and self.metric_diameter: self.__set_mesh() def update(self, context): self.__set_mesh() def __set_mesh(self): = ( , self.metric_diameter)
Only the import statement and the register and unregister functions are left. In the register function, we will also point to the PG_PropertyGroup class from an instance of ointerProperty, referenced from a new property of Blender's Scene type, which will make accessing from the rest of the plugin simple. This will result in a complete standoff_props.py module, as shown in the bullet points below:
from import PointerProperty, FloatProperty from import Mesh, PropertyGroup, Scene from import register_class, unregister_class from .standoff_mesh import Standoff def prop_methods(call, prop=None): def getter(self): try: value = self[prop] except: set_default = prop_methods("SET", prop) set_default(self, [prop]) if hasattr(self, "on_load"): self.on_load() value = self[prop] finally: return value def setter(self, value): self[prop] = value def updater(self, context): (context) methods = { "GET": getter, "SET": setter, "UPDATE": updater, } return methods[call] class PG_Standoff(PropertyGroup): metric_diameter: FloatProperty( name="Inner Diameter (Metric)", min=2, max=5, step=50, precision=1, set=prop_methods("SET", "metric_diameter"), get=prop_methods("GET", "metric_diameter"), update=prop_methods("UPDATE")) height: FloatProperty( name="Standoff Height", min=2, max=6, step=25, precision=2, set=prop_methods("SET", "height"), get=prop_methods("GET", "height"), update=prop_methods("UPDATE")) mesh: PointerProperty(type=Mesh) defaults = { "metric_diameter": 2.5, "height": 3 } standoff = Standoff() def on_load(self): if and self.metric_diameter: self.__set_mesh() def update(self, context): self.__set_mesh() def __set_mesh(self): = (, self.metric_diameter) def register(): register_class(PG_Standoff) = PointerProperty(type=PG_Standoff) def unregister(): unregister_class(PG_Standoff) del
IV. standoff_operator.py
The last two modules are short files and very simple to implement, as the heavy lifting of structuring and modifying data has been done in a way that simplifies their interaction with the Blender Python API. In the standoff_operator module, we will define (and register) a new class that can then be attached to any UI button.
There are some requirements on how to define a new Operator, but these are well documented and straightforward, and by starting Blender from the CLI, warnings and error messages will be immediately apparent to the misconfigured Operator class there. The manual section gives details of the expected definitions for the new Operators. The following script will define the values of the docstring, bl_idname, bl_label and bl_options properties. It will also define an execute method that takes the self and context parameters and contains the Operator logic and events that will be attached to any UI button call registered under the bl_idname name.
import bpy from import Operator from import register_class, unregister_class class DEMORACK_OT_AddNewStandoff(Operator): """adds standoff to test add-on registered ok""" bl_idname = 'scene.add_new_standoff' bl_label = 'New Standoff' bl_options = { "REGISTER", "UNDO" } def execute(self, context): name = "Standoff" standoff = # <- set in standoff_props.register() collection = obj = (name, ) (obj) obj.select_set(True) context.view_layer. = obj return { "FINISHED" } def register(): register_class(DEMORACK_OT_AddNewStandoff) def unregister(): unregister_class(DEMORACK_OT_AddNewStandoff)
V. standoff_panel.py
Defining a new Panel class follows almost the same pattern as defining a new Operator class, and conceptually, it's just a matter of replacing the draw method in the last step of execute with the method in this step. The related Panel manual section provides several useful examples, and the UI Scripts > Templates > Python menus contain more examples. More possibilities can be found in the manual section, which documents the imported items of the Panel object. In the draw method it is a simple (and very open) procedure:
- Access to the relevant data objects in the context
- Create objects for any Operator calls that require buttons.
- Creating objects for user-writable data properties
Of course, there's plenty of room for expansion and variation in that pattern, as well as in more complex data types. But at the base, this is another place where the heavy lifting is handled by the API and follows and instance of the built-in usage pattern. Due to this simplicity, this is another module that is simple enough:
from import Panel from import register_class, unregister_class class DemoRackPanel: bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_category = "DemoRack" class StandoffPanel(DemoRackPanel, Panel): bl_idname = "DEMORACK_PT_standoff_panel" bl_label = "Standoff" def draw(self, context): layout = standoff_data = # <- set in standoff_props.register() ("scene.add_new_standoff") # <- registered in standoff_operator.py (standoff_data, "metric_diameter") (standoff_data, "height") def register(): register_class(StandoffPanel) def unregister(): unregister_class(StandoffPanel)
The only additional change is the class definition for DemoRackPanel, which is inherited by StandoffPanel along with it. Since this is not the only Panel in the DemoRack plugin, and they will all be located under a single tab in the View 3D side drawer, eliminating the 3 rows of repetition is a simple matter. The modal and related functions in the DemoRack plugin take a Data object as the first parameter and the string identifier of the property within that object as the second parameter.
All that's left to do is compile the file from the DemoRack/src/ directory and then install that local file in Edit > Preferences > Add-ons like any other file.
Link to original article:Build a Blender Add-on Ready to Scale
The above is a summary of python scalable Blender 3D plugin development in detail, more information about python Blender 3D plugin please pay attention to my other related articles!