First Commit

This commit is contained in:
2026-04-08 11:47:23 +02:00
committed by GitHub
parent 67b5d118c2
commit 96d567eb9e
11 changed files with 1050 additions and 0 deletions

77
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,77 @@
# Contributing
Thank you for your interest in contributing to **Visemes Converter Tool**!
---
## Project Structure
Before contributing, make sure you are familiar with the project layout:
```
plugin_blender/
├── visemes.py # Entry point: bl_info, register/unregister
├── operators/
│ ├── __init__.py
│ ├── vrc.py # VRChat viseme operators
│ └── mmd.py # MMD conversion operators
├── panels/
│ ├── __init__.py
│ └── main_panel.py # UI panel
└── utilities/
├── constants.py
├── functions.py
└── helpers.py
```
---
## Getting Started
1. Fork the repository
2. Clone your fork locally
3. Open the project in your editor of choice
4. Install the add-on in Blender via `Edit > Preferences > Add-ons > Install`
---
## How to Contribute
### Reporting Bugs
- Open an issue and describe the problem clearly
- Include your Blender version and OS
- Attach steps to reproduce the bug
### Suggesting Features
- Open an issue with the `enhancement` label
- Describe the use case and the expected behavior
### Submitting Code
1. Create a new branch from `main`:
```
git checkout -b feature/your-feature-name
```
2. Make your changes following the guidelines below
3. Test the add-on inside Blender before submitting
4. Open a pull request with a clear description of the changes
---
## Code Guidelines
- **Language:** all code, variable names, and `bl_description` strings must be in **English**
- **Comments:** do not add inline comments or docstrings — refer to `DOCUMENTATION.md` instead
- **Operators:** new operators go in `operators/vrc.py` or `operators/mmd.py` depending on their scope, and must be exported from `operators/__init__.py`
- **Panels:** UI changes go in `panels/main_panel.py`
- **Data / mappings:** new constants go in `utilities/constants.py`, new mapping data in `utilities/functions.py`
- **Shared utilities:** helper functions and shared state go in `utilities/helpers.py`
- Keep `visemes.py` as a thin entry point — no logic belongs there
---
## Credits
If your contribution is inspired by or based on external work, add a credit line at the top of `visemes.py` in the existing credits block.

170
DOCUMENTATION.md Normal file
View File

