Files
Avatar-Toolkit/functions/visemes.py
T
Yusarina 1e734a518e Fix garbled Japanese/Unicode text in armature and mesh dropdowns
- Add proper caching to EnumProperty callbacks to prevent encoding corruption
- Use ASCII-safe identifiers (ARM_/MESH_ + pointer) with Unicode display names
- Add get_mesh_from_identifier() helper for safe mesh retrieval
- Update visemes panel to use new mesh identifier system
- Ensure stable string objects prevent Blender RNA encoding issues
2025-11-29 22:48:25 +00:00

365 lines
16 KiB
Python

# 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 traceback
import bpy
from typing import Dict, List, Optional, Tuple, Any, Set, Union
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_all_meshes,
validate_mesh_for_pose
)
import traceback
class VisemeCache:
"""Manages caching of generated viseme shape data for performance optimization"""
_cache: Dict[Tuple[str, Tuple[Tuple]], List] = {}
@classmethod
def get_cached_shape(cls, key: str, mix_data: List[List[Union[str, float]]]) -> Optional[List]:
"""Retrieves cached shape data for a given viseme key and mix configuration"""
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[List[Union[str, float]]], shape_data: List) -> None:
"""Stores shape data in cache for future retrieval"""
cache_key = (key, tuple(tuple(x) for x in mix_data))
cls._cache[cache_key] = shape_data
class VisemePreview:
"""Controls real-time preview functionality for viseme shapes"""
_preview_data: Dict[str, float] = {}
_active: bool = False
_preview_shapes: Optional[OrderedDict] = None
_mesh_name: str = ""
@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 = {}
cls._mesh_name = mesh.name
# 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
# Get the mesh by name instead of using active object
mesh = bpy.data.objects.get(cls._mesh_name)
if not mesh:
return
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
cls._mesh_name = ""
class AvatarToolkit_OT_PreviewVisemes(Operator):
"""Operator for previewing viseme shapes in real-time"""
bl_idname: str = "avatar_toolkit.preview_visemes"
bl_label: str = t("Visemes.preview_label")
bl_description: str = t("Visemes.preview_desc")
bl_options: Set[str] = {'REGISTER', 'UNDO', 'INTERNAL'}
@classmethod
def poll(cls, context: Context) -> bool:
if context.mode != 'OBJECT':
return False
# Get mesh from UI selection
from ..core.common import get_mesh_from_identifier
props = context.scene.avatar_toolkit
mesh_obj = get_mesh_from_identifier(props.viseme_mesh)
# Validate mesh
return mesh_obj and mesh_obj.type == 'MESH'
def execute(self, context: Context) -> Set[str]:
from ..core.common import get_mesh_from_identifier
props = context.scene.avatar_toolkit
mesh = get_mesh_from_identifier(props.viseme_mesh)
if props.viseme_preview_mode:
VisemePreview.end_preview(mesh)
props.viseme_preview_mode = False
else:
if not mesh or 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 AvatarToolkit_OT_CreateVisemes(Operator):
"""Operator for generating VRChat-compatible viseme shape keys"""
bl_idname: str = "avatar_toolkit.create_visemes"
bl_label: str = t("Visemes.create_label")
bl_description: str = t("Visemes.create_desc")
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
# Check if we're in object mode
if context.mode != 'OBJECT':
return False
# Get mesh from UI selection
from ..core.common import get_mesh_from_identifier
props = context.scene.avatar_toolkit
mesh_obj = get_mesh_from_identifier(props.viseme_mesh)
# Validate mesh
return mesh_obj and mesh_obj.type == 'MESH'
def execute(self, context: Context) -> Set[str]:
from ..core.common import get_mesh_from_identifier
props = context.scene.avatar_toolkit
mesh = get_mesh_from_identifier(props.viseme_mesh)
if not mesh or 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:
logger.error(f"Error creating visemes: {traceback.format_exc()}")
self.report({'ERROR'}, traceback.format_exc())
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, mesh) # Added mesh parameter
# 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, mesh: Object) -> None: # Added mesh parameter
"""Creates a new shape key by mixing existing ones"""
# 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)
old_active = context.view_layer.objects.active
context.view_layer.objects.active = mesh
bpy.ops.object.shape_key_remove()
context.view_layer.objects.active = old_active
# 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
old_active = context.view_layer.objects.active
context.view_layer.objects.active = mesh
mesh.shape_key_add(name=new_name, from_mix=True)
context.view_layer.objects.active = old_active
# 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]