361 lines
16 KiB
Python
361 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
|
|
props = context.scene.avatar_toolkit
|
|
mesh_obj = bpy.data.objects.get(props.viseme_mesh)
|
|
|
|
# Validate mesh
|
|
return mesh_obj and mesh_obj.type == 'MESH'
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
props = context.scene.avatar_toolkit
|
|
mesh = bpy.data.objects.get(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
|
|
props = context.scene.avatar_toolkit
|
|
mesh_obj = bpy.data.objects.get(props.viseme_mesh)
|
|
|
|
# Validate mesh
|
|
return mesh_obj and mesh_obj.type == 'MESH'
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
props = context.scene.avatar_toolkit
|
|
mesh = bpy.data.objects.get(props.viseme_mesh) # Changed from context.active_object
|
|
|
|
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]
|
|
|