# Copyright (C) 2021 Victor Soupday # This file is part of CC/iC Blender Tools # # CC/iC Blender Tools is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # CC/iC Blender Tools is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with CC/iC Blender Tools. If not, see . import bpy import math, os, random from . import facerig_data, lib, utils, vars from . import drivers, bones from . import rigutils from mathutils import Vector, Matrix, Quaternion def shrink_slider_coords(coords, by_length): d = coords[1] - coords[0] l = d.length t = max(0, min(by_length / l, 0.5)) v0 = utils.lerp(coords[0], coords[1], t) v1 = utils.lerp(coords[0], coords[1], 1 - t) coords[0] = v0 coords[1] = v1 def objects_have_shape_key(objects, shape_key_name): for obj in objects: if obj.type == "MESH": if obj.data.shape_keys and obj.data.shape_keys.key_blocks: if shape_key_name in obj.data.shape_keys.key_blocks: return True return False def get_objects_shape_key_name(objects, shape_key_name, try_substitutes=False): """Some older characters an profiles have inconsisent expression names, resolve them here""" if shape_key_name.startswith("Teeth_") or shape_key_name.startswith("Dummy_"): return shape_key_name for obj in objects: if obj.type == "MESH": if obj.data.shape_keys and obj.data.shape_keys.key_blocks: if shape_key_name in obj.data.shape_keys.key_blocks: return shape_key_name if try_substitutes: if shape_key_name.endswith("_L"): shape_key_name = shape_key_name[:-2] + "_Left" elif shape_key_name.endswith("_R"): shape_key_name = shape_key_name[:-2] + "_Right" else: return None for obj in objects: if obj.type == "MESH": if obj.data.shape_keys and obj.data.shape_keys.key_blocks: if shape_key_name in obj.data.shape_keys.key_blocks: return shape_key_name return None def is_valid_control_def(control_name, control_def, rigify_rig, objects): bone_collection = rigify_rig.data.edit_bones if utils.get_mode() == "EDIT" else rigify_rig.pose.bones count = 0 total = 0 #if "CTRL_C_head" not in control_name: # return 0, 0 if control_def["widget_type"] == "slider" or control_def["widget_type"] == "curve_slider": if "blendshapes" in control_def: blendshapes = control_def["blendshapes"] for shape_key_name in blendshapes: total += 1 real_shape_key_name = get_objects_shape_key_name(objects, shape_key_name) if real_shape_key_name: count += 1 if "rigify" in control_def: control_bones = control_def["rigify"] for bone_def in control_bones: total += 1 bone_name = bone_def["bone"] if bone_name in bone_collection: count += 1 #if "bones" in control_def: # control_bones = control_def["bones"] # for bone_def in control_bones: # bone_name = bone_def["bone"] # if bone_name not in rigify_rig.pose.bones: # return False elif control_def["widget_type"] == "rect": if "blendshapes" in control_def: blendshapes_x = control_def["blendshapes"]["x"] blendshapes_y = control_def["blendshapes"]["y"] for shape_key_name in blendshapes_x: total += 1 real_shape_key_name = get_objects_shape_key_name(objects, shape_key_name) if real_shape_key_name: count += 1 for shape_key_name in blendshapes_y: total += 1 real_shape_key_name = get_objects_shape_key_name(objects, shape_key_name) if real_shape_key_name: count += 1 if "rigify" in control_def: control_bones_x = control_def["rigify"]["horizontal"] control_bones_y = control_def["rigify"]["horizontal"] for bone_def in control_bones_x: total += 1 bone_name = bone_def["bone"] if bone_name in bone_collection: count += 1 for bone_def in control_bones_y: total += 1 bone_name = bone_def["bone"] if bone_name in bone_collection: count += 1 #if "bones" in control_def: # control_bones_x = control_def["bones"]["horizontal"] # control_bones_y = control_def["bones"]["horizontal"] # for bone_def in control_bones_x: # bone_name = bone_def["bone"] # if bone_name not in rigify_rig.pose.bones: # return False # for bone_def in control_bones_y: # bone_name = bone_def["bone"] # if bone_name not in rigify_rig.pose.bones: # return False return count, total def get_facerig_config(chr_cache): facial_profile, viseme_profile = chr_cache.get_facial_profile() if facial_profile == "EXT": return facerig_data.FACERIG_EXT_CONFIG elif facial_profile == "STD": return facerig_data.FACERIG_STD_CONFIG elif facial_profile == "TRA": return facerig_data.FACERIG_TRA_CONFIG elif facial_profile == "MH": return facerig_data.FACERIG_MH_CONFIG return None def build_facerig(chr_cache, rigify_rig, meta_rig, cc3_rig): prefs = vars.prefs() if not chr_cache.can_expression_rig(): return chr_cache.rigify_face_control_color = prefs.rigify_face_control_color facial_profile, viseme_profile = chr_cache.get_facial_profile() objects = chr_cache.get_cache_objects() wgt_collection = f"WGTS_{cc3_rig.name}_rig" WGT_OUTLINE, WGT_GROUPS, WGT_LABELS, WGT_LINES, WGT_SLIDER, WGT_RECT, WGT_NUB, WGT_NAME = \ rigutils.get_expression_widgets(chr_cache, wgt_collection) WGT_OUTLINE2, WGT_GROUPS2, WGT_LABELS2, WGT_LINES2 = \ rigutils.get_expression_widgets_2(chr_cache, wgt_collection) bone_scale = Vector((0.125, 0.125, 0.125)) R = Matrix.Rotation(90*math.pi/180, 3, 'X') slider_controls = {} rect_controls = {} utils.log_info(f"Building Expression Rig for facial profile: {facial_profile}") if rigutils.edit_rig(rigify_rig): # place the face rig parent at eye level eye_l = bones.get_edit_bone(rigify_rig, "ORG-eye.L") eye_r = bones.get_edit_bone(rigify_rig, "ORG-eye.R") head = bones.get_edit_bone(rigify_rig, "head") eye_pos = (eye_l.head + eye_r.head) * 0.5 head_pos = head.head.copy() z_pos = eye_pos.z head_pos.z = z_pos mch_parent = bones.copy_edit_bone(rigify_rig, "head", "MCH-facerig_parent", "neck", 1.0) mch_parent.head = head_pos mch_parent.tail = head_pos + Vector((0,0,0.1)) mch_parent.align_roll(Vector((0,-1,0))) bones.copy_edit_bone(rigify_rig, "MCH-facerig_parent", "MCH-facerig", "", 0.5) # place the face rig control ~20cm in front of face facerig_bone = bones.copy_edit_bone(rigify_rig, "MCH-facerig", "facerig", "MCH-facerig", 1.0) facerig_bone.head += Vector((0, -0.2, 0)) facerig_bone.tail += Vector((0, -0.2, 0)) bones.copy_edit_bone(rigify_rig, "facerig", "facerig_name", "facerig", 0.8) bones.copy_edit_bone(rigify_rig, "facerig", "facerig_groups", "facerig", 0.6) bones.copy_edit_bone(rigify_rig, "facerig", "facerig_labels", "facerig", 0.4) bones.copy_edit_bone(rigify_rig, "facerig", "MCH-facerig_controls", "facerig", 0.2) if facial_profile == "MH": facerig2_bone = bones.copy_edit_bone(rigify_rig, "MCH-facerig", "facerig2", "facerig", 1.0) facerig2_bone.head += Vector((0.2, -0.2, 0)) facerig2_bone.tail += Vector((0.2, -0.2, 0)) bones.copy_edit_bone(rigify_rig, "facerig2", "facerig2_groups", "facerig2", 0.6) bones.copy_edit_bone(rigify_rig, "facerig2", "facerig2_labels", "facerig2", 0.4) bones.copy_edit_bone(rigify_rig, "facerig2", "MCH-facerig2_controls", "facerig2", 0.2) # add MCH bone for head controls # MCH-ROT-head needs offset loc/rot constraints head_ctrl = bones.copy_edit_bone(rigify_rig, "head", "MCH-CTRL-head", "MCH-ROT-head", 0.4) head_ctrl.align_roll(Vector((0,-1,0))) head.parent = head_ctrl # add MCH bone for eye tracking controls left_eye_ctrl = bones.copy_edit_bone(rigify_rig, "MCH-eye.L", "MCH-CTRL-eye.L", "master_eye.L", 0.5) left_eye_mch = bones.get_edit_bone(rigify_rig, "MCH-eye.L") left_eye_ctrl.align_roll(Vector((0,0,1))) left_eye_mch.parent = left_eye_ctrl right_eye_ctrl = bones.copy_edit_bone(rigify_rig, "MCH-eye.R", "MCH-CTRL-eye.R", "master_eye.R", 0.5) right_eye_mch = bones.get_edit_bone(rigify_rig, "MCH-eye.R") right_eye_ctrl.align_roll(Vector((0,0,1))) right_eye_mch.parent = right_eye_ctrl # add MCH bone for jaw controls jaw_ctrl = bones.copy_edit_bone(rigify_rig, "jaw_master", "MCH-CTRL-jaw", "ORG-face", 0.5) jaw_master = bones.get_edit_bone(rigify_rig, "jaw_master") jaw_ctrl.align_roll(Vector((0,0,-1))) jaw_master.parent = jaw_ctrl FACERIG_CONFIG = get_facerig_config(chr_cache) for control_name, control_def in FACERIG_CONFIG.items(): count, total = is_valid_control_def(control_name, control_def, rigify_rig, objects) if count == 0: utils.log_warn(f"Invalid expression control: {control_name}") continue elif count != total: utils.log_warn(f"Missing shape keys or bones for control: {control_name}") if control_def["widget_type"] == "slider" or control_def["widget_type"] == "curve_slider": outline = control_def.get("outline", 1) if facial_profile == "MH" and outline == 2: LINES = WGT_LINES2 mch_facerig_name = "MCH-facerig2_controls" facerig_parent = facerig2_bone else: LINES = WGT_LINES mch_facerig_name = "MCH-facerig_controls" facerig_parent = facerig_bone invert = control_def["range"][1] < control_def["range"][0] zero = utils.inverse_lerp(control_def["range"][0], control_def["range"][1], 0.0) indices = control_def["indices"] coords = [ LINES.data.vertices[i].co.copy() for i in indices ] #shrink_slider_coords(coords, 0.01) line_bone = bones.new_edit_bone(rigify_rig, control_name+"_line", mch_facerig_name) line_bone.head = (R @ coords[0] * bone_scale * 1.0) + facerig_parent.head line_bone.tail = (R @ utils.lerp(coords[0], coords[1], 0.5) * bone_scale * 1.0) + facerig_parent.head line_bone.align_roll(Vector((0, -1, 0))) length = 2 * (line_bone.head - line_bone.tail).length nub_bone = bones.new_edit_bone(rigify_rig, control_name, line_bone.name) zy = (1-zero) if invert else zero nub_bone.head = utils.lerp(line_bone.head, line_bone.tail, zy * 2, clamp=False) nub_bone.tail = (line_bone.tail - line_bone.head).normalized() * (length / 2) + nub_bone.head nub_bone.align_roll(Vector((0, -1, 0))) slider_controls[control_name] = (control_def, line_bone.name, nub_bone.name, length, zero) elif control_def["widget_type"] == "rect": outline = control_def.get("outline", 1) if facial_profile == "MH" and outline == 2: LINES = WGT_LINES2 mch_facerig_name = "MCH-facerig2_controls" facerig_parent = facerig2_bone else: LINES = WGT_LINES mch_facerig_name = "MCH-facerig_controls" facerig_parent = facerig_bone x_invert = control_def["x_range"][1] < control_def["x_range"][0] y_invert = control_def["y_range"][1] < control_def["y_range"][0] zero_x = utils.inverse_lerp(control_def["x_range"][0], control_def["x_range"][1], 0.0) zero_y = utils.inverse_lerp(control_def["y_range"][0], control_def["y_range"][1], 0.0) indices = control_def["indices"] coords = [ LINES.data.vertices[i].co.copy() for i in indices ] p_min = Vector((min(coords[0].x, coords[1].x, coords[2].x, coords[3].x), min(coords[0].y, coords[1].y, coords[2].y, coords[3].y), 0)) p_max = Vector((max(coords[0].x, coords[1].x, coords[2].x, coords[3].x), max(coords[0].y, coords[1].y, coords[2].y, coords[3].y), 0)) width = (p_max.x - p_min.x) height = (p_max.y - p_min.y) pB0 = Vector((p_min.x + width / 2, p_min.y, 0.0)) pB1 = Vector((p_min.x + width / 2, p_min.y + height / 2, 0.0)) zx = (1-zero_x) if x_invert else zero_x zy = (1-zero_y) if y_invert else zero_y pN0 = Vector((p_min.x + width * zx, p_min.y + height * zy, 0)) pN1 = Vector((pN0.x, pN0.y + height / 2, 0)) pN2 = Vector((pN0.x, pN0.y - height / 2, 0)) box_bone = bones.new_edit_bone(rigify_rig, control_name+"_box", mch_facerig_name) box_bone.head = (R @ pB0 * bone_scale * 1.0) + facerig_parent.head box_bone.tail = (R @ pB1 * bone_scale * 1.0) + facerig_parent.head box_bone.align_roll(Vector((0, -1, 0))) nub_bone = bones.new_edit_bone(rigify_rig, control_name, box_bone.name) if control_def.get("x_mirror", False): nub_bone.head = (R @ pN0 * bone_scale * 1.0) + facerig_parent.head nub_bone.tail = (R @ pN2 * bone_scale * 1.0) + facerig_parent.head else: nub_bone.head = (R @ pN0 * bone_scale * 1.0) + facerig_parent.head nub_bone.tail = (R @ pN1 * bone_scale * 1.0) + facerig_parent.head nub_bone.align_roll(Vector((0, -1, 0))) width *= bone_scale.x height *= bone_scale.y rect_controls[control_name] = (control_def, box_bone.name, nub_bone.name, width, height, zero_x, zero_y) if rigutils.select_rig(rigify_rig): bones.set_bone_collection(rigify_rig, "MCH-CTRL-head", "MCH", None, 30) bones.set_bone_collection(rigify_rig, "MCH-CTRL-jaw", "MCH", None, 30) face_rig = bones.get_pose_bone(rigify_rig, "facerig") drivers.add_custom_float_property(face_rig, "eyes_track", 0.0, 0.0, 1.0, description="Eye tracking influence from Rigify eye rig") #drivers.add_custom_float_property(face_rig, "eyes_track_enable", 1.0, 0.0, 1.0, description="Enable/disable eye tracking from Rigify eye rig") bones.add_bone_collection(rigify_rig, "Face (Expressions)", "Face", color_set="CUSTOM", custom_color=chr_cache.rigify_face_control_color, lerp=0) bones.add_bone_collection(rigify_rig, "Face (UI)", "UI", color_set="CUSTOM", custom_color=(1,1,1)) bones.set_bone_collection_visibility(rigify_rig, "Face (Expressions)", 22, True) bones.set_bone_collection_visibility(rigify_rig, "Face (UI)", 23, True) bones.set_bone_collection(rigify_rig, "MCH-jaw_move", "MCH", None, 30) bones.set_bone_collection(rigify_rig, "MCH-facerig", "MCH", None, 30) bones.set_bone_collection(rigify_rig, "MCH-facerig_controls", "MCH", None, 30) bones.set_bone_collection(rigify_rig, "MCH-facerig_parent", "MCH", None, 30) if facial_profile == "MH": bones.set_bone_collection(rigify_rig, "MCH-facerig2", "MCH", None, 30) bones.set_bone_collection(rigify_rig, "MCH-facerig2_controls", "MCH", None, 30) bone_names = [ "facerig", "facerig_groups", "facerig_labels", "facerig_name", "facerig2", "facerig2_groups", "facerig2_labels" ] bone_colors = [ "WHITE", "GROUP", "WHITE", "WHITE", "WHITE", "GROUP", "WHITE" ] bone_groups = [ "UI", "UI", "UI", "UI", "UI", "UI", "UI" ] bone_shapes = [ WGT_OUTLINE, WGT_GROUPS, WGT_LABELS, WGT_NAME, WGT_OUTLINE2, WGT_GROUPS2, WGT_LABELS2 ] bone_selectable = [ True, False, False, False, True, False, False ] for i, bone_name in enumerate(bone_names): pose_bone = bones.get_pose_bone(rigify_rig, bone_name) if pose_bone: bones.set_bone_collection(rigify_rig, pose_bone, "Face (UI)", bone_groups[i], 23) bones.set_bone_color(rigify_rig, pose_bone, bone_colors[i]) pose_bone.custom_shape = bone_shapes[i] pose_bone.custom_shape_scale_xyz = bone_scale pose_bone.bone.hide_select = not bone_selectable[i] pose_bone.use_custom_shape_bone_size = False bones.keep_locks(pose_bone, no_bake=True) for slider_name, slider_def in slider_controls.items(): control_def, line_bone_name, nub_bone_name, length, zero = slider_def line_bone = bones.get_pose_bone(rigify_rig, line_bone_name) nub_bone = bones.get_pose_bone(rigify_rig, nub_bone_name) line_bone.custom_shape = WGT_SLIDER nub_bone.custom_shape = WGT_NUB line_bone.bone.hide_select = True bones.keep_locks(line_bone, no_bake=True) nub_bone.use_custom_shape_bone_size = False nub_bone.custom_shape_scale_xyz = bone_scale nub_bone.lock_location = [True, False, True] nub_bone.lock_rotation = [True, True, True] nub_bone.lock_scale = [True, True, True] hue_shift = control_def.get("color_shift", 0.0) bones.keep_locks(nub_bone) bones.set_bone_collection(rigify_rig, line_bone, "Face (UI)", "Face", 22) bones.set_bone_color(rigify_rig, line_bone, "FACERIG_DARK", "FACERIG_DARK", "FACERIG_DARK", chr_cache=chr_cache, hue_shift=hue_shift) bones.set_bone_collection(rigify_rig, nub_bone, "Face (Expressions)", "Face", 22) bones.set_bone_color(rigify_rig, nub_bone, "FACERIG", "FACERIG", "FACERIG", chr_cache=chr_cache, hue_shift=hue_shift) control_range_y = control_def["range"] y_invert = control_range_y[1] < control_range_y[0] if y_invert: neg_y = -length * (1 - zero) * control_range_y[1] pos_y = -length * zero * control_range_y[0] else: neg_y = length * zero * control_range_y[0] pos_y = length * (1 - zero) * control_range_y[1] drivers.add_custom_float_property(line_bone, "slider_length", length) bones.add_limit_location_constraint(rigify_rig, nub_bone_name, 0, neg_y, 0, 0, pos_y, 0, space="LOCAL", use_transform_limit=True) for rect_name, rect_def in rect_controls.items(): control_def, box_bone_name, nub_bone_name, width, height, zero_x, zero_y = rect_def box_bone = bones.get_pose_bone(rigify_rig, box_bone_name) nub_bone = bones.get_pose_bone(rigify_rig, nub_bone_name) box_bone.custom_shape = WGT_RECT nub_bone.custom_shape = WGT_NUB box_bone.bone.hide_select = True bones.keep_locks(box_bone, no_bake=True) aspect = width / height box_bone.custom_shape_scale_xyz = Vector((aspect,1,1)) nub_bone.use_custom_shape_bone_size = False nub_bone.custom_shape_scale_xyz = bone_scale nub_bone.lock_location = [False, False, True] nub_bone.lock_rotation = [True, True, True] nub_bone.lock_scale = [True, True, True] nub_bone.lock_rotation_w = True nub_bone.lock_rotations_4d = True hue_shift = control_def.get("color_shift", 0.0) bones.keep_locks(nub_bone) bones.set_bone_collection(rigify_rig, box_bone, "Face (UI)", "Face", 22) bones.set_bone_color(rigify_rig, box_bone, "FACERIG_DARK", "FACERIG_DARK", "FACERIG_DARK", chr_cache=chr_cache, hue_shift=hue_shift) bones.set_bone_collection(rigify_rig, nub_bone, "Face (Expressions)", "Face", 22) bones.set_bone_color(rigify_rig, nub_bone, "FACERIG", "FACERIG", "FACERIG", chr_cache=chr_cache, hue_shift=hue_shift) control_range_x = control_def["x_range"] control_range_y = control_def["y_range"] x_invert = control_range_x[1] < control_range_x[0] y_invert = control_range_y[1] < control_range_y[0] if x_invert: neg_x = width * (1 - zero_x) * control_range_x[1] pos_x = width * zero_x * control_range_x[0] else: neg_x = width * zero_x * control_range_x[0] pos_x = width * (1 - zero_x) * control_range_x[1] if y_invert: neg_y = height * (1 - zero_y) * control_range_y[1] pos_y = height * zero_y * control_range_y[0] else: neg_y = height * zero_y * control_range_y[0] pos_y = height * (1 - zero_y) * control_range_y[1] if control_def.get("x_mirror"): nub_bone.scale.y = -1 #m = min_y #min_y = max_y #max_y = m drivers.add_custom_float_property(box_bone, "x_slider_length", width) drivers.add_custom_float_property(box_bone, "y_slider_length", height) bones.add_limit_location_constraint(rigify_rig, nub_bone_name, neg_x, neg_y, 0, pos_x, pos_y, 0, space="LOCAL", use_transform_limit=True) def get_generated_controls(chr_cache, rigify_rig): slider_controls = {} curve_slider_controls = {} rect_controls = {} FACERIG_CONFIG = get_facerig_config(chr_cache) for control_name, control_def in FACERIG_CONFIG.items(): if control_def["widget_type"] == "slider": zero_point = utils.inverse_lerp(control_def["range"][0], control_def["range"][1], 0.0) line_bone_name = control_name + "_line" nub_bone_name = control_name if line_bone_name in rigify_rig.pose.bones: line_pose_bone = rigify_rig.pose.bones[line_bone_name] line_bone = line_pose_bone.bone length = line_pose_bone["slider_length"] if "slider_length" in line_pose_bone else line_bone.length * 2 slider_controls[control_name] = (control_def, line_bone_name, nub_bone_name, length, zero_point) if control_def["widget_type"] == "curve_slider": zero_point = utils.inverse_lerp(control_def["range"][0], control_def["range"][1], 0.0) line_bone_name = control_name + "_line" nub_bone_name = control_name if line_bone_name in rigify_rig.pose.bones: line_pose_bone = rigify_rig.pose.bones[line_bone_name] line_bone = line_pose_bone.bone length = line_pose_bone["slider_length"] if "slider_length" in line_pose_bone else line_bone.length * 2 curve_slider_controls[control_name] = (control_def, line_bone_name, nub_bone_name, length, zero_point) if control_def["widget_type"] == "rect": zero_x = utils.inverse_lerp(control_def["x_range"][0], control_def["x_range"][1], 0.0) zero_y = utils.inverse_lerp(control_def["y_range"][0], control_def["y_range"][1], 0.0) box_bone_name = control_name+"_box" nub_bone_name = control_name if box_bone_name in rigify_rig.pose.bones: box_pose_bone = rigify_rig.pose.bones[box_bone_name] box_bone = box_pose_bone.bone width = box_pose_bone["x_slider_length"] if "x_slider_length" in box_pose_bone else box_bone.length * 2 height = box_pose_bone["y_slider_length"] if "y_slider_length" in box_pose_bone else box_bone.length * 2 rect_controls[control_name] = (control_def, box_bone_name, nub_bone_name, width, height, zero_x, zero_y) return slider_controls, curve_slider_controls, rect_controls def get_expression_bones_def(chr_cache, control_name, control_def): bones_def = None if chr_cache: if control_def["widget_type"] == "rect": bones_def = {} for ctrl_dir, ctrl_axis in [("horizontal", "x"), ("vertical", "y")]: bones_def[ctrl_dir] = [] if "blendshapes" in control_def: for shape_key_name, value in control_def["blendshapes"][ctrl_axis].items(): range_value = utils.sign(value) for expression_cache in chr_cache.expression_set: if expression_cache.key_name == shape_key_name: bones_def[ctrl_dir].append({ "shape_key": shape_key_name, "bone": expression_cache.rigify_bone_name, "value": range_value, "Translate": utils.prop_to_list(expression_cache.rigify_translation), "Rotation": utils.prop_to_list(expression_cache.rigify_rotation), "Source Bone": expression_cache.bone_name, "Source Translate": utils.prop_to_list(expression_cache.translation), "Source Rotation": utils.prop_to_list(expression_cache.rotation), "Offset Bone": expression_cache.offset_bone_name, "Offset Translate": utils.prop_to_list(expression_cache.offset_translation), "Offset Rotation": utils.prop_to_list(expression_cache.offset_rotation), }) else: bones_def = [] if "blendshapes" in control_def: for shape_key_name, value in control_def["blendshapes"].items(): range_value = utils.sign(value) for expression_cache in chr_cache.expression_set: if expression_cache.key_name == shape_key_name: bones_def.append({ "shape_key": shape_key_name, "bone": expression_cache.rigify_bone_name, "value": range_value, "Translate": utils.prop_to_list(expression_cache.rigify_translation), "Rotation": utils.prop_to_list(expression_cache.rigify_rotation), "Source Bone": expression_cache.bone_name, "Source Translate": utils.prop_to_list(expression_cache.translation), "Source Rotation": utils.prop_to_list(expression_cache.rotation), "Offset Bone": expression_cache.offset_bone_name, "Offset Translate": utils.prop_to_list(expression_cache.offset_translation), "Offset Rotation": utils.prop_to_list(expression_cache.offset_rotation), }) return bones_def def collect_driver_defs(chr_cache, rigify_rig, slider_controls, curve_slider_controls, rect_controls): shape_key_driver_defs = {} bone_driver_defs = {} offset_driver_defs = {} use_offset_rig = True # collect slider control data into shapekey and bone driver defs for slider_name, slider_def in slider_controls.items(): control_def, line_bone_name, nub_bone_name, length, zero_point = slider_def control_range_y = control_def["range"] y_invert = control_range_y[1] < control_range_y[0] if y_invert: neg_y = length * (1 - zero_point) * control_range_y[1] pos_y = length * zero_point * control_range_y[0] else: neg_y = length * zero_point * control_range_y[0] pos_y = length * (1 - zero_point) * control_range_y[1] influence = control_def.get("influence", None) if "blendshapes" in control_def: num_keys = len(control_def["blendshapes"]) use_negative = (control_def.get("negative", False) or ((num_keys == 1) and (abs(control_range_y[1] - control_range_y[0]) == 2))) for i, (shape_key_name, shape_key_value) in enumerate(control_def["blendshapes"].items()): var_axis = facerig_data.LOC_AXES.get("y")[1] if shape_key_name not in shape_key_driver_defs: shape_key_driver_defs[shape_key_name] = {} key_control_def = { "value": abs(shape_key_value), "distance": neg_y if shape_key_value < 0 else pos_y, "var_axis": var_axis, "num_keys": num_keys, "invert": y_invert, "use_strength": control_def.get("strength", True), "use_negative": use_negative, "influence": influence, } shape_key_driver_defs[shape_key_name][nub_bone_name] = key_control_def #rigify_bones = control_def.get("rigify") bones_def = get_expression_bones_def(chr_cache, slider_name, control_def) if bones_def: for i, bone_def in enumerate(bones_def): bone_name = bone_def["bone"] value = bone_def.get("value", 1) if bone_name in rigify_rig.pose.bones: if "Translate" in bone_def: for i, def_axis in enumerate(["x", "y", "z"]): prop, axis, index = facerig_data.LOC_AXES.get(def_axis, (None, None, None)) scalar = bone_def["Translate"][i] if abs(scalar) > 0.0001: driver_id = (bone_name, prop, index) var_axis = facerig_data.LOC_AXES.get("y")[1] bone_control_def = { "bone": bone_def["bone"], "offset": 0, "scalar": scalar, "range": value, "distance": neg_y if value < 0 else pos_y, "var_axis": var_axis, "use_strength": control_def.get("strength", True), "use_negative": use_negative, "influence": influence, } if driver_id not in bone_driver_defs: bone_driver_defs[driver_id] = {} bone_driver_defs[driver_id][(nub_bone_name, value, var_axis)] = bone_control_def if "Rotation" in bone_def: for i, def_axis in enumerate(["x", "y", "z"]): prop, axis, index = facerig_data.ROT_AXES.get(def_axis, (None, None, None)) scalar = bone_def["Rotation"][i] if abs(scalar) > 0.001: driver_id = (bone_name, prop, index) var_axis = facerig_data.LOC_AXES.get("y")[1] bone_control_def = { "bone": bone_def["bone"], "offset": 0, "scalar": scalar, "range": value, "distance": neg_y if value < 0 else pos_y, "var_axis": var_axis, "use_strength": control_def.get("strength", True), "use_negative": use_negative, "influence": influence, } if driver_id not in bone_driver_defs: bone_driver_defs[driver_id] = {} bone_driver_defs[driver_id][(nub_bone_name, value, var_axis)] = bone_control_def if use_offset_rig and bone_def["Offset Bone"] and control_def.get("offset", False): offset_bone = bone_def["Offset Bone"] shape_key_name = bone_def["shape_key"] source_tra = utils.array_to_vector(bone_def["Offset Translate"]) if "Offset Translate" in bone_def else Vector((0,0,0)) source_euler = bone_def["Offset Rotation"] if "Offset Rotation" in bone_def else [0,0,0] axes = ["x", "y", "z"] if abs(source_euler[0]) + abs(source_euler[1]) + abs(source_euler[2]) > 0.01: index = utils.largest_index(source_euler, use_abs=True) axis = axes[index] prop, prop_axis, prop_index = facerig_data.ROT_AXES.get(axis, (None, None, None)) scalar = source_euler[index] elif source_tra.length > 0.001: index = utils.largest_index(source_tra, use_abs=True) axis = axes[index] prop, prop_axis, prop_index = facerig_data.LOC_AXES.get(axis, (None, None, None)) scalar = source_tra[index] if source_tra.length > 0.001 or (abs(source_euler[0]) + abs(source_euler[1]) + abs(source_euler[2])) > 0.01: if shape_key_name not in offset_driver_defs: offset_driver_defs[shape_key_name] = {} offset_driver_defs[shape_key_name][(nub_bone_name, value)] = { "dir": bone_def["value"], "bone": bone_def["Offset Bone"], "value": scalar, "influence": influence, "axis": prop_axis } # collect curve_slider control data into shapekey and bone driver defs for slider_name, slider_def in curve_slider_controls.items(): control_def, line_bone_name, nub_bone_name, length, zero_point = slider_def control_range_y = control_def["range"] y_invert = control_range_y[1] < control_range_y[0] if y_invert: neg_y = length * (1 - zero_point) * control_range_y[1] pos_y = length * zero_point * control_range_y[0] else: neg_y = length * zero_point * control_range_y[0] pos_y = length * (1 - zero_point) * control_range_y[1] influence = control_def.get("influence", None) # only blend shapes in curve sliders if "blendshapes" in control_def: num_keys = len(control_def["blendshapes"]) use_negative = (control_def.get("negative", False) or ((num_keys == 1) and (abs(control_range_y[1] - control_range_y[0]) == 2))) for i, (shape_key_name, shape_key_value) in enumerate(control_def["blendshapes"].items()): curve: list = control_def["curve"][i].copy() curve.sort() var_axis = facerig_data.LOC_AXES.get("y")[1] if shape_key_name not in shape_key_driver_defs: shape_key_driver_defs[shape_key_name] = {} key_control_def = { "start": curve[0], "mid": curve[1], "end": curve[-1], "value": abs(shape_key_value), "distance": neg_y if shape_key_value < 0 else pos_y, "invert": y_invert, "var_axis": var_axis, "num_keys": num_keys, "use_strength": control_def.get("strength", True), "use_negative": use_negative, "influence": influence, } shape_key_driver_defs[shape_key_name][nub_bone_name] = key_control_def # collect rect control data into shape key and bone driver defs for rect_name, rect_def in rect_controls.items(): control_def, box_bone_name, nub_bone_name, width, height, zero_x, zero_y = rect_def control_range_x = control_def["x_range"] control_range_y = control_def["y_range"] x_invert = control_range_x[1] < control_range_x[0] y_invert = control_range_y[1] < control_range_y[0] if x_invert: neg_x = width * (1 - zero_x) * control_range_x[1] pos_x = width * zero_x * control_range_x[0] else: neg_x = width * zero_x * control_range_x[0] pos_x = width * (1 - zero_x) * control_range_x[1] if y_invert: neg_y = height * (1 - zero_y) * control_range_y[1] pos_y = height * zero_y * control_range_y[0] else: neg_y = height * zero_y * control_range_y[0] pos_y = height * (1 - zero_y) * control_range_y[1] influence = control_def.get("influence", None) ctrl_axes = [ ("horizontal", "x", neg_x, pos_x, control_range_x, x_invert), ("vertical", "y", neg_y, pos_y, control_range_y, y_invert) ] if "blendshapes" in control_def: for ctrl_dir, ctrl_axis, neg_d, pos_d, control_range, invert in ctrl_axes: num_keys = len(control_def["blendshapes"][ctrl_axis]) use_negative = (control_def.get("negative", False) or ((num_keys == 1) and (abs(control_range[1] - control_range[0]) == 2))) parent = control_def.get(f"{ctrl_axis}_parent") for i, (shape_key_name, shape_key_value) in enumerate(control_def["blendshapes"][ctrl_axis].items()): var_axis = facerig_data.LOC_AXES.get(ctrl_axis)[1] if shape_key_name not in shape_key_driver_defs: shape_key_driver_defs[shape_key_name] = {} key_control_def = { "value": abs(shape_key_value), "distance": neg_d if shape_key_value < 0 else pos_d, "invert": invert, "var_axis": var_axis, "num_keys": num_keys, "use_strength": control_def.get("strength", True), "use_negative": use_negative, "influence": influence, } shape_key_driver_defs[shape_key_name][nub_bone_name] = key_control_def bones_def = get_expression_bones_def(chr_cache, rect_name, control_def) if bones_def: for ctrl_dir, ctrl_axis, neg_d, pos_d, control_range, invert in ctrl_axes: for bone_def in bones_def[ctrl_dir]: bone_name = bone_def["bone"] value = bone_def.get("value", 1) if bone_name in rigify_rig.pose.bones: if "Translate" in bone_def: for i, def_axis in enumerate(["x", "y", "z"]): prop, axis, index = facerig_data.LOC_AXES.get(def_axis, (None, None, None)) scalar = bone_def["Translate"][i] if abs(scalar) > 0.0001: driver_id = (bone_name, prop, index) var_axis = facerig_data.LOC_AXES.get(ctrl_axis)[1] bone_control_def = { "bone": bone_def["bone"], "offset": 0, "range": value, "scalar": scalar, "distance": neg_d if value < 0 else pos_d, "var_axis": var_axis, "use_strength": control_def.get("strength", True), "use_negative": use_negative, "influence": influence, } if driver_id not in bone_driver_defs: bone_driver_defs[driver_id] = {} bone_driver_defs[driver_id][(nub_bone_name, value, var_axis)] = bone_control_def if "Rotation" in bone_def: for i, def_axis in enumerate(["x", "y", "z"]): prop, axis, index = facerig_data.ROT_AXES.get(def_axis, (None, None, None)) scalar = bone_def["Rotation"][i] if abs(scalar) > 0.001: driver_id = (bone_name, prop, index) var_axis = facerig_data.LOC_AXES.get(ctrl_axis)[1] bone_control_def = { "bone": bone_def["bone"], "offset": 0, "scalar": scalar, "range": value, "distance": neg_d if value < 0 else pos_d, "var_axis": var_axis, "use_strength": control_def.get("strength", True), "use_negative": use_negative, "influence": influence, } #if bone_def["bone"] == "MCH-CTRL-eye.L": # print ("MCH-CTRL-eye.L") # print(driver_id) # print(bone_control_def) if driver_id not in bone_driver_defs: bone_driver_defs[driver_id] = {} bone_driver_defs[driver_id][(nub_bone_name, value, var_axis)] = bone_control_def if use_offset_rig and bone_def["Offset Bone"] and control_def.get("offset", False): offset_bone = bone_def["Offset Bone"] shape_key_name = bone_def["shape_key"] source_tra = utils.array_to_vector(bone_def["Offset Translate"]) if "Offset Translate" in bone_def else Vector((0,0,0)) source_euler = bone_def["Offset Rotation"] if "Offset Rotation" in bone_def else [0,0,0] axes = ["x", "y", "z"] if abs(source_euler[0]) + abs(source_euler[1]) + abs(source_euler[2]) > 0.01: index = utils.largest_index(source_euler, use_abs=True) axis = axes[index] prop, prop_axis, prop_index = facerig_data.ROT_AXES.get(axis, (None, None, None)) scalar = source_euler[index] elif source_tra.length > 0.001: index = utils.largest_index(source_tra, use_abs=True) axis = axes[index] prop, prop_axis, prop_index = facerig_data.LOC_AXES.get(axis, (None, None, None)) scalar = source_tra[index] if source_tra.length > 0.001 or (abs(source_euler[0]) + abs(source_euler[1]) + abs(source_euler[2])) > 0.01: if shape_key_name not in offset_driver_defs: offset_driver_defs[shape_key_name] = {} offset_driver_defs[shape_key_name][(nub_bone_name, value)] = { "dir": bone_def["value"], "bone": offset_bone, "value": scalar, "influence": influence, "axis": prop_axis } return shape_key_driver_defs, bone_driver_defs, offset_driver_defs def fvar(float_value): return "{0:0.5f}".format(float_value).rstrip('0').rstrip('.') def build_facerig_drivers(chr_cache, rigify_rig): # first drive the shape keys on any other body objects from the head body object # expression rig will then override these drivers.add_body_shape_key_drivers(chr_cache, True) BONE_CLEAR_CONSTRAINTS = [ #"MCH-eye.L", "MCH-eye.R" ] FACERIG_CONFIG = get_facerig_config(chr_cache) facerig_bone = bones.get_pose_bone(rigify_rig, "facerig") # initialize target bone rotation modes and clear unwanted constraints for control_name, control_def in FACERIG_CONFIG.items(): bones_def = get_expression_bones_def(chr_cache, control_name, control_def) if control_def["widget_type"] == "rect": if bones_def: for axis_dir, bone_list in bones_def.items(): for bone_def in bone_list: bone_name = bone_def["bone"] if bone_name in rigify_rig.pose.bones: pose_bone = rigify_rig.pose.bones[bone_name] pose_bone.rotation_mode = "XYZ" if bone_name in BONE_CLEAR_CONSTRAINTS: bones.clear_constraints(rigify_rig, bone_name) else: if bones_def: for bone_def in bones_def: bone_name = bone_def["bone"] if bone_name in rigify_rig.pose.bones: pose_bone = rigify_rig.pose.bones[bone_name] pose_bone.rotation_mode = "XYZ" if bone_name in BONE_CLEAR_CONSTRAINTS: bones.clear_constraints(rigify_rig, bone_name) if rigutils.select_rig(rigify_rig): bones.set_bone_collection_visibility(rigify_rig, "Face", 0, False) bones.set_bone_collection_visibility(rigify_rig, "Face (Primary)", 1, False) bones.set_bone_collection_visibility(rigify_rig, "Face (Secondary)", 2, False) facerig_bone = bones.get_pose_bone(rigify_rig, "facerig") if "head_follow" not in facerig_bone: drivers.add_custom_float_property(facerig_bone, "head_follow", 0.5, value_min=0.0, value_max=2.0, description="How much the expression rig follows the head movements") if "key_strength" not in facerig_bone: drivers.add_custom_float_property(facerig_bone, "key_strength", 1.0, value_min=0.0, value_max=2.0, precision=1, description="Overall strength of the expression rig shape keys") if "bone_strength" not in facerig_bone: drivers.add_custom_float_property(facerig_bone, "bone_strength", 1.0, value_min=0.0, value_max=2.0, precision=1, description="Overall strength of the expression rig bone movements") data_path = facerig_bone.path_from_id("[\"head_follow\"]") bones.clear_constraints(rigify_rig, "MCH-facerig") child_con = bones.add_child_of_constraint(rigify_rig, rigify_rig, "root", "MCH-facerig", 1.0) loc_con = bones.add_copy_location_constraint(rigify_rig, rigify_rig, "MCH-facerig_parent", "MCH-facerig", 0.2) rot_con1 = bones.add_copy_rotation_constraint(rigify_rig, rigify_rig, "MCH-facerig_parent", "MCH-facerig", 0.6, use_x=False, use_y=False, use_z=True) rot_con2 = bones.add_copy_rotation_constraint(rigify_rig, rigify_rig, "MCH-facerig_parent", "MCH-facerig", 0.6, use_x=True, use_y=True, use_z=False) bones.add_constraint_influence_driver(rigify_rig, "MCH-facerig", rigify_rig, data_path, "rf", constraint=child_con, expression="(1.0 if rf else 0.0)") bones.add_constraint_influence_driver(rigify_rig, "MCH-facerig", rigify_rig, data_path, "rf", loc_con) bones.add_constraint_influence_driver(rigify_rig, "MCH-facerig", rigify_rig, data_path, "rf", rot_con1) bones.add_constraint_influence_driver(rigify_rig, "MCH-facerig", rigify_rig, data_path, "rf", rot_con2, expression="(rf - 1)") face_rig = bones.get_pose_bone(rigify_rig, "facerig") data_paths = [ face_rig.path_from_id("[\"eyes_track\"]"), #face_rig.path_from_id("[\"eyes_track_enable\"]") ] var_names = [ "trck", #"trck_enable" ] bones.add_constraint_influence_driver(rigify_rig, "MCH-eye.L", face_rig, data_paths, var_names, constraint_type="DAMPED_TRACK", expression="trck") bones.add_constraint_influence_driver(rigify_rig, "MCH-eye.R", face_rig, data_paths, var_names, constraint_type="DAMPED_TRACK", expression="trck") # special control bone offsets for jaw, eyes and head #bones.add_copy_location_constraint(rigify_rig, rigify_rig, "MCH-CTRL-head", "MCH-ROT-head", space="LOCAL_OWNER_ORIENT", use_offset=True) #bones.add_copy_rotation_constraint(rigify_rig, rigify_rig, "MCH-CTRL-head", "MCH-ROT-head", space="LOCAL_OWNER_ORIENT", use_offset=True) #bones.add_copy_location_constraint(rigify_rig, rigify_rig, "MCH-CTRL-jaw", "MCH-jaw_master", space="LOCAL_OWNER_ORIENT", use_offset=True) #bones.add_copy_rotation_constraint(rigify_rig, rigify_rig, "MCH-CTRL-jaw", "MCH-jaw_master", space="LOCAL_OWNER_ORIENT", use_offset=True) #bones.add_copy_location_constraint(rigify_rig, rigify_rig, "MCH-CTRL-eye.L", "MCH-eye.L", space="LOCAL_OWNER_ORIENT", use_offset=True) #bones.add_copy_rotation_constraint(rigify_rig, rigify_rig, "MCH-CTRL-eye.L", "MCH-eye.L", space="LOCAL_OWNER_ORIENT", use_offset=True) #bones.add_copy_location_constraint(rigify_rig, rigify_rig, "MCH-CTRL-eye.R", "MCH-eye.R", space="LOCAL_OWNER_ORIENT", use_offset=True) #bones.add_copy_rotation_constraint(rigify_rig, rigify_rig, "MCH-CTRL-eye.R", "MCH-eye.R", space="LOCAL_OWNER_ORIENT", use_offset=True) objects = chr_cache.get_cache_objects() slider_controls, curve_slider_controls, rect_controls = get_generated_controls(chr_cache, rigify_rig) shape_key_driver_defs, bone_driver_defs, offset_driver_defs = \ collect_driver_defs(chr_cache, rigify_rig, slider_controls, curve_slider_controls, rect_controls) # build shape key drivers from shape key driver defs for shape_key_name, shape_key_driver_def in shape_key_driver_defs.items(): real_shape_key_name = get_objects_shape_key_name(objects, shape_key_name) var_defs = [] vidx = 0 var_expression = "" offset_expression = "" num_key_controls = len(shape_key_driver_def) use_negative = False use_strength = False value = 1.0 influence = None for nub_bone_name, key_control_def in shape_key_driver_def.items(): if nub_bone_name in rigify_rig.pose.bones: is_curve = "start" in key_control_def if is_curve: start = key_control_def.get("start", 0.0) mid = key_control_def.get("mid", 0.5) end = key_control_def.get("end", 1.0) if end == mid: end = mid + mid - start range = (end - mid + mid - start) / 2 num_keys = key_control_def["num_keys"] var_axis = key_control_def["var_axis"] distance = key_control_def["distance"] use_strength = key_control_def["use_strength"] influence = key_control_def["influence"] if "use_negative" in key_control_def: use_negative = key_control_def.get("use_negative", False) value = key_control_def["value"] var_name = drivers.find_bone_transform_var_def(var_defs, rigify_rig, nub_bone_name, var_axis, "TRANSFORM_SPACE") if not var_name: var_name = f"V{vidx}" vidx += 1 var_def = drivers.make_bone_transform_var_def(var_name, rigify_rig, nub_bone_name, var_axis, "TRANSFORM_SPACE") var_defs.append(var_def) expr = f"{value}*{var_name}/{fvar(distance)}" if is_curve: ve = f"({expr})" expr = f"min(1,max(0,1-abs({ve}-{fvar(mid)})/{fvar(range)}))" if var_expression: var_expression += "+" #use_negative = use_negative and (num_keys == 1 or num_key_controls > 1) if use_negative: var_expression += f"({expr})" else: var_expression += f"max(0,{expr})" if shape_key_name in offset_driver_defs: offset_driver_def = offset_driver_defs[shape_key_name] #print("OFFSET:", shape_key_name, offset_driver_def) for (nub_bone_name, nub_value), offset_key_control_def in offset_driver_def.items(): #print("NUBS", nub_bone_name, nub_value, offset_key_control_def) if nub_bone_name in rigify_rig.pose.bones: offset_bone_name = offset_key_control_def["bone"] value = offset_key_control_def["value"] var_axis = offset_key_control_def["axis"] dir = offset_key_control_def["dir"] var_name = drivers.find_bone_transform_var_def(var_defs, rigify_rig, offset_bone_name, var_axis) if not var_name: var_name = f"V{vidx}" vidx += 1 var_def = drivers.make_bone_transform_var_def(var_name, rigify_rig, offset_bone_name, var_axis) var_defs.append(var_def) expr = f"{var_name}/{fvar(value)}" if offset_expression: offset_expression += "+" if use_negative: offset_expression += f"({expr})" else: offset_expression += f"max(0,{expr})" #print(offset_expression) if influence: influence_var_name = drivers.find_custom_prop_var_def(var_defs, facerig_bone, "eyes_track") if not influence_var_name: influence_var_name = "IV" var_def = drivers.make_custom_prop_var_def(influence_var_name, facerig_bone, "eyes_track") var_defs.append(var_def) if var_expression: var_expression = f"(1-{influence_var_name})*({var_expression})" if offset_expression: offset_expression = f"{influence_var_name}*({offset_expression})" if var_expression and offset_expression: var_expression = f"{var_expression}+{offset_expression}" if use_strength: var_expression = f"KS*({var_expression})" var_def = drivers.make_custom_prop_var_def("KS", facerig_bone, "key_strength") var_defs.append(var_def) shape_key_range = 1.0 high = shape_key_range low = -shape_key_range if use_negative else 0 expression = f"max({fvar(low)},min({fvar(high)},{var_expression}))" driver_def = ["SCRIPTED", expression] for obj in objects: if utils.object_has_shape_keys(obj): drivers.add_shape_key_driver(rigify_rig, obj, real_shape_key_name, driver_def, var_defs, 1.0) # build bone transform drivers from bone driver defs for driver_id, bone_driver_def in bone_driver_defs.items(): bone_name, prop, index = driver_id #if bone_name == "MCH-CTRL-eye.L": # print("MCH-CTRL-eye.L") # print(driver_id, bone_driver_def) var_defs = [] vidx = 0 var_expression = "" use_strength = False influence = None for (nub_bone_name, nub_value, var_axis_id), bone_control_def in bone_driver_def.items(): offset = bone_control_def["offset"] scalar = bone_control_def["scalar"] var_axis = bone_control_def["var_axis"] distance = bone_control_def["distance"] dir_range = bone_control_def["range"] use_strength = bone_control_def["use_strength"] use_negative = bone_control_def.get("use_negative", False) influence = bone_control_def["influence"] var_name = drivers.find_bone_transform_var_def(var_defs, rigify_rig, nub_bone_name, var_axis, "TRANSFORM_SPACE") if not var_name: var_name = f"V{vidx}" vidx += 1 var_def = drivers.make_bone_transform_var_def(var_name, rigify_rig, nub_bone_name, var_axis, "TRANSFORM_SPACE") var_defs.append(var_def) if var_expression: var_expression += "+" if use_negative: var_expression += f"({var_name}*{fvar(scalar/distance)})" elif nub_value >= 0: var_expression += f"(max(0,{var_name})*{fvar(scalar/distance)})" else: var_expression += f"(min(0,{var_name})*{fvar(scalar/distance)})" if influence: influence_var_name = drivers.find_custom_prop_var_def(var_defs, facerig_bone, "eyes_track") if not influence_var_name: influence_var_name = "IV" var_def = drivers.make_custom_prop_var_def(influence_var_name, facerig_bone, "eyes_track") var_defs.append(var_def) if var_expression: var_expression = f"(1-{influence_var_name})*({var_expression})" expression = var_expression if use_strength: expression = f"BS*({var_expression})" var_def = drivers.make_custom_prop_var_def("BS", facerig_bone, "bone_strength") var_defs.append(var_def) else: expression = var_expression driver_def = ["SCRIPTED", prop, index, expression] drivers.add_bone_driver(rigify_rig, bone_name, driver_def, var_defs, 1.0) # constraint drivers facial_profile, viseme_profile = chr_cache.get_facial_profile() if facial_profile == "MH": build_expression_constraint_drivers(chr_cache, rigify_rig) def build_facerig_retarget_drivers(chr_cache, rigify_rig, source_rig, source_objects, shape_key_only=False, arkit=False): ctrl_drivers = {} FACERIG_CONFIG = get_facerig_config(chr_cache) if rigutils.select_rig(rigify_rig): facial_profile, viseme_profile = chr_cache.get_facial_profile() if facial_profile == "MH": # ensure curve retarget function is in driver namespace if ("rl_curve_retarget" not in bpy.app.driver_namespace or bpy.app.driver_namespace["rl_curve_retarget"] != func_rl_curve_slider_retarget): bpy.app.driver_namespace["rl_curve_retarget"] = func_rl_curve_slider_retarget for control_name, control_def in FACERIG_CONFIG.items(): bones_def = get_expression_bones_def(chr_cache, control_name, control_def) if control_def["widget_type"] == "rect": prefixes = [ ("x_", "x", "horizontal", "_box", 0), ("y_", "y", "vertical", "_box", 1) ] else: prefixes = [ ("", "", "", "_line", 1) ] for prefix, key_group, bone_group, line_suffix, index in prefixes: method = control_def.get(f"{prefix}method", "SUM") parent = control_def.get(f"{prefix}parent", "NONE") control_range = control_def.get(f"{prefix}range") control_scale = 1 / abs(control_range[1] - control_range[0]) invert = control_range[1] < control_range[0] mirror = control_def.get(f"{prefix}mirror", False) blend_shapes = control_def.get("blendshapes") if blend_shapes and key_group: blend_shapes = blend_shapes[key_group] bones_def = get_expression_bones_def(chr_cache, control_name, control_def) if bones_def and bone_group: bones_def = bones_def[bone_group] has_bones = bones_def and len(bones_def) > 0 line_bone_name = control_name + line_suffix line_bone = bones.get_pose_bone(rigify_rig, line_bone_name) if not line_bone: continue slider_length = line_bone[f"{prefix}slider_length"] if f"{prefix}slider_length" in line_bone else line_bone.bone.length * 2 #inv = -1 if control_def.get(f"{prefix}invert") else 1 #inv = 1 #if key_group == "y": # inv *= -1 slider_length *= control_scale driver_id = (control_name, "location", index) # only retarget from the bones if there are no blendshapes to use as a source retarget_bones = not blend_shapes and source_rig and has_bones if blend_shapes: for blend_shape in blend_shapes: if (blend_shape.startswith(("Dummy_")) or blend_shape.startswith(("Teeth_"))): retarget_bones = source_rig and has_bones if shape_key_only: retarget_bones = False if retarget_bones: ctrl_drivers[driver_id] = { "method": method, "parent": parent, "length": slider_length, "bones": [] } for bone_def in bones_def: source_name = bone_def["Source Bone"] if "retarget_bones" in control_def: if source_name not in control_def["retarget_bones"]: continue axis_dir = bone_def["value"] if source_name in source_rig.pose.bones: source_tra = utils.array_to_vector(bone_def["Source Translate"]) if "Source Translate" in bone_def else Vector((0,0,0)) source_euler = bone_def["Source Rotation"] if "Source Rotation" in bone_def else [0,0,0] axes = ["x", "y", "z"] if source_tra.length > 0.001: index = utils.largest_index(source_tra, use_abs=True) axis = axes[index] prop, prop_axis, prop_index = facerig_data.LOC_AXES.get(axis, (None, None, None)) scalar = source_tra[index] scale = slider_length ctrl_drivers[driver_id]["bones"].append({ "bone": source_name, "dir": axis_dir, "value": scalar, "scale": scale, "axis": prop_axis }) elif abs(source_euler[0]) + abs(source_euler[1]) + abs(source_euler[2]) > 0.01: index = utils.largest_index(source_euler, use_abs=True) axis = axes[index] prop, prop_axis, prop_index = facerig_data.ROT_AXES.get(axis, (None, None, None)) scalar = source_euler[index] scale = slider_length ctrl_drivers[driver_id]["bones"].append({ "bone": source_name, "dir": axis_dir, "value": scalar, "scale": scale, "axis": prop_axis }) elif blend_shapes: is_curve = control_def["widget_type"] == "curve_slider" ctrl_drivers[driver_id] = { "method": method, "parent": parent, "length": slider_length, "is_curve": is_curve, "shape_keys": [] } for i, (blend_shape_name, blend_shape_value) in enumerate(blend_shapes.items()): # if 'retarget' list exists in control def, only retarget blendshapes # in the list, to avoid uncontrolled duplicate retargets if "retarget" in control_def: if blend_shape_name not in control_def["retarget"]: continue if is_curve: curve = control_def["curve"][i] start = curve[0] mid = curve[1] end = curve[-1] if end == mid: end = mid + mid - start range = (end - mid + mid - start) / 2 else: mid = 0.5 range = 1 # if retargeting from an ARKit proxy, remap the shapes and only target these shapes if arkit: arkit_blend_shape_name = arkit_find_target_blend_shape(facial_profile, blend_shape_name) if arkit_blend_shape_name: blend_shape_name = arkit_blend_shape_name else: continue ctrl_drivers[driver_id]["shape_keys"].append({ "shape_key": blend_shape_name, "value": blend_shape_value, "scale": slider_length, "mid": mid, "range": range }) for driver_id, driver_def in ctrl_drivers.items(): bone_name, prop, index = driver_id parent = driver_def["parent"] if parent != "NONE": parent_id = (parent, prop, index) parent_def = ctrl_drivers[parent_id] expression, var_defs = build_retarget_driver(chr_cache, rigify_rig, parent_id, parent_def, source_rig, source_objects, no_driver=True, length_override=abs(driver_def["length"]), arkit=arkit) #print(bone_name, driver_id, parent_id, expression) #print("DRIVER:",driver_id, driver_def) #print("PARENT:",parent_id, parent_def) build_retarget_driver(chr_cache, rigify_rig, driver_id, driver_def, source_rig, source_objects, pre_expression=expression, pre_var_defs=var_defs, arkit=arkit) else: build_retarget_driver(chr_cache, rigify_rig, driver_id, driver_def, source_rig, source_objects, arkit=arkit) update_facerig_color(None, chr_cache=chr_cache) def build_retarget_driver(chr_cache, rigify_rig, driver_id, driver_def, source_rig, source_objects, no_driver=False, pre_expression=None, pre_var_defs=None, length_override=None, arkit=False): bone_name, prop, index = driver_id method = driver_def["method"] parent = driver_def["parent"] length = abs(driver_def["length"]) scale_override = 1 if length_override is not None: length_override = abs(length_override) scale_override = length_override / length length = length_override pose_bone = bones.get_pose_bone(rigify_rig, bone_name) expression = "" var_defs = [] prop_defs = [] is_curve = driver_def.get("is_curve", False) vidx = 0 if not pre_var_defs else len(pre_var_defs) if "shape_keys" in driver_def: count = 0 shape_key_defs = driver_def["shape_keys"] scale = 1 for key_def in shape_key_defs: scale = key_def["scale"] value = key_def["value"] mid = key_def["mid"] range = key_def["range"] if length_override: scale *= scale_override shape_key_name = key_def["shape_key"] real_shape_key_name = get_objects_shape_key_name(source_objects, shape_key_name) if real_shape_key_name: var_name = f"V{vidx}" var_expr = f"min(1,{var_name})" if count > 0: expression += "," if is_curve else "+" if is_curve: var_expression = f"({var_expr}/{fvar(value)},{fvar(mid)},{fvar(range)})" else: var_expression = f"({var_expr}*{fvar(scale/value)})" if arkit: var_expression = add_arkit_driver_func(chr_cache, var_expression, length, shape_key_name, prop_defs) expression += var_expression var_defs.append((var_name, real_shape_key_name)) vidx += 1 count += 1 shape_key_range = length low = -shape_key_range high = shape_key_range if bone_name == "CTRL_C_eye" and method == "AVERAGE" and count == 4: count = 2 if expression: if is_curve: expression = f"rl_curve_retarget([{expression}])*{fvar(scale)}" else: expression = f"({expression})" if expression and method == "AVERAGE" and count > 1: expression = f"min({fvar(high)},max({fvar(low)},{expression}/{count}))" if expression and parent != "NONE" and pre_expression and pre_var_defs: expression = f"{expression} - {pre_expression}" var_defs.extend(pre_var_defs) if expression and not no_driver: driver = drivers.make_driver(pose_bone, prop, "SCRIPTED", driver_expression=expression, index=index) if driver: for var_name, shape_key_name in var_defs: var = None key = None for obj in source_objects: key = drivers.get_shape_key(obj, shape_key_name) if key: break if not key: utils.log_warn(f"Unable to find source for shape key: {shape_key_name}") var = drivers.make_dummy_var(driver, var_name) else: # target_type="MESH", data_path="shape_keys.key_blocks[\"{shape_key}\"].value" data_path = "shape_keys." + key.path_from_id("value") var = drivers.make_driver_var(driver, "SINGLE_PROP", var_name, obj.data, target_type="MESH", data_path=data_path) for var_name, prop_obj, prop_name in prop_defs: # target_type="MESH", data_path="shape_keys.key_blocks[\"{shape_key}\"].value" data_path = f"[\"{prop_name}\"]" var = drivers.make_driver_var(driver, "SINGLE_PROP", var_name, prop_obj, target_type="OBJECT", data_path=data_path) if source_rig and "bones" in driver_def: count = 0 bone_defs = driver_def["bones"] for bone_def in bone_defs: scale = bone_def["scale"] value = bone_def["value"] axis = bone_def["axis"] axis_dir = bone_def["dir"] source_name = bone_def["bone"] if source_name in source_rig.pose.bones: var_name = f"V{vidx}" if count > 0: expression += "+" if axis_dir == 0: expression += f"({var_name}*{fvar(axis_dir*scale/value)})" elif axis_dir > 0: expression += f"max(0,{var_name}*{fvar(axis_dir*scale/value)})" else: expression += f"min(0,{var_name}*{fvar(axis_dir*scale/value)})" var_defs.append((var_name, source_name, axis)) vidx += 1 count += 1 control_range = length low = -control_range high = control_range #if expression: # expression = f"min({fvar(high)},max({fvar(low)},{expression}))" if expression and method == "AVERAGE" and count > 1: expression = f"({expression}/{count})" if expression and parent != "NONE" and pre_expression and pre_var_defs: expression = f"{expression} - {pre_expression}" var_defs.extend(pre_var_defs) if expression and not no_driver: bones.set_bone_color(rigify_rig, pose_bone, "DRIVER", "DRIVER", "DRIVER", chr_cache=chr_cache) driver = drivers.make_driver(pose_bone, prop, "SCRIPTED", driver_expression=expression, index=index) if driver: for var_name, source_name, axis in var_defs: var = drivers.make_driver_var(driver, "TRANSFORMS", var_name, source_rig, bone_target=source_name, transform_type=axis, transform_space="LOCAL_SPACE") return expression, var_defs def remove_facerig_retarget_drivers(chr_cache, rigify_rig: bpy.types.Object): if rigutils.select_rig(rigify_rig): FACERIG_CONFIG = get_facerig_config(chr_cache) for control_name, control_def in FACERIG_CONFIG.items(): if control_name in rigify_rig.pose.bones: pose_bone = rigify_rig.pose.bones[control_name] pose_bone.driver_remove("location", 0) pose_bone.driver_remove("location", 1) update_facerig_color(None, chr_cache=chr_cache) EYE_TRACK_STORE = {} def set_facerig_eye_tracking(rigify_rig, enable=True): face_rig = bones.get_pose_bone(rigify_rig, "facerig") if face_rig: face_rig["eyes_track"] = 1 if enable else 0 def func_rl_curve_slider_retarget(args): """args = [ (v0, m0, r0), (v1, m1, r1), ... ]""" T = 0.0001 result = 0 max_v = 0 min_v = 1 L = len(args) - 1 for arg in args: v = arg[0] if v > max_v: max_v = v if v < min_v: min_v = v if max_v < T: result = 0 elif args[L][0] > 1 - T: result = 1 else: for i, arg in enumerate(args): v, m, r = arg if v == max_v: if i == 0: v1 = args[i+1][0] if v1 < T: result = m - r * (1 - v) else: result = m + r * (1 - v) break elif i == L: result = m - r * (1 - v) break else: v0 = args[i-1][0] v1 = args[i+1][0] if v0 > v1: result = m - r * (1 - v) else: result = m + r * (1 - v) break return min(1, max(0, result)) def get_expression_shape_key_source_object(objects, head, shape_key_name): if utils.object_exists_is_mesh(head) and utils.object_has_shape_key(head, shape_key_name): return head for obj in objects: if utils.object_exists_is_mesh(obj) and utils.object_has_shape_key(obj, shape_key_name): return obj return None def get_expression_constraint_var_expression(var_name, points): var_expression = "" if len(points) == 2: x0 = points[0][0] # should be 0.0 x1 = points[1][0] # should be 0.0 y0 = points[0][1] # should be 1.0 y1 = points[1][1] # should be 1.0 dy = y1 - y0 dx = x1 - x0 if dx == 0: dx = 1 dybydx = dy / dx # y0 + (V - x0) * dy/dx var_expression = f"max(0,min(1,{fvar(y0)}+{fvar(dybydx)}*({var_name}-{fvar(x0)})))" elif len(points) == 3: xs = points[0][0] # should be 0.0 ys = points[0][1] # should be 0.0 xm = points[1][0] # should be 0.5 ym = points[1][1] # should be 1.0 xe = points[2][0] # should be 1.0 ye = points[2][1] # should be 0.0 dx = ((xe - xm) + (xm - xs)) / 2 # should be 0.5 dy = ((ye - ym) + (ys - ym)) / 2 # should be -1 if dx == 0: dx = 1 dybydx = dy / dx # ym + abs(V - xm) * dy/dx var_expression = f"max(0,min(1,{fvar(ym)}+{fvar(dybydx)}*abs({var_name}-{fvar(xm)})))" elif len(points) == 5: xs = points[1][0] # should be 0.25 ys = points[1][1] # should be 0.0 xm = points[2][0] # should be 0.5 ym = points[2][1] # should be 1.0 xe = points[3][0] # should be 0.75 ye = points[3][1] # should be 0.0 dx = ((xe - xm) + (xm - xs)) / 2 # should be 0.25 dy = ((ye - ym) + (ys - ym)) / 2 # should be -1 if dx == 0: dx = 1 dybydx = dy / dx var_expression = f"max(0,min(1,{fvar(ym)}+{fvar(dybydx)}*abs({var_name}-{fvar(xm)})))" return var_expression def build_expression_constraint_add_driver(chr_cache, rigify_rig, objects, head, source_keys, target_key, curve_mode, points): var_defs = [] vidx = 0 expression = "" for key in source_keys: source_obj = get_expression_shape_key_source_object(objects, head, key) var_name = f"V{vidx}" vidx += 1 var_def = drivers.make_shape_key_var_def(var_name, source_obj, key) var_defs.append(var_def) var_expression = get_expression_constraint_var_expression(var_name, points) if expression: expression += "*" expression += var_expression driver_def = ["SCRIPTED", expression] for obj in objects: if utils.object_has_shape_key(obj, target_key): drivers.add_shape_key_driver(rigify_rig, obj, target_key, driver_def, var_defs, 1.0) def apply_control_limit_constraint_drivers(rigify_rig, control_name, control_def, target_key, expression, var_defs, line_suffix="_line", key_group="", axis="y"): nub_bone_name = control_name nub_bone = bones.get_pose_bone(rigify_rig, nub_bone_name) line_bone_name = control_name + line_suffix line_bone = bones.get_pose_bone(rigify_rig, line_bone_name) if nub_bone and line_bone: if "blendshapes" in control_def: if key_group: prefix = f"{key_group}_" shapes = control_def["blendshapes"][key_group] else: prefix = "" shapes = control_def["blendshapes"] for i, (key_name, key_value) in enumerate(shapes.items()): if key_name == target_key: slider_length = line_bone[f"{prefix}slider_length"] if f"{prefix}slider_length" in line_bone else line_bone.bone.length * 2 control_range = control_def.get(f"{prefix}range") invert = control_range[1] < control_range[0] mirror = control_def.get(f"{prefix}mirror", False) if invert: key_value = -key_value if mirror: key_value = -key_value limit_value = slider_length * key_value if key_value < 0: prop = f"min_{axis}" else: prop = f"max_{axis}" limit_expression = f"{fvar(limit_value)}*{expression}" driver_def = ["SCRIPTED", prop, -1, limit_expression] drivers.add_constraint_prop_driver(rigify_rig, nub_bone_name, driver_def, var_defs, constraint_type="LIMIT_LOCATION") def build_expression_constraint_limit_driver(chr_cache, rigify_rig, objects, head, source_keys, target_key, curve_mode, points): prefs = vars.prefs() var_defs = [] vidx = 0 limit_expression = "" for key in source_keys: source_obj = get_expression_shape_key_source_object(objects, head, key) var_name = f"lv{vidx}" vidx += 1 var_def = drivers.make_shape_key_var_def(var_name, source_obj, key) var_defs.append(var_def) var_expression = get_expression_constraint_var_expression(var_name, points) if limit_expression: limit_expression += "*" limit_expression += var_expression # limit facerig control movement ranges if prefs.rigify_limit_control_range: FACERIG_CONFIG = get_facerig_config(chr_cache) for control_name, control_def in FACERIG_CONFIG.items(): if control_def["widget_type"] == "slider": apply_control_limit_constraint_drivers(rigify_rig, control_name, control_def, target_key, limit_expression, var_defs, "_line", "", "y") elif control_def["widget_type"] == "rect": apply_control_limit_constraint_drivers(rigify_rig, control_name, control_def, target_key, limit_expression, var_defs, "_box", "x", "x") apply_control_limit_constraint_drivers(rigify_rig, control_name, control_def, target_key, limit_expression, var_defs, "_box", "y", "y") # apply a min(expression, limit_expression) to existing shape key value driver # (driving the shape key max value has no effect) for obj in objects: if utils.object_has_shape_key(obj, target_key): driver: bpy.types.Driver = drivers.get_shape_key_driver(obj, target_key) if driver and driver.expression: driver_expression = driver.expression drivers.add_driver_var_defs(driver, var_defs) new_expression = f"min({driver_expression}, {limit_expression})" driver.expression = new_expression else: utils.log_warn(f"NO DRIVER: {obj.name} {target_key}") def build_expression_constraint_drivers(chr_cache, rigify_rig): objects = chr_cache.get_cache_objects() head = drivers.get_head_body_object(chr_cache) constraint_json = chr_cache.get_constraint_json() if constraint_json: for constraint_name, constraint_def in constraint_json.items(): source_keys = constraint_def["Source Channels"] target_key = constraint_def["Target Channel"] curve_mode = constraint_def["Curve Mode"] mode = constraint_def["Mode"] points = [] for point in constraint_def["Curve"]: co = (min(1, max(0, point[0])), (min(1, max(0, point[1])))) if co not in points: points.append(co) # sort by x value points.sort(key=lambda co: co[0]) if mode == "Add": build_expression_constraint_add_driver(chr_cache, rigify_rig, objects, head, source_keys, target_key, curve_mode, points) elif mode == "Limit": build_expression_constraint_limit_driver(chr_cache, rigify_rig, objects, head, source_keys, target_key, curve_mode, points) def clear_expression_pose(chr_cache, rigify_rig, selected=False): FACERIG_CONFIG = get_facerig_config(chr_cache) if selected: selected_names = [] if bpy.context.selected_bones: selected_names = [ b.name for b in bpy.context.selected_bones ] elif bpy.context.selected_pose_bones: selected_names = [ b.name for b in bpy.context.selected_pose_bones ] control_bones = [] for control_bone_name in FACERIG_CONFIG: if control_bone_name in rigify_rig.pose.bones and control_bone_name in selected_names: control_bones.append(control_bone_name) else: control_bones = [ "MCH-jaw_move", "jaw_master", "MCH-jaw_master" ] for control_bone_name in FACERIG_CONFIG: if control_bone_name in rigify_rig.pose.bones: control_bones.append(control_bone_name) state = bones.store_armature_settings(rigify_rig, include_selection=True) bones.clear_pose(rigify_rig, control_bones) bones.restore_armature_settings(rigify_rig, state, include_selection=True) def control_bone_has_driver(rigify_rig, control_bone_name): try: search = f"[\"{control_bone_name}\"]" for driver in rigify_rig.animation_data.drivers: data_path = driver.data_path if data_path.endswith("location"): if search in data_path: return True except: ... return False def update_facerig_color(context, chr_cache=None): if not chr_cache: chr_cache, obj, mat, obj_cache, mat_cache = utils.get_context_character(context) if chr_cache: FACERIG_CONFIG = get_facerig_config(chr_cache) rigify_rig = chr_cache.get_armature() if rigify_rig and "facerig" in rigify_rig.pose.bones: if utils.B410(): for control_bone_name, control_def in FACERIG_CONFIG.items(): color_shift = control_def.get("color_shift", 0.0) if control_bone_has_driver(rigify_rig, control_bone_name): color_code = "DRIVER" else: color_code = "FACERIG" bones.set_bone_color(rigify_rig, control_bone_name, color_code, color_code, color_code, chr_cache=chr_cache, hue_shift=color_shift) if control_def["widget_type"] == "rect": lines_bone_name = control_bone_name + "_box" else: lines_bone_name = control_bone_name + "_line" bones.set_bone_color(rigify_rig, lines_bone_name, "FACERIG_DARK", "FACERIG_DARK", "FACERIG_DARK", chr_cache=chr_cache, hue_shift=color_shift) else: props_color = chr_cache.rigify_face_control_color custom_color = (props_color[0], props_color[1], props_color[2]) bone_group = rigify_rig.pose.bone_groups["Face"] bone_group.colors.normal = utils.linear_to_srgb(custom_color) def is_position_locked(rig): if "facerig" in rig.pose.bones: return rig.pose.bones["facerig"].bone.hide_select def toggle_lock_position(chr_cache, rig): FACERIG_CONFIG = get_facerig_config(chr_cache) bone_names = [ "facerig", "facerig_groups", "facerig_labels", "facerig_name", "facerig2", "facerig2_groups", "facerig2_labels", ] bone_selectable = [ True, False, False, False, True, False, False ] is_locked = is_position_locked(rig) for i, bone_name in enumerate(bone_names): if bone_name in rig.pose.bones: pose_bone = rig.pose.bones[bone_name] if bone_selectable[i]: pose_bone.bone.hide_select = not is_locked else: pose_bone.bone.hide_select = True # make sure the controls selection properties are correct for control_name in FACERIG_CONFIG: if control_name in rig.pose.bones: pose_bone = rig.pose.bones[control_name] pose_bone.bone.hide_select = False if control_name+"_line" in rig.pose.bones: pose_bone = rig.pose.bones[control_name+"_line"] pose_bone.bone.hide_select = True if control_name+"_box" in rig.pose.bones: pose_bone = rig.pose.bones[control_name+"_box"] pose_bone.bone.hide_select = True def build_arkit_bone_constraints(chr_cache, rigify_rig, proxy_rig): con1 = bones.add_copy_rotation_constraint(proxy_rig, rigify_rig, "head", "head", space="LOCAL_WITH_PARENT") con2 = bones.add_copy_rotation_constraint(proxy_rig, rigify_rig, "head", "neck", 0.25, space="LOCAL_WITH_PARENT") con3 = bones.add_copy_rotation_constraint(proxy_rig, rigify_rig, "offset", "head", use_offset=True, space="LOCAL_WITH_PARENT") con4 = bones.add_copy_rotation_constraint(proxy_rig, rigify_rig, "offset", "neck", 0.25, use_offset=True, space="LOCAL_WITH_PARENT") con1.name = con1.name + "_ARKit_Proxy" con2.name = con2.name + "_ARKit_Proxy" con3.name = con1.name + "_ARKit_Proxy" con4.name = con2.name + "_ARKit_Proxy" data_path = proxy_rig.path_from_id("[\"head_blend\"]") bones.add_constraint_influence_driver(rigify_rig, "head", proxy_rig, data_path, "var_head_blend", con1, expression="var_head_blend*0.01") bones.add_constraint_influence_driver(rigify_rig, "neck", proxy_rig, data_path, "var_head_blend", con2, expression="var_head_blend*0.0025") bones.add_constraint_influence_driver(rigify_rig, "head", proxy_rig, data_path, "var_head_blend", con3, expression="var_head_blend*0.01") bones.add_constraint_influence_driver(rigify_rig, "head", proxy_rig, data_path, "var_head_blend", con4, expression="var_head_blend*0.0025") offset_bone = proxy_rig.pose.bones["offset"] offset_bone.rotation_mode = "XYZ" bones.add_bone_custom_props_driver(proxy_rig, "offset", "rotation_euler", 0, proxy_rig, "[\"head_pitch_offset\"]", "P", "-P") bones.add_bone_custom_props_driver(proxy_rig, "offset", "rotation_euler", 2, proxy_rig, "[\"head_roll_offset\"]", "R", "-R") bones.add_bone_custom_props_driver(proxy_rig, "offset", "rotation_euler", 1, proxy_rig, "[\"head_yaw_offset\"]", "Y", "Y") def remove_arkit_bone_constraints(chr_cache, rigify_rig): head_bone = bones.get_pose_bone(rigify_rig, "head") neck_bone = bones.get_pose_bone(rigify_rig, "neck") all_bones = [] if head_bone: all_bones.append(head_bone) if neck_bone: all_bones.append(neck_bone) for bone in all_bones: remove = [] for con in bone.constraints: if utils.strip_name(con.name).endswith("_ARKit_Proxy"): remove.append(con) for con in remove: bone.constraints.remove(con) def generate_arkit_proxy(chr_cache): if chr_cache and chr_cache.rigified: remove_arkit_proxy(chr_cache) rigify_rig = chr_cache.get_armature() facial_profile, viseme_profile = chr_cache.get_facial_profile() if rigify_rig and facial_profile in facerig_data.ARKIT_SHAPE_KEY_TARGETS: neck_bone = bones.get_pose_bone(rigify_rig, "neck") root_bone = bones.get_pose_bone(rigify_rig, "root") M = rigify_rig.matrix_world @ neck_bone.matrix loc = M @ Vector((-0.4, -0.05, -0.05)) rot = (rigify_rig.matrix_world @ root_bone.matrix).to_quaternion() chr_collections = utils.get_object_scene_collections(rigify_rig) objects = lib.get_object(["RL_ARKit_Proxy", "RL_ARKit_Proxy_Head"]) rig_name = f"{chr_cache.character_name}_ARKit_Proxy" mesh_name = f"{chr_cache.character_name}_ARKit_Proxy_Head" proxy_rig = None proxy_mesh = None for obj in objects: utils.move_object_to_scene_collections(obj, chr_collections) if obj.type == "ARMATURE": obj.name = rig_name obj.data.name = rig_name proxy_rig = obj elif obj.type == "MESH": obj.name = mesh_name obj.data.name = mesh_name proxy_mesh = obj obj["arkit_proxy"] = "fDsOJtp42n68X0e4ETVP" if proxy_rig and proxy_mesh: proxy_rig.location = loc utils.set_transform_rotation(proxy_rig, rot) chr_cache.arkit_proxy = proxy_rig build_arkit_proxy_drivers(chr_cache, rigify_rig, proxy_rig, proxy_mesh) wgt_collection = rigutils.get_widget_rig_collection(chr_cache) wgt_root = bones.make_root_widget(f"WGT-{chr_cache.character_name}_rig_arkit_proxy_root", 3.25) if wgt_collection: utils.remove_from_scene_collections(wgt_root) bones.add_widget_to_collection(wgt_root, wgt_collection) proxy_root_bone: bpy.types.PoseBone = proxy_rig.pose.bones["root"] proxy_root_bone.custom_shape = wgt_root bones.set_bone_color(proxy_rig, proxy_root_bone, "ROOT") return proxy_rig return None def add_arkit_driver_func(chr_cache, expression, length, shape_key_name, prop_defs: list): # dont adjust for these arkit blend shapes shape_key_name = shape_key_name.lower() exclude = ["eyelook", "eyewide", "eyeblink", "mouthclose", "jaw", "eyeroll", "eyepitch", "eyeyaw"] for pattern in exclude: if pattern in shape_key_name: return expression # ensure arkit function is in driver namespace if ("rl_arkit" not in bpy.app.driver_namespace or bpy.app.driver_namespace["rl_arkit"] != func_rl_arkit_proxy_mod): bpy.app.driver_namespace["rl_arkit"] = func_rl_arkit_proxy_mod # determine directional bias if "left" in shape_key_name: horz_bias = "1+H" horz_var = "horizontal_bias" elif "right" in shape_key_name: horz_bias = "1-H" horz_var = "horizontal_bias" else: horz_bias = "1" horz_var = None if "up" in shape_key_name or "upper" in shape_key_name: vert_bias = "1-V" vert_var = "vertical_bias" elif "down" in shape_key_name or "lower" in shape_key_name: vert_bias = "1+V" vert_var = "vertical_bias" else: vert_bias = "1" vert_var = None # extent expression with arkit adjustments proxy_rig, proxy_mesh = get_arkit_proxy(chr_cache) if proxy_rig: expression = f"rl_arkit({expression},{fvar(length)},S,{horz_bias},{vert_bias},R)" if ("S", proxy_rig, "strength") not in prop_defs: prop_defs.append(("S", proxy_rig, "strength")) if ("R", proxy_rig, "relaxation") not in prop_defs: prop_defs.append(("R", proxy_rig, "relaxation")) if horz_var and ("H", proxy_rig, horz_var) not in prop_defs: prop_defs.append(("H", proxy_rig, horz_var)) if vert_var and ("V", proxy_rig, vert_var) not in prop_defs: prop_defs.append(("V", proxy_rig, vert_var)) return expression def func_rl_arkit_proxy_mod(value, length, strength, horz_bias, vert_bias, relaxation): length = abs(length) if relaxation != 1.0: vN = value / length if vN < 0: vN = -pow(min(1,max(0,-vN)), relaxation) else: vN = pow(min(1,max(0,vN)), relaxation) value = vN * length # multiply the value by the adjustments value = (value * strength * horz_bias * vert_bias / 100.0) return max(-1, min(1, value)) def build_arkit_proxy_drivers(chr_cache, rigify_rig, proxy_rig, proxy_mesh): if chr_cache and rigify_rig and proxy_rig and proxy_mesh: build_facerig_retarget_drivers(chr_cache, rigify_rig, proxy_rig, [ proxy_mesh ], shape_key_only=True, arkit=True) drivers.add_custom_float_property(proxy_rig, "strength", 100.0, 0.0, 200.0, subtype="PERCENTAGE", precision=1, description="Overall strength of expressions") drivers.add_custom_float_property(proxy_rig, "relaxation", 1.0, 0.25, 2.0, description="How much to relax or exaggerate the expressions") drivers.add_custom_float_property(proxy_rig, "horizontal_bias", 0.0, -0.75, 0.75, description="How much to relax or exaggerate the expressions") drivers.add_custom_float_property(proxy_rig, "vertical_bias", 0.0, -0.75, 0.75, description="How much to relax or exaggerate the expressions") drivers.add_custom_float_property(proxy_rig, "random_variance", 0.0, 0.0, 80.0, subtype="PERCENTAGE", precision=1, description="How much to relax or exaggerate the expressions") drivers.add_custom_int_property(proxy_rig, "random_seed", 1000, 0, 99999999, description="Random seed for variance") drivers.add_custom_float_property(proxy_rig, "filter", 0.0, 0.0, 80.0, subtype="PERCENTAGE", precision=1, description="Low pass filter to reduce noise in expression data") drivers.add_custom_string_property(proxy_rig, "csv_file", "", description="path to the csv file to import") drivers.add_custom_string_property(proxy_rig, "bake_motion_id", "ARKit_Bake", description="Motion Name for baked action") drivers.add_custom_string_property(proxy_rig, "bake_motion_prefix", "", description="Motion prefix for baked action") drivers.add_custom_float_property(proxy_rig, "head_blend", 100.0, 0.0, 100.0, subtype="PERCENTAGE", precision=1, description="How much of the head movement to blend into the rig") drivers.add_custom_float_property(proxy_rig, "head_yaw_offset", 0.0, -60.0*math.pi/180, 60.0*math.pi/180, subtype="ANGLE", description="Head rotation Yaw adjust") drivers.add_custom_float_property(proxy_rig, "head_pitch_offset", 0.0, -60.0*math.pi/180, 60.0*math.pi/180, subtype="ANGLE", description="Head rotation Pitch adjust") drivers.add_custom_float_property(proxy_rig, "head_roll_offset", 0.0, -60.0*math.pi/180, 60.0*math.pi/180, subtype="ANGLE", description="Head rotation Roll adjust") build_arkit_bone_constraints(chr_cache, rigify_rig, proxy_rig) def get_arkit_proxy(chr_cache): if chr_cache and chr_cache.rigified and utils.object_exists_is_armature(chr_cache.arkit_proxy): proxy_rig = chr_cache.arkit_proxy for child in proxy_rig.children: if utils.prop(child, "arkit_proxy") == "fDsOJtp42n68X0e4ETVP": proxy_mesh = child return proxy_rig, proxy_mesh return None, None def remove_arkit_proxy(chr_cache): if chr_cache and chr_cache.rigified and chr_cache.arkit_proxy: rigify_rig = chr_cache.get_armature() if rigify_rig: remove_facerig_retarget_drivers(chr_cache, rigify_rig) remove_arkit_bone_constraints(chr_cache, rigify_rig) if utils.object_exists_is_armature(chr_cache.arkit_proxy): utils.delete_object_tree(chr_cache.arkit_proxy) chr_cache.arkit_proxy = None def arkit_find_target_blend_shape(facial_profile, blend_shape_name): if facial_profile in facerig_data.ARKIT_SHAPE_KEY_TARGETS: TARGETS = facerig_data.ARKIT_SHAPE_KEY_TARGETS[facial_profile] for arkit_blend_shape_name, targets in TARGETS.items(): if type(targets) is list: if blend_shape_name in targets: return arkit_blend_shape_name else: if targets == blend_shape_name: return arkit_blend_shape_name return None def decode_timecode(timecode: str, fps): split = timecode.split(":") h = int(split[0]) m = int(split[1]) s = int(split[2]) f = float(split[3]) return (int(h * 3600 + m * fps + s), int(f * 10000 / fps)) def timecode_to_frame(timecode: tuple, fps: int): s = timecode[0] f = timecode[1] * fps / 10000 return (s * fps) + f def load_csv(chr_cache, file_path): proxy_rig, proxy_mesh = get_arkit_proxy(chr_cache) if proxy_rig and proxy_mesh: tcurve: TCurve = None tcurves = parse_arkit_csv(file_path) process_tcurves(proxy_rig, tcurves) if tcurves: facial_profile, viseme_profile = chr_cache.get_facial_profile() if facial_profile in facerig_data.ARKIT_SHAPE_KEY_TARGETS: keys = facerig_data.ARKIT_SHAPE_KEY_TARGETS[facial_profile].keys() key_action = utils.make_action(f"{chr_cache.character_name}_ARKit_Proxy_Head", slot_type="KEY", clear=True, reuse=True) arm_action = utils.make_action(f"{chr_cache.character_name}_ARKit_Proxy", slot_type="OBJECT", clear=True, reuse=True) key_channels = utils.get_action_channels(key_action, slot_type="KEY") if key_channels: for key in keys: fcurve = key_channels.fcurves.new(f"key_blocks[\"{key}\"].value") for tcurve in tcurves: if tcurve.name.lower() == key.lower(): tcurve.to_fcurve(fcurve) break utils.safe_set_action(proxy_mesh.data.shape_keys, key_action) bone_channels = utils.get_action_channels(arm_action, slot_type="OBJECT") if bone_channels: for tcurve_name, bone_def in facerig_data.ARK_BONE_TARGETS.items(): for tcurve in tcurves: if tcurve.name.lower() == tcurve_name.lower(): bone_name = bone_def["bone"] bone = proxy_rig.pose.bones[bone_name] bone.rotation_mode = "XYZ" axis = bone_def["axis"] rotation = bone_def["rotation"] * math.pi / 180 prop, var, index = facerig_data.ROT_AXES[axis] data_path = bone.path_from_id(prop) fcurve = bone_channels.fcurves.new(data_path, index=index) tcurve.to_fcurve(fcurve, rotation) utils.safe_set_action(proxy_rig, arm_action) def get_arkit_proxy_prop(proxy_rig, prop): return proxy_rig[prop] def parse_arkit_csv(file_path): csv = [] maxf = 0 with open(file_path, "r") as file: file.seek(0) for line in file: cols = line.split(",") if not csv: for i, col in enumerate(cols): name = col.strip() data = [] column = { "index": i, "data": data, "name": name, "tcurve": None, } csv.append(column) else: for i, col in enumerate(cols): column = csv[i] data = column["data"] cell = col.strip() if i == 0: value = cell f = float(cell.split(":")[-1]) maxf = max(f, maxf) elif i == 1: value = int(cell) else: value = float(cell) data.append(value) csvfps = 60 if maxf >= 59 else 30 time_data = csv[0]["data"] tc_start = (0, 1) tc_end = (0, 1) for i, timestr in enumerate(time_data): tc = decode_timecode(time_data[i], csvfps) time_data[i] = tc if i == 0: tc_start = tc tc_end = tc tcurves = [] fps = bpy.context.scene.render.fps fps_base = bpy.context.scene.render.fps_base for i, column in enumerate(csv): if i > 1: tcurve = TCurve(csv, i, fps) tcurves.append(tcurve) #tcurve.dump() frame_start = timecode_to_frame(tc_start, fps) frame_end = timecode_to_frame(tc_end, fps) bpy.context.scene.frame_start = 1 bpy.context.scene.frame_end = int(frame_end - frame_start) + 1 return tcurves def process_tcurves(proxy_rig, tcurves): variance = get_arkit_proxy_prop(proxy_rig, "random_variance") / 100 seed = get_arkit_proxy_prop(proxy_rig, "random_seed") filter = get_arkit_proxy_prop(proxy_rig, "filter") / 100 random.seed(seed) tcurve: TCurve for tcurve in tcurves: tcurve.process(filter, variance) class TCurve(): name = "" points = None length = 0 frames = 0 def __init__(self, csv: list, column_index: int, fps: int): self.points = [] start_frame = 0 self.name = csv[column_index]["name"] for i, timecode in enumerate(csv[0]["data"]): if i == 0: start_frame = timecode_to_frame(timecode, fps) frame = timecode_to_frame(timecode, fps) - start_frame + 1 self.points.append((frame, csv[column_index]["data"][i])) self.frames = frame self.length = len(self.points) def eval(self, frame, start=0): for i in range(start, self.length): f, v = self.points[i] if frame <= f or frame >= self.frames: return v, i else: fn, vn = self.points[i + 1] if frame > f and frame < fn: res = v + (vn - v) * (frame - f) / (fn - f) return res, i #return utils.remap(f, fn, v, vn, frame) return 0.0, i def to_fcurve(self, fcurve: bpy.types.FCurve, mod=1.0): num_frames = int(self.frames) fcurve_data = [0] * (num_frames * 2) j = 0 for i in range(0, num_frames): fcurve_data[i * 2] = i + 1 v, j = self.eval(i + 0.5, j) fcurve_data[i * 2 + 1] = v * mod fcurve.keyframe_points.clear() fcurve.keyframe_points.add(num_frames) fcurve.keyframe_points.foreach_set('co', fcurve_data) def dump(self): utils.log_always(self.name) utils.log_always(self.length) for i, (f, v) in enumerate(self.points): utils.log_always(i, f, v) if i > 10: return def process(self, filter, variance): exclude = ["EyeLook", "Blink", "MouthClose", "Jaw", "EyeRoll", "EyePitch", "EyeYaw"] modify = True for e in exclude: if e in self.name: modify = False variance_mod = 1.0 if variance: variance_mod += random.random() * variance_mod * variance for i, (f, v) in enumerate(self.points): if i > 0: v = v0 * filter + v * (1 - filter) # TODO maybe scale the filter by the difference in f-f0 as the time stamps are uneven f0 = f v0 = v if modify: v = max(-1, min(1, (v * variance_mod))) self.points[i] = (f, v) class CCICImportARKitCSV(bpy.types.Operator): """Import ARKit LiveLink CSV""" bl_idname = "ccic.import_arkit_csv" bl_label = "Import ARKit LiveLink CSV" bl_options = {"REGISTER", "UNDO", 'PRESET'} filepath: bpy.props.StringProperty( name="Filepath", description="Filepath of the csv to import.", subtype="FILE_PATH" ) directory: bpy.props.StringProperty(subtype='DIR_PATH') files: bpy.props.CollectionProperty( type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'} ) filter_glob: bpy.props.StringProperty( default="*.csv", options={"HIDDEN"} ) param: bpy.props.StringProperty( name = "param", default = "", options={"HIDDEN"} ) def execute(self, context): props = vars.props() prefs = vars.prefs() chr_cache, obj, mat, obj_cache, mat_cache = utils.get_context_character(context) proxy_rig, proxy_mesh = get_arkit_proxy(chr_cache) if proxy_rig: if self.param == "RELOAD" and proxy_rig["csv_file"]: load_csv(chr_cache, proxy_rig["csv_file"]) elif self.files: list_file = self.files[0] dir = self.directory file = list_file.name proxy_rig["csv_file"] = os.path.join(dir, file) elif self.filepath: proxy_rig["csv_file"] = self.filepath else: proxy_rig["csv_file"] = "" if proxy_rig["csv_file"]: load_csv(chr_cache, proxy_rig["csv_file"]) return {"FINISHED"} def invoke(self, context, event): if self.param == "RELOAD": return self.execute(context) context.window_manager.fileselect_add(self) return {"RUNNING_MODAL"} @classmethod def description(cls, context, properties): return ""