Added Eye tracking and Visemes
This commit is contained in:
@@ -485,3 +485,94 @@ def remove_unused_shapekeys(mesh_obj: Object, tolerance: float = 0.001) -> int:
|
|||||||
removed_count += 1
|
removed_count += 1
|
||||||
|
|
||||||
return removed_count
|
return removed_count
|
||||||
|
|
||||||
|
def has_shapekeys(mesh_obj: Object) -> bool:
|
||||||
|
return mesh_obj.data.shape_keys is not None
|
||||||
|
|
||||||
|
# Identifier to indicate that an EnumProperty is empty
|
||||||
|
# This is the default identifier used when a wrapped items function returns an empty list
|
||||||
|
# This identifier needs to be something that should never normally be used, so as to avoid the possibility of
|
||||||
|
# conflicting with an enum value that exists.
|
||||||
|
_empty_enum_identifier = 'Cats_empty_enum_identifier'
|
||||||
|
|
||||||
|
# names - The first object will be the first one in the list. So the first one has to be the one that exists in the most models
|
||||||
|
# no_basis - If this is true the Basis will not be available in the list
|
||||||
|
def get_shapekeys(context, names, is_mouth, no_basis, return_list):
|
||||||
|
choices = []
|
||||||
|
choices_simple = []
|
||||||
|
meshes_list = get_meshes_objects(check=False)
|
||||||
|
|
||||||
|
if meshes_list:
|
||||||
|
if is_mouth:
|
||||||
|
meshes = [get_objects().get(context.scene.mesh_name_viseme)]
|
||||||
|
else:
|
||||||
|
meshes = [get_objects().get(context.scene.mesh_name_eye)]
|
||||||
|
else:
|
||||||
|
return choices
|
||||||
|
|
||||||
|
for mesh in meshes:
|
||||||
|
if not mesh or not has_shapekeys(mesh):
|
||||||
|
return choices
|
||||||
|
|
||||||
|
for shapekey in mesh.data.shape_keys.key_blocks:
|
||||||
|
name = shapekey.name
|
||||||
|
if name in choices_simple:
|
||||||
|
continue
|
||||||
|
if no_basis and name == 'Basis':
|
||||||
|
continue
|
||||||
|
# 1. Will be returned by context.scene
|
||||||
|
# 2. Will be shown in lists
|
||||||
|
# 3. will be shown in the hover description (below description)
|
||||||
|
choices.append((name, name, name))
|
||||||
|
choices_simple.append(name)
|
||||||
|
|
||||||
|
_sort_enum_choices_by_identifier_lower(choices)
|
||||||
|
|
||||||
|
choices2 = []
|
||||||
|
for name in names:
|
||||||
|
if name in choices_simple and len(choices) > 1 and choices[0][0] != name:
|
||||||
|
continue
|
||||||
|
choices2.append((name, name, name))
|
||||||
|
|
||||||
|
choices2.extend(choices)
|
||||||
|
|
||||||
|
if return_list:
|
||||||
|
shape_list = []
|
||||||
|
for choice in choices2:
|
||||||
|
shape_list.append(choice[0])
|
||||||
|
return shape_list
|
||||||
|
|
||||||
|
return choices2
|
||||||
|
|
||||||
|
# Default sorting for dynamic EnumProperty items
|
||||||
|
def _sort_enum_choices_by_identifier_lower(choices, in_place=True):
|
||||||
|
"""Sort a list of enum choices (items) by the lowercase of their identifier.
|
||||||
|
|
||||||
|
Sorting is performed in-place by default, but can be changed by setting in_place=False.
|
||||||
|
|
||||||
|
Returns the sorted list of enum choices."""
|
||||||
|
|
||||||
|
def identifier_lower(choice):
|
||||||
|
return choice[0].lower()
|
||||||
|
|
||||||
|
if in_place:
|
||||||
|
choices.sort(key=identifier_lower)
|
||||||
|
else:
|
||||||
|
choices = sorted(choices, key=identifier_lower)
|
||||||
|
return choices
|
||||||
|
|
||||||
|
def is_enum_empty(string):
|
||||||
|
"""Returns True only if the tested string is the string that signifies that an EnumProperty is empty.
|
||||||
|
|
||||||
|
Returns False in all other cases."""
|
||||||
|
return _empty_enum_identifier == string
|
||||||
|
|
||||||
|
|
||||||
|
# This function isn't needed since you can 'not is_enum_empty(string)', but is included for code clarity and readability
|
||||||
|
def is_enum_non_empty(string):
|
||||||
|
"""Returns False only if the tested string is not the string that signifies that an EnumProperty is empty.
|
||||||
|
|
||||||
|
Returns True in all other cases."""
|
||||||
|
return _empty_enum_identifier != string
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ from .translations import t, get_languages_list, update_language
|
|||||||
from .addon_preferences import get_preference, save_preference
|
from .addon_preferences import get_preference, save_preference
|
||||||
from .updater import get_version_list
|
from .updater import get_version_list
|
||||||
from .common import get_armature_list, get_active_armature, get_all_meshes
|
from .common import get_armature_list, get_active_armature, get_all_meshes
|
||||||
|
from ..functions.visemes import VisemePreview
|
||||||
|
from ..functions.eye_tracking import set_rotation
|
||||||
|
|
||||||
def update_validation_mode(self, context):
|
def update_validation_mode(self, context):
|
||||||
logger.info(f"Updating validation mode to: {self.validation_mode}")
|
logger.info(f"Updating validation mode to: {self.validation_mode}")
|
||||||
@@ -26,6 +28,11 @@ def update_logging_state(self, context):
|
|||||||
from .logging_setup import configure_logging
|
from .logging_setup import configure_logging
|
||||||
configure_logging(self.enable_logging)
|
configure_logging(self.enable_logging)
|
||||||
|
|
||||||
|
def update_shape_intensity(self, context):
|
||||||
|
if self.viseme_preview_mode:
|
||||||
|
from ..functions.visemes import VisemePreview
|
||||||
|
VisemePreview.update_preview(context)
|
||||||
|
|
||||||
class AvatarToolkitSceneProperties(PropertyGroup):
|
class AvatarToolkitSceneProperties(PropertyGroup):
|
||||||
"""Property group containing Avatar Toolkit scene-level settings and properties"""
|
"""Property group containing Avatar Toolkit scene-level settings and properties"""
|
||||||
|
|
||||||
@@ -112,6 +119,168 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
max=1.0
|
max=1.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
viseme_preview_mode: BoolProperty(
|
||||||
|
name=t("Visemes.preview_mode"),
|
||||||
|
description=t("Visemes.preview_mode_desc"),
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
viseme_preview_selection: StringProperty(
|
||||||
|
name=t("Visemes.preview_selection"),
|
||||||
|
description=t("Visemes.preview_selection_desc"),
|
||||||
|
default="vrc.v_aa"
|
||||||
|
)
|
||||||
|
|
||||||
|
mouth_a: StringProperty(
|
||||||
|
name=t("Visemes.mouth_a"),
|
||||||
|
description=t("Visemes.mouth_a_desc")
|
||||||
|
)
|
||||||
|
|
||||||
|
mouth_o: StringProperty(
|
||||||
|
name=t("Visemes.mouth_o"),
|
||||||
|
description=t("Visemes.mouth_o_desc")
|
||||||
|
)
|
||||||
|
|
||||||
|
mouth_ch: StringProperty(
|
||||||
|
name=t("Visemes.mouth_ch"),
|
||||||
|
description=t("Visemes.mouth_ch_desc")
|
||||||
|
)
|
||||||
|
|
||||||
|
shape_intensity: FloatProperty(
|
||||||
|
name=t("Visemes.shape_intensity"),
|
||||||
|
description=t("Visemes.shape_intensity_desc"),
|
||||||
|
default=1.0,
|
||||||
|
min=0.0,
|
||||||
|
max=2.0,
|
||||||
|
precision=3,
|
||||||
|
update=update_shape_intensity
|
||||||
|
)
|
||||||
|
|
||||||
|
viseme_preview_selection: EnumProperty(
|
||||||
|
name=t("Visemes.preview_selection"),
|
||||||
|
description=t("Visemes.preview_selection_desc"),
|
||||||
|
items=[
|
||||||
|
('vrc.v_aa', 'AA', 'A as in "bat"'),
|
||||||
|
('vrc.v_ch', 'CH', 'Ch as in "choose"'),
|
||||||
|
('vrc.v_dd', 'DD', 'D as in "dog"'),
|
||||||
|
('vrc.v_ih', 'IH', 'I as in "bit"'),
|
||||||
|
('vrc.v_ff', 'FF', 'F as in "fox"'),
|
||||||
|
('vrc.v_e', 'E', 'E as in "bet"'),
|
||||||
|
('vrc.v_kk', 'KK', 'K as in "cat"'),
|
||||||
|
('vrc.v_nn', 'NN', 'N as in "net"'),
|
||||||
|
('vrc.v_oh', 'OH', 'O as in "hot"'),
|
||||||
|
('vrc.v_ou', 'OU', 'O as in "go"'),
|
||||||
|
('vrc.v_pp', 'PP', 'P as in "pat"'),
|
||||||
|
('vrc.v_rr', 'RR', 'R as in "red"'),
|
||||||
|
('vrc.v_sil', 'SIL', 'Silence'),
|
||||||
|
('vrc.v_ss', 'SS', 'S as in "sit"'),
|
||||||
|
('vrc.v_th', 'TH', 'Th as in "think"')
|
||||||
|
],
|
||||||
|
update=lambda s, c: VisemePreview.update_preview(c)
|
||||||
|
)
|
||||||
|
eye_mode: EnumProperty(
|
||||||
|
name=t("EyeTracking.mode"),
|
||||||
|
items=[
|
||||||
|
('CREATION', t("EyeTracking.mode.creation"), ""),
|
||||||
|
('TESTING', t("EyeTracking.mode.testing"), "")
|
||||||
|
],
|
||||||
|
default='CREATION'
|
||||||
|
)
|
||||||
|
|
||||||
|
eye_rotation_x: FloatProperty(
|
||||||
|
name=t("EyeTracking.rotation.x"),
|
||||||
|
update=set_rotation
|
||||||
|
)
|
||||||
|
|
||||||
|
eye_rotation_y: FloatProperty(
|
||||||
|
name=t("EyeTracking.rotation.y"),
|
||||||
|
update=set_rotation
|
||||||
|
)
|
||||||
|
|
||||||
|
mesh_name_eye: StringProperty(
|
||||||
|
name=t("EyeTracking.mesh_name"),
|
||||||
|
description=t("EyeTracking.mesh_name_desc")
|
||||||
|
)
|
||||||
|
|
||||||
|
head: StringProperty(
|
||||||
|
name=t("EyeTracking.head_bone"),
|
||||||
|
description=t("EyeTracking.head_bone_desc")
|
||||||
|
)
|
||||||
|
|
||||||
|
eye_left: StringProperty(
|
||||||
|
name=t("EyeTracking.eye_left"),
|
||||||
|
description=t("EyeTracking.eye_left_desc")
|
||||||
|
)
|
||||||
|
|
||||||
|
eye_right: StringProperty(
|
||||||
|
name=t("EyeTracking.eye_right"),
|
||||||
|
description=t("EyeTracking.eye_right_desc")
|
||||||
|
)
|
||||||
|
|
||||||
|
disable_eye_movement: BoolProperty(
|
||||||
|
name=t("EyeTracking.disable_movement"),
|
||||||
|
description=t("EyeTracking.disable_movement_desc"),
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
disable_eye_blinking: BoolProperty(
|
||||||
|
name=t("EyeTracking.disable_blinking"),
|
||||||
|
description=t("EyeTracking.disable_blinking_desc"),
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
eye_distance: FloatProperty(
|
||||||
|
name=t("EyeTracking.distance"),
|
||||||
|
description=t("EyeTracking.distance_desc"),
|
||||||
|
default=0.0,
|
||||||
|
min=-1.0,
|
||||||
|
max=1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
iris_height: FloatProperty(
|
||||||
|
name=t("EyeTracking.iris_height"),
|
||||||
|
description=t("EyeTracking.iris_height_desc"),
|
||||||
|
default=0.0,
|
||||||
|
min=-1.0,
|
||||||
|
max=1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
eye_blink_shape: FloatProperty(
|
||||||
|
name=t("EyeTracking.blink_shape"),
|
||||||
|
description=t("EyeTracking.blink_shape_desc"),
|
||||||
|
default=1.0,
|
||||||
|
min=0.0,
|
||||||
|
max=1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
eye_lowerlid_shape: FloatProperty(
|
||||||
|
name=t("EyeTracking.lowerlid_shape"),
|
||||||
|
description=t("EyeTracking.lowerlid_shape_desc"),
|
||||||
|
default=1.0,
|
||||||
|
min=0.0,
|
||||||
|
max=1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
wink_left: StringProperty(
|
||||||
|
name=t("EyeTracking.wink_left"),
|
||||||
|
description=t("EyeTracking.wink_left_desc")
|
||||||
|
)
|
||||||
|
|
||||||
|
wink_right: StringProperty(
|
||||||
|
name=t("EyeTracking.wink_right"),
|
||||||
|
description=t("EyeTracking.wink_right_desc")
|
||||||
|
)
|
||||||
|
|
||||||
|
lowerlid_left: StringProperty(
|
||||||
|
name=t("EyeTracking.lowerlid_left"),
|
||||||
|
description=t("EyeTracking.lowerlid_left_desc")
|
||||||
|
)
|
||||||
|
|
||||||
|
lowerlid_right: StringProperty(
|
||||||
|
name=t("EyeTracking.lowerlid_right"),
|
||||||
|
description=t("EyeTracking.lowerlid_right_desc")
|
||||||
|
)
|
||||||
|
|
||||||
def register() -> None:
|
def register() -> None:
|
||||||
"""Register the Avatar Toolkit property group"""
|
"""Register the Avatar Toolkit property group"""
|
||||||
logger.info("Registering Avatar Toolkit properties")
|
logger.info("Registering Avatar Toolkit properties")
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ class AvatarToolkit_PT_UpdaterPanel(bpy.types.Panel):
|
|||||||
bl_category = CATEGORY_NAME
|
bl_category = CATEGORY_NAME
|
||||||
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
||||||
bl_order = 4
|
bl_order = 4
|
||||||
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
def draw(self, context: bpy.types.Context) -> None:
|
def draw(self, context: bpy.types.Context) -> None:
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
|||||||
@@ -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 = []
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
# MIT License
|
||||||
|
# This code was taken from Cats Blender Plugin Unoffical, some of this code is by the original developers, however was improved by myself.
|
||||||
|
# Didn't think it was necessary to re-make something that works well.
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from typing import Dict, List, Optional, Tuple, Any, Set
|
||||||
|
from bpy.types import Operator, Context, Object, ShapeKey
|
||||||
|
from collections import OrderedDict
|
||||||
|
from ..core.logging_setup import logger
|
||||||
|
from ..core.translations import t
|
||||||
|
from ..core.common import (
|
||||||
|
get_active_armature,
|
||||||
|
validate_armature,
|
||||||
|
get_all_meshes,
|
||||||
|
validate_mesh_for_pose
|
||||||
|
)
|
||||||
|
|
||||||
|
class VisemeCache:
|
||||||
|
"""Caches generated viseme shape data"""
|
||||||
|
_cache: Dict = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_cached_shape(cls, key: str, mix_data: List) -> Optional[List]:
|
||||||
|
cache_key = (key, tuple(tuple(x) for x in mix_data))
|
||||||
|
return cls._cache.get(cache_key)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def cache_shape(cls, key: str, mix_data: List, shape_data: List) -> None:
|
||||||
|
cache_key = (key, tuple(tuple(x) for x in mix_data))
|
||||||
|
cls._cache[cache_key] = shape_data
|
||||||
|
|
||||||
|
class VisemePreview:
|
||||||
|
"""Handles viseme preview functionality"""
|
||||||
|
_preview_data: Dict = {}
|
||||||
|
_active: bool = False
|
||||||
|
_preview_shapes: Optional[OrderedDict] = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def start_preview(cls, context: Context, mesh: Object, shapes: List[str]) -> bool:
|
||||||
|
if not mesh or not mesh.data or not mesh.data.shape_keys:
|
||||||
|
return False
|
||||||
|
|
||||||
|
cls._active = True
|
||||||
|
cls._preview_data = {}
|
||||||
|
|
||||||
|
# Store original values
|
||||||
|
for shape_key in mesh.data.shape_keys.key_blocks:
|
||||||
|
cls._preview_data[shape_key.name] = shape_key.value
|
||||||
|
|
||||||
|
# Get properties from avatar_toolkit
|
||||||
|
props = context.scene.avatar_toolkit
|
||||||
|
shape_a = props.mouth_a
|
||||||
|
shape_o = props.mouth_o
|
||||||
|
shape_ch = props.mouth_ch
|
||||||
|
|
||||||
|
|
||||||
|
cls._preview_shapes = OrderedDict()
|
||||||
|
cls._preview_shapes['vrc.v_aa'] = {'mix': [[(shape_a), (0.9998)]]}
|
||||||
|
cls._preview_shapes['vrc.v_ch'] = {'mix': [[(shape_ch), (0.9996)]]}
|
||||||
|
cls._preview_shapes['vrc.v_dd'] = {'mix': [[(shape_a), (0.3)], [(shape_ch), (0.7)]]}
|
||||||
|
cls._preview_shapes['vrc.v_ih'] = {'mix': [[(shape_ch), (0.7)], [(shape_o), (0.3)]]}
|
||||||
|
cls._preview_shapes['vrc.v_ff'] = {'mix': [[(shape_a), (0.2)], [(shape_ch), (0.4)]]}
|
||||||
|
cls._preview_shapes['vrc.v_e'] = {'mix': [[(shape_a), (0.5)], [(shape_ch), (0.2)]]}
|
||||||
|
cls._preview_shapes['vrc.v_kk'] = {'mix': [[(shape_a), (0.7)], [(shape_ch), (0.4)]]}
|
||||||
|
cls._preview_shapes['vrc.v_nn'] = {'mix': [[(shape_a), (0.2)], [(shape_ch), (0.7)]]}
|
||||||
|
cls._preview_shapes['vrc.v_oh'] = {'mix': [[(shape_a), (0.2)], [(shape_o), (0.8)]]}
|
||||||
|
cls._preview_shapes['vrc.v_ou'] = {'mix': [[(shape_o), (0.9994)]]}
|
||||||
|
cls._preview_shapes['vrc.v_pp'] = {'mix': [[(shape_a), (0.0004)], [(shape_o), (0.0004)]]}
|
||||||
|
cls._preview_shapes['vrc.v_rr'] = {'mix': [[(shape_ch), (0.5)], [(shape_o), (0.3)]]}
|
||||||
|
cls._preview_shapes['vrc.v_sil'] = {'mix': [[(shape_a), (0.0002)], [(shape_ch), (0.0002)]]}
|
||||||
|
cls._preview_shapes['vrc.v_ss'] = {'mix': [[(shape_ch), (0.8)]]}
|
||||||
|
cls._preview_shapes['vrc.v_th'] = {'mix': [[(shape_a), (0.4)], [(shape_o), (0.15)]]}
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update_preview(cls, context: Context) -> None:
|
||||||
|
if not cls._active or not cls._preview_shapes:
|
||||||
|
return
|
||||||
|
|
||||||
|
mesh = context.active_object
|
||||||
|
props = context.scene.avatar_toolkit
|
||||||
|
viseme_data = cls._preview_shapes.get(props.viseme_preview_selection)
|
||||||
|
if viseme_data:
|
||||||
|
cls.show_viseme(context, mesh, props.viseme_preview_selection, viseme_data['mix'])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def show_viseme(cls, context: Context, mesh: Object, viseme_name: str, mix_data: List) -> None:
|
||||||
|
if not cls._active:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get shape intensity from properties
|
||||||
|
intensity = context.scene.avatar_toolkit.shape_intensity
|
||||||
|
|
||||||
|
for shape_key in mesh.data.shape_keys.key_blocks:
|
||||||
|
shape_key.value = 0
|
||||||
|
|
||||||
|
for shape_name, value in mix_data:
|
||||||
|
if shape_name in mesh.data.shape_keys.key_blocks:
|
||||||
|
# Apply intensity to the preview value
|
||||||
|
mesh.data.shape_keys.key_blocks[shape_name].value = value * intensity
|
||||||
|
|
||||||
|
context.view_layer.update()
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def end_preview(cls, mesh: Object) -> None:
|
||||||
|
if not cls._active:
|
||||||
|
return
|
||||||
|
|
||||||
|
for shape_name, value in cls._preview_data.items():
|
||||||
|
if shape_name in mesh.data.shape_keys.key_blocks:
|
||||||
|
mesh.data.shape_keys.key_blocks[shape_name].value = value
|
||||||
|
|
||||||
|
cls._active = False
|
||||||
|
cls._preview_data.clear()
|
||||||
|
cls._preview_shapes = None
|
||||||
|
|
||||||
|
class ATOOLKIT_OT_preview_visemes(Operator):
|
||||||
|
bl_idname = "avatar_toolkit.preview_visemes"
|
||||||
|
bl_label = t("Visemes.preview_label")
|
||||||
|
bl_description = t("Visemes.preview_desc")
|
||||||
|
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context: Context) -> bool:
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
if not armature:
|
||||||
|
return False
|
||||||
|
valid, _ = validate_armature(armature)
|
||||||
|
return valid and context.active_object and context.active_object.type == 'MESH'
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
props = context.scene.avatar_toolkit
|
||||||
|
mesh = context.active_object
|
||||||
|
|
||||||
|
if props.viseme_preview_mode:
|
||||||
|
VisemePreview.end_preview(mesh)
|
||||||
|
props.viseme_preview_mode = False
|
||||||
|
else:
|
||||||
|
if not mesh.data.shape_keys:
|
||||||
|
self.report({'ERROR'}, t("Visemes.error.no_shapekeys"))
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
if VisemePreview.start_preview(context, mesh, [props.mouth_a, props.mouth_o, props.mouth_ch]):
|
||||||
|
props.viseme_preview_mode = True
|
||||||
|
props.viseme_preview_selection = 'vrc.v_aa'
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
def validate_deformation(mesh, mix_data):
|
||||||
|
"""Validates if shape key deformations are within reasonable ranges"""
|
||||||
|
base_coords = [v.co.copy() for v in mesh.data.shape_keys.key_blocks['Basis'].data]
|
||||||
|
max_deform = 0
|
||||||
|
|
||||||
|
for shape_data in mix_data:
|
||||||
|
shape_name, value = shape_data
|
||||||
|
if shape_name in mesh.data.shape_keys.key_blocks:
|
||||||
|
shape_key = mesh.data.shape_keys.key_blocks[shape_name]
|
||||||
|
for i, v in enumerate(shape_key.data):
|
||||||
|
deform = (v.co - base_coords[i]).length * value
|
||||||
|
max_deform = max(max_deform, deform)
|
||||||
|
|
||||||
|
mesh_size = max(mesh.dimensions)
|
||||||
|
return max_deform < (mesh_size * 0.4)
|
||||||
|
|
||||||
|
class ATOOLKIT_OT_create_visemes(Operator):
|
||||||
|
bl_idname = "avatar_toolkit.create_visemes"
|
||||||
|
bl_label = t("Visemes.create_label")
|
||||||
|
bl_description = t("Visemes.create_desc")
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context: Context) -> bool:
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
if not armature:
|
||||||
|
return False
|
||||||
|
valid, _ = validate_armature(armature)
|
||||||
|
return valid and context.active_object and context.active_object.type == 'MESH'
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
props = context.scene.avatar_toolkit
|
||||||
|
mesh = context.active_object
|
||||||
|
|
||||||
|
if not mesh.data.shape_keys:
|
||||||
|
self.report({'ERROR'}, t("Visemes.error.no_shapekeys"))
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
if props.mouth_a == "Basis" or props.mouth_o == "Basis" or props.mouth_ch == "Basis":
|
||||||
|
self.report({'ERROR'}, t("Visemes.error.select_shapekeys"))
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.create_visemes(context, mesh)
|
||||||
|
self.report({'INFO'}, t("Visemes.success"))
|
||||||
|
return {'FINISHED'}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error creating visemes: {str(e)}")
|
||||||
|
self.report({'ERROR'}, str(e))
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
def create_visemes(self, context: Context, mesh: Object) -> None:
|
||||||
|
"""Creates viseme shape keys by mixing existing shape keys"""
|
||||||
|
props = context.scene.avatar_toolkit
|
||||||
|
wm = context.window_manager
|
||||||
|
|
||||||
|
# Store original shape key names
|
||||||
|
shapes = [props.mouth_a, props.mouth_o, props.mouth_ch]
|
||||||
|
renamed_shapes = shapes.copy()
|
||||||
|
|
||||||
|
# Temporarily rename selected shapes to avoid conflicts
|
||||||
|
for shapekey in mesh.data.shape_keys.key_blocks:
|
||||||
|
if shapekey.name == props.mouth_a:
|
||||||
|
shapekey.name = f"{shapekey.name}_old"
|
||||||
|
props.mouth_a = shapekey.name
|
||||||
|
renamed_shapes[0] = shapekey.name
|
||||||
|
elif shapekey.name == props.mouth_o:
|
||||||
|
if props.mouth_a != props.mouth_o:
|
||||||
|
shapekey.name = f"{shapekey.name}_old"
|
||||||
|
props.mouth_o = shapekey.name
|
||||||
|
renamed_shapes[1] = shapekey.name
|
||||||
|
elif shapekey.name == props.mouth_ch:
|
||||||
|
if props.mouth_a != props.mouth_ch and props.mouth_o != props.mouth_ch:
|
||||||
|
shapekey.name = f"{shapekey.name}_old"
|
||||||
|
props.mouth_ch = shapekey.name
|
||||||
|
renamed_shapes[2] = shapekey.name
|
||||||
|
|
||||||
|
# Define viseme shape key data
|
||||||
|
shapekey_data = OrderedDict()
|
||||||
|
shapekey_data['vrc.v_aa'] = {'mix': [[(props.mouth_a), (0.9998)]]}
|
||||||
|
shapekey_data['vrc.v_ch'] = {'mix': [[(props.mouth_ch), (0.9996)]]}
|
||||||
|
shapekey_data['vrc.v_dd'] = {'mix': [[(props.mouth_a), (0.3)], [(props.mouth_ch), (0.7)]]}
|
||||||
|
shapekey_data['vrc.v_ih'] = {'mix': [[(props.mouth_ch), (0.7)], [(props.mouth_o), (0.3)]]}
|
||||||
|
shapekey_data['vrc.v_ff'] = {'mix': [[(props.mouth_a), (0.2)], [(props.mouth_ch), (0.4)]]}
|
||||||
|
shapekey_data['vrc.v_e'] = {'mix': [[(props.mouth_a), (0.5)], [(props.mouth_ch), (0.2)]]}
|
||||||
|
shapekey_data['vrc.v_kk'] = {'mix': [[(props.mouth_a), (0.7)], [(props.mouth_ch), (0.4)]]}
|
||||||
|
shapekey_data['vrc.v_nn'] = {'mix': [[(props.mouth_a), (0.2)], [(props.mouth_ch), (0.7)]]}
|
||||||
|
shapekey_data['vrc.v_oh'] = {'mix': [[(props.mouth_a), (0.2)], [(props.mouth_o), (0.8)]]}
|
||||||
|
shapekey_data['vrc.v_ou'] = {'mix': [[(props.mouth_o), (0.9994)]]}
|
||||||
|
shapekey_data['vrc.v_pp'] = {'mix': [[(props.mouth_a), (0.0004)], [(props.mouth_o), (0.0004)]]}
|
||||||
|
shapekey_data['vrc.v_rr'] = {'mix': [[(props.mouth_ch), (0.5)], [(props.mouth_o), (0.3)]]}
|
||||||
|
shapekey_data['vrc.v_sil'] = {'mix': [[(props.mouth_a), (0.0002)], [(props.mouth_ch), (0.0002)]]}
|
||||||
|
shapekey_data['vrc.v_ss'] = {'mix': [[(props.mouth_ch), (0.8)]]}
|
||||||
|
shapekey_data['vrc.v_th'] = {'mix': [[(props.mouth_a), (0.4)], [(props.mouth_o), (0.15)]]}
|
||||||
|
|
||||||
|
# Create progress tracker
|
||||||
|
total_steps = len(shapekey_data)
|
||||||
|
wm.progress_begin(0, total_steps)
|
||||||
|
|
||||||
|
# Create viseme shape keys
|
||||||
|
for index, (key, data) in enumerate(shapekey_data.items()):
|
||||||
|
wm.progress_update(index)
|
||||||
|
|
||||||
|
# Check cache first
|
||||||
|
cached_data = VisemeCache.get_cached_shape(key, data['mix'])
|
||||||
|
if cached_data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Create new shape key
|
||||||
|
self.mix_shapekey(context, renamed_shapes, data['mix'], key)
|
||||||
|
|
||||||
|
# Cache the new shape key data
|
||||||
|
shape_data = [v.co.copy() for v in mesh.data.shape_keys.key_blocks[key].data]
|
||||||
|
VisemeCache.cache_shape(key, data['mix'], shape_data)
|
||||||
|
|
||||||
|
# Restore original shape key names
|
||||||
|
self.restore_shape_names(context, mesh, shapes, renamed_shapes)
|
||||||
|
|
||||||
|
# Cleanup and finalize
|
||||||
|
mesh.active_shape_key_index = 0
|
||||||
|
wm.progress_end()
|
||||||
|
|
||||||
|
def mix_shapekey(self, context: Context, shapes: List[str], mix_data: List, new_name: str) -> None:
|
||||||
|
"""Creates a new shape key by mixing existing ones"""
|
||||||
|
mesh = context.active_object
|
||||||
|
|
||||||
|
# Remove existing shape key if it exists
|
||||||
|
if new_name in mesh.data.shape_keys.key_blocks:
|
||||||
|
mesh.active_shape_key_index = mesh.data.shape_keys.key_blocks.find(new_name)
|
||||||
|
bpy.ops.object.shape_key_remove()
|
||||||
|
|
||||||
|
# Reset all shape keys
|
||||||
|
for shapekey in mesh.data.shape_keys.key_blocks:
|
||||||
|
shapekey.value = 0
|
||||||
|
|
||||||
|
# Set mix values
|
||||||
|
for shape_name, value in mix_data:
|
||||||
|
if shape_name in mesh.data.shape_keys.key_blocks:
|
||||||
|
shapekey = mesh.data.shape_keys.key_blocks[shape_name]
|
||||||
|
shapekey.value = value
|
||||||
|
|
||||||
|
# Create mixed shape key
|
||||||
|
mesh.shape_key_add(name=new_name, from_mix=True)
|
||||||
|
|
||||||
|
# Reset values and restore shape key settings
|
||||||
|
for shapekey in mesh.data.shape_keys.key_blocks:
|
||||||
|
shapekey.value = 0
|
||||||
|
if shapekey.name in shapes:
|
||||||
|
shapekey.slider_max = 1
|
||||||
|
|
||||||
|
def restore_shape_names(self, context: Context, mesh: Object, original_names: List[str], current_names: List[str]) -> None:
|
||||||
|
"""Restores original shape key names"""
|
||||||
|
props = context.scene.avatar_toolkit
|
||||||
|
|
||||||
|
# Restore mouth_a
|
||||||
|
if original_names[0] not in mesh.data.shape_keys.key_blocks:
|
||||||
|
shapekey = mesh.data.shape_keys.key_blocks.get(current_names[0])
|
||||||
|
if shapekey:
|
||||||
|
shapekey.name = original_names[0]
|
||||||
|
if current_names[2] == current_names[0]:
|
||||||
|
current_names[2] = original_names[0]
|
||||||
|
if current_names[1] == current_names[0]:
|
||||||
|
current_names[1] = original_names[0]
|
||||||
|
current_names[0] = original_names[0]
|
||||||
|
|
||||||
|
# Restore mouth_o
|
||||||
|
if original_names[1] not in mesh.data.shape_keys.key_blocks:
|
||||||
|
shapekey = mesh.data.shape_keys.key_blocks.get(current_names[1])
|
||||||
|
if shapekey:
|
||||||
|
shapekey.name = original_names[1]
|
||||||
|
if current_names[2] == current_names[1]:
|
||||||
|
current_names[2] = original_names[1]
|
||||||
|
current_names[1] = original_names[1]
|
||||||
|
|
||||||
|
# Restore mouth_ch
|
||||||
|
if original_names[2] not in mesh.data.shape_keys.key_blocks:
|
||||||
|
shapekey = mesh.data.shape_keys.key_blocks.get(current_names[2])
|
||||||
|
if shapekey:
|
||||||
|
shapekey.name = original_names[2]
|
||||||
|
current_names[2] = original_names[2]
|
||||||
|
|
||||||
|
# Update properties
|
||||||
|
props.mouth_a = current_names[0]
|
||||||
|
props.mouth_o = current_names[1]
|
||||||
|
props.mouth_ch = current_names[2]
|
||||||
@@ -207,6 +207,103 @@
|
|||||||
"MMD.connect_bones": "Connect Bones",
|
"MMD.connect_bones": "Connect Bones",
|
||||||
"MMD.connect_bones_desc": "Connect bones in chain where appropriate",
|
"MMD.connect_bones_desc": "Connect bones in chain where appropriate",
|
||||||
|
|
||||||
|
"Visemes.panel_label": "Visemes",
|
||||||
|
"Visemes.shape_selection": "Shape Key Selection",
|
||||||
|
"Visemes.controls": "Viseme Controls",
|
||||||
|
"Visemes.no_shapekeys": "Select a mesh with shape keys",
|
||||||
|
"Visemes.mouth_a": "A Shape",
|
||||||
|
"Visemes.mouth_a_desc": "Shape key for 'A' sound",
|
||||||
|
"Visemes.mouth_o": "O Shape",
|
||||||
|
"Visemes.mouth_o_desc": "Shape key for 'O' sound",
|
||||||
|
"Visemes.mouth_ch": "CH Shape",
|
||||||
|
"Visemes.mouth_ch_desc": "Shape key for 'CH' sound",
|
||||||
|
"Visemes.shape_intensity": "Shape Intensity",
|
||||||
|
"Visemes.shape_intensity_desc": "Intensity multiplier for viseme shapes",
|
||||||
|
"Visemes.start_preview": "Start Preview",
|
||||||
|
"Visemes.stop_preview": "Stop Preview",
|
||||||
|
"Visemes.preview_mode_desc": "Toggle viseme preview mode",
|
||||||
|
"Visemes.preview_selection": "Preview Selection",
|
||||||
|
"Visemes.preview_selection_desc": "Select viseme to preview",
|
||||||
|
"Visemes.preview_label": "Preview Visemes",
|
||||||
|
"Visemes.preview_desc": "Preview viseme shapes in viewport",
|
||||||
|
"Visemes.create_label": "Create Visemes",
|
||||||
|
"Visemes.create_desc": "Create VRC viseme shape keys",
|
||||||
|
"Visemes.error.no_shapekeys": "Mesh has no shape keys",
|
||||||
|
"Visemes.error.select_shapekeys": "Please select shape keys for A, O and CH",
|
||||||
|
"Visemes.success": "Visemes created successfully",
|
||||||
|
|
||||||
|
"EyeTracking.label": "Eye Tracking",
|
||||||
|
"EyeTracking.setup": "Eye Tracking Setup",
|
||||||
|
"EyeTracking.mesh_select": "Mesh Selection",
|
||||||
|
"EyeTracking.bones": "Bone Selection",
|
||||||
|
"EyeTracking.head_bone": "Head Bone",
|
||||||
|
"EyeTracking.eye_left": "Left Eye Bone",
|
||||||
|
"EyeTracking.eye_right": "Right Eye Bone",
|
||||||
|
"EyeTracking.shapekeys": "Shape Key Selection",
|
||||||
|
"EyeTracking.options": "Options",
|
||||||
|
"EyeTracking.rotation": "Eye Rotation",
|
||||||
|
"EyeTracking.rotation.x": "Vertical Rotation",
|
||||||
|
"EyeTracking.rotation.y": "Horizontal Rotation",
|
||||||
|
"EyeTracking.adjust": "Eye Adjustments",
|
||||||
|
"EyeTracking.blinking": "Blinking Controls",
|
||||||
|
"EyeTracking.no_shapekeys": "No shape keys found on selected mesh",
|
||||||
|
"EyeTracking.no_armature": "No armature selected",
|
||||||
|
"EyeTracking.no_mesh": "No mesh found",
|
||||||
|
"EyeTracking.create.label": "Create Eye Tracking",
|
||||||
|
"EyeTracking.create.desc": "Set up eye tracking bones and shape keys",
|
||||||
|
"EyeTracking.testing.start.label": "Start Testing",
|
||||||
|
"EyeTracking.testing.start.desc": "Enter eye tracking test mode",
|
||||||
|
"EyeTracking.testing.stop.label": "Stop Testing",
|
||||||
|
"EyeTracking.testing.stop.desc": "Exit eye tracking test mode",
|
||||||
|
"EyeTracking.reset.label": "Reset Eye Tracking",
|
||||||
|
"EyeTracking.reset.desc": "Reset all eye tracking settings",
|
||||||
|
"EyeTracking.rotate.label": "Rotate Eye Bones",
|
||||||
|
"EyeTracking.rotate.desc": "Rotate eye bones for VRChat compatibility",
|
||||||
|
"EyeTracking.iris.label": "Adjust Iris Height",
|
||||||
|
"EyeTracking.iris.desc": "Adjust the height of iris vertices",
|
||||||
|
"EyeTracking.blink.test.label": "Test Blink",
|
||||||
|
"EyeTracking.blink.test.desc": "Test eye blinking shape keys",
|
||||||
|
"EyeTracking.lowerlid.test.label": "Test Lower Lid",
|
||||||
|
"EyeTracking.lowerlid.test.desc": "Test lower lid shape keys",
|
||||||
|
"EyeTracking.blink.reset.label": "Reset Blink Test",
|
||||||
|
"EyeTracking.blink.reset.desc": "Reset blink testing values",
|
||||||
|
"EyeTracking.validation.noArmature": "No armature found in scene",
|
||||||
|
"EyeTracking.validation.noMesh": "Mesh '{mesh}' not found",
|
||||||
|
"EyeTracking.validation.noShapekeys": "Selected mesh has no shape keys",
|
||||||
|
"EyeTracking.validation.leftEye": "Left Eye",
|
||||||
|
"EyeTracking.validation.rightEye": "Right Eye",
|
||||||
|
"EyeTracking.validation.missingGroups": "Missing vertex groups: {groups}",
|
||||||
|
"EyeTracking.validation.missingBones": "Missing required bones: {bones}",
|
||||||
|
"EyeTracking.validation.success": "Eye tracking setup validated successfully",
|
||||||
|
"EyeTracking.error.noMesh": "No mesh selected for eye tracking",
|
||||||
|
"EyeTracking.error.noVertexGroup": "No vertex group found for bone: {bone}",
|
||||||
|
"EyeTracking.error.noShapeSelected": "Please select all required shape keys",
|
||||||
|
"EyeTracking.success": "Eye tracking setup completed successfully",
|
||||||
|
"EyeTracking.mode_select": "Mode Selection",
|
||||||
|
"EyeTracking.mesh_setup": "Mesh Setup",
|
||||||
|
"EyeTracking.bone_setup": "Bone Setup",
|
||||||
|
"EyeTracking.shapekey_setup": "Shape Key Setup",
|
||||||
|
"EyeTracking.testing": "Testing Mode",
|
||||||
|
"EyeTracking.rotation_controls": "Eye Rotation Controls",
|
||||||
|
"EyeTracking.adjustments": "Eye Adjustments",
|
||||||
|
"EyeTracking.blink_testing": "Blink Testing",
|
||||||
|
"EyeTracking.wink_left": "Left Wink",
|
||||||
|
"EyeTracking.wink_right": "Right Wink",
|
||||||
|
"EyeTracking.lowerlid_left": "Left Lower Lid",
|
||||||
|
"EyeTracking.lowerlid_right": "Right Lower Lid",
|
||||||
|
"EyeTracking.mode.creation": "Creation Mode",
|
||||||
|
"EyeTracking.mode.testing": "Testing Mode",
|
||||||
|
"EyeTracking.disable_blinking": "Disable Eye Blinking",
|
||||||
|
"EyeTracking.disable_movement": "Disable Eye Movement",
|
||||||
|
"EyeTracking.distance": "Eye Distance",
|
||||||
|
"EyeTracking.distance_desc": "Adjust the distance between eyes",
|
||||||
|
"EyeTracking.mode": "Eye Tracking Mode",
|
||||||
|
"EyeTracking.mesh_name": "Mesh",
|
||||||
|
"EyeTracking.mesh_name_desc": "Select mesh for eye tracking",
|
||||||
|
"EyeTracking.head_bone_desc": "Select head bone",
|
||||||
|
"EyeTracking.eye_left_desc": "Select left eye bone",
|
||||||
|
"EyeTracking.eye_right_desc": "Select right eye bone",
|
||||||
|
|
||||||
"Settings.label": "Settings",
|
"Settings.label": "Settings",
|
||||||
"Settings.language": "Language",
|
"Settings.language": "Language",
|
||||||
"Settings.language_desc": "Select interface language",
|
"Settings.language_desc": "Select interface language",
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import bpy
|
||||||
|
from typing import Set
|
||||||
|
from bpy.types import Panel, Context, UILayout, Operator
|
||||||
|
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
||||||
|
from ..core.translations import t
|
||||||
|
from ..core.common import get_active_armature, get_all_meshes
|
||||||
|
from ..functions.eye_tracking import (
|
||||||
|
CreateEyesButton,
|
||||||
|
StartTestingButton,
|
||||||
|
StopTestingButton,
|
||||||
|
ResetRotationButton,
|
||||||
|
AdjustEyesButton,
|
||||||
|
TestBlinking,
|
||||||
|
TestLowerlid,
|
||||||
|
ResetBlinkTest,
|
||||||
|
ResetEyeTrackingButton,
|
||||||
|
RotateEyeBonesForAv3Button
|
||||||
|
)
|
||||||
|
|
||||||
|
class AvatarToolKit_PT_EyeTrackingPanel(Panel):
|
||||||
|
"""Panel containing eye tracking setup and testing tools"""
|
||||||
|
bl_label = t("EyeTracking.label")
|
||||||
|
bl_idname = "VIEW3D_PT_avatar_toolkit_eye_tracking"
|
||||||
|
bl_space_type = 'VIEW_3D'
|
||||||
|
bl_region_type = 'UI'
|
||||||
|
bl_category = CATEGORY_NAME
|
||||||
|
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
||||||
|
bl_order = 3
|
||||||
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
|
def draw(self, context: Context) -> None:
|
||||||
|
"""Draw the eye tracking panel interface"""
|
||||||
|
layout = self.layout
|
||||||
|
toolkit = context.scene.avatar_toolkit
|
||||||
|
|
||||||
|
# Mode Selection Box
|
||||||
|
mode_box = layout.box()
|
||||||
|
col = mode_box.column(align=True)
|
||||||
|
col.label(text=t("EyeTracking.mode_select"), icon='TOOL_SETTINGS')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
col.prop(toolkit, "eye_mode", expand=True)
|
||||||
|
|
||||||
|
if toolkit.eye_mode == 'CREATION':
|
||||||
|
# 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(toolkit, "mesh_name_eye", text="")
|
||||||
|
|
||||||
|
# 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')
|
||||||
|
|
||||||
|
# Shapekey Setup Box
|
||||||
|
shape_box = layout.box()
|
||||||
|
col = shape_box.column(align=True)
|
||||||
|
col.label(text=t("EyeTracking.shapekey_setup"), icon='SHAPEKEY_DATA')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
|
||||||
|
mesh = bpy.data.objects.get(toolkit.mesh_name_eye)
|
||||||
|
if mesh and mesh.data.shape_keys:
|
||||||
|
col.prop_search(toolkit, "wink_left", mesh.data.shape_keys, "key_blocks", text=t("EyeTracking.wink_left"))
|
||||||
|
col.prop_search(toolkit, "wink_right", mesh.data.shape_keys, "key_blocks", text=t("EyeTracking.wink_right"))
|
||||||
|
col.prop_search(toolkit, "lowerlid_left", mesh.data.shape_keys, "key_blocks", text=t("EyeTracking.lowerlid_left"))
|
||||||
|
col.prop_search(toolkit, "lowerlid_right", mesh.data.shape_keys, "key_blocks", text=t("EyeTracking.lowerlid_right"))
|
||||||
|
else:
|
||||||
|
col.label(text=t("EyeTracking.no_shapekeys"), icon='ERROR')
|
||||||
|
|
||||||
|
# Options Box
|
||||||
|
options_box = layout.box()
|
||||||
|
col = options_box.column(align=True)
|
||||||
|
col.label(text=t("EyeTracking.options"), icon='SETTINGS')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
col.prop(toolkit, "disable_eye_blinking")
|
||||||
|
col.prop(toolkit, "disable_eye_movement")
|
||||||
|
if not toolkit.disable_eye_movement:
|
||||||
|
col.prop(toolkit, "eye_distance")
|
||||||
|
|
||||||
|
# Create Button
|
||||||
|
row = layout.row(align=True)
|
||||||
|
row.scale_y = 1.5
|
||||||
|
row.operator(CreateEyesButton.bl_idname, icon='PLAY')
|
||||||
|
|
||||||
|
else:
|
||||||
|
if context.mode != 'POSE':
|
||||||
|
# Testing Start Box
|
||||||
|
test_box = layout.box()
|
||||||
|
col = test_box.column(align=True)
|
||||||
|
col.label(text=t("EyeTracking.testing"), icon='PLAY')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
row = col.row(align=True)
|
||||||
|
row.scale_y = 1.5
|
||||||
|
row.operator(StartTestingButton.bl_idname, icon='PLAY')
|
||||||
|
else:
|
||||||
|
# Eye Rotation Box
|
||||||
|
rotation_box = layout.box()
|
||||||
|
col = rotation_box.column(align=True)
|
||||||
|
col.label(text=t("EyeTracking.rotation_controls"), icon='DRIVER_ROTATIONAL_DIFFERENCE')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
col.prop(toolkit, "eye_rotation_x", text=t("EyeTracking.rotation.x"))
|
||||||
|
col.prop(toolkit, "eye_rotation_y", text=t("EyeTracking.rotation.y"))
|
||||||
|
col.operator(ResetRotationButton.bl_idname, icon='LOOP_BACK')
|
||||||
|
|
||||||
|
# Eye Adjustment Box
|
||||||
|
adjust_box = layout.box()
|
||||||
|
col = adjust_box.column(align=True)
|
||||||
|
col.label(text=t("EyeTracking.adjustments"), icon='MODIFIER')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
col.prop(toolkit, "eye_distance")
|
||||||
|
col.operator(AdjustEyesButton.bl_idname, icon='CON_TRACKTO')
|
||||||
|
|
||||||
|
# Blinking Test Box
|
||||||
|
blink_box = layout.box()
|
||||||
|
col = blink_box.column(align=True)
|
||||||
|
col.label(text=t("EyeTracking.blink_testing"), icon='HIDE_OFF')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
row = col.row(align=True)
|
||||||
|
row.prop(toolkit, "eye_blink_shape")
|
||||||
|
row.operator(TestBlinking.bl_idname, icon='RESTRICT_VIEW_OFF')
|
||||||
|
row = col.row(align=True)
|
||||||
|
row.prop(toolkit, "eye_lowerlid_shape")
|
||||||
|
row.operator(TestLowerlid.bl_idname, icon='RESTRICT_VIEW_OFF')
|
||||||
|
col.operator(ResetBlinkTest.bl_idname, icon='LOOP_BACK')
|
||||||
|
|
||||||
|
# Stop Testing Button
|
||||||
|
row = layout.row(align=True)
|
||||||
|
row.scale_y = 1.5
|
||||||
|
row.operator(StopTestingButton.bl_idname, icon='PAUSE')
|
||||||
|
|
||||||
|
# Reset Button
|
||||||
|
row = layout.row(align=True)
|
||||||
|
row.operator(ResetEyeTrackingButton.bl_idname, icon='FILE_REFRESH')
|
||||||
@@ -31,7 +31,6 @@ class AvatarToolKit_PT_AvatarToolkitPanel(Panel):
|
|||||||
bl_space_type: str = 'VIEW_3D'
|
bl_space_type: str = 'VIEW_3D'
|
||||||
bl_region_type: str = 'UI'
|
bl_region_type: str = 'UI'
|
||||||
bl_category: str = CATEGORY_NAME
|
bl_category: str = CATEGORY_NAME
|
||||||
bl_options: Set[str] = {'DEFAULT_CLOSED'}
|
|
||||||
|
|
||||||
def draw(self, context: Context) -> None:
|
def draw(self, context: Context) -> None:
|
||||||
"""Draw the main panel layout"""
|
"""Draw the main panel layout"""
|
||||||
|
|||||||
+41
-38
@@ -1,46 +1,49 @@
|
|||||||
import bpy
|
# MMD Tools disabled for the time being unto it can be fixed.
|
||||||
from typing import Set
|
|
||||||
from bpy.types import Panel, Context, UILayout
|
|
||||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
|
||||||
from ..core.translations import t
|
|
||||||
|
|
||||||
class AvatarToolKit_PT_MMDPanel(Panel):
|
# import bpy
|
||||||
"""Panel containing MMD bone standardization and cleanup tools"""
|
# from typing import Set
|
||||||
bl_label = t("MMD.label")
|
# from bpy.types import Panel, Context, UILayout
|
||||||
bl_idname = "OBJECT_PT_avatar_toolkit_mmd"
|
# from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
||||||
bl_space_type = 'VIEW_3D'
|
# from ..core.translations import t
|
||||||
bl_region_type = 'UI'
|
|
||||||
bl_category = CATEGORY_NAME
|
|
||||||
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
|
||||||
bl_order = 3
|
|
||||||
|
|
||||||
def draw(self, context: Context) -> None:
|
# class AvatarToolKit_PT_MMDPanel(Panel):
|
||||||
layout: UILayout = self.layout
|
# """Panel containing MMD bone standardization and cleanup tools"""
|
||||||
toolkit = context.scene.avatar_toolkit
|
# bl_label = t("MMD.label")
|
||||||
|
# bl_idname = "OBJECT_PT_avatar_toolkit_mmd"
|
||||||
|
# bl_space_type = 'VIEW_3D'
|
||||||
|
# bl_region_type = 'UI'
|
||||||
|
# bl_category = CATEGORY_NAME
|
||||||
|
# bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
||||||
|
# bl_order = 3
|
||||||
|
# bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
|
# def draw(self, context: Context) -> None:
|
||||||
|
# layout: UILayout = self.layout
|
||||||
|
# toolkit = context.scene.avatar_toolkit
|
||||||
|
|
||||||
# Bone Settings Box
|
# Bone Settings Box
|
||||||
bone_box: UILayout = layout.box()
|
# bone_box: UILayout = layout.box()
|
||||||
col: UILayout = bone_box.column(align=True)
|
# col: UILayout = bone_box.column(align=True)
|
||||||
col.label(text=t("MMD.bone_settings"), icon='BONE_DATA')
|
# col.label(text=t("MMD.bone_settings"), icon='BONE_DATA')
|
||||||
col.separator(factor=0.5)
|
# col.separator(factor=0.5)
|
||||||
col.prop(toolkit, "keep_twist_bones")
|
# col.prop(toolkit, "keep_twist_bones")
|
||||||
col.prop(toolkit, "keep_upper_chest")
|
# col.prop(toolkit, "keep_upper_chest")
|
||||||
col.operator("avatar_toolkit.standardize_mmd", icon='BONE_DATA')
|
# col.operator("avatar_toolkit.standardize_mmd", icon='BONE_DATA')
|
||||||
|
|
||||||
# Mesh Tools Box
|
# Mesh Tools Box
|
||||||
mesh_box: UILayout = layout.box()
|
# mesh_box: UILayout = layout.box()
|
||||||
col = mesh_box.column(align=True)
|
# col = mesh_box.column(align=True)
|
||||||
col.label(text=t("MMD.mesh_tools"), icon='MESH_DATA')
|
# col.label(text=t("MMD.mesh_tools"), icon='MESH_DATA')
|
||||||
col.separator(factor=0.5)
|
# col.separator(factor=0.5)
|
||||||
row: UILayout = col.row(align=True)
|
# row: UILayout = col.row(align=True)
|
||||||
row.operator("avatar_toolkit.fix_meshes", icon='MODIFIER')
|
# row.operator("avatar_toolkit.fix_meshes", icon='MODIFIER')
|
||||||
row.operator("avatar_toolkit.validate_meshes", icon='CHECKMARK')
|
# row.operator("avatar_toolkit.validate_meshes", icon='CHECKMARK')
|
||||||
|
|
||||||
# Cleanup Box
|
# Cleanup Box
|
||||||
cleanup_box: UILayout = layout.box()
|
# cleanup_box: UILayout = layout.box()
|
||||||
col = cleanup_box.column(align=True)
|
# col = cleanup_box.column(align=True)
|
||||||
col.label(text=t("MMD.cleanup"), icon='BRUSH_DATA')
|
# col.label(text=t("MMD.cleanup"), icon='BRUSH_DATA')
|
||||||
col.separator(factor=0.5)
|
# col.separator(factor=0.5)
|
||||||
col.operator("avatar_toolkit.cleanup_mmd", icon='SHADERFX')
|
# col.operator("avatar_toolkit.cleanup_mmd", icon='SHADERFX')
|
||||||
col.operator("avatar_toolkit.convert_mmd_morphs", icon='SHAPEKEY_DATA')
|
# col.operator("avatar_toolkit.convert_mmd_morphs", icon='SHAPEKEY_DATA')
|
||||||
col.operator("avatar_toolkit.reparent_meshes", icon='OUTLINER_OB_ARMATURE')
|
# col.operator("avatar_toolkit.reparent_meshes", icon='OUTLINER_OB_ARMATURE')
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class AvatarToolKit_PT_OptimizationPanel(Panel):
|
|||||||
bl_category: str = CATEGORY_NAME
|
bl_category: str = CATEGORY_NAME
|
||||||
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
||||||
bl_order: int = 1
|
bl_order: int = 1
|
||||||
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
def draw(self, context: Context) -> None:
|
def draw(self, context: Context) -> None:
|
||||||
"""Draws the optimization panel interface with material, mesh cleanup and join mesh tools"""
|
"""Draws the optimization panel interface with material, mesh cleanup and join mesh tools"""
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class AvatarToolKit_PT_SettingsPanel(Panel):
|
|||||||
bl_category: str = CATEGORY_NAME
|
bl_category: str = CATEGORY_NAME
|
||||||
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
||||||
bl_order: int = 5
|
bl_order: int = 5
|
||||||
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
def draw(self, context: Context) -> None:
|
def draw(self, context: Context) -> None:
|
||||||
"""Draw the settings panel layout with language selection"""
|
"""Draw the settings panel layout with language selection"""
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class AvatarToolKit_PT_ToolsPanel(Panel):
|
|||||||
bl_category: str = CATEGORY_NAME
|
bl_category: str = CATEGORY_NAME
|
||||||
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
||||||
bl_order: int = 2
|
bl_order: int = 2
|
||||||
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
def draw(self, context: Context) -> None:
|
def draw(self, context: Context) -> None:
|
||||||
"""Draw the tools panel interface"""
|
"""Draw the tools panel interface"""
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
from bpy.types import Panel, Context, UILayout
|
||||||
|
from ..core.translations import t
|
||||||
|
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
||||||
|
|
||||||
|
class AvatarToolKit_PT_VisemesPanel(Panel):
|
||||||
|
"""Panel containing viseme creation and preview tools"""
|
||||||
|
bl_label: str = t("Visemes.panel_label")
|
||||||
|
bl_idname: str = "VIEW3D_PT_avatar_toolkit_visemes"
|
||||||
|
bl_space_type: str = 'VIEW_3D'
|
||||||
|
bl_region_type: str = 'UI'
|
||||||
|
bl_category: str = CATEGORY_NAME
|
||||||
|
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
||||||
|
bl_order: int = 4
|
||||||
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
|
def draw(self, context: Context) -> None:
|
||||||
|
"""Draw the visemes panel interface"""
|
||||||
|
layout: UILayout = self.layout
|
||||||
|
props = context.scene.avatar_toolkit
|
||||||
|
|
||||||
|
# Check for valid mesh with shape keys
|
||||||
|
if not context.active_object or context.active_object.type != 'MESH' or not context.active_object.data.shape_keys:
|
||||||
|
layout.label(text=t("Visemes.no_shapekeys"))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Shape Key Selection Box
|
||||||
|
shape_box: UILayout = layout.box()
|
||||||
|
col: UILayout = shape_box.column(align=True)
|
||||||
|
col.label(text=t("Visemes.shape_selection"), icon='SHAPEKEY_DATA')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
|
||||||
|
# Shape key selection with valid data
|
||||||
|
shape_keys = context.active_object.data.shape_keys
|
||||||
|
col.prop_search(props, "mouth_a", shape_keys, "key_blocks", text=t("Visemes.mouth_a"))
|
||||||
|
col.prop_search(props, "mouth_o", shape_keys, "key_blocks", text=t("Visemes.mouth_o"))
|
||||||
|
col.prop_search(props, "mouth_ch", shape_keys, "key_blocks", text=t("Visemes.mouth_ch"))
|
||||||
|
|
||||||
|
# Shape intensity slider
|
||||||
|
col.separator()
|
||||||
|
col.prop(props, "shape_intensity", slider=True)
|
||||||
|
|
||||||
|
# Preview Box
|
||||||
|
preview_box: UILayout = layout.box()
|
||||||
|
col = preview_box.column(align=True)
|
||||||
|
col.label(text=t("Visemes.preview_label"), icon='HIDE_OFF')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
|
||||||
|
if props.viseme_preview_mode:
|
||||||
|
col.prop(props, "viseme_preview_selection", text="")
|
||||||
|
col.separator()
|
||||||
|
|
||||||
|
preview_text = t("Visemes.stop_preview") if props.viseme_preview_mode else t("Visemes.start_preview")
|
||||||
|
col.operator("avatar_toolkit.preview_visemes", text=preview_text, icon='HIDE_OFF')
|
||||||
|
|
||||||
|
# Create Box
|
||||||
|
create_box: UILayout = layout.box()
|
||||||
|
col = create_box.column(align=True)
|
||||||
|
col.label(text=t("Visemes.create_label"), icon='ADD')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
col.operator("avatar_toolkit.create_visemes", icon='ADD')
|
||||||
Reference in New Issue
Block a user