(un)Regular Tech Adventures: Blender Add-Ons
[
Recently I’ve coded a Blender Add-On to automate various tasks. Here’s what I’ve learnt from it.
Blender Add-On
Blender has a feature that allows you to script out actions in Python. Furthermore, you can then package your scripts as blender addons
In this case, I’ll be using the osu-replay-blender-animator I recently made as an example. I’ll be referencing most of the code from there. However, the main methods & concepts would be the same
Scripting in Blender
Blender allows you to manipulate the scene with Python. Here’s some sample code that allows you to
- Select object
- Translate & scale it, and key frame it
- Create objects, rename
- Create & assign materials to objects
import bpy.ops
import bpy
from bpy import context
# Get the current scene
scene = context.scene
# Get the 3D cursor location
#cursor = scene.cursor.location
# Get the active object (assume we have one)
obj = context.active_object
def add_keyframe(x, y, z, frame):
obj.location[0] = x
obj.location[1] = y
obj.location[2] = z
obj.scale[0] = 1
obj.scale[1] = 1
obj.scale[2] = 1
#obj.keyframe_delete(data_path='location', frame=frame)
obj.keyframe_insert(data_path='location', frame=frame) # , index=2
obj.keyframe_insert(data_path='scale', frame=frame)
add_keyframe(1,1,1,10)
# Adding Objects
class Wrapper
def generateAssembly(self):
origin = (0, 0, 0)
bpy.ops.object.empty_add(type='PLAIN_AXES')
bpy.context.active_object.name = 'osuReplayCursor'
self.cursor = bpy.context.object
self.cursor.location = origin
bpy.ops.mesh.primitive_cube_add()
bpy.context.active_object.name = 'osuReplayCursorVisible'
self.cursorVisible = bpy.context.object
self.cursorVisible.location = origin
self.cursorVisible.scale = self.cursor_scale
self.cursorVisible.parent = self.cursor
bpy.ops.mesh.primitive_plane_add()
bpy.context.active_object.name = 'osuReplayPlanePlayfield'
self.plane = bpy.context.object
self.plane.scale = (
self.dimensions[0]*self.scale_factor/2, # Divide to adjust scale factor
self.dimensions[1]*self.scale_factor/2,
1
)
self.plane.location = (
origin[0] + self.dimensions[0]*self.scale_factor/2, # midpoint
origin[1] + self.dimensions[1]*self.scale_factor/2,
0
) # middle of plane
bpy.ops.mesh.primitive_plane_add()
bpy.context.active_object.name = 'osuReplayPlaneVideo'
self.planeVideo = bpy.context.object
# Materials
def linkVideoToPlane(self, filename, filedirectory):
#bpy.ops.material.new()# "osuReplayPlaneVideoMaterial")
self.video_mat = bpy.data.materials.new(name="osuReplayPlaneVideoMaterial") #bpy.data.materials.get("osuReplayPlaneVideoMaterial")
self.video_mat.use_nodes = True
# Assign it to object
if self.planeVideo.data.materials:
# assign to 1st material slot
self.planeVideo.data.materials[0] = self.video_mat
else:
# no slots
self.planeVideo.data.materials.append(self.video_mat)
bpy.ops.image.open(filepath=filedirectory+filename)
video_mat_links = self.planeVideo.active_material.node_tree.links
video_mat_nodes = self.planeVideo.active_material.node_tree.nodes
node_texture = video_mat_nodes.new(type='ShaderNodeTexImage')
node_texture.image = bpy.data.images[filename]
MAX = 1048574
node_texture.image_user.frame_duration = MAX # Currently not working
node_texture.image_user.frame_offset = 217 # Hardcoded
node_texture.image_user.use_auto_refresh = True # Loop Through Video
video_shader = video_mat_nodes.get('Principled BSDF')
video_mat_links.new(video_shader.inputs["Base Color"], node_texture.outputs["Color"])
# bpy.data.materials.remove(self.video_mat)
If you want to figure out how to do something in Python, there probably is already a way. But you may need to google and search for stackoverflow posts. Alternatively, under preferences, you can check the show Python Tooltip option, which will show you the related python method for the action you are doing.
Structure of a Blender Add On
Your add on is in the form of a single zip file
addon.zip
- addon/
- __init__.py
- other python files you may want
Most of the code below should be in __init__.py
Metadata
Put this at the very top of the file
bl_info = {
"name": "Generate osu Replay Keyframes",
"blender": (2, 80, 0),
"category": "Object",
}
Importing Libraries
Remember to import your blender python libraries
import bpy
import bpy.ops
from bpy import context
You can also import other packages in the same directory. For example, if you have a package generator.py
, you can import it like this
from .generator import YourStuff
Panel
This is the part that gives your add on a usable Interface. For this, you would need to
-
Define your global variables that you can set in the add on panel
-
Add them in the panel with
row.prop()
-
Add an operator (to use your variables) with
col.operator()
# == GLOBAL VARIABLES
PROPS = [
('replay_file', bpy.props.StringProperty(name='Processed Replay File Path', default='/tmp/replay.osr')),
('video_file_directory', bpy.props.StringProperty(name='Video File Directory', default='/tmp/')),
('video_file_name', bpy.props.StringProperty(name='Video File Name', default='file.mp4')),
('aim', bpy.props.BoolProperty(name='Aiming', default=True)),
('tapx', bpy.props.BoolProperty(name='TapX', default=False)),
('tap_bitmask', bpy.props.IntProperty(name='TapX', default=31)),
]
class SamplePanel(bpy.types.Panel):
""" Display panel in 3D view"""
bl_label = "osu Replay Animator"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_options = {'HEADER_LAYOUT_EXPAND'}
def draw(self, context):
layout = self.layout
col = layout.column(align=True)
for (prop_name, _) in PROPS:
row = col.row()
if prop_name == 'version':
row = row.row()
row.enabled = context.scene.add_version
row.prop(context.scene, prop_name)
col.operator("object.osu_replay_keyframes", text="Generate Replay Object")
Operator
Operators are where your actual code happens. It is the section which modifies your blender scene.
The global variables here can be retrieved using context.scene.<var_name>
Here’s a sample format
class GenerateOsuReplayKeyframes(bpy.types.Operator):
"""My Object Moving Script""" # Use this as a tooltip for menu items and buttons.
bl_idname = "object.osu_replay_keyframes" # Unique identifier for buttons and menu items to reference.
bl_label = "Generate Object with osu Replay Keyframes" # Display name in the interface.
bl_options = {'REGISTER', 'UNDO'} # Enable undo for the operator.
def execute(self, context): # execute() is called when running the operator.
# Insert code to run here
value = context.scene.aim # Example retrieval of variable
return {'FINISHED'} # Lets Blender know the operator finished successfully.
Registering everything
This code is the one which actually sets up your classes in Blender. It puts your props/operators/panels etc. into your scene for you to use.
classes = (
SamplePanel, GenerateOsuReplayKeyframes
)
def register():
for (prop_name, prop_value) in PROPS:
setattr(bpy.types.Scene, prop_name, prop_value)
for cls in classes:
bpy.utils.register_class(cls)
'''
bpy.types.VIEW3D_MT_object.append(
lambda self, context: self.layout.operator(GenerateOsuReplayKeyframes.bl_idname)
) # Adds the new operator to an existing menu.
'''
def unregister():
for (prop_name, _) in PROPS:
delattr(bpy.types.Scene, prop_name)
for cls in classes:
bpy.utils.unregister_class(cls)
# This allows you to run the script directly from Blender's Text editor
# to test the add-on without having to install it.
if __name__ == "__main__":
register()
Conclusion
With that, you should have what you need to make a simple Blender Plugin. Check out my plugin too!
Useful Resources
- https://docs.blender.org/manual/en/latest/advanced/scripting/addon_tutorial.html
- https://blender.stackexchange.com/questions/78359/set-active-object-with-python
- https://blender.stackexchange.com/questions/45101/adding-named-objects-in-blender-with-python-api
- https://blender.stackexchange.com/questions/9200/how-to-make-object-a-a-parent-of-object-b-via-blenders-python-api
view rawSetLocationRotationScaleToAnObjectWithAName.py hosted with ❤ by GitHub
- https://blenderartists.org/t/create-and-assign-materials-to-selected-objects/1281181/2
- https://blenderartists.org/t/deleting-all-materials-in-script/594160/2
Useful - create panel