@@ -0,0 +1,170 @@
# Visemes Converter Tool — Documentation
Blender add-on for generating VRChat visemes from MMD blendshapes and for converting shape key names to Japanese MMD format.
**Version:** 2.0.0
**Minimum Blender:** 3.0.0
**UI Location:** View3D Sidebar (N) Visemes Tool
**Category:** Rigging
---
## Installation
1. `Edit > Preferences > Add-ons > Install`
2. Select the `visemes.py` file (or the zipped folder)
3. Enable the add-on from the list
---
## Project Structure
```
plugin_blender/
├── visemes.py # Entry point: bl_info, register/unregister
├── operators/
│ ├── __init__.py # Exports all operators
│ ├── vrc.py # VRChat viseme generation operators
│ └── mmd.py # Japanese MMD conversion operators
├── panels/
│ ├── __init__.py # Exports the panel
│ └── main_panel.py # UI panel and preview dropdown
└── utilities/
├── constants.py # MMD constants and auto-detect candidates
├── functions.py # Mapping data and build_viseme_map
└── helpers.py # Preview state and utility functions
```
---
## Modules
### `visemes.py`
Add-on entry point. Contains `bl_info`, the `_CLASSES` list of all classes to register, `_MMD_PROPS` with the dynamic MMD property names, and the `register()` / `unregister()` functions that manage the add-on lifecycle in Blender.
---
### `utilities/constants.py`
| Name | Type | Description |
|------|------|-------------|
| `MMD_VISEME_COUNT` | `int` | Number of primary MMD visemes (あいうえお = 5) |
| `MMD_SEP_1` | `str` | Separator shape key name for visemes (`---MMD Visemes---`) |
| `MMD_SEP_2` | `str` | Separator shape key name for other expressions (`^MMD Visemes / Other v`) |
| `MMD_CANDIDATES` | `dict` | Candidate names for auto-detection of source shape keys (keys: `a`, `o`, `ch`, `blink`) |
---
### `utilities/functions.py`
| Name | Type | Description |
|------|------|-------------|
| `MMD_JP_MAPPING` | `list[tuple]` | List of 15 entries `(jp_name, suffix, en_label, candidates)` mapping English/VRChat shape keys to Japanese MMD names |
| `VRC_ORDER` | `list[str]` | Standard VRChat order for the 16 visemes (`vrc.v_sil``vrc.blink`) |
| `build_viseme_map(shape_a, shape_o, shape_ch)` | `function` | Builds an `OrderedDict` mapping each VRChat name (`vrc.v_*`) to a list of `(shape_key, weight)` pairs computed from the three source shape keys |
#### `build_viseme_map` weights
| VRChat Viseme | Contributions |
|---------------|--------------|
| `vrc.v_aa` | A × 0.9998 |
| `vrc.v_ch` | CH × 0.9996 |
| `vrc.v_dd` | A × 0.3 + CH × 0.7 |
| `vrc.v_e` | A × 0.5 + CH × 0.2 |
| `vrc.v_ff` | A × 0.2 + CH × 0.4 |
| `vrc.v_ih` | CH × 0.7 + O × 0.3 |
| `vrc.v_kk` | A × 0.7 + CH × 0.4 |
| `vrc.v_nn` | A × 0.2 + CH × 0.7 |
| `vrc.v_oh` | A × 0.2 + O × 0.8 |
| `vrc.v_ou` | O × 0.9994 |
| `vrc.v_pp` | A × 0.0004 + O × 0.0004 |
| `vrc.v_rr` | CH × 0.5 + O × 0.3 |
| `vrc.v_sil`| A × 0.0002 + CH × 0.0002 |
| `vrc.v_ss` | CH × 0.8 |
| `vrc.v_th` | A × 0.4 + O × 0.15 |
---
### `utilities/helpers.py`
| Name | Type | Description |
|------|------|-------------|
| `_PreviewState` | `class` | Holds `active` (bool) and `saved_values` (dict) representing the current preview state |
| `_ps` | `_PreviewState` | Global instance shared across all modules |
| `guess_shape(mesh_obj, candidates)` | `function` | Iterates over `candidates` and returns the first name found among the mesh's shape keys, or `''` |
| `_save_shapekey_values(mesh_obj)` | `function` | Saves the current values of all shape keys into `_ps.saved_values` |
| `_restore_shapekey_values(mesh_obj)` | `function` | Restores shape key values from `_ps.saved_values` |
| `_reset_all_shapekeys(mesh_obj)` | `function` | Sets the value of every shape key on the mesh to `0.0` |
| `_apply_mix(mesh_obj, mix, intensity)` | `function` | Resets all shape keys then applies the `mix` list of `(name, weight)` pairs scaled by `intensity` |
---
### `operators/vrc.py`
Operators for VRChat viseme generation.
| Class | `bl_idname` | Description |
|-------|-------------|-------------|
| `VRCVIS_OT_AutoDetect` | `vrc_viseme.auto_detect` | Searches for common MMD shape key names and fills in the A/O/CH/Blink fields |
| `VRCVIS_OT_Preview` | `vrc_viseme.preview` | Toggles a live viewport preview of the selected viseme |
| `VRCVIS_OT_UpdatePreview` | `vrc_viseme.update_preview` | Refreshes the preview when the selected viseme or intensity changes |
| `VRCVIS_OT_Generate` | `vrc_viseme.generate` | Generates the 15 `vrc.v_*` shape keys plus `vrc.blink` by mixing the three source shape keys |
| `VRCVIS_OT_RemoveAll` | `vrc_viseme.remove_all` | Deletes all shape keys whose name starts with `vrc.` |
| `VRCVIS_OT_Reorder` | `vrc_viseme.reorder` | Reorders `vrc.*` shape keys according to `VRC_ORDER`, placing them immediately after `Basis` |
---
### `operators/mmd.py`
Operators for EN/VRChat → Japanese MMD conversion.
| Class | `bl_idname` | Description |
|-------|-------------|-------------|
| `VRCVIS_OT_MMDAutoDetect` | `vrc_viseme.mmd_auto_detect` | Automatically detects English/VRChat names and fills in the MMD fields |
| `VRCVIS_OT_MMDConvert` | `vrc_viseme.mmd_convert` | Duplicates shape keys with Japanese MMD names and inserts the separators |
| `VRCVIS_OT_MMDRemove` | `vrc_viseme.mmd_remove` | Removes all Japanese MMD shape keys and separators |
---
### `panels/main_panel.py`
| Name | Type | Description |
|------|------|-------------|
| `viseme_enum_items(self, context)` | `function` | Callback for the preview `EnumProperty`; returns all VRChat visemes plus `vrc.blink` |
| `VRCVIS_PT_Panel` | `bpy.types.Panel` | Main panel in the Blender Sidebar; `bl_idname = VIEW3D_PT_visemes_converter` |
---
## Scene Properties (`bpy.types.Scene`)
All properties are registered by `register()` in `visemes.py` with the `vrcvis_` prefix.
| Property | Type | Description |
|----------|------|-------------|
| `vrcvis_mesh` | `StringProperty` | Name of the target mesh |
| `vrcvis_mouth_a` | `StringProperty` | Open-mouth shape key (あ / A) |
| `vrcvis_mouth_o` | `StringProperty` | Round-mouth shape key (お / O) |
| `vrcvis_mouth_ch` | `StringProperty` | Narrow-mouth shape key (い / CH) |
| `vrcvis_blink` | `StringProperty` | Eyes-closed shape key (まばたき / Blink) |
| `vrcvis_intensity` | `FloatProperty` | Global weight multiplier (0.0 2.0, default 1.0) |
| `vrcvis_overwrite` | `BoolProperty` | If `True`, regenerates already-existing shape keys |
| `vrcvis_preview_active` | `BoolProperty` | Internal preview state (True = preview on) |
| `vrcvis_preview_viseme` | `EnumProperty` | Viseme selected for preview |
| `vrcvis_mmd_{suffix}` | `StringProperty` | One property per entry in `MMD_JP_MAPPING` (suffix from column 2) |
---
## Typical Workflow
```
1. Select a mesh with shape keys in the Mesh field
2. Press "Auto-detect (MMD → VRC)" to detect A / O / CH / Blink
or select them manually
3. (Optional) Use "Preview Viseme" to check the result before generating
4. Press "Generate VRChat Visemes" → creates the vrc.v_* + vrc.blink shape keys
5. (Optional) Press "Reorder VRC Keys" to apply the standard VRChat order
6. (Optional) In the MMD section:
a. "Auto-detect (EN → JP)" to map English names to Japanese
b. "Convert to MMD" to create Japanese shape keys with separators
```

