# 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): 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_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 control_def["widget_type"] == "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 return None def build_facerig(chr_cache, rigify_rig, meta_rig, cc3_rig): prefs = vars.prefs() chr_cache.rigify_face_control_color = prefs.rigify_face_control_color 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) bone_scale = Vector((0.125, 0.125, 0.125)) R = Matrix.Rotation(90*math.pi/180, 3, 'X') slider_controls = {} rect_controls = {} facial_profile, viseme_profile = chr_cache.get_facial_profile() 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") eye_pos = (eye_l.head + eye_r.head) * 0.5 z_pos = eye_pos.z mch_parent = bones.copy_edit_bone(rigify_rig, "head", "MCH-facerig_parent", "head", 1.0) mch_parent.head.z = z_pos mch_parent.tail.z = z_pos + 0.1 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) # add MCH bone for aligned axis based jaw move jaw_move = bones.copy_edit_bone(rigify_rig, "jaw_master", "MCH-jaw_move", "ORG-face", 0.5) jaw_move.tail = jaw_move.head + Vector((0, jaw_move.length, 0)) # reparent jaw_master to jaw_move jaw_master = bones.get_edit_bone(rigify_rig, "jaw_master") jaw_master.parent = jaw_move FACERIG_CONFIG = get_facerig_config(chr_cache) for control_name, control_def in FACERIG_CONFIG.items(): count, total = is_valid_control_def(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": zero = utils.inverse_lerp(control_def["range"][0], control_def["range"][1], 0.0) indices = control_def["indices"] coords = [ WGT_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_controls") line_bone.head = (R @ coords[0] * bone_scale * 1.0) + facerig_bone.head line_bone.tail = (R @ utils.lerp(coords[0], coords[1], 0.5) * bone_scale * 1.0) + facerig_bone.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) nub_bone.head = utils.lerp(line_bone.head, line_bone.tail, zero * 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": 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 = [ WGT_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 control_def.get("x_invert") else zero_x zy = (1-zero_y) if control_def.get("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_controls") box_bone.head = (R @ pB0 * bone_scale * 1.0) + facerig_bone.head box_bone.tail = (R @ pB1 * bone_scale * 1.0) + facerig_bone.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_bone.head nub_bone.tail = (R @ pN2 * bone_scale * 1.0) + facerig_bone.head else: nub_bone.head = (R @ pN0 * bone_scale * 1.0) + facerig_bone.head nub_bone.tail = (R @ pN1 * bone_scale * 1.0) + facerig_bone.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.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) bone_names = ["facerig", "facerig_groups", "facerig_labels", "facerig_name"] bone_colors = ["WHITE", "GROUP", "WHITE", "WHITE"] bone_groups = ["UI", "UI", "UI", "UI"] bone_shapes = [ WGT_OUTLINE, WGT_GROUPS, WGT_LABELS, WGT_NAME ] bone_selectable = [ True, False, 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] 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) 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) control_range = control_def["range"] soft = control_def.get("soft", False) min_y = (length * zero) * control_range[0] max_y = length * (1 - zero) * control_range[1] #if soft: max_y *= 2.0 drivers.add_custom_float_property(line_bone, "slider_length", length) bones.add_limit_location_constraint(rigify_rig, nub_bone_name, 0, min_y, 0, 0, max_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 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) 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) control_range_x = control_def["x_range"] control_range_y = control_def["y_range"] if control_def.get("x_invert"): min_x = -(width * (1 - zero_x)) * control_range_x[0] max_x = -width * zero_x * control_range_x[1] else: min_x = (width * zero_x) * control_range_x[0] max_x = width * (1 - zero_x) * control_range_x[1] if control_def.get("y_invert"): min_y = -height * (1 - zero_y) * control_range_y[1] max_y = -(height * zero_y) * control_range_y[0] else: min_y = (height * zero_y) * control_range_y[0] max_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, min_x, min_y, 0, max_x, max_y, 0, space="LOCAL", use_transform_limit=True) def get_generated_controls(chr_cache, rigify_rig): 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"] == "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, rect_controls def collect_driver_defs(chr_cache, rigify_rig, slider_controls, rect_controls): shape_key_driver_defs = {} bone_driver_defs = {} # 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"] min_y = (length * zero_point) * control_range_y[0] max_y = length * (1 - zero_point) * control_range_y[1] allow_negative = control_def.get("negative", False) if "blendshapes" in control_def: num_keys = len(control_def["blendshapes"]) for i, (shape_key_name, shape_key_value) in enumerate(control_def["blendshapes"].items()): distance = min_y if shape_key_value == control_range_y[0] else max_y 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": distance, "var_axis": var_axis, "num_keys": num_keys, "negative": allow_negative, "use_strength": control_def.get("strength", True), } shape_key_driver_defs[shape_key_name][nub_bone_name] = key_control_def rigify_bones = control_def.get("rigify") if rigify_bones: for i, bone_def in enumerate(rigify_bones): bone_name = bone_def["bone"] if bone_name in rigify_rig.pose.bones: if "translation" in bone_def: prop, axis, index = facerig_data.LOC_AXES.get(bone_def["axis"], (None, None, None)) offset = bone_def["offset"] / 100 # convert from cm to m if type(bone_def["translation"]) is list: scalar = [bone_def["translation"][0] / 100, bone_def["translation"][1] / 100] else: scalar = bone_def["translation"] / 100 else: prop, axis, index = facerig_data.ROT_AXES.get(bone_def["axis"], (None, None, None)) offset = bone_def["offset"] * math.pi/180 # convert angles to radians if type(bone_def["rotation"]) is list: scalar = [bone_def["rotation"][0] * math.pi/180, bone_def["rotation"][1] * math.pi/180] else: scalar = bone_def["rotation"] * math.pi/180 if axis: driver_id = (bone_name, prop, index) var_axis = facerig_data.LOC_AXES.get("y")[1] bone_control_def = { "bone": bone_def["bone"], "offset": offset, "scalar": scalar, "distance": [min_y, max_y], "var_axis": var_axis, "use_strength": control_def.get("strength", True), } if driver_id not in bone_driver_defs: bone_driver_defs[driver_id] = {} bone_driver_defs[driver_id][nub_bone_name] = bone_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"] min_x = (width * zero_x) * control_range_x[0] max_x = width * (1 - zero_x) * control_range_x[1] min_y = -(height * zero_y) * control_range_y[0] max_y = -height * (1 - zero_y) * control_range_y[1] allow_negative = control_def.get("negative", False) ctrl_axes = [ ("horizontal", "x", min_x, max_x, control_range_x), ("vertical", "y", min_y, max_y, control_range_y) ] if "blendshapes" in control_def: for ctrl_dir, ctrl_axis, min_d, max_d, control_range in ctrl_axes: num_keys = len(control_def["blendshapes"][ctrl_axis]) parent = control_def.get(f"{ctrl_axis}_parent") for i, (shape_key_name, shape_key_value) in enumerate(control_def["blendshapes"][ctrl_axis].items()): distance = min_d if utils.same_sign(shape_key_value, control_range[0]) else max_d 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": distance, "var_axis": var_axis, "num_keys": num_keys, "negative": allow_negative, "use_strength": control_def.get("strength", True), } shape_key_driver_defs[shape_key_name][nub_bone_name] = key_control_def rigify_bones = control_def.get("rigify") if rigify_bones: for ctrl_dir, ctrl_axis, min_d, max_d, control_range in ctrl_axes: for bone_def in rigify_bones[ctrl_dir]: bone_name = bone_def["bone"] if bone_name in rigify_rig.pose.bones: if "translation" in bone_def: prop, axis, index = facerig_data.LOC_AXES.get(bone_def["axis"], (None, None, None)) offset = bone_def["offset"] / 100 # convert from cm to m if type(bone_def["translation"]) is list: scalar = [bone_def["translation"][0] / 100, bone_def["translation"][1] / 100] else: scalar = bone_def["translation"] / 100 else: prop, axis, index = facerig_data.ROT_AXES.get(bone_def["axis"], (None, None, None)) offset = bone_def["offset"] * math.pi/180 # convert angles to radians if type(bone_def["rotation"]) is list: scalar = [bone_def["rotation"][0] * math.pi/180, bone_def["rotation"][1] * math.pi/180] else: scalar = bone_def["rotation"] * math.pi/180 if axis: driver_id = (bone_name, prop, index) var_axis = facerig_data.LOC_AXES.get(ctrl_axis)[1] bone_control_def = { "bone": bone_def["bone"], "offset": offset, "scalar": scalar, "distance": [min_d, max_d], "var_axis": var_axis, "use_strength": control_def.get("strength", True) } if driver_id not in bone_driver_defs: bone_driver_defs[driver_id] = {} bone_driver_defs[driver_id][nub_bone_name] = bone_control_def return shape_key_driver_defs, bone_driver_defs def fvar(float_value): return "{0:0.6f}".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) # initialize target bone rotation modes and clear unwanted constraints for control_name, control_def in FACERIG_CONFIG.items(): if control_def["widget_type"] == "slider": rigify_bones = control_def.get("rigify") if rigify_bones: for bone_def in rigify_bones: 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 control_def["widget_type"] == "rect": rigify_bones = control_def.get("rigify") if rigify_bones: for axis_dir, bone_list in rigify_bones.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) slider_controls, rect_controls = get_generated_controls(chr_cache, rigify_rig) objects = chr_cache.get_cache_objects() 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)") shape_key_driver_defs, bone_driver_defs = collect_driver_defs(chr_cache, rigify_rig, 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 = "" num_key_controls = len(shape_key_driver_def) allow_negative = False use_strength = False value = 1.0 for nub_bone_name, key_control_def in shape_key_driver_def.items(): if nub_bone_name in rigify_rig.pose.bones: 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"] allow_negative = key_control_def.get("negative", False) value = key_control_def["value"] var_name = f"var{vidx}" vidx += 1 if var_expression: var_expression += "+" use_negative = allow_negative and (num_keys == 1 or num_key_controls > 1) if use_negative: var_expression += f"({var_name}/{fvar(distance)})" else: var_expression += f"max(0,{var_name}/{fvar(distance)})" var_def = drivers.make_bone_transform_var_def(var_name, rigify_rig, nub_bone_name, var_axis, "TRANSFORM_SPACE") var_defs.append(var_def) expression = f"{fvar(value)}*({var_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) allow_negative = False shape_key_range = 1.0 high = shape_key_range low = -shape_key_range if allow_negative else 0 expression = f"max({fvar(low)},min({fvar(high)},{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 var_defs = [] vidx = 0 var_expression = "" use_strength = False for nub_bone_name, 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"] use_strength = bone_control_def["use_strength"] var_name = f"var{vidx}" vidx += 1 if var_expression: var_expression += "+" var_expression += f"({var_name}/{fvar(distance[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) expression = "" high = 0 low = 0 if type(scalar) is list: # asymmetric bone movements (eyes) on CC3+ characters. high = max(high, scalar[0], scalar[1]) low = min(low, scalar[0], scalar[1]) pos_var_expression = f"(max(0,{var_expression})*{fvar(scalar[0])})" neg_var_expression = f"(min(0,{var_expression})*{fvar(scalar[1])})" expression = f"{pos_var_expression}-{neg_var_expression}" else: high = max(high, abs(scalar)) low = min(low, -abs(scalar)) expression = f"{fvar(scalar)}*({var_expression})" if use_strength: expression = f"BS*({expression})" var_def = drivers.make_custom_prop_var_def("BS", facerig_bone, "bone_strength") var_defs.append(var_def) expression = f"max({fvar(low)},min({fvar(high)},{expression}))" driver_def = ["SCRIPTED", prop, index, expression] drivers.add_bone_driver(rigify_rig, bone_name, driver_def, var_defs, 1.0) 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() for control_name, control_def in FACERIG_CONFIG.items(): 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]) blend_shapes = control_def.get("blendshapes") if blend_shapes and key_group: blend_shapes = blend_shapes[key_group] rl_bones = control_def.get("bones") if rl_bones and bone_group: rl_bones = rl_bones[bone_group] 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 * inv driver_id = (control_name, "location", index) if not shape_key_only and source_rig and rl_bones and len(rl_bones) > 0: ctrl_drivers[driver_id] = { "method": method, "parent": parent, "length": slider_length, "bones": [] } for bone_def in rl_bones: source_name = bone_def["bone"] if source_name in source_rig.pose.bones: if "translation" in bone_def: prop, prop_axis, prop_index = facerig_data.LOC_AXES.get(bone_def["axis"], (None, None, None)) source_rig_axis_scale = source_rig.scale[prop_index] offset = bone_def["offset"] * source_rig_axis_scale if type(bone_def["translation"]) is list: scalar = [bone_def["translation"][0], bone_def["translation"][1]] scale = [slider_length/(control_range[1] * scalar[0]), slider_length/(control_range[1] * scalar[1])] else: scalar = bone_def["translation"] scale = slider_length/(control_range[1] * scalar) else: prop, prop_axis, prop_index = facerig_data.ROT_AXES.get(bone_def["axis"], (None, None, None)) offset = bone_def["offset"] * math.pi / 180 if type(bone_def["rotation"]) is list: scalar = [bone_def["rotation"][0] * math.pi / 180, bone_def["rotation"][1] * math.pi / 180] scale = [slider_length/(control_range[1] * scalar[0]), slider_length/(control_range[1] * scalar[1])] else: scalar = bone_def["rotation"] * math.pi / 180 scale = slider_length/(control_range[1] * scalar) ctrl_drivers[driver_id]["bones"].append({ "bone": source_name, "scale": scale, "offset": offset, "axis": prop_axis }) elif blend_shapes: ctrl_drivers[driver_id] = { "method": method, "parent": parent, "length": slider_length, "shape_keys": [] } left_shape = right_shape = None 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 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 if utils.same_sign(blend_shape_value, control_range[0]): left_shape = (blend_shape_name, abs(blend_shape_value), slider_length/control_range[0]) elif utils.same_sign(blend_shape_value, control_range[1]): right_shape = (blend_shape_name, abs(blend_shape_value), slider_length/control_range[1]) if left_shape and right_shape: if left_shape: ctrl_drivers[driver_id]["shape_keys"].append({ "shape_key": left_shape[0], "value": left_shape[1], "scale": left_shape[2] }) if right_shape: ctrl_drivers[driver_id]["shape_keys"].append({ "shape_key": right_shape[0], "value": right_shape[1], "scale": right_shape[2] }) left_shape = right_shape = None if left_shape: ctrl_drivers[driver_id]["shape_keys"].append({ "shape_key": left_shape[0], "value": left_shape[1], "scale": left_shape[2] }) if right_shape: ctrl_drivers[driver_id]["shape_keys"].append({ "shape_key": right_shape[0], "value": right_shape[1], "scale": right_shape[2] }) 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) 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"]) if length_override is not None: length_override = abs(length_override) pose_bone = bones.get_pose_bone(rigify_rig, bone_name) expression = "" var_defs = [] prop_defs = [] 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"] for key_def in shape_key_defs: scale = key_def["scale"] value = key_def["value"] if length_override: scale *= length_override / length length = length_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"var{vidx}" if count > 0: expression += "+" var_expression = f"({var_name}*{fvar(scale)}/{fvar(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: 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: for obj in source_objects: key = drivers.get_shape_key(obj, shape_key_name) if key: # 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) break 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"] if arkit and type(scale) is list: # assume non asymmetric bone movements for arkit retargeting # (for completeness as arkit is retargeted via the shape keys so shouldn't happen) scale = utils.sign(scale[0]) * max(abs(scale[0]), abs(scale[1])) axis = bone_def["axis"] offset = bone_def["offset"] source_name = bone_def["bone"] if source_name in source_rig.pose.bones: var_name = f"var{vidx}" if count > 0: expression += "+" if type(scale) is list: # asymmetric bone movements (eyes) on CC3+ characters. pos_var_expression = f"(max(0,{var_name})*{fvar(scale[0])})" neg_var_expression = f"(min(0,{var_name})*{fvar(scale[1])})" expression += f"({pos_var_expression}-{neg_var_expression})" else: expression += f"({var_name}*{fvar(scale)})" 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) 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) vis = bones.store_bone_locks_visibility(rigify_rig) bones.clear_pose(rigify_rig, control_bones) bones.restore_bone_locks_visibility(rigify_rig, vis) 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(): 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) 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) 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"] bone_selectable = [ True, False, 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") 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") 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 ""