Skip to content
Snippets Groups Projects
generate.py 23.5 KiB
Newer Older
  • Learn to ignore specific revisions
  • # SPDX-License-Identifier: GPL-2.0-or-later
    
    
    import bpy
    
    import time
    
    from .utils.errors import MetarigError
    
    from .utils.bones import new_bone
    from .utils.layers import ORG_LAYER, MCH_LAYER, DEF_LAYER, ROOT_LAYER
    
    from .utils.naming import ORG_PREFIX, MCH_PREFIX, DEF_PREFIX, ROOT_NAME, make_original_name, change_name_side, get_name_side, Side
    
    from .utils.widgets import WGT_PREFIX
    from .utils.widgets_special import create_root_widget
    
    from .utils.mechanism import refresh_all_drivers
    
    from .utils.misc import gamma_correct, select_object
    
    from .utils.collections import ensure_collection, list_layer_collections, filter_layer_collections_by_object
    
    from .utils.rig import get_rigify_type
    
    
    RIG_MODULE = "rigs"
    
    class Timer:
        def __init__(self):
            self.timez = time.time()
    
        def tick(self, string):
            t = time.time()
            print(string + "%.3f" % (t - self.timez))
            self.timez = t
    
    
    
    class Generator(base_generate.BaseGenerator):
        def __init__(self, context, metarig):
            super().__init__(context, metarig)
    
    Nathan Vegdahl's avatar
    Nathan Vegdahl committed
    
    
            self.id_store = context.window_manager
    
        def find_rig_class(self, rig_type):
            rig_module = rig_lists.rigs[rig_type]["module"]
    
        def __switch_to_usable_collection(self, obj, fallback=False):
            collections = filter_layer_collections_by_object(self.usable_collections, obj)
    
            if collections:
                self.layer_collection = collections[0]
            elif fallback:
                self.layer_collection = self.view_layer.layer_collection
    
            self.collection = self.layer_collection.collection
    
    
    
        def ensure_rig_object(self) -> bpy.types.Object:
            """Check if the generated rig already exists, so we can
            regenerate in the same object. If not, create a new
            object to generate the rig in.
            """
    
            meta_data = self.metarig.data
    
            target_rig = meta_data.rigify_target_rig
            if not target_rig:
                if "metarig" in self.metarig.name:
                    rig_new_name = self.metarig.name.replace("metarig", "rig")
                elif "META" in self.metarig.name:
                    rig_new_name = self.metarig.name.replace("META", "RIG")
                else:
                    rig_new_name = "RIG-" + self.metarig.name
    
                target_rig = bpy.data.objects.new(rig_new_name, bpy.data.armatures.new(rig_new_name))
                target_rig.display_type = 'WIRE'
    
            # If the object is already added to the scene, switch to its collection
    
            if target_rig in list(self.context.scene.collection.all_objects):
    
                self.__switch_to_usable_collection(target_rig)
    
            else:
                # Otherwise, add to the selected collection or the metarig collection if unusable
                if (self.layer_collection not in self.usable_collections
                    or self.layer_collection == self.view_layer.layer_collection):
                    self.__switch_to_usable_collection(self.metarig, True)
    
    
                self.collection.objects.link(target_rig)
    
            # Configure and remember the object
    
            meta_data.rigify_target_rig = target_rig
            target_rig.data.pose_position = 'POSE'
    
            return target_rig
    
        def __unhide_rig_object(self, obj):
            # Ensure the object is visible and selectable
            obj.hide_set(False, view_layer=self.view_layer)
            obj.hide_viewport = False
    
            if not obj.visible_get(view_layer=self.view_layer):
                raise Exception('Could not generate: Target rig is not visible')
    
            obj.select_set(True, view_layer=self.view_layer)
    
            if not obj.select_get(view_layer=self.view_layer):
                raise Exception('Could not generate: Cannot select target rig')
    
            if self.layer_collection not in self.usable_collections:
                raise Exception('Could not generate: Could not find a usable collection.')
    
    
    
        def __find_legacy_collection(self) -> bpy.types.Collection:
            """For backwards comp, matching by name to find a legacy collection.
            (For before there was a Widget Collection PointerProperty)
            """
            wgts_group_name = "WGTS_" + self.obj.name
    
            old_collection = bpy.data.collections.get(wgts_group_name)
    
            if old_collection.library:
                old_collection = None
    
    
            if not old_collection:
                # Update the old 'Widgets' collection
                legacy_collection = bpy.data.collections.get('Widgets')
    
    
                if legacy_collection and wgts_group_name in legacy_collection.objects and not legacy_collection.library:
    
                    legacy_collection.name = wgts_group_name
                    old_collection = legacy_collection
    
            if old_collection:
    
                old_collection.name = wgts_group_name
    
            return old_collection
    
        def ensure_widget_collection(self):
    
            self.widget_collection = self.metarig.data.rigify_widgets_collection
            if not self.widget_collection:
                self.widget_collection = self.__find_legacy_collection()
            if not self.widget_collection:
                wgts_group_name = "WGTS_" + self.obj.name.replace("RIG-", "")
                self.widget_collection = ensure_collection(self.context, wgts_group_name, hidden=True)
    
            self.metarig.data.rigify_widgets_collection = self.widget_collection
    
    
            self.use_mirror_widgets = self.metarig.data.rigify_mirror_widgets
    
            # Build tables for existing widgets
            self.old_widget_table = {}
            self.new_widget_table = {}
            self.widget_mirror_mesh = {}
    
            if self.metarig.data.rigify_force_widget_update:
                # Remove widgets if force update is set
                for obj in list(self.widget_collection.objects):
                    bpy.data.objects.remove(obj)
            elif self.obj.pose:
    
                # Find all widgets from the collection referenced by the old rig
                known_widgets = set(obj.name for obj in self.widget_collection.objects)
    
                for bone in self.obj.pose.bones:
                    if bone.custom_shape and bone.custom_shape.name in known_widgets:
                        self.old_widget_table[bone.name] = bone.custom_shape
    
                # Rename widgets in case the rig was renamed
                name_prefix = WGT_PREFIX + self.obj.name + "_"
    
                for bone_name, widget in self.old_widget_table.items():
                    old_data_name = change_name_side(widget.name, get_name_side(widget.data.name))
    
                    widget.name = name_prefix + bone_name
    
                    # If the mesh name is the same as the object, rename it too
                    if widget.data.name == old_data_name:
                        widget.data.name = change_name_side(widget.name, get_name_side(widget.data.name))
    
                # Find meshes for mirroring
                if self.use_mirror_widgets:
                    for bone_name, widget in self.old_widget_table.items():
                        mid_name = change_name_side(bone_name, Side.MIDDLE)
                        if bone_name != mid_name:
                            self.widget_mirror_mesh[mid_name] = widget.data
    
    
    
        def __duplicate_rig(self):
            obj = self.obj
            metarig = self.metarig
            context = self.context
    
            # Remove all bones from the generated rig armature.
            bpy.ops.object.mode_set(mode='EDIT')
            for bone in obj.data.edit_bones:
                obj.data.edit_bones.remove(bone)
    
            # Select and duplicate metarig
            select_object(context, metarig, deselect_all=True)
    
            # Rename org bones in the temporary object
            temp_obj = context.view_layer.objects.active
    
            assert temp_obj and temp_obj != metarig
    
            self.__freeze_driver_vars(temp_obj)
            self.__rename_org_bones(temp_obj)
    
    
            # Select the target rig and join
            select_object(context, obj)
    
            saved_matrix = obj.matrix_world.copy()
            obj.matrix_world = metarig.matrix_world
    
    
            # Select the generated rig
            select_object(context, obj, deselect_all=True)
    
            # Clean up animation data
            if obj.animation_data:
                obj.animation_data.action = None
    
                for track in obj.animation_data.nla_tracks:
                    obj.animation_data.nla_tracks.remove(track)
    
    
    
        def __freeze_driver_vars(self, obj):
            if obj.animation_data:
    
                # Freeze drivers referring to custom properties
                for d in obj.animation_data.drivers:
                    for var in d.driver.variables:
                        for tar in var.targets:
                            # If a custom property
                            if var.type == 'SINGLE_PROP' \
    
                            and re.match(r'^pose.bones\["[^"\]]*"\]\["[^"\]]*"\]$', tar.data_path):
    
                                tar.data_path = "RIGIFY-" + tar.data_path
    
    
    
            #----------------------------------
            # Make a list of the original bones so we can keep track of them.
            original_bones = [bone.name for bone in obj.data.bones]
    
            # Add the ORG_PREFIX to the original bones.
            for i in range(0, len(original_bones)):
    
                bone = obj.pose.bones[original_bones[i]]
    
    
                # Preserve the root bone as is if present
                if bone.name == ROOT_NAME:
                    if bone.parent:
                        raise MetarigError('Root bone must have no parent')
                    if get_rigify_type(bone) not in ('', 'basic.raw_copy'):
                        raise MetarigError('Root bone must have no rig, or use basic.raw_copy')
                    continue
    
    
                # This rig type is special in that it preserves the name of the bone.
                if get_rigify_type(bone) != 'basic.raw_copy':
                    bone.name = make_original_name(original_bones[i])
                    original_bones[i] = bone.name
    
    
            self.original_bones = original_bones
    
    
        def __create_root_bone(self):
            obj = self.obj
            metarig = self.metarig
    
    
            if ROOT_NAME in obj.data.bones:
                # Use the existing root bone
                root_bone = ROOT_NAME
            else:
                # Create the root bone.
                root_bone = new_bone(obj, ROOT_NAME)
                spread = get_xy_spread(metarig.data.bones) or metarig.data.bones[0].length
                spread = float('%.3g' % spread)
                scale = spread/0.589
                obj.data.edit_bones[root_bone].head = (0, 0, 0)
                obj.data.edit_bones[root_bone].tail = (0, scale, 0)
                obj.data.edit_bones[root_bone].roll = 0
    
    
            self.root_bone = root_bone
            self.bone_owners[root_bone] = None
    
            self.noparent_bones.add(root_bone)
    
    
    
        def __parent_bones_to_root(self):
            eb = self.obj.data.edit_bones
    
            # Parent loose bones to root
            for bone in eb:
                if bone.name in self.noparent_bones:
                    continue
                elif bone.parent is None:
                    bone.use_connect = False
                    bone.parent = eb[self.root_bone]
    
    
        def __lock_transforms(self):
            # Lock transforms on all non-control bones
            r = re.compile("[A-Z][A-Z][A-Z]-")
            for pb in self.obj.pose.bones:
                if r.match(pb.name):
                    pb.lock_location = (True, True, True)
                    pb.lock_rotation = (True, True, True)
                    pb.lock_rotation_w = True
                    pb.lock_scale = (True, True, True)
    
    
        def __assign_layers(self):
    
            pbones[self.root_bone].bone.layers = ROOT_LAYER
    
    
            # Every bone that has a name starting with "DEF-" make deforming.  All the
            # others make non-deforming.
    
    
                bone.use_deform = name.startswith(DEF_PREFIX)
    
                # Move all the original bones to their layer.
                if name.startswith(ORG_PREFIX):
    
                # Move all the bones with names starting with "MCH-" to their layer.
                elif name.startswith(MCH_PREFIX):
    
                # Move all the bones with names starting with "DEF-" to their layer.
                elif name.startswith(DEF_PREFIX):
    
                    layers = DEF_LAYER
    
                if layers is not None:
                    bone.layers = layers
    
                    # Remove custom shapes from non-control bones
    
                bone.bbone_x = bone.bbone_z = bone.length * 0.05
    
    
    
        def __restore_driver_vars(self):
            obj = self.obj
    
            # Alter marked driver targets
            if obj.animation_data:
                for d in obj.animation_data.drivers:
                    for v in d.driver.variables:
                        for tar in v.targets:
                            if tar.data_path.startswith("RIGIFY-"):
                                temp, bone, prop = tuple([x.strip('"]') for x in tar.data_path.split('["')])
                                if bone in obj.data.bones \
                                and prop in obj.pose.bones[bone].keys():
                                    tar.data_path = tar.data_path[7:]
                                else:
    
                                    org_name = make_original_name(bone)
                                    org_name = self.org_rename_table.get(org_name, org_name)
                                    tar.data_path = 'pose.bones["%s"]["%s"]' % (org_name, prop)
    
    
    
        def __assign_widgets(self):
            obj_table = {obj.name: obj for obj in self.scene.objects}
    
            # Assign shapes to bones
            # Object's with name WGT-<bone_name> get used as that bone's shape.
            for bone in self.obj.pose.bones:
    
                # First check the table built by create_widget
                if bone.name in self.new_widget_table:
                    bone.custom_shape = self.new_widget_table[bone.name]
                    continue
    
    
                # Object names are limited to 63 characters... arg
                wgt_name = (WGT_PREFIX + self.obj.name + '_' + bone.name)[:63]
    
                if wgt_name in obj_table:
                    bone.custom_shape = obj_table[wgt_name]
    
    
        def __compute_visible_layers(self):
            # Reveal all the layers with control bones on them
            vis_layers = [False for n in range(0, 32)]
    
            for bone in self.obj.data.bones:
                for i in range(0, 32):
                    vis_layers[i] = vis_layers[i] or bone.layers[i]
    
            for i in range(0, 32):
                vis_layers[i] = vis_layers[i] and not (ORG_LAYER[i] or MCH_LAYER[i] or DEF_LAYER[i])
    
            self.obj.data.layers = vis_layers
    
    
        def generate(self):
            context = self.context
            metarig = self.metarig
            scene = self.scene
            id_store = self.id_store
            view_layer = self.view_layer
            t = Timer()
    
            self.usable_collections = list_layer_collections(view_layer.layer_collection, selectable=True)
    
            bpy.ops.object.mode_set(mode='OBJECT')
    
            #------------------------------------------
            # Create/find the rig object and set it up
    
            self.obj = obj = self.ensure_rig_object()
    
            self.__unhide_rig_object(obj)
    
            # Select the chosen working collection in case it changed
            self.view_layer.active_layer_collection = self.layer_collection
    
    
            # Get rid of anim data in case the rig already existed
            print("Clear rig animation data.")
    
            obj.animation_data_clear()
            obj.data.animation_data_clear()
    
            select_object(context, obj, deselect_all=True)
    
            #------------------------------------------
    
            # Create Widget Collection
            self.ensure_widget_collection()
    
            t.tick("Create main WGTS: ")
    
    
            #------------------------------------------
            # Get parented objects to restore later
            childs = {}  # {object: bone}
            for child in obj.children:
                childs[child] = child.parent_bone
    
            #------------------------------------------
    
            # Copy bones from metarig to obj (adds ORG_PREFIX)
    
            t.tick("Duplicate rig: ")
    
            #------------------------------------------
            # Put the rig_name in the armature custom properties
            obj.data["rig_id"] = self.rig_id
    
            self.script = rig_ui_template.ScriptGenerator(self)
    
            #------------------------------------------
            bpy.ops.object.mode_set(mode='OBJECT')
    
            self.instantiate_rig_tree()
    
            t.tick("Instantiate rigs: ")
    
            #------------------------------------------
            bpy.ops.object.mode_set(mode='OBJECT')
    
            self.invoke_initialize()
    
    
            t.tick("Initialize rigs: ")
    
    
            #------------------------------------------
            bpy.ops.object.mode_set(mode='EDIT')
    
            self.invoke_prepare_bones()
    
            t.tick("Prepare bones: ")
    
            #------------------------------------------
    
            bpy.ops.object.mode_set(mode='OBJECT')
    
            bpy.ops.object.mode_set(mode='EDIT')
    
            #------------------------------------------
            bpy.ops.object.mode_set(mode='OBJECT')
            bpy.ops.object.mode_set(mode='EDIT')
    
            #------------------------------------------
            bpy.ops.object.mode_set(mode='OBJECT')
    
            self.invoke_configure_bones()
    
            t.tick("Configure bones: ")
    
    
            #------------------------------------------
            bpy.ops.object.mode_set(mode='OBJECT')
    
            self.invoke_preapply_bones()
    
            t.tick("Preapply bones: ")
    
    
            #------------------------------------------
            bpy.ops.object.mode_set(mode='EDIT')
    
            self.invoke_apply_bones()
    
            t.tick("Apply bones: ")
    
            #------------------------------------------
            bpy.ops.object.mode_set(mode='OBJECT')
    
            self.invoke_rig_bones()
    
            t.tick("Rig bones: ")
    
            #------------------------------------------
            bpy.ops.object.mode_set(mode='OBJECT')
    
            self.invoke_generate_widgets()
    
    
            # Generate the default root widget last in case it's rigged with raw_copy
            create_root_widget(obj, self.root_bone)
    
    
            t.tick("Generate widgets: ")
    
            #------------------------------------------
            bpy.ops.object.mode_set(mode='OBJECT')
    
            self.__lock_transforms()
            self.__assign_layers()
            self.__compute_visible_layers()
            self.__restore_driver_vars()
    
            t.tick("Assign layers: ")
    
            #------------------------------------------
            bpy.ops.object.mode_set(mode='OBJECT')
    
            self.invoke_finalize()
    
            t.tick("Finalize: ")
    
            #------------------------------------------
            bpy.ops.object.mode_set(mode='OBJECT')
    
            # Create Selection Sets
            create_selection_sets(obj, metarig)
    
            # Create Bone Groups
    
            create_bone_groups(obj, metarig, self.layer_group_priorities)
    
    
            t.tick("The rest: ")
    
            #----------------------------------
            # Deconfigure
            bpy.ops.object.mode_set(mode='OBJECT')
            obj.data.pose_position = 'POSE'
    
            # Restore parent to bones
            for child, sub_parent in childs.items():
                if sub_parent in obj.pose.bones:
                    mat = child.matrix_world.copy()
                    child.parent_bone = sub_parent
                    child.matrix_world = mat
    
    
            # Clear any transient errors in drivers
            refresh_all_drivers()
    
    
            #----------------------------------
            # Execute the finalize script
    
            if metarig.data.rigify_finalize_script:
                bpy.ops.object.mode_set(mode='OBJECT')
                exec(metarig.data.rigify_finalize_script.as_string(), {})
                bpy.ops.object.mode_set(mode='OBJECT')
    
    
            #----------------------------------
            # Restore active collection
            view_layer.active_layer_collection = self.layer_collection
    
    
    def generate_rig(context, metarig):
        """ Generates a rig from a metarig.
    
        """
        # Initial configuration
        rest_backup = metarig.data.pose_position
        metarig.data.pose_position = 'REST'
    
        try:
    
            generator = Generator(context, metarig)
    
            base_generate.BaseGenerator.instance = generator
    
            generator.generate()
    
    
            metarig.data.pose_position = rest_backup
    
        except Exception as e:
            # Cleanup if something goes wrong
            print("Rigify: failed to generate rig.")
    
            bpy.ops.object.mode_set(mode='OBJECT')
            metarig.data.pose_position = rest_backup
    
            # Continue the exception
            raise e
    
        finally:
            base_generate.BaseGenerator.instance = None
    
    
    def create_selection_set_for_rig_layer(
    
            rig: bpy.types.Object,
            set_name: str,
    
            layer_idx: int
        ) -> None:
        """Create a single selection set on a rig.
    
        The set will contain all bones on the rig layer with the given index.
        """
        selset = rig.selection_sets.add()
        selset.name = set_name
    
        for b in rig.pose.bones:
            if not b.bone.layers[layer_idx] or b.name in selset.bone_ids:
                continue
    
            bone_id = selset.bone_ids.add()
            bone_id.name = b.name
    
    
    def create_selection_sets(obj, metarig):
    
        """Create selection sets if the Selection Sets addon is enabled.
    
        Whether a selection set for a rig layer is created is controlled in the
        Rigify Layer Names panel.
        """
    
        # Check if selection sets addon is installed
    
        if 'bone_selection_groups' not in bpy.context.preferences.addons \
                and 'bone_selection_sets' not in bpy.context.preferences.addons:
    
        obj.selection_sets.clear()
    
    
        for i, name in enumerate(metarig.data.rigify_layers.keys()):
    
            if name == '' or not metarig.data.rigify_layers[i].selset:
    
            create_selection_set_for_rig_layer(obj, name, i)
    
    def create_bone_groups(obj, metarig, priorities={}):
    
    
        bpy.ops.object.mode_set(mode='OBJECT')
        pb = obj.pose.bones
        layers = metarig.data.rigify_layers
        groups = metarig.data.rigify_colors
    
    
        # Create BGs
        for l in layers:
            if l.group == 0:
                continue
            g_id = l.group - 1
            name = groups[g_id].name
            if name not in obj.pose.bone_groups.keys():
    
                bg = obj.pose.bone_groups.new(name=name)
    
                bg.color_set = 'CUSTOM'
    
                bg.colors.normal = gamma_correct(groups[g_id].normal)
                bg.colors.select = gamma_correct(groups[g_id].select)
                bg.colors.active = gamma_correct(groups[g_id].active)
    
                prios = priorities.get(b.name, dummy)
                enabled = [ i for i, v in enumerate(b.bone.layers) if v ]
                layer_index = max(enabled, key=lambda i: prios.get(i, 0))
    
            except ValueError:
                continue
            if layer_index > len(layers) - 1:   # bone is on reserved layers
                continue
            g_id = layers[layer_index].group - 1
            if g_id >= 0:
                name = groups[g_id].name
                b.bone_group = obj.pose.bone_groups[name]
    
    
    
    def get_xy_spread(bones):
        x_max = 0
        y_max = 0
        for b in bones:
            x_max = max((x_max, abs(b.head[0]), abs(b.tail[0])))
            y_max = max((y_max, abs(b.head[1]), abs(b.tail[1])))
    
        return max((x_max, y_max))
    
    
    
    def param_matches_type(param_name, rig_type):
        """ Returns True if the parameter name is consistent with the rig type.
        """
        if param_name.rsplit(".", 1)[0] == rig_type:
            return True
        else:
            return False
    
    
    def param_name(param_name, rig_type):
        """ Get the actual parameter name, sans-rig-type.
        """
        return param_name[len(rig_type) + 1:]