16
operators/__init__.py Normal file
View 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
View 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
View 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'}

4
panels/__init__.py Normal file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from .main_panel import VRCVIS_PT_Panel, viseme_enum_items

130
panels/main_panel.py Normal file
View File

@@ -0,0 +1,130 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import bpy
from utilities.constants import MMD_VISEME_COUNT
from utilities.functions import MMD_JP_MAPPING, VRC_ORDER, build_viseme_map
def viseme_enum_items(self, context):
vmap = build_viseme_map('_', '_', '_')
items = [(k, k, '') for k in vmap.keys()]
items.append(('vrc.blink', 'vrc.blink', ''))
return items
class VRCVIS_PT_Panel(bpy.types.Panel):
bl_idname = 'VIEW3D_PT_visemes_converter'
bl_label = 'Visemes Converter'
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Visemes Tool'
def draw(self, context):
layout = self.layout
scene = context.scene
box = layout.box()
box.label(text='Mesh', icon='MESH_DATA')
mesh_objects = [o for o in bpy.data.objects if o.type == 'MESH' and o.data.shape_keys]
if not mesh_objects:
box.label(text='Nessuna mesh con shape key', icon='ERROR')
return
box.prop_search(scene, 'vrcvis_mesh', bpy.data, 'objects', text='')
mesh_name = scene.vrcvis_mesh
if not mesh_name or mesh_name not in bpy.data.objects:
layout.label(text='Seleziona una mesh', icon='INFO')
return
obj = bpy.data.objects[mesh_name]
if obj.type != 'MESH' or not obj.data.shape_keys:
layout.label(text='Mesh senza shape key', icon='ERROR')
return
layout.separator(factor=0.3)
box2 = layout.box()
box2.label(text='VRChat Visemes (MMD → vrc.*)', icon='SHAPEKEY_DATA')
row = box2.row()
row.operator('vrc_viseme.auto_detect', icon='VIEWZOOM', text='Auto-rileva (MMD → VRC)')
box2.separator(factor=0.5)
row = box2.row(align=True)
row.label(text='A (あ):')
row.prop_search(scene, 'vrcvis_mouth_a', obj.data.shape_keys, 'key_blocks', text='')
row = box2.row(align=True)
row.label(text='O (お):')
row.prop_search(scene, 'vrcvis_mouth_o', obj.data.shape_keys, 'key_blocks', text='')
row = box2.row(align=True)
row.label(text='CH (い):')
row.prop_search(scene, 'vrcvis_mouth_ch', obj.data.shape_keys, 'key_blocks', text='')
row = box2.row(align=True)
row.label(text='Blink:')
row.prop_search(scene, 'vrcvis_blink', obj.data.shape_keys, 'key_blocks', text='')
box2.separator(factor=0.5)
box2.prop(scene, 'vrcvis_intensity', text='Intensità')
box2.prop(scene, 'vrcvis_overwrite', text='Sovrascrivi esistenti')
box2.separator(factor=0.5)
row = box2.row(align=True)
row.scale_y = 1.2
if scene.vrcvis_preview_active:
row.operator('vrc_viseme.preview', text='Stop Preview', icon='PAUSE')
row2 = box2.row(align=True)
row2.prop(scene, 'vrcvis_preview_viseme', text='')
row2.operator('vrc_viseme.update_preview', text='', icon='FILE_REFRESH')
else:
row.operator('vrc_viseme.preview', text='Preview Viseme', icon='PLAY')
box2.separator(factor=0.5)
row = box2.row(align=True)
row.scale_y = 1.4
row.operator('vrc_viseme.generate', icon='TRIA_RIGHT')
row2 = box2.row(align=True)
row2.operator('vrc_viseme.reorder', icon='SORTSIZE')
row2.operator('vrc_viseme.remove_all', icon='X')
if obj.data.shape_keys:
existing_vrc = [k.name for k in obj.data.shape_keys.key_blocks if k.name.startswith('vrc.')]
if existing_vrc:
total = len(VRC_ORDER)
box5 = box2.box()
box5.label(text=f'{len(existing_vrc)}/{total} vrc.* presenti', icon='CHECKMARK')
col = box5.column(align=True)
for name in VRC_ORDER:
r = col.row(align=True)
r.label(text=name, icon='CHECKBOX_HLT' if name in existing_vrc else 'CHECKBOX_DEHLT')
layout.separator(factor=0.3)
box_mmd = layout.box()
box_mmd.label(text='Converti in MMD (EN/VRC → JP)', icon='FILE_REFRESH')
row = box_mmd.row()
row.operator('vrc_viseme.mmd_auto_detect', icon='VIEWZOOM', text='Auto-rileva (EN → JP)')
box_mmd.separator(factor=0.4)
box_mmd.label(text='--- MMD Visemes ---', icon='SHAPEKEY_DATA')
for _jp, suffix, en_label, _ in MMD_JP_MAPPING[:MMD_VISEME_COUNT]:
row = box_mmd.row(align=True)
row.label(text=en_label + ':')
row.prop_search(scene, f'vrcvis_mmd_{suffix}',
obj.data.shape_keys, 'key_blocks', text='')
box_mmd.separator(factor=0.4)
box_mmd.label(text='--- Altre forme ---', icon='SHAPEKEY_DATA')
for _jp, suffix, en_label, _ in MMD_JP_MAPPING[MMD_VISEME_COUNT:]:
row = box_mmd.row(align=True)
row.label(text=en_label + ':')
row.prop_search(scene, f'vrcvis_mmd_{suffix}',
obj.data.shape_keys, 'key_blocks', text='')
box_mmd.separator(factor=0.5)
row = box_mmd.row(align=True)
row.scale_y = 1.4
row.operator('vrc_viseme.mmd_convert', icon='DUPLICATE')
row.operator('vrc_viseme.mmd_remove', icon='X')

