From 07b2dba51f1ad8bc5b87fec52b66e6469c7919cc Mon Sep 17 00:00:00 2001 From: Yusarina Date: Mon, 8 Jul 2024 09:41:46 +0100 Subject: [PATCH] Basic Viseme Creation Support Does not work yet, but it's the start --- core/common.py | 55 ++++++++++++++++++++++++++++++ core/properties.py | 12 ++++++- core/register.py | 1 + functions/viseme.py | 81 +++++++++++++++++++++++++++++++++++++++++++++ ui/viseme.py | 32 ++++++++++++++++++ 5 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 functions/viseme.py create mode 100644 ui/viseme.py diff --git a/core/common.py b/core/common.py index 6e08f93..297b37d 100644 --- a/core/common.py +++ b/core/common.py @@ -57,3 +57,58 @@ def get_armature(context, armature_name=None) -> Optional[Object]: if obj.type == "ARMATURE": return obj return next((obj for obj in context.view_layer.objects if obj.type == 'ARMATURE'), None) + +def has_shapekeys(mesh_obj): + return mesh_obj.data.shape_keys is not None + +def has_shapekeys(mesh_obj): + return mesh_obj.data.shape_keys is not None + +def sort_shape_keys(mesh): + if not has_shapekeys(mesh): + return + + order = [ + 'Basis', + 'vrc.blink_left', + 'vrc.blink_right', + 'vrc.lowerlid_left', + 'vrc.lowerlid_right', + 'vrc.v_aa', + 'vrc.v_ch', + 'vrc.v_dd', + 'vrc.v_e', + 'vrc.v_ff', + 'vrc.v_ih', + 'vrc.v_kk', + 'vrc.v_nn', + 'vrc.v_oh', + 'vrc.v_ou', + 'vrc.v_pp', + 'vrc.v_rr', + 'vrc.v_sil', + 'vrc.v_ss', + 'vrc.v_th', + ] + + shape_keys = mesh.data.shape_keys.key_blocks + for i, name in enumerate(order): + if name in shape_keys: + index = shape_keys.find(name) + if index != i: + bpy.context.object.active_shape_key_index = index + for _ in range(abs(index - i)): + bpy.ops.object.shape_key_move(type='UP' if index > i else 'DOWN') + + # Move any remaining shape keys to the end + for key in shape_keys: + if key.name not in order: + index = shape_keys.find(key.name) + bpy.context.object.active_shape_key_index = index + for _ in range(len(shape_keys) - index - 1): + bpy.ops.object.shape_key_move(type='DOWN') + +def get_shapekeys(mesh, prefix=''): + if not has_shapekeys(mesh): + return [] + return [(key.name, key.name, key.name) for key in mesh.data.shape_keys.key_blocks if key.name != 'Basis' and key.name.startswith(prefix)] diff --git a/core/properties.py b/core/properties.py index 9bf108d..33b7f6e 100644 --- a/core/properties.py +++ b/core/properties.py @@ -13,6 +13,16 @@ def register(): update=update_language ) + bpy.types.Scene.mouth_a = bpy.props.StringProperty(name=t("Scene.mouth_a.label"), description=t("Scene.mouth_a.desc")) + bpy.types.Scene.mouth_o = bpy.props.StringProperty(name=t("Scene.mouth_o.label"), description=t("Scene.mouth_o.desc")) + bpy.types.Scene.mouth_ch = bpy.props.StringProperty(name=t("Scene.mouth_ch.label"), description=t("Scene.mouth_ch.desc")) + bpy.types.Scene.shape_intensity = bpy.props.FloatProperty(name=t("Scene.shape_intensity.label"), description=t("Scene.shape_intensity.desc"), default=1.0, min=0.0, max=2.0) + def unregister(): if hasattr(bpy.types.Scene, "avatar_toolkit_language"): - del bpy.types.Scene.avatar_toolkit_language \ No newline at end of file + del bpy.types.Scene.avatar_toolkit_language + + del bpy.types.Scene.mouth_a + del bpy.types.Scene.mouth_o + del bpy.types.Scene.mouth_ch + del bpy.types.Scene.shape_intensity \ No newline at end of file diff --git a/core/register.py b/core/register.py index 449050e..aad610f 100644 --- a/core/register.py +++ b/core/register.py @@ -1,4 +1,5 @@ import bpy +import typing from typing import List, Type # List to store the classes to register diff --git a/functions/viseme.py b/functions/viseme.py new file mode 100644 index 0000000..e3fce99 --- /dev/null +++ b/functions/viseme.py @@ -0,0 +1,81 @@ +import bpy +from ..core import common +from ..core.register import register_wrap +from ..functions.translations import t + +@register_wrap +class AutoVisemeButton(bpy.types.Operator): + bl_idname = 'avatar_toolkit.create_visemes' + bl_label = t('AutoVisemeButton.label') + bl_description = t('AutoVisemeButton.desc') + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return context.active_object and context.active_object.type == 'MESH' + + def execute(self, context): + mesh = context.active_object + if not mesh or not common.has_shapekeys(mesh): + self.report({'ERROR'}, t('AutoVisemeButton.error.noShapekeys')) + return {'CANCELLED'} + + shape_a = context.scene.mouth_a + shape_o = context.scene.mouth_o + shape_ch = context.scene.mouth_ch + + if shape_a == "Basis" or shape_o == "Basis" or shape_ch == "Basis": + self.report({'ERROR'}, t('AutoVisemeButton.error.selectShapekeys')) + return {'CANCELLED'} + + # Create visemes + visemes = [ + ('vrc.v_aa', [(shape_a, 0.9998)]), + ('vrc.v_ch', [(shape_ch, 0.9996)]), + ('vrc.v_dd', [(shape_a, 0.3), (shape_ch, 0.7)]), + ('vrc.v_e', [(shape_a, 0.5), (shape_ch, 0.2)]), + ('vrc.v_ff', [(shape_a, 0.2), (shape_ch, 0.4)]), + ('vrc.v_ih', [(shape_ch, 0.7), (shape_o, 0.3)]), + ('vrc.v_kk', [(shape_a, 0.7), (shape_ch, 0.4)]), + ('vrc.v_nn', [(shape_a, 0.2), (shape_ch, 0.7)]), + ('vrc.v_oh', [(shape_a, 0.2), (shape_o, 0.8)]), + ('vrc.v_ou', [(shape_o, 0.9994)]), + ('vrc.v_pp', [(shape_a, 0.0004), (shape_o, 0.0004)]), + ('vrc.v_rr', [(shape_ch, 0.5), (shape_o, 0.3)]), + ('vrc.v_sil', [(shape_a, 0.0002), (shape_ch, 0.0002)]), + ('vrc.v_ss', [(shape_ch, 0.8)]), + ('vrc.v_th', [(shape_a, 0.4), (shape_o, 0.15)]) + ] + + for viseme_name, shape_mix in visemes: + self.create_viseme(mesh, viseme_name, shape_mix, context.scene.shape_intensity) + + # Sort shape keys + common.sort_shape_keys(mesh) + + self.report({'INFO'}, t('AutoVisemeButton.success')) + return {'FINISHED'} + + def create_viseme(self, mesh, viseme_name, shape_mix, intensity): + # Remove existing viseme if it exists + if viseme_name in mesh.data.shape_keys.key_blocks: + mesh.shape_key_remove(mesh.data.shape_keys.key_blocks[viseme_name]) + + # Create new viseme + new_key = mesh.shape_key_add(name=viseme_name, from_mix=False) + new_key.value = 1.0 + + # Mix shapes + for shape_name, value in shape_mix: + if shape_name in mesh.data.shape_keys.key_blocks: + shape = mesh.data.shape_keys.key_blocks[shape_name] + shape.value = value * intensity + + # Apply mix + mesh.shape_key_add(name=viseme_name, from_mix=True) + + # Reset shape key values + for shape in mesh.data.shape_keys.key_blocks: + shape.value = 0.0 + + new_key.value = 1.0 \ No newline at end of file diff --git a/ui/viseme.py b/ui/viseme.py new file mode 100644 index 0000000..5250fb3 --- /dev/null +++ b/ui/viseme.py @@ -0,0 +1,32 @@ +import bpy +from ..core.register import register_wrap +from ..functions.translations import t + +@register_wrap +class AvatarToolkitVisemePanel(bpy.types.Panel): + bl_label = t("Viseme.label") + bl_idname = "OBJECT_PT_avatar_toolkit_viseme" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = "Avatar Toolkit" + bl_parent_id = "OBJECT_PT_avatar_toolkit" + + def draw(self, context): + layout = self.layout + mesh = context.active_object + + if not mesh or mesh.type != 'MESH': + layout.label(text=t('VisemePanel.error.noMesh'), icon='ERROR') + return + + if not mesh.data.shape_keys: + layout.label(text=t('VisemePanel.error.noShapekeys'), icon='ERROR') + return + + layout.prop_search(context.scene, "mouth_a", mesh.data.shape_keys, "key_blocks", text=t('Scene.mouth_a.label')) + layout.prop_search(context.scene, "mouth_o", mesh.data.shape_keys, "key_blocks", text=t('Scene.mouth_o.label')) + layout.prop_search(context.scene, "mouth_ch", mesh.data.shape_keys, "key_blocks", text=t('Scene.mouth_ch.label')) + + layout.prop(context.scene, 'shape_intensity') + + layout.operator("avatar_toolkit.create_visemes", icon='TRIA_RIGHT')