├── .gitignore ├── README.md └── shapeKeyHelper_menu.py /.gitignore: -------------------------------------------------------------------------------- 1 | /desktop.ini -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ShapeKey-Helpers 2 | 3 | This Blender addon / python script is a collection of operators aimed at automating repetitive tasks in Blender when working with shape keys. 4 | 5 | More information on how to use this addon and detailed descriptions of the individual operators can be found in this Blenderartists thread: https://blenderartists.org/t/addon-shapekey-helpers/1131849 6 | -------------------------------------------------------------------------------- /shapeKeyHelper_menu.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "ShapeKey Helpers", 3 | "author": "Ott, Jan, Tyler Walker (BeyondDev)", 4 | "version": (1, 2, 0), 5 | "blender": (2, 80, 0), 6 | "description": "Adds three operators: 'Split Shapekeys', 'Apply Modifiers and Keep Shapekeys' and 'Apply Selected Shapekey as Basis'", 7 | "warning": "", 8 | "wiki_url": "https://blenderartists.org/t/addon-shapekey-helpers/1131849", 9 | "category": "'Mesh", 10 | } 11 | 12 | import bpy 13 | from inspect import currentframe, getframeinfo 14 | 15 | 16 | #__________________________________________________________________________ 17 | #__________________________________________________________________________ 18 | 19 | 20 | def SetActiveShapeKey (name): 21 | bpy.context.object.active_shape_key_index = bpy.context.object.data.shape_keys.key_blocks.keys().index(name) 22 | 23 | #__________________________________________________________________________ 24 | #__________________________________________________________________________ 25 | 26 | O = bpy.ops 27 | 28 | class ShapeKeySplitter(bpy.types.Operator): 29 | """Creates a new object with the shapekeys split based on two vertex groups, named 'left' and 'right', that you must create manually""" 30 | bl_idname = "object.shape_key_splitter" 31 | bl_label = "Split Shapekeys" 32 | 33 | def execute(self, context): 34 | 35 | O.object.select_all(action='DESELECT') 36 | bpy.context.active_object.select_set(True) 37 | #____________________________ 38 | #Generate copy of object 39 | #____________________________ 40 | originalName = bpy.context.object.name 41 | O.object.duplicate_move() 42 | bpy.context.object.name = originalName + "_SplitShapeKeys" 43 | 44 | 45 | listOfKeys = [] 46 | 47 | index = 0 48 | 49 | #__________________________________________________ 50 | 51 | for s_key in bpy.context.object.data.shape_keys.key_blocks: 52 | 53 | if(index == 0): 54 | index = index + 1 55 | continue 56 | 57 | if s_key.name.endswith('.L') or s_key.name.endswith('.R') or s_key.name.endswith('.B'): 58 | continue 59 | 60 | 61 | listOfKeys.append(s_key.name) 62 | 63 | #__________________________________________________ 64 | 65 | for name in listOfKeys: 66 | 67 | SetActiveShapeKey(name) 68 | 69 | savedName = name 70 | savedShapeKey = bpy.context.object.active_shape_key 71 | 72 | 73 | #Create left version 74 | 75 | O.object.shape_key_clear() 76 | 77 | SetActiveShapeKey(savedName) 78 | savedShapeKey.vertex_group = 'left' 79 | savedShapeKey.value = 1.0 80 | 81 | O.object.shape_key_add(from_mix=True) 82 | bpy.context.object.active_shape_key.name = savedName + ".L" 83 | 84 | 85 | #Create right version 86 | 87 | O.object.shape_key_clear() 88 | 89 | SetActiveShapeKey(savedName) 90 | savedShapeKey.vertex_group = 'right' 91 | savedShapeKey.value = 1.0 92 | 93 | O.object.shape_key_add(from_mix=True) 94 | bpy.context.object.active_shape_key.name = savedName + ".R" 95 | 96 | 97 | for name in listOfKeys: 98 | 99 | #Set index to target shapekey 100 | SetActiveShapeKey(name) 101 | #Remove 102 | O.object.shape_key_remove(all=False) 103 | 104 | 105 | return {'FINISHED'} 106 | 107 | 108 | # Beyond Dev 109 | def driver_settings_copy(copy_drv, tar_drv): 110 | scn_prop = bpy.context.scene.skmp_props 111 | 112 | tar_drv.driver.type = copy_drv.driver.type 113 | tar_drv.driver.use_self = copy_drv.driver.use_self 114 | 115 | for var in copy_drv.driver.variables: 116 | new_var = tar_drv.driver.variables.new() 117 | new_var.name = var.name 118 | new_var.type = var.type 119 | 120 | count = 0 121 | for tar in var.targets: 122 | new_var.targets[count].bone_target = tar.bone_target 123 | new_var.targets[count].data_path = tar.data_path 124 | 125 | if scn_prop.rename_driver_bones: 126 | new_var.targets[count].bone_target = tar.bone_target.replace( 127 | scn_prop.text_filter, scn_prop.text_rename) 128 | new_var.targets[count].data_path = tar.data_path.replace( 129 | scn_prop.text_filter, scn_prop.text_rename) 130 | 131 | new_var.targets[count].id = tar.id 132 | new_var.targets[count].transform_space = tar.transform_space 133 | new_var.targets[count].transform_type = tar.transform_type 134 | 135 | count += 1 136 | 137 | tar_drv.driver.expression = copy_drv.driver.expression 138 | print('copied driver settings...!') 139 | return 140 | 141 | # Beyond Dev 142 | def copy_drivers(copy_ob, tar_ob, copy_key, tar_key): 143 | copy_sk = copy_ob.data.shape_keys 144 | copy_drivers = copy_sk.animation_data.drivers 145 | 146 | if tar_ob.data.animation_data == None: 147 | tar_ob.data.animation_data_create() 148 | tar_sk = tar_ob.data.shape_keys 149 | 150 | for drv in copy_drivers: 151 | drv_name = drv.data_path.replace('key_blocks["', '') 152 | drv_name = drv_name.replace('"].value', '') 153 | 154 | if copy_key.name == drv_name: 155 | # new_driver = tar_sk.key_blocks[len( 156 | # tar_sk.key_blocks)-1].driver_add('value', -1) 157 | new_driver = tar_sk.key_blocks[tar_key.name].driver_add('value', -1) 158 | print('executing copy...') 159 | driver_settings_copy(drv, new_driver) 160 | 161 | 162 | class ShapeKeyPreserver(bpy.types.Operator): 163 | """Creates a new object with all modifiers applied and all shape keys + DRIVERS preserved""" 164 | """NOTE: Blender can only combine objects with a matching number of vertices. """ 165 | """As a result, you need to make sure that your shape keys don't change the number of vertices of the mesh. """ 166 | """Modifiers like 'Subdivision Surface' can always be applied without any problems, other modifiers like 'Bevel' or 'Edgesplit' may not.""" 167 | 168 | bl_idname = "object.shape_key_preserver" 169 | bl_label = "Apply Modifiers and Keep Shapekeys+Drivers" 170 | 171 | @classmethod 172 | def poll(cls, context): 173 | global updatedObject 174 | updatedObject = bpy.context.active_object 175 | return updatedObject and updatedObject.type == 'MESH' 176 | 177 | def execute(self, context): 178 | global updatedObject 179 | 180 | oldName = bpy.context.active_object.name 181 | 182 | #Change context to 'VIEW_3D' and store old context 183 | oldContext = bpy.context.area.type 184 | bpy.context.area.type = 'VIEW_3D' 185 | 186 | #selection setup, preserve drivers on this object 187 | driverObject = bpy.context.active_object 188 | 189 | #copy of driverObject to transfer everything from 190 | bpy.ops.object.duplicate( 191 | {"object" : driverObject, 192 | "selected_objects" : [driverObject]}, 193 | linked = False 194 | ) 195 | #store reference to copy of driver object 196 | originalObject = bpy.context.active_object 197 | 198 | #delete all drivers on this new object to allow proper shapekey transfer. Shapekeys cannot be modified while drivers exist on them. 199 | for shapekey in originalObject.data.shape_keys.key_blocks: 200 | # for driver in shapekey.id_data.animation_data.drivers: 201 | # shapekey.driver_remove(driver.data_path) 202 | 203 | try: 204 | shapekey_name = shapekey.name 205 | shapekeys = originalObject.data.shape_keys 206 | drivers = shapekeys.animation_data.drivers 207 | dr = drivers.find(f'key_blocks["{shapekey_name}"].value') 208 | if dr is not None: 209 | drivers.remove(dr) 210 | except: 211 | pass 212 | 213 | originalObject.select_set(True) 214 | 215 | listOfShapeInstances = [] 216 | listOfShapeKeyValues = [] 217 | 218 | #_______________________________________________________________ 219 | 220 | 221 | #Deactivate any armature modifiers 222 | for mod in originalObject.modifiers: 223 | if mod.type == 'ARMATURE': 224 | originalObject.modifiers[mod.name].show_viewport = False 225 | 226 | index = 0 227 | for shapekey in originalObject.data.shape_keys.key_blocks: 228 | if(index == 0): 229 | index = index + 1 230 | continue 231 | listOfShapeKeyValues.append(shapekey.value) 232 | 233 | index = 0 234 | for shapekey in originalObject.data.shape_keys.key_blocks: 235 | 236 | if(index == 0): 237 | index = index + 1 238 | continue 239 | 240 | bpy.ops.object.select_all(action='DESELECT') 241 | 242 | originalObject.select_set(True) 243 | bpy.context.view_layer.objects.active = originalObject 244 | 245 | bpy.ops.object.shape_key_clear() 246 | 247 | shapekey.value = 1.0 248 | 249 | #save name 250 | #____________________________ 251 | shapekeyname = shapekey.name 252 | 253 | #create new object from shapekey and add it to list 254 | #____________________________ 255 | bpy.ops.object.duplicate(linked=False, mode='TRANSLATION') 256 | bpy.ops.object.convert(target='MESH') 257 | listOfShapeInstances.append(bpy.context.active_object) 258 | 259 | #rename new object 260 | #____________________________ 261 | bpy.context.object.name = shapekeyname 262 | 263 | bpy.ops.object.select_all(action='DESELECT') 264 | originalObject.select_set(True) 265 | 266 | bpy.context.view_layer.objects.active = originalObject 267 | 268 | #_____________________________________________________________ 269 | #Prepare final empty container model for all those shape keys: 270 | #_____________________________________________________________ 271 | 272 | bpy.context.view_layer.objects.active = originalObject 273 | bpy.ops.object.shape_key_clear() 274 | 275 | bpy.ops.object.duplicate(linked=False, mode='TRANSLATION') 276 | newObject = bpy.context.active_object 277 | 278 | bpy.ops.object.shape_key_clear() 279 | bpy.ops.object.shape_key_remove(all=True) 280 | 281 | for mod in newObject.modifiers: 282 | # Not actually sure why this is necessary, but blender crashes without it. :| - Stel 283 | bpy.ops.object.mode_set(mode = 'EDIT') 284 | bpy.ops.object.mode_set(mode = 'OBJECT') 285 | if mod.type != 'ARMATURE': 286 | if (2, 90, 0) > bpy.app.version: 287 | bpy.ops.object.modifier_apply(apply_as='DATA', modifier=mod.name) 288 | else: 289 | bpy.ops.object.modifier_apply(modifier=mod.name) 290 | 291 | errorDuringShapeJoining = False 292 | 293 | for object in listOfShapeInstances: 294 | 295 | bpy.ops.object.select_all(action='DESELECT') 296 | newObject.select_set(True) 297 | object.select_set(True) 298 | 299 | bpy.context.view_layer.objects.active = newObject 300 | 301 | 302 | print("Trying to join shapes.") 303 | 304 | result = bpy.ops.object.join_shapes() 305 | 306 | if(result != {'FINISHED'}): 307 | print ("Could not add " + object.name + " as shape key.") 308 | errorDuringShapeJoining = True 309 | 310 | if(errorDuringShapeJoining == False): 311 | print("Success!") 312 | 313 | if(errorDuringShapeJoining == False): 314 | #Reset old shape key values on new object 315 | 316 | driverObject.select_set(True) 317 | newObject.select_set(True) 318 | bpy.context.view_layer.objects.active = newObject 319 | 320 | index = 0 321 | for shapekey, driverShapekey in zip(newObject.data.shape_keys.key_blocks, driverObject.data.shape_keys.key_blocks): 322 | if(index == 0): 323 | index = index + 1 324 | continue 325 | shapekey.value = listOfShapeKeyValues[index-1] 326 | index = index + 1 327 | #BEYOND DEV: Copy Drivers from old object to new object 328 | if driverShapekey.has_driver(): 329 | copy_drivers(driverObject, newObject, driverShapekey, shapekey) 330 | 331 | #Reset old shape key values on original object 332 | index = 0 333 | for shapekey in originalObject.data.shape_keys.key_blocks: 334 | if(index == 0): 335 | index = index + 1 336 | continue 337 | shapekey.value = listOfShapeKeyValues[index-1] 338 | index = index + 1 339 | 340 | 341 | #Select and delete all temporal shapekey objects 342 | bpy.ops.object.select_all(action='DESELECT') 343 | 344 | for object in listOfShapeInstances: 345 | object.select_set(True) 346 | 347 | bpy.ops.object.delete(use_global=False) 348 | 349 | #delete all drivers on new object that did not exist on original object 350 | for shapekey in newObject.data.shape_keys.key_blocks: 351 | # if shapekey name doesn't exist in original object, delete it 352 | if shapekey.name not in newObject.data.shape_keys.key_blocks: 353 | newObject.shape_key_remove(shapekey.name) 354 | 355 | 356 | #Reactivate armature modifiers on old and new object 357 | 358 | for mod in originalObject.modifiers: 359 | if mod.type == 'ARMATURE': 360 | originalObject.modifiers[mod.name].show_viewport = True 361 | 362 | for mod in newObject.modifiers: 363 | if mod.type == 'ARMATURE': 364 | newObject.modifiers[mod.name].show_viewport = True 365 | 366 | 367 | # Delete the copy object 368 | bpy.data.objects.remove(originalObject) 369 | 370 | # Hide driver backup object 371 | driverObject.hide = True 372 | 373 | newObject.name = oldName + "_Applied" 374 | 375 | 376 | bpy.context.area.type = oldContext 377 | 378 | #Beyond Dev - For other scripts to check identify the new object 379 | self.updatedObject = newObject 380 | 381 | return {'FINISHED'} 382 | 383 | 384 | 385 | class ShapeKeyApplier(bpy.types.Operator): 386 | """Replace the 'Basis' shape key with the currently selected shape key""" 387 | bl_idname = "object.shape_key_applier" 388 | bl_label = "Apply Selected Shapekey as Basis" 389 | 390 | def execute(self, context): 391 | 392 | O.object.select_all(action='DESELECT') 393 | bpy.context.object.select_set(True) 394 | 395 | #____________________________ 396 | #Generate copy of object 397 | #____________________________ 398 | originalName = bpy.context.object.name 399 | O.object.duplicate_move() 400 | bpy.context.object.name = originalName + "_Applied_Shape_Key" 401 | 402 | shapeKeyToBeApplied_name = bpy.context.object.active_shape_key.name 403 | 404 | listOfKeys = [] 405 | 406 | #__________________________________________________ 407 | #Store all shape keys in a list 408 | #__________________________________________________ 409 | 410 | for s_key in bpy.context.object.data.shape_keys.key_blocks: 411 | 412 | if s_key.name == shapeKeyToBeApplied_name: 413 | continue 414 | 415 | listOfKeys.append(s_key.name) 416 | 417 | #__________________________________________________ 418 | 419 | for name in listOfKeys: 420 | 421 | SetActiveShapeKey(name) 422 | currentShapeKey = bpy.context.object.active_shape_key 423 | 424 | SetActiveShapeKey(shapeKeyToBeApplied_name) 425 | applyShapeKey = bpy.context.object.active_shape_key 426 | 427 | #Add new shapekey from mix 428 | O.object.shape_key_clear() 429 | 430 | currentShapeKey.value = 1.0 431 | applyShapeKey.value = 1.0 432 | 433 | O.object.shape_key_add(from_mix=True) 434 | bpy.context.object.active_shape_key.name = currentShapeKey.name + "_" 435 | 436 | 437 | for name in listOfKeys: 438 | 439 | #Set index to target shapekey 440 | SetActiveShapeKey(name) 441 | #Remove 442 | O.object.shape_key_remove(all=False) 443 | 444 | 445 | SetActiveShapeKey(shapeKeyToBeApplied_name) 446 | O.object.shape_key_remove(all=False) 447 | 448 | #Remove the "_" at the end of each shapeKey 449 | for s_key in bpy.context.object.data.shape_keys.key_blocks: 450 | 451 | s_key.name = s_key.name[:-1] 452 | 453 | 454 | return {'FINISHED'} 455 | 456 | 457 | 458 | # Ott / Jan : 459 | # I'm honestly not sure how to add this to an existing menu in 2.8, so rather than go 460 | # down a rabbit-hole of research, I'm just adding a panel, because it works and is 461 | # quick to do. Someone should probably look at this and do better than I have. 462 | class PT_shapeKeyHelpers(bpy.types.Panel): 463 | """Creates a Panel in the Object properties window""" 464 | bl_label = "Shapekey tools" 465 | bl_idname = "SHAPEHELPER_PT_uipanel" 466 | bl_space_type = 'PROPERTIES' 467 | bl_region_type = 'WINDOW' 468 | bl_context = "data" 469 | 470 | 471 | 472 | @classmethod 473 | def poll(cls, context): 474 | return bpy.context.active_object.type == 'MESH' 475 | 476 | 477 | def draw(self, context): 478 | self.layout.separator() 479 | self.layout.operator(ShapeKeySplitter.bl_idname, text="Split Shapekeys", icon="FULLSCREEN_ENTER") 480 | self.layout.operator(ShapeKeyPreserver.bl_idname, text="Apply Modifiers and Keep Shapekeys", icon="MODIFIER") # TODO: Use this to Apply Rig Modifier to Avatar 481 | self.layout.operator(ShapeKeyApplier.bl_idname, text="Apply Selected Shapekey as Basis", icon="KEY_HLT") # TODO: Can use this for Applying Body Type in Avatar Generator 482 | 483 | 484 | classes = ( 485 | ShapeKeySplitter, 486 | ShapeKeyPreserver, 487 | ShapeKeyApplier, 488 | PT_shapeKeyHelpers 489 | 490 | ) 491 | 492 | 493 | def register(): 494 | from bpy.utils import register_class 495 | for cls in classes: 496 | register_class(cls) 497 | 498 | def unregister(): 499 | from bpy.utils import unregister_class 500 | for cls in reversed(classes): 501 | unregister_class(cls) 502 | 503 | 504 | 505 | if __name__ == "__main__": 506 | register() 507 | 508 | 509 | --------------------------------------------------------------------------------