18
utilities/constants.py Normal file
View File

@@ -0,0 +1,18 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
MMD_VISEME_COUNT = 5
MMD_SEP_1 = '---MMD Visemes---'
MMD_SEP_2 = '^MMD Visemes / Other v'
MMD_CANDIDATES = {
'a': ['', 'a', 'A', 'mouth_a', 'Mouth_A', 'あ口', 'v_a'],
'o': ['', 'o', 'O', 'mouth_o', 'Mouth_O', 'お口', 'v_o'],
'ch': ['', 'ch', 'CH', 'mouth_i', 'Mouth_I', 'い口', 'v_ch', 'i', 'I'],
'blink': [
'まばたき', '目閉じ', '眼閉じ', 'ウィンク2',
'Blink', 'blink', 'blink_all',
'eye_blink', 'Eye_Blink', 'eyes_blink', 'Eyes_Blink',
'eye_close', 'Eye_Close', 'eyes_close', 'Eyes_Close',
],
}

61
utilities/functions.py Normal file
View File

@@ -0,0 +1,61 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from collections import OrderedDict
MMD_JP_MAPPING = [
('', 'a', 'A (あ)', ['ah', 'a', 'A', 'vrc.v_aa', '']),
('', 'i', 'I / CH (い)', ['ch', 'i', 'I', 'CH', 'vrc.v_ch', 'vrc.v_ih', '']),
('', 'u', 'U (う)', ['u', 'U', 'vrc.v_ou', '']),
('', 'e', 'E (え)', ['e', 'E', 'vrc.v_e', '']),
('', 'o', 'O (お)', ['oh', 'o', 'O', 'vrc.v_oh', '']),
('笑い', 'warau', 'Blink Happy (笑い)', ['blink_happy', 'BlinkHappy', 'blink happy', 'smile', '笑い']),
('はう', 'hau', 'Close >< (はう)', ['close><', 'hau', 'はう']),
('ウィンク', 'wink', 'Wink (ウィンク)', ['wink', 'Wink', 'ウィンク']),
('ウィンク右', 'wink_r', 'Wink Right (ウィンク右)', ['wink_right', 'WinkRight', 'wink right', 'ウィンク右']),
('ウィンク2', 'wink2', 'Wink 2 (ウィンク2)', ['wink_2', 'wink2', 'Wink2', 'wink 2', 'ウィンク2']),
('ウィンク2右','wink2_r', 'Wink 2 Right (ウィンク2右)',['wink_2_right', 'Wink2Right', 'wink 2 right', 'ウィンク2右']),
('にこり', 'nikori', 'Cheerful (にこり)', ['cheerful', 'Cheerful', 'nikori', 'にこり']),
('真面目', 'majime', 'Serious (真面目)', ['serious', 'Serious', 'stare', 'calm', '真面目']),
('怒り', 'ikari', 'Anger (怒り)', ['anger', 'angry', 'Anger', 'Angry', '怒り']),
('困る', 'komaru', 'Sadness (困る)', ['sadness', 'sad', 'Sadness', 'Sad', 'komaru', '困る']),
]
VRC_ORDER = [
'vrc.v_sil',
'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_ss',
'vrc.v_th',
'vrc.blink',
]
def build_viseme_map(shape_a, shape_o, shape_ch):
m = OrderedDict()
m['vrc.v_aa'] = [(shape_a, 0.9998)]
m['vrc.v_ch'] = [(shape_ch, 0.9996)]
m['vrc.v_dd'] = [(shape_a, 0.3), (shape_ch, 0.7)]
m['vrc.v_e'] = [(shape_a, 0.5), (shape_ch, 0.2)]
m['vrc.v_ff'] = [(shape_a, 0.2), (shape_ch, 0.4)]
m['vrc.v_ih'] = [(shape_ch, 0.7), (shape_o, 0.3)]
m['vrc.v_kk'] = [(shape_a, 0.7), (shape_ch, 0.4)]
m['vrc.v_nn'] = [(shape_a, 0.2), (shape_ch, 0.7)]
m['vrc.v_oh'] = [(shape_a, 0.2), (shape_o, 0.8)]
m['vrc.v_ou'] = [(shape_o, 0.9994)]
m['vrc.v_pp'] = [(shape_a, 0.0004), (shape_o, 0.0004)]
m['vrc.v_rr'] = [(shape_ch, 0.5), (shape_o, 0.3)]
m['vrc.v_sil'] = [(shape_a, 0.0002), (shape_ch, 0.0002)]
m['vrc.v_ss'] = [(shape_ch, 0.8)]
m['vrc.v_th'] = [(shape_a, 0.4), (shape_o, 0.15)]
return m

