Eye tracking fixes

This commit is contained in:
Yusarina
2024-12-15 22:04:09 +00:00
parent 87a351cea4
commit 1916890966
4 changed files with 341 additions and 196 deletions
+12
View File
@@ -177,7 +177,19 @@ class AvatarToolkitSceneProperties(PropertyGroup):
('vrc.v_th', 'TH', 'Th as in "think"') ('vrc.v_th', 'TH', 'Th as in "think"')
], ],
update=lambda s, c: VisemePreview.update_preview(c) update=lambda s, c: VisemePreview.update_preview(c)
) )
eye_tracking_type: EnumProperty(
name=t("EyeTracking.type"),
description=t("EyeTracking.type_desc"),
items=[
('AV3', t("EyeTracking.type.av3"), t("EyeTracking.type.av3_desc")),
('SDK2', t("EyeTracking.type.sdk2"), t("EyeTracking.type.sdk2_desc"))
],
default='AV3'
)
eye_mode: EnumProperty( eye_mode: EnumProperty(
name=t("EyeTracking.mode"), name=t("EyeTracking.mode"),
items=[ items=[
+180 -102
View File
@@ -29,6 +29,177 @@ VALID_EYE_NAMES = {
'right': ['RightEye', 'Eye_R', 'eye_R', 'eye.R', 'EyeRight', 'right_eye', 'r_eye'] 'right': ['RightEye', 'Eye_R', 'eye_R', 'eye.R', 'EyeRight', 'right_eye', 'r_eye']
} }
class CreateEyesAV3Button(bpy.types.Operator):
"""Create eye tracking setup for VRChat Avatar 3.0"""
bl_idname = 'avatar_toolkit.create_eye_tracking_av3'
bl_label = t('EyeTracking.create.av3.label')
bl_description = t('EyeTracking.create.av3.desc')
bl_options = {'REGISTER', 'UNDO'}
mesh = None
@classmethod
def poll(cls, context):
toolkit = context.scene.avatar_toolkit
if not toolkit.head or not toolkit.eye_left or not toolkit.eye_right:
return False
return True
def execute(self, context):
toolkit = context.scene.avatar_toolkit
armature = get_active_armature(context)
with ProgressTracker(context, 100, "Creating AV3 Eye Tracking") as progress:
try:
context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='EDIT')
progress.step("Setting up bones")
# Set up bones
head = armature.data.edit_bones.get(toolkit.head)
old_eye_left = armature.data.edit_bones.get(toolkit.eye_left)
old_eye_right = armature.data.edit_bones.get(toolkit.eye_right)
# Store original names and transformations
left_name = old_eye_left.name
right_name = old_eye_right.name
left_matrix = old_eye_left.matrix.copy()
right_matrix = old_eye_right.matrix.copy()
left_length = old_eye_left.length
right_length = old_eye_right.length
# Unparent and remove original bones
old_eye_left.parent = None
old_eye_right.parent = None
armature.data.edit_bones.remove(old_eye_left)
armature.data.edit_bones.remove(old_eye_right)
# Create new eye bones with original names
new_left_eye = armature.data.edit_bones.new(left_name)
new_right_eye = armature.data.edit_bones.new(right_name)
# Parent them
new_left_eye.parent = head
new_right_eye.parent = head
# Calculate straight up orientation matrix
straight_up_matrix = mathutils.Matrix.Rotation(math.pi/2, 3, 'X')
# Apply rotation while preserving position
for eye_data in [(new_left_eye, left_matrix, left_length),
(new_right_eye, right_matrix, right_length)]:
new_eye, orig_matrix, length = eye_data
new_matrix = straight_up_matrix.to_4x4()
new_matrix.translation = orig_matrix.translation
new_eye.matrix = new_matrix
new_eye.length = length
# Disable mirroring to prevent unwanted behavior
armature.data.use_mirror_x = False
progress.step("Finalizing setup")
bpy.ops.object.mode_set(mode='OBJECT')
self.report({'INFO'}, t('EyeTracking.success'))
return {'FINISHED'}
except Exception as e:
logger.error(f"Eye tracking setup failed: {str(e)}")
return {'CANCELLED'}
class CreateEyesSDK2Button(bpy.types.Operator):
"""Create eye tracking setup for VRChat SDK2"""
bl_idname = 'avatar_toolkit.create_eye_tracking_sdk2'
bl_label = t('EyeTracking.create.sdk2.label')
bl_description = t('EyeTracking.create.sdk2.desc')
bl_options = {'REGISTER', 'UNDO'}
mesh = None
@classmethod
def poll(cls, context):
if not get_all_meshes(context):
return False
toolkit = context.scene.avatar_toolkit
if not toolkit.head or not toolkit.eye_left or not toolkit.eye_right:
return False
if toolkit.disable_eye_blinking and toolkit.disable_eye_movement:
return False
return True
def execute(self, context):
toolkit = context.scene.avatar_toolkit
armature = get_active_armature(context)
with ProgressTracker(context, 100, "Creating SDK2 Eye Tracking") as progress:
# Validate setup
validator = EyeTrackingValidator()
is_valid, message = validator.validate_setup(context, toolkit.mesh_name_eye)
if not is_valid:
self.report({'ERROR'}, message)
return {'CANCELLED'}
try:
context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='EDIT')
progress.step("Setting up bones")
self.mesh = bpy.data.objects.get(toolkit.mesh_name_eye)
# Set up bones
head = armature.data.edit_bones.get(toolkit.head)
old_eye_left = armature.data.edit_bones.get(toolkit.eye_left)
old_eye_right = armature.data.edit_bones.get(toolkit.eye_right)
# Create new eye bones
new_left_eye = armature.data.edit_bones.new('LeftEye')
new_right_eye = armature.data.edit_bones.new('RightEye')
# Parent them
new_left_eye.parent = head
new_right_eye.parent = head
# Calculate positions for SDK2 style
fix_eye_position(context, old_eye_left, new_left_eye, head, False)
fix_eye_position(context, old_eye_right, new_right_eye, head, True)
progress.step("Processing vertex groups")
if not toolkit.disable_eye_movement:
# Switch to object mode for vertex group operations
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
self.mesh.select_set(True)
context.view_layer.objects.active = self.mesh
copy_vertex_group(self, old_eye_left.name, 'LeftEye')
copy_vertex_group(self, old_eye_right.name, 'RightEye')
# Return to armature edit mode
context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='EDIT')
progress.step("Processing shape keys")
if not toolkit.disable_eye_blinking:
shapes = [toolkit.wink_left, toolkit.wink_right,
toolkit.lowerlid_left, toolkit.lowerlid_right]
new_shapes = ['vrc.blink_left', 'vrc.blink_right',
'vrc.lowerlid_left', 'vrc.lowerlid_right']
progress.step("Finalizing setup")
bpy.ops.object.mode_set(mode='OBJECT')
toolkit.eye_mode = 'TESTING'
self.report({'INFO'}, t('EyeTracking.success'))
return {'FINISHED'}
except Exception as e:
logger.error(f"Eye tracking setup failed: {str(e)}")
return {'CANCELLED'}
class EyeTrackingBackup: class EyeTrackingBackup:
def __init__(self): def __init__(self):
self.backup_path = os.path.join(bpy.app.tempdir, "eye_tracking_backup.json") self.backup_path = os.path.join(bpy.app.tempdir, "eye_tracking_backup.json")
@@ -127,108 +298,6 @@ class EyeTrackingValidator:
return True, t('EyeTracking.validation.success') return True, t('EyeTracking.validation.success')
class CreateEyesButton(bpy.types.Operator):
bl_idname = 'avatar_toolkit.create_eye_tracking'
bl_label = t('EyeTracking.create.label')
bl_description = t('EyeTracking.create.desc')
bl_options = {'REGISTER', 'UNDO'}
mesh = None
@classmethod
def poll(cls, context):
if not get_all_meshes(context):
return False
toolkit = context.scene.avatar_toolkit
if not toolkit.head or not toolkit.eye_left or not toolkit.eye_right:
return False
if toolkit.disable_eye_blinking and toolkit.disable_eye_movement:
return False
return True
def execute(self, context):
toolkit = context.scene.avatar_toolkit
armature = get_active_armature(context)
with ProgressTracker(context, 100, "Creating Eye Tracking") as progress:
# Validate setup
validator = EyeTrackingValidator()
is_valid, message = validator.validate_setup(context, toolkit.mesh_name_eye)
if not is_valid:
self.report({'ERROR'}, message)
return {'CANCELLED'}
# Create backup
backup = EyeTrackingBackup()
if not backup.store_bone_positions(armature):
logger.warning("Failed to create backup")
try:
# Set active object and mode
context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='EDIT')
progress.step("Setting up bones")
self.mesh = bpy.data.objects.get(toolkit.mesh_name_eye)
# Set up bones
head = armature.data.edit_bones.get(toolkit.head)
old_eye_left = armature.data.edit_bones.get(toolkit.eye_left)
old_eye_right = armature.data.edit_bones.get(toolkit.eye_right)
if not toolkit.disable_eye_blinking:
if not all([toolkit.wink_left, toolkit.wink_right,
toolkit.lowerlid_left, toolkit.lowerlid_right]):
self.report({'ERROR'}, t('EyeTracking.error.noShapeSelected'))
return {'CANCELLED'}
progress.step("Processing vertex groups")
# Create new eye bones
new_left_eye = armature.data.edit_bones.new('LeftEye')
new_right_eye = armature.data.edit_bones.new('RightEye')
# Parent them
new_left_eye.parent = head
new_right_eye.parent = head
# Calculate positions
fix_eye_position(context, old_eye_left, new_left_eye, head, False)
fix_eye_position(context, old_eye_right, new_right_eye, head, True)
progress.step("Processing shape keys")
# Process shape keys
if not toolkit.disable_eye_movement:
self.copy_vertex_group(old_eye_left.name, 'LeftEye')
self.copy_vertex_group(old_eye_right.name, 'RightEye')
# Handle shape keys
shapes = [toolkit.wink_left, toolkit.wink_right,
toolkit.lowerlid_left, toolkit.lowerlid_right]
new_shapes = ['vrc.blink_left', 'vrc.blink_right',
'vrc.lowerlid_left', 'vrc.lowerlid_right']
progress.step("Finalizing setup")
# Reset modes and cleanup
bpy.ops.object.mode_set(mode='OBJECT')
# Update scene properties
toolkit.eye_mode = 'TESTING'
self.report({'INFO'}, t('EyeTracking.success'))
return {'FINISHED'}
except Exception as e:
logger.error(f"Eye tracking setup failed: {str(e)}")
if backup.restore_bone_positions(get_active_armature(context)):
logger.info("Restored from backup")
return {'CANCELLED'}
class StartTestingButton(bpy.types.Operator): class StartTestingButton(bpy.types.Operator):
bl_idname = 'avatar_toolkit.start_eye_testing' bl_idname = 'avatar_toolkit.start_eye_testing'
bl_label = t('EyeTracking.testing.start.label') bl_label = t('EyeTracking.testing.start.label')
@@ -318,6 +387,8 @@ class StopTestingButton(bpy.types.Operator):
eye_left_rot = [] eye_left_rot = []
eye_right_rot = [] eye_right_rot = []
bpy.ops.object.mode_set(mode='OBJECT')
return {'FINISHED'} return {'FINISHED'}
def set_rotation(self, context): def set_rotation(self, context):
@@ -695,6 +766,12 @@ def vertex_group_exists(mesh_obj, group_name):
def copy_vertex_group(self, vertex_group, rename_to): def copy_vertex_group(self, vertex_group, rename_to):
"""Copy vertex group with new name""" """Copy vertex group with new name"""
vertex_group_index = 0 vertex_group_index = 0
# Select and make mesh active
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
self.mesh.select_set(True)
bpy.context.view_layer.objects.active = self.mesh
for group in self.mesh.vertex_groups: for group in self.mesh.vertex_groups:
if group.name == vertex_group: if group.name == vertex_group:
self.mesh.vertex_groups.active_index = vertex_group_index self.mesh.vertex_groups.active_index = vertex_group_index
@@ -703,6 +780,7 @@ def copy_vertex_group(self, vertex_group, rename_to):
break break
vertex_group_index += 1 vertex_group_index += 1
def copy_shape_key(self, context, from_shape, new_names, new_index): def copy_shape_key(self, context, from_shape, new_names, new_index):
"""Copy shape key with new name""" """Copy shape key with new name"""
blinking = not context.scene.avatar_toolkit.disable_eye_blinking blinking = not context.scene.avatar_toolkit.disable_eye_blinking
+11
View File
@@ -303,6 +303,17 @@
"EyeTracking.head_bone_desc": "Select head bone", "EyeTracking.head_bone_desc": "Select head bone",
"EyeTracking.eye_left_desc": "Select left eye bone", "EyeTracking.eye_left_desc": "Select left eye bone",
"EyeTracking.eye_right_desc": "Select right eye bone", "EyeTracking.eye_right_desc": "Select right eye bone",
"EyeTracking.type": "Eye Tracking Type",
"EyeTracking.type_desc": "Select the type of eye tracking setup to create",
"EyeTracking.create.av3.label": "Create AV3 Eye Tracking",
"EyeTracking.create.av3.desc": "Set up eye tracking for VRChat Avatar 3.0",
"EyeTracking.create.sdk2.label": "Create SDK2 Eye Tracking",
"EyeTracking.create.sdk2.desc": "Set up eye tracking for VRChat SDK2",
"EyeTracking.sdk_version": "SDK Version",
"EyeTracking.type.av3": "Avatar 3.0",
"EyeTracking.type.av3_desc": "VRChat Avatar 3.0 eye tracking setup",
"EyeTracking.type.sdk2": "SDK2 (Legacy)",
"EyeTracking.type.sdk2_desc": "VRChat SDK2 eye tracking setup",
"Settings.label": "Settings", "Settings.label": "Settings",
"Settings.language": "Language", "Settings.language": "Language",
+55 -11
View File
@@ -5,7 +5,8 @@ from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..core.translations import t from ..core.translations import t
from ..core.common import get_active_armature, get_all_meshes from ..core.common import get_active_armature, get_all_meshes
from ..functions.eye_tracking import ( from ..functions.eye_tracking import (
CreateEyesButton, CreateEyesAV3Button,
CreateEyesSDK2Button,
StartTestingButton, StartTestingButton,
StopTestingButton, StopTestingButton,
ResetRotationButton, ResetRotationButton,
@@ -33,20 +34,32 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
layout = self.layout layout = self.layout
toolkit = context.scene.avatar_toolkit toolkit = context.scene.avatar_toolkit
# SDK Version Selection Box
sdk_box = layout.box()
col = sdk_box.column(align=True)
col.label(text=t("EyeTracking.sdk_version"), icon='PRESET')
col.separator(factor=0.5)
row = col.row(align=True)
row.prop(toolkit, "eye_tracking_type", expand=True)
if toolkit.eye_tracking_type == 'SDK2':
# Mode Selection Box # Mode Selection Box
mode_box = layout.box() mode_box = layout.box()
col = mode_box.column(align=True) col = mode_box.column(align=True)
col.label(text=t("EyeTracking.mode_select"), icon='TOOL_SETTINGS') col.label(text=t("EyeTracking.setup"), icon='TOOL_SETTINGS')
col.separator(factor=0.5) col.separator(factor=0.5)
col.prop(toolkit, "eye_mode", expand=True) col.prop(toolkit, "eye_mode", expand=True)
if toolkit.eye_mode == 'CREATION': if toolkit.eye_mode == 'CREATION':
# Mesh Setup Box self.draw_creation_mode(context, layout)
mesh_box = layout.box() else:
col = mesh_box.column(align=True) self.draw_testing_mode(context, layout)
col.label(text=t("EyeTracking.mesh_setup"), icon='MESH_DATA') else:
col.separator(factor=0.5) # AV3 bone setup only
col.prop(toolkit, "mesh_name_eye", text="") self.draw_av3_setup(context, layout)
def draw_av3_setup(self, context: Context, layout: UILayout) -> None:
toolkit = context.scene.avatar_toolkit
# Bone Setup Box # Bone Setup Box
bone_box = layout.box() bone_box = layout.box()
@@ -62,7 +75,36 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
else: else:
col.label(text=t("EyeTracking.no_armature"), icon='ERROR') col.label(text=t("EyeTracking.no_armature"), icon='ERROR')
# Shapekey Setup Box # Create Button
row = layout.row(align=True)
row.scale_y = 1.5
row.operator(CreateEyesAV3Button.bl_idname, icon='PLAY')
def draw_creation_mode(self, context: Context, layout: UILayout) -> None:
toolkit = context.scene.avatar_toolkit
# Bone Setup Box
bone_box = layout.box()
col = bone_box.column(align=True)
col.label(text=t("EyeTracking.bone_setup"), icon='BONE_DATA')
col.separator(factor=0.5)
armature = get_active_armature(context)
if armature:
col.prop_search(toolkit, "head", armature.data, "bones", text=t("EyeTracking.head_bone"))
col.prop_search(toolkit, "eye_left", armature.data, "bones", text=t("EyeTracking.eye_left"))
col.prop_search(toolkit, "eye_right", armature.data, "bones", text=t("EyeTracking.eye_right"))
else:
col.label(text=t("EyeTracking.no_armature"), icon='ERROR')
# Mesh Setup Box
mesh_box = layout.box()
col = mesh_box.column(align=True)
col.label(text=t("EyeTracking.mesh_setup"), icon='MESH_DATA')
col.separator(factor=0.5)
col.prop_search(toolkit, "mesh_name_eye", bpy.data, "objects", text="")
# Shape Key Setup Box
shape_box = layout.box() shape_box = layout.box()
col = shape_box.column(align=True) col = shape_box.column(align=True)
col.label(text=t("EyeTracking.shapekey_setup"), icon='SHAPEKEY_DATA') col.label(text=t("EyeTracking.shapekey_setup"), icon='SHAPEKEY_DATA')
@@ -90,9 +132,11 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
# Create Button # Create Button
row = layout.row(align=True) row = layout.row(align=True)
row.scale_y = 1.5 row.scale_y = 1.5
row.operator(CreateEyesButton.bl_idname, icon='PLAY') row.operator(CreateEyesSDK2Button.bl_idname, icon='PLAY')
def draw_testing_mode(self, context: Context, layout: UILayout) -> None:
toolkit = context.scene.avatar_toolkit
else:
if context.mode != 'POSE': if context.mode != 'POSE':
# Testing Start Box # Testing Start Box
test_box = layout.box() test_box = layout.box()