Files
visemes/operators/vrc.py
2026-04-08 11:47:23 +02:00

266 lines
9.8 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import bpy
from utilities.constants import MMD_CANDIDATES
from utilities.functions import build_viseme_map, VRC_ORDER
from utilities.helpers import _ps, guess_shape, _save_shapekey_values, _restore_shapekey_values, _apply_mix
class VRCVIS_OT_AutoDetect(bpy.types.Operator):
bl_idname = 'vrc_viseme.auto_detect'
bl_label = 'Auto-rileva (MMD → VRC)'
bl_description = (
'Cerca automaticamente tra i nomi MMD comuni (あ/お/い/まばたき) '
'e precompila i campi A/O/CH/Blink'
)
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
scene = context.scene
mesh_name = scene.vrcvis_mesh
if not mesh_name or mesh_name not in bpy.data.objects:
self.report({'ERROR'}, 'Nessuna mesh selezionata')
return {'CANCELLED'}
obj = bpy.data.objects[mesh_name]
found_a = guess_shape(obj, MMD_CANDIDATES['a'])
found_o = guess_shape(obj, MMD_CANDIDATES['o'])
found_ch = guess_shape(obj, MMD_CANDIDATES['ch'])
found_blink = guess_shape(obj, MMD_CANDIDATES['blink'])
if found_a: scene.vrcvis_mouth_a = found_a
if found_o: scene.vrcvis_mouth_o = found_o
if found_ch: scene.vrcvis_mouth_ch = found_ch
if found_blink: scene.vrcvis_blink = found_blink
detected = ', '.join(filter(None, [found_a, found_o, found_ch, found_blink]))
if detected:
self.report({'INFO'}, f'Rilevate: {detected}')
else:
self.report({'WARNING'}, 'Nessun nome MMD riconosciuto trovato')
return {'FINISHED'}
class VRCVIS_OT_Preview(bpy.types.Operator):
bl_idname = 'vrc_viseme.preview'
bl_label = 'Preview Viseme'
bl_description = 'Attiva/disattiva anteprima viseme in viewport'
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
def execute(self, context):
scene = context.scene
mesh_name = scene.vrcvis_mesh
if not mesh_name or mesh_name not in bpy.data.objects:
self.report({'ERROR'}, 'Mesh non valida')
return {'CANCELLED'}
obj = bpy.data.objects[mesh_name]
if not obj.data.shape_keys:
self.report({'ERROR'}, 'La mesh non ha shape key')
return {'CANCELLED'}
if _ps.active:
_restore_shapekey_values(obj)
_ps.active = False
scene.vrcvis_preview_active = False
else:
_save_shapekey_values(obj)
_ps.active = True
scene.vrcvis_preview_active = True
sel = scene.vrcvis_preview_viseme
if sel == 'vrc.blink':
blink_src = scene.vrcvis_blink
if blink_src:
_apply_mix(obj, [(blink_src, 1.0)], scene.vrcvis_intensity)
else:
vmap = build_viseme_map(
scene.vrcvis_mouth_a, scene.vrcvis_mouth_o, scene.vrcvis_mouth_ch)
if sel in vmap:
_apply_mix(obj, vmap[sel], scene.vrcvis_intensity)
return {'FINISHED'}
class VRCVIS_OT_UpdatePreview(bpy.types.Operator):
bl_idname = 'vrc_viseme.update_preview'
bl_label = 'Aggiorna Preview'
bl_options = {'INTERNAL'}
def execute(self, context):
scene = context.scene
if not _ps.active:
return {'FINISHED'}
mesh_name = scene.vrcvis_mesh
if not mesh_name or mesh_name not in bpy.data.objects:
return {'CANCELLED'}
obj = bpy.data.objects[mesh_name]
sel = scene.vrcvis_preview_viseme
if sel == 'vrc.blink':
blink_src = scene.vrcvis_blink
if blink_src:
_apply_mix(obj, [(blink_src, 1.0)], scene.vrcvis_intensity)
else:
vmap = build_viseme_map(
scene.vrcvis_mouth_a, scene.vrcvis_mouth_o, scene.vrcvis_mouth_ch)
if sel in vmap:
_apply_mix(obj, vmap[sel], scene.vrcvis_intensity)
return {'FINISHED'}
class VRCVIS_OT_Generate(bpy.types.Operator):
bl_idname = 'vrc_viseme.generate'
bl_label = 'Genera Visemes VRChat'
bl_description = (
'Crea le 15 shape key vrc.v_* mescolando A/O/CH con i pesi standard VRChat, '
'più vrc.blink se il sorgente blink è impostato'
)
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
scene = context.scene
mesh_name = scene.vrcvis_mesh
if not mesh_name or mesh_name not in bpy.data.objects:
self.report({'ERROR'}, 'Seleziona una mesh valida')
return {'CANCELLED'}
obj = bpy.data.objects[mesh_name]
if not obj.data.shape_keys:
self.report({'ERROR'}, 'La mesh non ha shape key')
return {'CANCELLED'}
shape_a = scene.vrcvis_mouth_a
shape_o = scene.vrcvis_mouth_o
shape_ch = scene.vrcvis_mouth_ch
shape_blink = scene.vrcvis_blink
intensity = scene.vrcvis_intensity
keys = obj.data.shape_keys.key_blocks
for name, label in [(shape_a, 'A'), (shape_o, 'O'), (shape_ch, 'CH')]:
if not name or name not in keys:
self.report({'ERROR'}, f'Shape key {label} ("{name}") non trovata')
return {'CANCELLED'}
if len({shape_a, shape_o, shape_ch}) < 3:
self.report({'ERROR'}, 'Le tre shape key sorgente devono essere diverse')
return {'CANCELLED'}
if _ps.active:
_restore_shapekey_values(obj)
_ps.active = False
scene.vrcvis_preview_active = False
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
obj.show_only_shape_key = False
vmap = build_viseme_map(shape_a, shape_o, shape_ch)
if shape_blink and shape_blink in keys:
vmap['vrc.blink'] = [(shape_blink, 1.0)]
wm = bpy.context.window_manager
wm.progress_begin(0, len(vmap))
created = []
skipped = []
for idx, (vrc_name, mix) in enumerate(vmap.items()):
wm.progress_update(idx)
if vrc_name in keys:
if scene.vrcvis_overwrite:
obj.active_shape_key_index = keys.find(vrc_name)
bpy.ops.object.shape_key_remove()
else:
skipped.append(vrc_name)
continue
bpy.ops.object.shape_key_clear()
for shape_name, weight in mix:
if shape_name in keys:
keys[shape_name].slider_max = 10.0
keys[shape_name].value = weight * intensity
obj.shape_key_add(name=vrc_name, from_mix=True)
bpy.ops.object.shape_key_clear()
for shape_name, _ in mix:
if shape_name in keys:
keys[shape_name].slider_max = 1.0
created.append(vrc_name)
obj.active_shape_key_index = 0
wm.progress_end()
msg_parts = []
if created:
msg_parts.append(f'Create: {len(created)}')
if skipped:
msg_parts.append(f'Saltate (già esistenti): {len(skipped)}')
self.report({'INFO'}, ' | '.join(msg_parts) if msg_parts else 'Nessuna shape key generata')
return {'FINISHED'}
class VRCVIS_OT_RemoveAll(bpy.types.Operator):
bl_idname = 'vrc_viseme.remove_all'
bl_label = 'Rimuovi vrc.*'
bl_description = 'Cancella tutte le shape key il cui nome inizia con "vrc." (visemi + blink)'
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
scene = context.scene
mesh_name = scene.vrcvis_mesh
if not mesh_name or mesh_name not in bpy.data.objects:
self.report({'ERROR'}, 'Nessuna mesh selezionata')
return {'CANCELLED'}
obj = bpy.data.objects[mesh_name]
if not obj.data.shape_keys:
self.report({'INFO'}, 'Nessuna shape key presente')
return {'FINISHED'}
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
to_remove = [k.name for k in obj.data.shape_keys.key_blocks if k.name.startswith('vrc.')]
count = 0
for name in to_remove:
keys = obj.data.shape_keys.key_blocks
if name in keys:
obj.active_shape_key_index = keys.find(name)
bpy.ops.object.shape_key_remove()
count += 1
self.report({'INFO'}, f'Rimosse {count} shape key vrc.*')
return {'FINISHED'}
class VRCVIS_OT_Reorder(bpy.types.Operator):
bl_idname = 'vrc_viseme.reorder'
bl_label = 'Riordina VRC Keys'
bl_description = "Riordina le shape key vrc.* nell'ordine standard VRChat subito sotto Basis"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
scene = context.scene
mesh_name = scene.vrcvis_mesh
if not mesh_name or mesh_name not in bpy.data.objects:
self.report({'ERROR'}, 'Nessuna mesh selezionata')
return {'CANCELLED'}
obj = bpy.data.objects[mesh_name]
if not obj.data.shape_keys:
self.report({'INFO'}, 'Nessuna shape key presente')
return {'FINISHED'}
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
keys = obj.data.shape_keys.key_blocks
target_pos = 1
moved = 0
for name in VRC_ORDER:
if name not in keys:
continue
current_idx = keys.find(name)
steps = current_idx - target_pos
if steps > 0:
obj.active_shape_key_index = current_idx
for _ in range(steps):
bpy.ops.object.shape_key_move(type='UP')
target_pos += 1
moved += 1
obj.active_shape_key_index = 0
self.report({'INFO'}, f'Riordinate {moved} shape key')
return {'FINISHED'}