42
utilities/helpers.py Normal file
View File

@@ -0,0 +1,42 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
class _PreviewState:
active = False
saved_values: dict = {}
_ps = _PreviewState()
def guess_shape(mesh_obj, candidates):
if not mesh_obj or not mesh_obj.data.shape_keys:
return ''
keys = [k.name for k in mesh_obj.data.shape_keys.key_blocks]
for c in candidates:
if c in keys:
return c
return ''
def _save_shapekey_values(mesh_obj):
_ps.saved_values = {k.name: k.value for k in mesh_obj.data.shape_keys.key_blocks}
def _restore_shapekey_values(mesh_obj):
for k in mesh_obj.data.shape_keys.key_blocks:
k.value = _ps.saved_values.get(k.name, 0.0)
def _reset_all_shapekeys(mesh_obj):
for k in mesh_obj.data.shape_keys.key_blocks:
k.value = 0.0
def _apply_mix(mesh_obj, mix, intensity):
_reset_all_shapekeys(mesh_obj)
keys = mesh_obj.data.shape_keys.key_blocks
for shape_name, weight in mix:
if shape_name in keys:
keys[shape_name].value = weight * intensity

111
visemes.py Normal file
View File

@@ -0,0 +1,111 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Credits:
# Thanks to https://github.com/ShyWolf42/Copy-to-MMD-Visemes
# Thanks to https://github.com/teamneoneko/Cats-Blender-Plugin
bl_info = {
"name": "Visemes Converter Tool",
"author": "Custom",
"version": (2, 0, 0),
"blender": (3, 0, 0),
"location": "View3D > Sidebar > Visemes Tool",
"description": (
"Generates VRChat visemes (vrc.v_* + vrc.blink) from MMD blendshapes, "
"and converts blendshapes from English/VRChat to Japanese MMD format"
),
"category": "Rigging",
}
import bpy
from operators import (
VRCVIS_OT_AutoDetect,
VRCVIS_OT_Preview,
VRCVIS_OT_UpdatePreview,
VRCVIS_OT_Generate,
VRCVIS_OT_RemoveAll,
VRCVIS_OT_Reorder,
VRCVIS_OT_MMDAutoDetect,
VRCVIS_OT_MMDConvert,
VRCVIS_OT_MMDRemove,
)
from panels import VRCVIS_PT_Panel, viseme_enum_items
from utilities.functions import MMD_JP_MAPPING
from utilities.helpers import _ps, _restore_shapekey_values
_CLASSES = [
VRCVIS_OT_AutoDetect,
VRCVIS_OT_Preview,
VRCVIS_OT_UpdatePreview,
VRCVIS_OT_Generate,
VRCVIS_OT_RemoveAll,
VRCVIS_OT_Reorder,
VRCVIS_OT_MMDAutoDetect,
VRCVIS_OT_MMDConvert,
VRCVIS_OT_MMDRemove,
VRCVIS_PT_Panel,
]
_MMD_PROPS = [f'vrcvis_mmd_{suffix}' for _, suffix, _en, _ in MMD_JP_MAPPING]
def register():
for cls in _CLASSES:
bpy.utils.register_class(cls)
bpy.types.Scene.vrcvis_mesh = bpy.props.StringProperty(
name='Mesh', description='Mesh su cui operare', default='')
bpy.types.Scene.vrcvis_mouth_a = bpy.props.StringProperty(
name='Mouth A', description='Shape key bocca aperta (あ / A)', default='')
bpy.types.Scene.vrcvis_mouth_o = bpy.props.StringProperty(
name='Mouth O', description='Shape key bocca rotonda (お / O)', default='')
bpy.types.Scene.vrcvis_mouth_ch = bpy.props.StringProperty(
name='Mouth CH', description='Shape key bocca stretta (い / CH)', default='')
bpy.types.Scene.vrcvis_blink = bpy.props.StringProperty(
name='Blink', description='Shape key occhi chiusi (まばたき / Blink)', default='')
bpy.types.Scene.vrcvis_intensity = bpy.props.FloatProperty(
name='Intensità', description='Moltiplicatore globale dei pesi',
default=1.0, min=0.0, max=2.0, step=1)
bpy.types.Scene.vrcvis_overwrite = bpy.props.BoolProperty(
name='Sovrascrivi esistenti',
description='Rigenera le shape key già presenti',
default=True)
bpy.types.Scene.vrcvis_preview_active = bpy.props.BoolProperty(
name='Preview attiva', default=False)
bpy.types.Scene.vrcvis_preview_viseme = bpy.props.EnumProperty(
name='Viseme', description='Viseme da mostrare in anteprima',
items=viseme_enum_items)
for jp_name, suffix, _en, _ in MMD_JP_MAPPING:
setattr(bpy.types.Scene, f'vrcvis_mmd_{suffix}',
bpy.props.StringProperty(
name=jp_name,
description=f'Shape key sorgente per {jp_name}',
default=''))
def unregister():
scene = bpy.context.scene if bpy.context else None
if _ps.active and scene:
mesh_name = scene.get('vrcvis_mesh', '')
if mesh_name and mesh_name in bpy.data.objects:
_restore_shapekey_values(bpy.data.objects[mesh_name])
_ps.active = False
for cls in reversed(_CLASSES):
bpy.utils.unregister_class(cls)
for prop in (
'vrcvis_mesh', 'vrcvis_mouth_a', 'vrcvis_mouth_o', 'vrcvis_mouth_ch',
'vrcvis_blink', 'vrcvis_intensity', 'vrcvis_overwrite',
'vrcvis_preview_active', 'vrcvis_preview_viseme',
*_MMD_PROPS,
):
if hasattr(bpy.types.Scene, prop):
delattr(bpy.types.Scene, prop)
if __name__ == '__main__':
register()