Some tips for python scripting in Blender 2.79 for Artists
(last updated 02-2024) added some tips for migrating code from Blender 2.79 to higher versions
What is in this?
It covers some specific areas in Blender python scripting especially useful in areas such as Rigging, Animation and Modeling. It also has a little information on Cloth and Hair scripting as well. I also want to try to learn more about classes (object oriented programming) and how classes can help an Artist with scripting in Blender.
Some specific areas of blender i will cover are: joints, animation, curves, constraints, drivers, shapekeys/blendshapes, modifiers and then at the end will go over some longer examples. although these notes are specific to blender 2.79 they can still be helpful when scripting in higher blender versions. also learning how to use an api such as Blender's can help with learning api's for other cg applications. If you are new to python i strongly suggest checking out Chad Vernon’s website that has an introduction to python (www dot chadvernon dot com) another very helpful website to learn python is (www dot stackoverflow dot com)
Intro to blender dataSome tips for migrating code from 2.79 to higher Blender versions
Joints/Bones
Animation
Bendy Bones
Constraints
Materials
Weight Painting
Properties
Drivers
Shapekeys/Blendshapes
Modifiers
Lattices
Sequencer
Node Editor (Shaders)
Cloth
Intro to working with text files (xml,json)
Intro to running script without blender gui
Intro to creating command line tool
Simple Class Examples
Add-on examples
Some strategies for learning Blender python
There a lots of ways to learn new software but here are just a few tips that i found helpful.
first start by hovering mouse over the ui element of interest. that way can find things like what is attribute for rotation or translation by hovering mouse over channel box.
second can read the info panel after doing something in Blender then can see what function usually a bpy.ops method that did the action. with that information can try searching for non ops way of doing method on google. why the non ops way? from my understanding non ops way requires less assumptions on the selection requirements for getting the desired result.
Third can use auto completion in the python console to see what possible attribute could be what is needed. (for mac ctrl+space bar autocompletes) by usually searching bpy.data.objects["name of object"]. then use auto completion to see possible things that can go afterwards. or bpy.data. and auto completion to see what options could come next.
Fourth can begin to read simple Blender add-ons on git to learn more about Blender scripting.
Fifth if know the type of object looking for can search Blender's python api docs for the different methods it has and what parameters are needed.
also using google to search for a specific question looking for. like search for "blender bpy how to create a bone"
Intro to Blender data
here are some examples of the different types of data available in Blender. It is helpful for when searching python api documentation online for Blender. These notes are for blender 2.79.
>>> type(bpy.data.scenes['Scene']) class 'bpy.types.Scene' >>> bpy.data.objects['Camera'].data bpy.data.cameras['Camera'] >>> type(bpy.data.objects['Camera'].data) class 'bpy.types.Camera' >>> type(bpy.data.objects['Cube'].data) class 'bpy_types.Mesh' >>> type(bpy.data.objects['Armature'].data) class 'bpy.types.Armature' >>> bpy.data.objects['Armature'].data.bones['Bone'] bpy.data.armatures['Armature'].bones["Bone"] >>> type(bpy.data.objects['Armature'].data.bones['Bone']) class 'bpy_types.Bone' >>> type(bpy.data.objects['Lamp'].data) class 'bpy.types.PointLamp' >>> type(bpy.data.objects['Cube'].animation_data) class 'bpy.types.AnimData' >>> bpy.data.objects['Cube'].animation_data.action bpy.data.actions['CubeAction'] >>> type(bpy.data.objects['Cube'].animation_data.action) class 'bpy.types.Action' >>> bpy.data.objects['Cube'].animation_data.action.fcurves[0] bpy.data.actions['CubeAction']...FCurve >>> type(bpy.data.objects['Cube'].animation_data.action.fcurves[0]) class 'bpy.types.FCurve' >>> bpy.data.objects['Cube'].animation_data.action.fcurves[0].keyframe_points[0] bpy.data.actions['CubeAction']...Keyframe >>> type(bpy.data.objects['Cube'].animation_data.action.fcurves[0].keyframe_points[0]) class 'bpy.types.Keyframe' >>> type(bpy.data.objects['Cube'].animation_data.action.fcurves[0].keyframe_points[0].co) class 'Vector'so if googled
bpy.types.Armatureshould be able to go to Blender’s api docs and find all methods available for an armature.
Some tips for migrating code from 2.79 to higher Blender versions
#old 2.79 object.select = True #new 3.6.5 object.select_set(True) #select_get() #old bpy.context.scene.objects.active = obj #new bpy.context.view_layer.objects.active = obj # charArmatureObj.pose.bones[self.baseMid].use_bbone_custom_handles = True # charArmatureObj.data.bones[self.baseMid].bbone_handle_type_start = 'ABSOLUTE' # charArmatureObj.pose.bones[self.baseMid].bbone_custom_handle_start # charArmatureObj.data.bones[self.baseMid].bbone_custom_handle_start # selectedObj.matrix_world*face.calc_center_median() # selectedObj.matrix_world @ face.calc_center_median() # bpy.context.scene.objects.link(obj) # bpy.context.scene.collection.objects.link(obj) # importObjPath = StringProperty # importObjPath : StringProperty # bmesh.ops.create_circle(bm,cap_ends=False,diameter=0.1,segments=8) # bmesh.ops.create_circle(bm,cap_ends=False,radius=0.1,segments=8) # scene.update() # scene.view_layers.update() # bpy.ops.transform.rotate(value=amount, axis=rotateAxis) # bpy.ops.transform.rotate(value=amount, orient_axis=rotateAxis) # charArmatureObj.data.draw_type='BBONE' # charArmatureObj.data.display_type='BBONE' #old boneObj = bpy.data.objects[armatureName].pose.bones[boneName] boneObj[propertyName] = 0.0 bone_prop = rna_idprop_ui_prop_get(boneObj, propertyName, create = True) bone_prop['default']= 1.0 bone_prop['min']= 0.0 bone_prop['max']= 1.0 #new boneObj = bpy.data.objects[armatureName].pose.bones[boneName] boneObj[propertyName] = 0.0 bone_prop = boneObj.id_properties_ui(propertyName) bone_prop.update(default=1.0) bone_prop.update(min=0.0) bone_prop.update(max=1.0) #old bpy.data.objects['Armature'].pose.bones['globalMover.C'].custom_shape_scale = 2 #new bpy.data.objects['Armature'].pose.bones['globalMover.C'].custom_shape_scale_xyz = (2,2,2) #old bpy.data.objects[armatureNameSource].hide = True #new bpy.data.objects[armatureNameSource].hide_set(True)
Joints/Bones
bones/joints are important for areas such as rigging because they can be used to move the mesh of the character.
briefly to make a bone need to first have an armature created (here the armature data is given a specific name in quotes) in the snippets after >>> symbol is the actual code run in blender’s python console, underneath that line is what blender returns from calling the command. to quickly get to blender’s console can click the ui drop down and switch to Scripting option. The python console should be at the bottom.
>>> type(bpy.data.armatures) class 'bpy_prop_collection'in blender the bpy_prop_collection is kind of like a python dictionary. we can access its keys and values. with no armatures in scene get an empty list.
>>> bpy.data.armatures.keys() []
>>> bpy.data.armatures.new('testArmat') bpy.data.armatures['testArmat']in the above notice the bpy. well that is used for accessing blender’s api. like other applications like Maya’s cmds or Houdini’s hou or Katana’s api. lots of Blender api can be accessed via bpy. the .new is telling blender to create a new armature. once the armature is created we can see we get something for the keys.
>>> bpy.data.armatures.keys() ['testArmat']once the armature data is created can create an armature object using that data.
>>> bpy.data.objects.new('testArm', bpy.data.armatures['testArmat'] ) bpy.data.objects['testArm']similar to bpy.data.armatures bpy.data.objects is like a python dictionary as well.
>>> type(bpy.data.objects) class 'bpy_prop_collection'after creating the armature object we get:
>>> bpy.data.objects.keys() ['Camera', 'Cube', 'Lamp', 'testArm']we can see there are some other objects in the default blender scene like camera and lamp. some more info on the object we created:
>>> 'testArm' in bpy.data.objects True >>> bpy.data.objects.get('testArm') bpy.data.objects['testArm'] >>> type(bpy.data.objects.get('testArm')) class 'bpy_types.Object' >>>this next part is specific to blender 2.79 and is different for later versions. once armature object is created need to link it to the blender scene.
>>> bpy.data.scenes['Scene'].objects.link( bpy.data.objects['testArm'] )in above see the use of ’Scene’. in a single blender file can have multiple scenes so we want to add the armature to a specific scene in the blender file. like bpy.data.armatures and bpy.data.objects bpy.data.scenes is like a python dictionary as well.
>>> type(bpy.data.scenes) class 'bpy_prop_collection'
>>> bpy.data.scenes.keys() ['Scene']we can see the scene type here:
>>> type(bpy.data.scenes['Scene']) class 'bpy.types.Scene'we can see the scene.objects is like a python dictionary as well.
>>> type(bpy.data.scenes['Scene'].objects) class 'bpy_prop_collection'to see some of the methods available for the scene.objects can type
bpy.data.scenes['Scene'].objects.then on mac click ctrl+spacebar. and it brings up all the methods available. in blender 2.79 link method is the one used to link armature object to scene.
once the armature object is linked to the scene we can begin adding bones. in blender there’s a distinction between the kind of bones either an edit bone or a pose bone. edit bones are like the rest bones whereas pose bones are the bones that actually get animated. moving the position of an edit bone changes the position of the pose bone but changing the position of pose bone doesn’t change position of edit bone.
to add an edit bone we need to be in edit mode. one way to enter edit mode is to select the armature object in outliner and then click the drop down at bottom of viewport and switch to edit mode.Blender has something called modes that is important when scripting. they are used alot so for example some python code might need to be run in edit mode instead of object mode. for example creating edit bones needs to be done in edit mode.
>>> bpy.data.armatures['testArmat'].edit_bones.new('bone_a')with the bone added we can position it by assigning it a trio tuple for x,y,z positions to its head or tail:
>>> bpy.data.armatures['testArmat'].edit_bones['bone_a'].head Vector((0.0, 0.0, 0.0)) >>> bpy.data.armatures['testArmat'].edit_bones['bone_a'].tail = (2,0,0)the pose bones get automatically created after edit bones are created. they will be used by animator. we can add shapes to them for easier selection. we can add colours to them for easier readability (via bone groups). and there are lots more attributes we can change basically if we can do it in the ui then there is an attribute/property we can change via code.
there are lots more properties for bones to go over but above is a starting point to learning about joints in blender.
Animation
creating animation on a pose bone
first create animation_data on the armature object. then a new action needs to be created then a new fcurve created then a new keyframe point created.
#assumes an armature has been created with a bone named Bone. #start with no animation_data >>> bpy.data.objects['Armature'].animation_data is None True1. create animation_data
>>> bpy.data.objects['Armature'].animation_data_create() bpy.data.objects['Armature']...AnimData >>> bpy.data.objects['Armature'].animation_data is None False2. create action in animation_data
>>> bpy.data.objects['Armature'].animation_data.action is None True >>> bpy.data.objects['Armature'].animation_data.action = bpy.data.actions.new(name = "BoneAction") >>> bpy.data.objects['Armature'].animation_data.action is None False3a. create an fcurve in action
>>> fcurve = bpy.data.objects['Armature'].animation_data.action.fcurves.new('pose.bones["Bone"].location',2) #should be on translation in z. 0 would be x, 1 would be y. in blender translation is same as location.3b. toggle in and out of pose mode should see color green on translation z channel
4. create a key frame point in fcurve
>>> fcurve.keyframe_points.insert(1,20) #created on frame 1 with a value of 20 bpy.data.actions['BoneAction']...Keyframe #5. click in graph editor so can see new keyframe and click in viewport and frame bone to see it keyed on frame 1 #6. can create another keyframe to see bone animate >>> fcurve.keyframe_points.insert(24,0)
these are all the steps together
bpy.data.objects['Armature'].animation_data_create() bpy.data.objects['Armature'].animation_data.action = bpy.data.actions.new(name = "BoneAction") fcurve = bpy.data.objects['Armature'].animation_data.action.fcurves.new('pose.bones["Bone"].location',2) fcurve.keyframe_points.insert(1,20) fcurve.keyframe_points.insert(24,0)
if go into the dope sheet the action we created and the keyframes should be visible. i think there can be many fcurves for a single action. And each fcurve can have multiple keyframe points. (note about the path used in creating fcurve: pose.bones["Bone"].location. it has the bone name of interest and what attribute should be keyed.)
removing animation on a pose bone
removing an entire fcurve. first need to get the FCurve object to remove then remove it from action’s fcurves.
>>> fcurve = bpy.data.objects['Armature'].animation_data.action.fcurves[0] >>> fcurve bpy.data.actions['BoneAction']...FCurve >>> bpy.data.objects['Armature'].animation_data.action.fcurves.remove(fcurve) >>> #i think have to go in and out of pose mode to see that fcurve was removed
removing a single keyframe point. need to first have an fcurve. then need to get the Keyframe object for keyframe want to remove. then we can remove it from keyframe_points.
>>> keypoint = fcurve.keyframe_points[1] >>> keypoint bpy.data.actions['BoneAction.001']...Keyframe >>> keypoint.co Vector((24.0, 0.0)) >>> fcurve.keyframe_points.remove(keypoint) #i think have to go in and out of pose mode to see that the keyframe on frame 24 is now gone.
Bendy Bones
Bendy bones can be used to get simple curvy posing of a bone chain.
For this simple example assumes 3 bones have been created: a middle bone, a low bone and a tail bone. assumes the tail bone is unparented from middle bone.
arm_obj = bpy.data.objects['Armature'] bpy.context.scene.objects.active = arm_objmake sure armature in bendy bone view mode
arm_obj.data.draw_type = 'BBONE'set the segments for bendy bone
arm_obj.data.bones[midBoneName].bbone_segments = segmentsset the handles for bendy bone
arm_obj.pose.bones[midBoneName].use_bbone_custom_handles = True arm_obj.pose.bones[midBoneName].bbone_custom_handle_start = arm_obj.pose.bones[headBoneName] arm_obj.pose.bones[midBoneName].bbone_custom_handle_end = arm_obj.pose.bones[tailBoneName]to add stretchiness to bone chain a stretch to constraint on middle bone aiming at tail bone can be made - see constraints section.
Constraints
its helpful when scripting constraints to first create them via the ui and see in the info panel what properties are being used and what ui fields are changed.
Action Constraint
This is sort of like a blendshape for joints. it can be used to create an animation for some joints and have a different animator control drive the pose. to make this constraint we first need to have an action already created with possible animation.
Creating the action
bpy.data.actions.new('TestAction')how to create action animation for some joints/bones?
bpy.data.objects['Armature'].animation_data.action = bpy.data.actions['TestAction'] #makes action selected #adding a new fcurve to existing action for a given pose bone bpy.data.objects['Armature'].animation_data.action.fcurves.new('pose.bones["Bone"].location’,1)#y #adding a keyframe to existing action and fcurve on a given pose bone bpy.data.objects['Armature'].animation_data.action.fcurves[0].keyframe_points.insert(1,0) bpy.data.objects['Armature'].animation_data.action.fcurves[0].keyframe_points.insert(20,5) #toggle in out of pose mode to refresh animation - might want to try bpy.context.scene.frame_set(4)
Creating the action constraint given the action
act = bpy.data.objects['Armature'].pose.bones['Bone'].constraints.new('ACTION') #this is bone to be driven by constraint act.target = bpy.data.objects['Armature'] act.subtarget = 'anim' #this is bone used to drive the animation for the blendshape act.transform_channel = 'LOCATION_X' act.target_space = 'LOCAL' act.min = 0 #what value by animator act.max = 1 act.frame_start = 1 #what value for bone action animation act.frame_end = 20 act.action = bpy.data.actions['TestAction']
Stretch-to constraint
this constraint can be used add squash and stretch to a bone. so can have for example squash and stretch head for cartoony animation. how to create a stretch to constraint? this example also shows use of function in python
def UT_CreateStretch( bone = None, endBone = None, armatureName = None ): """create stretchto constraint on given bone aiming at given end bone bone - bone string name with the stretchto constraint endBone - end bone, stretching aims at this bone armatureName - armature string name """ arm_obj = bpy.data.objects[armatureName] bpy.context.scene.objects.active = arm_obj bpy.ops.object.mode_set(mode='OBJECT') constraint = arm_obj.pose.bones[bone].constraints.new('STRETCH_TO') constraint.target = arm_obj constraint.subtarget = endBone
how to create other constraints example spline-ik constraint?
bpy.ops.pose.constraint_add(type='SPLINE_IK') bpy.context.object.pose.bones["Bone.001"].constraints["Spline IK"].target = bpy.data.objects["BezierCurve"] bpy.context.object.pose.bones["Bone.001"].constraints["Spline IK"].chain_count = 5
then from the above info we can see how to build the constraint but this time not using bpy.ops:
act = bpy.data.objects['Armature'].pose.bones['Bone.001'].constraints.new('SPLINE_IK') act.target = bpy.data.objects["BezierCurve"] act.chain_count = 5
the above example is hypothetical and requires the armature, bones and curve to exist in the blender scene prior to calling the commands. also notice how we can store the result of a command inside of variable to be later used when setting its properties.
similar technique can be used to create any constraint in blender. simply make it one time using ui read from the info panel the properties used. then remake it but this time using scripting without using bpy.ops.
notice also the use of the pose bone in the commands. constraints are added to the pose bone of armature and not the edit bones.
Materials
if we want to color a mesh then adding a material is one way to do that.
#making a new material >>> mat = bpy.data.materials.new('matname') >>> mat bpy.data.materials['matname'] #assign material to a mesh >>> bpy.data.objects['Plane'].data.materials.append(mat) #change material to green color >>> mat.diffuse_color = (0, 1, 0 ) #using rgb
Weight Painting
for weight painting in Blender a couple of topics that are useful are vertex groups and vertex indices. There is a way to show vertex ids in viewport using bpy.app.debug set to True. it will show the index of the selected vertex. The other concept is vertex group. when bones are skinned to mesh vertex groups are created on the mesh with the same name as the bones. Here are a couple of snippets to understand how to use python for weight painting.
#get weight of selected vertex (after setting weight in edit mode, needed to go to object mode and then back to have weight updated) >>> bpy.context.edit_object.data.vertices[7].groups[0].weight 0.699999988079071 >>> bpy.context.edit_object.data.vertices[7].groups[1].weight 0.30000001192092896
here the two bones we are setting weight on are named ‘Bone’ and ‘Bone.001’
#set weight on selected vertex (here vertex index is 7) >>> obj = bpy.context.edit_object #needs to be in edit mode #need to be in object mode for setting weights >>> obj.vertex_groups['Bone.001'].add([7], 0.8, 'REPLACE') >>> obj.vertex_groups['Bone'].add([7], 0.2, 'REPLACE') >>>
Properties/Custom Attributes
why are they used? how to create them?
properties or custom attributes can be added to various objects in blender for example on a pose bone. they are helpful to be able to control some behaviour for example turning on a blendshape or controlling a translation/rotation on a bone or turning on a constraint etc. They can be helpful to be controls the animator can use to make things happen for example a blink.
here’s an example for creating a property on a pose bone assuming bone and armature are already created:
from rna_prop_ui import rna_idprop_ui_prop_get pb = bpy.data.objects['Armature'].pose.bones['Bone'] pb['yeaah']=0.0 prop = rna_idprop_ui_prop_get(pb,'yeaah',create = True) prop['default']=0.0 prop['min']=0.0 prop['max']=100.0
to see the created property can scroll down N-panel to the properties section. or click on the bone icon in menu underneath outliner and scroll down to Custom Properties. clicking Edit of custom property can show that values that were set. hovering over the various fields in the custom property ui can show what attributes to be use for scripting for example it shows description, and soft_min, soft_max could also be set in the prop dictionary from above snippet.
to go back and add a description to the property just created:
>>> prop1 = rna_idprop_ui_prop_get(pb,'yeaah') >>> prop1['description'] = 'a comment'
Drivers
why are they used? how to create them?
Drivers are a way for one property/channel/attribute to communicate with a different property/channel/attribute. For example a blink animator control drives an action constraint that does the blink closing. Or the movement of a pose bone animator constraint in translation Z tells a shapekey/blendshape when to turn on. Its helpful before trying to script a driver to first use blender’s ui to create a driver. Also when creating the driver via blender ui its helpful to read the info panel to see what driver attribute gets changed when a field or drop down is changed in Driver’s tab ui.
here’s an example where an Empty objects translation in Z direction drives a pose bone’s translation in Y. it assumes the armature, bone and Empty have already been created and have the names used in snippet:
pb = bpy.data.objects['Armature'].pose.bones['Bone'] drv = pb.driver_add('location',1).driver drv.type = 'SCRIPTED' var = drv.variables.new() var.name = 'vary' var.targets[0].id=bpy.data.objects['Empty'] var.targets[0].data_path='location.z' drv.expression = 'vary*3'
here’s a brief breakdown of above snippet. first we save the pose bone inside of a variable. then we add a driver to the translation/location 1 index or Y channel. then we specify the driver as a scripted expression (we can use math in the relationship). next since we are using only one thing to drive the pose bone we create a single driver variable and store it into a variable. then we set certain properties on the driver variable and the driver itself. in the scripted expression we have used a multiplication by 3 to show math can be used in the expression.
for seeing the created driver can open a Graph Editor panel. Then switch from F-curve to Drivers in the drop down to the right of key. Then can click the y location channel in graph window. Then with the N-panel open for the graph editor panel can click on Drivers to see the created driver expression.
here’s another example where one bone’s custom property drives the Y location of a different bone. it assumes two bones are created and on the first bone there is a custom property of needed name.
pb = bpy.data.objects['Armature'].pose.bones['Bone.001'] drv = pb.driver_add('location',1).driver drv.type='SCRIPTED' var = drv.variables.new() var.name = 'varBlink' var.targets[0].id = bpy.data.objects['Armature'] var.targets[0].data_path = 'pose.bones["Bone"]["blink"]' drv.expression='varBlink'
(in the above example bone called Bone has a custom property called blink that drives the translation Y of bone called Bone.001.)
here’s another example where one bone’s translation in Y drives another bones translation in Y, it assumes armature and two bones have been created:
#one bones local translation drives another bones translation pb = bpy.data.objects['Armature'].pose.bones['Bone'] drv = pb.driver_add('location',1).driver var = drv.variables.new() var.name = 'varName' var.type = 'TRANSFORMS' var.targets[0].id = bpy.data.objects['Armature'] var.targets[0].bone_target = 'Bone.001' var.targets[0].transform_type = 'LOC_Y' var.targets[0].transform_space = 'LOCAL_SPACE' drv.expression = 'varName'
here’s another example similar to above but this time uses the SINGLE_PROP type of driver, it assumes armature and two bones have been created:
#one bones local translation drives another bones translation pb = bpy.data.objects['Armature'].pose.bones['Bone'] drv = pb.driver_add('location',1).driver var = drv.variables.new() var.name = 'varName' var.type = 'SINGLE_PROP' var.targets[0].id = bpy.data.objects['Armature'] var.targets[0].data_path = "pose.bones[\"Bone.001\"].location.y" drv.expression = 'varName'
to help get the data_path in above for mac can hold command and click on channel and choose ‘Copy Data Path’. for the above example still needed to add the .y to specify the channel to use of the driver object
Blendshapes/Shapekeys
for animating faces in 3d one way to get different poses of the face is using a blendshape which is also called Shapekey in Blender. Here are some snippets relating to shapekeys.
#getting vertex position of shape key for meshes >>> bpy.data.meshes['Plane'].shape_keys.key_blocks['Key 1'].data[3].co Vector((1.0, 2.113257646560669, 0.0)) >>> bpy.data.meshes['Plane'].shape_keys.key_blocks['Basis'].data[3].co Vector((1.0, 1.0, 0.0)) (vectors can be subtracted) #for lattices #number of lattice points >>> len(bpy.data.objects['Lattice'].data.shape_keys.key_blocks['Key 1'].data) 8 >>> >>> bpy.data.objects['Lattice'].data.shape_keys.key_blocks['Key 1'].data[7].co Vector((0.5, 1.5631684064865112, 0.5))these are some more snippets they will need to be edited as the mesh names need to be already created and the syntax needs to be edited.
#set position of shape key vertex to default >>> default_pos = bpy.data.meshes['Plane'].shape_keys.key_blocks['Basis'].data[3].co #3 is the vertex index >>> bpy.data.meshes['Plane'].shape_keys.key_blocks['Key 1'].data[3].co = default_pos >>> ##deleting all shape keys from mesh #first turn on selected shape key bpy.data.objects['Plane'].show_only_shape_key = True >>> for shp in bpy.data.meshes['Plane'].shape_keys.key_blocks: ... bpy.data.objects['Plane'].shape_key_remove(shp) ## #deleting a single key by its name >>> shp = bpy.data.meshes['Plane'].shape_keys.key_blocks["Key 1"] >>> type(shp) class 'bpy.types.ShapeKey' >>> bpy.data.objects['Plane'].shape_key_remove(shp) #deleting all shape keys leaving mesh as default mesh non_basis_shps = [shp for shp in bpy.data.meshes['Plane'].shape_keys.key_blocks if shp.name != 'Basis'] for shp in non_basis_shps: bpy.data.objects['Plane'].shape_key_remove(shp) #remove the basis basis_shp = bpy.data.meshes['Plane'].shape_keys.key_blocks[0] bpy.data.objects['Plane'].shape_key_remove(basis_shp) ######
Storyboard Video Sequencer
the video sequencer can be used as a tool for creating animatic of storyboards. here are a couple snippets on scripting with the video sequencer.
i think to start to add strip or scenes to video sequencer a sequence editor needs to be created.
>>> bpy.data.scenes['Scene'].sequence_editor_create() bpy.data.scenes['Scene']...SequenceEditor >>> >>> bpy.data.scenes['Scene'].sequence_editor bpy.data.scenes['Scene']...SequenceEditor >>> bpy.data.scenes['Scene'].sequence_editor is None False
if a scene has been added to the sequence editor this is snippet to change the start frame of the strip. This way to whole strip can be moved to wherever the frame_start is specified. all the animation in the scene is properly offset. setting frame_start to 1 says to start the scene strip at frame 1.
>>> bpy.data.scenes['Scene'].sequence_editor.sequences['Scene.001'].frame_start
i think to set the length of video strip want to use the frame_final_duration. as this way the strip can be moved and changing the frame_final_duration is not in global position but more the offset from the start of the strip to the end. so using frame_final_duration gives the value more likely expecting. for example setting it to 50 say scene is 50 frames long.
>>> bpy.data.scenes['Scene'].sequence_editor.sequences['Scene.001'].frame_final_duration = 90
here is a bit on adding a scene to the sequencer.
>>> scn = bpy.data.scenes['Scene'] >>> scn bpy.data.scenes['Scene'] >>> type(scn) class 'bpy.types.Scene' >>> bpy.data.scenes['Scene'].sequence_editor.sequences.new_scene('testA',scn,1,10) bpy.data.scenes['Scene'].sequence_editor.sequences_all["testA"] #bpy.ops.sequencer.scene_strip_add(frame_start=10,channel=1,scene = 'Scene' )
how to playblast a scene strip from sequencer? one way is have avi jpeg video set on the scene. also need to specify the output path set on scene
bpy.data.scenes['Scene'].render.filepath = '/Users/Nathaniel/Desktop/doodles_021922/images/' bpy.data.scenes['Scene'].render.image_settings.file_format = 'AVI_JPEG' bpy.ops.render.opengl(animation=True,sequencer = True)
Modifiers
these can be used to modify the shape of a mesh. an example is the armature modifier which is used to have joints/bones moving the skin of a mesh. another example is mesh deform modifier which allows one mesh to deform a second mesh. Here are some snippets:
### #trying to not use ops as much as possible, Cube to be moved by Cube.001 mesh_def= bpy.data.objects['Cube'].modifiers.new("mesh_def",type="MESH_DEFORM") #i think toggle in and out of object mode mesh_def.object = bpy.data.objects['Cube.001'] bpy.context.scene.objects.active = bpy.data.objects['Cube'] #in 2.8 this is different bpy.ops.object.meshdeform_bind(modifier=mesh_def.name) #i don’t think there’s a non ops replacement #i think toggle in and out of object mode ###some more snippets.
###removing modifier - assume object is selected bpy.ops.object.modifier_remove(modifier = bpy.data.objects['Plane'].modifiers[0].name )
Lattices
Lattices are a way to deform a mesh at a higher level of granularity than moving individual vertices. it also allows for smooth like deformations.
The process to create a lattice via scripting is to first create the lattice data then create the lattice object using that data. Then depending on whether you are in blender 2.79 or higher versions the object needs to be linked. For this example will show blender 2.79 way to link the object to the scene.
once the lattice is created it can then be assigned to a mesh to deform.
once a lattice is able to deform a mesh we can have other objects example joints/bones move the lattice points to get an even higher level of control. joint moves lattice point which moves vertices of mesh. One process to get joints to move lattice points is to add a hook modifier to the lattice and assign it a joint. It is slightly different on how to assign lattice points to the hook modifier depending whether in blender 2.79 or in newer versions.
to create a lattice
>>> dat = bpy.data.lattices.new("testLattice") >>> dat bpy.data.lattices['testLattice'] >>> type(dat)>>> latticeObj = bpy.data.objects.new("testLat",dat) >>> latticeObj bpy.data.objects['testLat'] >>> type(latticeObj) >>> bpy.data.scenes['Scene'].objects.link( bpy.data.objects['testLat'] )
to change dimension of lattice
>>> bpy.data.objects['testLat'].dimensions = (2,2,2) >>> bpy.data.objects['testLat'].data bpy.data.lattices['testLattice'] >>> type( bpy.data.objects['testLat'].data )>>> bpy.data.objects['testLat'].data.points_u = 5 >>> bpy.data.objects['testLat'].data.points_v = 5 >>> bpy.data.objects['testLat'].data.points_w = 5
to apply lattice to some geometry. here its applying lattice to a sphere named 'Sphere'
>>> bpy.data.objects['Sphere'].modifiers.new('lat','LATTICE') bpy.data.objects['Sphere'].modifiers["lat"] >>> type( bpy.data.objects['Sphere'].modifiers['lat'] )>>> bpy.data.objects['Sphere'].modifiers['lat'].object = bpy.data.objects['testLat']
to move lattice points with a joint/bone
>>> len(bpy.data.objects['testLat'].data.points) 125 ##get selected lattice point >>> [x for x in range(0,125) if bpy.data.objects['testLat'].data.points[x].select == True ] [112] #make hook modifier >>> bpy.data.objects['testLat'].modifiers.new('hookMod', type = 'HOOK' ) bpy.data.objects['testLat'].modifiers["hookMod"] >>> type( bpy.data.objects['testLat'].modifiers["hookMod"] )bpy.data.objects['testLat'].modifiers["hookMod"].object = bpy.data.objects['Armature'] bpy.data.objects['testLat'].modifiers["hookMod"].subtarget = 'Bone' #for blender 2.79 need to use ops to actual assign lattice points to bones (in higher blender versions hook modifier has vertex_indices_set to help assign points to bones) #need to be in edit mode of lattice with point selected bpy.ops.object.hook_assign(modifier= 'hookMod') bpy.ops.object.hook_reset(modifier= 'hookMod') #need this so lattice isnt in deformed state
Node Editor (Shaders)
introduction to using scripting to create materials. this knowledge can apply to other tools like Nuke, Katana, Houdini, Maya etc.
will go over creating a node, connecting two different nodes, changing attributes of a node, and moving node around in ui.
creating a node (can see type by hovering over Add menu items in node editor): bpy.data.materials['Material'].node_tree.nodes.new("ShaderNodeGeometry") connecting two nodes: >>> a = bpy.data.materials['Material'].node_tree.nodes['Geometry'].outputs['UV'] >>> b = bpy.data.materials['Material'].node_tree.nodes['Texture'].inputs['Vector'] >>> bpy.data.materials['Material'].node_tree.links.new(a,b) #a -> b bpy.data.node_groups['Shader Nodetree']...NodeLink changing an attribute/property of a node >>> bpy.data.materials['Material'].node_tree.nodes['Texture'].texture = bpy.data.textures['Tex'] or >>> bpy.data.materials['Material'].node_tree.nodes['Material'].inputs['DiffuseIntensity'].default_value = 0.5 changing position of node in node editor >>> bpy.data.materials['Material'].node_tree.nodes['Texture'].location = (-50,150) #x,y ## >>> type(bpy.data.materials['Material'].node_tree.nodes['Geometry'].outputs['UV']) class 'bpy.types.NodeSocketVector' >>> type(bpy.data.materials['Material'].node_tree.nodes['Geometry'] ) class 'bpy.types.ShaderNodeGeometry' >>> type(bpy.data.materials['Material'].node_tree) class 'bpy.types.ShaderNodeTree' ## >>> type(bpy.data.textures['Tex']) class 'bpy.types.ImageTexture'
Cloth
At the most basic level to get a mesh to act like cloth we create a cloth modifier. we then need to tell it to not completely fall by holding some vertices still via pinning. also we want it to collide with another mesh (ex a sphere) we add a collision modifier to the sphere. For the cloth mesh to get it to collide with the sphere we need to tell it to use collisions.
Before we can get cloth like behaviour on mesh we need to make a cloth modifier on it. To make the modifier need to know what type it is here the type is 'CLOTH'. also the modifier are added to the data object of type 'bpy_types.Object'. Here our cloth mesh is called 'Plane':
bpy.data.objects['Plane'].modifiers.new('testModifier','CLOTH')
after the cloth modifier is created there are two sets of settings to access that we can tweak to get the behaviour we want. They are the "collision_settings" and the "settings". Here is an example to tell the cloth to use collisions:
bpy.data.objects['Plane'].modifiers['testModifier'].collision_settings.use_collision = True
what above is saying is look at the mesh called Plane and its cloth modifier called 'testModifier' and turn on its collision ability.
suppose we have some vertices we have already assigned to a vertex group on the cloth mesh. We can tell the cloth modifier to not move these points. We can get what properties to use by hovering over the label in Blender's ui. These two lines should pin the cloth here 'Group' is the name of the vertex group of some vertices of cloth:
bpy.data.objects['Plane'].modifiers['testModifier'].settings.use_pin_cloth = True bpy.data.objects['Plane'].modifiers['testModifier'].settings.vertex_group_mass = 'Group'
finally to get the collision object to collide with our cloth we need to add a collision modifier on the collision object. Here the collision object is a sphere and the type of modifier is 'COLLISION':
>>> bpy.data.objects['Sphere'].modifiers.new('testCollision','COLLISION')
There are lots of properties to tweak to edit the behaviour of the cloth simulation. Many of those can be accessed using either the 'settings' or 'collision_settings' of the cloth modifier. One example is changing the mass of the cloth:
bpy.data.objects['Plane'].modifiers['testModifier'].settings.mass = 5
Again by hovering over the settings labels in the Physics panel shows what the properties are called when used in a script.
Intro to working with text files (xml,json)
The two file examples covering are for xml and json. (yaml is another option but won't cover it since i don't think its available in blender 2.79). writing to text files is one way to store information that can later be accessed when the information is read back in (for example could write out an action constraint to a json text file and that info could be later read back in possibly to a different blender file)#writing to xml import xml.etree.ElementTree as ET from xml.dom import minidom root = ET.Element('rig') bone1 = ET.SubElement( root, 'bone', name = "upArm.L") bone2 = ET.SubElement( root, 'bone', name = "lowArm.L") xmlstr = minidom.parseString( ET.tostring(root)).toprettyxml(indent=" ") with open('temp.xml','w') as ofile: ofile.write( xmlstr )
#reading xml import xml.etree.ElementTree as ET tree = ET.parse('temp.xml') root = tree.getroot() """seeing the type of first item >>> type(root[0])""" """from the help can see what attributes Element has #help(root[0]) >>> root[0].attrib {'name': 'upArm.L'} """ """ >>> for bone in root: ... print(bone.attrib) ... {'name': 'upArm.L'} {'name': 'lowArm.L'} """ """ >>> for bone in root: ... print(bone.tag) ... print(bone.attrib) ... bone {'name': 'upArm.L'} bone {'name': 'lowArm.L'} """ ############ #writing to json import json d = {"upLeg":(0,0,0)} with open('temp.json',"w") as outf: json.dump(d,outf,indent=4) #this is what temp.json looks like """ { "upLeg": [ 0, 0, 0 ] } """ #reading json import json with open('temp.json') as f: r = json.load(f) """ >>> print(r) {u'upLeg': [0, 0, 0]} """
Intro to running script without blender gui
Using subprocess is one way to be able to open blender and do some stuff in blender without needing to open the gui.
in this first example its kindof an intro to using subprocess to list things in current folder
import subprocess #calling ls from shell subprocess.call( ['ls','-ltrA'] ) #to get the output of the shell command p = subprocess.Popen( ['ls','-ltrA'], stdout = subprocess.PIPE, stderr = subprocess.PIPE ) out, err = p.communicate() print(out)
in this second example we try to create a folder.
#making a folder p = subprocess.Popen( ['mkdir','folderA'], stdout = subprocess.PIPE, stderr = subprocess.PIPE ) out, err = p.communicate() """ >>> out '' >>> err '' """ #but if make same folder again should get an error because it already exists """ >>> p = subprocess.Popen( ['mkdir','folderA'], stdout = subprocess.PIPE, stderr = subprocess.PIPE ) >>> out, err = p.communicate() >>> err 'mkdir: folderA: File exists\n' >>> out '' >>> """
in this last example we run a python script that list names of objects in a blender file (here called bfile.blend) by launching blender in the background
bcmd_str = """ import bpy obj_names = [] for obj in bpy.data.objects: obj_names.append(obj.name) print(obj_names) """ #and here is using subprocess to open blender in background and run that python snippet import subprocess blenderPath = "/Applications/blender.app/Contents/MacOS/blender" #change this. this was for a mac p = subprocess.Popen( [blenderPath,"--background", "bfile.blend", "--python-expr", bcmd_str], stdout = subprocess.PIPE, stderr = subprocess.PIPE ) out, err = p.communicate() objNames = eval(out.split("\n")[0]) #>>> objNames #['Camera', 'Cube', 'Lamp']
Intro to creating command line tool
This is an intro to writing a command line tool that uses arguments provided by user. Here we are using argparse for managing the arguments.
#!/usr/bin/env python """ need the shebang above to be able to run script without having python in front ex: ./simple_cmd_tool.py instead of: python simple_cmd_tool.py also needed to make script executable """ import argparse def doIt(_file, _mesh, _scale, _required): print("meat of tool using file:{0} mesh:{1} scale:{2} required:{3}".format(_file,_mesh,_scale,_required)) if __name__ == "__main__": parser = argparse.ArgumentParser(description="this is a simple example of a command line tool using arguments") parser.add_argument("-f", "--file", type=str, help="blender file name", default="defaultBlendFile") #the default above will be used if -f not used when calling command parser.add_argument("-m", "--mesh", action="store_true", help="should meshes be printed") #for above including -m in the arguments sets the variable to True otherwise its False parser.add_argument("-s", "--scale", type=float, help="scale value to use on objects", default=2.0) parser.add_argument("-r", "--required", type=int, help="this is a required argument it should be any int", required = True) args = parser.parse_args() print("blend file {0}".format(args.file)) print("meshes {0}".format(args.mesh)) print("scale {0}".format(args.scale)) print("required {0}".format(args.required)) #these parameters can then be used in the script. doIt(args.file, args.mesh, args.scale, args.required)
Some Simple Class Examples
Mirror Meshes Example
I’m by no means an expert on classes but here is one very simple example using a class to help with mirroring separate meshes and also mirror its bone parenting. it could have use in modelling or rigging. In writing this class i first wrote how it will be used the mirror methods then i slowly built those methods. i then started writing the helper methods. then i used blender to help test the helper methods slowly working out each of them. The helper methods that are not the focus of the script were given an underscore at the start.
here is an example usage of the class.geo = GeometryMirrorOperator( geometryName = "Cone.L") geo.mirror() geo.mirrorBoneParenting( armatureName = "Armature" )and here is the mirror mesh operator class and some helper methods.
class GeometryMirrorOperator(object): """a simple class to handle some mirroring operations for a Mesh. used for example if have a left hand with geo pieces and want to mirror it to right side. also should support mirroring the bone parenting """ def __init__(self, geometryName): """ @param geometryName : data object name for source geometry """ self.geometryName = geometryName self.geometryObject = bpy.data.objects[self.geometryName] self.mirrorGeometryName = None self.mirrorGeometryObject = None def mirror(self): """this is different from applying mirror modifier by not having to separate loose pieces. so mesh can have multiple separate pieces joined and those pieces will be joined in mirror result. """ if not _isHaveSide(self.geometryName): print("cannot find a side for {0}".format(self.geometryName) ) return False #duplicate the geometry dupGeoName = self._duplicate() dupRenamedName = self._mirrorName( dupGeoName ) #rename duplicate geometry self.mirrorGeometryName = dupRenamedName self.mirrorGeometryObject = bpy.data.objects[self.mirrorGeometryName] #may need to apply all modifiers to duplicate geo #sets its position to x mirror mirrorPosition = self._getMirrorPosition( axis = 'x' ) #assume mirror in x axis self.mirrorGeometryObject.location = mirrorPosition def mirrorBoneParenting(self, armatureName): """tries to mirror the bone parenting from one side to another does nothing if this geometry doesnt end in .L or .R also does nothing if this geometry doesnt have a bone parent ending in .L or .R @param armatureName - required. name for armature that has bones """ #validate all conditions needed for this method are met if not _isHaveSide(self.geometryName): print("cannot find a side for {0}".format(self.geometryName) ) return False if not _isHaveParent(self.geometryName): print("cannot find a parent for {0}".format(self.geometryName) ) return False boneName = self.geometryObject.parent_bone if not _isBoneHaveMirror(boneName, armatureName): print("cannot find a bone mirror for {0}".format(boneName) ) return False #figure out mirror bone and parent this geometry to it mirrorBone = _getMirrorBone(boneName, armatureName) print("found mirrorBone {0}".format(mirrorBone)) _parentGeoToBone( self.mirrorGeometryName, mirrorBone, armatureName ) #self.mirrorGeometryObject.parent = bpy.data.objects[armatureName] #self.mirrorGeometryObject.parent_bone = mirrorBone return True def _duplicate(self): """duplicate geometry for this class @return - string name for duplicated geometry """ sourceObj = self.geometryObject sourceObjData = sourceObj.data dupObjData = sourceObjData.copy() dupObj = sourceObj.copy() dupObj.data = dupObjData _addObjectToBlender( dupObj.name ) return dupObj.name def _mirrorName( self, editGeometryName ): """mirror the name of geometry of this class """ if not _isHaveSide(self.geometryName): print("cannot find a side for {0}".format(self.geometryName) ) return False editObj = bpy.data.objects[editGeometryName] if self.geometryName.endswith('.L'): editObj.name = self.geometryName.replace('.L','.R') else: editObj.name = self.geometryName.replace('.R','.L') return editObj.name def _getMirrorPosition( self, axis = 'x' ): """get the mirror position of this object """ result = (0,0,0) currentPosition = self.geometryObject.location #only supporting mirroring along x axis if axis in ["x"]: result = ( currentPosition[0]*(-1), currentPosition[1], currentPosition[2] ) return result def _addObjectToBlender( objectName ): """adds object with given name to blender. currently supporting 2.79 workflow adding to scene. for higher blender versions need to add to a collection """ obj = bpy.data.objects[objectName] bpy.context.scene.objects.link(obj) def _isHaveSide(name): """does thing with given name end with a side @return boolean """ if name.endswith('.L') or name.endswith('.R'): return True return False def _isHaveParent(objectName): """does object with given name have a bone parent @return boolean """ obj = bpy.data.objects[objectName] par = obj.parent_bone #if its in ui it should exist. for checking bone parent if par: return True return False def _isBoneHaveMirror(boneName, armatureName): """does bone have a mirror @return boolean True if bone has a mirror bone that exists """ if not _isHaveSide(boneName): return False #search armature for mirror bone mirrorBone = _getMirrorBone(boneName, armatureName) if mirrorBone: return True return False def _getMirrorBone(boneName, armatureName): """ @return - string for mirror bone name """ if not _isHaveSide(boneName): print("cannot find side for bone {0}".format(boneName) ) return False if boneName.endswith('.L'): result = boneName[:-2]+".R" elif boneName.endswith('.R'): result = boneName[:-2]+".L" #print("Testing >>> _getMirrorBone result {0}".format(result) ) if _isBoneExist(result, armatureName): return result return False def _isBoneExist(boneName, armatureName): """does given bone name exist in given armature """ armatureObj = bpy.data.objects[armatureName] return boneName in armatureObj.data.bones.keys() def _parentGeoToBone( geoName, boneName, armatureName ): geoObj = bpy.data.objects[geoName] armObj = bpy.data.objects[armatureName] bpy.ops.object.select_all(action='DESELECT') bpy.context.scene.objects.active = armObj #2.79 dependent i think bpy.ops.object.mode_set(mode='POSE') armObj.data.bones.active = armObj.data.bones[boneName] geoObj.select = True bpy.ops.object.parent_set(type = 'BONE')
Select opposite controls Example (assumes controls end in .L or .R)
import bpy class Selector(object): """used to select opposite controls. tested in blender 2.79 import imp testTool = imp.load_source("tool","/Users/Nathaniel/Documents/src_blender/python/snippets/selectOpposite.py") #change to path to python file ss = testTool.Selector() ss.selectOpposite() """ def __init__(self): self.armatureName = None self.activeBoneName = None if bpy.context.object.type == "ARMATURE": self.armatureName = bpy.context.object.name self.activeBoneName = bpy.context.active_pose_bone.name def selectOpposite(self): """ select opposite animator control. assumes animator control ends with .L or .R """ #get current selection. could be multiple msg = "requires a pose bone selected in pose mode. doing nothing" if not bpy.context.object: print(msg) return if not bpy.context.object.type == "ARMATURE": print(msg) return curSel = [] curSel = [bone.name for bone in bpy.context.selected_pose_bones] #reset selection self._clearSelection() #select opposite oppositeSel = [] for sel in curSel: _oppositeSel = self._getOpposite(sel) oppositeSel.append(_oppositeSel) self._select(oppositeSel) oppositeActiveBone = self._getOpposite( self.activeBoneName ) self._setActiveBone(oppositeActiveBone) def _getOpposite(self, arg): """get the opposite side animator control """ if not arg.endswith('.L') and not arg.endswith('.R'): print("requires selected to end in .L or .R") return None if arg.endswith('.L'): return arg.split('.L')[0]+'.R' else: return arg.split('.R')[0]+'.L' def _clearSelection(self): """deselect all""" bpy.ops.pose.select_all(action = 'DESELECT') def _select(self, nodes=[] ): """ select all nodes @param nodes - list of str ex: ['fk_arm.L'] """ for node in nodes: #i think this is specific to blender 2.79 bpy.data.objects[self.armatureName].pose.bones[node].bone.select = True def _setActiveBone(self, node): """set active pose bone to given node @param node - str ex: 'fk_arm.R' """ #i think this is specific to blender 2.79 bpy.data.objects[self.armatureName].data.bones.active = bpy.data.objects[self.armatureName].data.bones[node] #toggle to refresh bone highlight bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="POSE") #inspired by #https://www.11secondclub.com/forum/viewtopic.php?id=2068
Paint meshes a color Example
import bpy class Painter(object): """used to paint meshes a color. tested in blender 2.79 import imp testTool = imp.load_source("tool","/Users/Nathaniel/Documents/src_blender/python/snippets/paintMeshes.py") #change to path to python file matName = testTool._createMaterial(materialName = "bodyColor", rgb=(0,0,0.5) ) pp = testTool.Painter() pp.assignMaterialToMeshes(materialName = matName, meshes = ["Cube"]) """ def assignMaterialToMeshes( self, materialName = None, meshes = [], replace = True): """assign given material to given meshes. assumes all inputs exist @param materialName - str material name @param meshes - list of str of mesh names @param replace - bool whether or not to replace material. when true it removes all existing materials from mesh first """ #assign material to meshes materialObj = bpy.data.materials[materialName] if replace: #remove any existing material self._removeAllMaterials(meshes = meshes) for mesh in meshes: if self._isMaterialAdded( mesh, materialName ): continue bpy.data.objects[mesh].data.materials.append(materialObj) def _isMaterialAdded( self, geoName = None, materialName = None ): """check if material was already added to mesh """ geo = bpy.data.objects[geoName] mat = bpy.data.materials[materialName] if len(geo.data.materials) == 0: return False for m in geo.data.materials: if m.name == mat.name: return True def _removeAllMaterials( self, meshes = [] ): """remove all materials on given meshes @param meshes - list of str of mesh names """ for mesh in meshes: meshObject = bpy.data.objects[mesh] meshObject.active_material_index = 0 for i in range(0,len(meshObject.material_slots)): bpy.ops.object.material_slot_remove({'object':meshObject}) def _createMaterial( materialName = None, rgb = (0,1,0) ): """create material @param materialName - str material name @param rgb - tuple of 3 elements for rgb @return str - created material name """ if not materialName: print("requires a material name to use") return mat = None if not materialName in bpy.data.materials: #create material bpy.ops.object.mode_set(mode='OBJECT') mat = bpy.data.materials.new(materialName) else: mat = bpy.data.materials[materialName] #assign rgb to material even when material already exists mat.diffuse_color = rgb return materialName def _assignRGBToMaterial( materialName = None, rgb = (0,1,0) ): """assign rgb to material @param materialName - str material name @param rgb - tuple of 3 elements for rgb """ #assign rgb to material mat = bpy.data.materials[materialName] mat.diffuse_color = rgb #inspired by #https://www.11secondclub.com/forum/viewtopic.php?id=1128
Some Simple Addon Examples
These examples are specific to Blender 2.79 there are changes needed for getting them to work in newer Blender versions.
naShapekeyIOAddOn.py
for the ui portion this goes over using property group to keep ui elements organized. operators to be used for the buttons. register and unregister ui functions needed for addon to work in Blender.
for the content of the add-on it goes over shapekeys in some more detail. it goes over importing and exporting shape keys in Blender.
the main take away for writing the ui portion of the add-on is the use of bl_idname in the operator. this is later used in panel to know what operator is what. also the execute method of the operator is where the meat of what the button does gets added. for the panel the meat of what gets done is in the draw method. the bl_space_type and bl_region_type are also important as some scripting requires the ui to be in the viewport to preserve proper selections for example. in the register and unregister all the ui elements created operators, panels, properties need to be registered and unregistered with Blender.
import bpy from mathutils import Vector import json import os ####add on portion bl_info = { "name":"blendshapes import export", "description":"blendshapes import export", "category": "Object", "author":"Nathaniel Anozie", "blender":(2,79,0) } from bpy.types import( Operator, Panel, PropertyGroup ) from bpy.props import( StringProperty, PointerProperty ) class blendshapeIOProperties(PropertyGroup): dir_path = StringProperty( name = "Browse", description = "Pick path to export/import to/from either a file or a directory", maxlen = 200, subtype = 'FILE_PATH' ) mesh = StringProperty( name = "mesh", description = "mesh name with shapekeys - the data object name" ) blendshapes = StringProperty( name = "blendshapes", description = "optional specific blendshapes to export/import - comma separated. if none specified it exports/imports all blendshapes of mesh" ) class exportBlendshapeOperator(Operator): """export blendshape. first enter in some blendshapes and enter in face mesh with blendshapes to export. if no blendshapes provided it exports all blendshapes. """ bl_idname = "obj.exportblendshape" bl_label = "export" bl_options = {"REGISTER"} def execute(self, context): path = context.scene.blendshapeio_prop.dir_path mesh = context.scene.blendshapeio_prop.mesh blendshapes_arg = context.scene.blendshapeio_prop.blendshapes abs_path = bpy.path.abspath(path) if not os.path.isdir(abs_path): self.report({'INFO'},"please navigate to a folder to export blendshapes") return {'FINISHED'} if not mesh: self.report({'INFO'},"please enter in name for the mesh with shape keys - the data object name") return {'FINISHED'} blendshapes = [] if blendshapes_arg: blendshapes = blendshapes_arg.split(',') exportShapeKey(mesh = mesh, shapes = blendshapes, dirPath = abs_path ) return {'FINISHED'} class importBlendshapeOperator(Operator): """import blendshape. first enter in some blendshapes and enter in face mesh with blendshapes to import. if no blendshapes provided it imports all blendshapes that exist in folder specified. """ bl_idname = "obj.importblendshape" bl_label = "import" bl_options = {"REGISTER"} def execute(self, context): path = context.scene.blendshapeio_prop.dir_path mesh = context.scene.blendshapeio_prop.mesh blendshapes_arg = context.scene.blendshapeio_prop.blendshapes abs_path = bpy.path.abspath(path) if not os.path.isdir(abs_path) and not os.path.isfile(abs_path) : self.report({'INFO'},"please navigate to a folder or file to import blendshapes") return {'FINISHED'} if not mesh: self.report({'INFO'},"please enter in name for the mesh with shape keys - the data object name") return {'FINISHED'} import_file_paths = [] #if navigated to an exact file only import that file if os.path.isfile(abs_path): import_file_paths = [abs_path] else: #since navigated to a directory figure out files to import blendshapes = [] if blendshapes_arg: blendshapes = blendshapes_arg.split(',') #try to import only the blendshapes specified if blendshapes: for shp in blendshapes: fname = _getBlendshapeFileFromBlendshape(shp) import_file_paths.append( os.path.join(abs_path,fname) ) else: #otherwise try to import all blendshape files in folder import_file_paths = getAllFullFilesWithSuffixInFolder( folder = abs_path ) print("importing >>> mesh %s file_paths %s" %(mesh,import_file_paths) ) importShapeKey( mesh = mesh, shapeFiles = import_file_paths ) return {'FINISHED'} class naBlendshapeIOPanel(Panel): bl_label = "Blendshape Import/Export Panel" bl_space_type = "VIEW_3D" #needed for ops working properly bl_region_type = "UI" def draw(self, context): layout = self.layout layout.prop(context.scene.blendshapeio_prop, "dir_path") layout.prop(context.scene.blendshapeio_prop, "mesh") layout.prop(context.scene.blendshapeio_prop, "blendshapes") layout.operator( "obj.exportblendshape" ) layout.operator( "obj.importblendshape" ) def register(): bpy.utils.register_class(exportBlendshapeOperator) bpy.utils.register_class(importBlendshapeOperator) bpy.utils.register_class(naBlendshapeIOPanel) bpy.utils.register_class(blendshapeIOProperties) bpy.types.Scene.blendshapeio_prop = PointerProperty( type = blendshapeIOProperties ) def unregister(): bpy.utils.unregister_class(exportBlendshapeOperator) bpy.utils.unregister_class(importBlendshapeOperator) bpy.utils.unregister_class(naBlendshapeIOPanel) bpy.utils.unregister_class(blendshapeIOProperties) del bpy.types.Scene.blendshapeio_prop ##end ui portion def exportShapeKey( mesh = '', shapes = [], dirPath = '' ): """exports one file per blendshape mesh - is the data.objects name, it can be different from data.objects.data name shapes - are the actual shapekey names wish to export. if none provided it expects to export all blendshapes note it saves file names of form shape_shapeName where it replaces spaces with underscores so exporting a 'Key 1' shape with an already saved shape_Key_1.json would overwrite that shape dirPath - is directory to save the shape json files """ """form of shape key data written out (vertDelta is in order of all vertices) { 'shape':{ 'shapeName':'Key 1', 'vertDelta':[(0,0,0),(0,.25,.35)...(0,0,0)] } } """ if not os.path.isdir(dirPath): print('Requires an out directory that exists to write shapekey file') return if not mesh in bpy.data.objects: print('Requires mesh: %s to exist in scene' %mesh) return if not bpy.data.objects[mesh].type == 'MESH': print('Requires mesh type for %s' %mesh) return #require mesh to have shape keys mesh_obj = bpy.data.objects[mesh].data if mesh_obj.shape_keys is None: print('requires mesh %s to have shape keys' %mesh) return #exit if no Basis shape key basis_name = 'Basis' if not basis_name in mesh_obj.shape_keys.key_blocks.keys(): print('requires a basis shape named Basis on mesh %s exiting' %mesh) return if not shapes: print('trying to export all blendshapes') shapes = [x.name for x in mesh_obj.shape_keys.key_blocks if x.name != 'Basis'] #start looping over shape keys to export print("trying to export shapes: %s" %shapes) for shp in shapes: shape_dict = {} num_verts = 0 #check shp shapekey exists on mesh if not shp in mesh_obj.shape_keys.key_blocks.keys(): print('could not find shapekey %s in mesh %s so skipping it' %(shp,mesh) ) continue #todo: verify only upper and lower case letters and spaces and dots in shape key name num_verts = len( mesh_obj.shape_keys.key_blocks[shp].data ) vert_delta_list = [] for i in range(num_verts): vdelta = mesh_obj.shape_keys.key_blocks[basis_name].data[i].co - mesh_obj.shape_keys.key_blocks[shp].data[i].co vert_delta_list.append( (vdelta.x, vdelta.y, vdelta.z) ) shape_dict['shape']={'shapeName':shp,'vertDelta':vert_delta_list} #export this shapekey and continue to next shp_filename = _getBlendshapeFileFromBlendshape(shp) shp_file = os.path.join(dirPath,shp_filename) with open( shp_file, 'w' ) as f: json.dump(shape_dict, f, indent=4) def _getBlendshapeFileFromBlendshape(blendshape = ''): shp_edit = blendshape.replace(' ','_') #dont want spaces in file names return shp_edit+'_shape.json' def importShapeKey( mesh = '', shapeFiles = [] ): """ it should create the shape key if it doesnt exist if it does exist should overwrite only that shape key. not saving mesh in shape paths in case want to import onto a different named mesh with same topology. mesh - is mesh data.objects name it needs to have at least a Basis shape. shapeFiles - list of full path to blendshape files - a shape file has vertex deltas from basis for a single shape key. """ def _isShapeExists( mesh, shapeName ): mesh_obj = bpy.data.objects[mesh].data return shapeName in mesh_obj.shape_keys.key_blocks def _sculptShape( mesh, shapeName, vertDelta ): #sculpt shape using delta from basis mesh_obj = bpy.data.objects[mesh].data ##go through each vertex using the basis and the delta stored on disc for vid in range(num_mesh_verts): basis_pos = mesh_obj.shape_keys.key_blocks['Basis'].data[vid].co shp_pos = Vector( vertDelta[vid] ) #Vector so can subtract it #move vertices to sculpt shape mesh_obj.shape_keys.key_blocks[shapeName].data[vid].co = basis_pos - shp_pos ##check inputs #assert mesh exists #assert shapeFiles provided #assert mesh has a shapekey #check Basis shape exists on mesh if not mesh in bpy.data.objects: print('Requires mesh: %s to exist in scene' %mesh) return if not shapeFiles: print('Requires shape file names to import') return mesh_obj = bpy.data.objects[mesh].data #bpy.data.meshes[mesh] mesh_name = mesh_obj.name if mesh_obj.shape_keys is None: print("Requires at least a Basis shape on mesh %s" %mesh) return #exit if no Basis shape key on mesh basis_name = 'Basis' if not basis_name in mesh_obj.shape_keys.key_blocks.keys(): print('requires a basis shape named Basis on mesh %s exiting' %mesh) return #need to be in object mode to edit shape keys bpy.ops.object.mode_set(mode="OBJECT") #loop over shape files for shp_file in shapeFiles: if not os.path.exists(shp_file): print('could not find shape file %s , skipping' %shp_file) continue #read shape from disc shp_dict = {} with open( shp_file, 'r' ) as f: shp_dict = json.load(f) shapeName = shp_dict['shape']['shapeName'] vertDelta = shp_dict['shape']['vertDelta'] #skip if saved shape has different topology as mesh num_mesh_verts = len( mesh_obj.shape_keys.key_blocks['Basis'].data ) num_shp_file_verts = len(vertDelta) if num_mesh_verts != num_shp_file_verts: print('Requires shape stored on file %s to have same topology as mesh %s, skipping' %(shp_file,mesh) ) continue if _isShapeExists( mesh, shapeName ): #Edit existing shape _sculptShape( mesh, shapeName, vertDelta ) else: #Create a new shape new_shp = bpy.data.objects[mesh].shape_key_add(shapeName) _sculptShape( mesh, shapeName, vertDelta ) new_shp.interpolation = 'KEY_LINEAR' def getAllFullFilesWithSuffixInFolder( folder = '', suffix = '_shape.json' ): """assumes all files end with given suffix. returns full paths """ if not os.path.isdir(folder): print("requires a folder to exist: %s" %folder) return [] files_in_folder = os.listdir(folder) return [os.path.join(folder,x) for x in files_in_folder if x.endswith(suffix) ]
Testing code in separate python file
one way to begin writing add-ons is to first test parts of the code the methods, classes in Blender. here is an example snippet of code to test parts of a future add-on.
this is the simplest way i've been using to test a code snippet. using imp.load_source.
import imp testTool = imp.load_source("tool","pathToPythonFile.py") #change to path to python file testTool.putTestMethodHere()
this is another way to test some methods when not using ui
import bpy import sys #example if wanted to test script without addon part. change to your path here sys.path.append('/Users/Nathaniel/Documents/src_blender/python/naBlendShape') import naShapekeyIO as mod import imp imp.reload(mod) #mod.exportShapeKey( mesh = 'Plane', shapes = ['Key 1'], dirPath = '/Users/Nathaniel/Documents/src_blender/python/naBlendShape/tmp' ) #mod.importShapeKey( mesh = 'Plane', shapeFiles = ['/Users/Nathaniel/Documents/src_blender/python/naBlendShape/tmp/shape_Key_1.json'])
notice the use of sys.path.append so we can add the path of where the python file is we are working on. also notice imp.reload(mod) which is a way of reloading the module to pickup any latest changes.
Thanks for reading.