├── __init__.py ├── LICENSE ├── README.md └── RetargetCell.py /__init__.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "Rig quick map", 3 | 'version': (1, 0, 0), 4 | "blender": (2, 83, 5), 5 | 'author': 'Naru', 6 | 'description': 'A tool to do quick rig map', 7 | "category": "Rigging", 8 | 'wiki_url': 'https://github.com/Arisego/BlenderQuickMap', 9 | 'tracker_url': 'https://github.com/Arisego/BlenderQuickMap/issues', 10 | } 11 | 12 | 13 | from . import RetargetCell 14 | 15 | 16 | def register(): 17 | print("QuickMap register") 18 | RetargetCell.register() 19 | 20 | def unregister(): 21 | print("QuickMap unregister") 22 | RetargetCell.unregister() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 naru 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blender Quick Map plugin 2 | 3 | A simple blender addon helps retarget animation. 4 | 5 | ## How it work 6 | 7 | The method used to retargeting animation in addon is shared by *Maciej Szcześnik* in Unity Connect article "Retargeting animations with Blender 2.80". I made this plugin just to speed up the retargeting steps, so we can make things happen more quickly. 8 | 9 | > As unity connect is gone, you may visit this [archived version](https://web.archive.org/web/20200812235638/https://connect.unity.com/p/retargeting-animations-with-blender-2-80) instead. It's an archive of the [original link](https://connect.unity.com/p/retargeting-animations-with-blender-2-80). 10 | 11 | This plugins helps to automatically generating and changing follow chains , and provides save/load to save out retarget map config. 12 | 13 | ## Concepts 14 | 15 | Some concept name differs from the origin article: 16 | 17 | * Source Armature: The armature you want to have animation on it 18 | * Target Armature: Animation is on this armature 19 | * Follower A: *Source Bone* will follow this object by constrain 20 | * Follower B: This object is parent of *Follower A*, and will follow *Target Bone* 21 | 22 | The follow chain works like this: 23 | 24 | 1. Animation moves bone in *Taret Armature* 25 | 2. *Target Bone* in *Target Armature* will move *Follower B* 26 | 3. *Follower B* then moves *Follower A* 27 | 4. *Follower A* then moves *Source Bone* in *Source Armature* 28 | 29 | ## Usage 30 | 31 | You will see a "QuickMap" tab in side panels, try press *N* in main view if you do not see any side panel. 32 | 33 | Follow the tips in panel will help, you can find more detail steps in [wiki page](https://github.com/Arisego/BlenderQuickMap/wiki). 34 | -------------------------------------------------------------------------------- /RetargetCell.py: -------------------------------------------------------------------------------- 1 | from bpy_extras.io_utils import ExportHelper, ImportHelper 2 | import json 3 | import bpy 4 | 5 | from bpy.types import (Operator, 6 | Panel, 7 | PropertyGroup, 8 | UIList) 9 | 10 | from bpy.props import (IntProperty, 11 | BoolProperty, 12 | StringProperty, 13 | CollectionProperty, 14 | PointerProperty) 15 | 16 | # Constrain Name 17 | l_Source_Location_Name = "naru_source_location" 18 | l_Source_Rotation_Name = "naru_source_rotation" 19 | 20 | 21 | # Class to be regist 22 | classes = () 23 | 24 | 25 | # Collection Operate 26 | def make_collection(collection_name): 27 | if collection_name in bpy.data.collections: # Does the collection already exist? 28 | return bpy.data.collections[collection_name] 29 | else: 30 | new_collection = bpy.data.collections.new(collection_name) 31 | bpy.context.scene.collection.children.link( 32 | new_collection) # Add the new collection under a parent 33 | return new_collection 34 | 35 | 36 | # ListBox 37 | # Show list of retarget cell 38 | class QM_UL_ControlCell(UIList): 39 | """ 40 | List box for retarget cells, this is the list in side panel 41 | """ 42 | # Constants (flags) 43 | # Be careful not to shadow FILTER_ITEM! 44 | VGROUP_EMPTY = 1 << 0 45 | 46 | # Custom properties, saved with .blend file. 47 | use_filter_name_reverse: bpy.props.BoolProperty(name="Reverse Name", default=False, options=set(), 48 | description="Reverse name filtering") 49 | 50 | use_order_name: bpy.props.BoolProperty(name="Name", default=False, options=set(), 51 | description="Sort groups by their name (case-insensitive)") 52 | 53 | use_filter_linked: bpy.props.BoolProperty(name="Linked", default=False, options=set(), 54 | description="Filter linked only") 55 | 56 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): 57 | row = layout.row() 58 | 59 | subrow = row.row(align=True) 60 | subrow.prop(item, "source_name", text="", emboss=False) 61 | 62 | subrow = subrow.row(align=True) 63 | if len(item.target_name) > 0: 64 | subrow.prop(item, "source_follow_location", text="", 65 | toggle=True, icon="CON_LOCLIKE") 66 | subrow.prop(item, "source_follow_rotation", text="", 67 | toggle=True, icon="CON_ROTLIKE") 68 | 69 | def invoke(self, context, event): 70 | pass 71 | 72 | def draw_filter(self, context, layout): 73 | # Nothing much to say here, it's usual UI code... 74 | row = layout.row() 75 | 76 | subrow = row.row(align=True) 77 | subrow.prop(self, "filter_name", text="") 78 | icon = 'ZOOM_OUT' if self.use_filter_name_reverse else 'ZOOM_IN' 79 | subrow.prop(self, "use_filter_name_reverse", text="", icon=icon) 80 | 81 | icon = 'LINKED' if self.use_filter_linked else 'UNLINKED' 82 | subrow.prop(self, "use_filter_linked", text="", icon=icon) 83 | 84 | subrow = layout.row(align=True) 85 | subrow.label(text="Order by:") 86 | subrow.prop(self, "use_order_name", toggle=True) 87 | 88 | # Filter 89 | # 1. we use [source_name] not [name] 90 | # 2. provide filter for targeted cell 91 | def filter_items(self, context, data, propname): 92 | statelist = getattr(data, propname) 93 | helper_funcs = bpy.types.UI_UL_list 94 | 95 | # Default return values. 96 | flt_flags = [] 97 | flt_neworder = [] 98 | 99 | # Filtering by name 100 | if self.filter_name: 101 | flt_flags = helper_funcs.filter_items_by_name(self.filter_name, self.bitflag_filter_item, statelist, "source_name", 102 | reverse=self.use_filter_name_reverse) 103 | if not flt_flags: 104 | flt_flags = [self.bitflag_filter_item] * len(statelist) 105 | 106 | # Filtering cell with target 107 | if self.use_filter_linked: 108 | for i, item in enumerate(statelist): 109 | if len(item.target_name) == 0: 110 | flt_flags[i] &= ~self.bitflag_filter_item 111 | 112 | # Reorder by name 113 | if self.use_order_name: 114 | flt_neworder = helper_funcs.sort_items_by_name( 115 | statelist, "source_name") 116 | 117 | return flt_flags, flt_neworder 118 | 119 | 120 | classes += (QM_UL_ControlCell,) 121 | 122 | 123 | # Generate constrain from [Follower B] to [Target bone] 124 | def AddTargetLink(InFollower, InArmature, InTargetBone): 125 | print("(AddTargetLink) Follower: " + str(InFollower)) 126 | print("(AddTargetLink) Target: " + str(InTargetBone)) 127 | 128 | # location sontrain 129 | ts_Name = "naru_follow_location" 130 | if ts_Name in InFollower.constraints: 131 | print("Target exists") 132 | ts_CopyLocation = InFollower.constraints[ts_Name] 133 | pass 134 | else: 135 | print("New Constrain: Location") 136 | ts_CopyLocation = InFollower.constraints.new('COPY_LOCATION') 137 | ts_CopyLocation.name = ts_Name 138 | 139 | ts_CopyLocation.target = InArmature 140 | ts_CopyLocation.subtarget = InTargetBone 141 | 142 | # rotation constrain 143 | ts_Name = "naru_follow_rotation" 144 | if ts_Name in InFollower.constraints: 145 | print("Target exists") 146 | ts_CopyRotation = InFollower.constraints[ts_Name] 147 | pass 148 | else: 149 | print("New Constrain: Rotation") 150 | ts_CopyRotation = InFollower.constraints.new('COPY_ROTATION') 151 | ts_CopyRotation.name = ts_Name 152 | 153 | ts_CopyRotation.target = InArmature 154 | ts_CopyRotation.subtarget = InTargetBone 155 | 156 | 157 | # Target changes 158 | # Trigged while target bone changes in single cell 159 | # 1. Will Call RefreshSourceLink() to try generate [Follower A] and [Follower B] if not generated yet 160 | # 2. Call AddTargetLink to connect [follower B] to [Target bone] 161 | def OnTargetChange(self, value): 162 | """Target changed in single QM_Map_Control""" 163 | context = bpy.context 164 | obj = context.active_object 165 | scn = context.scene 166 | 167 | # Try pick out source armature 168 | if obj and obj.type == 'ARMATURE': 169 | qm_state = obj.quickmap_state 170 | elif scn.quickmap_armature: 171 | obj = scn.quickmap_armature 172 | if obj.type == 'ARMATURE': 173 | qm_state = obj.quickmap_state 174 | else: 175 | print("OnTargetChange: no armature 01") 176 | return 177 | else: 178 | print("OnTargetChange: no armature 02") 179 | return 180 | 181 | item = self 182 | 183 | # Step1: Try create followers if not exist 184 | ts_SrcBone = obj.pose.bones[item.source_name] 185 | TryInitLink(ts_SrcBone, item) 186 | RefreshSourceLink(ts_SrcBone, item) 187 | 188 | # Step2: Try link follower to target bone 189 | ts_FollowTarget = item.follow_target 190 | AddTargetLink(ts_FollowTarget, qm_state.map_target, item.target_name) 191 | 192 | 193 | # Source follow type changed 194 | # Will call RefreshSourceLink 195 | def OnSourceFollowTypeChange(self, value): 196 | """ 197 | Refresh link type from [Source bone] to [Follower A] 198 | """ 199 | 200 | print("OnSourceFollowTypeChange") 201 | print(self) 202 | print(value) 203 | 204 | context = bpy.context 205 | obj = context.active_object 206 | scn = context.scene 207 | 208 | # Try find source armature 209 | if obj and obj.type == 'ARMATURE': 210 | pass 211 | elif scn.quickmap_armature: 212 | obj = scn.quickmap_armature 213 | if obj.type == 'ARMATURE': 214 | pass 215 | else: 216 | print("OnSourceFollowTypeChange: no armature 01") 217 | return 218 | else: 219 | print("OnSourceFollowTypeChange: no armature 02") 220 | return 221 | 222 | #item = qm_state.quickmap_celllist[qm_state.quickmap_celllist_index] 223 | item = self 224 | 225 | print("Change source follow type: " + item.source_name) 226 | ts_SrcBone = obj.pose.bones[item.source_name] 227 | RefreshSourceLink(ts_SrcBone, item) 228 | 229 | 230 | class QM_Map_Control(bpy.types.PropertyGroup): 231 | """Single data for link""" 232 | 233 | follow_target: PointerProperty(type=bpy.types.Object) 234 | """ 235 | [Follower B] Parent of source_follow, will follow bone in target animation 236 | """ 237 | 238 | source_follow: PointerProperty(type=bpy.types.Object) 239 | """ 240 | [Follower A] Source bone will follow this 241 | """ 242 | 243 | # Switch: Whether source bone follow [Follower A] with location 244 | source_follow_location: BoolProperty( 245 | update=OnSourceFollowTypeChange, default=True) 246 | 247 | # Switch: Whether source bone follow [Follower A] with rotation 248 | source_follow_rotation: BoolProperty( 249 | update=OnSourceFollowTypeChange, default=True) 250 | 251 | # relate rotaion between [Follower A] and [Follower B] 252 | # [Follower B] is parent of [Follower A] 253 | relate_a_b_rotation: bpy.props.FloatVectorProperty( 254 | size=3, update=OnTargetChange, description="Rotation between follower A and follower B", subtype='EULER') 255 | 256 | # relate location between [Follower A] and [Follower B] 257 | # [Follower B] is parent of [Follower A] 258 | relate_a_b_location: bpy.props.FloatVectorProperty( 259 | size=3, update=OnTargetChange, description="Rotation between follower A and follower B", subtype='TRANSLATION') 260 | 261 | source_name: StringProperty() 262 | """ 263 | Name of source bone 264 | """ 265 | 266 | target_name: StringProperty(update=OnTargetChange) 267 | """ 268 | Name of target bone 269 | """ 270 | 271 | 272 | classes += (QM_Map_Control,) 273 | 274 | 275 | # Will try create [Follower A] and [Folloer B] 276 | # [Source Bone] => [Follower A] => [Folloer B] => [Target Animation] 277 | def TryInitLink(InBone, InItem): 278 | """ 279 | Try create followers if not exists. 280 | 281 | Parameters 282 | ---------- 283 | InBone : Bone 284 | Bone in the source armature 285 | 286 | InItem : QM_Map_Control 287 | Map data 288 | 289 | """ 290 | ts_CollectionName = "qm_test" 291 | ts_Collection = make_collection(ts_CollectionName) 292 | 293 | ts_Bone = InBone # Source bone 294 | ts_BoneName = ts_Bone.name 295 | 296 | # FollowTarget|[Folloer B] 297 | if not InItem.follow_target: 298 | ts_FollowTargetName = ts_BoneName + "_T" 299 | ts_FollowTarget = bpy.data.objects.new(ts_FollowTargetName, None) 300 | 301 | # due to the new mechanism of "collection" 302 | ts_Collection.objects.link(ts_FollowTarget) 303 | 304 | # empty_draw was replaced by empty_display 305 | ts_FollowTarget.empty_display_size = 0.05 306 | ts_FollowTarget.empty_display_type = 'CUBE' 307 | 308 | InItem.follow_target = ts_FollowTarget 309 | 310 | # SourceFollow|[Follower A] 311 | if not InItem.source_follow: 312 | ts_SourceFollowName = ts_BoneName + "_S" 313 | ts_SourceFollow = bpy.data.objects.new(ts_SourceFollowName, None) 314 | 315 | # due to the new mechanism of "collection" 316 | ts_Collection.objects.link(ts_SourceFollow) 317 | 318 | # empty_draw was replaced by empty_display 319 | ts_SourceFollow.empty_display_size = 0.05 320 | ts_SourceFollow.empty_display_type = 'ARROWS' 321 | 322 | InItem.source_follow = ts_SourceFollow 323 | 324 | # Parent [Follower A] to [Follower B] 325 | if InItem.follow_target and InItem.source_follow: 326 | InItem.source_follow.parent = InItem.follow_target 327 | InItem.source_follow.rotation_euler = InItem.relate_a_b_rotation 328 | InItem.source_follow.location = InItem.relate_a_b_location 329 | 330 | 331 | # Refresh constrain link from [Source Bone] to [Follower A] 332 | # Will create constrain if not exist 333 | def RefreshSourceLink(InBone, InItem): 334 | if not InItem.source_follow: 335 | print("RefreshSourceLink: Item no source follow") 336 | return 337 | 338 | # If target is none, just set influence to 0, constrains are still keeeped in place 339 | ts_HasTarget = len(InItem.target_name) > 0 340 | 341 | # [Source Bone] to [Follower A]: Location Constrain 342 | if l_Source_Location_Name in InBone.constraints: 343 | print("- Location exsits") 344 | ts_CopyLocation = InBone.constraints[l_Source_Location_Name] 345 | pass 346 | else: 347 | ts_CopyLocation = InBone.constraints.new('COPY_LOCATION') 348 | ts_CopyLocation.target = InItem.source_follow 349 | ts_CopyLocation.name = l_Source_Location_Name 350 | 351 | if InItem.source_follow_location and ts_HasTarget: 352 | ts_CopyLocation.influence = 1.0 353 | else: 354 | ts_CopyLocation.influence = 0.0 355 | 356 | # [Source Bone] to [Follower A]: Rotation Constrain 357 | if l_Source_Rotation_Name in InBone.constraints: 358 | print("- Rotation exists") 359 | ts_CopyRotation = InBone.constraints[l_Source_Rotation_Name] 360 | pass 361 | else: 362 | ts_CopyRotation = InBone.constraints.new('COPY_ROTATION') 363 | ts_CopyRotation.target = InItem.source_follow 364 | ts_CopyRotation.name = l_Source_Rotation_Name 365 | 366 | if InItem.source_follow_rotation and ts_HasTarget: 367 | ts_CopyRotation.influence = 1.0 368 | else: 369 | ts_CopyRotation.influence = 0.0 370 | 371 | 372 | # Operator 373 | # Generate source bone list from selected armature 374 | class QM_OT_GenerateMap(Operator): 375 | """Generate controls for current armature""" 376 | bl_idname = "qm.generate_control" 377 | bl_label = "Generate Control" 378 | bl_description = "Generate controls for current armature" 379 | bl_options = {'INTERNAL'} 380 | 381 | @classmethod 382 | def poll(cls, context): 383 | obj = context.active_object 384 | return bpy.context.mode == 'OBJECT' and obj and obj.type == 'ARMATURE' 385 | 386 | def execute(self, context): 387 | obj = context.active_object 388 | obj.quickmap_state.quickmap_celllist.clear() 389 | for ts_Bone in obj.pose.bones: 390 | item = obj.quickmap_state.quickmap_celllist.add() 391 | item.source_name = ts_Bone.name 392 | 393 | return{'FINISHED'} 394 | 395 | 396 | classes += (QM_OT_GenerateMap,) 397 | 398 | 399 | # Operator 400 | # Bake control into animation, all constrains in source bone will be removed 401 | class QM_QT_BakeControl(Operator): 402 | """Bake control to animation""" 403 | bl_idname = "qm.bake_control" 404 | bl_label = "Bake Control" 405 | bl_description = "Bake control to animation" 406 | bl_options = {'INTERNAL'} 407 | 408 | @classmethod 409 | def poll(cls, context): 410 | scene = context.scene 411 | obj = scene.quickmap_armature 412 | return bpy.context.mode == 'OBJECT' and obj and obj.type == 'ARMATURE' 413 | 414 | def execute(self, context): 415 | td_FrameEnd = bpy.context.scene.frame_end 416 | print("Start bake: " + str(td_FrameEnd)) 417 | 418 | if 0 == td_FrameEnd: 419 | return{"CANCEL"} 420 | 421 | scene = context.scene 422 | arm = scene.quickmap_armature 423 | 424 | # ensure that only the armature is selected in Object mode 425 | bpy.ops.object.mode_set(mode='OBJECT') 426 | bpy.ops.object.select_all(action='DESELECT') 427 | arm.select_set(True) 428 | scene.view_layers[0].objects.active = arm 429 | 430 | # enter pose mode and bake 431 | bpy.ops.object.mode_set(mode='POSE') 432 | bpy.ops.pose.select_all(action='SELECT') 433 | bpy.ops.nla.bake(frame_start=0, frame_end=td_FrameEnd, step=1, only_selected=True, 434 | visual_keying=True, clear_constraints=True, clear_parents=False, 435 | use_current_action=True, bake_types={'POSE'}) 436 | return{'FINISHED'} 437 | 438 | 439 | classes += (QM_QT_BakeControl,) 440 | 441 | 442 | # Operator 443 | # Refresh control link, trigger OnTargetChange for every source bone that has target setted 444 | # Used for link repair after Bake Animation 445 | class QM_QT_RefreshControl(Operator): 446 | """Refresh control link""" 447 | bl_idname = "qm.refresh_link" 448 | bl_label = "Refresh Control" 449 | bl_description = "Refresh all the link" 450 | bl_options = {'INTERNAL'} 451 | 452 | @classmethod 453 | def poll(cls, context): 454 | scene = context.scene 455 | obj = scene.quickmap_armature 456 | return bpy.context.mode == 'OBJECT' and obj and obj.type == 'ARMATURE' 457 | 458 | def execute(self, context): 459 | td_FrameEnd = bpy.context.scene.frame_end 460 | print("Start bake: " + str(td_FrameEnd)) 461 | 462 | if 0 == td_FrameEnd: 463 | return{"CANCEL"} 464 | 465 | scene = context.scene 466 | arm = scene.quickmap_armature 467 | 468 | for ts_State in arm.quickmap_state.quickmap_celllist: 469 | if len(ts_State.target_name) == 0: 470 | continue 471 | 472 | print("Relink: " + ts_State.source_name + 473 | " -> " + ts_State.target_name) 474 | OnTargetChange(ts_State, context) 475 | 476 | return{'FINISHED'} 477 | 478 | 479 | classes += (QM_QT_RefreshControl,) 480 | 481 | 482 | """ 483 | Read write config from file 484 | """ 485 | 486 | 487 | # Save list config to file 488 | class OP_BF_SaveToFile(bpy.types.Operator, ExportHelper): 489 | bl_idname = 'qm.bf_save' 490 | bl_label = 'Save Config' 491 | filename_ext = '.qm.bf' 492 | 493 | filter_glob: bpy.props.StringProperty( 494 | default='*.qm.bf', 495 | options={'HIDDEN'}, 496 | maxlen=255 497 | ) 498 | 499 | def execute(self, context): 500 | 501 | obj = context.active_object 502 | scn = context.scene 503 | 504 | if obj and obj.type == 'ARMATURE': 505 | qm_state = obj.quickmap_state 506 | elif scn.quickmap_armature: 507 | obj = scn.quickmap_armature 508 | if obj.type == 'ARMATURE': 509 | qm_state = obj.quickmap_state 510 | else: 511 | print(text="Select an armature to work t1") 512 | return {'CANCELLED'} 513 | else: 514 | print(text="Select an armature to work t2") 515 | return {'CANCELLED'} 516 | 517 | with open(self.filepath, 'w') as f: 518 | tlist_QmStates = {} 519 | for ts_State in qm_state.quickmap_celllist: 520 | if len(ts_State.target_name) == 0: 521 | continue 522 | 523 | tdic_State = {} 524 | tdic_State["target_name"] = ts_State.target_name 525 | tdic_State["source_follow_location"] = ts_State.source_follow_location 526 | tdic_State["source_follow_rotation"] = ts_State.source_follow_rotation 527 | tdic_State["relate_a_b_rotation"] = [ts_State.relate_a_b_rotation[0], 528 | ts_State.relate_a_b_rotation[1], ts_State.relate_a_b_rotation[2]] 529 | tdic_State["relate_a_b_location"] = [ts_State.relate_a_b_location[0], 530 | ts_State.relate_a_b_location[1], ts_State.relate_a_b_location[2]] 531 | tlist_QmStates[ts_State.source_name] = tdic_State 532 | 533 | ts_DumpedJson = json.dumps(tlist_QmStates) 534 | print(ts_DumpedJson) 535 | f.write(ts_DumpedJson) 536 | 537 | return {'FINISHED'} 538 | 539 | 540 | classes += (OP_BF_SaveToFile,) 541 | 542 | 543 | # Load list config from file 544 | class OP_BF_LoadFromFile(bpy.types.Operator, ImportHelper): 545 | bl_idname = 'qm.bf_load' 546 | bl_label = 'Load Config' 547 | 548 | filter_glob: bpy.props.StringProperty( 549 | default='*.qm.bf', 550 | options={'HIDDEN'}, 551 | maxlen=255 552 | ) 553 | 554 | def execute(self, context): 555 | 556 | obj = context.active_object 557 | scn = context.scene 558 | 559 | if obj and obj.type == 'ARMATURE': 560 | qm_state = obj.quickmap_state 561 | elif scn.quickmap_armature: 562 | obj = scn.quickmap_armature 563 | if obj.type == 'ARMATURE': 564 | qm_state = obj.quickmap_state 565 | else: 566 | print(text="Select an armature to work t1") 567 | return {'CANCELLED'} 568 | else: 569 | print(text="Select an armature to work t2") 570 | return {'CANCELLED'} 571 | 572 | # Read file 573 | with open(self.filepath, 'r') as f: 574 | ts_LoadedList = json.load(f) 575 | print("Loaded") 576 | print(ts_LoadedList) 577 | 578 | ts_ReadLen = len(ts_LoadedList) 579 | if 0 == ts_ReadLen: 580 | print("Nothing readed: " + self.filepath) 581 | return {'CANCELLED'} 582 | 583 | # deserialize json to state map 584 | for ts_State in qm_state.quickmap_celllist: 585 | ts_StateSrcName = ts_State.source_name 586 | if ts_StateSrcName in ts_LoadedList: 587 | ts_CurLoad = ts_LoadedList[ts_StateSrcName] 588 | ts_State.target_name = ts_CurLoad["target_name"] 589 | ts_State.source_follow_location = ts_CurLoad["source_follow_location"] 590 | ts_State.source_follow_rotation = ts_CurLoad["source_follow_rotation"] 591 | ts_State.relate_a_b_rotation[0] = ts_CurLoad["relate_a_b_rotation"][0] 592 | ts_State.relate_a_b_rotation[1] = ts_CurLoad["relate_a_b_rotation"][1] 593 | ts_State.relate_a_b_rotation[2] = ts_CurLoad["relate_a_b_rotation"][2] 594 | 595 | ts_State.relate_a_b_location[0] = ts_CurLoad["relate_a_b_location"][0] 596 | ts_State.relate_a_b_location[1] = ts_CurLoad["relate_a_b_location"][1] 597 | ts_State.relate_a_b_location[2] = ts_CurLoad["relate_a_b_location"][2] 598 | 599 | print("Read in config: " + ts_StateSrcName + 600 | " -> " + ts_State.target_name) 601 | OnTargetChange(ts_State, context) 602 | 603 | # bpy.ops.naru.bf_clearlist() 604 | # print(len(ts_LoadedList)) 605 | # for ts_ItemKey in ts_LoadedList: 606 | # print(ts_ItemKey) 607 | # print(ts_LoadedList[ts_ItemKey]) 608 | 609 | # bpy.context.scene.naru_bonelist_index = len( 610 | # bpy.context.scene.naru_bonelist)-1 611 | # bpy.context.area.tag_redraw() 612 | return {'FINISHED'} 613 | 614 | 615 | classes += (OP_BF_LoadFromFile,) 616 | 617 | 618 | # Draw read/write button to UI 619 | def _Draw_SaveLoad(layout): 620 | row = layout.row() 621 | row.operator('qm.bf_load', icon='FILEBROWSER') 622 | row.operator('qm.bf_save', icon='FILE_TICK') 623 | 624 | 625 | # Side panel, main view of plugin 626 | # As by blender's design, property could not be modifed in ui 627 | class TP_LinkGenerate(bpy.types.Panel): 628 | bl_idname = "QM_PT_ViewPanel" 629 | bl_label = "LinkGenerate" 630 | bl_category = "QuickMap" 631 | bl_space_type = "VIEW_3D" 632 | bl_region_type = "UI" 633 | 634 | @classmethod 635 | def poll(cls, context): 636 | return True 637 | 638 | def draw(self, context): 639 | layout = self.layout 640 | obj = context.active_object 641 | scn = context.scene 642 | 643 | # Work in object mode only to avoid mistacke 644 | if bpy.context.mode != 'OBJECT': 645 | layout.label(text="Work in object mode only", icon='ERROR') 646 | return 647 | 648 | # Draw button for Generate control ist 649 | box = layout.box() 650 | box.operator("qm.generate_control") 651 | 652 | # Pick out source armature 653 | if obj and obj.type == 'ARMATURE': 654 | qm_state = obj.quickmap_state 655 | elif scn.quickmap_armature: 656 | obj = scn.quickmap_armature 657 | if obj.type == 'ARMATURE': 658 | qm_state = obj.quickmap_state 659 | else: 660 | layout.label(text="Select an armature to work.", icon='ERROR') 661 | return 662 | else: 663 | layout.label(text="Select an armature to work", icon='INFO') 664 | return 665 | 666 | layout.label(text="Follower: "+obj.name, icon='INFO') 667 | layout.template_list("QM_UL_ControlCell", "", 668 | qm_state, "quickmap_celllist", 669 | qm_state, "quickmap_celllist_index" 670 | ) 671 | 672 | # Stop show target oprators if no control, avoid miss oprate 673 | if len(qm_state.quickmap_celllist) == 0: 674 | layout.label(text="Generate control to continue", icon='INFO') 675 | return 676 | 677 | # Target armature that provide animation 678 | layout.separator() 679 | layout.prop(qm_state, 'map_target', 680 | text='Target', icon='ARMATURE_DATA') 681 | 682 | box = layout.box() 683 | if qm_state.map_target == None: 684 | box.label(text="Target is missing", icon='UNLINKED') 685 | return 686 | 687 | if qm_state.map_target == obj: 688 | box.label(text="Target is source", icon='ERROR') 689 | return 690 | 691 | box.label(text="Target: "+qm_state.map_target.name, icon='LINKED') 692 | 693 | # Show detail control for selected source bone 694 | tb_FollowerValid = False 695 | try: 696 | item = qm_state.quickmap_celllist[qm_state.quickmap_celllist_index] 697 | tb_FollowerValid = len(item.source_name) > 0 698 | pass 699 | except IndexError: 700 | pass 701 | 702 | if tb_FollowerValid: 703 | box.label(text="Follower bone: " + 704 | item.source_name, icon='COPY_ID') 705 | 706 | box.prop_search(item, 'target_name', qm_state.map_target.pose, 707 | 'bones', text='', icon='BONE_DATA') 708 | 709 | row = box.row() 710 | row.enabled = len(item.target_name) > 0 711 | row.prop(item, "source_follow_location", text="Location", 712 | toggle=True, icon="CON_LOCLIKE") 713 | row.prop(item, "source_follow_rotation", text="Rotation", 714 | toggle=True, icon="CON_ROTLIKE") 715 | 716 | box.prop(item, "relate_a_b_rotation", text="Relative Rotation") 717 | box.prop(item, "relate_a_b_location", text="Relative Location") 718 | else: 719 | box.label(text="Choose a follower bone above", icon='PANEL_CLOSE') 720 | 721 | # Draw read/write config from file 722 | _Draw_SaveLoad(layout) 723 | 724 | # Buttons used for animation bake 725 | layout.separator() 726 | box = layout.box() 727 | box.operator("qm.bake_control") 728 | box.operator("qm.refresh_link") 729 | 730 | 731 | classes += (TP_LinkGenerate,) 732 | 733 | 734 | # OnSelect event handler for control map list 735 | def OnSelect_ControlList(self, value): 736 | """ 737 | Called while single control in list is selected 738 | 739 | Parameters 740 | ---------- 741 | self : QmState 742 | Parent Property 743 | value : struct 744 | Not used 745 | """ 746 | 747 | if bpy.context.mode != 'OBJECT': 748 | self.report({'INFO'}, "OnSelect_ControlList works in object mode only") 749 | return 750 | 751 | try: 752 | item = self.quickmap_celllist[self.quickmap_celllist_index] 753 | pass 754 | except IndexError: 755 | self.report({'INFO'}, "OnSelect_ControlList got invalid index") 756 | return 757 | 758 | print("Select: " + item.source_name) 759 | 760 | # Cache source armature as we may change selected object 761 | if bpy.context.active_object and bpy.context.active_object.type == 'ARMATURE': 762 | bpy.context.scene.quickmap_armature = bpy.context.active_object 763 | 764 | # Select linked follower if possible 765 | bpy.ops.object.select_all(action='DESELECT') 766 | ts_FollowTarget = item.follow_target 767 | if ts_FollowTarget: 768 | ts_FollowTarget.select_set(state=True) 769 | bpy.context.view_layer.objects.active = ts_FollowTarget 770 | 771 | ts_SourceFollow = item.source_follow 772 | if ts_SourceFollow: 773 | ts_SourceFollow.select_set(state=True) 774 | 775 | return 776 | 777 | 778 | # Core data for quick map control 779 | # grouped together to avoid Register/UnRegister for every single data 780 | class QMState(bpy.types.PropertyGroup): 781 | map_target: bpy.props.PointerProperty( 782 | type=bpy.types.Object, 783 | name="FollowTarget", 784 | poll=lambda self, obj: obj.type == 'ARMATURE' and obj != bpy.context.object 785 | ) 786 | quickmap_celllist: CollectionProperty(type=QM_Map_Control) 787 | quickmap_celllist_index: IntProperty(update=OnSelect_ControlList) 788 | 789 | 790 | classes += (QMState,) 791 | 792 | 793 | """ 794 | Blender class register/unregister 795 | """ 796 | 797 | 798 | def register(): 799 | from bpy.utils import register_class 800 | for cls in classes: 801 | register_class(cls) 802 | 803 | bpy.types.Object.quickmap_state = bpy.props.PointerProperty(type=QMState) 804 | bpy.types.Scene.quickmap_armature = bpy.props.PointerProperty( 805 | name="QuickMap Armature", type=bpy.types.Object) 806 | 807 | 808 | def unregister(): 809 | from bpy.utils import unregister_class 810 | for cls in reversed(classes): 811 | unregister_class(cls) 812 | 813 | del bpy.types.Object.quickmap_state 814 | del bpy.types.Scene.quickmap_armature 815 | --------------------------------------------------------------------------------