First Commit
This commit is contained in:
16
operators/__init__.py
Normal file
16
operators/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from .vrc import (
|
||||
VRCVIS_OT_AutoDetect,
|
||||
VRCVIS_OT_Preview,
|
||||
VRCVIS_OT_UpdatePreview,
|
||||
VRCVIS_OT_Generate,
|
||||
VRCVIS_OT_RemoveAll,
|
||||
VRCVIS_OT_Reorder,
|
||||
)
|
||||
from .mmd import (
|
||||
VRCVIS_OT_MMDAutoDetect,
|
||||
VRCVIS_OT_MMDConvert,
|
||||
VRCVIS_OT_MMDRemove,
|
||||
)
|
||||
156
operators/mmd.py
Normal file
156
operators/mmd.py
Normal file
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import bpy
|
||||
from utilities.constants import MMD_VISEME_COUNT, MMD_SEP_1, MMD_SEP_2
|
||||
from utilities.functions import MMD_JP_MAPPING
|
||||
from utilities.helpers import _ps, guess_shape, _restore_shapekey_values
|
||||
|
||||
|
||||
class VRCVIS_OT_MMDAutoDetect(bpy.types.Operator):
|
||||
bl_idname = 'vrc_viseme.mmd_auto_detect'
|
||||
bl_label = 'Auto-rileva (EN → JP)'
|
||||
bl_description = (
|
||||
'Cerca automaticamente i nomi inglesi/VRChat comuni e precompila '
|
||||
'i campi di destinazione MMD giapponese'
|
||||
)
|
||||
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 = 0
|
||||
for _jp, suffix, _en, candidates in MMD_JP_MAPPING:
|
||||
result = guess_shape(obj, candidates)
|
||||
if result:
|
||||
setattr(scene, f'vrcvis_mmd_{suffix}', result)
|
||||
found += 1
|
||||
if found:
|
||||
self.report({'INFO'}, f'Rilevate {found} shape key')
|
||||
else:
|
||||
self.report({'WARNING'}, 'Nessun nome riconosciuto trovato')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class VRCVIS_OT_MMDConvert(bpy.types.Operator):
|
||||
bl_idname = 'vrc_viseme.mmd_convert'
|
||||
bl_label = 'Converti in MMD'
|
||||
bl_description = (
|
||||
'Duplica le shape key selezionate con i nomi MMD giapponesi, '
|
||||
'aggiunge i separatori e posiziona tutto in fondo alla lista'
|
||||
)
|
||||
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'}
|
||||
|
||||
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
|
||||
|
||||
keys = obj.data.shape_keys.key_blocks
|
||||
overwrite = scene.vrcvis_overwrite
|
||||
created = []
|
||||
skipped = []
|
||||
|
||||
def _add_empty(name):
|
||||
if name in keys:
|
||||
if overwrite:
|
||||
obj.active_shape_key_index = keys.find(name)
|
||||
bpy.ops.object.shape_key_remove()
|
||||
else:
|
||||
skipped.append(name)
|
||||
return
|
||||
bpy.ops.object.shape_key_clear()
|
||||
obj.shape_key_add(name=name, from_mix=True)
|
||||
created.append(name)
|
||||
|
||||
def _add_copy(src_name, dst_name):
|
||||
if not src_name or src_name not in keys:
|
||||
return
|
||||
if dst_name in keys:
|
||||
if overwrite:
|
||||
obj.active_shape_key_index = keys.find(dst_name)
|
||||
bpy.ops.object.shape_key_remove()
|
||||
else:
|
||||
skipped.append(dst_name)
|
||||
return
|
||||
bpy.ops.object.shape_key_clear()
|
||||
keys[src_name].slider_max = max(keys[src_name].slider_max, 1.0)
|
||||
keys[src_name].value = 1.0
|
||||
obj.shape_key_add(name=dst_name, from_mix=True)
|
||||
bpy.ops.object.shape_key_clear()
|
||||
created.append(dst_name)
|
||||
|
||||
_add_empty(MMD_SEP_1)
|
||||
for jp_name, suffix, _en, _ in MMD_JP_MAPPING[:MMD_VISEME_COUNT]:
|
||||
src = getattr(scene, f'vrcvis_mmd_{suffix}')
|
||||
_add_copy(src, jp_name)
|
||||
|
||||
_add_empty(MMD_SEP_2)
|
||||
for jp_name, suffix, _en, _ in MMD_JP_MAPPING[MMD_VISEME_COUNT:]:
|
||||
src = getattr(scene, f'vrcvis_mmd_{suffix}')
|
||||
_add_copy(src, jp_name)
|
||||
|
||||
obj.active_shape_key_index = 0
|
||||
|
||||
msg_parts = []
|
||||
if created:
|
||||
msg_parts.append(f'Create: {len(created)}')
|
||||
if skipped:
|
||||
msg_parts.append(f'Saltate: {len(skipped)}')
|
||||
self.report({'INFO'}, ' | '.join(msg_parts) if msg_parts else 'Nessuna shape key creata')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class VRCVIS_OT_MMDRemove(bpy.types.Operator):
|
||||
bl_idname = 'vrc_viseme.mmd_remove'
|
||||
bl_label = 'Rimuovi MMD'
|
||||
bl_description = (
|
||||
'Rimuove le shape key MMD giapponesi e i separatori '
|
||||
'(---MMD Visemes--- e ^MMD Visemes / Other v)'
|
||||
)
|
||||
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)
|
||||
|
||||
mmd_names = {jp for jp, _, _en, _ in MMD_JP_MAPPING} | {MMD_SEP_1, MMD_SEP_2}
|
||||
to_remove = [k.name for k in obj.data.shape_keys.key_blocks if k.name in mmd_names]
|
||||
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 MMD')
|
||||
return {'FINISHED'}
|
||||
265
operators/vrc.py
Normal file
265
operators/vrc.py
Normal file
@@ -0,0 +1,265 @@
|
||||
#!/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'}
|
||||
Reference in New Issue
Block a user