Rigify: support generating mirrored linked duplicate widgets.

After the custom shape scale property was split into an XYZ vector,
it is possible to generate truly mirrored widgets using the same
mesh for the left and right side. The widgets Rigify generates are
originally symmetrical, but in practice they usually need to be
tweaked to fit the character better, and proper mirroring matters.

This commit implements widget mirroring and enables it by default.
When reusing widgets left from a previous generation the code tries
to detect whether they were actually originally mirrored by checking
object scale to avoid flipping in pre-existing rigs.

As an aside, reusing pre-existing widgets is made more robust
to random name changes by building a table via scanning the old
generated rig before overwriting it.
This commit is contained in:
Alexander Gavrilov 2021-09-20 22:16:41 +03:00
parent ecf30de46c
commit eed6d6cc13
5 changed files with 130 additions and 44 deletions

View File

@ -530,6 +530,10 @@ def register():
description="Forces Rigify to delete and rebuild all the rig widgets. if unset, only missing widgets will be created",
default=False)
bpy.types.Armature.rigify_mirror_widgets = BoolProperty(name="Mirror Widgets",
description="Make widgets for left and right side bones linked duplicates with negative X scale for the right side, based on bone name symmetry",
default=True)
bpy.types.Armature.rigify_target_rig = PointerProperty(type=bpy.types.Object,
name="Rigify Target Rig",
description="Defines which rig to overwrite. If unset, a new one called 'rig' will be created",

View File

@ -25,7 +25,7 @@ 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
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
@ -165,27 +165,44 @@ class Generator(base_generate.BaseGenerator):
for obj in list(old_collection.objects):
bpy.data.objects.remove(obj)
# Rename widgets and collection if renaming
if self.rig_old_name:
old_prefix = WGT_PREFIX + self.rig_old_name + "_"
new_prefix = WGT_PREFIX + self.obj.name + "_"
for obj in list(old_collection.objects):
if obj.name.startswith(old_prefix):
new_name = new_prefix + obj.name[len(old_prefix):]
elif obj.name == wgts_group_name:
new_name = new_group_name
else:
continue
obj.data.name = new_name
obj.name = new_name
old_collection.name = new_group_name
# Rename the collection
old_collection.name = new_group_name
# Create/find widget collection
self.widget_collection = ensure_widget_collection(self.context, new_group_name)
self.wgts_group_name = new_group_name
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 not self.metarig.data.rigify_force_widget_update and 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):
@ -373,6 +390,11 @@ class Generator(base_generate.BaseGenerator):
# 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]

View File

