Files
blender-portable-repo/scripts/addons/cc_blender_tools-main/facerig.py
T
2026-03-17 14:30:01 -06:00

1597 lines
77 KiB
Python

# Copyright (C) 2021 Victor Soupday
# This file is part of CC/iC Blender Tools <https://github.com/soupday/cc_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 <https://www.gnu.org/licenses/>.
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 ""