Dynamic Blender Properties
As part of my most recent adventures in game engine programming, I came across a small problem—I needed a way to edit levels (both their geometry, and the entities within the level and their associated components). Writing an editor to do this is a rather daunting task. Thankfully, Blender is a free, open-source 3D application that is “easily” extended (well, easy-ish). So, instead of writing my own editor, I can write a Blender addon to make it do what I need it to. First up in that, was presenting an interface for editing which components an object has, and setting the values of each components’ attributes. I found this to be more difficult that I expected, thanks to the way Blender handles and presents data. I will show you here how I got things working, as there doesn’t seem to be documentation on this and I had to wade through a lot of half-expired forum posts to get things working.
Basically, in order to display any data in a panel in Blender, you must pre-define the property classes that it will render. The following code listing shows a simple panel which will show up in the object section of the properties panel:
import bpy
from bpy.props import FloatProperty, PointerProperty
from bpy.types import PropertyGroup
class SamplePropertyGroup(PropertyGroup):
a = FloatProperty(name="A", default=5.0)
b = FloatProperty(name="B", default=42.0)
class SamplePanel(bpy.types.Panel):
bl_label = "Sample Panel"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "object"
def draw(self, context):
layout = self.layout
obj = context.object
sampleProperty = obj.samplePropertyGroup
col = layout.column(align=True)
col.prop(sampleProperty, "a")
col.prop(sampleProperty, "b")
def register():
bpy.utils.register_class(SamplePropertyGroup)
bpy.utils.register_class(SamplePanel)
bpy.types.Object.samplePropertyGroup = PointerProperty(type=SamplePropertyGroup)
def unregister():
bpy.utils.unregister_class(SamplePropertyGroup)
bpy.utils.unregister_class(SamplePanel)
del bpy.types.Object.samplePropertyGroup
if __name__ == "__main__":
register()
A problem arises, however, when you don’t know ahead of time what that SamplePropertyGroup
class will look like. Or even if it will exist (or what custom property groups will exist). For my use case, I want to have each component exist in blender as it’s own subclass of the PropertyGroup
class, with its list of attributes defined as class members (as relevant PropertyTypes
). Forunately for us, Python is a scripted language, and comes with some tools to dynamically create classes and interact with them via the type()
, setattr()
, getattr()
, and delattr()
built-in functions.
Simply put, we can use type()
to go from this:
class SamplePropertyGroup(PropertyGroup):
a = FloatProperty(name="A", default=5.0)
b = FloatProperty(name="B", default=42.0)
to this:
SamplePropertyGroup = type(
"SamplePropertyGroup",
(PropertyGroup,),
{
"a": FloatProperty(name="A", default=5.0),
"b": FloatProperty(name="B", default=42.0)
})
We can easily script this to be more dynamic, such that given a dictionary (which we can parse from a JSON file at runtime) that looks like this:
propertyGroupLayouts = {
"Health": [
{ "name": "current", "type": "float" },
{ "name": "max", "type": "float" }
],
"Character": [
{ "name": "first_name", "type": "string" },
{ "name": "last_name", "type": "string" }
]
}
We can dynamically create our property groups and register them with Blender by simply iterating the dictionary and constructing classes as we go:
# iterate over our list of property groups
for groupName, attributeDefinitions in propertyGroupLayouts.items():
# build the attribute dictionary for this group
attributes = {}
for attributeDefinition in attributeDefinitions:
attType = attributeDefinition['type']
attName = attributeDefinition['name']
if attType == 'float':
attributes[attName] = FloatProperty(name=attName.title())
elif attType == 'string':
attributes[attName] = StringProperty(name=attName.title())
else:
raise TypeError('Unsupported type (%s) for %s on %s!' % (attType, attName, groupName))
# now build the property group class
propertyGroupClass = type(groupName, (PropertyGroup,), attributes)
# register it with Blender
bpy.utils.register_class(propertyGroupClass)
# apply it to all Objects
setattr(bpy.types.Object, groupName, PointerProperty(type=propertyGroupClass))
Now, whenever we register we just call the above code, and our PropertyGroup
classes will be defined and registered in Blender as properties of all objects! Don’t forget to clean up the registered objects in unregister()
!
Finally, to actually show our properties in a Blender panel is relatively straightforward:
class SamplePanel(bpy.types.Panel):
bl_label = "Sample Panel"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "object"
def draw(self, context):
layout = self.layout
obj = context.object
# use our layout definition to dynamically create our panel items
for groupName, attributeDefinitions in propertyGroupLayouts.items():
# get the instance of our group
# dynamic equivalent of `obj.samplePropertyGroup` from before
propertyGroup = getattr(obj, groupName)
# start laying this group out
col = layout.column()
col.label(groupName)
# loop through all the attributes and show them
for attributeDefinition in attributeDefinitions:
col.prop(propertyGroup, attributeDefinition["name"])
# draw a separation between groups
layout.separator()
And there you have it, dynamic properties (created at run-time, not write-time) in Blender!
Here is the full source code for the above example:
import bpy
from bpy.props import FloatProperty, StringProperty, PointerProperty
from bpy.types import PropertyGroup
# TODO: load dynamically at runtime from a JSON file!
bpy.propertyGroupLayouts = {
"Health": [
{ "name": "current", "type": "float" },
{ "name": "max", "type": "float" }
],
"Character": [
{ "name": "first_name", "type": "string" },
{ "name": "last_name", "type": "string" }
]
}
bpy.samplePropertyGroups = {}
class SamplePanel(bpy.types.Panel):
bl_label = "Sample Panel"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "object"
def draw(self, context):
layout = self.layout
obj = context.object
# use our layout definition to dynamically create our panel items
for groupName, attributeDefinitions in bpy.propertyGroupLayouts.items():
# get the instance of our group
# dynamic equivalent of `obj.samplePropertyGroup` from before
propertyGroup = getattr(obj, groupName)
# start laying this group out
col = layout.column()
col.label(groupName)
# loop through all the attributes and show them
for attributeDefinition in attributeDefinitions:
col.prop(propertyGroup, attributeDefinition["name"])
# draw a separation between groups
layout.separator()
def register():
# register the panel class
bpy.utils.register_class(SamplePanel)
# iterate over our list of property groups
for groupName, attributeDefinitions in bpy.propertyGroupLayouts.items():
# build the attribute dictionary for this group
attributes = {}
for attributeDefinition in attributeDefinitions:
attType = attributeDefinition['type']
attName = attributeDefinition['name']
if attType == 'float':
attributes[attName] = FloatProperty(name=attName.title())
elif attType == 'string':
attributes[attName] = StringProperty(name=attName.title())
else:
raise TypeError('Unsupported type (%s) for %s on %s!' % (attType, attName, groupName))
# now build the property group class
propertyGroupClass = type(groupName, (PropertyGroup,), attributes)
# register it with Blender
bpy.utils.register_class(propertyGroupClass)
# apply it to all Objects
setattr(bpy.types.Object, groupName, PointerProperty(type=propertyGroupClass))
# store it for later
bpy.samplePropertyGroups[groupName] = propertyGroupClass
def unregister():
# unregister the panel class
bpy.utils.unregister_class(SamplePanel)
# unregister our components
try:
for key, value in bpy.samplePropertyGroups.items():
delattr(bpy.types.Object, key)
bpy.utils.unregister_class(value)
except UnboundLocalError:
pass
bpy.samplePropertyGroups = {}
if __name__ == "__main__":
register()
There are of course many more things that need to be done to make this a fully working system, but hopefully this can help you get started. I will continue working on my engine tools addon and release it when it’s ready (you can follow progress at BlazingMammothGames/mammoth_blender_tools if you really like). The tool, when done, will hopefully server as a solid example for adapting Blender to your own needs in the future. For now, if you run into any trouble, don’t hesitate to ask for help in the comments!