diff options
author | Alexander Gavrilov <angavrilov@gmail.com> | 2019-11-02 11:57:25 +0300 |
---|---|---|
committer | Alexander Gavrilov <angavrilov@gmail.com> | 2019-12-16 18:53:14 +0300 |
commit | a41ceb3ac8d48fcf8f47360376d009ea08dbb894 (patch) | |
tree | d5d24b37de1db587b30ccc04d8ca8ee4b8396b56 /rigify | |
parent | bf89c1eb28007fdd491b5a0b7cb3709b84d88b85 (diff) |
Rigify: add optional primitive IK in limbs.super_finger.
Basic IK support in fingers could be useful for easily
avoiding fingertips sliding when animating minor movement
between the hand and an object it is holding.
As there are 10 fingers, to limit the performance impact the
IK itself is implemented using just one extra control, one
constraint, and one driver. The parent switch adds one more
bone, constraint and driver.
This simple implementation requires applying IK as a correction
on top of the FK shape to share the FK controls for precisely
defining the shape, which means that stretch can't be implemented
without giving up on exact IK<->FK snapping. This also means
that unlike limbs this IK is not indended for independent use,
and must always be used as a local anti-slide fix on top of
primarily FK animation.
The parent switch is designed to work with the extra wrist
control and/or a held object pivot, demonstrating the tag
feature of SwitchParentBuilder.
Diffstat (limited to 'rigify')
-rw-r--r-- | rigify/rigs/limbs/super_finger.py | 310 |
1 files changed, 298 insertions, 12 deletions
diff --git a/rigify/rigs/limbs/super_finger.py b/rigify/rigs/limbs/super_finger.py index 0b3fcd8a..978c0e09 100644 --- a/rigify/rigs/limbs/super_finger.py +++ b/rigify/rigs/limbs/super_finger.py @@ -20,6 +20,7 @@ import bpy import re +import json from itertools import count @@ -27,9 +28,11 @@ from ...utils.errors import MetarigError from ...utils.bones import put_bone, flip_bone, align_chain_x_axis, set_bone_widget_transform from ...utils.naming import make_derived_name from ...utils.widgets import create_widget -from ...utils.widgets_basic import create_circle_widget +from ...utils.widgets_basic import create_circle_widget, create_sphere_widget from ...utils.misc import map_list from ...utils.layers import ControlLayersOption +from ...utils.switch_parent import SwitchParentBuilder +from ...utils.animation import add_generic_snap, add_fk_ik_snap_buttons from ...base_rig import stage @@ -42,12 +45,15 @@ class Rig(SimpleChainRig): super().initialize() self.bbone_segments = self.params.bbones - self.first_parent = self.get_bone_parent(self.bones.org[0]) + self.make_ik = self.params.make_extra_ik_control def prepare_bones(self): if self.params.primary_rotation_axis == 'automatic': align_chain_x_axis(self.obj, self.bones.org) + def parent_bones(self): + self.rig_parent_bone = self.get_bone_parent(self.bones.org[0]) + ############################## # Master Control @@ -138,6 +144,76 @@ class Rig(SimpleChainRig): create_circle_widget(self.obj, ctrl, radius=0.3, head_tail=0.5) ############################## + # IK Control + + @stage.generate_bones + def make_ik_control(self): + if self.make_ik: + self.bones.ctrl.ik = self.make_ik_control_bone(self.bones.org) + + self.build_ik_parent_switch(SwitchParentBuilder(self.generator)) + + def make_ik_control_bone(self, orgs): + name = self.copy_bone(orgs[-1], make_derived_name(orgs[0], 'ctrl', '_ik'), scale=0.7) + put_bone(self.obj, name, self.get_bone(orgs[-1]).tail) + return name + + def build_ik_parent_switch(self, pbuilder): + ctrl = self.bones.ctrl + + pbuilder.build_child( + self, ctrl.ik, prop_bone=ctrl.ik, + select_tags=['held_object', 'limb_ik', {'child', 'limb_end'}], only_selected=True, + prop_id='IK_parent', prop_name='IK Parent', controls=[ ctrl.ik ], + no_fix_rotation=True, no_fix_scale=True, + ) + + @stage.configure_bones + def configure_ik_control(self): + if self.make_ik: + bone = self.get_bone(self.bones.ctrl.ik) + bone.lock_rotation = True, True, True + bone.lock_rotation_w = True + bone.lock_scale = True, True, True + + @stage.configure_bones + def configure_ik_control_properties(self): + if self.make_ik: + ctrl = self.bones.ctrl + rig_name = ctrl.fk[0] + + panel = self.script.panel_with_selected_check(self, self.bones.ctrl.flatten()) + + self.make_property( + ctrl.ik, 'FK_IK', 0.0, + description="Enable simple IK correction on top of FK posing" + ) + panel.custom_prop(ctrl.ik, 'FK_IK', text="Finger IK ({})".format(rig_name), slider=True) + + axis = self.params.primary_rotation_axis + + add_finger_snap_fk_to_ik( + panel, master=ctrl.master, fk_bones=ctrl.fk, + ik_bones=self.bones.org, ik_control=ctrl.ik, + ik_constraint_bone=self.bones.org[-1], + axis=self.axis_options[axis]['id'], + rig_name=rig_name, compact=True, + ) + + add_generic_snap( + panel, output_bones=[ctrl.ik], input_bones=ctrl.fk[-1:], + input_ctrl_bones=[ctrl.master, *ctrl.fk], + label='IK->FK', rig_name=rig_name, tooltip='IK to FK', + compact=True, locks=(False,True,True), + ) + + + @stage.generate_widgets + def make_ik_control_widget(self): + if self.make_ik: + create_sphere_widget(self.obj, self.bones.ctrl.ik) + + ############################## # MCH bend chain @stage.generate_bones @@ -145,29 +221,29 @@ class Rig(SimpleChainRig): self.bones.mch.bend = map_list(self.make_mch_bend_bone, self.bones.org) def make_mch_bend_bone(self, org): - return self.copy_bone(org, make_derived_name(org, 'mch', '_drv'), parent=False) + return self.copy_bone(org, make_derived_name(org, 'mch', '_drv'), parent=False, scale=0.3) @stage.parent_bones def parent_mch_bend_chain(self): ctrls = self.bones.ctrl.fk - for args in zip(self.bones.mch.bend, [self.first_parent] + ctrls): + for args in zip(self.bones.mch.bend, [self.rig_parent_bone] + ctrls): self.set_bone_parent(*args) # Match axis to expression axis_options = { - "automatic": {"axis": 0, + "automatic": {"axis": 0, "id": '+X', "expr": '(1-sy)*pi'}, - "X": {"axis": 0, + "X": {"axis": 0, "id": '+X', "expr": '(1-sy)*pi'}, - "-X": {"axis": 0, + "-X": {"axis": 0, "id": '-X', "expr": '-((1-sy)*pi)'}, - "Y": {"axis": 1, + "Y": {"axis": 1, "id": '+Y', "expr": '(1-sy)*pi'}, - "-Y": {"axis": 1, + "-Y": {"axis": 1, "id": '-Y', "expr": '-((1-sy)*pi)'}, - "Z": {"axis": 2, + "Z": {"axis": 2, "id": '+Z', "expr": '(1-sy)*pi'}, - "-Z": {"axis": 2, + "-Z": {"axis": 2, "id": '-Z', "expr": '-((1-sy)*pi)'} } @@ -207,7 +283,7 @@ class Rig(SimpleChainRig): @stage.parent_bones def parent_mch_stretch_chain(self): ctrls = self.bones.ctrl.fk - for args in zip(self.bones.mch.stretch, [self.first_parent] + ctrls[1:]): + for args in zip(self.bones.mch.stretch, [self.rig_parent_bone] + ctrls[1:]): self.set_bone_parent(*args) @stage.rig_bones @@ -232,6 +308,30 @@ class Rig(SimpleChainRig): for args in zip(count(0), self.bones.org, self.bones.mch.stretch): self.rig_org_bone(*args) + if self.make_ik: + self.rig_org_ik(self.bones.org, self.bones.ctrl.ik) + + def rig_org_ik(self, orgs, ik_ctrl): + axis = self.params.primary_rotation_axis + options = self.axis_options[axis] + + # Lock IK axis on child bones, using stiffness to preserve + # original rotation rather than zeroing it out. + stiffness = [1.0, 1.0, 1.0] + stiffness[options['axis']] = 0.0 + + for org in orgs[1:]: + bone = self.get_bone(org) + bone.ik_stiffness_x, bone.ik_stiffness_y, bone.ik_stiffness_z = stiffness + + # Add the constraint + con = self.make_constraint( + orgs[-1], 'IK', ik_ctrl, name='FingerIK', + chain_count=len(orgs), use_stretch=False, + ) + + self.make_driver(con, "influence", variables=[(ik_ctrl, 'FK_IK')]) + ############################## # Deform chain @@ -275,6 +375,12 @@ class Rig(SimpleChainRig): description = 'Number of B-Bone segments' ) + params.make_extra_ik_control = bpy.props.BoolProperty( + name = "Extra IK Control", + default = False, + description = "Create an optional IK control" + ) + ControlLayersOption.TWEAK.add_parameters(params) @classmethod @@ -286,9 +392,189 @@ class Rig(SimpleChainRig): r.prop(params, "primary_rotation_axis", text="") layout.prop(params, 'bbones') + layout.prop(params, 'make_extra_ik_control', text='IK Control') ControlLayersOption.TWEAK.parameters_ui(layout, params) +############################# +# Finger FK to IK operator ## +############################# + +SCRIPT_REGISTER_OP_SNAP_FK_IK = ['POSE_OT_rigify_finger_fk2ik', 'POSE_OT_rigify_finger_fk2ik_bake'] + +SCRIPT_UTILITIES_OP_SNAP_FK_IK = [''' +######################## +## Limb Snap IK to FK ## +######################## + +class RigifyFingerFk2IkBase: + ik_control: StringProperty(name="IK Control") + ik_chain: StringProperty(name="IK output chain") + constraint_bone: StringProperty(name="Bone With the IK Constraint") + fk_master: StringProperty(name="FK Master Control") + fk_chain: StringProperty(name="FK Bone Chain") + axis: StringProperty(name="Main Rotation Axis", default="+X") + + def init_execute(self, context): + self.ik_chain_list = json.loads(self.ik_chain) + self.fk_chain_list = json.loads(self.fk_chain) + + # Extracting the IK state - requires forcing IK on temporarily + def find_constraint_drivers(self, obj): + self.driver_fcurves = {} + self.ik_constraint = None + + if self.constraint_bone: + bone = obj.pose.bones[self.constraint_bone] + self.ik_constraint = con = bone.constraints['FingerIK'] + self.driver_fcurves = DriverCurveTable(obj).get_prop_curves(con, 'influence') + + def before_save_state(self, context, obj): + self.find_constraint_drivers(obj) + + if self.ik_constraint: + for fcu in self.driver_fcurves.values(): + fcu.mute = True + + self.ik_constraint.influence = 1 + + context.view_layer.update() + + def get_fk_axis_angles(self, obj): + options = self.axis_options[self.axis] + angles = [] + + for bone in self.fk_chain_list[1:-1]: + matrix = obj.pose.bones[bone].matrix_basis + eulers = matrix.to_euler(options['order']) + angles.append(eulers[options['axis']]) + + return angles + + def get_ik_original_matrix(self, obj): + bone = obj.pose.bones[self.ik_chain_list[0]] + + if len(bone.constraints) == 1 and bone.constraints[0].type == 'COPY_TRANSFORMS': + target = bone.constraints[0].subtarget + return obj.pose.bones[target].matrix + + def save_frame_state(self, context, obj): + matrices = get_chain_transform_matrices(obj, self.ik_chain_list) + fk_matrices = get_chain_transform_matrices(obj, self.fk_chain_list) + angles = self.get_fk_axis_angles(obj) + ik_original = self.get_ik_original_matrix(obj) + return (matrices, fk_matrices, angles, ik_original) + + def after_save_state(self, context, obj): + if self.ik_constraint: + for fcu in self.driver_fcurves.values(): + fcu.mute = False + + context.view_layer.update() + + # Applying the state + axis_options = { + "+X": {"axis": 0, "sign": 1, "order": 'ZYX'}, + "-X": {"axis": 0, "sign": -1, "order": 'ZYX'}, + "+Y": {"axis": 1, "sign": 1, "order": 'ZXY'}, + "-Y": {"axis": 1, "sign": -1, "order": 'ZXY'}, + "+Z": {"axis": 2, "sign": 1, "order": 'XYZ'}, + "-Z": {"axis": 2, "sign": -1, "order": 'XYZ'}, + } + + def apply_frame_state(self, context, obj, state): + matrices, fk_matrices, old_angles, ik_original = state + + fk_master = obj.pose.bones[self.fk_master] + fk_chain = [ obj.pose.bones[k] for k in self.fk_chain_list ] + + # Set the master control position and rotation. + master_mat = matrices[0] + + if ik_original: + master_mat = master_mat @ ik_original.inverted() @ fk_matrices[0] + + master_mat = obj.convert_space( + pose_bone=fk_chain[0].parent, matrix=master_mat, + from_space='POSE', to_space='LOCAL' + ) + master_mat = obj.convert_space( + pose_bone=fk_master, matrix=master_mat, + from_space='LOCAL', to_space='POSE' + ) + master_mat.translation = matrices[0].translation + + set_transform_from_matrix(obj, self.fk_master, master_mat) + + fk_master.scale = (1, 1, 1) + + if self.keyflags is not None: + keyframe_transform_properties(obj, self.fk_master, self.keyflags) + + context.view_layer.update() + + # Apply the detail controls + set_chain_transforms_from_matrices( + context, obj, self.fk_chain_list[:-1], matrices, keyflags=self.keyflags, + ) + + set_transform_from_matrix( + obj, self.fk_chain_list[-1], Matrix.Identity(4), space='LOCAL', keyflags=self.keyflags + ) + + # Compute the master scale from average control angle, biased by original + options = self.axis_options[self.axis] + + angles = self.get_fk_axis_angles(obj) + avg_angle = sum(a - b for a, b in zip(angles, old_angles)) / len(angles) + + fk_master.scale[1] = 1 - avg_angle * options['sign'] / pi + + if self.keyflags is not None: + keyframe_transform_properties(obj, self.fk_master, self.keyflags) + + context.view_layer.update() + + # Re-apply the rest of the detail controls + set_chain_transforms_from_matrices( + context, obj, self.fk_chain_list[1:-1], matrices[1:], keyflags=self.keyflags, + ) + +class POSE_OT_rigify_finger_fk2ik(RigifyFingerFk2IkBase, RigifySingleUpdateMixin, bpy.types.Operator): + bl_idname = "pose.rigify_finger_fk2ik_" + rig_id + bl_label = "Snap FK->IK" + bl_description = "Snap the FK chain to IK result" + +class POSE_OT_rigify_finger_fk2ik_bake(RigifyFingerFk2IkBase, RigifyBakeKeyframesMixin, bpy.types.Operator): + bl_idname = "pose.rigify_finger_fk2ik_bake_" + rig_id + bl_label = "Apply Snap FK->IK To Keyframes" + bl_description = "Snap the FK chain keyframes to IK result" + + def execute_scan_curves(self, context, obj): + fk_bones = [self.fk_master, *self.fk_chain_list] + self.bake_add_bone_frames(fk_bones + [self.ik_control], TRANSFORM_PROPS_ALL) + return self.bake_get_all_bone_curves(fk_bones, TRANSFORM_PROPS_ALL) +'''] + +def add_finger_snap_fk_to_ik(panel, *, master=None, fk_bones=[], ik_bones=[], ik_control=None, ik_constraint_bone=None, axis='+X', rig_name='', compact=None): + panel.use_bake_settings() + panel.script.add_utilities(SCRIPT_UTILITIES_OP_SNAP_FK_IK) + panel.script.register_classes(SCRIPT_REGISTER_OP_SNAP_FK_IK) + + op_props = { + 'fk_master': master, + 'fk_chain': json.dumps(fk_bones), + 'ik_chain': json.dumps(ik_bones), + 'ik_control': ik_control, + 'constraint_bone': ik_constraint_bone, + 'axis': axis, + } + + add_fk_ik_snap_buttons( + panel, 'pose.rigify_finger_fk2ik_{rig_id}', 'pose.rigify_finger_fk2ik_bake_{rig_id}', + label='FK->IK', rig_name=rig_name, properties=op_props, + clear_bones=[master, *fk_bones], compact=compact, + ) def create_sample(obj): # generated by rigify.utils.write_metarig |