First Commit
This commit is contained in:
77
CONTRIBUTING.md
Normal file
77
CONTRIBUTING.md
Normal 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
170
DOCUMENTATION.md
Normal 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
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'}
|
||||
4
panels/__init__.py
Normal file
4
panels/__init__.py
Normal 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
130
panels/main_panel.py
Normal 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
18
utilities/constants.py
Normal 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
61
utilities/functions.py
Normal 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
42
utilities/helpers.py
Normal 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
111
visemes.py
Normal 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()
|
||||
Reference in New Issue
Block a user