Added Eye tracking and Visemes
This commit is contained in:
@@ -0,0 +1,867 @@
|
||||
import os
|
||||
import bpy
|
||||
import copy
|
||||
import math
|
||||
import bmesh
|
||||
import mathutils
|
||||
import json
|
||||
from bpy.types import Operator, Object, Context
|
||||
from typing import Optional, Dict, Tuple, Set
|
||||
from collections import OrderedDict
|
||||
from random import random
|
||||
from itertools import chain
|
||||
|
||||
from ..core.logging_setup import logger
|
||||
from ..core.translations import t
|
||||
from ..core.common import (
|
||||
ProgressTracker,
|
||||
get_active_armature,
|
||||
get_all_meshes,
|
||||
get_armature_list,
|
||||
validate_armature,
|
||||
validate_mesh_for_pose,
|
||||
cache_vertex_positions,
|
||||
apply_vertex_positions
|
||||
)
|
||||
|
||||
VALID_EYE_NAMES = {
|
||||
'left': ['LeftEye', 'Eye_L', 'eye_L', 'eye.L', 'EyeLeft', 'left_eye', 'l_eye'],
|
||||
'right': ['RightEye', 'Eye_R', 'eye_R', 'eye.R', 'EyeRight', 'right_eye', 'r_eye']
|
||||
}
|
||||
|
||||
class EyeTrackingBackup:
|
||||
def __init__(self):
|
||||
self.backup_path = os.path.join(bpy.app.tempdir, "eye_tracking_backup.json")
|
||||
self.bone_positions: Dict[str, Dict[str, Tuple[float, float, float]]] = {}
|
||||
|
||||
def store_bone_positions(self, armature) -> bool:
|
||||
try:
|
||||
self.bone_positions = {
|
||||
'LeftEye': {
|
||||
'head': tuple(armature.data.bones['LeftEye'].head_local),
|
||||
'tail': tuple(armature.data.bones['LeftEye'].tail_local)
|
||||
},
|
||||
'RightEye': {
|
||||
'head': tuple(armature.data.bones['RightEye'].head_local),
|
||||
'tail': tuple(armature.data.bones['RightEye'].tail_local)
|
||||
}
|
||||
}
|
||||
|
||||
with open(self.backup_path, 'w') as f:
|
||||
json.dump(self.bone_positions, f)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Backup failed: {str(e)}")
|
||||
return False
|
||||
|
||||
def restore_bone_positions(self, armature) -> bool:
|
||||
try:
|
||||
if not os.path.exists(self.backup_path):
|
||||
return False
|
||||
|
||||
with open(self.backup_path, 'r') as f:
|
||||
backup_data = json.load(f)
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
for bone_name, positions in backup_data.items():
|
||||
if bone_name in armature.data.edit_bones:
|
||||
bone = armature.data.edit_bones[bone_name]
|
||||
bone.head = positions['head']
|
||||
bone.tail = positions['tail']
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Restore failed: {str(e)}")
|
||||
return False
|
||||
|
||||
class EyeTrackingValidator:
|
||||
@staticmethod
|
||||
def find_eye_vertex_groups(mesh_name: str) -> Tuple[str, str]:
|
||||
mesh = bpy.data.objects.get(mesh_name)
|
||||
if not mesh:
|
||||
return None, None
|
||||
|
||||
left_group = None
|
||||
right_group = None
|
||||
|
||||
for group in mesh.vertex_groups:
|
||||
if any(name.lower() in group.name.lower() for name in VALID_EYE_NAMES['left']):
|
||||
left_group = group.name
|
||||
if any(name.lower() in group.name.lower() for name in VALID_EYE_NAMES['right']):
|
||||
right_group = group.name
|
||||
|
||||
return left_group, right_group
|
||||
|
||||
@staticmethod
|
||||
def validate_setup(context, mesh_name: str) -> Tuple[bool, str]:
|
||||
armature = get_active_armature(context)
|
||||
if not armature:
|
||||
return False, t('EyeTracking.validation.noArmature')
|
||||
|
||||
mesh = bpy.data.objects.get(mesh_name)
|
||||
if not mesh:
|
||||
return False, t('EyeTracking.validation.noMesh', mesh=mesh_name)
|
||||
|
||||
if not mesh.data.shape_keys:
|
||||
return False, t('EyeTracking.validation.noShapekeys')
|
||||
|
||||
left_group, right_group = EyeTrackingValidator.find_eye_vertex_groups(mesh_name)
|
||||
missing_groups = []
|
||||
|
||||
if not left_group:
|
||||
missing_groups.append(t('EyeTracking.validation.leftEye'))
|
||||
if not right_group:
|
||||
missing_groups.append(t('EyeTracking.validation.rightEye'))
|
||||
|
||||
if missing_groups:
|
||||
return False, t('EyeTracking.validation.missingGroups', groups=', '.join(missing_groups))
|
||||
|
||||
required_bones = [context.scene.avatar_toolkit.head,
|
||||
context.scene.avatar_toolkit.eye_left,
|
||||
context.scene.avatar_toolkit.eye_right]
|
||||
missing_bones = [bone for bone in required_bones if bone not in armature.data.bones]
|
||||
|
||||
if missing_bones:
|
||||
return False, t('EyeTracking.validation.missingBones', bones=', '.join(missing_bones))
|
||||
|
||||
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):
|
||||
bl_idname = 'avatar_toolkit.start_eye_testing'
|
||||
bl_label = t('EyeTracking.testing.start.label')
|
||||
bl_description = t('EyeTracking.testing.start.desc')
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
armature = get_active_armature(context)
|
||||
return armature and 'LeftEye' in armature.pose.bones and 'RightEye' in armature.pose.bones
|
||||
|
||||
def execute(self, context):
|
||||
armature = get_active_armature(context)
|
||||
bpy.ops.object.mode_set(mode='POSE')
|
||||
armature.data.pose_position = 'POSE'
|
||||
|
||||
global eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot
|
||||
eye_left = armature.pose.bones.get('LeftEye')
|
||||
eye_right = armature.pose.bones.get('RightEye')
|
||||
eye_left_data = armature.data.bones.get('LeftEye')
|
||||
eye_right_data = armature.data.bones.get('RightEye')
|
||||
|
||||
# Save initial rotations
|
||||
eye_left.rotation_mode = 'XYZ'
|
||||
eye_left_rot = copy.deepcopy(eye_left.rotation_euler)
|
||||
eye_right.rotation_mode = 'XYZ'
|
||||
eye_right_rot = copy.deepcopy(eye_right.rotation_euler)
|
||||
|
||||
if not all([eye_left, eye_right, eye_left_data, eye_right_data]):
|
||||
return {'FINISHED'}
|
||||
|
||||
# Reset shape keys
|
||||
mesh = bpy.data.objects[context.scene.avatar_toolkit.mesh_name_eye]
|
||||
for shape_key in mesh.data.shape_keys.key_blocks:
|
||||
shape_key.value = 0
|
||||
|
||||
# Clear transforms
|
||||
for pb in armature.data.bones:
|
||||
pb.select = True
|
||||
bpy.ops.pose.transforms_clear()
|
||||
for pb in armature.data.bones:
|
||||
pb.select = False
|
||||
pb.hide = True
|
||||
|
||||
eye_left_data.hide = False
|
||||
eye_right_data.hide = False
|
||||
|
||||
context.scene.avatar_toolkit.eye_rotation_x = 0
|
||||
context.scene.avatar_toolkit.eye_rotation_y = 0
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
class StopTestingButton(bpy.types.Operator):
|
||||
bl_idname = 'avatar_toolkit.stop_eye_testing'
|
||||
bl_label = t('EyeTracking.testing.stop.label')
|
||||
bl_description = t('EyeTracking.testing.stop.desc')
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
global eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot
|
||||
toolkit = context.scene.avatar_toolkit
|
||||
|
||||
if eye_left:
|
||||
toolkit.eye_rotation_x = 0
|
||||
toolkit.eye_rotation_y = 0
|
||||
|
||||
if not context.object or context.object.mode != 'POSE':
|
||||
armature = get_active_armature(context)
|
||||
bpy.ops.object.mode_set(mode='POSE')
|
||||
|
||||
armature = get_active_armature(context)
|
||||
for pb in armature.data.bones:
|
||||
pb.hide = False
|
||||
pb.select = True
|
||||
bpy.ops.pose.transforms_clear()
|
||||
for pb in armature.data.bones:
|
||||
pb.select = False
|
||||
|
||||
mesh = bpy.data.objects[toolkit.mesh_name_eye]
|
||||
for shape_key in mesh.data.shape_keys.key_blocks:
|
||||
shape_key.value = 0
|
||||
|
||||
eye_left = None
|
||||
eye_right = None
|
||||
eye_left_data = None
|
||||
eye_right_data = None
|
||||
eye_left_rot = []
|
||||
eye_right_rot = []
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def set_rotation(self, context):
|
||||
global eye_left, eye_right, eye_left_rot, eye_right_rot
|
||||
toolkit = context.scene.avatar_toolkit
|
||||
|
||||
if not eye_left or not eye_right:
|
||||
StartTestingButton.execute(StartTestingButton, context)
|
||||
return None
|
||||
|
||||
eye_left.rotation_mode = 'XYZ'
|
||||
eye_right.rotation_mode = 'XYZ'
|
||||
|
||||
x_rotation = math.radians(toolkit.eye_rotation_x)
|
||||
y_rotation = math.radians(toolkit.eye_rotation_y)
|
||||
|
||||
eye_left.rotation_euler[0] = eye_left_rot[0] + x_rotation
|
||||
eye_left.rotation_euler[1] = eye_left_rot[1] + y_rotation
|
||||
|
||||
eye_right.rotation_euler[0] = eye_right_rot[0] + x_rotation
|
||||
eye_right.rotation_euler[1] = eye_right_rot[1] + y_rotation
|
||||
|
||||
return None
|
||||
|
||||
class ResetRotationButton(bpy.types.Operator):
|
||||
bl_idname = 'avatar_toolkit.reset_eye_rotation'
|
||||
bl_label = t('EyeTracking.reset.label')
|
||||
bl_description = t('EyeTracking.reset.desc')
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
armature = get_active_armature(context)
|
||||
return armature and 'LeftEye' in armature.pose.bones and 'RightEye' in armature.pose.bones
|
||||
|
||||
def execute(self, context):
|
||||
toolkit = context.scene.avatar_toolkit
|
||||
armature = get_active_armature(context)
|
||||
|
||||
toolkit.eye_rotation_x = 0
|
||||
toolkit.eye_rotation_y = 0
|
||||
|
||||
global eye_left, eye_right, eye_left_data, eye_right_data
|
||||
eye_left = armature.pose.bones.get('LeftEye')
|
||||
eye_right = armature.pose.bones.get('RightEye')
|
||||
eye_left_data = armature.data.bones.get('LeftEye')
|
||||
eye_right_data = armature.data.bones.get('RightEye')
|
||||
|
||||
for eye in [eye_left, eye_right]:
|
||||
eye.rotation_mode = 'XYZ'
|
||||
for i in range(3):
|
||||
eye.rotation_euler[i] = 0
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
class AdjustEyesButton(bpy.types.Operator):
|
||||
bl_idname = 'avatar_toolkit.adjust_eyes'
|
||||
bl_label = t('EyeTracking.adjust.label')
|
||||
bl_description = t('EyeTracking.adjust.desc')
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
armature = get_active_armature(context)
|
||||
return armature and all(bone in armature.pose.bones for bone in ['LeftEye', 'RightEye'])
|
||||
|
||||
def execute(self, context):
|
||||
toolkit = context.scene.avatar_toolkit
|
||||
if toolkit.disable_eye_movement:
|
||||
return {'FINISHED'}
|
||||
|
||||
mesh_name = toolkit.mesh_name_eye
|
||||
mesh = bpy.data.objects.get(mesh_name)
|
||||
|
||||
if not mesh:
|
||||
self.report({'ERROR'}, t('EyeTracking.error.noMesh'))
|
||||
return {'CANCELLED'}
|
||||
|
||||
for eye in ['LeftEye', 'RightEye']:
|
||||
if not any(g.group == mesh.vertex_groups[eye].index for v in mesh.data.vertices for g in v.groups):
|
||||
self.report({'ERROR'}, t('EyeTracking.error.noVertexGroup', bone=eye))
|
||||
return {'CANCELLED'}
|
||||
|
||||
armature = get_active_armature(context)
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
new_eye_left = armature.data.edit_bones.get('LeftEye')
|
||||
new_eye_right = armature.data.edit_bones.get('RightEye')
|
||||
old_eye_left = armature.pose.bones.get(toolkit.eye_left)
|
||||
old_eye_right = armature.pose.bones.get(toolkit.eye_right)
|
||||
|
||||
fix_eye_position(context, old_eye_left, new_eye_left, None, False)
|
||||
fix_eye_position(context, old_eye_right, new_eye_right, None, True)
|
||||
|
||||
bpy.ops.object.mode_set(mode='POSE')
|
||||
|
||||
global eye_left, eye_right, eye_left_data, eye_right_data
|
||||
eye_left = armature.pose.bones.get('LeftEye')
|
||||
eye_right = armature.pose.bones.get('RightEye')
|
||||
eye_left_data = armature.data.bones.get('LeftEye')
|
||||
eye_right_data = armature.data.bones.get('RightEye')
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
class StartIrisHeightButton(bpy.types.Operator):
|
||||
bl_idname = 'avatar_toolkit.adjust_iris_height'
|
||||
bl_label = t('EyeTracking.iris.label')
|
||||
bl_description = t('EyeTracking.iris.desc')
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
armature = get_active_armature(context)
|
||||
return armature and all(bone in armature.pose.bones for bone in ['LeftEye', 'RightEye'])
|
||||
|
||||
def execute(self, context):
|
||||
toolkit = context.scene.avatar_toolkit
|
||||
if toolkit.disable_eye_movement:
|
||||
return {'FINISHED'}
|
||||
|
||||
armature = get_active_armature(context)
|
||||
armature.hide_viewport = True
|
||||
|
||||
mesh = bpy.data.objects[toolkit.mesh_name_eye]
|
||||
mesh.select_set(True)
|
||||
context.view_layer.objects.active = mesh
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
if len(mesh.vertex_groups) > 0:
|
||||
bpy.ops.mesh.select_mode(type='VERT')
|
||||
|
||||
for vg_name in ['LeftEye', 'RightEye']:
|
||||
vg = mesh.vertex_groups.get(vg_name)
|
||||
if vg:
|
||||
bpy.ops.object.vertex_group_set_active(group=vg.name)
|
||||
bpy.ops.object.vertex_group_select()
|
||||
|
||||
bm = bmesh.from_edit_mesh(mesh.data)
|
||||
for v in bm.verts:
|
||||
if v.select:
|
||||
v.co.y += toolkit.iris_height * 0.01
|
||||
logger.debug(f"Adjusted vertex position: {v.co}")
|
||||
bmesh.update_edit_mesh(mesh.data)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
class TestBlinking(bpy.types.Operator):
|
||||
bl_idname = 'avatar_toolkit.test_blinking'
|
||||
bl_label = t('EyeTracking.blink.test.label')
|
||||
bl_description = t('EyeTracking.blink.test.desc')
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
toolkit = context.scene.avatar_toolkit
|
||||
mesh = bpy.data.objects.get(toolkit.mesh_name_eye)
|
||||
return (mesh and mesh.data.shape_keys and
|
||||
all(key in mesh.data.shape_keys.key_blocks for key in ['vrc.blink_left', 'vrc.blink_right']))
|
||||
|
||||
def execute(self, context):
|
||||
toolkit = context.scene.avatar_toolkit
|
||||
mesh = bpy.data.objects[toolkit.mesh_name_eye]
|
||||
shapes = ['vrc.blink_left', 'vrc.blink_right']
|
||||
|
||||
for shape_key in mesh.data.shape_keys.key_blocks:
|
||||
shape_key.value = toolkit.eye_blink_shape if shape_key.name in shapes else 0
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
class TestLowerlid(bpy.types.Operator):
|
||||
bl_idname = 'avatar_toolkit.test_lowerlid'
|
||||
bl_label = t('EyeTracking.lowerlid.test.label')
|
||||
bl_description = t('EyeTracking.lowerlid.test.desc')
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
toolkit = context.scene.avatar_toolkit
|
||||
mesh = bpy.data.objects.get(toolkit.mesh_name_eye)
|
||||
return (mesh and mesh.data.shape_keys and
|
||||
all(key in mesh.data.shape_keys.key_blocks for key in ['vrc.lowerlid_left', 'vrc.lowerlid_right']))
|
||||
|
||||
def execute(self, context):
|
||||
toolkit = context.scene.avatar_toolkit
|
||||
mesh = bpy.data.objects[toolkit.mesh_name_eye]
|
||||
shapes = OrderedDict()
|
||||
shapes['vrc.lowerlid_left'] = toolkit.eye_lowerlid_shape
|
||||
shapes['vrc.lowerlid_right'] = toolkit.eye_lowerlid_shape
|
||||
|
||||
for shape_key in mesh.data.shape_keys.key_blocks:
|
||||
shape_key.value = toolkit.eye_lowerlid_shape if shape_key.name in shapes else 0
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
class ResetBlinkTest(bpy.types.Operator):
|
||||
bl_idname = 'avatar_toolkit.reset_blink_test'
|
||||
bl_label = t('EyeTracking.blink.reset.label')
|
||||
bl_description = t('EyeTracking.blink.reset.desc')
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
toolkit = context.scene.avatar_toolkit
|
||||
mesh = bpy.data.objects[toolkit.mesh_name_eye]
|
||||
|
||||
for shape_key in mesh.data.shape_keys.key_blocks:
|
||||
shape_key.value = 0
|
||||
|
||||
toolkit.eye_blink_shape = 1
|
||||
toolkit.eye_lowerlid_shape = 1
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def fix_eye_position(context, old_eye, new_eye, head, right_side):
|
||||
toolkit = context.scene.avatar_toolkit
|
||||
scale = -toolkit.eye_distance + 1
|
||||
mesh = bpy.data.objects[toolkit.mesh_name_eye]
|
||||
|
||||
if not toolkit.disable_eye_movement:
|
||||
if head:
|
||||
coords_eye = find_center_vector_of_vertex_group(mesh, old_eye.name)
|
||||
else:
|
||||
coords_eye = find_center_vector_of_vertex_group(mesh, new_eye.name)
|
||||
|
||||
if coords_eye is False:
|
||||
return
|
||||
|
||||
if head:
|
||||
p1 = mesh.matrix_world @ head.head
|
||||
p2 = mesh.matrix_world @ coords_eye
|
||||
length = (p1 - p2).length
|
||||
logger.debug(f"Eye distance: {length}")
|
||||
|
||||
x_cord, y_cord, z_cord = get_bone_orientations()
|
||||
|
||||
if toolkit.disable_eye_movement:
|
||||
if head is not None:
|
||||
new_eye.head[x_cord] = head.head[x_cord] + (0.05 if right_side else -0.05)
|
||||
new_eye.head[y_cord] = head.head[y_cord]
|
||||
new_eye.head[z_cord] = head.head[z_cord]
|
||||
else:
|
||||
new_eye.head[x_cord] = old_eye.head[x_cord] + scale * (coords_eye[0] - old_eye.head[x_cord])
|
||||
new_eye.head[y_cord] = old_eye.head[y_cord] + scale * (coords_eye[1] - old_eye.head[y_cord])
|
||||
new_eye.head[z_cord] = old_eye.head[z_cord] + scale * (coords_eye[2] - old_eye.head[z_cord])
|
||||
|
||||
new_eye.tail[x_cord] = new_eye.head[x_cord]
|
||||
new_eye.tail[y_cord] = new_eye.head[y_cord]
|
||||
new_eye.tail[z_cord] = new_eye.head[z_cord] + 0.1
|
||||
|
||||
def repair_shapekeys(mesh_name, vertex_group):
|
||||
"""Fix VRC shape keys by slightly adjusting vertex positions"""
|
||||
armature = get_active_armature(bpy.context)
|
||||
mesh = bpy.data.objects[mesh_name]
|
||||
mesh.select_set(True)
|
||||
bpy.context.view_layer.objects.active = mesh
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh.data)
|
||||
bm.verts.ensure_lookup_table()
|
||||
|
||||
logger.debug(f'Processing vertex group: {vertex_group}')
|
||||
group = mesh.vertex_groups.get(vertex_group)
|
||||
if group is None:
|
||||
logger.warning(f'Group {vertex_group} not found, using fallback method')
|
||||
repair_shapekeys_mouth(mesh_name)
|
||||
return
|
||||
|
||||
vcoords = None
|
||||
gi = group.index
|
||||
for v in mesh.data.vertices:
|
||||
for g in v.groups:
|
||||
if g.group == gi:
|
||||
vcoords = v.co.xyz
|
||||
|
||||
if not vcoords:
|
||||
return
|
||||
|
||||
logger.info('Repairing shape keys')
|
||||
moved = False
|
||||
i = 0
|
||||
for key in bm.verts.layers.shape.keys():
|
||||
if not key.startswith('vrc.'):
|
||||
continue
|
||||
logger.debug(f'Repairing shape: {key}')
|
||||
value = bm.verts.layers.shape.get(key)
|
||||
for index, vert in enumerate(bm.verts):
|
||||
if vert.co.xyz == vcoords:
|
||||
if index < i:
|
||||
continue
|
||||
shapekey = vert
|
||||
shapekey_coords = mesh.matrix_world @ shapekey[value]
|
||||
shapekey_coords[0] -= 0.00007 * randBoolNumber()
|
||||
shapekey_coords[1] -= 0.00007 * randBoolNumber()
|
||||
shapekey_coords[2] -= 0.00007 * randBoolNumber()
|
||||
shapekey[value] = mesh.matrix_world.inverted() @ shapekey_coords
|
||||
logger.debug(f'Repaired shape: {key}')
|
||||
i += 1
|
||||
moved = True
|
||||
break
|
||||
|
||||
bm.to_mesh(mesh.data)
|
||||
|
||||
if not moved:
|
||||
logger.warning('Shape key repair failed, using random method')
|
||||
repair_shapekeys_mouth(mesh_name)
|
||||
|
||||
def randBoolNumber():
|
||||
return -1 if random() < 0.5 else 1
|
||||
|
||||
def repair_shapekeys_mouth(mesh_name):
|
||||
mesh = bpy.data.objects[mesh_name]
|
||||
mesh.select_set(True)
|
||||
bpy.context.view_layer.objects.active = mesh
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh.data)
|
||||
bm.verts.ensure_lookup_table()
|
||||
|
||||
moved = False
|
||||
for key in bm.verts.layers.shape.keys():
|
||||
if not key.startswith('vrc'):
|
||||
continue
|
||||
value = bm.verts.layers.shape.get(key)
|
||||
for vert in bm.verts:
|
||||
shapekey = vert
|
||||
shapekey_coords = mesh.matrix_world @ shapekey[value]
|
||||
shapekey_coords[0] -= 0.00007
|
||||
shapekey_coords[1] -= 0.00007
|
||||
shapekey_coords[2] -= 0.00007
|
||||
shapekey[value] = mesh.matrix_world.inverted() @ shapekey_coords
|
||||
moved = True
|
||||
break
|
||||
|
||||
bm.to_mesh(mesh.data)
|
||||
|
||||
if not moved:
|
||||
logger.error('Random shape key repair failed')
|
||||
|
||||
def get_bone_orientations():
|
||||
"""Get bone orientation axes"""
|
||||
return (0, 1, 2) # x, y, z coordinates
|
||||
|
||||
def find_center_vector_of_vertex_group(mesh, group_name):
|
||||
"""Calculate center position of vertex group"""
|
||||
group = mesh.vertex_groups.get(group_name)
|
||||
if not group:
|
||||
return False
|
||||
|
||||
vertices = []
|
||||
for vert in mesh.data.vertices:
|
||||
for g in vert.groups:
|
||||
if g.group == group.index:
|
||||
vertices.append(vert.co)
|
||||
|
||||
if not vertices:
|
||||
return False
|
||||
|
||||
return sum((v for v in vertices), mathutils.Vector()) / len(vertices)
|
||||
|
||||
def vertex_group_exists(mesh_obj, group_name):
|
||||
"""Check if vertex group exists and has weights"""
|
||||
if not mesh_obj or group_name not in mesh_obj.vertex_groups:
|
||||
return False
|
||||
|
||||
group = mesh_obj.vertex_groups[group_name]
|
||||
for vert in mesh_obj.data.vertices:
|
||||
for g in vert.groups:
|
||||
if g.group == group.index and g.weight > 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
def copy_vertex_group(self, vertex_group, rename_to):
|
||||
"""Copy vertex group with new name"""
|
||||
vertex_group_index = 0
|
||||
for group in self.mesh.vertex_groups:
|
||||
if group.name == vertex_group:
|
||||
self.mesh.vertex_groups.active_index = vertex_group_index
|
||||
bpy.ops.object.vertex_group_copy()
|
||||
self.mesh.vertex_groups[vertex_group + '_copy'].name = rename_to
|
||||
break
|
||||
vertex_group_index += 1
|
||||
|
||||
def copy_shape_key(self, context, from_shape, new_names, new_index):
|
||||
"""Copy shape key with new name"""
|
||||
blinking = not context.scene.avatar_toolkit.disable_eye_blinking
|
||||
new_name = new_names[new_index - 1]
|
||||
|
||||
# Rename existing shapekey if it exists
|
||||
for shapekey in self.mesh.data.shape_keys.key_blocks:
|
||||
shapekey.value = 0
|
||||
if shapekey.name == new_name:
|
||||
shapekey.name = shapekey.name + '_old'
|
||||
if from_shape == new_name:
|
||||
from_shape = shapekey.name
|
||||
|
||||
# Create new shape key
|
||||
for index, shapekey in enumerate(self.mesh.data.shape_keys.key_blocks):
|
||||
if from_shape == shapekey.name:
|
||||
self.mesh.active_shape_key_index = index
|
||||
shapekey.value = 1
|
||||
self.mesh.shape_key_add(name=new_name, from_mix=blinking)
|
||||
break
|
||||
|
||||
# Reset shape keys
|
||||
for shapekey in self.mesh.data.shape_keys.key_blocks:
|
||||
shapekey.value = 0
|
||||
self.mesh.active_shape_key_index = 0
|
||||
|
||||
return from_shape
|
||||
|
||||
# Global state for eye tracking
|
||||
eye_left = None
|
||||
eye_right = None
|
||||
eye_left_data = None
|
||||
eye_right_data = None
|
||||
eye_left_rot = []
|
||||
eye_right_rot = []
|
||||
|
||||
class VertexGroupCache:
|
||||
"""Cache for vertex group operations"""
|
||||
_cache = {}
|
||||
|
||||
@classmethod
|
||||
def get_vertex_indices(cls, mesh_name: str, group_name: str) -> Optional[set]:
|
||||
cache_key = f"{mesh_name}_{group_name}"
|
||||
|
||||
if cache_key in cls._cache:
|
||||
return cls._cache[cache_key]
|
||||
|
||||
mesh = bpy.data.objects.get(mesh_name)
|
||||
if not mesh:
|
||||
return None
|
||||
|
||||
group = mesh.vertex_groups.get(group_name)
|
||||
if not group:
|
||||
return None
|
||||
|
||||
indices = {v.index for v in mesh.data.vertices
|
||||
if any(g.group == group.index for g in v.groups)}
|
||||
|
||||
cls._cache[cache_key] = indices
|
||||
return indices
|
||||
|
||||
@classmethod
|
||||
def clear_cache(cls):
|
||||
cls._cache.clear()
|
||||
|
||||
class RotateEyeBonesForAv3Button(Operator):
|
||||
"""Reorient eye bones for proper VRChat eye tracking"""
|
||||
bl_idname = "avatar_toolkit.rotate_eye_bones"
|
||||
bl_label = t("EyeTracking.rotate.label")
|
||||
bl_description = t("EyeTracking.rotate.desc")
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
armature = get_active_armature(context)
|
||||
return armature and all(bone in armature.pose.bones for bone in ['LeftEye', 'RightEye'])
|
||||
|
||||
def execute(self, context):
|
||||
armature = get_active_armature(context)
|
||||
straight_up_matrix = mathutils.Matrix.Rotation(math.pi/2, 3, 'X')
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
for eye_name in ['LeftEye', 'RightEye']:
|
||||
eye_bone = armature.data.edit_bones[eye_name]
|
||||
new_matrix = straight_up_matrix.to_4x4()
|
||||
new_matrix.translation = eye_bone.matrix.translation
|
||||
eye_bone.matrix = new_matrix
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
return {'FINISHED'}
|
||||
|
||||
class ResetEyeTrackingButton(Operator):
|
||||
"""Reset all eye tracking settings and state"""
|
||||
bl_idname = 'avatar_toolkit.reset_eye_tracking'
|
||||
bl_label = t('EyeTracking.reset.label')
|
||||
bl_description = t('EyeTracking.reset.desc')
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
global eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot
|
||||
eye_left = eye_right = eye_left_data = eye_right_data = None
|
||||
eye_left_rot = eye_right_rot = []
|
||||
context.scene.avatar_toolkit.eye_mode = 'CREATION'
|
||||
return {'FINISHED'}
|
||||
|
||||
def validate_weights(mesh_obj: Object, vertex_group: str) -> bool:
|
||||
"""Validate vertex group weights"""
|
||||
group = mesh_obj.vertex_groups.get(vertex_group)
|
||||
if not group:
|
||||
return False
|
||||
|
||||
for vertex in mesh_obj.data.vertices:
|
||||
for group_element in vertex.groups:
|
||||
if group_element.group == group.index and group_element.weight > 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_eye_bone_names(armature: Object) -> Dict[str, str]:
|
||||
"""Get standardized eye bone names"""
|
||||
eye_bones = {'left': None, 'right': None}
|
||||
|
||||
for bone in armature.data.bones:
|
||||
if any(name.lower() in bone.name.lower() for name in VALID_EYE_NAMES['left']):
|
||||
eye_bones['left'] = bone.name
|
||||
if any(name.lower() in bone.name.lower() for name in VALID_EYE_NAMES['right']):
|
||||
eye_bones['right'] = bone.name
|
||||
|
||||
return eye_bones
|
||||
|
||||
def stop_testing(context: Context) -> None:
|
||||
"""Stop eye tracking testing mode"""
|
||||
global eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot
|
||||
|
||||
if not all([eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot]):
|
||||
return
|
||||
|
||||
armature = get_active_armature(context)
|
||||
if not armature:
|
||||
return
|
||||
|
||||
bpy.ops.object.mode_set(mode='POSE')
|
||||
|
||||
# Reset rotations
|
||||
context.scene.avatar_toolkit.eye_rotation_x = 0
|
||||
context.scene.avatar_toolkit.eye_rotation_y = 0
|
||||
|
||||
# Clear transforms
|
||||
for bone in armature.data.bones:
|
||||
bone.hide = False
|
||||
bone.select = True
|
||||
bpy.ops.pose.transforms_clear()
|
||||
|
||||
# Reset shape keys
|
||||
mesh = bpy.data.objects.get(context.scene.avatar_toolkit.mesh_name_eye)
|
||||
if mesh and mesh.data.shape_keys:
|
||||
for shape_key in mesh.data.shape_keys.key_blocks:
|
||||
shape_key.value = 0
|
||||
|
||||
# Clear globals
|
||||
eye_left = eye_right = eye_left_data = eye_right_data = None
|
||||
eye_left_rot = eye_right_rot = []
|
||||
Reference in New Issue
Block a user