@ -97,7 +97,7 @@ def create_ikarrow_widget(rig, bone_name, size=1.0, bone_transform_name=None, ro
def create_hand_widget(rig, bone_name, size=1.0, bone_transform_name=None):
# Create hand widget
obj = create_widget(rig, bone_name, bone_transform_name)
obj = create_widget(rig, bone_name, bone_transform_name, subsurf=2)
if obj is not None:
verts = [(0.0*size, 1.5*size, -0.7000000476837158*size), (1.1920928955078125e-07*size, -0.25*size, -0.6999999284744263*size), (0.0*size, -0.25*size, 0.7000000476837158*size), (-1.1920928955078125e-07*size, 1.5*size, 0.6999999284744263*size), (5.960464477539063e-08*size, 0.7229999899864197*size, -0.699999988079071*size), (-5.960464477539063e-08*size, 0.7229999899864197*size, 0.699999988079071*size), (1.1920928955078125e-07*size, -2.9802322387695312e-08*size, -0.699999988079071*size), (0.0*size, 2.9802322387695312e-08*size, 0.699999988079071*size), ]
edges = [(1, 2), (0, 3), (0, 4), (3, 5), (4, 6), (1, 6), (5, 7), (2, 7)]
@ -107,8 +107,6 @@ def create_hand_widget(rig, bone_name, size=1.0, bone_transform_name=None):
mesh.from_pydata(verts, edges, faces)
mesh.update()
mod = obj.modifiers.new("subsurf", 'SUBSURF')
mod.levels = 2
return obj
else:
return None
@ -116,7 +114,7 @@ def create_hand_widget(rig, bone_name, size=1.0, bone_transform_name=None):
def create_foot_widget(rig, bone_name, size=1.0, bone_transform_name=None):
# Create hand widget
obj = create_widget(rig, bone_name, bone_transform_name)
obj = create_widget(rig, bone_name, bone_transform_name, subsurf=2)
if obj is not None:
verts = [(-0.6999998688697815*size, -0.5242648720741272*size, 0.0*size), (-0.7000001072883606*size, 1.2257349491119385*size, 0.0*size), (0.6999998688697815*size, 1.2257351875305176*size, 0.0*size), (0.7000001072883606*size, -0.5242648720741272*size, 0.0*size), (-0.6999998688697815*size, 0.2527350187301636*size, 0.0*size), (0.7000001072883606*size, 0.2527352571487427*size, 0.0*size), (-0.7000001072883606*size, 0.975735068321228*size, 0.0*size), (0.6999998688697815*size, 0.9757352471351624*size, 0.0*size), ]
edges = [(1, 2), (0, 3), (0, 4), (3, 5), (4, 6), (1, 6), (5, 7), (2, 7), ]
@ -126,8 +124,6 @@ def create_foot_widget(rig, bone_name, size=1.0, bone_transform_name=None):
mesh.from_pydata(verts, edges, faces)
mesh.update()
mod = obj.modifiers.new("subsurf", 'SUBSURF')
mod.levels = 2
return obj
else:
return None

View File

@ -177,6 +177,7 @@ class DATA_PT_rigify_buttons(bpy.types.Panel):
if armature_id_store.rigify_generate_mode == 'new':
row.enabled = False
col.prop(armature_id_store, "rigify_mirror_widgets")
col.prop(armature_id_store, "rigify_finalize_script", text="Run Script")
elif obj.mode == 'EDIT':

View File

@ -28,6 +28,7 @@ from itertools import count
from .errors import MetarigError
from .collections import ensure_widget_collection
from .naming import change_name_side, get_name_side, Side
WGT_PREFIX = "WGT-" # Prefix for widget objects
@ -56,47 +57,113 @@ def obj_to_bone(obj, rig, bone_name, bone_transform_name=None):
elif bone.custom_shape_transform:
bone = bone.custom_shape_transform
shape_mat = Matrix.Translation(loc) @ (Euler(rot).to_matrix() @ Matrix.Diagonal(scale)).to_4x4()
shape_mat = Matrix.LocRotScale(loc, Euler(rot), scale)
obj.rotation_mode = 'XYZ'
obj.matrix_basis = rig.matrix_world @ bone.bone.matrix_local @ shape_mat
def create_widget(rig, bone_name, bone_transform_name=None, *, widget_name=None, widget_force_new=False):
def create_widget(rig, bone_name, bone_transform_name=None, *, widget_name=None, widget_force_new=False, subsurf=0):
""" Creates an empty widget object for a bone, and returns the object.
"""
assert rig.mode != 'EDIT'
obj_name = widget_name or WGT_PREFIX + rig.name + '_' + bone_name
from ..base_generate import BaseGenerator
scene = bpy.context.scene
collection = ensure_widget_collection(bpy.context, 'WGTS_' + rig.name)
bone = rig.pose.bones[bone_name]
# Access the current generator instance when generating (ugh, globals)
generator = BaseGenerator.instance
if generator:
collection = generator.widget_collection
else:
collection = ensure_widget_collection(bpy.context, 'WGTS_' + rig.name)
use_mirror = generator and generator.use_mirror_widgets
if use_mirror:
bone_mid_name = change_name_side(bone_name, Side.MIDDLE)
obj_name = widget_name or WGT_PREFIX + rig.name + '_' + bone_name
reuse_mesh = None
# Check if it already exists in the scene
if not widget_force_new:
if obj_name in scene.objects:
obj = None
if generator:
# Check if the widget was already generated
if bone_name in generator.new_widget_table:
return None
# If re-generating, check widgets used by the previous rig
obj = generator.old_widget_table.get(bone_name)
if not obj:
# Search the scene by name
obj = scene.objects.get(obj_name)
if obj:
# Record the generated widget
if generator:
generator.new_widget_table[bone_name] = obj
# Re-add to the collection if not there for some reason
if obj.name not in collection.objects:
collection.objects.link(obj)
# Flip scale for originally mirrored widgets
if obj.scale.x < 0 and bone.custom_shape_scale_xyz.x > 0:
bone.custom_shape_scale_xyz.x *= -1
# Move object to bone position, in case it changed
obj = scene.objects[obj_name]
obj_to_bone(obj, rig, bone_name, bone_transform_name)
return None
# Delete object if it exists in blend data but not scene data.
# This is necessary so we can then create the object without
# name conflicts.
if obj_name in bpy.data.objects:
bpy.data.objects.remove(bpy.data.objects[obj_name])
# Create a linked duplicate of the widget assigned in the metarig
reuse_widget = rig.pose.bones[bone_name].custom_shape
if reuse_widget:
subsurf = 0
reuse_mesh = reuse_widget.data
# Create mesh object
mesh = reuse_mesh or bpy.data.meshes.new(obj_name)
# Create a linked duplicate with the mirror widget
if not reuse_mesh and use_mirror and bone_mid_name != bone_name:
reuse_mesh = generator.widget_mirror_mesh.get(bone_mid_name)
# Create an empty mesh datablock if not linking
if reuse_mesh:
mesh = reuse_mesh
elif use_mirror and bone_mid_name != bone_name:
# When mirroring, untag side from mesh name, and remember it
mesh = bpy.data.meshes.new(change_name_side(obj_name, Side.MIDDLE))
generator.widget_mirror_mesh[bone_mid_name] = mesh
else:
mesh = bpy.data.meshes.new(obj_name)
# Create the object
obj = bpy.data.objects.new(obj_name, mesh)
collection.objects.link(obj)
# Add the subdivision surface modifier
if subsurf > 0:
mod = obj.modifiers.new("subsurf", 'SUBSURF')
mod.levels = subsurf
# Record the generated widget
if generator:
generator.new_widget_table[bone_name] = obj
# Flip scale for right side if mirroring widgets
if use_mirror and get_name_side(bone_name) == Side.RIGHT:
if bone.custom_shape_scale_xyz.x > 0:
bone.custom_shape_scale_xyz.x *= -1
# Move object to bone position and set layers
obj_to_bone(obj, rig, bone_name, bone_transform_name)
@ -187,7 +254,7 @@ def widget_generator(generate_func=None, *, register=None, subsurf=0):
"""
@functools.wraps(generate_func)
def wrapper(rig, bone_name, bone_transform_name=None, widget_name=None, widget_force_new=False, **kwargs):
obj = create_widget(rig, bone_name, bone_transform_name, widget_name=widget_name, widget_force_new=widget_force_new)
obj = create_widget(rig, bone_name, bone_transform_name, widget_name=widget_name, widget_force_new=widget_force_new, subsurf=subsurf)
if obj is not None:
geom = GeometryData()
@ -197,10 +264,6 @@ def widget_generator(generate_func=None, *, register=None, subsurf=0):
mesh.from_pydata(geom.verts, geom.edges, geom.faces)
mesh.update()
if subsurf:
mod = obj.modifiers.new("subsurf", 'SUBSURF')
mod.levels = subsurf
return obj
else:
return None