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