Compare commits

..

108 Commits

Author SHA1 Message Date
Yusarina 82a7e67d7e Merge pull request #149 from teamneoneko/Current-Dev
Avatar Toolkit 0.2.1
2025-04-02 16:55:30 +01:00
Yusarina 87c40c02d6 Merge pull request #148 from Yusarina/Current-Dev
Small fixes
2025-04-02 16:54:08 +01:00
Yusarina 2ad5393f06 Removed this as in Utils 2025-04-01 18:09:50 +01:00
Yusarina 12083c28c5 Small fixes 2025-04-01 17:32:45 +01:00
Onan Chew c67f30fb97 Merge pull request #147 from Yusarina/Current-Dev
Move simpify_bonename to dictionaries
2025-03-31 20:01:42 -04:00
Yusarina 5a43a9d66d Fix two different error
- bones_names is a dictionary so bone_names.items should be used
- also len(mappings) was being used incorrectly should be using range(len(mappings))
- The last one is it just didn't like upperchest in bone_names.update so as a very temp solution i used upper_chest no an ideal solution but I too tired to investogate today and just wanted the plugin to work. however it should be simpilify anyone for now.
2025-04-01 00:56:56 +01:00
Yusarina 1ca45ad901 Move simpify_bonename to dictionaries 2025-04-01 00:45:56 +01:00
Yusarina bc034c5308 Merge pull request #146 from 989onan/patch-1
Create centralized method for identifying bones
2025-04-01 00:05:14 +01:00
989onan eba18d72a6 Fix poisoned name sets
this is a simple patch because policing the standard is literally impossible
2025-03-31 18:45:30 -04:00
Onan Chew 416fbe40e7 Merge branch 'Current-Dev' into patch-1 2025-03-31 18:36:53 -04:00
Onan Chew d296d548e8 Merge pull request #145 from Yusarina/Current-Dev
Texture Atlas now requries the user to save before we allow them to atlas.
2025-03-31 18:29:50 -04:00
989onan feb2f5ac85 Create centralized method for identifying bones
- also fixes an issue where VRM bones would never be identified due to the names having "_" in them.
2025-03-31 18:28:04 -04:00
Yusarina 0ff2dc1c38 I swear I fixed the issue before we Armautre and Mesh attach, also fixed permission. 2025-03-31 23:22:35 +01:00
Yusarina a407e99ebd Update atlas_materials.py 2025-03-31 23:03:09 +01:00
Yusarina ff5efc9639 Update blender_manifest.toml 2025-03-31 22:51:35 +01:00
Yusarina 345ba44463 Texture Atlas should require the user to save the blender files before we allow them to atlas 2025-03-31 22:39:49 +01:00
Onan Chew b67d94e89d Merge pull request #143 from Yusarina/Current-Dev
Alpha 2 0.2.1
2025-03-31 13:49:03 -04:00
Yusarina 64a78dbbb2 Remove the annoying logger about non standard bones 2025-03-31 13:57:12 +01:00
Yusarina 1e8784d0e4 Alpha 2 0.2.1 2025-03-31 13:49:44 +01:00
Yusarina c0943e0d20 Atlas Materials UI Update
- Separated things out so it looks better.
- Done small changes to make the ui look a bit better.
- UI auto refreshes after a successful atlas.
2025-03-31 13:48:52 +01:00
Yusarina af5b79e314 Removed extra registrations in properties
This was a hot fix for some issues we were having, however this is no longer needed as my fix to auto loader fixed the issues which meant we were trying to double regisiter these things.
2025-03-31 13:05:50 +01:00
Yusarina 334f299e0e User Preferences should save in blenders user dictionary not in the Plugins Dictionary 2025-03-31 13:03:37 +01:00
Onan Chew 9a5f13f858 Merge pull request #137 from Yusarina/Current-Dev
Auto load now works with custom blender extensions folders
2025-03-27 18:34:13 -04:00
Yusarina 77b7b429a5 Remove wheel installation
- Blender should handle this for us, we were installing system wide which is bad.
2025-03-27 21:05:59 +00:00
Yusarina 9a0521dad5 Fix because i stupid 2025-03-27 20:44:39 +00:00
Yusarina d7fee2c961 I don't need to add that check duh 2025-03-27 20:42:43 +00:00
Yusarina 357aa1b6d9 Let's not use System path 2025-03-27 20:16:27 +00:00
Yusarina 02c73ccd2a Auto load now works with custom blender extensions folders
- Enable autoload, load things from outside the default blender folder, before is a user tried to install the plugin outside from blender default extension folder it will error. I found this out when i broke my own blender helping someone.
2025-03-27 18:00:46 +00:00
Yusarina 940854cade Merge pull request #134 from teamneoneko/Current-Dev
Bring Alpha 2 to current
2025-03-25 20:07:33 +00:00
Yusarina 7a1531fbd6 Merge branch 'Current' into Current-Dev 2025-03-25 20:07:27 +00:00
Yusarina 672517a771 Merge pull request #133 from teamneoneko/Alpha-2
Finale Alpha 2
2025-03-25 20:04:00 +00:00
Yusarina 4a039421f5 Merge pull request #132 from Yusarina/Alpha-2
Alpha 2 Finale Updates
2025-03-25 20:03:34 +00:00
Yusarina 15101fa887 Ensure Alpha 2 can mot load on Blender 4.5 or above. 2025-03-25 20:02:56 +00:00
Yusarina 5411acca45 New updater from Alpha 1 2025-03-25 19:53:57 +00:00
Yusarina eeb41dec40 Update ko_KR.json 2025-03-25 19:42:29 +00:00
Yusarina 10fb112de7 Update ja_JP.json 2025-03-25 19:42:23 +00:00
Yusarina c532e2a6a0 Update en_US.json 2025-03-25 19:42:17 +00:00
Yusarina 4df5369cbb Merge pull request #131 from Yusarina/Current
Stop this version from loading on newer blenders
2025-03-25 19:41:54 +00:00
Yusarina 36550a42e5 Stop this version from loading on newer blenders 2025-03-25 19:41:33 +00:00
Yusarina 2dc3a19283 Update blender_manifest.toml 2025-03-25 19:34:52 +00:00
Yusarina 30115eeaac Update ko_KR.json 2025-03-25 19:34:09 +00:00
Yusarina 285c331f79 Update ja_JP.json 2025-03-25 19:34:02 +00:00
Yusarina 57ded41f2f Update en_US.json 2025-03-25 19:33:56 +00:00
Yusarina 948b1bb352 Merge pull request #130 from Yusarina/Current
Updated Alpha 1 updater
2025-03-25 19:33:31 +00:00
Yusarina 9104bfae67 Updated Alpha 1 updater
- Updater will only look for Alpha 1 updates now
- Will check for updates automatically when the updater tab is opened.

(Same for Alpha 2, just needed update to Alpha 1 as Alpha 2 is for Blender 4.4)
2025-03-25 19:32:49 +00:00
Yusarina 23b4462f7e Update Translations 2025-03-25 19:05:44 +00:00
Yusarina dd3d21d9d5 Merge pull request #129 from teamneoneko/Alpha-2
Push Alpha 2 into the current dev branch
2025-03-24 23:13:47 +00:00
Yusarina 73c2404010 Merge pull request #128 from Yusarina/Alpha-2
Alpha 2 Finale Updates Before Release
2025-03-24 23:00:20 +00:00
Yusarina 546fec6039 Bug fixes
- When i updated Wheels I used the Python 13 one, blender is Python 11 so provided the correct wheels packages. Also added macos 10.9 for Intel users.
- Fixed issue where Join Meshes, made it faster as well.
- Fixed issues with viseme generation and previewing.
- Removed MMD Panel and Tools.
- Fixed issue with armature merging
- Fixed reset eye tracking button throwing errors in the SDK2 panel.
- Added info in the SDK2 panel about this only being used for other games, VRChat Eye tracking is setup in Unity.
- Fixed issue where if models have upper ears and lower ears the amrature validation system would mark it as problem bones.
- Added instructions to armature validation about other bones and re the standardization button.
2025-03-24 22:58:51 +00:00
Yusarina c90bf4e36c Merge pull request #127 from Yusarina/Amrature-Validation-P2
Bone Standardization
2025-03-24 21:13:32 +00:00
Yusarina 5b9acb496f Bone Standardization 2025-03-24 19:06:57 +00:00
Yusarina 08a65f9fa7 Update README.md 2025-03-24 16:41:35 +00:00
Yusarina ba85666d9f Update README.md 2025-03-24 16:41:22 +00:00
Yusarina 52a51ea6a2 Merge pull request #125 from Yusarina/Alpha-2-Prearing
Alpha 2 Preparing
2025-03-24 16:39:01 +00:00
Yusarina 47e3ea2d29 Merge pull request #124 from Yusarina/Amrature-Validation-P2
Armature Validation P2
2025-03-24 16:17:09 +00:00
Yusarina 12d06638fe Updates
- Removed the PMX Importer Completely for now.
- Updated version number to 0.2.0
- Updated min Blender version to 4.4.0
- Updated lz4 wheels to the latest version.
2025-03-24 02:26:19 +00:00
Yusarina 2a2c3d3973 Merge branch 'Alpha-2' into Amrature-Validation-P2 2025-03-24 02:14:41 +00:00
Yusarina c65bed3ff4 Armature Validation P2
- Added Highlight Bone System in the 3D View, can be turned off in settings.
- Added more bones to the acceptable bone lists.
- Fixed issue with properties registrations and unregistration, the system is more rebust now.
- Added a validate t-pose system
- Added a detect bone scales system.
- Fixed some translation strings
- Armature validation now uses logger system.
2025-03-24 02:12:03 +00:00
Onan Chew abc1fe955b Merge pull request #121 from Yusarina/alpha2-bugfixes
Bug Fixes for Alpha 2
2025-03-23 20:10:48 -04:00
Yusarina dac25e0dc0 Bug Fixes
- Fixes issue where some tools would not be displayed, fixes: #120
- Fixes issue with the resonite utils throwing errors.
- Fixes issue with visemes panel throwing errors.
- Fixes issue where the viseme mesh selector was showing all objects (Armature and etc) it now just shows meshes.
2025-03-23 13:38:18 +00:00
Yusarina 6d71669849 Update README.md 2025-03-02 15:09:36 +00:00
Yusarina b946041ec1 Merge pull request #119 from 989onan/PoseMode
fix bad armature merging issues
2025-02-19 00:37:01 +00:00
989onan 07adaa590b fix bad armature merging issues
also merge all bones isn't needed. we should do that by default

This also now uses dictionary matching to find bone types like hips, spine, and chest that should be merged.

Deletes bone shared and merges armatures, and parents bones back, causing a seamless merge.
2025-02-18 19:30:56 -05:00
Onan Chew 855bb84e76 Merge pull request #117 from Yusarina/Armature_Validation_Version2
Armature validation version 2 Part 1 (Don't merge yet)
2025-02-08 11:39:10 -05:00
Yusarina fbb07aec10 Fixes
- Fixed issue with plugin registration
- Fixed bones symmetry not working
- Fixed different modes not working
-  Some other small fixes.
2025-02-08 14:18:53 +00:00
Yusarina dd36ccaece Acceptable Standards Added 2025-02-08 11:03:22 +00:00
Yusarina 017633696a Merge branch 'teamneoneko:Current' into Armature_Validation_Version2 2025-02-07 18:43:25 +00:00
Yusarina 71cba9a40f Merge branch 'Alpha-2' into Armature_Validation_Version2 2025-02-07 18:43:00 +00:00
Yusarina 53cc5c28ae Merge pull request #118 from Yusarina/texture-atlas
Texture atlas
2025-02-07 18:38:21 +00:00
Yusarina 21ddc20119 Merge branch 'Alpha-2' into texture-atlas 2025-02-07 18:38:12 +00:00
Yusarina 2524634ef4 Update common.py 2025-02-07 18:33:56 +00:00
Yusarina bf6a32febb Went into wrong branch, whoops
This went into the current branch as it was approved I just going to directly add into Alpha 2.
2025-02-07 18:31:04 +00:00
Yusarina 4576b27b53 Merge branch 'Alpha-2' into Armature_Validation_Version2 2025-02-07 18:23:00 +00:00
Yusarina 6412b6f619 Better checks
- Added standard list.
- Added bone_hierarchy list
- Added bone_hierarchy
- Better checks.
- Better UI.

This is the first part, still needs alot of work, but this is better then before. Need to add some more standards and then we will be golden.
2025-02-07 18:18:09 +00:00
Onan Chew f043c6099e Merge pull request #114 from Yusarina/logging-improvements
Improvement to logging.
2025-02-07 12:06:45 -05:00
Onan Chew a20a306582 Merge pull request #104 from Yusarina/rigfy-to-unity-Alpha2
Rigify To Unity (Don't Merge Yet)
2025-02-07 12:05:33 -05:00
Yusarina 4b59147649 Moved Armature Validation to it's own file 2025-02-07 16:04:54 +00:00
Yusarina 6038177383 Further logging and typing, couple of fixes 2025-02-07 15:35:30 +00:00
Yusarina 251c006498 Conversation Complete
- This now removes bones which are not needed for Unity.
- Renamed the armature to armature.
2025-02-07 15:27:48 +00:00
Onan Chew 3eb0029b5e Merge pull request #115 from Yusarina/texture-atlas
Added back texture Atlas
2025-02-04 03:12:00 -05:00
Yusarina 686bc0bda1 Added back texture Atlas
- Now working with Alpha 2.
- Did some changed but it should still work, did some basic testing.
- Do want to make further changes and make the system better where possible.
2025-02-04 04:06:34 +00:00
Yusarina 1482632405 Improvement to logging. 2025-02-01 21:39:43 +00:00
Onan Chew 2a7cb16fea Merge pull request #113 from Yusarina/logging-improvements
Improve Logging
2025-02-01 12:02:16 -05:00
Yusarina 1187949280 Improve Logging 2025-02-01 15:41:06 +00:00
Yusarina af9c597dd2 Merge pull request #106 from 989onan/Alpha-2
Fix merge doubles being slow and advanced doing nothing
2025-01-27 01:07:42 +00:00
989onan fc9b1e42a2 Fix remove doubles perf issue and fix it not working
Why did the merge doubles at point get removed? it was a core component.

Anyways this should fix the performance issue greatly and fix advanced mode literally doing nothing
2025-01-26 20:01:47 -05:00
Onan Chew af311d7d2e Merge pull request #105 from Yusarina/zero-weight-removal-improvements-alpha2
Remove Zero Weights Improvements
- Add option to not remove 0 weight bones with children for things like skirts
- add option for listing zero weight bones rather than going ahead with deleting them
- add option for removing zero weight bones only if they have deform enabled.
2025-01-26 10:43:44 -05:00
Yusarina 239e212cf4 Remove Zero Weights Improvements
- Added Options to preserve Parent Bones.
- Added List mode only where the user can select the bones there want to remove.
- Added Options to only target Deform bones only and non deform bones only.

This is complete, the UI needs a little cleanup but I do this in a UI cleanup nearer Alpha 2.
2025-01-26 15:22:20 +00:00
Yusarina 1333b4d2d4 Rigify To Unity
Not Complete Yet however this is almost ready for Prime time. This converts the basic human armature for righfy to a Unity standard.

This may work for the more advanced human rigfy armatures as well and I planning to improve this before Alpha 2.
2025-01-26 14:51:02 +00:00
Yusarina 7fb1b9a8a4 Merge pull request #103 from Yusarina/Alpha-2
Forgot To add translations
2025-01-25 18:00:18 +00:00
Yusarina 2283a44579 Forgot To add translations 2025-01-25 17:59:45 +00:00
Yusarina c7318fbd0c Merge pull request #102 from Yusarina/Alpha-2
Fix
2025-01-25 17:57:53 +00:00
Yusarina f376b06caf Fix 2025-01-25 17:57:33 +00:00
Yusarina 4b69832ca1 Merge pull request #101 from Yusarina/Alpha-2
Add UV Tools
2025-01-25 17:50:01 +00:00
Yusarina 071b8186c9 Add UV Tools 2025-01-25 17:48:54 +00:00
Yusarina 146dec71f8 Merge pull request #97 from teamneoneko/Current
Bring Alpha 2 up to date
2025-01-03 09:26:09 +00:00
Yusarina d7cc8096b9 Merge pull request #96 from teamneoneko/Current-Dev
Current dev
2025-01-03 09:25:19 +00:00
Yusarina 08f37d3202 Version Bump 2025-01-03 09:24:33 +00:00
Onan Chew cb0abf3053 Merge pull request #93 from Yusarina/Current-Dev
Fixes
2024-12-25 10:32:20 -05:00
Yusarina 2bb1826346 Importer Fix
- Not sure how I managed to just hardcode fbx only and not add import anything back.
2024-12-23 23:46:29 +00:00
Yusarina 9ad760bfb8 Fixes
- Fixes issue with addon registration which just randomly broke at some point
- Fixes issue where merge armatures decided to break due to me messing up with properties.
- Fixed issue where you still had to select the mesh in the 3D Scene for viseme creation even though we have a UI selector now.
2024-12-23 18:16:52 +00:00
Yusarina 44593813b2 Merge pull request #92 from teamneoneko/Current
Bring Alpha 2 up to date
2024-12-19 00:04:22 +00:00
Yusarina 4d1a468db1 Merge pull request #91 from teamneoneko/Current-Dev
Current dev
2024-12-19 00:03:44 +00:00
Onan Chew bf5de6665c Merge pull request #90 from Yusarina/Current-Dev
Fixes and Improvements (Version 0.1.1 release candiate).
2024-12-18 18:53:35 -05:00
Yusarina b776ef78cb Hopeful Fix to UV's Imploding 2024-12-18 23:32:17 +00:00
Yusarina cbc973b0be Combine Materials Add UV map synchronization 2024-12-18 23:28:45 +00:00
Yusarina 8665292c7b Fixes and Improvements
- Improved typing in some areas.
- Improved code readability in some areas.
- Delete bone constraints would error out if the user is in edit mode, we now start in Object mode first.
- Fixed Eye tracking Ajust string not being in the translation files.
- There is now a selection box to select the mesh in the current active armature for viseme creation instead of the user having to select it in the 3D scene.
- Viseme preview mode won't allow you to start it if your in a other mode, you need to be in Object mode now.
- Combine Materials won't allow you to start it if your in a other mode, you need to be in Object mode now.
- Added Japanese and Korean UI Languages.
2024-12-18 02:44:26 +00:00
Yusarina c5d07892c2 Version Bump 2024-12-18 00:32:39 +00:00
50 changed files with 5647 additions and 3257 deletions
+1
View File
@@ -1,3 +1,4 @@
*.pyc
.vscode/settings.json
core/preferences.json
+8 -5
View File
@@ -11,9 +11,12 @@ Join the Neoneko Discord here: https://discord.catsblenderplugin.xyz
Need a more stable toolset while Avatar Toolkit is in Alpha? Then please use Blender 4.x and use our Unofficial Cats Blender Plugin which you can find [here](https://github.com/unofficalcats/Cats-Blender-Plugin-Unofficial-).
### Support us:
If you like what we do and want to help support the development of cats you can do it on our pally.gg [here](https://pally.gg/p/teamneoneko) all money is split automatically between all developers and any support is appreciated.
## Blender version support policies.
You can find them on the wiki here [HERE](https://avatartoolkit.xyz/wiki.html?version=0.1.0#what-is-avatar-toolkits-version-support-policy)
You can find them on the wiki here [HERE](https://avatartoolkit.xyz/wiki.html?version=0.2.0#what-is-avatar-toolkits-version-support-policy)
## Features
@@ -22,8 +25,8 @@ See everything Avatar Toolkit has ot offer [here](https://avatartoolkit.xyz/wiki
## Requirements
1) Blender Version
- Blender 4.3 or newer is required
- Blender 4.3 is the current recommended version
- Blender 4.4 or newer is required
- Blender 4.4 is the current recommended version
2) Python Requirements
@@ -32,13 +35,13 @@ See everything Avatar Toolkit has ot offer [here](https://avatartoolkit.xyz/wiki
3) Recommended Setup
- Download Blender directly from https://blender.org
- Use Blender 4.3 for the best experience
- Use Blender 4.4 for the best experience
#### Additional Plugins Requirements.
Currently None.
## Installation
You can find out how to install Avatar Toolkit [here](https://avatartoolkit.xyz/wiki.html?version=0.1.0#how-to-install-avatar-toolkit)
You can find out how to install Avatar Toolkit [here](https://avatartoolkit.xyz/wiki.html?version=0.2.0#how-to-install-avatar-toolkit)
## Help
+32 -1
View File
@@ -1,11 +1,42 @@
import bpy
from bpy.app.handlers import persistent
modules = None
ordered_classes = None
def show_version_error_popup():
def draw(self, context):
self.layout.label(text="Sorry, this version of Avatar Toolkit does not work on this version of Blender.")
self.layout.label(text="Please check the GitHub repository for the correct version for your Blender.")
self.layout.operator("wm.url_open", text="Open GitHub Repository").url = "https://github.com/teamneoneko/Avatar-Toolkit"
bpy.context.window_manager.popup_menu(draw, title="Avatar Toolkit Version Error", icon='ERROR')
def register():
from .core import auto_load
import bpy
version = bpy.app.version
if version[0] > 4 or (version[0] == 4 and version[1] >= 5):
show_version_error_popup()
return
print("Starting registration")
# Import modules using relative imports
from . import core
from .core import auto_load
from .core.logging_setup import configure_logging
# Initialize logging
configure_logging(False)
auto_load.init()
auto_load.register()
# Verify property registration
if not hasattr(bpy.types.Scene, "avatar_toolkit"):
from .core.properties import register as register_properties
register_properties()
print("Registration complete")
def unregister():
+9 -5
View File
@@ -3,21 +3,25 @@
schema_version = "1.0.0"
id = "avatar_toolkit"
version = "0.1.0"
version = "0.2.1"
name = "Avatar Toolkit"
tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games."
maintainer = "Team NekoNeo"
type = "add-on"
blender_version_min = "4.3.0"
blender_version_min = "4.4.0"
license = [
"SPDX:GPL-3.0-or-later",
]
wheels = [
"./wheels/lz4-4.3.3-cp311-cp311-macosx_11_0_arm64.whl",
"./wheels/lz4-4.3.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
"./wheels/lz4-4.3.3-cp311-cp311-win_amd64.whl"
"./wheels/lz4-4.4.3-cp311-cp311-macosx_11_0_arm64.whl",
"./wheels/lz4-4.3.3-cp311-cp311-macosx_10_9_x86_64.whl",
"./wheels/lz4-4.4.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
"./wheels/lz4-4.4.3-cp311-cp311-win_amd64.whl"
]
[permissions]
network = "For the auto updater to work, you need to allow network access"
files = "Import/Export files, saving atlas images, saving preferences"
+10 -4
View File
@@ -6,9 +6,14 @@ from ..core.logging_setup import logger
from bpy.types import AddonPreferences
from typing import Any, Dict
# Get the directory of the current file
PREFERENCES_DIR = os.path.dirname(os.path.abspath(__file__))
PREFERENCES_FILE = os.path.join(PREFERENCES_DIR, "preferences.json")
# Get the user preferences directory instead of addon directory
def get_preferences_path():
user_path = bpy.utils.resource_path('USER')
addon_prefs_dir = os.path.join(user_path, "config", "avatar_toolkit_prefs")
os.makedirs(addon_prefs_dir, exist_ok=True)
return os.path.join(addon_prefs_dir, "preferences.json")
PREFERENCES_FILE = get_preferences_path()
def get_current_version():
main_dir = os.path.dirname(os.path.dirname(__file__))
@@ -59,4 +64,5 @@ def get_addon_preferences(context):
if not os.path.exists(PREFERENCES_FILE):
save_preference("language", 0) # Set default language to 0 (auto)
save_preference("validation_mode", "STRICT") # Set default validation mode
save_preference("enable_logging", False) # Set default logging mode
save_preference("enable_logging", False) # Set default logging mode
save_preference("highlight_problem_bones", True) # Set default bone highlighting
+566
View File
@@ -0,0 +1,566 @@
import bpy
import math
from mathutils import Vector, Color
from typing import Tuple, List, Dict, Set, Optional, Union
from bpy.types import Object, Bone, Operator
from ..core.common import get_armature_list, get_active_armature
from ..core.translations import t
from ..core.dictionaries import (
standard_bones,
bone_hierarchy,
finger_hierarchy,
acceptable_bone_hierarchy,
acceptable_bone_names
)
from ..core.logging_setup import logger
def validate_armature(armature: Object, detailed_messages: bool = False) -> Union[Tuple[bool, List[str], bool], Tuple[bool, List[str], bool, List[str], List[str], List[str]]]:
"""
Validates armature and returns validation results
"""
logger.debug(f"Validating armature: {armature.name if armature else 'None'}")
validation_mode = bpy.context.scene.avatar_toolkit.validation_mode
messages: List[str] = []
hierarchy_messages: List[str] = []
non_standard_messages: List[str] = []
scale_messages: List[str] = []
if validation_mode == 'NONE':
logger.debug("Validation mode is NONE, skipping validation")
if detailed_messages:
return True, [], False, [], [], []
else:
return True, [], False
if not armature or armature.type != 'ARMATURE' or not armature.data.bones:
logger.warning("Basic armature check failed")
if detailed_messages:
return False, [t("Armature.validation.basic_check_failed")], False, [], [], []
else:
return False, [t("Armature.validation.basic_check_failed")], False
found_bones: Dict[str, Bone] = {bone.name: bone for bone in armature.data.bones}
logger.debug(f"Found {len(found_bones)} bones in armature")
is_acceptable = check_acceptable_standards(found_bones)
# List all bones in armature
bone_list = "\n".join([f"- {bone}" for bone in found_bones.keys()])
messages.append(t("Armature.validation.found_bones", bones=bone_list))
# Basic validation for both STRICT and LIMITED modes
# Check for missing required bones
essential_bones = {standard_bones[key] for key in ['hips', 'spine', 'chest', 'neck', 'head']}
missing_bones = [bone for bone in essential_bones if bone not in found_bones]
if missing_bones:
missing_list = "\n".join([f"- {bone}" for bone in missing_bones])
logger.warning(f"Missing essential bones: {', '.join(missing_bones)}")
hierarchy_messages.append(t("Armature.validation.missing_bones", bones=missing_list))
if validation_mode == 'STRICT':
logger.debug("Performing strict validation")
# Add scale issue detection in STRICT mode
scale_issues = detect_scale_issues(found_bones)
if scale_issues:
logger.warning(f"Found {len(scale_issues)} scale issues")
# CHANGE: Don't combine into a single string, keep as separate items
scale_messages.extend(scale_issues)
# Validate bone hierarchy
for parent, child in bone_hierarchy:
if parent in found_bones and child in found_bones:
if not validate_bone_hierarchy(found_bones, parent, child):
logger.warning(f"Invalid hierarchy: {parent} -> {child}")
hierarchy_messages.append(t("Armature.validation.invalid_hierarchy",
parent=parent, child=child))
# Validate symmetry
logger.debug("Validating bone symmetry")
symmetry_pairs = [('arm', 'L', 'R'), ('leg', 'L', 'R')]
for base, left, right in symmetry_pairs:
if not validate_symmetry(found_bones, base, left, right):
logger.warning(f"Asymmetric bones found: {base}")
hierarchy_messages.append(t("Armature.validation.asymmetric_bones", bone=base))
if (not validate_symmetry(found_bones, 'hand', 'L', 'R') and
not validate_symmetry(found_bones, 'wrist', 'L', 'R')):
logger.warning("Asymmetric hand/wrist bones found")
hierarchy_messages.append(t("Armature.validation.asymmetric_hand_wrist"))
# Validate finger hierarchies
logger.debug("Validating finger hierarchies")
for side in ['left', 'right']:
for finger_chain in finger_hierarchy[side]:
if all(bone in found_bones for bone in finger_chain):
if not validate_finger_chain(found_bones, finger_chain):
logger.warning(f"Invalid finger hierarchy: {finger_chain[0]}")
hierarchy_messages.append(t("Armature.validation.invalid_finger", finger=finger_chain[0]))
# Non-standard bones check
non_standard_bones = []
required_patterns = [
'Hips', 'Spine', 'Chest', 'Neck', 'Head',
'Upper', 'Lower', 'Hand', 'Foot', 'Toe',
'Thumb', 'Index', 'Middle', 'Ring', 'Pinky',
'Eye'
]
for bone_name in found_bones:
if any(pattern in bone_name for pattern in required_patterns):
is_standard = bone_name in standard_bones.values()
is_acceptable_bone = any(bone_name in names for names in acceptable_bone_names.values())
if not (is_standard or is_acceptable_bone):
non_standard_bones.append(bone_name)
if non_standard_bones:
non_standard_list = "\n".join([f"- {bone}" for bone in non_standard_bones])
non_standard_messages.append(t("Armature.validation.non_standard_bones", bones=non_standard_list))
non_standard_messages.append(t("Armature.validation.accessory_bones_note.line1"))
non_standard_messages.append(t("Armature.validation.accessory_bones_note.line2"))
non_standard_messages.append(t("Armature.validation.accessory_bones_note.line3"))
non_standard_messages.append(t("Armature.validation.accessory_bones_note.line4"))
non_standard_messages.append("") # Add a blank line for spacing
non_standard_messages.append(t("Armature.validation.standardize_note.line1"))
non_standard_messages.append(t("Armature.validation.standardize_note.line2"))
non_standard_messages.append(t("Armature.validation.standardize_note.line3"))
# Combine messages in correct order
messages.extend(non_standard_messages)
is_valid = len(non_standard_messages) == 0 and len(hierarchy_messages) == 0 and len(scale_messages) == 0
if not is_valid and is_acceptable:
if non_standard_bones:
logger.info("Armature has non-standard bones but is acceptable")
if detailed_messages:
return False, messages, False, hierarchy_messages, scale_messages, non_standard_messages
else:
return False, messages, False
logger.info("Armature meets acceptable standards")
messages = [
t("Armature.validation.acceptable_standard.success"),
t("Armature.validation.acceptable_standard.note"),
t("Armature.validation.acceptable_standard.option")
]
if detailed_messages:
return True, messages, True, [], [], []
else:
return True, messages, True
logger.info(f"Armature validation complete. Valid: {is_valid}")
if detailed_messages:
return is_valid, messages, False, hierarchy_messages, scale_messages, non_standard_messages
else:
return is_valid, messages, False
def validate_bone_hierarchy(bones: Dict[str, Bone], parent_name: str, child_name: str) -> bool:
"""Validate if there is a valid parent-child relationship between bones"""
if parent_name not in bones or child_name not in bones:
return False
return bones[child_name].parent == bones[parent_name]
def validate_symmetry(bones: Dict[str, Bone], base: str, left: str, right: str) -> bool:
"""Validate if matching left and right bones exist for a given base bone name"""
# Extract left and right bone names from both hierarchies
left_bone_names = set()
right_bone_names = set()
# Add standard bones
for key, value in standard_bones.items():
if base in key.lower():
if '_l' in key.lower():
left_bone_names.add(value)
elif '_r' in key.lower():
right_bone_names.add(value)
# Add acceptable bones
for key, names in acceptable_bone_names.items():
if base in key.lower():
if '_l' in key.lower():
left_bone_names.update(names)
elif '_r' in key.lower():
right_bone_names.update(names)
# Check if at least one pair exists and matches
left_exists = any(name in bones for name in left_bone_names)
right_exists = any(name in bones for name in right_bone_names)
return left_exists == right_exists
def validate_finger_chain(bones: Dict[str, Bone], chain: Tuple[str, ...]) -> bool:
"""Validate if a finger bone chain has correct hierarchy"""
for i in range(len(chain) - 1):
if not validate_bone_hierarchy(bones, chain[i], chain[i + 1]):
return False
return True
def check_acceptable_standards(bones: Dict[str, Bone]) -> bool:
"""Check if armature matches acceptable non-standard hierarchy"""
logger.debug("Checking for acceptable standards")
# Check if bones exist in acceptable list
for bone_category, acceptable_names in acceptable_bone_names.items():
found = False
for name in acceptable_names:
if name in bones:
found = True
break
if not found:
logger.debug(f"Missing acceptable bone for category: {bone_category}")
return False
# Validate acceptable hierarchy
for parent, child in acceptable_bone_hierarchy:
if parent in bones and child in bones:
if not validate_bone_hierarchy(bones, parent, child):
logger.debug(f"Invalid acceptable hierarchy: {parent} -> {child}")
return False
logger.debug("Armature meets acceptable standards")
return True
def validate_tpose(armature):
"""Validate if armature is in a proper T-pose"""
logger.debug(f"Validating T-pose for armature: {armature.name if armature else 'None'}")
if not armature or armature.type != 'ARMATURE':
logger.warning("No valid armature for T-pose validation")
return False, [t("Validation.tpose.no_armature")]
issues = []
if armature.mode == 'POSE':
bones_collection = armature.pose.bones
get_direction = lambda bone: bone.matrix.to_3x3().col[1].normalized()
else:
bones_collection = armature.data.bones
get_direction = lambda bone: bone.y_axis
# Get left and right upper arm bones using standard bone names
left_arm = None
right_arm = None
left_arm_candidates = [standard_bones['left_arm']] # UpperArm.L
if 'arm_l' in acceptable_bone_names:
left_arm_candidates.extend(acceptable_bone_names['arm_l'])
right_arm_candidates = [standard_bones['right_arm']] # UpperArm.R
if 'arm_r' in acceptable_bone_names:
right_arm_candidates.extend(acceptable_bone_names['arm_r'])
for name in left_arm_candidates:
if name in armature.data.bones:
left_arm = armature.data.bones[name]
logger.debug(f"Found left arm bone: {name}")
break
for name in right_arm_candidates:
if name in armature.data.bones:
right_arm = armature.data.bones[name]
logger.debug(f"Found right arm bone: {name}")
break
# Check arm bones are horizontal
if left_arm:
direction = left_arm.y_axis
if abs(direction.x) < 0.7: # Not pointing mostly along X axis
logger.warning("Left arm is not horizontal")
issues.append(t("Validation.tpose.left_arm_not_horizontal"))
if right_arm:
direction = right_arm.y_axis
if abs(direction.x) < 0.7: # Not pointing mostly along X axis
logger.warning("Right arm is not horizontal")
issues.append(t("Validation.tpose.right_arm_not_horizontal"))
spine = None
spine_candidates = [standard_bones['spine']] # Spine
if 'spine' in acceptable_bone_names:
spine_candidates.extend(acceptable_bone_names['spine'])
for name in spine_candidates:
if name in armature.data.bones:
spine = armature.data.bones[name]
logger.debug(f"Found spine bone: {name}")
break
if spine:
direction = spine.y_axis
if abs(direction.z) < 0.7: # Not pointing mostly along Z axis
logger.warning("Spine is not vertical")
issues.append(t("Validation.tpose.spine_not_vertical"))
if issues:
logger.warning(f"T-pose validation failed with {len(issues)} issues")
return False, issues
logger.info("T-pose validation successful")
return True, []
def detect_scale_issues(bones):
"""Detect bones with abnormal scale (too small or too large)"""
logger.debug("Detecting scale issues")
scale_issues = []
# Calculate median bone length for reference (more robust than average)
lengths = [bone.length for bone in bones.values()]
lengths.sort()
if not lengths:
logger.debug("No bones with length found")
return []
median_length = lengths[len(lengths) // 2]
# Filter out zero-length bones for standard deviation calculation
non_zero_lengths = [l for l in lengths if l > 0.0001]
if not non_zero_lengths:
logger.debug("No non-zero length bones found")
return []
mean = sum(non_zero_lengths) / len(non_zero_lengths)
variance = sum((l - mean) ** 2 for l in non_zero_lengths) / len(non_zero_lengths)
std_dev = math.sqrt(variance)
small_threshold = max(median_length * 0.05, mean - 3 * std_dev)
large_threshold = min(median_length * 15, mean + 5 * std_dev)
logger.debug(f"Scale thresholds - small: {small_threshold}, large: {large_threshold}")
# Get finger bones from standard and acceptable bone dictionaries
finger_bone_names = set()
for key in standard_bones:
if any(finger in key.lower() for finger in ['thumb', 'index', 'middle', 'ring', 'pinky', 'finger']):
finger_bone_names.add(standard_bones[key])
for key, names in acceptable_bone_names.items():
if any(finger in key.lower() for finger in ['thumb', 'index', 'middle', 'ring', 'pinky', 'finger']):
finger_bone_names.update(names)
for name, bone in bones.items():
is_finger = (name in finger_bone_names or
any(finger in name.lower() for finger in ['thumb', 'index', 'middle', 'ring', 'pinky', 'finger']))
if bone.length < small_threshold and not is_finger:
logger.debug(f"Bone {name} is too small: {bone.length}")
scale_issues.append(f"- {name}: {t('Validation.scale_issue.too_small')} ({bone.length:.4f})")
elif bone.length > large_threshold:
logger.debug(f"Bone {name} is too large: {bone.length}")
scale_issues.append(f"- {name}: {t('Validation.scale_issue.too_large')} ({bone.length:.4f})")
logger.debug(f"Found {len(scale_issues)} scale issues")
return scale_issues
def clear_bone_highlighting(armature: Object) -> None:
"""Clear bone highlighting by removing bone collections and resetting colors"""
logger.debug(f"Clearing bone highlighting for armature: {armature.name if armature else 'None'}")
if not armature or armature.type != 'ARMATURE':
logger.warning("No valid armature for clearing bone highlighting")
return
current_mode = bpy.context.mode
collection_name = "Problem Bones"
if collection_name in armature.data.collections:
problem_collection = armature.data.collections[collection_name]
armature.data.collections.remove(problem_collection)
logger.debug("Removed problem bones collection")
for bone in armature.data.bones:
bone.color.palette = 'DEFAULT'
if len(armature.data.collections) == 0:
armature.data.show_bone_colors = False
logger.debug("Disabled bone colors display")
logger.info("Bone highlighting cleared")
return
class AvatarToolkit_OT_HighlightProblemBones(Operator):
"""Highlight bones that fail validation in the 3D viewport"""
bl_idname = "avatar_toolkit.highlight_problem_bones"
bl_label = t("Validation.highlight_problem_bones")
bl_description = t("Validation.highlight_problem_bones_desc")
@classmethod
def poll(cls, context):
return get_active_armature(context) is not None
def execute(self, context):
armature = get_active_armature(context)
if not armature:
logger.warning("No active armature found for highlighting problem bones")
self.report({'ERROR'}, t("Validation.no_armature"))
return {'CANCELLED'}
logger.info(f"Highlighting problem bones for armature: {armature.name}")
current_mode = context.mode
if current_mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
context.view_layer.objects.active = armature
# First remove all bone collections
collection_name = "Problem Bones"
if collection_name in armature.data.collections:
problem_collection = armature.data.collections[collection_name]
armature.data.collections.remove(problem_collection)
logger.debug("Removed existing problem bones collection")
is_valid, messages, _ = validate_armature(armature)
if is_valid:
logger.info("No validation issues found")
self.report({'INFO'}, t("Validation.no_issues"))
bpy.ops.object.mode_set(mode='EDIT')
return {'FINISHED'}
problem_collection = armature.data.collections.new(name="Problem Bones")
logger.debug("Created new problem bones collection")
armature.data.show_bone_colors = True
bpy.ops.object.mode_set(mode='EDIT')
# Extract bone names from validation messages
problem_bones = self._extract_problem_bones(messages)
# Assign bones to collection and set colors
highlighted_count = 0
for category, bone_names in problem_bones.items():
for bone_name in bone_names:
if bone_name in armature.data.edit_bones:
bone = armature.data.edit_bones[bone_name]
problem_collection.assign(bone)
if 'hierarchy' in category.lower():
bone.color.palette = 'THEME09' # Orange
elif 'scale' in category.lower():
bone.color.palette = 'THEME03' # Yellow
else:
bone.color.palette = 'THEME01' # Red
highlighted_count += 1
logger.info(f"Highlighted {highlighted_count} problem bones")
self.report({'INFO'}, t("Validation.highlighting_complete"))
return {'FINISHED'}
def _extract_problem_bones(self, messages):
problem_bones = {
"Hierarchy Issues": [],
"Scale Issues": [],
"Missing Bones": []
}
# Extract bone names from validation messages
for message in messages:
if isinstance(message, str):
# Parse message to extract bone names
for line in message.split('\n'):
if '- ' in line:
bone_name = line.split('- ')[1].strip()
if ':' in bone_name: # Handle "bone_name: message" format
bone_name = bone_name.split(':')[0].strip()
if 'hierarchy' in message.lower():
problem_bones["Hierarchy Issues"].append(bone_name)
elif 'scale' in message.lower():
problem_bones["Scale Issues"].append(bone_name)
else:
problem_bones["Missing Bones"].append(bone_name)
logger.debug(f"Extracted problem bones: {problem_bones}")
return problem_bones
class AvatarToolkit_OT_ValidateTPose(Operator):
"""Validate if armature is in a proper T-pose"""
bl_idname = "avatar_toolkit.validate_tpose"
bl_label = t("Validation.tpose.label")
bl_description = t("Validation.tpose.desc")
@classmethod
def poll(cls, context):
return get_active_armature(context) is not None
def execute(self, context):
armature = get_active_armature(context)
if not armature:
logger.warning("No active armature found for T-pose validation")
self.report({'ERROR'}, t("Validation.no_armature"))
return {'CANCELLED'}
logger.info(f"Validating T-pose for armature: {armature.name}")
is_valid, messages = validate_tpose(armature)
props = context.scene.avatar_toolkit
props.tpose_validation_result = is_valid
props.tpose_validation_messages.clear()
for msg in messages:
item = props.tpose_validation_messages.add()
item.name = msg
props.show_tpose_validation = True
if is_valid:
logger.info("T-pose validation successful")
self.report({'INFO'}, t("Validation.tpose.valid"))
else:
for msg in messages:
self.report({'WARNING'}, msg)
logger.warning("T-pose validation failed")
self.report({'WARNING'}, t("Validation.tpose.warning"))
return {'FINISHED'}
class AvatarToolkit_OT_ClearBoneHighlighting(Operator):
"""Clear bone highlighting and reset bone colors"""
bl_idname = "avatar_toolkit.clear_bone_highlighting"
bl_label = t("Validation.clear_bone_highlighting")
bl_description = t("Validation.clear_bone_highlighting_desc")
@classmethod
def poll(cls, context):
return get_active_armature(context) is not None
def execute(self, context):
armature = get_active_armature(context)
if not armature:
logger.warning("No active armature found for clearing bone highlighting")
self.report({'ERROR'}, t("Validation.no_armature"))
return {'CANCELLED'}
logger.info(f"Clearing bone highlighting for armature: {armature.name}")
current_mode = context.mode
# Switch to object mode as collection editing is not possible in edit mode
if current_mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
context.view_layer.objects.active = armature
collection_name = "Problem Bones"
if collection_name in armature.data.collections:
# Remove the collection
problem_collection = armature.data.collections[collection_name]
armature.data.collections.remove(problem_collection)
logger.debug("Removed problem bones collection")
bpy.ops.object.mode_set(mode='EDIT')
# Reset all bone colors
for bone in armature.data.edit_bones:
bone.color.palette = 'DEFAULT'
# Turn off bone colors display if no other collections are using it
if len(armature.data.collections) == 0:
armature.data.show_bone_colors = False
logger.debug("Disabled bone colors display")
bpy.ops.object.mode_set(mode='OBJECT')
logger.info("Bone highlighting cleared")
self.report({'INFO'}, t("Validation.highlighting_cleared"))
return {'FINISHED'}
+68 -68
View File
@@ -1,6 +1,5 @@
import os
import bpy
import sys
import typing
import inspect
import pkgutil
@@ -24,7 +23,6 @@ def init() -> None:
global modules
global ordered_classes
# Configure logging first
from .logging_setup import configure_logging
configure_logging(False)
@@ -32,14 +30,24 @@ def init() -> None:
configure_logging(get_preference("enable_logging", False))
print("Auto-load init starting")
modules = get_all_submodules(Path(__file__).parent.parent)
package_name = __package__.rsplit('.', 1)[0]
directory = Path(__file__).parent.parent
modules = get_all_submodules(directory, package_name)
ordered_classes = get_ordered_classes_to_register(modules)
print(f"Found modules: {modules}")
print(f"Found classes: {ordered_classes}")
def register() -> None:
"""Register all discovered classes and modules"""
global modules, ordered_classes
print("Registering classes")
if not ordered_classes:
print("Warning: No classes to register")
ordered_classes = []
for cls in ordered_classes:
print(f"Registering: {cls}")
try:
@@ -47,6 +55,10 @@ def register() -> None:
except ValueError:
continue
if not modules:
print("Warning: No modules to register")
modules = []
for module in modules:
if module.__name__ == __name__:
continue
@@ -56,7 +68,10 @@ def register() -> None:
def unregister() -> None:
"""Unregister all classes and modules in reverse order"""
for cls in reversed(ordered_classes):
bpy.utils.unregister_class(cls)
try:
bpy.utils.unregister_class(cls)
except RuntimeError:
continue
for module in modules:
if module.__name__ == __name__:
@@ -64,44 +79,29 @@ def unregister() -> None:
if hasattr(module, "unregister"):
module.unregister()
def get_manifest_id() -> str:
"""Get the addon ID from the manifest file"""
manifest_path = Path(__file__).parent.parent / "blender_manifest.toml"
with open(manifest_path, "rb") as f:
manifest = tomllib.load(f)
return manifest["id"]
def get_all_submodules(directory: Path) -> List[Any]:
def get_all_submodules(directory: Path, package_name: str) -> List[Any]:
"""Discover and import all submodules in the given directory"""
modules = []
addon_id = get_manifest_id()
for root, dirs, files in os.walk(directory):
if "__pycache__" in root:
continue
path = Path(root)
if path == directory:
package_name = f"bl_ext.user_default.{addon_id}"
else:
relative_path = path.relative_to(directory).as_posix().replace('/', '.')
package_name = f"bl_ext.user_default.{addon_id}.{relative_path}"
for name in sorted(iter_module_names(path)):
modules.append(importlib.import_module(f".{name}", package_name))
return modules
return list(iter_submodules(directory, package_name))
def iter_submodules(path: Path, package_name: str) -> Generator[Any, None, None]:
def iter_submodules(directory: Path, package_name: str) -> Generator[Any, None, None]:
"""Iterate through submodules in a package"""
for name in sorted(iter_module_names(path)):
yield importlib.import_module("." + name, package_name)
for name in sorted(iter_submodule_names(directory)):
try:
yield importlib.import_module("." + name, package_name)
print(f"Successfully imported {name} from {package_name}")
except ImportError as e:
print(f"Error importing {name} from {package_name}: {e}")
def iter_module_names(path: Path) -> Generator[str, None, None]:
def iter_submodule_names(path: Path, root: str = "") -> Generator[str, None, None]:
"""Iterate through module names in a directory"""
print(f"Scanning path: {path}")
modules_list = list(pkgutil.iter_modules([str(path)]))
print(f"Found these modules: {modules_list}")
for _, module_name, is_pkg in modules_list:
if not is_pkg:
print(f"Found module: {module_name}")
yield module_name
for _, module_name, is_package in pkgutil.iter_modules([str(path)]):
if is_package:
sub_path = path / module_name
sub_root = root + module_name + "."
yield from iter_submodule_names(sub_path, sub_root)
else:
yield root + module_name
def get_ordered_classes_to_register(modules: List[Any]) -> List[Type]:
"""Get a topologically sorted list of classes to register"""
@@ -109,28 +109,37 @@ def get_ordered_classes_to_register(modules: List[Any]) -> List[Type]:
def get_register_deps_dict(modules: List[Any]) -> Dict[Type, Set[Type]]:
"""Get dependencies dictionary for class registration"""
my_classes = set(iter_classes_to_register(modules))
my_classes_by_idname = {cls.bl_idname: cls for cls in my_classes if hasattr(cls, "bl_idname")}
deps_dict = {}
classes_to_register = set(iter_classes_to_register(modules))
for cls in classes_to_register:
deps_dict[cls] = set(iter_own_register_deps(cls, classes_to_register))
for cls in my_classes:
deps_dict[cls] = set()
deps_dict[cls].update(iter_deps_from_annotations(cls, my_classes))
deps_dict[cls].update(iter_deps_from_parent_id(cls, my_classes_by_idname))
return deps_dict
def iter_own_register_deps(cls: Type, classes_to_register: Set[Type]) -> Generator[Type, None, None]:
"""Iterate through a class's own registration dependencies"""
yield from (dep for dep in iter_register_deps(cls) if dep in classes_to_register)
def iter_register_deps(cls: Type) -> Generator[Type, None, None]:
"""Iterate through all registration dependencies of a class"""
def iter_deps_from_annotations(cls: Type, my_classes: Set[Type]) -> Generator[Type, None, None]:
"""Iterate through dependencies from class annotations"""
for value in typing.get_type_hints(cls, {}, {}).values():
dependency = get_dependency_from_annotation(value)
if dependency is not None:
if dependency is not None and dependency in my_classes:
yield dependency
def iter_deps_from_parent_id(cls: Type, my_classes_by_idname: Dict[str, Type]) -> Generator[Type, None, None]:
"""Iterate through dependencies from panel parent IDs"""
if bpy.types.Panel in cls.__bases__:
parent_idname = getattr(cls, "bl_parent_id", None)
if parent_idname is not None:
parent_cls = my_classes_by_idname.get(parent_idname)
if parent_cls is not None:
yield parent_cls
def get_dependency_from_annotation(value: Any) -> Optional[Type]:
"""Get dependency type from a type annotation"""
if isinstance(value, tuple) and len(value) == 2:
if value[0] in (bpy.props.PointerProperty, bpy.props.CollectionProperty):
return value[1]["type"]
if isinstance(value, bpy.props._PropertyDeferred):
return value.keywords.get("type")
return None
def iter_classes_to_register(modules: List[Any]) -> Generator[Type, None, None]:
@@ -161,7 +170,8 @@ def get_register_base_types() -> Set[Type]:
"Panel", "Operator", "PropertyGroup",
"AddonPreferences", "Header", "Menu",
"Node", "NodeSocket", "NodeTree",
"UIList", "RenderEngine"
"UIList", "RenderEngine",
"Gizmo", "GizmoGroup",
])
def toposort(deps_dict: Dict[Type, Set[Type]]) -> List[Type]:
@@ -169,25 +179,15 @@ def toposort(deps_dict: Dict[Type, Set[Type]]) -> List[Type]:
sorted_list = []
sorted_values = set()
panels_to_sort = [(value, deps) for value, deps in deps_dict.items()
if hasattr(value, 'bl_parent_id')]
base_panels = [(value, deps) for value, deps in deps_dict.items()
if not hasattr(value, 'bl_parent_id')]
for value, deps in base_panels:
if len(deps) == 0:
sorted_list.append(value)
sorted_values.add(value)
while len(deps_dict) > len(sorted_values):
while len(deps_dict) > 0:
unsorted = []
for value, deps in deps_dict.items():
if value not in sorted_values:
if len(deps - sorted_values) == 0:
sorted_list.append(value)
sorted_values.add(value)
else:
unsorted.append(value)
if len(deps) == 0:
sorted_list.append(value)
sorted_values.add(value)
else:
unsorted.append(value)
deps_dict = {value: deps_dict[value] - sorted_values for value in unsorted}
return sorted_list
+241 -256
View File
@@ -6,25 +6,69 @@ import webbrowser
import typing
import struct
from io import BytesIO
import numpy.typing as npt
from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable
from mathutils import Vector
from bpy.types import Context, Object, Modifier, EditBone, Operator
from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable, Union, Type
from mathutils import Vector, Matrix
from bpy.types import (Context, Object, Modifier, EditBone, Operator, Material,
VertexGroup, ShapeKey, Bone, Mesh, Armature, PropertyGroup)
from functools import lru_cache
from bpy.props import PointerProperty, IntProperty, StringProperty
from bpy.utils import register_class
from ..core.logging_setup import logger
from ..core.translations import t
from ..core.dictionaries import bone_names
from .dictionaries import reverse_bone_lookup, bone_names
class SceneMatClass(PropertyGroup):
mat: PointerProperty(type=Material)
register_class(SceneMatClass)
class MaterialListBool:
#For the love that is holy do not ever touch these. If this was java I would make these private
#They should only be accessed via context.scene.texture_atlas_Has_Mat_List_Shown
#This is so we know if the materials are up to date. messing with these variables directly will make the thing blow up.
#The only exception to this is the ExpandSection_Materials operator which populates this with new data once the materials have changed and need reloading.
old_list: dict[str,list[Material]] = {}
bool_material_list_expand: dict[str,bool] = {}
def set_bool(self, value: bool) -> None:
MaterialListBool.bool_material_list_expand[bpy.context.scene.name] = value
if value == False:
MaterialListBool.old_list[bpy.context.scene.name] = []
def get_bool(self) -> bool:
newlist: list[Material] = []
for obj in bpy.context.scene.objects:
if len(obj.material_slots)>0:
for mat_slot in obj.material_slots:
if mat_slot.material:
if mat_slot.material not in newlist:
newlist.append(mat_slot.material)
still_the_same: bool = True
if bpy.context.scene.name in MaterialListBool.old_list:
for item in newlist:
if item not in MaterialListBool.old_list[bpy.context.scene.name]:
still_the_same = False
break
for item in MaterialListBool.old_list[bpy.context.scene.name]:
if item not in newlist:
still_the_same = False
break
else:
still_the_same = False
MaterialListBool.bool_material_list_expand[bpy.context.scene.name] = still_the_same
return MaterialListBool.bool_material_list_expand[bpy.context.scene.name]
class ProgressTracker:
"""Universal progress tracking for Avatar Toolkit operations"""
def __init__(self, context: Context, total_steps: int, operation_name: str = "Operation"):
self.context = context
self.total = total_steps
self.current = 0
self.operation_name = operation_name
def __init__(self, context: Context, total_steps: int, operation_name: str = "Operation") -> None:
self.context: Context = context
self.total: int = total_steps
self.current: int = 0
self.operation_name: str = operation_name
self.wm = context.window_manager
def step(self, message: str = "") -> None:
@@ -35,26 +79,28 @@ class ProgressTracker:
self.wm.progress_update(progress * 100)
logger.debug(f"{self.operation_name} - {progress:.1%}: {message}")
def __enter__(self):
def __enter__(self) -> 'ProgressTracker':
logger.info(f"Starting {self.operation_name}")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
def __exit__(self, exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[Any]) -> None:
self.wm.progress_end()
logger.info(f"Completed {self.operation_name}")
def get_active_armature(context: bpy.types.Context) -> Optional[bpy.types.Object]:
def get_active_armature(context: Context) -> Optional[Object]:
"""Get the currently selected armature from Avatar Toolkit properties"""
armature_name = context.scene.avatar_toolkit.active_armature
armature_name = str(context.scene.avatar_toolkit.active_armature)
if armature_name and armature_name != 'NONE':
return bpy.data.objects.get(armature_name)
return None
def set_active_armature(context: bpy.types.Context, armature: bpy.types.Object) -> None:
def set_active_armature(context: Context, armature: Object) -> None:
"""Set the active armature for Avatar Toolkit operations"""
context.scene.avatar_toolkit.active_armature = armature
def get_armature_list(self=None, context: bpy.types.Context = None) -> List[Tuple[str, str, str]]:
def get_armature_list(self: Optional[Any] = None, context: Optional[Context] = None) -> List[Tuple[str, str, str]]:
"""Get list of all armature objects in the scene"""
if context is None:
context = bpy.context
@@ -62,117 +108,22 @@ def get_armature_list(self=None, context: bpy.types.Context = None) -> List[Tupl
if not armatures:
return [('NONE', t("Armature.validation.no_armature"), '')]
return armatures
def validate_armature(armature: bpy.types.Object) -> Tuple[bool, List[str]]:
"""Enhanced armature validation with multiple validation modes"""
validation_mode = bpy.context.scene.avatar_toolkit.validation_mode
# Skip validation if mode is NONE
if validation_mode == 'NONE':
return True, []
messages = []
# Basic checks always run if not NONE
if not armature or armature.type != 'ARMATURE' or not armature.data.bones:
return False, [t("Armature.validation.basic_check_failed")]
found_bones = {bone.name.lower(): bone for bone in armature.data.bones}
# Essential bones check (BASIC and STRICT)
essential_bones = {'hips', 'spine', 'chest', 'neck', 'head'}
missing_bones = []
for bone in essential_bones:
if not any(alt_name in found_bones for alt_name in bone_names[bone]):
missing_bones.append(bone)
if missing_bones:
messages.append(t("Armature.validation.missing_bones", bones=", ".join(missing_bones)))
# Additional checks for STRICT mode only
if validation_mode == 'STRICT':
# Hierarchy validation
hierarchy = [('hips', 'spine'), ('spine', 'chest'), ('chest', 'neck'), ('neck', 'head')]
for parent, child in hierarchy:
if not validate_bone_hierarchy(found_bones, parent, child):
messages.append(t("Armature.validation.invalid_hierarchy", parent=parent, child=child))
# Symmetry validation
symmetry_pairs = [('arm', 'l', 'r'), ('leg', 'l', 'r')]
for base, left, right in symmetry_pairs:
if not validate_symmetry(found_bones, base, left, right):
messages.append(t("Armature.validation.asymmetric_bones", bone=base))
# Special handling for hand/wrist symmetry
if (not validate_symmetry(found_bones, 'hand', 'l', 'r') and
not validate_symmetry(found_bones, 'wrist', 'l', 'r')):
messages.append(t("Armature.validation.asymmetric_hand_wrist"))
is_valid = len(messages) == 0
return is_valid, messages
def validate_bone_hierarchy(bones: Dict[str, bpy.types.Bone], parent_name: str, child_name: str) -> bool:
"""Validate if there is a valid parent-child relationship between bones"""
# Find matching parent and child bones using bone_names dictionary
parent_bone = None
child_bone = None
# Check for parent bone matches
for alt_name in bone_names[parent_name]:
if alt_name in bones:
parent_bone = bones[alt_name]
break
# Check for child bone matches
for alt_name in bone_names[child_name]:
if alt_name in bones:
child_bone = bones[alt_name]
break
if not parent_bone or not child_bone:
return False
# Check if child's parent matches parent bone
return child_bone.parent == parent_bone
def validate_symmetry(bones: Dict[str, bpy.types.Bone], base: str, left: str, right: str) -> bool:
"""
Validate if matching left and right bones exist for a given base bone name
"""
# Define common naming patterns
left_patterns = [
f"{base}.{left}",
f"{base}_{left}",
f"{left}_{base}"
]
right_patterns = [
f"{base}.{right}",
f"{base}_{right}",
f"{right}_{base}"
]
# Check if any of the patterns exist in the bones dictionary
left_exists = any(pattern in bones for pattern in left_patterns)
right_exists = any(pattern in bones for pattern in right_patterns)
return left_exists and right_exists
def auto_select_single_armature(context: bpy.types.Context) -> None:
def auto_select_single_armature(context: Context) -> None:
"""Automatically select armature if only one exists in scene"""
armatures = get_armature_list(context)
armatures: List[Tuple[str, str, str]] = get_armature_list(context)
if len(armatures) == 1 and armatures[0][0] != 'NONE':
toolkit = context.scene.avatar_toolkit
set_active_armature(context, armatures[0])
def clear_default_objects() -> None:
"""Removes default Blender objects (cube, light, camera)"""
"""Removes default Blender objects"""
default_names: Set[str] = {'Cube', 'Light', 'Camera'}
for obj in bpy.data.objects:
if obj.name.split('.')[0] in default_names:
bpy.data.objects.remove(obj, do_unlink=True)
def get_armature_stats(armature: bpy.types.Object) -> dict:
def get_armature_stats(armature: Object) -> Dict[str, Union[int, bool, str]]:
"""Get statistics about the armature"""
return {
'bone_count': len(armature.data.bones),
@@ -183,7 +134,7 @@ def get_armature_stats(armature: bpy.types.Object) -> dict:
def get_all_meshes(context: Context) -> List[Object]:
"""Get all mesh objects parented to the active armature"""
armature = get_active_armature(context)
armature: Optional[Object] = get_active_armature(context)
if armature:
return [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == armature]
return []
@@ -196,26 +147,26 @@ def validate_mesh_for_pose(mesh_obj: Object) -> Tuple[bool, str]:
if not mesh_obj.vertex_groups:
return False, t("Mesh.validation.no_vertex_groups")
armature_mods = [mod for mod in mesh_obj.modifiers if mod.type == 'ARMATURE']
armature_mods: List[Modifier] = [mod for mod in mesh_obj.modifiers if mod.type == 'ARMATURE']
if not armature_mods:
return False, t("Mesh.validation.no_armature_modifier")
return True, t("Mesh.validation.valid")
def cache_vertex_positions(mesh_obj: Object) -> np.ndarray:
def cache_vertex_positions(mesh_obj: Object) -> npt.NDArray[np.float32]:
"""Cache vertex positions for a mesh object"""
vertices = mesh_obj.data.vertices
positions = np.empty(len(vertices) * 3, dtype=np.float32)
positions: npt.NDArray[np.float32] = np.empty(len(vertices) * 3, dtype=np.float32)
vertices.foreach_get('co', positions)
return positions.reshape(-1, 3)
def apply_vertex_positions(vertices: Object, positions: np.ndarray) -> None:
def apply_vertex_positions(vertices: Object, positions: npt.NDArray[np.float32]) -> None:
"""Apply cached vertex positions to mesh in batch"""
vertices.foreach_set('co', positions.flatten())
def process_armature_modifiers(mesh_obj: Object) -> List[Dict[str, Any]]:
"""Process and store armature modifier states"""
modifier_states = []
modifier_states: List[Dict[str, Any]] = []
for mod in mesh_obj.modifiers:
if mod.type == 'ARMATURE':
modifier_states.append({
@@ -252,10 +203,10 @@ def apply_pose_as_rest(context: Context, armature_obj: Object, meshes: List[Obje
except Exception as e:
logger.error(f"Error applying pose as rest: {str(e)}")
return False, str(e)
def apply_armature_to_mesh(armature_obj: Object, mesh_obj: Object) -> None:
"""Apply armature deformation to mesh"""
armature_mod = mesh_obj.modifiers.new('PoseToRest', 'ARMATURE')
armature_mod: Modifier = mesh_obj.modifiers.new('PoseToRest', 'ARMATURE')
armature_mod.object = armature_obj
if bpy.app.version >= (3, 5):
@@ -269,13 +220,13 @@ def apply_armature_to_mesh(armature_obj: Object, mesh_obj: Object) -> None:
def apply_armature_to_mesh_with_shapekeys(armature_obj: Object, mesh_obj: Object, context: Context) -> None:
"""Apply armature deformation to mesh with shape keys"""
old_active_index = mesh_obj.active_shape_key_index
old_show_only = mesh_obj.show_only_shape_key
old_active_index: int = mesh_obj.active_shape_key_index
old_show_only: bool = mesh_obj.show_only_shape_key
mesh_obj.show_only_shape_key = True
shape_keys = mesh_obj.data.shape_keys.key_blocks
vertex_groups = []
mutes = []
shape_keys: List[ShapeKey] = mesh_obj.data.shape_keys.key_blocks
vertex_groups: List[str] = []
mutes: List[bool] = []
for sk in shape_keys:
vertex_groups.append(sk.vertex_group)
@@ -283,17 +234,17 @@ def apply_armature_to_mesh_with_shapekeys(armature_obj: Object, mesh_obj: Object
mutes.append(sk.mute)
sk.mute = False
disabled_mods = []
disabled_mods: List[Modifier] = []
for mod in mesh_obj.modifiers:
if mod.show_viewport:
mod.show_viewport = False
disabled_mods.append(mod)
arm_mod = mesh_obj.modifiers.new('PoseToRest', 'ARMATURE')
arm_mod: Modifier = mesh_obj.modifiers.new('PoseToRest', 'ARMATURE')
arm_mod.object = armature_obj
co_length = len(mesh_obj.data.vertices) * 3
eval_cos = np.empty(co_length, dtype=np.single)
co_length: int = len(mesh_obj.data.vertices) * 3
eval_cos: npt.NDArray[np.float32] = np.empty(co_length, dtype=np.single)
for i, shape_key in enumerate(shape_keys):
mesh_obj.active_shape_key_index = i
@@ -328,41 +279,67 @@ def validate_meshes(meshes: List[Object]) -> Tuple[bool, str]:
return False, t("Optimization.non_mesh_objects")
return True, ""
def fast_uv_fix(obj: Object) -> None:
"""Fast UV coordinate fixing for joined meshes"""
if not obj or not obj.data or not obj.data.uv_layers:
return
current_mode = bpy.context.mode
if current_mode != 'EDIT_MESH':
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
# Process all UV layers at once
bpy.ops.uv.select_all(action='SELECT')
bpy.ops.uv.pack_islands(margin=0.001)
if current_mode != 'EDIT_MESH':
bpy.ops.object.mode_set(mode=current_mode)
def join_mesh_objects(context: Context, meshes: List[Object], progress: Optional[ProgressTracker] = None) -> Optional[Object]:
"""Combines multiple mesh objects into a single mesh with proper cleanup and UV fixing"""
"""Combines multiple mesh objects into a single mesh with optimized performance"""
try:
if not meshes:
return None
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
for mesh in meshes:
# Create a list of valid meshes
valid_meshes = [mesh for mesh in meshes if mesh.name in bpy.data.objects]
if not valid_meshes:
return None
for mesh in valid_meshes:
mesh.select_set(True)
if context.selected_objects:
context.view_layer.objects.active = context.selected_objects[0]
context.view_layer.objects.active = valid_meshes[0]
if progress:
progress.step(t("Optimization.joining_meshes"))
if progress:
progress.step(t("Optimization.joining_meshes"))
bpy.ops.object.join()
bpy.ops.object.join()
joined_mesh = context.active_object
if progress:
progress.step(t("Optimization.applying_transforms"))
if progress:
progress.step(t("Optimization.applying_transforms"))
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
if progress:
progress.step(t("Optimization.fixing_uvs"))
if progress:
progress.step(t("Optimization.fixing_uvs"))
fix_uv_coordinates(context)
# Return the joined mesh object
return context.active_object
else:
# No objects were selected, return None
return None
fast_uv_fix(joined_mesh)
return joined_mesh
except Exception as e:
logger.error(f"Failed to join meshes: {str(e)}")
return None
def fix_uv_coordinates(context: Context) -> None:
"""Normalizes and fixes UV coordinates for the active mesh object"""
obj: Object = context.object
@@ -374,13 +351,17 @@ def fix_uv_coordinates(context: Context) -> None:
bpy.ops.object.mode_set(mode='OBJECT')
obj.select_set(True)
context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
with context.temp_override(active_object=obj):
bpy.ops.uv.select_all(action='SELECT')
bpy.ops.uv.average_islands_scale()
# Process each UV layer
for uv_layer in obj.data.uv_layers:
obj.data.uv_layers.active = uv_layer
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
with context.temp_override(active_object=obj):
bpy.ops.uv.select_all(action='SELECT')
bpy.ops.uv.pack_islands(margin=0.001)
bpy.ops.uv.average_islands_scale()
logger.debug(f"UV Fix - Successfully processed {obj.name}")
@@ -392,24 +373,32 @@ def fix_uv_coordinates(context: Context) -> None:
for sel_obj in current_selected:
sel_obj.select_set(True)
context.view_layer.objects.active = current_active
# This should be at the top level, not indented inside any class or function
def clear_unused_data_blocks() -> int:
"""Removes all unused data blocks from the current Blender file"""
initial_count = sum(len(getattr(bpy.data, attr)) for attr in dir(bpy.data)
initial_count: int = sum(len(getattr(bpy.data, attr)) for attr in dir(bpy.data)
if isinstance(getattr(bpy.data, attr), bpy.types.bpy_prop_collection))
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
final_count = sum(len(getattr(bpy.data, attr)) for attr in dir(bpy.data)
final_count: int = sum(len(getattr(bpy.data, attr)) for attr in dir(bpy.data)
if isinstance(getattr(bpy.data, attr), bpy.types.bpy_prop_collection))
return initial_count - final_count
def simplify_bonename(name: str) -> str:
"""Simplify bone name by removing spaces, underscores, dots and converting to lowercase"""
return name.lower().translate(dict.fromkeys(map(ord, u" _.")))
def identify_bones(arm_data: bpy.types.Armature, context: bpy.types.Context) -> Dict[str,str]:
"""Identify bone names in an armature based on our reverse dictionary, so there is no confusion to what a bone is.
Essentially makes a dictionary of keys from dictionaries.bone_names like "hips", and the corosponding value is the bone that can be mapped to that key."""
returned: Dict[str,str] = {}
for bone in arm_data.bones:
simplified_name = simplify_bonename(bone.name)
if simplified_name in reverse_bone_lookup:
returned[reverse_bone_lookup[simplified_name]] = bone.name
return returned
def duplicate_bone_chain(bones: List[EditBone]) -> List[EditBone]:
"""Duplicate a chain of bones while preserving hierarchy"""
new_bones = []
parent_map = {}
new_bones: List[EditBone] = []
parent_map: Dict[EditBone, EditBone] = {}
for bone in bones:
new_bone = duplicate_bone(bone)
@@ -429,37 +418,31 @@ def restore_bone_transforms(bone: EditBone, transforms: Dict[str, Any]) -> None:
def get_vertex_weights(mesh_obj: Object, group_name: str) -> Dict[int, float]:
"""Get vertex weights for a specific vertex group"""
weights = {}
group_index = mesh_obj.vertex_groups[group_name].index
weights: Dict[int, float] = {}
group_index: int = mesh_obj.vertex_groups[group_name].index
for vertex in mesh_obj.data.vertices:
for group in vertex.groups:
if group.group == group_index:
weights[vertex.index] = group.weight
return weights
def transfer_vertex_weights(mesh_obj: Object,
source_name: str,
target_name: str,
threshold: float = 0.01) -> None:
def transfer_vertex_weights(mesh_obj: Object, source_name: str, target_name: str, threshold: float = 0.01) -> None:
"""Transfer vertex weights from source to target group"""
if source_name not in mesh_obj.vertex_groups:
return
source_group = mesh_obj.vertex_groups[source_name]
target_group = mesh_obj.vertex_groups.get(target_name)
source_group: VertexGroup = mesh_obj.vertex_groups[source_name]
target_group: Optional[VertexGroup] = mesh_obj.vertex_groups.get(target_name)
if not target_group:
target_group = mesh_obj.vertex_groups.new(name=target_name)
# Get source weights
weights = get_vertex_weights(mesh_obj, source_name)
weights: Dict[int, float] = get_vertex_weights(mesh_obj, source_name)
# Transfer weights above threshold
for vertex_index, weight in weights.items():
if weight > threshold:
target_group.add([vertex_index], weight, 'ADD')
# Remove source group
mesh_obj.vertex_groups.remove(source_group)
def remove_unused_shapekeys(mesh_obj: Object, tolerance: float = 0.001) -> int:
@@ -467,35 +450,30 @@ def remove_unused_shapekeys(mesh_obj: Object, tolerance: float = 0.001) -> int:
if not mesh_obj.data.shape_keys:
return 0
key_blocks = mesh_obj.data.shape_keys.key_blocks
vertex_count = len(mesh_obj.data.vertices)
removed_count = 0
key_blocks: List[ShapeKey] = mesh_obj.data.shape_keys.key_blocks
vertex_count: int = len(mesh_obj.data.vertices)
removed_count: int = 0
# Cache for relative key locations
cache = {}
locations = np.empty(3 * vertex_count, dtype=np.float32)
to_delete = []
cache: Dict[str, npt.NDArray[np.float32]] = {}
locations: npt.NDArray[np.float32] = np.empty(3 * vertex_count, dtype=np.float32)
to_delete: List[str] = []
for key in key_blocks:
if key == key.relative_key:
continue
# Get current key locations
key.data.foreach_get("co", locations)
# Get or calculate relative key locations
if key.relative_key.name not in cache:
rel_locations = np.empty(3 * vertex_count, dtype=np.float32)
rel_locations: npt.NDArray[np.float32] = np.empty(3 * vertex_count, dtype=np.float32)
key.relative_key.data.foreach_get("co", rel_locations)
cache[key.relative_key.name] = rel_locations
# Compare locations
locations -= cache[key.relative_key.name]
if (np.abs(locations) < tolerance).all():
if not any(c in key.name for c in "-=~"): # Skip category markers
if not any(c in key.name for c in "-=~"):
to_delete.append(key.name)
# Remove marked shape keys
for key_name in to_delete:
mesh_obj.shape_key_remove(key_blocks[key_name])
removed_count += 1
@@ -503,20 +481,52 @@ def remove_unused_shapekeys(mesh_obj: Object, tolerance: float = 0.001) -> int:
return removed_count
def has_shapekeys(mesh_obj: Object) -> bool:
"""Check if mesh object has shape keys"""
return mesh_obj.data.shape_keys is not None
# Identifier to indicate that an EnumProperty is empty
# This is the default identifier used when a wrapped items function returns an empty list
# This identifier needs to be something that should never normally be used, so as to avoid the possibility of
# conflicting with an enum value that exists.
_empty_enum_identifier = 'Cats_empty_enum_identifier'
def fix_zero_length_bones(armature: Object) -> None:
"""Fix zero length bones by setting a minimum length"""
if not armature:
return
bpy.ops.object.mode_set(mode='EDIT')
for bone in armature.data.edit_bones:
if bone.length < 0.001:
bone.length = 0.001
bpy.ops.object.mode_set(mode='OBJECT')
# names - The first object will be the first one in the list. So the first one has to be the one that exists in the most models
# no_basis - If this is true the Basis will not be available in the list
def get_shapekeys(context, names, is_mouth, no_basis, return_list):
choices = []
choices_simple = []
meshes_list = get_meshes_objects(check=False)
def calculate_bone_orientation(mesh: Object, vertices: List[Any]) -> Tuple[Vector, float]:
"""Calculate optimal bone orientation based on mesh geometry"""
if not vertices:
return Vector((0, 0, 0.1)), 0.0
coords: List[Vector] = [mesh.data.vertices[v.index].co for v in vertices]
min_co: Vector = Vector(map(min, zip(*coords)))
max_co: Vector = Vector(map(max, zip(*coords)))
dimensions: Vector = max_co - min_co
roll_angle: float = 0.0
return dimensions, roll_angle
def add_armature_modifier(mesh: Object, armature: Object) -> None:
"""Add armature modifier to mesh"""
for mod in mesh.modifiers:
if mod.type == 'ARMATURE':
mesh.modifiers.remove(mod)
modifier: Modifier = mesh.modifiers.new('Armature', 'ARMATURE')
modifier.object = armature
def get_shapekeys(context: Context,
names: List[str],
is_mouth: bool,
no_basis: bool,
return_list: bool) -> Union[List[Tuple[str, str, str]], List[str]]:
"""Get shape keys based on specified criteria"""
choices: List[Tuple[str, str, str]] = []
choices_simple: List[str] = []
meshes_list: List[Object] = get_meshes_objects(check=False)
if meshes_list:
if is_mouth:
@@ -536,15 +546,12 @@ def get_shapekeys(context, names, is_mouth, no_basis, return_list):
continue
if no_basis and name == 'Basis':
continue
# 1. Will be returned by context.scene
# 2. Will be shown in lists
# 3. will be shown in the hover description (below description)
choices.append((name, name, name))
choices_simple.append(name)
_sort_enum_choices_by_identifier_lower(choices)
choices2 = []
choices2: List[Tuple[str, str, str]] = []
for name in names:
if name in choices_simple and len(choices) > 1 and choices[0][0] != name:
continue
@@ -553,22 +560,16 @@ def get_shapekeys(context, names, is_mouth, no_basis, return_list):
choices2.extend(choices)
if return_list:
shape_list = []
shape_list: List[str] = []
for choice in choices2:
shape_list.append(choice[0])
return shape_list
return choices2
# Default sorting for dynamic EnumProperty items
def _sort_enum_choices_by_identifier_lower(choices, in_place=True):
"""Sort a list of enum choices (items) by the lowercase of their identifier.
Sorting is performed in-place by default, but can be changed by setting in_place=False.
Returns the sorted list of enum choices."""
def identifier_lower(choice):
def _sort_enum_choices_by_identifier_lower(choices: List[Tuple[str, str, str]], in_place: bool = True) -> List[Tuple[str, str, str]]:
"""Sort a list of enum choices by the lowercase of their identifier"""
def identifier_lower(choice: Tuple[str, str, str]) -> str:
return choice[0].lower()
if in_place:
@@ -577,55 +578,39 @@ def _sort_enum_choices_by_identifier_lower(choices, in_place=True):
choices = sorted(choices, key=identifier_lower)
return choices
def is_enum_empty(string):
"""Returns True only if the tested string is the string that signifies that an EnumProperty is empty.
Returns False in all other cases."""
def is_enum_empty(string: str) -> bool:
"""Returns True only if the tested string is the empty enum identifier"""
return _empty_enum_identifier == string
# This function isn't needed since you can 'not is_enum_empty(string)', but is included for code clarity and readability
def is_enum_non_empty(string):
"""Returns False only if the tested string is not the string that signifies that an EnumProperty is empty.
Returns True in all other cases."""
def is_enum_non_empty(string: str) -> bool:
"""Returns False only if the tested string is not the empty enum identifier"""
return _empty_enum_identifier != string
def fix_zero_length_bones(armature: Object) -> None:
"""Fix zero length bones by setting a minimum length"""
if not armature:
return
bpy.ops.object.mode_set(mode='EDIT')
for bone in armature.data.edit_bones:
if bone.length < 0.001:
bone.length = 0.001
bpy.ops.object.mode_set(mode='OBJECT')
_empty_enum_identifier: str = 'Cats_empty_enum_identifier'
def get_meshes_objects(check: bool = True) -> List[Object]:
"""Get all mesh objects in the scene"""
meshes: List[Object] = [obj for obj in bpy.data.objects if obj.type == 'MESH']
if check and not meshes:
return []
return meshes
def calculate_bone_orientation(mesh, vertices):
"""Calculate optimal bone orientation based on mesh geometry."""
if not vertices:
return Vector((0, 0, 0.1)), 0.0
coords = [mesh.data.vertices[v.index].co for v in vertices]
min_co = Vector(map(min, zip(*coords)))
max_co = Vector(map(max, zip(*coords)))
dimensions = max_co - min_co
roll_angle = 0.0
return dimensions, roll_angle
def get_objects() -> bpy.types.BlendData:
"""Get all objects in the current Blender scene"""
return bpy.data.objects
def add_armature_modifier(mesh: Object, armature: Object):
"""Add armature modifier to mesh."""
for mod in mesh.modifiers:
if mod.type == 'ARMATURE':
mesh.modifiers.remove(mod)
modifier = mesh.modifiers.new('Armature', 'ARMATURE')
modifier.object = armature
def duplicate_bone(bone: EditBone) -> EditBone:
"""Create a duplicate of the given bone"""
new_bone: EditBone = bone.id_data.edit_bones.new(bone.name + "_copy")
new_bone.head = bone.head.copy()
new_bone.tail = bone.tail.copy()
new_bone.roll = bone.roll
new_bone.use_connect = bone.use_connect
new_bone.use_local_location = bone.use_local_location
new_bone.use_inherit_rotation = bone.use_inherit_rotation
new_bone.use_inherit_scale = bone.use_inherit_scale
new_bone.use_deform = bone.use_deform
return new_bone
#Binary tools
+624 -18
View File
@@ -1,9 +1,14 @@
# GPL Licence
# Bone names from https://github.com/triazo/immersive_scaler/
# Note from @989onan: Please make sure to make your names are lowercase in this array. I banged my head metaphorically till I figured that out...
# Note from @989onan: Please make sure to make your names are lowercase in this array, or it will never find a match. I banged my head metaphorically till I figured that out...
# Note2: Remove all "_", ".", and " " (space) from your values array or it will also not ever find a match!!!!
# Taken from Tuxedo/Cats
def simplify_bonename(name: str) -> str:
"""Simplify bone name by removing spaces, underscores, dots and converting to lowercase"""
return name.lower().translate(dict.fromkeys(map(ord, u" _.")))
bone_names = {
# Right side bones
"right_shoulder": [
@@ -254,26 +259,26 @@ bone_names = {
# Add VRM bone name variations
bone_names.update({
'hips': bone_names['hips'] + ['j_bip_c_hips', 'j_hips', 'vrm_hips'],
'spine': bone_names['spine'] + ['j_bip_c_spine', 'j_spine', 'vrm_spine'],
'chest': bone_names['chest'] + ['j_bip_c_chest', 'j_chest', 'vrm_chest'],
'upper_chest': bone_names['upper_chest'] + ['j_bip_c_upper_chest', 'j_upper_chest', 'vrm_upperchest'],
'neck': bone_names['neck'] + ['j_bip_c_neck', 'j_neck', 'vrm_neck'],
'head': bone_names['head'] + ['j_bip_c_head', 'j_head', 'vrm_head'],
'hips': bone_names['hips'] + ['jbipchips', 'jhips', 'vrmhips'],
'spine': bone_names['spine'] + ['jbipcspine', 'jspine', 'vrmspine'],
'chest': bone_names['chest'] + ['jbipcchest', 'jchest', 'vrmchest'],
'upper_chest': bone_names['upper_chest'] + ['jbipcupperchest', 'jupperchest', 'vrmupperchest'],
'neck': bone_names['neck'] + ['jbipcneck', 'jneck', 'vrmneck'],
'head': bone_names['head'] + ['jbipchead', 'jhead', 'vrmhead'],
# VRM specific finger naming
'thumb_0_l': bone_names['thumb_0_l'] + ['thumb_metacarpal_l', 'j_thumb1_l'],
'index_0_l': bone_names['index_0_l'] + ['index_metacarpal_l', 'j_index1_l'],
'middle_0_l': bone_names['middle_0_l'] + ['middle_metacarpal_l', 'j_middle1_l'],
'ring_0_l': bone_names['ring_0_l'] + ['ring_metacarpal_l', 'j_ring1_l'],
'pinkie_0_l': bone_names['pinkie_0_l'] + ['little_metacarpal_l', 'j_little1_l'],
'thumb_0_l': bone_names['thumb_0_l'] + ['thumbmetacarpall', 'jthumb1l'],
'index_0_l': bone_names['index_0_l'] + ['indexmetacarpall', 'jindex1l'],
'middle_0_l': bone_names['middle_0_l'] + ['middlemetacarpall', 'jmiddle1l'],
'ring_0_l': bone_names['ring_0_l'] + ['ringmetacarpall', 'jring1l'],
'pinkie_0_l': bone_names['pinkie_0_l'] + ['littlemetacarpall', 'jlittle1l'],
# Mirror for right side
'thumb_0_r': bone_names['thumb_0_r'] + ['thumb_metacarpal_r', 'j_thumb1_r'],
'index_0_r': bone_names['index_0_r'] + ['index_metacarpal_r', 'j_index1_r'],
'middle_0_r': bone_names['middle_0_r'] + ['middle_metacarpal_r', 'j_middle1_r'],
'ring_0_r': bone_names['ring_0_r'] + ['ring_metacarpal_r', 'j_ring1_r'],
'pinkie_0_r': bone_names['pinkie_0_r'] + ['little_metacarpal_r', 'j_little1_r']
'thumb_0_r': bone_names['thumb_0_r'] + ['thumbmetacarpalr', 'jthumb1r'],
'index_0_r': bone_names['index_0_r'] + ['indexmetacarpalr', 'jindex1r'],
'middle_0_r': bone_names['middle_0_r'] + ['middlemetacarpalr', 'jmiddle1r'],
'ring_0_r': bone_names['ring_0_r'] + ['ringmetacarpalr', 'jring1r'],
'pinkie_0_r': bone_names['pinkie_0_r'] + ['littlemetacarpalr', 'jlittle1r']
})
# array taken from cats
@@ -354,3 +359,604 @@ resonite_translations = {
'thumb_2_r': "thumb2.R",
'thumb_3_r': "thumb3.R"
}
standard_bones = {
# Core Structure
'hips': 'Hips',
'spine': 'Spine',
'chest': 'Chest',
'upper_chest': 'Chest.Up',
'neck': 'Neck',
'head': 'Head',
# Arms
'left_arm': 'UpperArm.L',
'left_elbow': 'LowerArm.L',
'left_wrist': 'Hand.L',
'right_arm': 'UpperArm.R',
'right_elbow': 'LowerArm.R',
'right_wrist': 'Hand.R',
# Legs
'left_leg': 'UpperLeg.L',
'left_knee': 'LowerLeg.L',
'left_ankle': 'Foot.L',
'left_toe': 'Toes.L',
'right_leg': 'UpperLeg.R',
'right_knee': 'LowerLeg.R',
'right_ankle': 'Foot.R',
'right_toe': 'Toes.R',
# Fingers Left
'thumb_1_l': 'Thumb1.L',
'thumb_2_l': 'Thumb2.L',
'thumb_3_l': 'Thumb3.L',
'index_1_l': 'Index1.L',
'index_2_l': 'Index2.L',
'index_3_l': 'Index3.L',
'middle_1_l': 'Middle1.L',
'middle_2_l': 'Middle2.L',
'middle_3_l': 'Middle3.L',
'ring_1_l': 'Ring1.L',
'ring_2_l': 'Ring2.L',
'ring_3_l': 'Ring3.L',
'pinkie_1_l': 'Pinky1.L',
'pinkie_2_l': 'Pinky2.L',
'pinkie_3_l': 'Pinky3.L',
# Fingers Right
'thumb_1_r': 'Thumb1.R',
'thumb_2_r': 'Thumb2.R',
'thumb_3_r': 'Thumb3.R',
'index_1_r': 'Index1.R',
'index_2_r': 'Index2.R',
'index_3_r': 'Index3.R',
'middle_1_r': 'Middle1.R',
'middle_2_r': 'Middle2.R',
'middle_3_r': 'Middle3.R',
'ring_1_r': 'Ring1.R',
'ring_2_r': 'Ring2.R',
'ring_3_r': 'Ring3.R',
'pinkie_1_r': 'Pinky1.R',
'pinkie_2_r': 'Pinky2.R',
'pinkie_3_r': 'Pinky3.R',
# Eyes
'left_eye': 'Eye.L',
'right_eye': 'Eye.R'
}
bone_hierarchy = [
('Hips', 'Spine'),
('Spine', 'Chest'),
('Chest', 'Chest.Up'),
('Chest.Up', 'Neck'),
('Neck', 'Head'),
('Head', 'Eye.L'),
('Head', 'Eye.R'),
# Left Arm Chain
('Chest.Up', 'UpperArm.L'),
('UpperArm.L', 'LowerArm.L'),
('LowerArm.L', 'Hand.L'),
# Right Arm Chain
('Chest.Up', 'UpperArm.R'),
('UpperArm.R', 'LowerArm.R'),
('LowerArm.R', 'Hand.R'),
# Left Leg Chain
('Hips', 'UpperLeg.L'),
('UpperLeg.L', 'LowerLeg.L'),
('LowerLeg.L', 'Foot.L'),
('Foot.L', 'Toes.L'),
# Right Leg Chain
('Hips', 'UpperLeg.R'),
('UpperLeg.R', 'LowerLeg.R'),
('LowerLeg.R', 'Foot.R'),
('Foot.R', 'Toes.R')
]
finger_hierarchy = {
'left': [
('Hand.L', 'Thumb1.L', 'Thumb2.L', 'Thumb3.L'),
('Hand.L', 'Index1.L', 'Index2.L', 'Index3.L'),
('Hand.L', 'Middle1.L', 'Middle2.L', 'Middle3.L'),
('Hand.L', 'Ring1.L', 'Ring2.L', 'Ring3.L'),
('Hand.L', 'Pinky1.L', 'Pinky2.L', 'Pinky3.L')
],
'right': [
('Hand.R', 'Thumb1.R', 'Thumb2.R', 'Thumb3.R'),
('Hand.R', 'Index1.R', 'Index2.R', 'Index3.R'),
('Hand.R', 'Middle1.R', 'Middle2.R', 'Middle3.R'),
('Hand.R', 'Ring1.R', 'Ring2.R', 'Ring3.R'),
('Hand.R', 'Pinky1.R', 'Pinky2.R', 'Pinky3.R')
]
}
acceptable_bone_hierarchy = [
# Right side chain
('Hips', 'Chest'),
('Chest', 'Shoulder.R'),
('Shoulder.R', 'Arm.R'),
('Arm.R', 'Elbow.R'),
('Elbow.R', 'Wrist.R'),
('Hips', 'Leg.R'),
('Leg.R', 'Knee.R'),
('Knee.R', 'Foot.R'),
('Foot.R', 'Toes.R'),
# Left side chain
('Chest', 'Shoulder.L'),
('Shoulder.L', 'Arm.L'),
('Arm.L', 'Elbow.L'),
('Elbow.L', 'Wrist.L'),
('Hips', 'Leg.L'),
('Leg.L', 'Knee.L'),
('Knee.L', 'Foot.L'),
('Foot.L', 'Toes.L'),
# Head and Eyes
('Chest', 'Neck'),
('Neck', 'Head'),
('Head', 'Eye_L'),
('Head', 'Eye_R'),
('Head', 'LeftEye'),
('Head', 'RightEye'),
# Unity humanoid naming
('Hips', 'Spine'),
('Spine', 'Chest'),
('Chest', 'UpperChest'),
('UpperChest', 'Neck'),
('Neck', 'Head'),
('Head', 'LeftEye'),
('Head', 'RightEye'),
]
acceptable_bone_names = {
'hips': ['Hips', 'pelvis', 'root', 'Root', 'ROOT'],
'chest': ['Chest', 'spine1', 'Spine1', 'spine_01', 'SPINE1', 'Spine01'],
'neck': ['Neck', 'neck_01', 'Neck01'],
'head': ['Head', 'head_01', 'Head01'],
'eye_l': ['Eye_L', 'LeftEye', 'lefteye', 'eye_left', 'EyeLeft'],
'eye_r': ['Eye_R', 'RightEye', 'righteye', 'eye_right', 'EyeRight'],
'shoulder_r': ['Shoulder.R', 'clavicle_r', 'ClavicleRight', 'RightShoulder'],
'arm_r': ['Arm.R', 'upperarm_r', 'UpperArmRight', 'RightArm'],
'elbow_r': ['Elbow.R', 'lowerarm_r', 'ForearmRight', 'RightForeArm'],
'wrist_r': ['Wrist.R', 'hand_r', 'HandRight', 'RightHand'],
'leg_r': ['Leg.R', 'thigh_r', 'ThighRight', 'RightLeg', 'RightUpLeg'],
'knee_r': ['Knee.R', 'calf_r', 'CalfRight', 'RightShin', 'RightLowerLeg'],
'foot_r': ['Foot.R', 'foot_r', 'FootRight', 'RightFoot'],
'toes_r': ['Toes.R', 'ball_r', 'ToeRight', 'RightToeBase'],
'shoulder_l': ['Shoulder.L', 'clavicle_l', 'ClavicleLeft', 'LeftShoulder'],
'arm_l': ['Arm.L', 'upperarm_l', 'UpperArmLeft', 'LeftArm'],
'elbow_l': ['Elbow.L', 'lowerarm_l', 'ForearmLeft', 'LeftForeArm'],
'wrist_l': ['Wrist.L', 'hand_l', 'HandLeft', 'LeftHand'],
'leg_l': ['Leg.L', 'thigh_l', 'ThighLeft', 'LeftLeg', 'LeftUpLeg'],
'knee_l': ['Knee.L', 'calf_l', 'CalfLeft', 'LeftShin', 'LeftLowerLeg'],
'foot_l': ['Foot.L', 'foot_l', 'FootLeft', 'LeftFoot'],
'toes_l': ['Toes.L', 'ball_l', 'ToeLeft', 'LeftToeBase'],
# Add finger bones for left hand
'thumb_0_l': ['Thumb0_L'],
'thumb_1_l': ['Thumb1_L'],
'thumb_2_l': ['Thumb2_L'],
'index_1_l': ['IndexFinger1_L'],
'index_2_l': ['IndexFinger2_L'],
'index_3_l': ['IndexFinger3_L'],
'middle_1_l': ['MiddleFinger1_L'],
'middle_2_l': ['MiddleFinger2_L'],
'middle_3_l': ['MiddleFinger3_L'],
'ring_1_l': ['RingFinger1_L'],
'ring_2_l': ['RingFinger2_L'],
'ring_3_l': ['RingFinger3_L'],
# Add finger bones for right hand
'thumb_0_r': ['Thumb0_R', 'ThumbO_R'],
'thumb_1_r': ['Thumb1_R'],
'thumb_2_r': ['Thumb2_R'],
'index_1_r': ['IndexFinger1_R'],
'index_2_r': ['IndexFinger2_R'],
'index_3_r': ['IndexFinger3_R'],
'middle_1_r': ['MiddleFinger1_R'],
'middle_2_r': ['MiddleFinger2_R'],
'middle_3_r': ['MiddleFinger3_R'],
'ring_1_r': ['RingFinger1_R'],
'ring_2_r': ['RingFinger2_R'],
'ring_3_r': ['RingFinger3_R'],
'breast_upper_1_l': ['BreastUpper1_L'],
'breast_upper_2_l': ['BreastUpper2_L'],
'breast_upper_1_r': ['BreastUpper1_R'],
'breast_upper_2_r': ['BreastUpper2_R'],
'ear_upper_l': ['UpperEar.L', 'Upper Ear.L', 'Upper Ear_L'],
'ear_upper_r': ['UpperEar.R', 'Upper Ear.R', 'Upper Ear_R'],
'ear_lower_l': ['LowerEar.L', 'Lower Ear.L', 'Lower Ear_L'],
'ear_lower_r': ['LowerEar.R', 'Lower Ear.R', 'Lower Ear_R'],
'ears_upper': ['Ears Upper', 'EarsUpper', 'ears_upper'],
'ears_lower': ['Ears Lower', 'EarsLower', 'ears_lower']
}
rigify_unity_names = {
"DEF-spine": "Hips",
"DEF-spine.001": "Spine",
"DEF-spine.002": "Chest",
"DEF-spine.003": "UpperChest",
"DEF-neck": "Neck",
"DEF-head": "Head",
"DEF-shoulder.L": "LeftShoulder",
"DEF-upper_arm.L": "LeftUpperArm",
"DEF-forearm.L": "LeftLowerArm",
"DEF-hand.L": "LeftHand",
"DEF-shoulder.R": "RightShoulder",
"DEF-upper_arm.R": "RightUpperArm",
"DEF-forearm.R": "RightLowerArm",
"DEF-hand.R": "RightHand",
"DEF-thigh.L": "LeftUpperLeg",
"DEF-shin.L": "LeftLowerLeg",
"DEF-foot.L": "LeftFoot",
"DEF-toe.L": "LeftToes",
"DEF-thigh.R": "RightUpperLeg",
"DEF-shin.R": "RightLowerLeg",
"DEF-foot.R": "RightFoot",
"DEF-toe.R": "RightToes"
}
rigify_basic_unity_names = {
"spine": "Hips",
"spine.001": "Spine",
"spine.002": "Chest",
"spine.003": "UpperChest",
"neck": "Neck",
"head": "Head",
"shoulder.L": "LeftShoulder",
"upper_arm.L": "LeftUpperArm",
"forearm.L": "LeftLowerArm",
"hand.L": "LeftHand",
"shoulder.R": "RightShoulder",
"upper_arm.R": "RightUpperArm",
"forearm.R": "RightLowerArm",
"hand.R": "RightHand",
"thigh.L": "LeftUpperLeg",
"shin.L": "LeftLowerLeg",
"foot.L": "LeftFoot",
"toe.L": "LeftToes",
"thigh.R": "RightUpperLeg",
"shin.R": "RightLowerLeg",
"foot.R": "RightFoot",
"toe.R": "RightToes"
}
rigify_unnecessary_bones = [
'face',
'ear.l', 'ear.r',
'forehead',
'cheek.t.l', 'cheek.t.r',
'cheek.b.l', 'cheek.b.r',
'brow.t.l', 'brow.t.r',
'brow.b.l', 'brow.b.r',
'jaw',
'chin',
'nose',
'temple.l', 'temple.r',
'teeth',
'lip',
'lid',
'heel',
'pelvis.'
]
# Non-standard bone mappings to standard bones
non_standard_mappings = {
'hips': [
'mixamorig:Hips', 'mixamorig_Hips',
'ORG-spine', 'spine', 'root',
'hip', 'pelvis'
],
'spine': [
'mixamorig:Spine', 'mixamorig_Spine',
'ORG-spine.001', 'spine.001',
'abdomenLower', 'lowerback'
],
'chest': [
'mixamorig:Spine1', 'mixamorig_Spine1',
'ORG-spine.002', 'spine.002',
'abdomenUpper', 'upperback', 'spine1'
],
'upper_chest': [
'mixamorig:Spine2', 'mixamorig_Spine2',
'ORG-spine.003', 'spine.003',
'chestLower', 'chest', 'spine2'
],
'neck': [
'mixamorig:Neck', 'mixamorig_Neck',
'ORG-spine.004', 'spine.004', 'neck',
'neckLower'
],
'head': [
'mixamorig:Head', 'mixamorig_Head',
'ORG-spine.005', 'spine.005', 'face', 'head'
],
'left_shoulder': [
'mixamorig:LeftShoulder', 'mixamorig_LeftShoulder',
'ORG-shoulder.L', 'shoulder.L',
'lCollar', 'lShldr', 'lClavicle'
],
'left_arm': [
'mixamorig:LeftArm', 'mixamorig_LeftArm',
'ORG-upper_arm.L', 'upper_arm.L',
'lShldrBend', 'lShldrTwist', 'lArm'
],
'left_elbow': [
'mixamorig:LeftForeArm', 'mixamorig_LeftForeArm',
'ORG-forearm.L', 'forearm.L',
'lForearmBend', 'lElbow', 'lForeArm'
],
'left_wrist': [
'mixamorig:LeftHand', 'mixamorig_LeftHand',
'ORG-hand.L', 'hand.L',
'lHand', 'lWrist'
],
'right_shoulder': [
'mixamorig:RightShoulder', 'mixamorig_RightShoulder',
'ORG-shoulder.R', 'shoulder.R',
'rCollar', 'rShldr', 'rClavicle'
],
'right_arm': [
'mixamorig:RightArm', 'mixamorig_RightArm',
'ORG-upper_arm.R', 'upper_arm.R',
'rShldrBend', 'rShldrTwist', 'rArm'
],
'right_elbow': [
'mixamorig:RightForeArm', 'mixamorig_RightForeArm',
'ORG-forearm.R', 'forearm.R',
'rForearmBend', 'rElbow', 'rForeArm'
],
'right_wrist': [
'mixamorig:RightHand', 'mixamorig_RightHand',
'ORG-hand.R', 'hand.R',
'rHand', 'rWrist'
],
'left_leg': [
'mixamorig:LeftUpLeg', 'mixamorig_LeftUpLeg',
'ORG-thigh.L', 'thigh.L',
'lThighBend', 'lThigh'
],
'left_knee': [
'mixamorig:LeftLeg', 'mixamorig_LeftLeg',
'ORG-shin.L', 'shin.L',
'lShin', 'lKnee', 'lLeg'
],
'left_ankle': [
'mixamorig:LeftFoot', 'mixamorig_LeftFoot',
'ORG-foot.L', 'foot.L',
'lFoot', 'lAnkle'
],
'left_toe': [
'mixamorig:LeftToeBase', 'mixamorig_LeftToeBase',
'ORG-toe.L', 'toe.L',
'lToe'
],
'right_leg': [
'mixamorig:RightUpLeg', 'mixamorig_RightUpLeg',
'ORG-thigh.R', 'thigh.R',
'rThighBend', 'rThigh'
],
'right_knee': [
'mixamorig:RightLeg', 'mixamorig_RightLeg',
'ORG-shin.R', 'shin.R',
'rShin', 'rKnee', 'rLeg'
],
'right_ankle': [
'mixamorig:RightFoot', 'mixamorig_RightFoot',
'ORG-foot.R', 'foot.R',
'rFoot', 'rAnkle'
],
'right_toe': [
'mixamorig:RightToeBase', 'mixamorig_RightToeBase',
'ORG-toe.R', 'toe.R',
'rToe'
],
'thumb_1_l': [
'mixamorig:LeftHandThumb1', 'mixamorig_LeftHandThumb1',
'ORG-thumb.01.L', 'thumb.01.L',
'lThumb1'
],
'thumb_2_l': [
'mixamorig:LeftHandThumb2', 'mixamorig_LeftHandThumb2',
'ORG-thumb.02.L', 'thumb.02.L',
'lThumb2'
],
'thumb_3_l': [
'mixamorig:LeftHandThumb3', 'mixamorig_LeftHandThumb3',
'ORG-thumb.03.L', 'thumb.03.L',
'lThumb3'
],
'index_1_l': [
'mixamorig:LeftHandIndex1', 'mixamorig_LeftHandIndex1',
'ORG-f_index.01.L', 'f_index.01.L',
'lIndex1'
],
'index_2_l': [
'mixamorig:LeftHandIndex2', 'mixamorig_LeftHandIndex2',
'ORG-f_index.02.L', 'f_index.02.L',
'lIndex2'
],
'index_3_l': [
'mixamorig:LeftHandIndex3', 'mixamorig_LeftHandIndex3',
'ORG-f_index.03.L', 'f_index.03.L',
'lIndex3'
],
'middle_1_l': [
'mixamorig:LeftHandMiddle1', 'mixamorig_LeftHandMiddle1',
'ORG-f_middle.01.L', 'f_middle.01.L',
'lMid1'
],
'middle_2_l': [
'mixamorig:LeftHandMiddle2', 'mixamorig_LeftHandMiddle2',
'ORG-f_middle.02.L', 'f_middle.02.L',
'lMid2'
],
'middle_3_l': [
'mixamorig:LeftHandMiddle3', 'mixamorig_LeftHandMiddle3',
'ORG-f_middle.03.L', 'f_middle.03.L',
'lMid3'
],
'ring_1_l': [
'mixamorig:LeftHandRing1', 'mixamorig_LeftHandRing1',
'ORG-f_ring.01.L', 'f_ring.01.L',
'lRing1'
],
'ring_2_l': [
'mixamorig:LeftHandRing2', 'mixamorig_LeftHandRing2',
'ORG-f_ring.02.L', 'f_ring.02.L',
'lRing2'
],
'ring_3_l': [
'mixamorig:LeftHandRing3', 'mixamorig_LeftHandRing3',
'ORG-f_ring.03.L', 'f_ring.03.L',
'lRing3'
],
'pinkie_1_l': [
'mixamorig:LeftHandPinky1', 'mixamorig_LeftHandPinky1',
'ORG-f_pinky.01.L', 'f_pinky.01.L',
'lPinky1'
],
'pinkie_2_l': [
'mixamorig:LeftHandPinky2', 'mixamorig_LeftHandPinky2',
'ORG-f_pinky.02.L', 'f_pinky.02.L',
'lPinky2'
],
'pinkie_3_l': [
'mixamorig:LeftHandPinky3', 'mixamorig_LeftHandPinky3',
'ORG-f_pinky.03.L', 'f_pinky.03.L',
'lPinky3'
],
'thumb_1_r': [
'mixamorig:RightHandThumb1', 'mixamorig_RightHandThumb1',
'ORG-thumb.01.R', 'thumb.01.R',
'rThumb1'
],
'thumb_2_r': [
'mixamorig:RightHandThumb2', 'mixamorig_RightHandThumb2',
'ORG-thumb.02.R', 'thumb.02.R',
'rThumb2'
],
'thumb_3_r': [
'mixamorig:RightHandThumb3', 'mixamorig_RightHandThumb3',
'ORG-thumb.03.R', 'thumb.03.R',
'rThumb3'
],
'index_1_r': [
'mixamorig:RightHandIndex1', 'mixamorig_RightHandIndex1',
'ORG-f_index.01.R', 'f_index.01.R',
'rIndex1'
],
'index_2_r': [
'mixamorig:RightHandIndex2', 'mixamorig_RightHandIndex2',
'ORG-f_index.02.R', 'f_index.02.R',
'rIndex2'
],
'index_3_r': [
'mixamorig:RightHandIndex3', 'mixamorig_RightHandIndex3',
'ORG-f_index.03.R', 'f_index.03.R',
'rIndex3'
],
'middle_1_r': [
'mixamorig:RightHandMiddle1', 'mixamorig_RightHandMiddle1',
'ORG-f_middle.01.R', 'f_middle.01.R',
'rMid1'
],
'middle_2_r': [
'mixamorig:RightHandMiddle2', 'mixamorig_RightHandMiddle2',
'ORG-f_middle.02.R', 'f_middle.02.R',
'rMid2'
],
'middle_3_r': [
'mixamorig:RightHandMiddle3', 'mixamorig_RightHandMiddle3',
'ORG-f_middle.03.R', 'f_middle.03.R',
'rMid3'
],
'ring_1_r': [
'mixamorig:RightHandRing1', 'mixamorig_RightHandRing1',
'ORG-f_ring.01.R', 'f_ring.01.R',
'rRing1'
],
'ring_2_r': [
'mixamorig:RightHandRing2', 'mixamorig_RightHandRing2',
'ORG-f_ring.02.R', 'f_ring.02.R',
'rRing2'
],
'ring_3_r': [
'mixamorig:RightHandRing3', 'mixamorig_RightHandRing3',
'ORG-f_ring.03.R', 'f_ring.03.R',
'rRing3'
],
'pinkie_1_r': [
'mixamorig:RightHandPinky1', 'mixamorig_RightHandPinky1',
'ORG-f_pinky.01.R', 'f_pinky.01.R',
'rPinky1'
],
'pinkie_2_r': [
'mixamorig:RightHandPinky2', 'mixamorig_RightHandPinky2',
'ORG-f_pinky.02.R', 'f_pinky.02.R',
'rPinky2'
],
'pinkie_3_r': [
'mixamorig:RightHandPinky3', 'mixamorig_RightHandPinky3',
'ORG-f_pinky.03.R', 'f_pinky.03.R',
'rPinky3'
],
'left_eye': [
'mixamorig:LeftEye', 'mixamorig_LeftEye',
'ORG-eye.L', 'eye.L',
'lEye'
],
'right_eye': [
'mixamorig:RightEye', 'mixamorig_RightEye',
'ORG-eye.R', 'eye.R',
'rEye'
]
}
for category, mappings in non_standard_mappings.items():
if category in bone_names:
bone_names[category].extend(mappings)
else:
bone_names[category] = mappings
# Since data set is very poisoned by bone names that aren't simplified (And as such will not map properly using the function) we will just force convert them to the proper format at the end here. - @989onan
for standard, mappings in bone_names.items():
for i in range(len(mappings)):
bone_names[standard][i] = simplify_bonename(mappings[i])
# Create reverse lookup dictionary (conversion/translation)
reverse_bone_lookup = {}
for preferred_name, name_list in bone_names.items():
for name in name_list:
reverse_bone_lookup[name] = preferred_name
-271
View File
@@ -1,271 +0,0 @@
import bpy
import struct
import mathutils
import traceback
import os
from bpy.types import Material, Operator, Context, Object, Image, Mesh, MeshUVLoopLayer, Float2AttributeValue, ShaderNodeTexImage, ShaderNodeBsdfPrincipled, ShaderNodeOutputMaterial
def read_pmd_header(file):
# Read PMD header information
magic = file.read(3)
if magic != b'Pmd':
raise ValueError("Invalid PMD file")
version = struct.unpack('<f', file.read(4))[0]
# Read additional header fields
model_name = file.read(20).decode('shift-jis').rstrip('\0')
comment = file.read(256).decode('shift-jis').rstrip('\0')
return version, model_name, comment
def read_pmd_vertex(file):
# Read PMD vertex information
position = struct.unpack('<3f', file.read(12))
normal = struct.unpack('<3f', file.read(12))
uv = struct.unpack('<2f', file.read(8))
bone_indices = list(struct.unpack('<2H', file.read(4)))
bone_weights = struct.unpack('<b', file.read(1))[0] / 100
edge_flag = struct.unpack('<b', file.read(1))[0]
return position, normal, uv, bone_indices, bone_weights, edge_flag
def read_pmd_material(file):
# Read PMD material information
diffuse_color = struct.unpack('<4f', file.read(16))
specular_color = struct.unpack('<3f', file.read(12))
specular_intensity = struct.unpack('<f', file.read(4))[0]
ambient_color = struct.unpack('<3f', file.read(12))
toon_index = struct.unpack('<b', file.read(1))[0]
edge_flag = struct.unpack('<b', file.read(1))[0]
vertex_count = struct.unpack('<i', file.read(4))[0]
texture_file_name = file.read(20).decode('shift-jis').rstrip('\0')
return diffuse_color, specular_color, specular_intensity, ambient_color, toon_index, edge_flag, vertex_count, texture_file_name
def read_pmd_bone(file):
# Read PMD bone information
bone_name = file.read(20).decode('shift-jis').rstrip('\0')
parent_bone_index = struct.unpack('<h', file.read(2))[0]
tail_pos_bone_index = struct.unpack('<h', file.read(2))[0]
bone_type = struct.unpack('<b', file.read(1))[0]
ik_parent_bone_index = struct.unpack('<h', file.read(2))[0]
bone_head_pos = struct.unpack('<3f', file.read(12))
return bone_name, parent_bone_index, tail_pos_bone_index, bone_type, ik_parent_bone_index, bone_head_pos
def read_pmd_ik(file):
# Read PMD IK information
ik_bone_index = struct.unpack('<h', file.read(2))[0]
ik_target_bone_index = struct.unpack('<h', file.read(2))[0]
ik_chain_length = struct.unpack('<b', file.read(1))[0]
iterations = struct.unpack('<h', file.read(2))[0]
limit_angle = struct.unpack('<f', file.read(4))[0]
ik_child_bone_indices = []
for _ in range(ik_chain_length):
ik_child_bone_index = struct.unpack('<h', file.read(2))[0]
ik_child_bone_indices.append(ik_child_bone_index)
return ik_bone_index, ik_target_bone_index, ik_chain_length, iterations, limit_angle, ik_child_bone_indices
def read_pmd_morph(file):
# Read PMD morph information
morph_name = file.read(20).decode('shift-jis').rstrip('\0')
morph_vertex_count = struct.unpack('<i', file.read(4))[0]
morph_type = struct.unpack('<b', file.read(1))[0]
morph_vertices = []
for _ in range(morph_vertex_count):
morph_vertex_index = struct.unpack('<i', file.read(4))[0]
morph_vertex_pos = struct.unpack('<3f', file.read(12))
morph_vertices.append((morph_vertex_index, morph_vertex_pos))
return morph_name, morph_vertex_count, morph_type, morph_vertices
def import_pmd(filepath):
try:
with open(filepath, 'rb') as file:
version, model_name, comment = read_pmd_header(file)
# Read vertices
vertex_count = struct.unpack('<i', file.read(4))[0]
vertices = []
for _ in range(vertex_count):
position, normal, uv, bone_indices, bone_weights, edge_flag = read_pmd_vertex(file)
vertices.append((position, normal, uv, bone_indices, bone_weights, edge_flag))
# Read faces
face_count = struct.unpack('<i', file.read(4))[0]
faces = []
for _ in range(face_count // 3):
face_indices = struct.unpack('<3i', file.read(12))
faces.append(face_indices)
# Read materials
material_count = struct.unpack('<i', file.read(4))[0]
materials = []
for _ in range(material_count):
diffuse_color, specular_color, specular_intensity, ambient_color, toon_index, edge_flag, vertex_count, texture_file_name = read_pmd_material(file)
materials.append((diffuse_color, specular_color, specular_intensity, ambient_color, toon_index, edge_flag, vertex_count, texture_file_name))
# Read bones
bone_count = struct.unpack('<h', file.read(2))[0]
bones = []
for _ in range(bone_count):
bone_name, parent_bone_index, tail_pos_bone_index, bone_type, ik_parent_bone_index, bone_head_pos = read_pmd_bone(file)
bones.append((bone_name, parent_bone_index, tail_pos_bone_index, bone_type, ik_parent_bone_index, bone_head_pos))
# Read IKs
ik_count = struct.unpack('<h', file.read(2))[0]
iks = []
for _ in range(ik_count):
ik_bone_index, ik_target_bone_index, ik_chain_length, iterations, limit_angle, ik_child_bone_indices = read_pmd_ik(file)
iks.append((ik_bone_index, ik_target_bone_index, ik_chain_length, iterations, limit_angle, ik_child_bone_indices))
# Read morphs
morph_count = struct.unpack('<h', file.read(2))[0]
morphs = []
for _ in range(morph_count):
morph_name, morph_vertex_count, morph_type, morph_vertices = read_pmd_morph(file)
morphs.append((morph_name, morph_vertex_count, morph_type, morph_vertices))
# Create Blender objects and assign PMD data
mesh = bpy.data.meshes.new(model_name)
mesh.from_pydata([v[0] for v in vertices], [], faces)
mesh.update()
obj = bpy.data.objects.new(model_name, mesh)
bpy.context.collection.objects.link(obj)
# Assign vertex normals
for i, vertex in enumerate(vertices):
mesh.vertices[i].normal = vertex[1]
# Assign UV coordinates
uv_layer = mesh.uv_layers.new()
for i, vertex in enumerate(vertices):
uv_layer.data[i].uv = vertex[2]
# Assign materials
for material_data in materials:
material: bpy.types.Material
if f"Material_{len(mesh.materials)}" in bpy.data.materials:
material = bpy.data.materials[f"Material_{len(mesh.materials)}"]
else:
material = bpy.data.materials.new(f"Material_{len(mesh.materials)}")
material.use_nodes = True
for node in [node for node in material.node_tree.nodes]:
material.node_tree.nodes.remove(node)
principled_node: ShaderNodeBsdfPrincipled = material.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled")
principled_node.location.x = 7.29706335067749
principled_node.location.y = 298.918212890625
principled_node.inputs["Base Color"].default_value = material_data[0]
principled_node.inputs["Specular Tint"].default_value = [material_data[1][0],material_data[1][1],material_data[1][2],1.0]
principled_node.inputs["Specular IOR Level"].default_value = material_data[2]
output_node: ShaderNodeOutputMaterial = material.node_tree.nodes.new(type="ShaderNodeOutputMaterial")
output_node.location.x = 297.29705810546875
output_node.location.y = 298.918212890625
albedo_node: ShaderNodeTexImage = material.node_tree.nodes.new(type="ShaderNodeTexImage")
albedo_node.location.x = -588.6177978515625
albedo_node.location.y = 414.1948547363281
if texture_file_name in bpy.data.images:
albedo_node.image = bpy.data.images[texture_file_name]
else:
albedo_node.image = bpy.data.images.new(name=texture_file_name,width=32,height=32)
albedo_node.image.filepath = os.path.join(os.path.dirname(filepath),texture_file_name)
albedo_node.image.source = 'FILE'
albedo_node.image.reload()
material.node_tree.links.new(principled_node.inputs["Base Color"], albedo_node.outputs["Color"])
material.node_tree.links.new(principled_node.inputs["Alpha"], albedo_node.outputs["Alpha"])
material.node_tree.links.new(output_node.inputs["Surface"], principled_node.outputs["BSDF"])
#material.ambient = material_data[5] #TODO: this doesn't exist
# Set other material properties based on the PMX data
if not (material.name in mesh.materials):
mesh.materials.append(material)
#surprised this works - @989onan
end: int = cur_polygon_index+material_data[15]-1
for face in mesh.polygons.items()[cur_polygon_index:end]:
face[1].material_index = mesh.materials.find(material.name)
cur_polygon_index = cur_polygon_index+material_data[15]
# Set other material properties based on the PMD data
# Create armature and assign bones
armature = bpy.data.armatures.new(model_name + "_Armature")
armature_obj = bpy.data.objects.new(model_name + "_Armature", armature)
bpy.context.collection.objects.link(armature_obj)
bpy.context.view_layer.objects.active = armature_obj
bpy.ops.object.mode_set(mode='EDIT')
for bone_data in bones:
bone = armature.edit_bones.new(bone_data[0])
bone.head = bone_data[5]
if bone_data[1] != -1:
parent_bone = armature.edit_bones[bone_data[1]]
bone.parent = parent_bone
bone.tail = parent_bone.head
else:
bone.tail = bone.head + mathutils.Vector((0, 0.1, 0))
# Set other bone properties based on the PMD data
bpy.ops.object.mode_set(mode='OBJECT')
# Assign bone weights to the mesh
for i, vertex in enumerate(vertices):
for j in range(2):
if vertex[3][j] != 65535:
bone_name = bones[vertex[3][j]][0]
weight = vertex[4] if j == 0 else 1 - vertex[4]
vertex_group = obj.vertex_groups.get(bone_name)
if not vertex_group:
vertex_group = obj.vertex_groups.new(name=bone_name)
vertex_group.add([i], weight, 'REPLACE')
# Assign IK constraints to bones
for ik_data in iks:
ik_bone = armature.bones[bones[ik_data[0]][0]]
ik_target_bone = armature.bones[bones[ik_data[1]][0]]
ik_constraint = ik_bone.constraints.new('IK')
ik_constraint.target = armature_obj
ik_constraint.subtarget = ik_target_bone.name
ik_constraint.chain_count = ik_data[2]
ik_constraint.iterations = ik_data[3]
ik_constraint.limit_mode = 'LIMITDIST_INSIDE'
ik_constraint.limit_mode_max_x = ik_data[4]
# Assign morphs to the mesh
for morph_data in morphs:
morph_name = morph_data[0]
morph_type = morph_data[2]
if morph_type == 0: # Vertex morph
shape_key = obj.shape_key_add(name=morph_name)
for vertex_data in morph_data[3]:
vertex_index = vertex_data[0]
vertex_offset = vertex_data[1]
shape_key.data[vertex_index].co += mathutils.Vector(vertex_offset)
print(f"Successfully imported PMD file: {filepath}")
print(f"Model Name: {model_name}")
print(f"Comment: {comment}")
except Exception:
print(f"Error importing PMD file: {filepath}")
print(f"Error details: {traceback.format_exc()}")
-861
View File
@@ -1,861 +0,0 @@
from io import BufferedReader
import os
import bpy
import struct
import traceback
import mathutils
from mathutils import Matrix, Vector
class PMXVertex:
def __init__(self, position, normal, uv, bone_indices, bone_weights, edge_scale, additional_uvs):
self.position = position
self.normal = normal
self.uv = uv
self.bone_indices = bone_indices
self.bone_weights = bone_weights
self.edge_scale = edge_scale
self.additional_uvs = additional_uvs
class PMXBone:
def __init__(self, name, english_name, position, parent_index, layer, flag,
tail_position, inherit_parent_index, inherit_influence,
fixed_axis, local_x, local_z, external_key,
ik_target_index, ik_loop_count, ik_limit_rad, ik_links):
self.name = name
self.english_name = english_name
self.position = position
self.parent_index = parent_index
self.layer = layer
self.flag = flag
self.tail_position = tail_position
self.inherit_parent_index = inherit_parent_index
self.inherit_influence = inherit_influence
self.fixed_axis = fixed_axis
self.local_x = local_x
self.local_z = local_z
self.external_key = external_key
self.ik_target_index = ik_target_index
self.ik_loop_count = ik_loop_count
self.ik_limit_rad = ik_limit_rad
self.ik_links = ik_links
class PMXMaterial:
def __init__(self, name, english_name, diffuse, specular, specular_strength,
ambient, flag, edge_color, edge_size, texture_index,
sphere_texture_index, sphere_mode, toon_sharing_flag,
toon_texture_index, comment, surface_count):
self.name = name
self.english_name = english_name
self.diffuse = diffuse
self.specular = specular
self.specular_strength = specular_strength
self.ambient = ambient
self.flag = flag
self.edge_color = edge_color
self.edge_size = edge_size
self.texture_index = texture_index
self.sphere_texture_index = sphere_texture_index
self.sphere_mode = sphere_mode
self.toon_sharing_flag = toon_sharing_flag
self.toon_texture_index = toon_texture_index
self.comment = comment
self.surface_count = surface_count
class PMXMorph:
def __init__(self, name, english_name, panel, morph_type, offsets):
self.name = name
self.english_name = english_name
self.panel = panel
self.morph_type = morph_type
self.offsets = offsets
class PMXRigidBody:
def __init__(self, name, bone_index, group, shape_type, size, position, rotation, mass, linear_damping, angular_damping, restitution, friction, mode):
self.name = name
self.bone_index = bone_index
self.group = group
self.shape_type = shape_type
self.size = size
self.position = position
self.rotation = rotation
self.mass = mass
self.linear_damping = linear_damping
self.angular_damping = angular_damping
self.restitution = restitution
self.friction = friction
self.mode = mode
class PMXJoint:
def __init__(self, name, joint_type, rigid_body_a, rigid_body_b, position, rotation, linear_limit_min, linear_limit_max, angular_limit_min, angular_limit_max, spring_constant_translation, spring_constant_rotation):
self.name = name
self.joint_type = joint_type
self.rigid_body_a = rigid_body_a
self.rigid_body_b = rigid_body_b
self.position = position
self.rotation = rotation
self.linear_limit_min = linear_limit_min
self.linear_limit_max = linear_limit_max
self.angular_limit_min = angular_limit_min
self.angular_limit_max = angular_limit_max
self.spring_constant_translation = spring_constant_translation
self.spring_constant_rotation = spring_constant_rotation
def read_pmx_header(file: BufferedReader):
magic = file.read(4)
if magic != b'PMX ':
raise ValueError("Invalid PMX file")
version = struct.unpack('<f', file.read(4))[0]
data_size = struct.unpack('<b', file.read(1))[0]
encoding = struct.unpack('<b', file.read(1))[0]
additional_uvs = struct.unpack('<b', file.read(1))[0]
vertex_index_size = struct.unpack('<b', file.read(1))[0]
texture_index_size = struct.unpack('<b', file.read(1))[0]
material_index_size = struct.unpack('<b', file.read(1))[0]
bone_index_size = struct.unpack('<b', file.read(1))[0]
morph_index_size = struct.unpack('<b', file.read(1))[0]
rigid_body_index_size = struct.unpack('<b', file.read(1))[0]
model_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
model_english_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
model_comment = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
model_english_comment = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
return (version, encoding, additional_uvs, vertex_index_size, texture_index_size,
material_index_size, bone_index_size, morph_index_size, rigid_body_index_size,
model_name, model_english_name, model_comment, model_english_comment)
def read_index_size(index, types):
struct_format = "<??"
byte_size = 0
if index == 1:
struct_format = replace_char(struct_format, 2, types[0])
byte_size = 1
elif index == 2:
struct_format = replace_char(struct_format, 2, types[1])
byte_size = 2
else:
struct_format = replace_char(struct_format, 2, types[2])
byte_size = 4
return struct_format, byte_size
def replace_char(string, index, character):
temp = list(string)
temp[index] = character
return "".join(temp)
def read_morph(file: BufferedReader, vertex_struct, vertex_size):
try:
name_length = struct.unpack('<i', file.read(4))[0]
name = str(file.read(name_length), 'utf-16-le', errors='replace')
english_name_length = struct.unpack('<i', file.read(4))[0]
english_name = str(file.read(english_name_length), 'utf-16-le', errors='replace')
panel = int.from_bytes(file.read(1), byteorder='little', signed=True)
morph_type = int.from_bytes(file.read(1), byteorder='little', signed=True)
# Read offset count with error checking
offset_count_bytes = file.read(4)
if len(offset_count_bytes) != 4:
return PMXMorph(name, english_name, panel, morph_type, [])
offset_count = struct.unpack('<i', offset_count_bytes)[0]
offsets = []
if morph_type == 1: # Vertex morph
for _ in range(offset_count):
vertex_index = struct.unpack(replace_char(vertex_struct, 1, '1'), file.read(vertex_size))[0]
offset = struct.unpack('<3f', file.read(12))
offsets.append((vertex_index, offset))
return PMXMorph(name, english_name, panel, morph_type, offsets)
except:
return PMXMorph("", "", 0, 0, [])
def validate_pmx_data(header_data, vertices, faces, materials, bones):
"""Validate PMX data integrity"""
if not vertices:
raise ValueError("No vertices found in PMX file")
if not faces:
raise ValueError("No faces found in PMX file")
if not materials:
raise ValueError("No materials found in PMX file")
if not bones:
raise ValueError("No bones found in PMX file")
return True
def handle_import_error(context, error_msg):
"""Handle import errors with user feedback"""
context.window_manager.progress_end()
bpy.ops.ui.popup_menu(message=error_msg)
return {'CANCELLED'}
def read_vertex(file: BufferedReader, string_build, byte_size, additional_uvs):
position = struct.unpack('<3f', file.read(12))
normal = struct.unpack('<3f', file.read(12))
uv = struct.unpack('<2f', file.read(8))
uv = [uv[0], (1.0-uv[1])-1.0]
additional_uv_read = []
for _ in range(additional_uvs):
additional_uv_read.append(struct.unpack('<4f', file.read(16)))
weight_deform_type = struct.unpack('<B', file.read(1))[0]
bone_indices = []
bone_weights = []
if weight_deform_type == 0: # BDEF1
string_build = replace_char(string_build, 1, '1')
bone_indices = list(struct.unpack(string_build, file.read(byte_size*1)))
bone_weights = [1.0]
elif weight_deform_type == 1: # BDEF2
string_build = replace_char(string_build, 1, '2')
bone_indices = list(struct.unpack(string_build, file.read(byte_size*2)))
weight = struct.unpack('<f', file.read(4))[0]
bone_weights = [weight, 1.0-weight]
elif weight_deform_type == 2: # BDEF4
string_build = replace_char(string_build, 1, '4')
bone_indices = list(struct.unpack(string_build, file.read(byte_size*4)))
bone_weights = list(struct.unpack('<4f', file.read(16)))
elif weight_deform_type == 3: # SDEF
string_build = replace_char(string_build, 1, '2')
bone_indices = list(struct.unpack(string_build, file.read(byte_size*2)))
weight = struct.unpack('<f', file.read(4))[0]
bone_weights = [weight, 1.0-weight]
# Skip SDEF data as we don't use it
file.read(36) # 3 vectors of 3 floats each (C, R0, R1)
elif weight_deform_type == 4: # QDEF
string_build = replace_char(string_build, 1, '4')
bone_indices = list(struct.unpack(string_build, file.read(byte_size*4)))
bone_weights = list(struct.unpack('<4f', file.read(16)))
edge_scale = struct.unpack('<f', file.read(4))[0]
return PMXVertex(position, normal, uv, bone_indices, bone_weights, edge_scale, additional_uv_read)
def read_material(file: BufferedReader, string_build, byte_size):
material_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
material_english_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
diffuse_color = struct.unpack('<4f', file.read(16))
specular_color = struct.unpack('<3f', file.read(12))
specular_strength = struct.unpack('<f', file.read(4))[0]
ambient_color = struct.unpack('<3f', file.read(12))
flag = struct.unpack('<b', file.read(1))[0]
edge_color = struct.unpack('<4f', file.read(16))
edge_size = struct.unpack('<f', file.read(4))[0]
texture_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
sphere_texture_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
sphere_mode = struct.unpack('<b', file.read(1))[0]
toon_sharing_flag = struct.unpack('<b', file.read(1))[0]
if toon_sharing_flag == 0:
toon_texture_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
else:
toon_texture_index = struct.unpack('<b', file.read(1))[0]
comment = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
surface_count = int(struct.unpack('<i', file.read(4))[0]/3)
return PMXMaterial(material_name, material_english_name, diffuse_color, specular_color,
specular_strength, ambient_color, flag, edge_color, edge_size,
texture_index, sphere_texture_index, sphere_mode,
toon_sharing_flag, toon_texture_index, comment, surface_count)
def create_material_nodes(material: bpy.types.Material, texture_path: str, diffuse_color, specular_color, specular_strength, toon_texture_path=None):
material.use_nodes = True
nodes = material.node_tree.nodes
links = material.node_tree.links
nodes.clear()
principled = nodes.new("ShaderNodeBsdfPrincipled")
principled.location = (0, 0)
principled.inputs["Base Color"].default_value = diffuse_color
principled.inputs["Specular IOR Level"].default_value = specular_strength
principled.inputs["Specular Tint"].default_value = (*specular_color, 1.0)
# Handle transparency
if diffuse_color[3] < 1.0:
material.blend_method = 'HASHED'
principled.inputs["Alpha"].default_value = diffuse_color[3]
output = nodes.new("ShaderNodeOutputMaterial")
output.location = (300, 0)
# Main texture
if texture_path and os.path.exists(texture_path):
texture = nodes.new("ShaderNodeTexImage")
texture.location = (-300, 0)
texture.image = bpy.data.images.load(texture_path)
links.new(texture.outputs["Color"], principled.inputs["Base Color"])
links.new(texture.outputs["Alpha"], principled.inputs["Alpha"])
# Toon texture
if toon_texture_path and os.path.exists(toon_texture_path):
toon = nodes.new("ShaderNodeTexImage")
toon.location = (-300, -300)
toon.image = bpy.data.images.load(toon_texture_path)
mix = nodes.new("ShaderNodeMixRGB")
mix.location = (-50, -150)
mix.blend_type = 'MULTIPLY'
links.new(toon.outputs["Color"], mix.inputs[2])
links.new(mix.outputs["Color"], principled.inputs["Base Color"])
links.new(principled.outputs["BSDF"], output.inputs["Surface"])
def read_bone(file: BufferedReader, string_build, byte_size):
bone_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
bone_english_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
position = struct.unpack('<3f', file.read(12))
parent_bone_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
layer = struct.unpack('<i', file.read(4))[0]
flag = struct.unpack('<H', file.read(2))[0]
tail_position = [None, None, None]
inherit_bone_parent_index = 0
inherit_bone_parent_influence = 0.0
fixed_axis = [0.0, 0.0, 0.0]
local_x_vector = [0.0, 0.0, 0.0]
local_z_vector = [0.0, 0.0, 0.0]
external_key = 0
ik_target_bone_index = 0
ik_loop_count = -1
ik_limit_radian = 0.0
ik_links = []
if not (flag & 0x0001):
tail_position = struct.unpack('<3f', file.read(12))
else:
tail_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
if flag & 0x0100 or flag & 0x0200:
inherit_bone_parent_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
inherit_bone_parent_influence = struct.unpack('<f', file.read(4))[0]
if flag & 0x0400:
fixed_axis = struct.unpack('<3f', file.read(12))
if flag & 0x0800:
local_x_vector = struct.unpack('<3f', file.read(12))
local_z_vector = struct.unpack('<3f', file.read(12))
if flag & 0x2000:
external_key = struct.unpack('<i', file.read(4))[0]
if flag & 0x0020:
ik_target_bone_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
ik_loop_count = struct.unpack('<i', file.read(4))[0]
ik_limit_radian = struct.unpack('<f', file.read(4))[0]
ik_link_count = struct.unpack('<i', file.read(4))[0]
for _ in range(ik_link_count):
ik_link_bone_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
ik_link_limit = struct.unpack('<b', file.read(1))[0]
if ik_link_limit == 1:
angle_limit = (struct.unpack('<3f', file.read(12)), struct.unpack('<3f', file.read(12)))
ik_links.append((ik_link_bone_index, True, angle_limit))
else:
ik_links.append((ik_link_bone_index, False, None))
return PMXBone(bone_name, bone_english_name, position, parent_bone_index, layer,
flag, tail_position, inherit_bone_parent_index, inherit_bone_parent_influence,
fixed_axis, local_x_vector, local_z_vector, external_key,
ik_target_bone_index, ik_loop_count, ik_limit_radian, ik_links)
def create_bone_constraints(armature_obj: bpy.types.Object, bones: list[PMXBone]):
bpy.context.view_layer.objects.active = armature_obj
bpy.ops.object.mode_set(mode='POSE')
# Clear existing constraints
for pose_bone in armature_obj.pose.bones:
while pose_bone.constraints:
pose_bone.constraints.remove(pose_bone.constraints[0])
# Handle rotation inheritance first
for bone_data in bones:
pose_bone = armature_obj.pose.bones.get(bone_data.name)
if not pose_bone or bone_data.parent_index < 0:
continue
# Check if bone has vertex groups
if not pose_bone.bone.use_deform:
continue
if bone_data.flag & 0x0100: # Rotation inheritance
if bone_data.inherit_parent_index >= 0:
constraint = pose_bone.constraints.new('COPY_ROTATION')
constraint.name = "MMD Rotation"
constraint.target = armature_obj
constraint.subtarget = bones[bone_data.inherit_parent_index].name
constraint.influence = bone_data.inherit_influence
constraint.target_space = 'LOCAL'
constraint.owner_space = 'LOCAL'
# Then handle IK constraints
for bone_data in bones:
pose_bone = armature_obj.pose.bones.get(bone_data.name)
if not pose_bone:
continue
# Skip non-deforming bones
if not pose_bone.bone.use_deform:
continue
if bone_data.flag & 0x0020: # IK
if bone_data.ik_target_index >= 0:
constraint = pose_bone.constraints.new('IK')
constraint.name = "MMD IK"
constraint.target = armature_obj
constraint.subtarget = bones[bone_data.ik_target_index].name
constraint.chain_count = min(len(bone_data.ik_links), 3)
constraint.iterations = min(bone_data.ik_loop_count, 8)
constraint.use_tail = False
constraint.use_stretch = False
# Configure IK chain
for link_bone_index, has_limits, angle_limits in bone_data.ik_links:
link_pose_bone = armature_obj.pose.bones.get(bones[link_bone_index].name)
if link_pose_bone and link_pose_bone.bone.use_deform:
link_pose_bone.rotation_mode = 'XYZ'
link_pose_bone.use_ik_limit_x = True
link_pose_bone.use_ik_limit_y = True
link_pose_bone.use_ik_limit_z = True
if has_limits and angle_limits:
min_angles, max_angles = angle_limits
link_pose_bone.ik_min_x = max(-1.4, min_angles[0])
link_pose_bone.ik_max_x = min(1.4, max_angles[0])
link_pose_bone.ik_min_y = max(-1.4, min_angles[1])
link_pose_bone.ik_max_y = min(1.4, max_angles[1])
link_pose_bone.ik_min_z = max(-1.4, min_angles[2])
link_pose_bone.ik_max_z = min(1.4, max_angles[2])
# Reset pose to default state
bpy.ops.pose.select_all(action='SELECT')
bpy.ops.pose.transforms_clear()
bpy.ops.pose.select_all(action='DESELECT')
bpy.ops.object.mode_set(mode='OBJECT')
def setup_physics(obj: bpy.types.Object, armature_obj: bpy.types.Object, rigid_bodies: list[PMXRigidBody], joints: list[PMXJoint]):
"""Set up physics for PMX model"""
# Create rigid body collection if it doesn't exist
if 'RigidBodies' not in bpy.data.collections:
rigid_body_collection = bpy.data.collections.new('RigidBodies')
bpy.context.scene.collection.children.link(rigid_body_collection)
else:
rigid_body_collection = bpy.data.collections['RigidBodies']
# Create rigid bodies
for rb in rigid_bodies:
# Create mesh based on shape type
if rb.shape_type == 0: # Sphere
bpy.ops.mesh.primitive_uv_sphere_add(radius=rb.size[0])
elif rb.shape_type == 1: # Box
bpy.ops.mesh.primitive_cube_add()
bpy.context.active_object.scale = rb.size
elif rb.shape_type == 2: # Capsule
bpy.ops.mesh.primitive_cylinder_add(radius=rb.size[0], depth=rb.size[1])
rb_obj = bpy.context.active_object
rb_obj.name = f"RB_{rb.name}"
rb_obj.location = rb.position
rb_obj.rotation_euler = rb.rotation
# Set up rigid body physics
rb_obj.rigid_body.type = 'ACTIVE' if rb.mode == 0 else 'PASSIVE'
rb_obj.rigid_body.mass = rb.mass
rb_obj.rigid_body.linear_damping = rb.linear_damping
rb_obj.rigid_body.angular_damping = rb.angular_damping
rb_obj.rigid_body.restitution = rb.restitution
rb_obj.rigid_body.friction = rb.friction
# Parent to bone if specified
if rb.bone_index >= 0:
rb_obj.parent = armature_obj
rb_obj.parent_type = 'BONE'
rb_obj.parent_bone = bones[rb.bone_index].name
# Move to rigid body collection
rigid_body_collection.objects.link(rb_obj)
bpy.context.scene.collection.objects.unlink(rb_obj)
# Create joints
for joint in joints:
empty = bpy.data.objects.new(f"Joint_{joint.name}", None)
empty.empty_display_type = 'ARROWS'
empty.location = joint.position
empty.rotation_euler = joint.rotation
bpy.context.scene.collection.objects.link(empty)
# Set up constraint
constraint = empty.constraints.new('RIGID_BODY_JOINT')
constraint.target = rigid_bodies[joint.rigid_body_a]
constraint.child = rigid_bodies[joint.rigid_body_b]
constraint.use_limit_lin_x = True
constraint.use_limit_lin_y = True
constraint.use_limit_lin_z = True
constraint.use_limit_ang_x = True
constraint.use_limit_ang_y = True
constraint.use_limit_ang_z = True
# Set limits
constraint.limit_lin_x_lower = joint.linear_limit_min[0]
constraint.limit_lin_x_upper = joint.linear_limit_max[0]
constraint.limit_lin_y_lower = joint.linear_limit_min[1]
constraint.limit_lin_y_upper = joint.linear_limit_max[1]
constraint.limit_lin_z_lower = joint.linear_limit_min[2]
constraint.limit_lin_z_upper = joint.linear_limit_max[2]
constraint.limit_ang_x_lower = joint.angular_limit_min[0]
constraint.limit_ang_x_upper = joint.angular_limit_max[0]
constraint.limit_ang_y_lower = joint.angular_limit_min[1]
constraint.limit_ang_y_upper = joint.angular_limit_max[1]
constraint.limit_ang_z_lower = joint.angular_limit_min[2]
constraint.limit_ang_z_upper = joint.angular_limit_max[2]
def create_armature(model_name: str, bones: list[PMXBone]) -> bpy.types.Object:
# Handle CJK characters in model name
if isinstance(model_name, bytes):
try:
model_name = model_name.decode('gbk') # Try Chinese encoding first
except UnicodeDecodeError:
try:
model_name = model_name.decode('utf-8')
except UnicodeDecodeError:
try:
model_name = model_name.decode('shift-jis')
except UnicodeDecodeError:
model_name = model_name.decode('latin1')
armature = bpy.data.armatures.new(f"{model_name}_Armature")
armature_obj = bpy.data.objects.new(f"{model_name}_Armature", armature)
bpy.context.collection.objects.link(armature_obj)
bpy.context.view_layer.objects.active = armature_obj
bpy.ops.object.mode_set(mode='EDIT')
# First pass: Create bones with proper names and types
edit_bones = []
for i, bone_data in enumerate(bones):
bone_name = bone_data.name if bone_data.name else bone_data.english_name
if not bone_name:
bone_name = f"bone_{i}"
edit_bone = armature.edit_bones.new(bone_name)
edit_bone.head = Vector(bone_data.position)
# Handle different bone types based on flags and names
is_expression = bool(bone_data.flag & 0x0004)
is_rotation_influenced = bool(bone_data.flag & 0x0100)
is_ik = bool(bone_data.flag & 0x0020)
is_twist = "twist" in bone_name.lower()
if is_twist:
# Twist bones need specific handling
parent_pos = bones[bone_data.parent_index].position if bone_data.parent_index >= 0 else None
if parent_pos:
direction = Vector(bone_data.position) - Vector(parent_pos)
if direction.length > 0.001:
edit_bone.tail = edit_bone.head + direction.normalized() * 0.1
else:
edit_bone.tail = edit_bone.head + Vector((0, 0.05, 0))
else:
edit_bone.tail = edit_bone.head + Vector((0, 0.05, 0))
elif is_expression:
edit_bone.tail = edit_bone.head + Vector((0, 0.02, 0))
edit_bone.use_deform = False
elif is_ik:
if bone_data.ik_links:
target_pos = bones[bone_data.ik_links[0][0]].position
direction = Vector(target_pos) - Vector(edit_bone.head)
if direction.length > 0.001:
edit_bone.tail = edit_bone.head + direction.normalized() * 0.1
else:
edit_bone.tail = edit_bone.head + Vector((0, 0.1, 0))
else:
edit_bone.tail = edit_bone.head + Vector((0, 0.1, 0))
elif is_rotation_influenced:
# Handle rotation influenced bones
if bone_data.inherit_parent_index >= 0:
target_pos = bones[bone_data.inherit_parent_index].position
direction = Vector(target_pos) - Vector(edit_bone.head)
if direction.length > 0.001:
edit_bone.tail = edit_bone.head + direction.normalized() * 0.08
else:
edit_bone.tail = edit_bone.head + Vector((0, 0.08, 0))
else:
edit_bone.tail = edit_bone.head + Vector((0, 0.08, 0))
else:
# Standard bones
if bone_data.tail_position[0] is not None:
edit_bone.tail = Vector(bone_data.tail_position)
else:
child_positions = [bones[j].position for j in range(len(bones))
if bones[j].parent_index == i]
if child_positions:
avg_child_pos = Vector((0, 0, 0))
for pos in child_positions:
avg_child_pos += Vector(pos)
avg_child_pos /= len(child_positions)
edit_bone.tail = avg_child_pos
else:
bone_length = 0.1 if bone_data.layer == 0 else 0.05
edit_bone.tail = edit_bone.head + Vector((0, bone_length, 0))
edit_bones.append(edit_bone)
# Second pass: Set up hierarchy and orientations
for i, bone_data in enumerate(bones):
edit_bone = edit_bones[i]
# Parent bones
if bone_data.parent_index >= 0:
parent_bone = edit_bones[bone_data.parent_index]
edit_bone.parent = parent_bone
# Connect bones only if they should be connected
if (Vector(bone_data.position) - Vector(parent_bone.tail)).length < 0.01:
edit_bone.use_connect = True
# Handle bone orientation
if bone_data.fixed_axis != [0.0, 0.0, 0.0]:
edit_bone.align_roll(Vector(bone_data.fixed_axis))
elif bone_data.local_x != [0.0, 0.0, 0.0]:
x_axis = Vector(bone_data.local_x).normalized()
z_axis = Vector(bone_data.local_z).normalized()
y_axis = z_axis.cross(x_axis)
# Create and apply orientation matrix
matrix_3x3 = Matrix((x_axis, y_axis, z_axis)).to_3x3()
matrix_4x4 = matrix_3x3.to_4x4()
edit_bone.matrix = matrix_4x4
bpy.ops.object.mode_set(mode='OBJECT')
return armature_obj
def assign_vertex_weights(obj: bpy.types.Object, vertices: list[PMXVertex], bones: list[PMXBone]):
# Pre-create vertex groups
vertex_groups = {}
for bone in bones:
vertex_groups[bone.name] = obj.vertex_groups.new(name=bone.name)
# Batch assign weights
for vertex_index, vertex in enumerate(vertices):
for bone_idx, weight in zip(vertex.bone_indices, vertex.bone_weights):
if bone_idx != -1 and weight > 0:
vertex_groups[bones[bone_idx].name].add([vertex_index], weight, 'REPLACE')
def assign_materials(obj: bpy.types.Object, materials: list[PMXMaterial], textures: list[str], base_path: str):
current_face_index = 0
for material in materials:
# Create or get material
mat_name = material.name or f"Material_{len(obj.data.materials)}"
if mat_name in bpy.data.materials:
mat = bpy.data.materials[mat_name]
else:
mat = bpy.data.materials.new(name=mat_name)
# Set up material nodes
texture_path = None
if material.texture_index >= 0 and material.texture_index < len(textures):
texture_path = os.path.join(base_path, textures[material.texture_index])
create_material_nodes(mat, texture_path, material.diffuse, material.specular,
material.specular_strength)
# Assign material to mesh
if mat.name not in obj.data.materials:
obj.data.materials.append(mat)
# Assign faces to material
mat_index = obj.data.materials.find(mat.name)
for face in obj.data.polygons[current_face_index:current_face_index + material.surface_count]:
face.material_index = mat_index
current_face_index += material.surface_count
def import_pmx(filepath: str):
wm = bpy.context.window_manager
wm.progress_begin(0, 100)
try:
with open(filepath, 'rb') as file:
# Read header (5%)
wm.progress_update(5)
header_data = read_pmx_header(file)
version, encoding, additional_uvs, vertex_index_size, texture_index_size, \
material_index_size, bone_index_size, morph_index_size, rigid_body_index_size, \
model_name, model_english_name, model_comment, model_english_comment = header_data
# Set up index size formats (10%)
wm.progress_update(10)
vertex_struct, vertex_size = read_index_size(vertex_index_size, 'BHi')
bone_struct, bone_size = read_index_size(bone_index_size, 'bhi')
texture_struct, texture_size = read_index_size(texture_index_size, 'bhi')
# Read vertices (25%)
vertex_count = struct.unpack('<i', file.read(4))[0]
vertices = []
for i in range(vertex_count):
vertices.append(read_vertex(file, bone_struct, bone_size, additional_uvs))
if i % 1000 == 0:
wm.progress_update(10 + (i/vertex_count * 15))
# Read faces (35%)
wm.progress_update(35)
face_count = struct.unpack('<i', file.read(4))[0] // 3
faces = []
for _ in range(face_count):
if vertex_index_size == 1:
faces.append(struct.unpack('<3B', file.read(3)))
elif vertex_index_size == 2:
faces.append(struct.unpack('<3H', file.read(6)))
else:
faces.append(struct.unpack('<3i', file.read(12)))
# Read textures (45%)
wm.progress_update(45)
texture_count = struct.unpack('<i', file.read(4))[0]
textures = []
for _ in range(texture_count):
texture_path = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
textures.append(texture_path)
# Read materials (55%)
wm.progress_update(55)
material_count = struct.unpack('<i', file.read(4))[0]
materials = []
for _ in range(material_count):
materials.append(read_material(file, texture_struct, texture_size))
# Read bones (65%)
wm.progress_update(65)
bone_count = struct.unpack('<i', file.read(4))[0]
bones = []
for _ in range(bone_count):
bones.append(read_bone(file, bone_struct, bone_size))
# Read morphs (75%)
wm.progress_update(75)
morph_count = struct.unpack('<i', file.read(4))[0]
morphs = []
for _ in range(morph_count):
morphs.append(read_morph(file, vertex_struct, vertex_size))
# Read rigid bodies (85%)
wm.progress_update(85)
try:
rigid_body_count_bytes = file.read(4)
if len(rigid_body_count_bytes) == 4:
rigid_body_count = struct.unpack('<i', rigid_body_count_bytes)[0]
rigid_bodies = []
for _ in range(rigid_body_count):
rigid_bodies.append(read_rigid_body(file, bone_struct, bone_size))
else:
rigid_bodies = []
except:
rigid_bodies = []
# Read joints (90%)
wm.progress_update(90)
try:
joint_count_bytes = file.read(4)
if len(joint_count_bytes) == 4:
joint_count = struct.unpack('<i', joint_count_bytes)[0]
joints = []
for _ in range(joint_count):
joints.append(read_joint(file, rigid_body_struct, rigid_body_size))
else:
joints = []
except:
joints = []
# Validate data (92%)
wm.progress_update(92)
validate_pmx_data(header_data, vertices, faces, materials, bones)
# Create mesh and object (94%)
wm.progress_update(94)
mesh = bpy.data.meshes.new(model_name)
mesh.from_pydata([v.position for v in vertices], [], faces)
mesh.update()
obj = bpy.data.objects.new(model_name, mesh)
bpy.context.collection.objects.link(obj)
# Create and set up armature (96%)
wm.progress_update(96)
armature_obj = create_armature(model_name, bones)
obj.parent = armature_obj
# Create shape keys (97%)
wm.progress_update(97)
for morph in morphs:
if morph.morph_type == 1:
if not obj.data.shape_keys:
obj.shape_key_add(name='Basis')
shape_key = obj.shape_key_add(name=morph.name)
for vertex_index, offset in morph.offsets:
shape_key.data[vertex_index].co = (
vertices[vertex_index].position[0] + offset[0],
vertices[vertex_index].position[1] + offset[1],
vertices[vertex_index].position[2] + offset[2]
)
# Set up physics (98%)
wm.progress_update(98)
setup_physics(obj, armature_obj, rigid_bodies, joints)
# Final setup (99%)
wm.progress_update(99)
base_path = os.path.dirname(filepath)
assign_materials(obj, materials, textures, base_path)
assign_vertex_weights(obj, vertices, bones)
# Add armature modifier
mod = obj.modifiers.new(name="Armature", type='ARMATURE')
mod.object = armature_obj
# Set proper scale and orientation
armature_obj.scale = (0.08, 0.08, 0.08)
armature_obj.rotation_euler = (1.5708, 0, 0)
# Select objects and set active
armature_obj.select_set(True)
obj.select_set(True)
bpy.context.view_layer.objects.active = armature_obj
# Disable automatic mirroring
armature_obj.data.use_mirror_x = False
# Add constraints
create_bone_constraints(armature_obj, bones)
# Apply transforms
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
# Ensure object mode
bpy.context.view_layer.objects.active = armature_obj
bpy.ops.object.mode_set(mode='OBJECT')
wm.progress_end()
return {'FINISHED'}
except Exception as e:
wm.progress_end()
error_msg = f"PMX Import Error: {str(e)}\n{traceback.format_exc()}"
print(error_msg) # Console output for debugging
return {'CANCELLED'}
+69 -4
View File
@@ -1,11 +1,13 @@
import bpy
import logging
import os
import pathlib
import typing
from bpy.types import Operator, Context
from bpy_extras.io_utils import ImportHelper
from typing import Optional, Callable, Dict, List, Union, Set
from ..common import clear_default_objects
from .import_pmx import import_pmx
from .import_pmd import import_pmd
from ..translations import t
# Configure logging
logging.basicConfig(level=logging.INFO)
@@ -118,8 +120,6 @@ import_types: Dict[str, ImportMethod] = {
method=lambda directory, filepath: bpy.ops.tuxedo.import_mmd_animation(directory=directory, filepath=filepath)
),
"vrm": lambda directory, files, filepath: bpy.ops.import_scene.vrm(filepath=filepath),
"pmx": lambda directory, files, filepath: import_pmx(filepath),
"pmd": lambda directory, files, filepath: import_pmd(filepath),
"animx": (lambda directory, files, filepath : bpy.ops.avatar_toolkit.animx_importer(directory=directory,files=files,filepath=filepath)),
}
@@ -128,3 +128,68 @@ def concat_imports_filter(imports: Dict[str, ImportMethod]) -> str:
return "".join(f"*.{importer};" for importer in imports.keys())
imports: str = concat_imports_filter(import_types)
class AvatarToolKit_OT_Import(Operator, ImportHelper):
"""Import files into Blender with Avatar Toolkit settings"""
bl_idname: str = "avatar_toolkit.import"
bl_label: str = t("QuickAccess.import")
files: bpy.props.CollectionProperty(
type=bpy.types.OperatorFileListElement,
options={'HIDDEN', 'SKIP_SAVE'}
)
filter_glob: bpy.props.StringProperty(
default=imports,
options={'HIDDEN', 'SKIP_SAVE'}
)
directory: bpy.props.StringProperty(
maxlen=1024,
subtype='FILE_PATH',
options={'HIDDEN', 'SKIP_SAVE'}
)
def execute(self, context: Context) -> Set[str]:
clear_default_objects()
file_grouping_dict: Dict[str, List[Dict[str, str]]] = {}
is_multi = len(self.files) > 0
if is_multi:
for file in self.files:
fullpath = os.path.join(self.directory, os.path.basename(file.name))
ext = pathlib.Path(fullpath).suffix.replace(".", "")
if ext not in file_grouping_dict:
file_grouping_dict[ext] = []
file_grouping_dict[ext].append({"name": os.path.basename(file.name)})
else:
fullpath = os.path.join(os.path.dirname(self.filepath), os.path.basename(self.filepath))
ext = pathlib.Path(fullpath).suffix.replace(".", "")
if ext not in file_grouping_dict:
file_grouping_dict[ext] = []
file_grouping_dict[ext].append({"name": fullpath})
for file_group_name, files in file_grouping_dict.items():
try:
if file_group_name == "vrm" and not hasattr(bpy.ops.import_scene, "vrm"):
bpy.ops.wm.vrm_importer_popup('INVOKE_DEFAULT')
return {'CANCELLED'}
directory = self.directory if self.directory else ""
import_types[file_group_name](directory, files, self.filepath)
except AttributeError as e:
if file_group_name == "vrm":
bpy.ops.wm.vrm_importer_popup('INVOKE_DEFAULT')
else:
self.report({'ERROR'}, t('Importing.need_importer').format(extension=file_group_name))
logger.error(f"Importer error: {e}")
return {'CANCELLED'}
self.report({'INFO'}, t('Quick_Access.import_success'))
return {'FINISHED'}
+21 -2
View File
@@ -1,7 +1,10 @@
import logging
from typing import Optional
import traceback
from typing import Optional, Any
from bpy.types import Context
logger = logging.getLogger('avatar_toolkit')
_original_error = logger.error
def configure_logging(enabled: bool = False) -> None:
"""Configure logging for Avatar Toolkit"""
@@ -17,10 +20,26 @@ def configure_logging(enabled: bool = False) -> None:
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
def error_with_traceback(msg, *args, **kwargs):
if kwargs.get('exc_info', False) or isinstance(msg, Exception):
full_msg = f"{msg}\n{traceback.format_exc()}"
_original_error(full_msg, *args, **{**kwargs, 'exc_info': False})
else:
_original_error(msg, *args, **kwargs)
logger.error = error_with_traceback
def update_logging_state(self, context) -> None:
def update_logging_state(self: Any, context: Context) -> None:
"""Update logging state based on user preference"""
from .addon_preferences import save_preference
enabled = self.enable_logging
save_preference("enable_logging", enabled)
configure_logging(enabled)
def highlight_problem_bones(self: Any, context: Context) -> None:
"""Log when problem bones are highlighted"""
from .addon_preferences import save_preference
enabled = self.highlight_problem_bones
save_preference("highlight_problem_bones", enabled)
logger.debug(f"Problem bone highlighting {'enabled' if enabled else 'disabled'}")
+152
View File
@@ -0,0 +1,152 @@
# thank you https://stackoverflow.com/a/71432759
from __future__ import annotations
from typing import Optional
from bpy.types import Image, Material
# Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016 Jake Gordon and contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
class Rectangle_Obj:
x: int = 0
y: int = 0
w: int = 0
h: int = 0
down: Rectangle_Obj = None
used: bool = False
right: Rectangle_Obj = None
def __init__(self, x:int, y:int, w:int, h:int, down=None, used =False, right=None):
self.x = x
self.y = y
self.w = w
self.h = h
self.down = down
self.used = used
self.right = right
def split(self, w, h) -> Rectangle_Obj:
self.used = True
self.down = Rectangle_Obj(x=self.x, y=self.y + h, w=self.w, h=self.h - h)
self.right = Rectangle_Obj(x=self.x + w, y=self.y, w=self.w - w, h=h)
return self
def find(self, w, h) -> Optional[Rectangle_Obj]:
if self.used:
return self.right.find(w, h) or self.down.find(w, h)
elif (w <= self.w) and (h <= self.h):
return self
return None
class MaterialImageList:
albedo: Image
normal: Image
emission: Image
ambient_occlusion: Image
height: Image
roughness: Image
fit: Rectangle_Obj
material: Material
def __init__(self):
pass
x: int = 0
y: int = 0
w: int = 0
h: int = 0
class BinPacker(object):
root: Rectangle_Obj
bin: list[MaterialImageList] = []
def __init__(self, structure: list[MaterialImageList]):
self.root = None
self.bin = structure
def fit(self):
structure = self.bin
structure_len = len(self.bin)
w: int = 0
h: int = 0
if structure_len > 0:
w = structure[0].w
h = structure[0].h
self.root = Rectangle_Obj(x=0, y=0, w=w, h=h)
for img in structure:
w = img.w
h = img.h
node = self.root.find(w, h)
if node:
img.fit = node.split(w, h)
else:
img.fit = self.grow_node(w, h)
return structure
def grow_node(self, w, h) -> Optional[Rectangle_Obj]:
can_grow_right = (h <= self.root.h)
can_grow_down = (w <= self.root.w)
should_grow_right = can_grow_right and (self.root.h >= (self.root.w + w))
should_grow_down = can_grow_down and (self.root.w >= (self.root.h + h))
if should_grow_right:
return self.grow_right(w, h)
elif should_grow_down:
return self.grow_down(w, h)
elif can_grow_right:
return self.grow_right(w, h)
elif can_grow_down:
return self.grow_down(w, h)
return None
def grow_right(self, w, h) -> Optional[Rectangle_Obj]:
self.root = Rectangle_Obj(
used=True,
x=0,
y=0,
w=self.root.w + w,
h=self.root.h,
down=self.root,
right=Rectangle_Obj(x=self.root.w, y=0, w=w, h=self.root.h))
node = self.root.find(w, h)
if node:
return node.split(w, h)
return None
def grow_down(self, w, h) -> Optional[Rectangle_Obj]:
self.root = Rectangle_Obj(
used=True,
x=0,
y=0,
w=self.root.w,
h=self.root.h + h,
down=Rectangle_Obj(x=0, y=self.root.h, w=self.root.w, h=h),
right=self.root
)
node = self.root.find(w, h)
if node:
return node.split(w, h)
return None
+280 -58
View File
@@ -1,5 +1,5 @@
import bpy
from typing import List, Tuple, Optional
from typing import List, Tuple, Optional, Any, Dict, Union, Callable
from bpy.types import PropertyGroup, Material, Scene, Object, Context
from bpy.props import (
StringProperty,
@@ -14,27 +14,171 @@ from .logging_setup import logger
from .translations import t, get_languages_list, update_language
from .addon_preferences import get_preference, save_preference
from .updater import get_version_list
from .common import get_armature_list, get_active_armature, get_all_meshes
from .common import get_armature_list, get_active_armature, get_all_meshes, SceneMatClass
from ..functions.visemes import VisemePreview
from ..functions.eye_tracking import set_rotation
def update_validation_mode(self, context):
class ValidationMessageItem(PropertyGroup):
"""Property group for validation message items"""
name: StringProperty(name="Message")
class ZeroWeightBoneItem(PropertyGroup):
"""Property group for zero weight bone list items"""
name: StringProperty(name="Bone Name")
selected: BoolProperty(name="Selected", default=True)
has_children: BoolProperty(name="Has Children", default=False)
is_deform: BoolProperty(name="Is Deform Bone", default=False)
def update_validation_mode(self: PropertyGroup, context: Context) -> None:
"""Updates validation mode and saves preference"""
logger.info(f"Updating validation mode to: {self.validation_mode}")
save_preference("validation_mode", self.validation_mode)
def update_logging_state(self, context):
def update_logging_state(self: PropertyGroup, context: Context) -> None:
"""Updates logging state and configures logging"""
logger.info(f"Updating logging state to: {self.enable_logging}")
save_preference("enable_logging", self.enable_logging)
from .logging_setup import configure_logging
configure_logging(self.enable_logging)
def update_shape_intensity(self, context):
def update_shape_intensity(self: PropertyGroup, context: Context) -> None:
"""Updates shape key intensity and refreshes preview"""
if self.viseme_preview_mode:
from ..functions.visemes import VisemePreview
VisemePreview.update_preview(context)
def highlight_problem_bones(self: PropertyGroup, context: Context) -> None:
"""Updates problem bone highlighting state and saves preference"""
logger.info(f"Updating problem bone highlighting to: {self.highlight_problem_bones}")
save_preference("highlight_problem_bones", self.highlight_problem_bones)
def get_mesh_objects(self, context):
meshes = [(obj.name, obj.name, "") for obj in bpy.data.objects if obj.type == 'MESH']
if not meshes:
return [('NONE', t("Visemes.no_meshes"), '')]
return meshes
class AvatarToolkitSceneProperties(PropertyGroup):
"""Property group containing Avatar Toolkit scene-level settings and properties"""
show_found_bones: BoolProperty(
name="Show Found Bones",
default=False
)
show_non_standard: BoolProperty(
name="Show Non-Standard Bones",
default=False
)
show_hierarchy: BoolProperty(
name="Show Hierarchy Issues",
default=False
)
material_search_filter: StringProperty(
name=t("TextureAtlas.search_materials"),
description=t("TextureAtlas.search_materials_desc"),
default=""
)
def get_texture_node_list(self: Material, context: Context) -> list[tuple]:
if self.use_nodes:
Object.Enum = [((i.image.name if i.image else i.name+"_image"),
(i.image.name if i.image else "node with no image..."),
(i.image.name if i.image else i.name), index+1)
for index, i in enumerate(self.node_tree.nodes)
if i.bl_idname == "ShaderNodeTexImage"]
if not len(Object.Enum):
Object.Enum = [(t("TextureAtlas.error.label"),
t("TextureAtlas.no_images_error.desc"),
t("TextureAtlas.error.label"), 0)]
else:
Object.Enum = [(t("TextureAtlas.error.label"),
t("TextureAtlas.no_nodes_error.desc"),
t("TextureAtlas.error.label"), 0)]
Object.Enum.append((t("TextureAtlas.none.label"),
t("TextureAtlas.none.label"),
t("TextureAtlas.none.label"), 0))
return Object.Enum
Material.texture_atlas_albedo = EnumProperty(
name=t("TextureAtlas.albedo"),
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.albedo").lower()),
default=0,
items=get_texture_node_list
)
Material.texture_atlas_normal = EnumProperty(
name=t("TextureAtlas.normal"),
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.normal").lower()),
default=0,
items=get_texture_node_list
)
Material.texture_atlas_emission = EnumProperty(
name=t("TextureAtlas.emission"),
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.emission").lower()),
default=0,
items=get_texture_node_list
)
Material.texture_atlas_ambient_occlusion = EnumProperty(
name=t("TextureAtlas.ambient_occlusion"),
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.ambient_occlusion").lower()),
default=0,
items=get_texture_node_list
)
Material.texture_atlas_height = EnumProperty(
name=t("TextureAtlas.height"),
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.height").lower()),
default=0,
items=get_texture_node_list
)
Material.texture_atlas_roughness = EnumProperty(
name=t("TextureAtlas.roughness"),
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.roughness").lower()),
default=0,
items=get_texture_node_list
)
list_only_mode: BoolProperty(
name=t("Tools.list_only_mode"),
description=t("Tools.list_only_mode_desc"),
default=False
)
Material.include_in_atlas = BoolProperty(
name=t("TextureAtlas.include_in_atlas"),
description=t("TextureAtlas.include_in_atlas_desc"),
default=False
)
Material.material_expanded = BoolProperty(
name=t("TextureAtlas.material_expanded"),
description=t("TextureAtlas.material_expanded_desc"),
default=False
)
texture_atlas_Has_Mat_List_Shown: BoolProperty(
name=t("TextureAtlas.list_shown"),
description=t("TextureAtlas.list_shown_desc"),
default=False
)
texture_atlas_material_index: IntProperty(
default=-1,
get=lambda self: -1,
set=lambda self, context: None
)
materials: CollectionProperty(
type=SceneMatClass
)
avatar_toolkit_updater_version_list: EnumProperty(
items=get_version_list,
@@ -133,13 +277,7 @@ class AvatarToolkitSceneProperties(PropertyGroup):
description=t("Visemes.preview_mode_desc"),
default=False
)
viseme_preview_selection: StringProperty(
name=t("Visemes.preview_selection"),
description=t("Visemes.preview_selection_desc"),
default="vrc.v_aa"
)
mouth_a: StringProperty(
name=t("Visemes.mouth_a"),
description=t("Visemes.mouth_a_desc")
@@ -155,6 +293,12 @@ class AvatarToolkitSceneProperties(PropertyGroup):
description=t("Visemes.mouth_ch_desc")
)
viseme_mesh: EnumProperty(
name=t("Visemes.mesh_select"),
description=t("Visemes.mesh_select_desc"),
items=get_mesh_objects
)
shape_intensity: FloatProperty(
name=t("Visemes.shape_intensity"),
description=t("Visemes.shape_intensity_desc"),
@@ -166,38 +310,37 @@ class AvatarToolkitSceneProperties(PropertyGroup):
)
viseme_preview_selection: EnumProperty(
name=t("Visemes.preview_selection"),
description=t("Visemes.preview_selection_desc"),
items=[
('vrc.v_aa', 'AA', 'A as in "bat"'),
('vrc.v_ch', 'CH', 'Ch as in "choose"'),
('vrc.v_dd', 'DD', 'D as in "dog"'),
('vrc.v_ih', 'IH', 'I as in "bit"'),
('vrc.v_ff', 'FF', 'F as in "fox"'),
('vrc.v_e', 'E', 'E as in "bet"'),
('vrc.v_kk', 'KK', 'K as in "cat"'),
('vrc.v_nn', 'NN', 'N as in "net"'),
('vrc.v_oh', 'OH', 'O as in "hot"'),
('vrc.v_ou', 'OU', 'O as in "go"'),
('vrc.v_pp', 'PP', 'P as in "pat"'),
('vrc.v_rr', 'RR', 'R as in "red"'),
('vrc.v_sil', 'SIL', 'Silence'),
('vrc.v_ss', 'SS', 'S as in "sit"'),
('vrc.v_th', 'TH', 'Th as in "think"')
],
update=lambda s, c: VisemePreview.update_preview(c)
)
name=t("Visemes.preview_selection"),
description=t("Visemes.preview_selection_desc"),
items=[
('vrc.v_aa', 'AA', 'A as in "bat"'),
('vrc.v_ch', 'CH', 'Ch as in "choose"'),
('vrc.v_dd', 'DD', 'D as in "dog"'),
('vrc.v_ih', 'IH', 'I as in "bit"'),
('vrc.v_ff', 'FF', 'F as in "fox"'),
('vrc.v_e', 'E', 'E as in "bet"'),
('vrc.v_kk', 'KK', 'K as in "cat"'),
('vrc.v_nn', 'NN', 'N as in "net"'),
('vrc.v_oh', 'OH', 'O as in "hot"'),
('vrc.v_ou', 'OU', 'O as in "go"'),
('vrc.v_pp', 'PP', 'P as in "pat"'),
('vrc.v_rr', 'RR', 'R as in "red"'),
('vrc.v_sil', 'SIL', 'Silence'),
('vrc.v_ss', 'SS', 'S as in "sit"'),
('vrc.v_th', 'TH', 'Th as in "think"')
],
update=lambda s, c: VisemePreview.update_preview(c)
)
eye_tracking_type: EnumProperty(
name=t("EyeTracking.type"),
description=t("EyeTracking.type_desc"),
items=[
('AV3', t("EyeTracking.type.av3"), t("EyeTracking.type.av3_desc")),
('SDK2', t("EyeTracking.type.sdk2"), t("EyeTracking.type.sdk2_desc"))
],
default='AV3'
)
name=t("EyeTracking.type"),
description=t("EyeTracking.type_desc"),
items=[
('AV3', t("EyeTracking.type.av3"), t("EyeTracking.type.av3_desc")),
('SDK2', t("EyeTracking.type.sdk2"), t("EyeTracking.type.sdk2_desc"))
],
default='AV3'
)
eye_mode: EnumProperty(
name=t("EyeTracking.mode"),
@@ -336,12 +479,6 @@ class AvatarToolkitSceneProperties(PropertyGroup):
default=""
)
merge_all_bones: BoolProperty(
name=t('MergeArmature.merge_all'),
description=t('MergeArmature.merge_all_desc'),
default=True
)
apply_transforms: BoolProperty(
name=t('MergeArmature.apply_transforms'),
description=t('MergeArmature.apply_transforms_desc'),
@@ -360,30 +497,115 @@ class AvatarToolkitSceneProperties(PropertyGroup):
default=True
)
preserve_parent_bones: BoolProperty(
name=t("Tools.preserve_parent_bones"),
description=t("Tools.preserve_parent_bones_desc"),
default=True
)
target_bone_type: EnumProperty(
name=t("Tools.target_bone_type"),
description=t("Tools.target_bone_type_desc"),
items=[
('ALL', t("Tools.target_all_bones"), ""),
('DEFORM', t("Tools.target_deform_bones"), ""),
('NON_DEFORM', t("Tools.target_non_deform_bones"), "")
],
default='ALL'
)
zero_weight_bones: CollectionProperty(
type=ZeroWeightBoneItem,
name="Zero Weight Bones",
description="List of bones with zero weights"
)
zero_weight_bones_index: IntProperty(
name="Zero Weight Bone Index",
default=0
)
list_only_mode: BoolProperty(
name=t("Tools.list_only_mode"),
description=t("Tools.list_only_mode_desc"),
default=False
)
cleanup_shape_keys: BoolProperty(
name=t('MergeArmature.cleanup_shape_keys'),
description=t('MergeArmature.cleanup_shape_keys_desc'),
default=True
)
attach_mesh: StringProperty(
name=t("Tools.attach_mesh_select"),
description=t("Tools.attach_mesh_select_desc")
merge_twist_bones: BoolProperty(
name=t("Tools.merge_twist_bones"),
description=t("Tools.merge_twist_bones_desc"),
default=True
)
attach_bone: StringProperty(
name=t("Tools.attach_bone_select"),
description=t("Tools.attach_bone_select_desc")
highlight_problem_bones: BoolProperty(
name=t("Settings.highlight_problem_bones"),
description=t("Settings.highlight_problem_bones_desc"),
default=get_preference("highlight_problem_bones", True),
update=highlight_problem_bones
)
show_scale_issues: BoolProperty(
name="Show Scale Issues",
default=False
)
tpose_validation_result: BoolProperty(
name="T-Pose Validation Result",
default=True
)
tpose_validation_messages: CollectionProperty(
type=bpy.types.PropertyGroup,
name="T-Pose Validation Messages"
)
show_tpose_validation: BoolProperty(
name="Show T-Pose Validation Results",
default=False
)
standardize_fix_names: BoolProperty(
name=t("Tools.standardize_fix_names"),
description=t("Tools.standardize_fix_names_desc"),
default=True
)
standardize_fix_hierarchy: BoolProperty(
name=t("Tools.standardize_fix_hierarchy"),
description=t("Tools.standardize_fix_hierarchy_desc"),
default=True
)
standardize_fix_scale: BoolProperty(
name=t("Tools.standardize_fix_scale"),
description=t("Tools.standardize_fix_scale_desc"),
default=True
)
def register() -> None:
"""Register the Avatar Toolkit property group"""
logger.info("Registering Avatar Toolkit properties")
# Only register the property, not the classes (auto_load will handle that)
bpy.types.Scene.avatar_toolkit = PointerProperty(type=AvatarToolkitSceneProperties)
logger.debug("Properties registered successfully")
def unregister() -> None:
"""Unregister the Avatar Toolkit property group"""
logger.info("Unregistering Avatar Toolkit properties")
del bpy.types.Scene.avatar_toolkit
logger.debug("Properties unregistered successfully")
# Remove the property
if hasattr(bpy.types.Scene, "avatar_toolkit"):
try:
del bpy.types.Scene.avatar_toolkit
logger.debug("Removed avatar_toolkit property")
except Exception as e:
logger.warning(f"Failed to remove avatar_toolkit property: {e}")
# Not fatal - continue
+12 -19
View File
@@ -3,14 +3,16 @@ import bpy
import bpy_extras
from numpy import double
from typing import Set, Dict
import re
from .common import get_active_armature, simplify_bonename, validate_armature, ProgressTracker
from .common import get_active_armature, simplify_bonename, validate_armature, ProgressTracker, identify_bones
from bpy.types import Context, Operator
from ..core.translations import t
from ..core.dictionaries import bone_names, resonite_translations
from ..core.logging_setup import logger
from ..core.armature_validation import validate_armature
import re
from .resonite_loader import resonite_animx, resonite_types
import os
@@ -50,7 +52,7 @@ class AvatarToolkit_OT_ConvertResonite(Operator):
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
is_valid, _, _ = validate_armature(armature)
return is_valid
def execute(self, context: Context) -> Set[str]:
@@ -64,30 +66,21 @@ class AvatarToolkit_OT_ConvertResonite(Operator):
untranslated_bones: Set[str] = set()
simplified_names: Dict[str, str] = {}
# Create reverse lookup dictionary
reverse_bone_lookup = {}
for preferred_name, name_list in bone_names.items():
for name in name_list:
reverse_bone_lookup[name] = preferred_name
try:
context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.mode_set(mode='OBJECT')
arm_data: bpy.types.Armature = armature.data
# Cache simplified bone names
for bone in armature.data.bones:
simplified_names[bone.name] = simplify_bonename(bone.name)
for bone in arm_data.bones:
bone.name = re.compile(re.escape("<noik>"), re.IGNORECASE).sub("", bone.name)
total_bones = len(armature.data.bones)
total_bones = len(arm_data.bones)
with ProgressTracker(context, total_bones, t("Tools.convert_resonite.operation")) as progress:
for bone in armature.data.bones:
# Remove any existing "<noik>" tags
bone.name = re.compile(re.escape("<noik>"), re.IGNORECASE).sub("", bone.name)
simplified_name = simplified_names[bone.name]
for key_simple,bone_name in identify_bones(arm_data,context).items():
if simplified_name in reverse_bone_lookup and reverse_bone_lookup[simplified_name] in resonite_translations:
new_name = resonite_translations[reverse_bone_lookup[simplified_name]]
if key_simple in resonite_translations:
new_name = resonite_translations[key_simple]
logger.debug(f"Translating bone: {bone.name} -> {new_name}")
bone.name = new_name
else:
+78 -7
View File
@@ -17,11 +17,17 @@ from typing import Dict, List, Tuple, Optional, Set, Any
GITHUB_REPO = "teamneoneko/Avatar-Toolkit"
# Define which version series this installation can update to
# For example: ["0.1"] means only look for 0.1.x updates
# ["0.2", "0.3"] would look for both 0.2.x and 0.3.x updates
ALLOWED_VERSION_SERIES = ["0.2"]
is_checking_for_update: bool = False
update_needed: bool = False
latest_version: Optional[str] = None
latest_version_str: str = ''
version_list: Optional[Dict[str, List[str]]] = None
last_manual_check_time: float = 0
main_dir: str = os.path.dirname(os.path.dirname(__file__))
downloads_dir: str = os.path.join(main_dir, "downloads")
@@ -34,7 +40,9 @@ class AvatarToolkit_OT_CheckForUpdate(bpy.types.Operator):
bl_options = {'INTERNAL'}
def execute(self, context: bpy.types.Context) -> Set[str]:
global last_manual_check_time
check_for_update_background()
last_manual_check_time = time.time() # Reset the timer on manual check
return {'FINISHED'}
@@ -76,11 +84,20 @@ class AvatarToolkit_PT_UpdaterPanel(bpy.types.Panel):
bl_region_type = 'UI'
bl_category = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 8
bl_order = 9
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context: bpy.types.Context) -> None:
global last_manual_check_time
layout = self.layout
# Auto-check for updates when panel is drawn, but not too frequently
current_time = time.time()
if current_time - last_manual_check_time > 300: # 5 minutes between auto-checks
if not is_checking_for_update and not update_needed:
check_for_update_background()
last_manual_check_time = current_time
draw_updater_panel(context, layout)
@@ -158,11 +175,23 @@ def get_github_releases() -> bool:
return True
def check_for_update_available() -> bool:
global latest_version, latest_version_str
global latest_version, latest_version_str, version_list
if not version_list:
return False
latest_version = max(version_list.keys(), key=lambda v: [int(x) for x in v.split('.')])
# Filter versions by allowed version series
compatible_versions = {}
for v, info in version_list.items():
for prefix in ALLOWED_VERSION_SERIES:
if v.startswith(prefix):
compatible_versions[v] = info
break
if not compatible_versions:
print(f"No compatible versions found in series: {', '.join(ALLOWED_VERSION_SERIES)}")
return False
latest_version = max(compatible_versions.keys(), key=lambda v: [int(x) for x in v.split('.')])
latest_version_str = latest_version
current_version = get_current_version()
@@ -195,11 +224,37 @@ def update_now(latest: bool = False) -> None:
if not version_list:
print("No version list available. Please check for updates first.")
return
if latest:
update_link = version_list[latest_version_str][0]
# Filter compatible versions
compatible_versions = {}
for v, info in version_list.items():
for prefix in ALLOWED_VERSION_SERIES:
if v.startswith(prefix):
compatible_versions[v] = info
break
if not compatible_versions:
print(f"No compatible versions found in series: {', '.join(ALLOWED_VERSION_SERIES)}")
return
latest_compatible = max(compatible_versions.keys(), key=lambda v: [int(x) for x in v.split('.')])
update_link = version_list[latest_compatible][0]
else:
update_link = version_list[bpy.context.scene.avatar_toolkit_updater_version_list][0]
selected_version = bpy.context.scene.avatar_toolkit_updater_version_list
# Check if selected version is compatible
is_compatible = False
for prefix in ALLOWED_VERSION_SERIES:
if selected_version.startswith(prefix):
is_compatible = True
break
if not is_compatible:
print(f"Selected version {selected_version} is not in allowed series: {', '.join(ALLOWED_VERSION_SERIES)}")
return
update_link = version_list[selected_version][0]
download_file(update_link)
ui_refresh()
@@ -274,7 +329,17 @@ def finish_update(error: str = '') -> None:
ui_refresh()
def get_version_list(self, context: bpy.types.Context) -> List[Tuple[str, str, str]]:
return [(v, v, '') for v in version_list.keys()] if version_list else []
if not version_list:
return []
compatible_versions = []
for v in version_list.keys():
for prefix in ALLOWED_VERSION_SERIES:
if v.startswith(prefix):
compatible_versions.append(v)
break
return [(v, v, '') for v in compatible_versions]
def draw_updater_panel(context: bpy.types.Context, layout: bpy.types.UILayout) -> None:
box = layout.box()
@@ -287,6 +352,12 @@ def draw_updater_panel(context: bpy.types.Context, layout: bpy.types.UILayout) -
col.separator()
# Show compatibility info
col.label(text=f"Update series: {', '.join(s + '.x' for s in ALLOWED_VERSION_SERIES)}", icon='INFO')
col.label(text=f"Blender version: {bpy.app.version_string}", icon='BLENDER')
col.separator()
# Update check/status section
if is_checking_for_update:
col.operator(AvatarToolkit_OT_CheckForUpdate.bl_idname,
+311
View File
@@ -0,0 +1,311 @@
from pathlib import Path
import numpy
import bpy
import os
from typing import List, Optional
from bpy.types import Material, Operator, Context, Object, Image, Mesh, MeshUVLoopLayer, Float2AttributeValue, ShaderNodeTexImage, ShaderNodeBsdfPrincipled, ShaderNodeNormalMap
from ..core.common import SceneMatClass, MaterialListBool, ProgressTracker
from ..core.packer.rectangle_packer import MaterialImageList, BinPacker
from ..core.translations import t
from ..core.logging_setup import logger
class MaterialImageList:
def __init__(self):
self.albedo: Image = None
self.normal: Image = None
self.emission: Image = None
self.ambient_occlusion: Image = None
self.height: Image = None
self.roughness: Image = None
self.material: Material = None
self.parent_mesh: Object = None
self.w: int = 0
self.h: int = 0
self.fit = None
def scale_images_to_largest(images: List[Image]) -> tuple[int, int]:
x: int = 0
y: int = 0
valid_images = [img for img in images if img and img.has_data]
if not valid_images:
return 0, 0
for image in valid_images:
x = max(x, image.size[0])
y = max(y, image.size[1])
for image in valid_images:
image.scale(width=int(x), height=int(y))
return x, y
def MaterialImageList_to_Image_list(classitem: MaterialImageList) -> List[Image]:
return [
classitem.albedo,
classitem.normal,
classitem.emission,
classitem.ambient_occlusion,
classitem.height,
classitem.roughness
]
def get_material_images_from_scene(context: Context) -> list[MaterialImageList]:
material_image_list: list[MaterialImageList] = []
with ProgressTracker(context, len(context.scene.objects), "Processing Materials") as progress:
for obj in context.scene.objects:
if obj.type == 'MESH':
for mat_slot in obj.material_slots:
# Only process materials that are selected for atlas
if mat_slot.material and mat_slot.material.include_in_atlas is True:
new_mat_image_item = MaterialImageList()
try:
new_mat_image_item.albedo = bpy.data.images[mat_slot.material.texture_atlas_albedo]
except Exception:
name = mat_slot.material.name + "_albedo_replacement"
if name in bpy.data.images:
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
new_mat_image_item.albedo = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
new_mat_image_item.albedo.pixels[:] = numpy.tile(numpy.array([0.0,0.0,0.0,1.0]), 32*32)
try:
new_mat_image_item.normal = bpy.data.images[mat_slot.material.texture_atlas_normal]
except Exception:
name = mat_slot.material.name + "_normal_replacement"
if name in bpy.data.images:
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
new_mat_image_item.normal = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
new_mat_image_item.normal.pixels[:] = numpy.tile(numpy.array([0.5,0.5,1.0,1.0]), 32*32)
try:
new_mat_image_item.emission = bpy.data.images[mat_slot.material.texture_atlas_emission]
except Exception:
name = mat_slot.material.name + "_emission_replacement"
if name in bpy.data.images:
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
new_mat_image_item.emission = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
new_mat_image_item.emission.pixels[:] = numpy.tile(numpy.array([0.0,0.0,0.0,1.0]), 32*32)
try:
new_mat_image_item.ambient_occlusion = bpy.data.images[mat_slot.material.texture_atlas_ambient_occlusion]
except Exception:
name = mat_slot.material.name + "_ambient_occlusion_replacement"
if name in bpy.data.images:
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
new_mat_image_item.ambient_occlusion = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
new_mat_image_item.ambient_occlusion.pixels[:] = numpy.tile(numpy.array([1.0,1.0,1.0,1.0]), 32*32)
try:
new_mat_image_item.height = bpy.data.images[mat_slot.material.texture_atlas_height]
except Exception:
name = mat_slot.material.name + "_height_replacement"
if name in bpy.data.images:
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
new_mat_image_item.height = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
new_mat_image_item.height.pixels[:] = numpy.tile(numpy.array([0.5,0.5,0.5,1.0]), 32*32)
try:
new_mat_image_item.roughness = bpy.data.images[mat_slot.material.texture_atlas_roughness]
except Exception:
name = mat_slot.material.name + "_roughness_replacement"
if name in bpy.data.images:
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
new_mat_image_item.roughness = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
new_mat_image_item.roughness.pixels[:] = numpy.tile(numpy.array([1.0,1.0,1.0,0.0]), 32*32)
new_mat_image_item.material = mat_slot.material
new_mat_image_item.parent_mesh = obj
material_image_list.append(new_mat_image_item)
progress.step(f"Processed {obj.name}")
return material_image_list
def prep_images_in_scene(context: Context) -> List[MaterialImageList]:
preped_images = get_material_images_from_scene(context)
with ProgressTracker(context, len(preped_images), "Preparing Images") as progress:
for MaterialImageClass in preped_images:
ImageList = MaterialImageList_to_Image_list(MaterialImageClass)
MaterialImageClass.w, MaterialImageClass.h = scale_images_to_largest(ImageList)
progress.step(f"Scaled images for {MaterialImageClass.material.name}")
return preped_images
class AvatarToolKit_OT_AtlasMaterials(Operator):
bl_idname = "avatar_toolkit.atlas_materials"
bl_label = t("TextureAtlas.atlas_materials")
bl_description = t("TextureAtlas.atlas_materials_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
# Only allow operation if the file is saved and materials are selected.
if not bpy.data.filepath:
cls.poll_message_set(t("TextureAtlas.save_file_first"))
return False
return context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown
def execute(self, context: Context) -> set:
try:
selected_materials = [m for m in prep_images_in_scene(context)
if m.material and m.material.include_in_atlas]
if not selected_materials:
self.report({'WARNING'}, t("TextureAtlas.no_materials_selected"))
return {'CANCELLED'}
logger.info("Starting material atlas creation")
packer = BinPacker(selected_materials)
mat_images = packer.fit()
size = [
max([matimg.fit.w + matimg.albedo.size[0] for matimg in mat_images]),
max([matimg.fit.h + matimg.albedo.size[1] for matimg in mat_images])
]
atlased_mat = MaterialImageList()
# UV Remapping
with ProgressTracker(context, len(bpy.data.objects), "Remapping UVs") as progress:
for mat in mat_images:
x, y = int(mat.fit.x), int(mat.fit.y)
w, h = int(mat.albedo.size[0]), int(mat.albedo.size[1])
for obj in bpy.data.objects:
if obj.type == 'MESH':
mesh = obj.data
for layer in mesh.polygons:
if (obj.material_slots[layer.material_index].material and
obj.material_slots[layer.material_index].material == mat.material):
for loop_idx in layer.loop_indices:
for layer_loops in mesh.uv_layers:
uv_item = layer_loops.uv[loop_idx]
uv_item.vector.x = (uv_item.vector.x*(w/size[0]))+(x/size[0])
uv_item.vector.y = (uv_item.vector.y*(h/size[1]))+(y/size[1])
progress.step(f"Processed UVs for {obj.name}")
# Create atlas textures
texture_types = ["albedo", "normal", "emission", "ambient_occlusion", "height", "roughness"]
with ProgressTracker(context, len(texture_types), "Creating Atlas Textures") as progress:
for type_name in texture_types:
new_image_name = f"Atlas_{type_name}_{context.scene.name}_{Path(bpy.data.filepath).stem}"
logger.debug(f"Processing {type_name} atlas image")
if new_image_name in bpy.data.images:
bpy.data.images.remove(bpy.data.images[new_image_name])
canvas = bpy.data.images.new(name=new_image_name, width=int(size[0]),
height=int(size[1]), alpha=True)
c_w = canvas.size[0]
canvas_pixels = list(canvas.pixels[:])
for mat in mat_images:
x, y = int(mat.fit.x), int(mat.fit.y)
w, h = int(mat.albedo.size[0]), int(mat.albedo.size[1])
image_var = getattr(mat, type_name)
image_pixels = list(image_var.pixels[:])
for k in range(h):
for i in range(w):
for channel in range(4):
canvas_pixels[int((((k+y)*c_w)+(i+x))*4)+channel] = \
image_pixels[int(((k*w)+i)*4)+channel]
canvas.pixels[:] = canvas_pixels[:]
try:
save_dir = os.path.dirname(bpy.data.filepath)
canvas.save(filepath=os.path.join(save_dir, new_image_name+".png"))
except Exception as save_error:
logger.error(f"Failed to save atlas texture: {str(save_error)}")
self.report({'WARNING'}, f"Could not save texture to disk, This may be due to a lack of permissions.")
setattr(atlased_mat, type_name, canvas)
progress.step(f"Created {type_name} atlas")
# Create material nodes
atlased_mat.material = bpy.data.materials.new(
name=f"Atlas_Final_{context.scene.name}_{Path(bpy.data.filepath).stem}")
atlased_mat.material.use_nodes = True
atlased_mat.material.node_tree.nodes.clear()
principled_node = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled")
principled_node.location.x = 7.29706335067749
principled_node.location.y = 298.918212890625
output_node = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeOutputMaterial")
output_node.location.x = 297.29705810546875
output_node.location.y = 298.918212890625
albedo_node = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
albedo_node.location.x = -588.6177978515625
albedo_node.location.y = 414.1948547363281
albedo_node.image = atlased_mat.albedo
emission_node = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
emission_node.location.x = -588.6177978515625
emission_node.location.y = -173.9259033203125
emission_node.image = atlased_mat.emission
normal_node = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
normal_node.location.x = -941.4189453125
normal_node.location.y = -20.8391780853271
normal_node.image = atlased_mat.normal
normal_map_node = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeNormalMap")
normal_map_node.location.x = -545.550537109375
normal_map_node.location.y = -0.7543716430664062
roughness_node = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
roughness_node.location.x = -592.1703491210938
roughness_node.location.y = 206.74075317382812
roughness_node.image = atlased_mat.roughness
ambient_occlusion_node = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
ambient_occlusion_node.location.x = -906.4371337890625
ambient_occlusion_node.location.y = -389.9602355957031
ambient_occlusion_node.image = atlased_mat.ambient_occlusion
height_node = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
height_node.location.x = -1222.383056640625
height_node.location.y = -375.48406982421875
height_node.image = atlased_mat.height
atlased_mat.material.node_tree.links.new(principled_node.inputs["Base Color"], albedo_node.outputs["Color"])
atlased_mat.material.node_tree.links.new(principled_node.inputs["Metallic"], roughness_node.outputs["Alpha"])
atlased_mat.material.node_tree.links.new(principled_node.inputs["Roughness"], roughness_node.outputs["Color"])
atlased_mat.material.node_tree.links.new(principled_node.inputs["Alpha"], albedo_node.outputs["Alpha"])
atlased_mat.material.node_tree.links.new(principled_node.inputs["Normal"], normal_map_node.outputs["Normal"])
atlased_mat.material.node_tree.links.new(principled_node.inputs["Emission Color"], emission_node.outputs["Color"])
atlased_mat.material.node_tree.links.new(output_node.inputs["Surface"], principled_node.outputs["BSDF"])
atlased_mat.material.node_tree.links.new(normal_map_node.inputs["Color"], normal_node.outputs["Color"])
# Update materials
with ProgressTracker(context, len(context.scene.objects), "Updating Materials") as progress:
for obj in context.scene.objects:
if obj.type == 'MESH':
mesh = obj.data
for i, mat_slot in enumerate(obj.material_slots):
if mat_slot.material and mat_slot.material.include_in_atlas:
mesh.materials[i] = atlased_mat.material
progress.step(f"Updated materials for {obj.name}")
MaterialListBool.old_list.pop(context.scene.name, None)
was_open = context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown
context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown = False
if was_open:
bpy.ops.avatar_toolkit.expand_section_materials()
for area in context.screen.areas:
if area.type == 'VIEW_3D':
area.tag_redraw()
logger.info("Material atlas creation completed successfully")
self.report({'INFO'}, t("TextureAtlas.atlas_completed"))
return {"FINISHED"}
except Exception as e:
logger.error(f"Error creating material atlas: {str(e)}", exc_info=True)
self.report({'ERROR'}, t("TextureAtlas.atlas_error"))
raise e
+145 -125
View File
@@ -1,8 +1,8 @@
import bpy
import numpy as np
from typing import List, Optional, Dict, Set
from bpy.types import Context, Object, Operator
from typing import List, Optional, Dict, Set, Tuple, Any
from bpy.types import Context, Object, Operator, ArmatureModifier, EditBone, VertexGroup, Mesh, ShapeKey
from ...core.dictionaries import bone_names
from ...core.logging_setup import logger
from ...core.translations import t
from ...core.common import (
@@ -10,29 +10,31 @@ from ...core.common import (
fix_zero_length_bones,
clear_unused_data_blocks,
join_mesh_objects,
remove_unused_shapekeys
remove_unused_shapekeys,
)
from ...core.dictionaries import simplify_bonename
class AvatarToolkit_OT_MergeArmature(Operator):
bl_idname = 'avatar_toolkit.merge_armatures'
bl_label = t('MergeArmature.label')
bl_description = t('MergeArmature.desc')
bl_options = {'REGISTER', 'UNDO'}
class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
"""Operator for merging two armatures together with their associated meshes"""
bl_idname: str = 'avatar_toolkit.merge_armatures'
bl_label: str = t('MergeArmature.label')
bl_description: str = t('MergeArmature.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
def poll(cls, context: Context) -> bool:
return len(get_all_meshes(context)) > 1
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
try:
wm = context.window_manager
wm.progress_begin(0, 100)
# Get both armatures
base_armature_name = context.scene.merge_armature_into
merge_armature_name = context.scene.merge_armature
base_armature = bpy.data.objects.get(base_armature_name)
merge_armature = bpy.data.objects.get(merge_armature_name)
base_armature_name: str = context.scene.avatar_toolkit.merge_armature_into
merge_armature_name: str = context.scene.avatar_toolkit.merge_armature
base_armature: Optional[Object] = bpy.data.objects.get(base_armature_name)
merge_armature: Optional[Object] = bpy.data.objects.get(merge_armature_name)
if not base_armature or not merge_armature:
logger.error(f"Armature not found: {merge_armature_name}")
@@ -51,15 +53,13 @@ class AvatarToolkit_OT_MergeArmature(Operator):
wm.progress_update(80)
# Get settings from scene properties
merge_all_bones = context.scene.avatar_toolkit.merge_all_bones
join_meshes = context.scene.avatar_toolkit.join_meshes
join_meshes: bool = context.scene.avatar_toolkit.join_meshes
# Merge armatures
merge_armatures(
base_armature_name,
merge_armature_name,
mesh_only=False,
merge_all_bones=context.scene.avatar_toolkit.merge_all_bones,
join_meshes=join_meshes,
operator=self
)
@@ -76,10 +76,10 @@ class AvatarToolkit_OT_MergeArmature(Operator):
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def delete_rigidbodies_and_joints(armature: Object):
"""Delete rigid bodies and joints associated with the armature."""
to_delete = []
parent = armature
def delete_rigidbodies_and_joints(armature: Object) -> None:
"""Delete rigid bodies and joints associated with an armature"""
to_delete: List[Object] = []
parent: Object = armature
while parent.parent:
parent = parent.parent
@@ -94,39 +94,35 @@ def delete_rigidbodies_and_joints(armature: Object):
bpy.data.objects.remove(obj, do_unlink=True)
def validate_parents_and_transforms(merge_armature: Object, base_armature: Object, context: Context) -> bool:
"""Validate parents and transformations of armatures before merging."""
merge_parent = merge_armature.parent
base_parent = base_armature.parent
"""Validate parent relationships and transformations of armatures"""
merge_parent: Optional[Object] = merge_armature.parent
base_parent: Optional[Object] = base_armature.parent
if merge_parent or base_parent:
if context.scene.merge_all_bones:
for armature, parent in [(merge_armature, merge_parent), (base_armature, base_parent)]:
if parent:
if not is_transform_clean(parent):
logger.error("Parent transforms are not clean")
return False
bpy.data.objects.remove(parent, do_unlink=True)
else:
logger.error("Parent relationships need fixing")
return False
for armature, parent in [(merge_armature, merge_parent), (base_armature, base_parent)]:
if parent:
if not is_transform_clean(parent):
logger.error("Parent transforms are not clean")
return False
bpy.data.objects.remove(parent, do_unlink=True)
return True
def is_transform_clean(obj: Object) -> bool:
"""Check if an object's transforms are at default values."""
"""Check if object transforms are at default values"""
for i in range(3):
if obj.scale[i] != 1 or obj.location[i] != 0 or obj.rotation_euler[i] != 0:
return False
return True
def prepare_mesh_vertex_groups(mesh: Object):
"""Prepare mesh by assigning all vertices to a new vertex group."""
def prepare_mesh_vertex_groups(mesh: Object) -> None:
"""Initialize mesh vertex groups for merging process"""
if mesh.vertex_groups:
for vg in mesh.vertex_groups:
mesh.vertex_groups.remove(vg)
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
vg = mesh.vertex_groups.new(name=mesh.name)
vg: VertexGroup = mesh.vertex_groups.new(name=mesh.name)
bpy.ops.object.vertex_group_assign()
bpy.ops.object.mode_set(mode='OBJECT')
@@ -134,16 +130,15 @@ def merge_armatures(
base_armature_name: str,
merge_armature_name: str,
mesh_only: bool,
merge_all_bones: bool = False,
join_meshes: bool = False,
operator=None
):
"""Main function to merge two armatures."""
operator: Optional[Operator] = None
) -> None:
"""Main function to merge two armatures with their associated meshes and data"""
logger.info(f"Merging armatures: {merge_armature_name} into {base_armature_name}")
tolerance = 0.00008726647 # around 0.005 degrees
tolerance: float = 0.00008726647 # around 0.005 degrees
base_armature = bpy.data.objects.get(base_armature_name)
merge_armature = bpy.data.objects.get(merge_armature_name)
base_armature: Optional[Object] = bpy.data.objects.get(base_armature_name)
merge_armature: Optional[Object] = bpy.data.objects.get(merge_armature_name)
if not base_armature or not merge_armature:
logger.error(f"Armature not found: {merge_armature_name}")
@@ -151,6 +146,9 @@ def merge_armatures(
operator.report({'ERROR'}, t('MergeArmature.error.notFound', name=merge_armature_name))
return
# Store meshes that need to be reparented
meshes_to_reparent = [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == merge_armature]
# Check transforms early
if not validate_merge_armature_transforms(base_armature, merge_armature, None, tolerance):
if not bpy.context.scene.avatar_toolkit.apply_transforms:
@@ -172,26 +170,50 @@ def merge_armatures(
fix_zero_length_bones(merge_armature)
# Store original parent relationships
original_parents = {}
for bone in merge_armature.data.bones:
original_parents: Dict[str, Optional[str]] = {}
merge_armature_data: bpy.types.Armature = merge_armature.data
for bone in merge_armature_data.bones:
original_parents[bone.name] = bone.parent.name if bone.parent else None
#create reverse lookup
reverse_bone_lookup = {}
for preferred_name, name_list in bone_names.items():
for name in name_list:
reverse_bone_lookup[name] = preferred_name
# Get base bone names
base_bone_names = set(bone.name for bone in base_armature.data.bones)
base_bone_names: Set[str] = {bone.name for bone in base_armature.data.bones}
base_armature_standards: Dict[str,Optional[str]] = {}
for bone in base_bone_names:
if simplify_bonename(bone) in reverse_bone_lookup:
base_armature_standards[reverse_bone_lookup[simplify_bonename(bone)]] = bone
# Switch to edit mode on merge armature and rename bones
bpy.context.view_layer.objects.active = merge_armature
bpy.ops.object.mode_set(mode='EDIT')
# Handle bone renaming based on merge_all_bones setting
for bone in merge_armature.data.edit_bones:
if not merge_all_bones:
# Only rename bones that don't exist in base armature
if bone.name not in base_bone_names:
bone.name += '.merge'
# Handle bone renaming/removing to target armature.
bone_names_source: list[str] = [bone.name for bone in merge_armature_data.edit_bones]
for bone in bone_names_source:
bone_name = bone
if bone_name not in base_bone_names: #not auto mergable to original
if simplify_bonename(bone_name) in reverse_bone_lookup: #if is a standard bone through standard translation.
if reverse_bone_lookup[simplify_bonename(bone_name)] in base_armature_standards: #if this bone equals for example, "hips", does a bone that should be "hips" exist on our target armature?
#if so, rename this bone to that one
merge_armature_data.edit_bones[bone_name].name = base_armature_standards[reverse_bone_lookup[simplify_bonename(bone_name)]]
bone_name = merge_armature_data.edit_bones[bone_name].name
#adjust original parents list to point to the new name.
for child_bone in merge_armature_data.edit_bones[bone_name]:
original_parents[child_bone.name] = bone_name
#then remove so it doesn't clash when merged.
merge_armature_data.edit_bones.remove(merge_armature_data.edit_bones[bone_name])
continue
#if it really doesn't have a counter part, just don't bother.
else:
# Rename all bones from merge armature
bone.name += '.merge'
merge_armature_data.edit_bones.remove(merge_armature_data.edit_bones[bone_name])
# Return to object mode
bpy.ops.object.mode_set(mode='OBJECT')
@@ -203,27 +225,32 @@ def merge_armatures(
bpy.context.view_layer.objects.active = base_armature
bpy.ops.object.join()
# Explicitly set active object after join
bpy.context.view_layer.objects.active = base_armature
base_armature_data: bpy.types.Armature = base_armature.data
# Restore parent relationships
bpy.ops.object.mode_set(mode='EDIT')
for bone in base_armature.data.edit_bones:
base_name = bone.name.replace('.merge', '')
if base_name in original_parents:
parent_name = original_parents[base_name]
for bone in base_armature_data.edit_bones:
if bone.name in original_parents:
parent_name: Optional[str] = original_parents[bone.name]
if parent_name:
parent_bone = base_armature.data.edit_bones.get(parent_name)
parent_bone: Optional[EditBone] = base_armature_data.edit_bones.get(parent_name)
if parent_bone:
bone.parent = parent_bone
bpy.ops.object.mode_set(mode='OBJECT')
# Update mesh parenting
for obj in bpy.data.objects:
if obj.type == 'MESH' and obj.parent == merge_armature:
obj.parent = base_armature
for mesh_obj in meshes_to_reparent:
if mesh_obj and mesh_obj.name in bpy.data.objects:
mesh_obj.parent = base_armature
for mod in mesh_obj.modifiers:
if mod.type == 'ARMATURE':
mod.object = base_armature
# Process vertex groups if not mesh_only
if not mesh_only:
meshes = [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == base_armature]
meshes: List[Object] = [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == base_armature]
process_vertex_groups(meshes)
# Remove zero weight vertex groups if enabled
@@ -235,11 +262,13 @@ def merge_armatures(
# Join meshes if requested
if join_meshes:
meshes_to_join = [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == base_armature]
meshes_to_join: List[Object] = [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == base_armature]
if meshes_to_join:
joined_mesh = join_mesh_objects(bpy.context, meshes_to_join)
joined_mesh: Optional[Object] = join_mesh_objects(bpy.context, meshes_to_join)
if joined_mesh:
logger.info(f"Joined meshes into {joined_mesh.name}")
# Ensure the joined mesh is properly parented
joined_mesh.parent = base_armature
# Clean up shape keys if enabled
if bpy.context.scene.avatar_toolkit.cleanup_shape_keys:
@@ -249,24 +278,18 @@ def merge_armatures(
# Remove any remaining .merge bones
bpy.context.view_layer.objects.active = base_armature
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = base_armature.data.edit_bones
bones_to_remove = [bone for bone in edit_bones if bone.name.endswith('.merge')]
for bone in bones_to_remove:
edit_bones.remove(bone)
bpy.ops.object.mode_set(mode='OBJECT')
# Final cleanup
clear_unused_data_blocks()
def validate_merge_armature_transforms(
base_armature: Object,
merge_armature: Object,
mesh_merge: Optional[Object],
tolerance: float
) -> bool:
"""Validate transforms of both armatures and mesh."""
"""Validate transforms of both armatures and mesh"""
for i in [0, 1, 2]:
if abs(base_armature.scale[i] - merge_armature.scale[i]) > tolerance:
return False
@@ -280,10 +303,10 @@ def validate_merge_armature_transforms(
def adjust_merge_armature_transforms(
merge_armature: Object,
mesh_merge: Object
):
"""Adjust transforms of the merge armature."""
old_loc = list(merge_armature.location)
old_scale = list(merge_armature.scale)
) -> None:
"""Adjust transforms of the merge armature"""
old_loc: List[float] = list(merge_armature.location)
old_scale: List[float] = list(merge_armature.scale)
for i in [0, 1, 2]:
merge_armature.location[i] = (mesh_merge.location[i] * old_scale[i]) + old_loc[i]
@@ -295,27 +318,25 @@ def adjust_merge_armature_transforms(
mesh_merge.rotation_euler[i] = 0
mesh_merge.scale[i] = 1
def detect_bones_to_merge(
base_edit_bones: bpy.types.ArmatureEditBones,
merge_edit_bones: bpy.types.ArmatureEditBones,
tolerance: float,
merge_all_bones: bool
tolerance: float
) -> List[str]:
"""Detect corresponding bones between base and merge armatures using smart detection and position tolerance."""
bones_to_merge = []
"""Detect corresponding bones between base and merge armatures using smart detection and position tolerance"""
bones_to_merge: List[str] = []
# Cache base bone positions
base_bones_positions = {
base_bones_positions: Dict[str, np.ndarray] = {
bone.name: np.array(bone.head) for bone in base_edit_bones
}
# Smart bone detection
for merge_bone in merge_edit_bones:
merge_bone_position = np.array(merge_bone.head)
found_match = False
merge_bone_position: np.ndarray = np.array(merge_bone.head)
found_match: bool = False
if merge_all_bones and merge_bone.name in base_bones_positions:
if merge_bone.name in base_bones_positions:
# If merging same bones by name
bones_to_merge.append(merge_bone.name)
found_match = True
@@ -333,17 +354,16 @@ def detect_bones_to_merge(
return bones_to_merge
def process_vertex_groups(meshes: List[Object]):
"""Process vertex groups in meshes."""
def process_vertex_groups(meshes: List[Object]) -> None:
"""Process vertex groups in meshes"""
for mesh in meshes:
vg_names = {vg.name for vg in mesh.vertex_groups}
merge_vg_names = [vg_name for vg_name in vg_names if vg_name.endswith('.merge')]
vg_names: Set[str] = {vg.name for vg in mesh.vertex_groups}
merge_vg_names: List[str] = [vg_name for vg_name in vg_names if vg_name.endswith('.merge')]
for vg_merge_name in merge_vg_names:
base_name = vg_merge_name[:-6]
vg_merge = mesh.vertex_groups.get(vg_merge_name)
vg_base = mesh.vertex_groups.get(base_name)
base_name: str = vg_merge_name[:-6]
vg_merge: Optional[VertexGroup] = mesh.vertex_groups.get(vg_merge_name)
vg_base: Optional[VertexGroup] = mesh.vertex_groups.get(base_name)
if vg_merge is None:
continue
@@ -353,20 +373,20 @@ def process_vertex_groups(meshes: List[Object]):
else:
vg_merge.name = base_name
def mix_vertex_groups(mesh: Object, vg_from_name: str, vg_to_name: str):
"""Mix vertex group weights."""
vg_from = mesh.vertex_groups.get(vg_from_name)
vg_to = mesh.vertex_groups.get(vg_to_name)
def mix_vertex_groups(mesh: Object, vg_from_name: str, vg_to_name: str) -> None:
"""Mix vertex group weights"""
vg_from: Optional[VertexGroup] = mesh.vertex_groups.get(vg_from_name)
vg_to: Optional[VertexGroup] = mesh.vertex_groups.get(vg_to_name)
if not vg_from or not vg_to:
return
num_vertices = len(mesh.data.vertices)
weights_from = np.zeros(num_vertices)
weights_to = np.zeros(num_vertices)
num_vertices: int = len(mesh.data.vertices)
weights_from: np.ndarray = np.zeros(num_vertices)
weights_to: np.ndarray = np.zeros(num_vertices)
idx_from = vg_from.index
idx_to = vg_to.index
idx_from: int = vg_from.index
idx_to: int = vg_to.index
for v in mesh.data.vertices:
for g in v.groups:
@@ -375,14 +395,14 @@ def mix_vertex_groups(mesh: Object, vg_from_name: str, vg_to_name: str):
elif g.group == idx_to:
weights_to[v.index] = g.weight
weights_combined = np.clip(weights_from + weights_to, 0.0, 1.0)
weights_combined: np.ndarray = np.clip(weights_from + weights_to, 0.0, 1.0)
vg_to.add(range(num_vertices), weights_combined.tolist(), 'REPLACE')
mesh.vertex_groups.remove(vg_from)
def remove_unused_vertex_groups(mesh: Object):
"""Remove vertex groups with no weights."""
def remove_unused_vertex_groups(mesh: Object) -> None:
"""Remove vertex groups with no weights"""
for vg in mesh.vertex_groups:
has_weights = False
has_weights: bool = False
for vert in mesh.data.vertices:
for group in vert.groups:
if group.group == vg.index and group.weight > 0.001:
@@ -393,9 +413,9 @@ def remove_unused_vertex_groups(mesh: Object):
if not has_weights:
mesh.vertex_groups.remove(vg)
def apply_armature_to_mesh(armature: Object, mesh: Object):
"""Apply armature deformation to mesh."""
armature_mod = mesh.modifiers.new('PoseToRest', 'ARMATURE')
def apply_armature_to_mesh(armature: Object, mesh: Object) -> None:
"""Apply armature deformation to mesh"""
armature_mod: ArmatureModifier = mesh.modifiers.new('PoseToRest', 'ARMATURE')
armature_mod.object = armature
if bpy.app.version >= (3, 5):
@@ -407,15 +427,15 @@ def apply_armature_to_mesh(armature: Object, mesh: Object):
with bpy.context.temp_override(object=mesh):
bpy.ops.object.modifier_apply(modifier=armature_mod.name)
def apply_armature_to_mesh_with_shapekeys(armature: Object, mesh: Object, context: Context):
"""Apply armature deformation to mesh with shape keys."""
old_active_index = mesh.active_shape_key_index
old_show_only = mesh.show_only_shape_key
def apply_armature_to_mesh_with_shapekeys(armature: Object, mesh: Object, context: Context) -> None:
"""Apply armature deformation to mesh with shape keys"""
old_active_index: int = mesh.active_shape_key_index
old_show_only: bool = mesh.show_only_shape_key
mesh.show_only_shape_key = True
shape_keys = mesh.data.shape_keys.key_blocks
vertex_groups = []
mutes = []
shape_keys: List[ShapeKey] = mesh.data.shape_keys.key_blocks
vertex_groups: List[str] = []
mutes: List[bool] = []
for sk in shape_keys:
vertex_groups.append(sk.vertex_group)
@@ -423,23 +443,23 @@ def apply_armature_to_mesh_with_shapekeys(armature: Object, mesh: Object, contex
mutes.append(sk.mute)
sk.mute = False
disabled_mods = []
disabled_mods: List[Any] = []
for mod in mesh.modifiers:
if mod.show_viewport:
mod.show_viewport = False
disabled_mods.append(mod)
arm_mod = mesh.modifiers.new('PoseToRest', 'ARMATURE')
arm_mod: ArmatureModifier = mesh.modifiers.new('PoseToRest', 'ARMATURE')
arm_mod.object = armature
co_length = len(mesh.data.vertices) * 3
eval_cos = np.empty(co_length, dtype=np.single)
co_length: int = len(mesh.data.vertices) * 3
eval_cos: np.ndarray = np.empty(co_length, dtype=np.single)
for i, shape_key in enumerate(shape_keys):
mesh.active_shape_key_index = i
depsgraph = context.evaluated_depsgraph_get()
eval_mesh = mesh.evaluated_get(depsgraph)
eval_mesh: Mesh = mesh.evaluated_get(depsgraph)
eval_mesh.data.vertices.foreach_get('co', eval_cos)
shape_key.data.foreach_set('co', eval_cos)
+31 -23
View File
@@ -1,42 +1,48 @@
import bpy
from bpy.types import Operator, Context, Object
from bpy.types import Operator, Context, Object, ArmatureModifier, VertexGroup
from mathutils import Vector
from typing import Set, Optional
from typing import Set, Optional, List, Any
from ...core.logging_setup import logger
from ...core.translations import t
from ...core.common import (
get_active_armature,
validate_armature,
get_all_meshes,
ProgressTracker,
calculate_bone_orientation,
add_armature_modifier
)
from ...core.armature_validation import validate_armature
class AvatarToolkit_OT_AttachMesh(Operator):
"""Attach a mesh to an armature bone with automatic weight setup"""
bl_idname = "avatar_toolkit.attach_mesh"
bl_label = t("AttachMesh.label")
bl_description = t("AttachMesh.desc")
bl_options = {'REGISTER', 'UNDO'}
"""Operator to attach a mesh to an armature bone with automatic weight setup"""
bl_idname: str = "avatar_toolkit.attach_mesh"
bl_label: str = t("AttachMesh.label")
bl_description: str = t("AttachMesh.desc")
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
return armature is not None and context.mode == 'OBJECT' and len(get_all_meshes(context)) > 0
"""Check if operator can be executed"""
armature: Optional[Object] = get_active_armature(context)
if not armature:
return False
valid, _, _ = validate_armature(armature)
return valid
def execute(self, context: Context) -> Set[str]:
try:
logger.info("Starting mesh attachment process")
mesh_name = context.scene.avatar_toolkit.attach_mesh
armature = get_active_armature(context)
attach_bone_name = context.scene.avatar_toolkit.attach_bone
mesh = bpy.data.objects.get(mesh_name)
mesh_name: str = context.scene.avatar_toolkit.attach_mesh
armature: Object = get_active_armature(context)
attach_bone_name: str = context.scene.avatar_toolkit.attach_bone
mesh: Optional[Object] = bpy.data.objects.get(mesh_name)
with ProgressTracker(context, 10, "Attaching Mesh") as progress:
# Validation steps
is_valid: bool
error_msg: str
is_valid, error_msg = validate_mesh_transforms(mesh)
if not is_valid:
raise ValueError(error_msg)
@@ -63,7 +69,7 @@ class AvatarToolkit_OT_AttachMesh(Operator):
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
vg = mesh.vertex_groups.new(name=mesh_name)
vg: VertexGroup = mesh.vertex_groups.new(name=mesh_name)
bpy.ops.object.vertex_group_assign()
bpy.ops.object.mode_set(mode='OBJECT')
progress.step(t("AttachMesh.setup_weights"))
@@ -83,12 +89,14 @@ class AvatarToolkit_OT_AttachMesh(Operator):
progress.step(t("AttachMesh.create_bone"))
# Calculate bone placement
verts_in_group = [v for v in mesh.data.vertices
verts_in_group: List[Any] = [v for v in mesh.data.vertices
for g in v.groups if g.group == vg.index]
dimensions: Vector
roll_angle: float
dimensions, roll_angle = calculate_bone_orientation(mesh, verts_in_group)
# Set bone position and orientation
center = Vector((0, 0, 0))
center: Vector = Vector((0, 0, 0))
for v in verts_in_group:
center += mesh.data.vertices[v.index].co
center /= len(verts_in_group)
@@ -111,20 +119,20 @@ class AvatarToolkit_OT_AttachMesh(Operator):
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def validate_mesh_transforms(mesh):
"""Validate mesh transforms are suitable for attaching."""
def validate_mesh_transforms(mesh: Optional[Object]) -> tuple[bool, str]:
"""Validate mesh transforms are suitable for attaching"""
if not mesh:
return False, "Mesh not found"
# Check for non-uniform scale
scale = mesh.scale
scale: Vector = mesh.scale
if abs(scale[0] - scale[1]) > 0.001 or abs(scale[1] - scale[2]) > 0.001:
return False, "Mesh has non-uniform scale. Please apply scale (Ctrl+A)"
return True, ""
def validate_mesh_name(armature, mesh_name):
"""Validate mesh name doesn't conflict with existing bones."""
def validate_mesh_name(armature: Object, mesh_name: str) -> tuple[bool, str]:
"""Validate mesh name doesn't conflict with existing bones"""
if mesh_name in armature.data.bones:
return False, f"Bone named '{mesh_name}' already exists in armature"
return True, ""
return True, ""
+122 -83
View File
@@ -5,8 +5,8 @@ import math
import bmesh
import mathutils
import json
from bpy.types import Operator, Object, Context
from typing import Optional, Dict, Tuple, Set
from bpy.types import Operator, Object, Context, UILayout, WindowManager, Event, ShapeKey, EditBone, PoseBone
from typing import Optional, Dict, Tuple, Set, List, Any, Union, ClassVar
from collections import OrderedDict
from random import random
from itertools import chain
@@ -18,25 +18,25 @@ from ..core.common import (
get_active_armature,
get_all_meshes,
get_armature_list,
validate_armature,
validate_mesh_for_pose,
cache_vertex_positions,
apply_vertex_positions
)
from ..core.armature_validation import validate_armature
VALID_EYE_NAMES = {
VALID_EYE_NAMES: Dict[str, List[str]] = {
'left': ['LeftEye', 'Eye_L', 'eye_L', 'eye.L', 'EyeLeft', 'left_eye', 'l_eye'],
'right': ['RightEye', 'Eye_R', 'eye_R', 'eye.R', 'EyeRight', 'right_eye', 'r_eye']
}
class CreateEyesAV3Button(bpy.types.Operator):
"""Create eye tracking setup for VRChat Avatar 3.0"""
bl_idname = 'avatar_toolkit.create_eye_tracking_av3'
bl_label = t('EyeTracking.create.av3.label')
bl_description = t('EyeTracking.create.av3.desc')
bl_options = {'REGISTER', 'UNDO'}
"""Creates eye tracking setup compatible with VRChat Avatar 3.0 system"""
bl_idname: str = 'avatar_toolkit.create_eye_tracking_av3'
bl_label: str = t('EyeTracking.create.av3.label')
bl_description: str = t('EyeTracking.create.av3.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
mesh = None
mesh: Optional[Object] = None
@classmethod
def poll(cls, context):
@@ -109,13 +109,13 @@ class CreateEyesAV3Button(bpy.types.Operator):
return {'CANCELLED'}
class CreateEyesSDK2Button(bpy.types.Operator):
"""Create eye tracking setup for VRChat SDK2"""
bl_idname = 'avatar_toolkit.create_eye_tracking_sdk2'
bl_label = t('EyeTracking.create.sdk2.label')
bl_description = t('EyeTracking.create.sdk2.desc')
bl_options = {'REGISTER', 'UNDO'}
"""Creates eye tracking setup compatible with VRChat SDK2 system"""
bl_idname: str = 'avatar_toolkit.create_eye_tracking_sdk2'
bl_label: str = t('EyeTracking.create.sdk2.label')
bl_description: str = t('EyeTracking.create.sdk2.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
mesh = None
mesh: Optional[Object] = None
@classmethod
def poll(cls, context):
@@ -201,8 +201,9 @@ class CreateEyesSDK2Button(bpy.types.Operator):
return {'CANCELLED'}
class EyeTrackingBackup:
def __init__(self):
self.backup_path = os.path.join(bpy.app.tempdir, "eye_tracking_backup.json")
"""Manages backup and restoration of eye bone positions"""
def __init__(self) -> None:
self.backup_path: str = os.path.join(bpy.app.tempdir, "eye_tracking_backup.json")
self.bone_positions: Dict[str, Dict[str, Tuple[float, float, float]]] = {}
def store_bone_positions(self, armature) -> bool:
@@ -247,8 +248,10 @@ class EyeTrackingBackup:
return False
class EyeTrackingValidator:
"""Validates eye tracking setup requirements and configurations"""
@staticmethod
def find_eye_vertex_groups(mesh_name: str) -> Tuple[str, str]:
def find_eye_vertex_groups(mesh_name: str) -> Tuple[Optional[str], Optional[str]]:
"""Locates left and right eye vertex groups in mesh"""
mesh = bpy.data.objects.get(mesh_name)
if not mesh:
return None, None
@@ -265,7 +268,8 @@ class EyeTrackingValidator:
return left_group, right_group
@staticmethod
def validate_setup(context, mesh_name: str) -> Tuple[bool, str]:
def validate_setup(context: Context, mesh_name: str) -> Tuple[bool, str]:
"""Validates complete eye tracking setup configuration"""
armature = get_active_armature(context)
if not armature:
return False, t('EyeTracking.validation.noArmature')
@@ -299,10 +303,11 @@ class EyeTrackingValidator:
return True, t('EyeTracking.validation.success')
class StartTestingButton(bpy.types.Operator):
bl_idname = 'avatar_toolkit.start_eye_testing'
bl_label = t('EyeTracking.testing.start.label')
bl_description = t('EyeTracking.testing.start.desc')
bl_options = {'REGISTER', 'UNDO'}
"""Initiates eye tracking testing mode"""
bl_idname: str = 'avatar_toolkit.start_eye_testing'
bl_label: str = t('EyeTracking.testing.start.label')
bl_description: str = t('EyeTracking.testing.start.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
@@ -351,10 +356,11 @@ class StartTestingButton(bpy.types.Operator):
return {'FINISHED'}
class StopTestingButton(bpy.types.Operator):
bl_idname = 'avatar_toolkit.stop_eye_testing'
bl_label = t('EyeTracking.testing.stop.label')
bl_description = t('EyeTracking.testing.stop.desc')
bl_options = {'REGISTER', 'UNDO'}
"""Terminates eye tracking testing mode"""
bl_idname: str = 'avatar_toolkit.stop_eye_testing'
bl_label: str = t('EyeTracking.testing.stop.label')
bl_description: str = t('EyeTracking.testing.stop.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
def execute(self, context):
global eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot
@@ -392,6 +398,7 @@ class StopTestingButton(bpy.types.Operator):
return {'FINISHED'}
def set_rotation(self, context):
"""Updates eye bone rotations based on current settings"""
global eye_left, eye_right, eye_left_rot, eye_right_rot
toolkit = context.scene.avatar_toolkit
@@ -399,6 +406,14 @@ def set_rotation(self, context):
StartTestingButton.execute(StartTestingButton, context)
return None
# Check if rotation data is available
if not eye_left_rot or len(eye_left_rot) < 3 or not eye_right_rot or len(eye_right_rot) < 3:
# Initialize rotation data if missing
eye_left.rotation_mode = 'XYZ'
eye_left_rot = list(eye_left.rotation_euler)
eye_right.rotation_mode = 'XYZ'
eye_right_rot = list(eye_right.rotation_euler)
eye_left.rotation_mode = 'XYZ'
eye_right.rotation_mode = 'XYZ'
@@ -414,10 +429,11 @@ def set_rotation(self, context):
return None
class ResetRotationButton(bpy.types.Operator):
bl_idname = 'avatar_toolkit.reset_eye_rotation'
bl_label = t('EyeTracking.reset.label')
bl_description = t('EyeTracking.reset.desc')
bl_options = {'REGISTER', 'UNDO'}
"""Resets eye bone rotations to default values"""
bl_idname: str = 'avatar_toolkit.reset_eye_rotation'
bl_label: str = t('EyeTracking.reset.label')
bl_description: str = t('EyeTracking.reset.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
@@ -445,10 +461,11 @@ class ResetRotationButton(bpy.types.Operator):
return {'FINISHED'}
class AdjustEyesButton(bpy.types.Operator):
bl_idname = 'avatar_toolkit.adjust_eyes'
bl_label = t('EyeTracking.adjust.label')
bl_description = t('EyeTracking.adjust.desc')
bl_options = {'REGISTER', 'UNDO'}
"""Adjusts eye bone positions and orientations"""
bl_idname: str = 'avatar_toolkit.adjust_eyes'
bl_label: str = t('EyeTracking.adjust.label')
bl_description: str = t('EyeTracking.adjust.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
@@ -494,10 +511,11 @@ class AdjustEyesButton(bpy.types.Operator):
return {'FINISHED'}
class StartIrisHeightButton(bpy.types.Operator):
bl_idname = 'avatar_toolkit.adjust_iris_height'
bl_label = t('EyeTracking.iris.label')
bl_description = t('EyeTracking.iris.desc')
bl_options = {'REGISTER', 'UNDO'}
"""Adjusts iris height for eye meshes"""
bl_idname: str = 'avatar_toolkit.adjust_iris_height'
bl_label: str = t('EyeTracking.iris.label')
bl_description: str = t('EyeTracking.iris.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
@@ -536,10 +554,11 @@ class StartIrisHeightButton(bpy.types.Operator):
return {'FINISHED'}
class TestBlinking(bpy.types.Operator):
bl_idname = 'avatar_toolkit.test_blinking'
bl_label = t('EyeTracking.blink.test.label')
bl_description = t('EyeTracking.blink.test.desc')
bl_options = {'REGISTER', 'UNDO'}
"""Tests eye blinking animations"""
bl_idname: str = 'avatar_toolkit.test_blinking'
bl_label: str = t('EyeTracking.blink.test.label')
bl_description: str = t('EyeTracking.blink.test.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
@@ -559,10 +578,11 @@ class TestBlinking(bpy.types.Operator):
return {'FINISHED'}
class TestLowerlid(bpy.types.Operator):
bl_idname = 'avatar_toolkit.test_lowerlid'
bl_label = t('EyeTracking.lowerlid.test.label')
bl_description = t('EyeTracking.lowerlid.test.desc')
bl_options = {'REGISTER', 'UNDO'}
"""Tests lower eyelid movements"""
bl_idname: str = 'avatar_toolkit.test_lowerlid'
bl_label: str = t('EyeTracking.lowerlid.test.label')
bl_description: str = t('EyeTracking.lowerlid.test.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
@@ -584,10 +604,11 @@ class TestLowerlid(bpy.types.Operator):
return {'FINISHED'}
class ResetBlinkTest(bpy.types.Operator):
bl_idname = 'avatar_toolkit.reset_blink_test'
bl_label = t('EyeTracking.blink.reset.label')
bl_description = t('EyeTracking.blink.reset.desc')
bl_options = {'REGISTER', 'UNDO'}
"""Resets all eye blinking test values"""
bl_idname: str = 'avatar_toolkit.reset_blink_test'
bl_label: str = t('EyeTracking.blink.reset.label')
bl_description: str = t('EyeTracking.blink.reset.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
def execute(self, context):
toolkit = context.scene.avatar_toolkit
@@ -601,7 +622,8 @@ class ResetBlinkTest(bpy.types.Operator):
return {'FINISHED'}
def fix_eye_position(context, old_eye, new_eye, head, right_side):
def fix_eye_position(context: Context, old_eye: Union[EditBone, PoseBone], new_eye: EditBone, head: Optional[EditBone], right_side: bool) -> None:
"""Adjusts eye bone positions and orientations for proper tracking"""
toolkit = context.scene.avatar_toolkit
scale = -toolkit.eye_distance + 1
mesh = bpy.data.objects[toolkit.mesh_name_eye]
@@ -637,8 +659,8 @@ def fix_eye_position(context, old_eye, new_eye, head, right_side):
new_eye.tail[y_cord] = new_eye.head[y_cord]
new_eye.tail[z_cord] = new_eye.head[z_cord] + 0.1
def repair_shapekeys(mesh_name, vertex_group):
"""Fix VRC shape keys by slightly adjusting vertex positions"""
def repair_shapekeys(mesh_name: str, vertex_group: str) -> None:
"""Repairs VRChat shape keys by adjusting vertex positions"""
armature = get_active_armature(bpy.context)
mesh = bpy.data.objects[mesh_name]
mesh.select_set(True)
@@ -696,10 +718,12 @@ def repair_shapekeys(mesh_name, vertex_group):
logger.warning('Shape key repair failed, using random method')
repair_shapekeys_mouth(mesh_name)
def randBoolNumber():
def randBoolNumber() -> int:
"""Generates random boolean value as integer"""
return -1 if random() < 0.5 else 1
def repair_shapekeys_mouth(mesh_name):
def repair_shapekeys_mouth(mesh_name: str) -> None:
"""Repairs mouth-related shape keys using fallback method"""
mesh = bpy.data.objects[mesh_name]
mesh.select_set(True)
bpy.context.view_layer.objects.active = mesh
@@ -730,12 +754,12 @@ def repair_shapekeys_mouth(mesh_name):
if not moved:
logger.error('Random shape key repair failed')
def get_bone_orientations():
"""Get bone orientation axes"""
def get_bone_orientations() -> Tuple[int, int, int]:
"""Returns standardized bone orientation axes"""
return (0, 1, 2) # x, y, z coordinates
def find_center_vector_of_vertex_group(mesh, group_name):
"""Calculate center position of vertex group"""
def find_center_vector_of_vertex_group(mesh: Object, group_name: str) -> Union[mathutils.Vector, bool]:
"""Calculates center position of vertex group"""
group = mesh.vertex_groups.get(group_name)
if not group:
return False
@@ -751,8 +775,8 @@ def find_center_vector_of_vertex_group(mesh, group_name):
return sum((v for v in vertices), mathutils.Vector()) / len(vertices)
def vertex_group_exists(mesh_obj, group_name):
"""Check if vertex group exists and has weights"""
def vertex_group_exists(mesh_obj: Object, group_name: str) -> bool:
"""Verifies existence and validity of vertex group"""
if not mesh_obj or group_name not in mesh_obj.vertex_groups:
return False
@@ -763,8 +787,8 @@ def vertex_group_exists(mesh_obj, group_name):
return True
return False
def copy_vertex_group(self, vertex_group, rename_to):
"""Copy vertex group with new name"""
def copy_vertex_group(self: Any, vertex_group: str, rename_to: str) -> None:
"""Creates copy of vertex group with new name"""
vertex_group_index = 0
# Select and make mesh active
bpy.ops.object.mode_set(mode='OBJECT')
@@ -781,8 +805,8 @@ def copy_vertex_group(self, vertex_group, rename_to):
vertex_group_index += 1
def copy_shape_key(self, context, from_shape, new_names, new_index):
"""Copy shape key with new name"""
def copy_shape_key(self: Any, context: Context, from_shape: str, new_names: List[str], new_index: int) -> str:
"""Creates copy of shape key with new name"""
blinking = not context.scene.avatar_toolkit.disable_eye_blinking
new_name = new_names[new_index - 1]
@@ -847,11 +871,11 @@ class VertexGroupCache:
cls._cache.clear()
class RotateEyeBonesForAv3Button(Operator):
"""Reorient eye bones for proper VRChat eye tracking"""
bl_idname = "avatar_toolkit.rotate_eye_bones"
bl_label = t("EyeTracking.rotate.label")
bl_description = t("EyeTracking.rotate.desc")
bl_options = {'REGISTER', 'UNDO'}
"""Reorients eye bones for VRChat Avatar 3.0 compatibility"""
bl_idname: str = "avatar_toolkit.rotate_eye_bones"
bl_label: str = t("EyeTracking.rotate.label")
bl_description: str = t("EyeTracking.rotate.desc")
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
@@ -874,21 +898,36 @@ class RotateEyeBonesForAv3Button(Operator):
return {'FINISHED'}
class ResetEyeTrackingButton(Operator):
"""Reset all eye tracking settings and state"""
bl_idname = 'avatar_toolkit.reset_eye_tracking'
bl_label = t('EyeTracking.reset.label')
bl_description = t('EyeTracking.reset.desc')
bl_options = {'REGISTER', 'UNDO'}
"""Resets all eye tracking settings to default values"""
bl_idname: str = 'avatar_toolkit.reset_eye_tracking'
bl_label: str = t('EyeTracking.reset.label')
bl_description: str = t('EyeTracking.reset.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
def execute(self, context):
global eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot
eye_left = eye_right = eye_left_data = eye_right_data = None
eye_left_rot = eye_right_rot = []
context.scene.avatar_toolkit.eye_mode = 'CREATION'
context.scene.avatar_toolkit.eye_rotation_x = 0
context.scene.avatar_toolkit.eye_rotation_y = 0
eye_left = None
eye_right = None
eye_left_data = None
eye_right_data = None
eye_left_rot = []
eye_right_rot = []
mesh_name = context.scene.avatar_toolkit.mesh_name_eye
mesh = bpy.data.objects.get(mesh_name)
if mesh and mesh.data.shape_keys:
for shape_key in mesh.data.shape_keys.key_blocks:
shape_key.value = 0
return {'FINISHED'}
def validate_weights(mesh_obj: Object, vertex_group: str) -> bool:
"""Validate vertex group weights"""
"""Validates vertex group weight assignments"""
group = mesh_obj.vertex_groups.get(vertex_group)
if not group:
return False
@@ -899,8 +938,8 @@ def validate_weights(mesh_obj: Object, vertex_group: str) -> bool:
return True
return False
def get_eye_bone_names(armature: Object) -> Dict[str, str]:
"""Get standardized eye bone names"""
def get_eye_bone_names(armature: Object) -> Dict[str, Optional[str]]:
"""Retrieves standardized eye bone names from armature"""
eye_bones = {'left': None, 'right': None}
for bone in armature.data.bones:
@@ -912,7 +951,7 @@ def get_eye_bone_names(armature: Object) -> Dict[str, str]:
return eye_bones
def stop_testing(context: Context) -> None:
"""Stop eye tracking testing mode"""
"""Stops eye tracking testing mode and resets all values"""
global eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot
if not all([eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot]):
-792
View File
@@ -1,792 +0,0 @@
import bpy
from mathutils import Vector
from typing import Dict, List, Tuple, Set, Optional
from bpy.types import Object, Armature, EditBone, Bone, Operator, Context
from ..core.logging_setup import logger
from ..core.common import (
ProgressTracker,
get_active_armature,
validate_armature,
get_vertex_weights,
transfer_vertex_weights,
get_all_meshes
)
from ..core.translations import t
from ..core.dictionaries import bone_names, dont_delete_these_main_bones
class AVATAR_TOOLKIT_OT_StandardizeMmd(Operator):
"""MMD Bone standardization system"""
bl_idname = "avatar_toolkit.standardize_mmd"
bl_label = t("MMD.standardize")
bl_options = {'REGISTER', 'UNDO'}
def __init__(self):
self.bone_mapping: Dict[str, str] = {}
self.processed_bones: Set[str] = set()
def execute(self, context: Context) -> Set[str]:
self.armature = get_active_armature(context)
if not self.armature:
self.report({'ERROR'}, t("MMD.no_armature"))
return {'CANCELLED'}
try:
with ProgressTracker(context, 5, "MMD Standardization") as progress:
# Step 1: Process bone names
self.process_bone_names(context)
progress.step("Processed bone names")
# Step 2: Fix bone structure
self.fix_bone_structure(context)
progress.step("Fixed bone structure")
# Step 3: Process weights
self.process_weights(context)
progress.step("Processed weights")
# Step 4: Clean up
self.cleanup_armature(context)
progress.step("Cleaned up armature")
# Step 5: Final validation
self.validate_results(context)
progress.step("Validated results")
self.report({'INFO'}, t("MMD.standardization_complete"))
return {'FINISHED'}
except Exception as e:
logger.error(f"MMD Standardization failed: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def process_bone_names(self, context: Context) -> None:
"""Process and standardize bone names"""
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = self.armature.data.edit_bones
# First pass - handle IK bones
ik_bones = [bone for bone in edit_bones if 'IK' in bone.name or 'IK' in bone.name]
for bone in ik_bones:
new_name = f"ik_{self.standardize_bone_name(bone.name.replace('IK', '').replace('IK', ''))}"
self.bone_mapping[bone.name] = new_name
bone.name = new_name
# Second pass - standard bones
for bone in edit_bones:
if bone not in ik_bones:
new_name = self.standardize_bone_name(bone.name)
if new_name != bone.name:
self.bone_mapping[bone.name] = new_name
bone.name = new_name
def translate_japanese_bone_name(self, name: str) -> str:
"""Translate Japanese bone names to English standardized names"""
name_lower = name.lower()
for bone_category, variations in bone_names.items():
for variation in variations:
if variation in name_lower:
return bone_category
return name
def standardize_bone_name(self, name: str) -> str:
"""Standardize individual bone names"""
result = self.translate_japanese_bone_name(name)
prefixes = ['ValveBiped_', 'Bip01_', 'MMD_', 'Armature|']
for prefix in prefixes:
if result.lower().startswith(prefix.lower()):
result = result[len(prefix):]
if result.endswith('_L') or result.endswith('.L'):
result = f"{result[:-2]}.L"
elif result.endswith('_R') or result.endswith('.R'):
result = f"{result[:-2]}.R"
return result
return result
def fix_bone_structure(self, context: Context) -> None:
"""Fix bone hierarchy and orientations"""
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = self.armature.data.edit_bones
self.process_spine_chain(context)
self.fix_bone_orientations(context)
self.connect_bones(context)
def process_weights(self, context: Context) -> None:
"""Process and clean up vertex weights"""
for mesh in self.get_associated_meshes(context):
# Transfer weights based on bone mapping
for old_name, new_name in self.bone_mapping.items():
if old_name != new_name:
transfer_vertex_weights(mesh, old_name, new_name)
# Clean up zero weights
self.cleanup_vertex_groups(mesh, context)
def cleanup_armature(self, context: Context) -> None:
"""Perform final cleanup operations"""
self.remove_unused_bones(context)
self.cleanup_constraints(context)
self.fix_zero_length_bones(context)
def get_associated_meshes(self, context: Context) -> List[Object]:
"""Get all mesh objects associated with the armature"""
return [obj for obj in bpy.data.objects
if obj.type == 'MESH'
and obj.parent == self.armature]
def process_spine_chain(self, context: Context) -> None:
"""Process and fix spine bone chain hierarchy"""
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = self.armature.data.edit_bones
spine_bones = {
'hips': None,
'spine': None,
'chest': None,
'upper_chest': None,
'neck': None,
'head': None
}
# Find spine bones using bone_names dictionary
for bone in edit_bones:
for spine_part, _ in spine_bones.items():
if any(alt_name in bone.name.lower() for alt_name in bone_names[spine_part]):
spine_bones[spine_part] = bone
break
# Set up spine hierarchy
hierarchy = [
('hips', 'spine'),
('spine', 'chest'),
('chest', 'neck'),
('neck', 'head')
]
for parent_name, child_name in hierarchy:
parent = spine_bones.get(parent_name)
child = spine_bones.get(child_name)
if parent and child:
child.parent = parent
child.use_connect = True
def fix_bone_orientations(self, context: Context) -> None:
"""Fix bone orientations for standard pose compatibility"""
edit_bones = self.armature.data.edit_bones
# Define standardized roll values for key bones
roll_values = {
'upper_arm.L': -0.1,
'upper_arm.R': 0.1,
'forearm.L': -0.1,
'forearm.R': 0.1,
'thigh.L': 0.0,
'thigh.R': 0.0,
'shin.L': 0.0,
'shin.R': 0.0,
'foot.L': 0.0,
'foot.R': 0.0,
'spine': 0.0,
'chest': 0.0,
'neck': 0.0
}
# Apply roll corrections
for bone in edit_bones:
if bone.name.lower() in roll_values:
bone.roll = roll_values[bone.name.lower()]
# Process arm chains
arm_pairs = [
('upper_arm', 'forearm'),
('forearm', 'hand')
]
for side in ['.L', '.R']:
for parent, child in arm_pairs:
parent_bone = next((b for b in edit_bones if b.name.lower().startswith(parent) and b.name.endswith(side)), None)
child_bone = next((b for b in edit_bones if b.name.lower().startswith(child) and b.name.endswith(side)), None)
if parent_bone and child_bone:
child_bone.use_connect = True
child_bone.use_inherit_rotation = True
# Process leg chains
leg_pairs = [
('thigh', 'shin'),
('shin', 'foot')
]
for side in ['.L', '.R']:
for parent, child in leg_pairs:
parent_bone = next((b for b in edit_bones if b.name.lower().startswith(parent) and b.name.endswith(side)), None)
child_bone = next((b for b in edit_bones if b.name.lower().startswith(child) and b.name.endswith(side)), None)
if parent_bone and child_bone:
child_bone.use_connect = True
child_bone.use_inherit_rotation = True
# Align twist bones if present
twist_bones = [b for b in edit_bones if 'twist' in b.name.lower()]
for twist_bone in twist_bones:
if twist_bone.parent:
twist_bone.roll = twist_bone.parent.roll
def remove_unused_bones(self, context: Context) -> None:
"""Remove unused and unnecessary bones from the armature"""
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = self.armature.data.edit_bones
# Get list of bones that have vertex weights
used_bones = set()
for mesh in self.get_associated_meshes(context):
for group in mesh.vertex_groups:
used_bones.add(group.name)
# Get list of essential bones to always keep
essential_bones = {
'hips', 'spine', 'chest', 'upper_chest', 'neck', 'head',
'left_leg', 'right_leg', 'left_knee', 'right_knee',
'left_ankle', 'right_ankle', 'left_toe', 'right_toe'
}
# Add any additional bones you want to preserve
essential_bones.update(dont_delete_these_main_bones)
# Remove unused bones
for bone in edit_bones:
# Skip if bone is essential
if bone.name.lower() in essential_bones:
continue
# Skip if bone has weights
if bone.name in used_bones:
continue
# Remove the bone
edit_bones.remove(bone)
def connect_bones(self, context: Context) -> None:
"""Connect bones that should be connected in the hierarchy"""
edit_bones = self.armature.data.edit_bones
connect_chains = [
['hips', 'spine', 'chest', 'neck', 'head'],
['shoulder.L', 'upper_arm.L', 'forearm.L', 'hand.L'],
['shoulder.R', 'upper_arm.R', 'forearm.R', 'hand.R'],
['thigh.L', 'shin.L', 'foot.L', 'toe.L'],
['thigh.R', 'shin.R', 'foot.R', 'toe.R']
]
for chain in connect_chains:
prev_bone = None
for bone_name in chain:
bone = next((b for b in edit_bones if b.name.lower().endswith(bone_name.lower())), None)
if bone and prev_bone:
bone.parent = prev_bone
bone.use_connect = True
prev_bone = bone
def cleanup_vertex_groups(self, mesh_obj: Object, context: Context) -> None:
"""Clean up vertex groups by removing zero weights and merging similar groups"""
threshold = context.scene.avatar_toolkit.merge_weights_threshold
vertex_groups = mesh_obj.vertex_groups
groups_to_remove = set()
for group in vertex_groups:
weights = get_vertex_weights(mesh_obj, group.name)
if not any(weight > threshold for weight in weights.values()):
groups_to_remove.add(group.name)
for group_name in groups_to_remove:
group = vertex_groups.get(group_name)
if group:
vertex_groups.remove(group)
def validate_results(self, context: Context) -> None:
"""Validate the results of standardization"""
valid, messages = validate_armature(self.armature)
if not valid:
raise ValueError("\n".join(messages))
def cleanup_constraints(self, context: Context) -> None:
"""Remove all constraints from the armature."""
bpy.ops.object.mode_set(mode='POSE')
for pose_bone in self.armature.pose.bones:
constraints_to_remove = [constraint for constraint in pose_bone.constraints]
for constraint in constraints_to_remove:
pose_bone.constraints.remove(constraint)
def fix_zero_length_bones(self, context: Context) -> None:
"""Fix zero-length bones by setting minimal length"""
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = self.armature.data.edit_bones
min_length = 0.01 # Minimum bone length in Blender units
for bone in edit_bones:
bone_length = (bone.tail - bone.head).length
if bone_length < min_length:
if bone.parent:
direction = bone.parent.tail - bone.parent.head
direction.normalize()
else:
direction = Vector((0, 0, 1))
bone.tail = bone.head + (direction * min_length)
class ReparentMeshesOperator(bpy.types.Operator):
bl_idname = "avatar_toolkit.reparent_meshes"
bl_label = t("MMD.reparent_meshes")
bl_description = t("MMD.reparent_meshes_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
armature = get_active_armature(context)
return armature is not None and get_all_meshes(context)
def execute(self, context):
armature = get_active_armature(context)
if not armature:
self.report({'ERROR'}, t("MMD.no_armature"))
return {'CANCELLED'}
meshes = get_all_meshes(context)
if not meshes:
self.report({'ERROR'}, t("MMD.no_meshes"))
return {'CANCELLED'}
try:
with ProgressTracker(context, len(meshes) + 1, "Reparenting Meshes") as progress:
# Get or create main collection
main_collection = self._get_main_collection(context)
progress.step("Setting up collections")
# Process each mesh
for mesh in meshes:
progress.step(f"Processing {mesh.name}")
self._process_mesh(mesh, armature, main_collection)
self.report({'INFO'}, t("MMD.reparenting_complete"))
return {'FINISHED'}
except Exception as e:
logger.error(f"Error reparenting meshes: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def _get_main_collection(self, context) -> bpy.types.Collection:
"""Get or create the main collection for the armature"""
if hasattr(context.scene, 'collection'):
return context.scene.collection
return context.scene.collection
def _process_mesh(self, mesh: bpy.types.Object,
armature: bpy.types.Object,
main_collection: bpy.types.Collection) -> None:
"""Process individual mesh parenting and collection management"""
# Unlink from other collections
for col in mesh.users_collection:
if col != main_collection:
col.objects.unlink(mesh)
# Ensure mesh is in main collection
if mesh.name not in main_collection.objects:
main_collection.objects.link(mesh)
# Set parent to armature
mesh.parent = armature
if not mesh.parent_type == 'ARMATURE':
mesh.parent_type = 'ARMATURE'
class AVATAR_TOOLKIT_OT_ConvertMmdMorphs(Operator):
"""Convert MMD morph data to shape keys"""
bl_idname = "avatar_toolkit.convert_mmd_morphs"
bl_label = t("MMD.convert_morphs")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
armature = get_active_armature(context)
return armature is not None and get_all_meshes(context)
def execute(self, context):
armature = get_active_armature(context)
if not armature:
self.report({'ERROR'}, t("MMD.no_armature"))
return {'CANCELLED'}
try:
with ProgressTracker(context, 3, "Converting MMD Morphs") as progress:
# Convert bone morphs to shape keys
if hasattr(armature, 'mmd_root') and armature.mmd_root.bone_morphs:
self.process_bone_morphs(context, armature, progress)
progress.step("Processed bone morphs")
# Clean up unused data
self.cleanup_unused_data(context)
progress.step("Cleaned up data")
# Validate results
self.validate_results(context)
progress.step("Validated results")
self.report({'INFO'}, t("MMD.conversion_complete"))
return {'FINISHED'}
except Exception as e:
logger.error(f"Error converting MMD morphs: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def process_bone_morphs(self, context, armature, progress):
"""Process bone morphs into shape keys"""
for morph in armature.mmd_root.bone_morphs:
for mesh in get_all_meshes(context):
# Create armature modifier
mod = mesh.modifiers.new(morph.name, 'ARMATURE')
mod.object = armature
# Apply as shape key
with context.temp_override(object=mesh):
bpy.ops.object.modifier_apply(modifier=mod.name)
class AVATAR_TOOLKIT_OT_CleanupMmdModel(Operator):
"""Clean up MMD model by removing unused data and fixing display settings"""
bl_idname = "avatar_toolkit.cleanup_mmd"
bl_label = t("MMD.cleanup")
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
armature = get_active_armature(context)
if not armature:
self.report({'ERROR'}, t("MMD.no_armature"))
return {'CANCELLED'}
try:
with ProgressTracker(context, 4, "Cleaning MMD Model") as progress:
# Remove rigid bodies and joints
self.remove_physics_objects(armature)
progress.step("Removed physics objects")
# Clean up collections and hierarchy
self.cleanup_hierarchy(context, armature)
progress.step("Cleaned hierarchy")
# Fix viewport settings
self.fix_viewport_settings(context)
progress.step("Fixed viewport")
# Final cleanup
clear_unused_data_blocks()
progress.step("Cleared unused data")
self.report({'INFO'}, t("MMD.cleanup_complete"))
return {'FINISHED'}
except Exception as e:
logger.error(f"Error cleaning MMD model: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def remove_physics_objects(self, armature):
"""Remove physics-related objects"""
to_delete = []
for child in armature.children:
if any(x in child.name.lower() for x in ['rigidbodies', 'joints', 'physics']):
to_delete.append(child)
for obj in to_delete:
bpy.data.objects.remove(obj, do_unlink=True)
def cleanup_hierarchy(self, context, armature):
"""Clean up object hierarchy and collections"""
meshes = get_all_meshes(context)
for mesh in meshes:
# Ensure proper parenting
mesh.parent = armature
mesh.parent_type = 'ARMATURE'
# Clean up collections
for col in mesh.users_collection:
if col != context.scene.collection:
col.objects.unlink(mesh)
if mesh.name not in context.scene.collection.objects:
context.scene.collection.objects.link(mesh)
def fix_viewport_settings(self, context):
"""Fix viewport display settings"""
# Set armature display
armature = get_active_armature(context)
armature.data.display_type = 'OCTAHEDRAL'
armature.show_in_front = True
# Set viewport shading
for area in context.screen.areas:
if area.type == 'VIEW_3D':
space = area.spaces[0]
space.shading.type = 'MATERIAL'
space.clip_start = 0.01
space.clip_end = 300
class AVATAR_TOOLKIT_OT_FixMeshes(Operator):
"""Clean up and optimize mesh materials, shading, and shape keys"""
bl_idname = "avatar_toolkit.fix_meshes"
bl_label = t("Optimization.fix_meshes")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
armature = get_active_armature(context)
return armature is not None and get_all_meshes(context)
def execute(self, context):
try:
meshes = get_all_meshes(context)
if not meshes:
self.report({'ERROR'}, t("Optimization.no_meshes"))
return {'CANCELLED'}
with ProgressTracker(context, len(meshes), "Fixing Meshes") as progress:
for mesh in meshes:
self.process_mesh(context, mesh)
progress.step(f"Processed {mesh.name}")
self.report({'INFO'}, t("Optimization.meshes_fixed"))
return {'FINISHED'}
except Exception as e:
logger.error(f"Error fixing meshes: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def process_mesh(self, context: Context, mesh: Object) -> None:
"""Process and fix individual mesh"""
# Unlock transforms
for i in range(3):
mesh.lock_location[i] = False
mesh.lock_rotation[i] = False
mesh.lock_scale[i] = False
# Process shape keys
if mesh.data.shape_keys:
self.fix_shape_keys(mesh)
# Process materials
self.fix_materials(context, mesh)
def fix_shape_keys(self, mesh: Object) -> None:
"""Fix and clean up shape keys"""
if not mesh.data.shape_keys:
return
shape_keys = mesh.data.shape_keys.key_blocks
# Rename basis
if shape_keys[0].name != "Basis":
shape_keys[0].name = "Basis"
# Clean up names
for key in shape_keys:
# Remove common prefixes/suffixes
clean_name = key.name
for prefix in ['Face.M F00 000 Fcl ', 'Face.M F00 000 00 Fcl ']:
clean_name = clean_name.replace(prefix, '')
# Replace underscores with spaces
clean_name = clean_name.replace('_', ' ')
key.name = clean_name
# Sort shape keys by category
categories = ['MTH', 'EYE', 'BRW', 'ALL']
# Create sorted list of shape key names
ordered_names = []
# Add categorized keys first
for category in categories:
category_keys = [key.name for key in shape_keys if key.name.startswith(category)]
ordered_names.extend(sorted(category_keys))
# Add remaining keys
remaining = [key.name for key in shape_keys if not any(key.name.startswith(c) for c in categories)]
ordered_names.extend(sorted(remaining))
# Reorder using context override
with bpy.context.temp_override(active_object=mesh, selected_objects=[mesh]):
for idx, name in enumerate(ordered_names):
mesh.active_shape_key_index = shape_keys.find(name)
while mesh.active_shape_key_index > idx:
bpy.ops.object.shape_key_move(type='UP')
def fix_materials(self, context: Context, mesh: Object) -> None:
"""Fix and optimize materials"""
for slot in mesh.material_slots:
if not slot.material:
continue
material = slot.material
# Set up basic material properties
material.use_backface_culling = True
material.blend_method = 'HASHED'
material.shadow_method = 'HASHED'
# Clean up material name
material.name = self.clean_material_name(material.name)
# Consolidate similar materials
for other_slot in mesh.material_slots:
if other_slot.material and other_slot.material != material:
if materials_match(material, other_slot.material):
other_slot.material = material
def clean_material_name(self, name: str) -> str:
"""Clean up material name"""
# Remove common prefixes/suffixes
prefixes = ['material', 'mat', 'mtl', 'material.']
for prefix in prefixes:
if name.lower().startswith(prefix):
name = name[len(prefix):]
# Remove numbers at end
while name and name[-1].isdigit():
name = name[:-1]
return name.strip()
class AVATAR_TOOLKIT_OT_ValidateMeshes(Operator):
"""Validate meshes and UV maps for common issues"""
bl_idname = "avatar_toolkit.validate_meshes"
bl_label = t("Validation.check_meshes")
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
armature = get_active_armature(context)
if not armature:
self.report({'ERROR'}, t("Validation.no_armature"))
return {'CANCELLED'}
try:
with ProgressTracker(context, 3, "Validating Meshes") as progress:
# Check bone hierarchy
hierarchy_issues = self.validate_bone_hierarchy(armature)
progress.step("Checked bone hierarchy")
# Check UV coordinates
uv_issues = self.validate_uv_maps(context)
progress.step("Checked UV maps")
# Generate report
self.generate_validation_report(context, hierarchy_issues, uv_issues)
progress.step("Generated report")
return {'FINISHED'}
except Exception as e:
logger.error(f"Error validating meshes: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def validate_bone_hierarchy(self, armature: Object) -> List[str]:
"""Validate bone hierarchy against standard structure"""
issues = []
# Define expected hierarchy
hierarchy = [
['hips', 'spine', 'chest', 'neck', 'head'],
['hips', 'left_leg', 'left_knee', 'left_ankle'],
['hips', 'right_leg', 'right_knee', 'right_ankle'],
['chest', 'left_shoulder', 'left_arm', 'left_elbow', 'left_wrist'],
['chest', 'right_shoulder', 'right_arm', 'right_elbow', 'right_wrist']
]
for chain in hierarchy:
previous = None
for bone_name in chain:
# Check if bone exists
bone = None
for alt_name in bone_names[bone_name]:
if alt_name in armature.data.bones:
bone = armature.data.bones[alt_name]
break
if not bone:
issues.append(t("Validation.missing_bone", bone=bone_name))
continue
# Check parent relationship
if previous:
if not bone.parent:
issues.append(t("Validation.no_parent", bone=bone.name))
elif bone.parent.name != previous.name:
issues.append(t("Validation.wrong_parent",
bone=bone.name,
expected=previous.name,
actual=bone.parent.name))
previous = bone
return issues
def validate_uv_maps(self, context: Context) -> Dict[str, int]:
"""Check UV maps for issues"""
issues = {'nan_coords': 0, 'missing_uvs': 0}
for mesh in get_all_meshes(context):
if not mesh.data.uv_layers:
issues['missing_uvs'] += 1
continue
for uv_layer in mesh.data.uv_layers:
for uv in uv_layer.data:
if math.isnan(uv.uv.x):
uv.uv.x = 0
issues['nan_coords'] += 1
if math.isnan(uv.uv.y):
uv.uv.y = 0
issues['nan_coords'] += 1
return issues
def generate_validation_report(self, context: Context,
hierarchy_issues: List[str],
uv_issues: Dict[str, int]) -> None:
"""Generate and display validation report"""
report_lines = []
# Add hierarchy issues
if hierarchy_issues:
report_lines.append(t("Validation.hierarchy_issues"))
report_lines.extend(hierarchy_issues)
# Add UV issues
if uv_issues['nan_coords'] > 0:
report_lines.append(t("Validation.uv_nan_coords",
count=uv_issues['nan_coords']))
if uv_issues['missing_uvs'] > 0:
report_lines.append(t("Validation.missing_uvs",
count=uv_issues['missing_uvs']))
# Show report
if report_lines:
self.report({'WARNING'}, "\n".join(report_lines))
else:
self.report({'INFO'}, t("Validation.no_issues"))
+10 -2
View File
@@ -14,10 +14,10 @@ from ...core.translations import t
from ...core.common import (
get_active_armature,
get_all_meshes,
validate_armature,
clear_unused_data_blocks,
ProgressTracker
)
from ...core.armature_validation import validate_armature
def textures_match(tex1: ShaderNodeTexImage, tex2: ShaderNodeTexImage) -> bool:
"""Compare two texture nodes for matching properties and image data"""
@@ -27,6 +27,11 @@ def consolidate_nodes(node1: ShaderNodeTexImage, node2: ShaderNodeTexImage) -> N
"""Transfer properties from one texture node to another to ensure consistency"""
node2.color_space = node1.color_space
node2.coordinates = node1.coordinates
# Add UV map synchronization
if node1.texture_mapping and node2.texture_mapping:
node2.texture_mapping.vector_type = node1.texture_mapping.vector_type
if hasattr(node1, "uv_map") and hasattr(node2, "uv_map"):
node2.uv_map = node1.uv_map
def consolidate_textures(node_tree1: NodeTree, node_tree2: NodeTree) -> None:
"""Synchronize texture nodes between two material node trees"""
@@ -81,10 +86,13 @@ class AvatarToolkit_OT_CombineMaterials(Operator):
@classmethod
def poll(cls, context: Context) -> bool:
"""Check if the operator can be executed"""
if context.mode != 'OBJECT':
return False
armature = get_active_armature(context)
if not armature:
return False
valid, _ = validate_armature(armature)
valid, _, _ = validate_armature(armature)
return valid
def execute(self, context: Context) -> Set[str]:
+3 -3
View File
@@ -6,11 +6,11 @@ from ...core.translations import t
from ...core.common import (
get_active_armature,
get_all_meshes,
validate_armature,
validate_meshes,
join_mesh_objects,
ProgressTracker
)
from ...core.armature_validation import validate_armature
class AvatarToolkit_OT_JoinAllMeshes(Operator):
"""Operator to join all meshes in the scene"""
@@ -25,7 +25,7 @@ class AvatarToolkit_OT_JoinAllMeshes(Operator):
if not armature:
return False
valid: bool
valid, _ = validate_armature(armature)
valid, _, _ = validate_armature(armature)
return valid
def execute(self, context: Context) -> Set[str]:
@@ -69,7 +69,7 @@ class AvatarToolkit_OT_JoinSelectedMeshes(Operator):
if not armature:
return False
valid: bool
valid, _ = validate_armature(armature)
valid, _, _ = validate_armature(armature)
return (valid and
context.mode == 'OBJECT' and
len([obj for obj in context.selected_objects if obj.type == 'MESH']) > 1)
+55 -22
View File
@@ -7,8 +7,8 @@ from ...core.translations import t
from ...core.common import (
get_active_armature,
get_all_meshes,
validate_armature
)
from ...core.armature_validation import validate_armature
# Constants
MERGE_ITERATION_COUNT = 20
@@ -54,6 +54,28 @@ def process_vertex_merging(mesh_data: bpy.types.Mesh, vertices_original: dict[in
return merged_vertices
def vertex_moves(mesh_data: bpy.types.Mesh, vertex: int) -> bool:
for shapekey in mesh_data.shape_keys.key_blocks:
data: bpy.types.ShapeKey = shapekey
if data.points[vertex].co.xyz != mesh_data.vertices[vertex].co.xyz:
return True
return False
def merge_vertex_at_index(mesh_data: bpy.types.Mesh, index: int, distance: float):
select_target_vertex = [False]*len(mesh_data.vertices)
select_target_vertex[index] = True
bpy.ops.object.mode_set(mode='OBJECT')
mesh_data.vertices.foreach_set("select",select_target_vertex)
bpy.ops.object.mode_set(mode='EDIT')
for _ in range(0,20): #for some reason, if using merge to unselected on a vertex, the vertex will only merge to 1 other vertex. so we gotta spam it to fix it.
bpy.ops.mesh.remove_doubles(threshold=distance, use_unselected=True, use_sharp_edge_from_normals=False)
bpy.ops.object.mode_set(mode='OBJECT')
class AvatarToolkit_OT_RemoveDoublesAdvanced(Operator):
bl_idname = "avatar_toolkit.remove_doubles_advanced"
bl_label = t("Optimization.remove_doubles_advanced")
@@ -66,7 +88,7 @@ class AvatarToolkit_OT_RemoveDoublesAdvanced(Operator):
armature = get_active_armature(context)
if not armature:
return False
valid, _ = validate_armature(armature)
valid, _, _ = validate_armature(armature)
return valid
def execute(self, context: Context) -> set[str]:
@@ -89,7 +111,7 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
armature = get_active_armature(context)
if not armature:
return False
valid, _ = validate_armature(armature)
valid, _, _ = validate_armature(armature)
return valid
def draw(self, context: Context) -> None:
@@ -168,7 +190,7 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
except Exception as e:
logger.error(f"Error in modify_mesh: {str(e)}")
def modify_mesh_advanced(self, context: Context, mesh_entry: MeshEntry) -> bool:
def modify_mesh_advanced(self, context: Context, mesh_entry: MeshEntry) -> int:
"""Advanced mesh modification with shape key handling"""
try:
final_merged_vertex_group = []
@@ -179,26 +201,28 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
duplicate = create_duplicate_for_merge(context, mesh_entry["mesh"], shapekey_name)
vertices_original = {i: v.co.xyz for i, v in enumerate(duplicate.data.vertices)}
merge_vertex_at_index(duplicate.data, mesh_entry["cur_vertex_pass"], merge_distance) #merge the vertex at our pass to find vertices that would merge to our vertex at this shapekey.
# Process merging
merged_vertices = process_vertex_merging(duplicate.data, vertices_original, mesh_entry["cur_vertex_pass"])
merged_vertices = process_vertex_merging(duplicate.data, vertices_original, mesh_entry["cur_vertex_pass"]) # find what vertices actually merged.
if not initialized_final:
final_merged_vertex_group = merged_vertices.copy()
initialized_final = True
else:
final_merged_vertex_group = [v for v in final_merged_vertex_group if v in merged_vertices]
final_merged_vertex_group = [v for v in final_merged_vertex_group if v in merged_vertices] # remove vertices that merged from the list if they didn't merge during this shapkey.
bpy.ops.object.delete()
# Apply final merging
if final_merged_vertex_group:
self.apply_final_merging(context, mesh_entry, final_merged_vertex_group, merge_distance)
self.apply_final_merging(context, mesh_entry, final_merged_vertex_group, merge_distance) # merge all vertices that merged on every shapekey no matter the shapekey during the loop.
return not (len(final_merged_vertex_group) > 1)
return len(final_merged_vertex_group)
except Exception as e:
logger.error(f"Error in modify_mesh_advanced: {str(e)}")
return True
return 1
def apply_final_merging(self, context: Context, mesh_entry: MeshEntry, vertex_group: list[int], merge_distance: float) -> None:
"""Apply final vertex merging operations"""
@@ -232,16 +256,14 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
def finish_mesh_processing(self, context: Context, mesh: MeshEntry, advanced: bool, merge_distance: float) -> None:
"""Complete the mesh processing by performing final merge operations"""
logger.debug("Finishing mesh processing")
mesh["mesh"].select_set(True)
context.view_layer.objects.active = mesh["mesh"]
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action="INVERT")
bpy.ops.mesh.remove_doubles(threshold=merge_distance, use_unselected=False)
if not advanced:
mesh["mesh"].select_set(True)
context.view_layer.objects.active = mesh["mesh"]
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action="INVERT")
bpy.ops.mesh.remove_doubles(threshold=merge_distance, use_unselected=False)
bpy.ops.object.mode_set(mode='OBJECT')
mesh["mesh"].select_set(False)
bpy.ops.object.mode_set(mode='OBJECT')
mesh["mesh"].select_set(False)
def modal(self, context: Context, event: Event) -> set[ModalReturnType]:
"""Modal operator execution"""
@@ -266,10 +288,21 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
self.process_simple_mesh(context, mesh, merge_distance)
self.objects_to_do.pop(0)
elif not (mesh["cur_vertex_pass"] > mesh["vertices"]) and advanced:
if self.modify_mesh_advanced(context, mesh):
elif not (mesh["cur_vertex_pass"] > mesh["vertices"]) and advanced: #advanced merging vertex by vertex
if(mesh["cur_vertex_pass"] < 0): #make sure it doesn't go below 0 and explode when advancing backwards from a previous step
mesh["cur_vertex_pass"] = 0
if vertex_moves(mesh["mesh"].data, mesh["cur_vertex_pass"]): # do not do advanced merging for vertices that don't move
mesh["cur_vertex_pass"] -= self.modify_mesh_advanced(context, mesh)-2 #advance forward or backwards based on how many vertices actually got merged, changing the list size.
#if above returns 1 (no vertices other than this one being merged to ourselves), advance by 1. else don't advance or go backwards. Makes sure all vertices get merged in the end.
else:
mesh["cur_vertex_pass"] += 1
elif (mesh["cur_vertex_pass"] > mesh["vertices"]) and advanced and len(mesh['shapekeys']) > 0: #after advanced merging has gone past all the moving vertices, now we need to merge non moving vertices.
shapekeyname = mesh['shapekeys'].pop(0)
mesh["mesh"].active_shape_key_index = mesh_data.shape_keys.key_blocks.find(shapekeyname)
logger.debug(f"Processing shapekey {shapekeyname}")
self.modify_mesh(context, mesh)
else:
self.finish_mesh_processing(context, mesh, advanced, merge_distance)
self.objects_to_do.pop(0)
+3 -3
View File
@@ -8,13 +8,13 @@ from ..core.common import (
get_active_armature,
get_all_meshes,
apply_pose_as_rest,
validate_armature,
cache_vertex_positions,
apply_vertex_positions,
validate_mesh_for_pose,
process_armature_modifiers,
ProgressTracker
)
from ..core.armature_validation import validate_armature
class BatchPoseOperationMixin:
"""Base class for batch pose operations"""
@@ -23,7 +23,7 @@ class BatchPoseOperationMixin:
armature = get_active_armature(context)
if not armature:
return False
valid, _ = validate_armature(armature)
valid, _, _ = validate_armature(armature)
return valid and context.mode == 'POSE'
def validate_meshes(self, meshes: List[Object]) -> List[Tuple[Object, str]]:
@@ -46,7 +46,7 @@ class AvatarToolkit_OT_StartPoseMode(Operator):
armature = get_active_armature(context)
if not armature or context.mode == "POSE":
return False
valid, _ = validate_armature(armature)
valid, _, _ = validate_armature(armature)
return valid
def execute(self, context: Context) -> Set[str]:
+6 -5
View File
@@ -4,7 +4,8 @@ from bpy.types import Operator, Context
from typing import Set
from ...core.translations import t
from ...core.logging_setup import logger
from ...core.common import get_active_armature, get_all_meshes, validate_armature, remove_unused_shapekeys
from ...core.common import get_active_armature, get_all_meshes, remove_unused_shapekeys
from ...core.armature_validation import validate_armature
class AvatarToolkit_OT_ApplyTransforms(Operator):
"""Apply all transformations to armature and associated meshes"""
@@ -18,8 +19,8 @@ class AvatarToolkit_OT_ApplyTransforms(Operator):
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return is_valid and context.mode == 'OBJECT'
valid, _, _ = validate_armature(armature)
return valid and context.mode == 'OBJECT'
def execute(self, context: Context) -> Set[str]:
try:
@@ -66,8 +67,8 @@ class AvatarToolkit_OT_CleanShapekeys(Operator):
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return is_valid and context.mode == 'OBJECT' and len(get_all_meshes(context)) > 0
valid, _, _ = validate_armature(armature)
return valid and context.mode == 'OBJECT' and len(get_all_meshes(context)) > 0
def execute(self, context: Context) -> Set[str]:
try:
+71 -16
View File
@@ -5,12 +5,11 @@ from typing import Optional, Dict, Any, List, Tuple
from ...core.translations import t
from ...core.common import (
get_active_armature,
validate_armature,
get_all_meshes,
ProgressTracker,
validate_bone_hierarchy,
restore_bone_transforms
)
from ...core.armature_validation import validate_armature, validate_bone_hierarchy
def duplicate_bone(bone: EditBone) -> EditBone:
"""Create a duplicate of the given bone"""
@@ -35,8 +34,8 @@ class AvatarToolKit_OT_CreateDigitigradeLegs(Operator):
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return (is_valid and
valid, _, _ = validate_armature(armature)
return (valid and
context.mode == 'EDIT_ARMATURE' and
context.selected_editable_bones is not None and
len(context.selected_editable_bones) == 2)
@@ -129,18 +128,16 @@ class AvatarToolKit_OT_DeleteBoneConstraints(Operator):
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return is_valid
valid, _, _ = validate_armature(armature)
return valid
def execute(self, context: Context) -> set[str]:
"""Execute the constraint removal operation"""
bpy.ops.object.mode_set(mode='OBJECT')
armature = get_active_armature(context)
# Select armature and make it active before changing mode
bpy.ops.object.select_all(action='DESELECT')
armature.select_set(True)
context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='POSE')
constraints_removed = 0
@@ -153,7 +150,6 @@ class AvatarToolKit_OT_DeleteBoneConstraints(Operator):
self.report({'INFO'}, t("Tools.clean_constraints_success", count=constraints_removed))
return {'FINISHED'}
class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
"""Operator to remove bones with no vertex weights"""
bl_idname = "avatar_toolkit.clean_weights"
@@ -163,10 +159,37 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
def should_preserve_bone(self, bone_name: str, context: Context) -> bool:
"""Check if bone should be preserved based on settings"""
if context.scene.avatar_toolkit.merge_twist_bones:
return "twist" in bone_name.lower()
toolkit = context.scene.avatar_toolkit
bone = context.active_object.data.bones.get(bone_name)
if not bone:
return False
if toolkit.preserve_parent_bones and bone.children:
return True
if toolkit.target_bone_type == 'DEFORM' and not bone.use_deform:
return True
if toolkit.target_bone_type == 'NON_DEFORM' and bone.use_deform:
return True
return False
def populate_bone_list(self, context: Context, zero_weight_bones: List[str]) -> None:
"""Populate the zero weight bones list"""
toolkit = context.scene.avatar_toolkit
toolkit.zero_weight_bones.clear()
armature = get_active_armature(context)
for bone_name in zero_weight_bones:
bone = armature.data.bones.get(bone_name)
if bone:
item = toolkit.zero_weight_bones.add()
item.name = bone_name
item.has_children = len(bone.children) > 0
item.is_deform = bone.use_deform
def execute(self, context: Context) -> set[str]:
"""Execute the zero weight bone removal operation"""
armature = get_active_armature(context)
@@ -188,6 +211,7 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
# Get weighted bones
weighted_bones: List[str] = []
meshes = get_all_meshes(context)
zero_weight_bones: List[str] = []
for mesh in meshes:
mesh_data: Mesh = mesh.data
@@ -205,6 +229,10 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
if (bone.name not in weighted_bones and
not self.should_preserve_bone(bone.name, context)):
if context.scene.avatar_toolkit.list_only_mode:
zero_weight_bones.append(bone.name)
continue
# Store children data
children = bone.children
children_data = {child.name: initial_transforms[child.name] for child in children}
@@ -223,11 +251,38 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
for child_name, data in children_data.items():
if child_name in armature_data.edit_bones:
child = armature_data.edit_bones[child_name]
child.head = data['head']
child.tail = data['tail']
child.roll = data['roll']
child.matrix = data['matrix']
restore_bone_transforms(child, data)
bpy.ops.object.mode_set(mode='OBJECT')
if context.scene.avatar_toolkit.list_only_mode:
self.populate_bone_list(context, zero_weight_bones)
return {'FINISHED'}
self.report({'INFO'}, t("Tools.clean_weights_success", count=removed_count))
return {'FINISHED'}
class AvatarToolKit_OT_RemoveSelectedBones(Operator):
"""Operator to remove selected bones from the zero weight bones list"""
bl_idname = "avatar_toolkit.remove_selected_bones"
bl_label = t("Tools.remove_selected_bones")
bl_description = t("Tools.remove_selected_bones_desc")
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context: Context) -> set[str]:
armature = get_active_armature(context)
toolkit = context.scene.avatar_toolkit
selected_bones = [item.name for item in toolkit.zero_weight_bones
if item.selected]
bpy.ops.object.mode_set(mode='EDIT')
for bone_name in selected_bones:
if bone_name in armature.data.edit_bones:
armature.data.edit_bones.remove(armature.data.edit_bones[bone_name])
bpy.ops.object.mode_set(mode='OBJECT')
toolkit.zero_weight_bones.clear()
self.report({'INFO'}, t("Tools.bones_removed", count=len(selected_bones)))
return {'FINISHED'}
-89
View File
@@ -1,89 +0,0 @@
import bpy
import re
from typing import Set, Dict, Optional
from bpy.types import Operator, Context
from ...core.translations import t
from ...core.logging_setup import logger
from ...core.common import get_active_armature, simplify_bonename, validate_armature, ProgressTracker
from ...core.dictionaries import bone_names, resonite_translations
class AvatarToolkit_OT_ConvertResonite(Operator):
"""Convert armature bone names to Resonite format with progress tracking and validation"""
bl_idname = "avatar_toolkit.convert_resonite"
bl_label = t("Tools.convert_resonite")
bl_description = t("Tools.convert_resonite_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return is_valid
def execute(self, context: Context) -> Set[str]:
armature = get_active_armature(context)
if not armature:
logger.warning("No armature selected for Resonite conversion")
self.report({'WARNING'}, t("Armature.validation.no_armature"))
return {'CANCELLED'}
translate_bone_fails: int = 0
untranslated_bones: Set[str] = set()
simplified_names: Dict[str, str] = {}
# Create reverse lookup dictionary
reverse_bone_lookup = {}
for preferred_name, name_list in bone_names.items():
for name in name_list:
reverse_bone_lookup[name] = preferred_name
try:
context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.mode_set(mode='OBJECT')
# Cache simplified bone names
for bone in armature.data.bones:
simplified_names[bone.name] = simplify_bonename(bone.name)
total_bones = len(armature.data.bones)
with ProgressTracker(context, total_bones, t("Tools.convert_resonite.operation")) as progress:
for bone in armature.data.bones:
# Remove any existing "<noik>" tags
bone.name = re.compile(re.escape("<noik>"), re.IGNORECASE).sub("", bone.name)
simplified_name = simplified_names[bone.name]
if simplified_name in reverse_bone_lookup and reverse_bone_lookup[simplified_name] in resonite_translations:
new_name = resonite_translations[reverse_bone_lookup[simplified_name]]
logger.debug(f"Translating bone: {bone.name} -> {new_name}")
bone.name = new_name
else:
untranslated_bones.add(bone.name)
bone.name = bone.name + "<noik>"
translate_bone_fails += 1
logger.debug(f"Failed to translate bone: {bone.name}")
progress.step(t("Tools.convert_resonite.processing", name=bone.name))
except Exception as e:
logger.error(f"Error during Resonite conversion: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
finally:
try:
bpy.ops.object.mode_set(mode='OBJECT')
except Exception as e:
logger.warning(f"Error returning to object mode: {str(e)}")
if translate_bone_fails > 0:
logger.info(f"Conversion completed with {translate_bone_fails} untranslated bones")
logger.debug(f"Untranslated bones: {untranslated_bones}")
self.report({'INFO'}, t("Tools.bones_translated_with_fails", translate_bone_fails=translate_bone_fails))
else:
logger.info("All bones translated successfully")
self.report({'INFO'}, t("Tools.bones_translated_success"))
return {'FINISHED'}
+4 -3
View File
@@ -4,7 +4,8 @@ from typing import Set, List
from bpy.types import Operator, Context, Armature, EditBone
from ...core.translations import t
from ...core.logging_setup import logger
from ...core.common import get_active_armature, get_all_meshes, get_vertex_weights, transfer_vertex_weights, validate_armature
from ...core.common import get_active_armature, get_all_meshes, get_vertex_weights, transfer_vertex_weights
from ...core.armature_validation import validate_armature
class AvatarToolkit_OT_ConnectBones(Operator):
"""Connect disconnected bones in chain"""
@@ -18,8 +19,8 @@ class AvatarToolkit_OT_ConnectBones(Operator):
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return is_valid
valid, _, _ = validate_armature(armature)
return valid
def execute(self, context: Context) -> Set[str]:
try:
+6 -5
View File
@@ -1,7 +1,8 @@
import bpy
from bpy.types import Operator, Context
from ...core.translations import t
from ...core.common import get_active_armature, validate_armature
from ...core.common import get_active_armature
from ...core.armature_validation import validate_armature
class AvatarToolKit_OT_SeparateByMaterials(Operator):
"""Operator to separate mesh by materials"""
@@ -16,10 +17,10 @@ class AvatarToolKit_OT_SeparateByMaterials(Operator):
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
valid, _, _ = validate_armature(armature)
return (context.active_object and
context.active_object.type == 'MESH' and
is_valid)
valid)
def execute(self, context: Context) -> set[str]:
"""Execute the separation operation"""
@@ -48,10 +49,10 @@ class AvatarToolKit_OT_SeparateByLooseParts(Operator):
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
valid, _, _ = validate_armature(armature)
return (context.active_object and
context.active_object.type == 'MESH' and
is_valid)
valid)
def execute(self, context: Context) -> set[str]:
"""Execute the separation operation"""
+256
View File
@@ -0,0 +1,256 @@
import bpy
from typing import Dict, List, Set, Optional, Tuple, Any
from bpy.types import Operator, Context, Object, PoseBone, EditBone, Bone, Constraint
from ...core.common import get_active_armature
from ...core.logging_setup import logger
from ...core.translations import t
from ...core.dictionaries import rigify_unity_names, rigify_basic_unity_names, rigify_unnecessary_bones
from ...core.armature_validation import validate_armature
class AvatarToolkit_OT_ConvertRigifyToUnity(Operator):
"""Convert Rigify armature to Unity-compatible format"""
bl_idname = "avatar_toolkit.convert_rigify_to_unity"
bl_label = t("Tools.convert_rigify_to_unity")
bl_description = t("Tools.convert_rigify_to_unity_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
if not armature:
return False
return ("DEF-spine" in armature.data.bones or
"spine" in armature.data.bones and "metarig" in armature.name.lower())
def execute(self, context: Context) -> Set[str]:
try:
logger.info("Starting Rigify to Unity conversion")
armature = get_active_armature(context)
if not armature:
logger.error("No armature found")
self.report({'ERROR'}, t("Tools.no_armature"))
return {'CANCELLED'}
logger.debug(f"Converting armature: {armature.name}")
armature.name = "Armature"
armature.data.name = "Armature"
logger.debug("Renamed armature to 'Armature'")
if "DEF-spine" in armature.data.bones:
logger.info("Processing DEF bones")
self.move_def_bones(armature)
self.rename_bones_for_unity(armature)
else:
logger.info("Processing basic bones")
self.cleanup_extra_bones(armature)
self.rename_basic_bones_for_unity(armature)
logger.debug("Cleaning up bone collections")
self.cleanup_bone_collections(armature)
if context.scene.avatar_toolkit.merge_twist_bones:
logger.info("Merging twist bones")
self.handle_twist_bones(armature)
logger.info("Successfully converted Rigify armature to Unity format")
self.report({'INFO'}, t("Tools.rigify_converted"))
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to convert Rigify: {str(e)}", exc_info=True)
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def cleanup_extra_bones(self, armature: Object) -> None:
"""Remove unnecessary bones and merge neck bones"""
logger.debug("Starting cleanup of extra bones")
# Set armature as active object before mode switch
bpy.context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='EDIT')
bones_to_remove: List[str] = []
for bone in armature.data.edit_bones:
if any(pattern in bone.name.lower() for pattern in rigify_unnecessary_bones):
bones_to_remove.append(bone.name)
for bone_name in bones_to_remove:
if bone_name in armature.data.edit_bones:
logger.debug(f"Removing bone: {bone_name}")
armature.data.edit_bones.remove(armature.data.edit_bones[bone_name])
if 'spine.004' in armature.data.edit_bones and 'spine.005' in armature.data.edit_bones:
logger.debug("Merging neck bones")
neck_start = armature.data.edit_bones['spine.004']
neck_end = armature.data.edit_bones['spine.005']
neck_start.tail = neck_end.tail
armature.data.edit_bones.remove(neck_end)
neck_start.name = "Neck"
if 'spine.006' in armature.data.edit_bones:
logger.debug("Renaming head bone")
head_bone = armature.data.edit_bones['spine.006']
head_bone.name = "Head"
def move_def_bones(self, armature: Object) -> None:
"""Move DEF bones to their correct positions"""
logger.debug("Moving DEF bones to correct positions")
# Set armature as active object
bpy.context.view_layer.objects.active = armature
remap: Dict[str, str] = self.get_org_remap(armature)
remap.update(self.get_special_remap())
remove_bones_in_chain: List[str] = [
'DEF-upper_arm.L.001', 'DEF-forearm.L.001',
'DEF-upper_arm.R.001', 'DEF-forearm.R.001',
'DEF-thigh.L.001', 'DEF-shin.L.001',
'DEF-thigh.R.001', 'DEF-shin.R.001'
]
transform_copies: List[str] = self.get_transform_copies(armature)
logger.debug("Setting up transform copies")
bpy.ops.object.mode_set(mode='POSE')
for bone_name in transform_copies:
bone = armature.pose.bones[bone_name]
org_name = 'ORG-' + self.get_proto_name(bone_name)
if org_name in armature.pose.bones:
constraint = bone.constraints.new('COPY_TRANSFORMS')
constraint.target = armature
constraint.subtarget = org_name
constr_count = len(bone.constraints)
if constr_count > 1:
bone.constraints.move(constr_count-1, 0)
logger.debug("Remapping bone parents")
bpy.ops.object.mode_set(mode='EDIT')
for remap_key in remap:
if remap_key in armature.data.edit_bones and remap[remap_key] in armature.data.edit_bones:
armature.data.edit_bones[remap_key].parent = armature.data.edit_bones[remap[remap_key]]
logger.debug("Processing bone chain removal")
bpy.ops.object.mode_set(mode='OBJECT')
for bone_name in remove_bones_in_chain:
if bone_name in armature.data.bones:
armature.data.bones[bone_name].use_deform = False
bpy.ops.object.mode_set(mode='EDIT')
for bone_name in remove_bones_in_chain:
if bone_name in armature.data.bones:
remove_bone = armature.data.edit_bones[bone_name]
parent_bone = remove_bone.parent
parent_bone.tail = remove_bone.tail
retarget_bones = list(remove_bone.children)
for bone in retarget_bones:
bone.parent = parent_bone
armature.data.edit_bones.remove(remove_bone)
def rename_bones_for_unity(self, armature: Object) -> None:
"""Rename bones to Unity-compatible names"""
logger.debug("Renaming bones to Unity format")
for old_name, new_name in rigify_unity_names.items():
bone = armature.pose.bones.get(old_name)
if bone:
logger.debug(f"Renaming bone: {old_name} -> {new_name}")
bone.name = new_name
def rename_basic_bones_for_unity(self, armature: Object) -> None:
"""Rename basic metarig bones to Unity-compatible names"""
logger.debug("Renaming basic metarig bones")
for old_name, new_name in rigify_basic_unity_names.items():
bone = armature.pose.bones.get(old_name)
if bone:
logger.debug(f"Renaming basic bone: {old_name} -> {new_name}")
bone.name = new_name
def cleanup_bone_collections(self, armature: Object) -> None:
"""Remove all bone collections since they're not needed for Unity"""
logger.debug("Cleaning up bone collections")
if hasattr(armature.data, 'collections') and armature.data.collections:
while len(armature.data.collections) > 0:
collection = armature.data.collections[0]
armature.data.collections.remove(collection)
while len(armature.data.collections) > 1:
collection = armature.data.collections[1]
armature.data.collections.remove(collection)
def handle_twist_bones(self, armature: Object) -> None:
"""Handle twist bones during conversion"""
logger.debug("Processing twist bones")
twist_bones: List[Tuple[str, str]] = [
("DEF-upper_arm_twist.L", "DEF-upper_arm.L"),
("DEF-upper_arm_twist.R", "DEF-upper_arm.R"),
("DEF-forearm_twist.L", "DEF-forearm.L"),
("DEF-forearm_twist.R", "DEF-forearm.R"),
("DEF-thigh_twist.L", "DEF-thigh.L"),
("DEF-thigh_twist.R", "DEF-thigh.R")
]
bpy.ops.object.mode_set(mode='EDIT')
for twist_bone, parent_bone in twist_bones:
if twist_bone in armature.data.edit_bones and parent_bone in armature.data.edit_bones:
logger.debug(f"Merging twist bone: {twist_bone} into {parent_bone}")
twist = armature.data.edit_bones[twist_bone]
parent = armature.data.edit_bones[parent_bone]
parent.tail = twist.tail
for child in twist.children:
child.parent = parent
armature.data.edit_bones.remove(twist)
bpy.ops.object.mode_set(mode='OBJECT')
def get_org_remap(self, armature: Object) -> Dict[str, str]:
"""Get original bone remapping"""
logger.debug("Getting original bone remapping")
remap: Dict[str, str] = {}
for bone in armature.data.bones:
if self.is_def_bone(bone.name):
name = self.get_proto_name(bone.name)
parent = bone.parent
while parent:
parent_name = self.get_proto_name(parent.name)
if parent_name != name:
if ('DEF-' + parent_name) in armature.data.bones:
remap[bone.name] = 'DEF-' + parent_name
break
parent = parent.parent
return remap
def get_special_remap(self) -> Dict[str, str]:
"""Get special bone remapping cases"""
logger.debug("Getting special bone remapping")
return {
'DEF-thigh.L': 'DEF-pelvis.L',
'DEF-thigh.R': 'DEF-pelvis.R',
'DEF-upper_arm.L': 'DEF-shoulder.L',
'DEF-upper_arm.R': 'DEF-shoulder.R',
}
def get_transform_copies(self, armature: Object) -> List[str]:
"""Get bones that need transform copies"""
logger.debug("Getting transform copy bones")
result: List[str] = []
for bone in armature.pose.bones:
if self.is_def_bone(bone.name) and not self.has_transform_copies(bone):
result.append(bone.name)
return result
def has_transform_copies(self, bone: PoseBone) -> bool:
"""Check if bone has transform copy constraints"""
return any(constraint.type == 'COPY_TRANSFORMS' for constraint in bone.constraints)
def is_def_bone(self, bone_name: str) -> bool:
"""Check if bone is a DEF bone"""
return bone_name.startswith('DEF-')
def is_org_bone(self, bone_name: str) -> bool:
"""Check if bone is an ORG bone"""
return bone_name.startswith('ORG-')
def get_proto_name(self, bone_name: str) -> str:
"""Get the prototype name of a bone"""
if self.is_def_bone(bone_name) or self.is_org_bone(bone_name):
return bone_name[4:]
return bone_name
+308
View File
@@ -0,0 +1,308 @@
import bpy
import math
from typing import Dict, List, Set, Tuple, Optional, Any, Union
from bpy.types import Operator, Context, Object, EditBone, Bone
from ...core.translations import t
from ...core.logging_setup import logger
from ...core.common import get_active_armature, ProgressTracker
from ...core.armature_validation import validate_armature
from ...core.dictionaries import (
standard_bones,
bone_names,
bone_hierarchy,
acceptable_bone_names,
acceptable_bone_hierarchy,
non_standard_mappings
)
class AvatarToolkit_OT_StandardizeArmature(Operator):
"""Standardize armature bone names and hierarchy to match Avatar Toolkit requirements"""
bl_idname: str = "avatar_toolkit.standardize_armature"
bl_label: str = t("Tools.standardize_armature")
bl_description: str = t("Tools.standardize_armature_desc")
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature: Optional[Object] = get_active_armature(context)
return armature is not None and context.mode in {'OBJECT', 'EDIT_ARMATURE'}
def invoke(self, context: Context, event: Any) -> Set[str]:
logger.debug("Invoking standardize armature dialog")
return context.window_manager.invoke_props_dialog(self)
def draw(self, context: Context) -> None:
layout = self.layout
toolkit = context.scene.avatar_toolkit
layout.prop(toolkit, "standardize_fix_names")
layout.prop(toolkit, "standardize_fix_hierarchy")
layout.prop(toolkit, "standardize_fix_scale")
layout.separator()
layout.label(text=t("Tools.standardize_warning"), icon='ERROR')
def execute(self, context: Context) -> Set[str]:
armature: Optional[Object] = get_active_armature(context)
toolkit = context.scene.avatar_toolkit
if not armature:
logger.warning("No active armature found for standardization")
self.report({'ERROR'}, t("Validation.no_armature"))
return {'CANCELLED'}
logger.info(f"Starting armature standardization for {armature.name}")
is_valid, _, _ = validate_armature(armature)
if is_valid:
logger.info("Armature already meets standards, no changes needed")
self.report({'INFO'}, t("Tools.standardize_already_valid"))
return {'FINISHED'}
original_mode: str = context.mode
logger.debug(f"Original mode: {original_mode}")
bpy.ops.object.mode_set(mode='OBJECT')
context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='EDIT')
try:
with ProgressTracker(context, 3, "Standardizing Armature") as progress:
# Step 1: Fix bone names
if toolkit.standardize_fix_names:
progress.step("Fixing bone names")
renamed_bones: Dict[str, str] = self.standardize_bone_names(armature)
logger.info(f"Renamed {len(renamed_bones)} bones")
for old_name, new_name in renamed_bones.items():
logger.debug(f"Renamed bone: {old_name} -> {new_name}")
# Step 2: Fix hierarchy
if toolkit.standardize_fix_hierarchy:
progress.step("Fixing bone hierarchy")
fixed_hierarchy: int = self.standardize_bone_hierarchy(armature)
logger.info(f"Fixed {fixed_hierarchy} hierarchy relationships")
# Step 3: Fix scale issues
if toolkit.standardize_fix_scale:
progress.step("Fixing bone scale")
fixed_scale: int = self.standardize_bone_scale(armature)
logger.info(f"Fixed {fixed_scale} scale issues")
bpy.ops.object.mode_set(mode='OBJECT')
is_valid, messages, _ = validate_armature(armature)
if is_valid:
logger.info("Armature successfully standardized")
self.report({'INFO'}, t("Tools.standardize_success"))
else:
logger.warning(f"Armature partially standardized. {len(messages)} issues remain")
bpy.ops.avatar_toolkit.standardize_issues_popup('INVOKE_DEFAULT')
self.report({'WARNING'}, t("Tools.standardize_partial"))
if original_mode == 'EDIT_ARMATURE':
bpy.ops.object.mode_set(mode='EDIT')
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to standardize armature: {str(e)}")
self.report({'ERROR'}, str(e))
try:
if original_mode == 'EDIT_ARMATURE':
bpy.ops.object.mode_set(mode='EDIT')
else:
bpy.ops.object.mode_set(mode='OBJECT')
except Exception as restore_error:
logger.error(f"Failed to restore original mode: {str(restore_error)}")
return {'CANCELLED'}
def standardize_bone_names(self, armature: Object) -> Dict[str, str]:
"""Rename bones to match standard naming conventions"""
logger.debug("Starting bone name standardization")
renamed_bones: Dict[str, str] = {}
edit_bones = armature.data.edit_bones
# First, check which standard bones already exist
existing_standard_bones: Set[str] = set()
for bone in edit_bones:
if bone.name in standard_bones.values():
existing_standard_bones.add(bone.name)
logger.debug(f"Found existing standard bone: {bone.name}")
# Build a mapping of non-standard bone names to standard names
name_mapping: Dict[str, str] = {}
for category, standard_name in standard_bones.items():
# Skip if this standard bone already exists
if standard_name in existing_standard_bones:
continue
# Get all variants for this category
if category in non_standard_mappings:
for variant in non_standard_mappings[category]:
name_mapping[variant.lower()] = standard_name
# First pass: identify bones to rename
bones_to_rename: Dict[str, str] = {}
for bone in edit_bones:
original_name: str = bone.name
# Skip if this is already a standard bone name
if original_name in standard_bones.values():
continue
simplified_name: str = original_name.lower().replace(' ', '').replace('_', '').replace('.', '')
# Check if this bone matches any known pattern
for variant, standard_name in name_mapping.items():
# More precise matching - exact match or with common separators
if (variant == simplified_name or
variant == original_name.lower() or
f"{variant}_" in simplified_name or
f"{variant}." in simplified_name):
if original_name != standard_name:
bones_to_rename[original_name] = standard_name
logger.debug(f"Identified bone to rename: {original_name} -> {standard_name}")
break
# Special case for spine/chest hierarchy
# If we don't have an upper chest, don't rename chest to upper chest because it will break hierarchy
has_chest: bool = False
has_upper_chest: bool = False
for bone_name in edit_bones.keys():
if bone_name == standard_bones['chest']:
has_chest = True
elif bone_name == standard_bones['upper_chest']:
has_upper_chest = True
# If we have a chest but no upper chest, don't rename anything to upper chest
if has_chest and not has_upper_chest:
for original_name, new_name in list(bones_to_rename.items()):
if new_name == standard_bones['upper_chest']:
logger.debug(f"Skipping upper chest rename for {original_name} as chest already exists")
del bones_to_rename[original_name]
# Second pass: rename bones (in reverse to avoid naming conflicts)
for original_name, new_name in sorted(bones_to_rename.items(), reverse=True):
if original_name in edit_bones:
temp_name: str = f"TEMP_{original_name}"
edit_bones[original_name].name = temp_name
renamed_bones[original_name] = new_name
logger.debug(f"Temporarily renamed: {original_name} -> {temp_name}")
# Third pass: apply final names
for original_name, new_name in renamed_bones.items():
temp_name: str = f"TEMP_{original_name}"
if temp_name in edit_bones:
edit_bones[temp_name].name = new_name
logger.debug(f"Applied final rename: {temp_name} -> {new_name}")
logger.info(f"Standardized {len(renamed_bones)} bone names")
return renamed_bones
def standardize_bone_hierarchy(self, armature: Object) -> int:
"""Fix bone hierarchy to match standard relationships"""
logger.debug("Starting bone hierarchy standardization")
edit_bones = armature.data.edit_bones
fixed_count: int = 0
# Build a mapping of standard bone names to their expected parents
hierarchy_map: Dict[str, str] = {}
for parent, child in bone_hierarchy:
if parent in edit_bones and child in edit_bones:
hierarchy_map[child] = parent
logger.debug(f"Found standard hierarchy: {parent} -> {child}")
for parent, child in acceptable_bone_hierarchy:
if parent in edit_bones and child in edit_bones:
# Only add if not already in the map
if child not in hierarchy_map:
hierarchy_map[child] = parent
logger.debug(f"Found acceptable hierarchy: {parent} -> {child}")
for child_name, parent_name in hierarchy_map.items():
if child_name in edit_bones and parent_name in edit_bones:
child_bone: EditBone = edit_bones[child_name]
parent_bone: EditBone = edit_bones[parent_name]
if child_bone.parent != parent_bone:
logger.debug(f"Fixing hierarchy: {child_name} parent was {child_bone.parent.name if child_bone.parent else 'None'}, setting to {parent_name}")
child_bone.parent = parent_bone
fixed_count += 1
logger.info(f"Fixed {fixed_count} bone hierarchy relationships")
return fixed_count
def standardize_bone_scale(self, armature: Object) -> int:
"""Fix bone scale issues by normalizing bone lengths"""
logger.debug("Starting bone scale standardization")
edit_bones = armature.data.edit_bones
fixed_count: int = 0
# Calculate median bone length for reference
lengths: List[float] = [bone.length for bone in edit_bones if bone.length > 0.0001]
if not lengths:
logger.warning("No valid bone lengths found for scale standardization")
return 0
lengths.sort()
median_length: float = lengths[len(lengths) // 2]
logger.debug(f"Median bone length: {median_length}")
# Calculate mean and standard deviation
mean: float = sum(lengths) / len(lengths)
variance: float = sum((l - mean) ** 2 for l in lengths) / len(lengths)
std_dev: float = math.sqrt(variance)
logger.debug(f"Mean bone length: {mean}, Standard deviation: {std_dev}")
small_threshold: float = max(median_length * 0.05, mean - 3 * std_dev)
large_threshold: float = min(median_length * 15, mean + 5 * std_dev)
logger.debug(f"Scale thresholds - small: {small_threshold}, large: {large_threshold}")
for bone in edit_bones:
is_finger: bool = any(finger in bone.name.lower() for finger in ['thumb', 'index', 'middle', 'ring', 'pinky', 'finger'])
if bone.length < small_threshold and not is_finger:
old_length: float = bone.length
bone.length = small_threshold
logger.debug(f"Fixed small bone {bone.name}: {old_length} -> {bone.length}")
fixed_count += 1
elif bone.length > large_threshold:
old_length: float = bone.length
bone.length = large_threshold
logger.debug(f"Fixed large bone {bone.name}: {old_length} -> {bone.length}")
fixed_count += 1
logger.info(f"Fixed {fixed_count} bone scale issues")
return fixed_count
class AvatarToolkit_OT_StandardizeIssuesPopup(Operator):
"""Display information about remaining issues after standardization"""
bl_idname: str = "avatar_toolkit.standardize_issues_popup"
bl_label: str = t("Tools.standardize_issues_title")
bl_options: Set[str] = {'INTERNAL'}
def execute(self, context: Context) -> Set[str]:
return {'FINISHED'}
def invoke(self, context: Context, event: Any) -> Set[str]:
logger.debug("Showing standardization issues popup")
return context.window_manager.invoke_props_dialog(self, width=400)
def draw(self, context: Context) -> None:
layout = self.layout
col = layout.column(align=True)
col.label(text=t("Tools.standardize_issues_header"), icon='INFO')
col.separator()
col.label(text=t("Tools.standardize_issues_line1"))
col.label(text=t("Tools.standardize_issues_line2"))
col.label(text=t("Tools.standardize_issues_line3"))
col.separator()
col.label(text=t("Tools.standardize_issues_line4"))
col.label(text=t("Tools.standardize_issues_line5"))
col.separator()
col.label(text=t("Tools.standardize_issues_line6"))
+253
View File
@@ -0,0 +1,253 @@
from typing import TypedDict, Set, Dict, List, Optional, Any, Tuple
import bpy
from bpy.types import Operator, Object, Context, Mesh, MeshUVLoopLayer
import bmesh
import numpy as np
import math
from ...core.translations import t
from ...core.logging_setup import logger
class GenerateLoopTreeResult(TypedDict):
tree: Dict[str, Set[str]]
selected_loops: Dict[str, List[int]]
selected_verts: Dict[str, int]
class AvatarToolkit_OT_AlignUVEdgesToTarget(Operator):
"""Operator to align selected UV edges to target edge"""
bl_idname = "avatar_toolkit.align_uv_edges_to_target"
bl_label = t("UVTools.align_edges")
bl_description = t("UVTools.align_edges_desc")
bl_options = {'REGISTER', 'UNDO'}
#all selected objects need to be meshes for this to work - @989onan
@classmethod
def poll(cls, context: Context) -> bool:
if not ((context.view_layer.objects.active is not None) and (len(context.view_layer.objects.selected) > 0)):
return False
if context.mode != "EDIT_MESH":
return False
for obj in context.view_layer.objects.selected:
if obj.type != "MESH":
return False
if not context.space_data:
return False
if not context.space_data.show_uvedit:
return False
if context.scene.tool_settings.use_uv_select_sync:
return False
return True
def execute(self, context: Context) -> Set[str]:
target: str = context.view_layer.objects.active.name #The object which we want to align every other selected object's selected UV vertex line to
sources: List[str] = [i.name for i in context.view_layer.objects.selected] #The objects which we want to align their selected UV lines to the target's UV line
prev_mode: str = bpy.context.object.mode
bpy.ops.object.mode_set(mode='OBJECT')
def generate_loop_tree(obj_name: str) -> GenerateLoopTreeResult:
logger.debug(f"Finding selected line for: {obj_name}")
vert_target_loops: Dict[str, List[int]] = {}
vert_target_verts: Dict[str, int] = {}
me: Mesh = bpy.data.objects[obj_name].data
uv_lay: MeshUVLoopLayer = me.uv_layers.active
bm: bmesh.types.BMesh = bmesh.new()
bm.from_mesh(me)
bm.verts.ensure_lookup_table()
# To explain:
# So loops in UV maps are X polygons that make up a face (So a MeshLoop represent a face and each vertex on that face is in order)
#
# For some preknowledge:
# When a mesh is UV unwrapped, if a vertice is shared by two different faces on the model in the viewport and the vertice of both faces are in
# the same position on the UV map, then it considers it one point and the user can move it
# (is why the uv map doesn't split apart when you try to move a vertex because that would be annoying)
#
# The problem:
# The problem is that the data for whether the uv corners of two faces that share a vertex physically being connected and selected as one vertex on the uv map does not exist
# Though thankfully, blender forcibly (whether you like it or not) merges vertices of a uv map if the vertex of two different faces are actually shared in the UI,
# allowing for the moving of vertices of 4 faces connected by a single vertex. Behavior every normal blender user is familiar with.
#
# The solution
# We can use this to our advantage, by finding vertices on the uv map that share the same coridinate as another vertex that is also selected.
# that way we can group each pair shared in a line as the same vertex, and identify the line using these pairs and using the data that says for certain
# that two vertices share the same face loop, and therefore are connected.
#hmmm real stupid grimlin hours with this one. Using a string as the index of a dictionary of loop corners that end up on the same coordinate
for k,i in enumerate(uv_lay.vertex_selection):
if (i.value == True) and (bm.verts[me.loops[k].vertex_index].select == True) and (bm.verts[me.loops[k].vertex_index].hide == False):
key = np.array(uv_lay.uv[k].vector[:])
key = key.round(decimals=5)
if str(key) not in vert_target_loops:
vert_target_loops[str(key)] = []
vert_target_loops[str(key)].append(k)
vert_target_verts[str(key)] = me.loops[k].vertex_index
if len(vert_target_loops) > 4000:
self.report({'WARNING'}, t("UVTools.too_many_vertices"))
return {"tree": {}, "selected_loops": {}, "selected_verts": {}}
logger.debug(f"Finding connections on line for {obj_name}")
me.validate()
bm = bmesh.new()
bm.from_mesh(me)
tree: Dict[str, Set[str]] = {}
selected_verts = np.hstack(list(vert_target_loops.values()))
bm.verts.ensure_lookup_table()
for uvcoordsstr in vert_target_loops:
uv_lay = me.uv_layers.active
#before this section, each vert_target_loops is just groupings of vertices that share coordinates.
# Using the data that determines UV face corners (uvloops) that are associated with the real vertex,
# and the uv face corners (loops) that are on the same faces as the vertices that share coordinates in
# vert_target_loops, we can now identify them
#TL;DR: pairs of vertices that share cooridinates (chain links) find their buddies (make chain connected)
# Someone explain this better than me if you can please - @989onan
extension_loops = []
loops = bm.verts[vert_target_verts[uvcoordsstr]].link_loops
loops_indexes = [i.index for i in loops]
for loop in vert_target_loops[uvcoordsstr]:
if loop in loops_indexes:
loop_obj = loops[loops_indexes.index(loop)]
extension_loops.append(loop_obj.link_loop_next.index)
extension_loops.append(loop_obj.link_loop_prev.index)
#make a tree out of the vertices we identified as sharing faces with the vertices in vert_target_loops, and then link them together in a dictionary.
#the order of this dictionary is unknown.
# Someone explain this better than me if you can please - @989onan
tree[uvcoordsstr] = set()
for i in extension_loops:
if i in selected_verts:
key = np.array(uv_lay.uv[i].vector[:])
key = key.round(decimals=5)
tree[uvcoordsstr].add(str(key))
if uvcoordsstr in tree:
if len(tree[uvcoordsstr]) > 2:
self.report({'WARNING'}, t("UVTools.need_line", obj=obj_name))
return {"tree": {}, "selected_loops": {}, "selected_verts": {}}
uv_lay = me.uv_layers.active
for uvcoordstr in vert_target_loops:
for loop in vert_target_loops[uvcoordstr]:
uv_lay.vertex_selection[loop].value = True
bm.free()
me.validate()
logger.debug(f"Found UV line connections for {obj_name}")
return {"tree": tree, "selected_loops": vert_target_loops, "selected_verts": vert_target_verts}
def sort_uv_tree(originaltree: Dict[str, Set[str]], obj_name: str) -> List[str]:
sortedtree: Dict[str, Set[str]] = originaltree.copy()
startpoints: List[str] = []
for i in sortedtree:
if len(sortedtree[i]) < 2:
startpoints.append(i)
if len(startpoints) != 2:
self.report({'WARNING'}, t("UVTools.need_line", obj=obj_name))
return []
uvcoords1 = [float(x) for x in startpoints[0].replace("[","").replace("]","").split()]
uvcoords2 = [float(x) for x in startpoints[1].replace("[","").replace("]","").split()]
cursor = context.space_data.cursor_location
startpoint = startpoints[0] if math.sqrt((uvcoords1[0] - cursor[0])**2 + (uvcoords1[1] - cursor[1])**2) > math.sqrt((uvcoords2[0] - cursor[0])**2 + (uvcoords2[1] - cursor[1])**2) else startpoints[1]
#Wew my first actual recursive sort! - @989onan
def recursive_sort_uv_tree(point: str, sortedfinal: List[str]) -> List[str]:
#print("appending "+point)
sortedfinal.append(point)
new_point: str = ""
for i in sortedtree:
if point in sortedtree[i]:
new_point = i
removed_value = sortedtree.pop(i)
#print(removed_value)
break
if new_point == "":
logger.debug("Sorting complete, remaining tree:")
logger.debug(sortedtree)
return sortedfinal
return recursive_sort_uv_tree(new_point, sortedfinal)
sortedtree.pop(startpoint)
return recursive_sort_uv_tree(startpoint, [])
def lerp(v0: float, v1: float, t: float) -> float:
return v0 + t * (v1 - v0)
target_data = generate_loop_tree(target)
sorted_target_tree = sort_uv_tree(target_data["tree"], target)
logger.debug("Sorted target tree")
for source in sources:
if source == target:
continue
try:
source_data = generate_loop_tree(source)
sorted_source_tree = sort_uv_tree(source_data["tree"], source)
logger.debug(f"Sorted source {source}")
vertex_factor = float(len(sorted_target_tree)-1) / float(len(sorted_source_tree)-1)
logger.debug(f"Vertex factor: {vertex_factor}")
for k, i in enumerate(sorted_source_tree):
try:
#find where we are on the target edges, to interpolate the current point we're placing along the target point's line.
progress_along_edge = float(k) * vertex_factor
previous_vertex_index = math.floor(progress_along_edge)
next_vertex_index = math.ceil(progress_along_edge)
#find the uv coordinates of the previous and next points on the target uv line.
previous_point = [float(x) for x in sorted_target_tree[previous_vertex_index].replace("[","").replace("]","").split()]
next_point = [float(x) for x in sorted_target_tree[next_vertex_index].replace("[","").replace("]","").split()]
#create a point between these two values that represents a decimal 0-1 going where we are to where we are going between the two current points on the edge we are targeting this whole shebang with.
progress_between_points = progress_along_edge - int(progress_along_edge)
lerped_point = [
lerp(previous_point[0], next_point[0], progress_between_points),
lerp(previous_point[1], next_point[1], progress_between_points)
]
#grab our uv face corners for each uv coord that we saved.
#Since each face is considered separate internally, we have to treat each connected face to a vertex in a uv map as separate entities/vertexes.
#basically pretend they are split apart.
uv_face_corners = source_data["selected_loops"][i]
me = bpy.data.objects[source].data
me.validate()
bm = bmesh.new()
bm.from_mesh(me)
uv_lay = me.uv_layers.active
bm.verts.ensure_lookup_table()
for corner in uv_face_corners:
uv_lay.uv[corner].vector = lerped_point
except:
#This is probably fine? - @989onan
#TODO: What happened here? The magic of making code so complex you forget if this is even an issue. - @989onan
pass
logger.info(f"Finished mesh {source} for UV's")
except Exception as e:
logger.error(f"Error processing source {source}: {str(e)}")
return {'CANCELLED'}
bpy.ops.object.mode_set(mode=prev_mode)
return {'FINISHED'}
+62 -29
View File
@@ -1,39 +1,41 @@
# MIT License
# This code was taken from Cats Blender Plugin Unoffical, some of this code is by the original developers, however was improved by myself.
# Didn't think it was necessary to re-make something that works well.
import bpy
from typing import Dict, List, Optional, Tuple, Any, Set
from typing import Dict, List, Optional, Tuple, Any, Set, Union
from bpy.types import Operator, Context, Object, ShapeKey
from collections import OrderedDict
from ..core.logging_setup import logger
from ..core.translations import t
from ..core.common import (
get_active_armature,
validate_armature,
get_all_meshes,
validate_mesh_for_pose
)
from ..core.armature_validation import validate_armature
class VisemeCache:
"""Caches generated viseme shape data"""
_cache: Dict = {}
"""Manages caching of generated viseme shape data for performance optimization"""
_cache: Dict[Tuple[str, Tuple[Tuple]], List] = {}
@classmethod
def get_cached_shape(cls, key: str, mix_data: List) -> Optional[List]:
def get_cached_shape(cls, key: str, mix_data: List[List[Union[str, float]]]) -> Optional[List]:
"""Retrieves cached shape data for a given viseme key and mix configuration"""
cache_key = (key, tuple(tuple(x) for x in mix_data))
return cls._cache.get(cache_key)
@classmethod
def cache_shape(cls, key: str, mix_data: List, shape_data: List) -> None:
def cache_shape(cls, key: str, mix_data: List[List[Union[str, float]]], shape_data: List) -> None:
"""Stores shape data in cache for future retrieval"""
cache_key = (key, tuple(tuple(x) for x in mix_data))
cls._cache[cache_key] = shape_data
class VisemePreview:
"""Handles viseme preview functionality"""
_preview_data: Dict = {}
"""Controls real-time preview functionality for viseme shapes"""
_preview_data: Dict[str, float] = {}
_active: bool = False
_preview_shapes: Optional[OrderedDict] = None
_mesh_name: str = ""
@classmethod
def start_preview(cls, context: Context, mesh: Object, shapes: List[str]) -> bool:
@@ -42,6 +44,7 @@ class VisemePreview:
cls._active = True
cls._preview_data = {}
cls._mesh_name = mesh.name
# Store original values
for shape_key in mesh.data.shape_keys.key_blocks:
@@ -78,7 +81,11 @@ class VisemePreview:
if not cls._active or not cls._preview_shapes:
return
mesh = context.active_object
# Get the mesh by name instead of using active object
mesh = bpy.data.objects.get(cls._mesh_name)
if not mesh:
return
props = context.scene.avatar_toolkit
viseme_data = cls._preview_shapes.get(props.viseme_preview_selection)
if viseme_data:
@@ -115,30 +122,40 @@ class VisemePreview:
cls._active = False
cls._preview_data.clear()
cls._preview_shapes = None
cls._mesh_name = ""
class ATOOLKIT_OT_preview_visemes(Operator):
bl_idname = "avatar_toolkit.preview_visemes"
bl_label = t("Visemes.preview_label")
bl_description = t("Visemes.preview_desc")
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
"""Operator for previewing viseme shapes in real-time"""
bl_idname: str = "avatar_toolkit.preview_visemes"
bl_label: str = t("Visemes.preview_label")
bl_description: str = t("Visemes.preview_desc")
bl_options: Set[str] = {'REGISTER', 'UNDO', 'INTERNAL'}
@classmethod
def poll(cls, context: Context) -> bool:
if context.mode != 'OBJECT':
return False
# Get mesh from UI selection
props = context.scene.avatar_toolkit
mesh_obj = bpy.data.objects.get(props.viseme_mesh)
# Validate armature and mesh
armature = get_active_armature(context)
if not armature:
return False
valid, _ = validate_armature(armature)
return valid and context.active_object and context.active_object.type == 'MESH'
valid, _, _ = validate_armature(armature)
return valid and mesh_obj and mesh_obj.type == 'MESH'
def execute(self, context: Context) -> Set[str]:
props = context.scene.avatar_toolkit
mesh = context.active_object
mesh = bpy.data.objects.get(props.viseme_mesh)
if props.viseme_preview_mode:
VisemePreview.end_preview(mesh)
props.viseme_preview_mode = False
else:
if not mesh.data.shape_keys:
if not mesh or not mesh.data.shape_keys:
self.report({'ERROR'}, t("Visemes.error.no_shapekeys"))
return {'CANCELLED'}
@@ -165,24 +182,34 @@ def validate_deformation(mesh, mix_data):
return max_deform < (mesh_size * 0.4)
class ATOOLKIT_OT_create_visemes(Operator):
bl_idname = "avatar_toolkit.create_visemes"
bl_label = t("Visemes.create_label")
bl_description = t("Visemes.create_desc")
bl_options = {'REGISTER', 'UNDO'}
"""Operator for generating VRChat-compatible viseme shape keys"""
bl_idname: str = "avatar_toolkit.create_visemes"
bl_label: str = t("Visemes.create_label")
bl_description: str = t("Visemes.create_desc")
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
# Check if we're in object mode
if context.mode != 'OBJECT':
return False
# Get mesh from UI selection
props = context.scene.avatar_toolkit
mesh_obj = bpy.data.objects.get(props.viseme_mesh)
# Validate armature and mesh
armature = get_active_armature(context)
if not armature:
return False
valid, _ = validate_armature(armature)
return valid and context.active_object and context.active_object.type == 'MESH'
valid, _, _ = validate_armature(armature)
return valid and mesh_obj and mesh_obj.type == 'MESH'
def execute(self, context: Context) -> Set[str]:
props = context.scene.avatar_toolkit
mesh = context.active_object
mesh = bpy.data.objects.get(props.viseme_mesh) # Changed from context.active_object
if not mesh.data.shape_keys:
if not mesh or not mesh.data.shape_keys:
self.report({'ERROR'}, t("Visemes.error.no_shapekeys"))
return {'CANCELLED'}
@@ -257,7 +284,7 @@ class ATOOLKIT_OT_create_visemes(Operator):
continue
# Create new shape key
self.mix_shapekey(context, renamed_shapes, data['mix'], key)
self.mix_shapekey(context, renamed_shapes, data['mix'], key, mesh) # Added mesh parameter
# Cache the new shape key data
shape_data = [v.co.copy() for v in mesh.data.shape_keys.key_blocks[key].data]
@@ -270,14 +297,16 @@ class ATOOLKIT_OT_create_visemes(Operator):
mesh.active_shape_key_index = 0
wm.progress_end()
def mix_shapekey(self, context: Context, shapes: List[str], mix_data: List, new_name: str) -> None:
def mix_shapekey(self, context: Context, shapes: List[str], mix_data: List, new_name: str, mesh: Object) -> None: # Added mesh parameter
"""Creates a new shape key by mixing existing ones"""
mesh = context.active_object
# Remove existing shape key if it exists
if new_name in mesh.data.shape_keys.key_blocks:
mesh.active_shape_key_index = mesh.data.shape_keys.key_blocks.find(new_name)
old_active = context.view_layer.objects.active
context.view_layer.objects.active = mesh
bpy.ops.object.shape_key_remove()
context.view_layer.objects.active = old_active
# Reset all shape keys
for shapekey in mesh.data.shape_keys.key_blocks:
@@ -290,7 +319,10 @@ class ATOOLKIT_OT_create_visemes(Operator):
shapekey.value = value
# Create mixed shape key
old_active = context.view_layer.objects.active
context.view_layer.objects.active = mesh
mesh.shape_key_add(name=new_name, from_mix=True)
context.view_layer.objects.active = old_active
# Reset values and restore shape key settings
for shapekey in mesh.data.shape_keys.key_blocks:
@@ -333,3 +365,4 @@ class ATOOLKIT_OT_create_visemes(Operator):
props.mouth_a = current_names[0]
props.mouth_o = current_names[1]
props.mouth_ch = current_names[2]
+143 -21
View File
@@ -1,7 +1,7 @@
{
"authors": ["Avatar Toolkit Team"],
"messages": {
"AvatarToolkit.label": "Avatar Toolkit (Alpha 0.1.0)",
"AvatarToolkit.label": "Avatar Toolkit (Alpha 0.2.1)",
"AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there",
"AvatarToolkit.desc2": "will be issues, if you find any issues,",
"AvatarToolkit.desc3": "please report it on our Github.",
@@ -50,6 +50,7 @@
"QuickAccess.validation_basic_details": "Only essential bone structure is being validated",
"QuickAccess.validation_none_warning": "Validation Disabled",
"QuickAccess.validation_none_details": "No armature validation checks are being performed",
"Quick_Access.import_success": "Import successful",
"PoseMode.error.start": "Failed to start pose mode: {error}",
"PoseMode.error.stop": "Failed to stop pose mode: {error}",
@@ -69,6 +70,46 @@
"Armature.validation.invalid_hierarchy": "Invalid bone hierarchy between {parent} and {child}",
"Armature.validation.asymmetric_bones": "Missing symmetric bones for {bone}",
"Armature.validation.asymmetric_hand_wrist": "Missing symmetric bones for hands/wrists",
"Armature.validation.found_bones": "Found bones in armature:\n- {bones}",
"Armature.validation.non_standard_bones": "Non-standard bones found:\n- {bones}",
"Armature.validation.accessory_bones_note.line1": "If you have hair bones, skirt bones, or other",
"Armature.validation.accessory_bones_note.line2": "accessorybones named similarly to main armature",
"Armature.validation.accessory_bones_note.line3": "bones (e.g., Head1, Head2), please rename them to",
"Armature.validation.accessory_bones_note.line4": "more descriptive names like Hair_1, Skirt_1.",
"Armature.validation.standardize_note.line1": "You can standardize your armature",
"Armature.validation.standardize_note.line2": "automatically by using the 'Standardize Armature'",
"Armature.validation.standardize_note.line3": "button in the Tools section.",
"Validation.section.found_bones": "Found Bones",
"Validation.section.non_standard": "Non-Standard Bones",
"Validation.section.hierarchy": "Hierarchy Issues",
"Validation.status.failed": "Validation has failed",
"Validation.message.failed.line1": "Armature validation has failed",
"Validation.message.failed.line2": "Please check below what the",
"Validation.message.failed.line3": "issues are",
"Validation.highlight_problem_bones_desc": "Visually highlight bones that have validation issues in the viewport",
"Validation.no_armature": "No armature selected",
"Validation.no_issues": "No validation issues found to highlight",
"Validation.highlighting_complete": "Problem bones highlighted successfully",
"Validation.tpose.no_armature": "No armature found for T-pose validation",
"Validation.tpose.left_arm_not_horizontal": "Left arm is not in a horizontal T-pose position",
"Validation.tpose.right_arm_not_horizontal": "Right arm is not in a horizontal T-pose position",
"Validation.tpose.spine_not_vertical": "Spine is not in a vertical position",
"Validation.tpose.warning": "T-Pose Validation Warning",
"Validation.tpose.recommendation": "We recommend fixing the T-pose before importing into Unity or other platforms",
"Validation.scale_issues": "Bones with abnormal scale detected:",
"Validation.scale_issue.too_small": "Bone is extremely small",
"Validation.scale_issue.too_large": "Bone is extremely large",
"Validation.section.scale_issues": "Scale Issues",
"Validation.tpose.label": "Validate T-Pose",
"Validation.no_scale_issues": "No scale issues detected",
"Validation.no_hierarchy_issues": "No hierarchy issues detected",
"Validation.no_non_standard_issues": "No non-standard bone issues detected",
"Validation.tpose.valid": "T-Pose validation passed successfully",
"Validation.tpose.desc": "Check if armature is in a proper T-pose",
"Validation.highlight_problem_bones": "Highlight Problem Bones",
"Validation.clear_bone_highlighting": "Clear Bone Highlighting",
"Validation.clear_bone_highlighting_desc": "Remove bone highlighting and reset bone colors to default",
"Validation.highlighting_cleared": "Bone highlighting cleared successfully",
"Mesh.validation.no_data": "No mesh data",
"Mesh.validation.no_vertex_groups": "No vertex groups found",
@@ -127,6 +168,7 @@
"Tools.label": "Tools",
"Tools.general_title": "General Tools",
"Tools.select_armature": "Select an Armature",
"Tools.convert_resonite": "Convert to Resonite",
"Tools.convert_resonite_desc": "Convert model for use in Resonite",
"Tools.convert_resonite.operation": "Converting to Resonite",
@@ -149,6 +191,19 @@
"Tools.merge_twist_bones_desc": "When checked, twist bones will be kept, even if there are zero-weight",
"Tools.clean_weights": "Remove Zero Weight Bones",
"Tools.clean_weights_desc": "Remove bones with no vertex weights",
"Tools.preserve_parent_bones": "Preserve Parent Bones",
"Tools.preserve_parent_bones_desc": "Keep bones that have children even if they have no weights",
"Tools.target_bone_type": "Target Bone Type",
"Tools.target_bone_type_desc": "Filter which types of bones to process",
"Tools.target_all_bones": "All Bones",
"Tools.target_deform_bones": "Deform Bones Only",
"Tools.target_non_deform_bones": "Non-Deform Bones Only",
"Tools.list_only_mode": "List Mode Only",
"Tools.list_only_mode_desc": "List zero weight bones instead of removing them",
"Tools.zero_weight_bones_found": "Zero weight bones found: {bones}",
"Tools.remove_selected_bones": "Remove Selected Bones",
"Tools.remove_selected_bones_desc": "Remove selected zero weight bones from armature",
"Tools.bones_removed": "Removed {count} bones",
"Tools.clean_constraints": "Delete Bone Constraints",
"Tools.clean_constraints_desc": "Remove all bone constraints from armature",
"Tools.clean_constraints_success": "Removed {count} bone constraints",
@@ -187,25 +242,38 @@
"Tools.shapekey_tolerance": "Shape Key Tolerance",
"Tools.shapekey_tolerance_desc": "Minimum difference to consider a shape key as used",
"Tools.shapekeys_removed": "Removed {count} unused shape keys",
"Tools.rigify_title": "Rigify Tools",
"Tools.convert_rigify_to_unity": "Convert Rigify to Unity",
"Tools.convert_rigify_to_unity_desc": "Convert Rigify armature to Unity-compatible format",
"Tools.rigify_converted": "Rigify armature converted successfully",
"Tools.no_armature": "No armature selected",
"Tools.standardize_title": "Standardization",
"Tools.standardize_armature": "Standardize Armature",
"Tools.standardize_armature_desc": "Convert non-standard armature to Avatar Toolkit standards",
"Tools.standardize_fix_names": "Fix Bone Names",
"Tools.standardize_fix_names_desc": "Rename bones to match standard naming conventions",
"Tools.standardize_fix_hierarchy": "Fix Bone Hierarchy",
"Tools.standardize_fix_hierarchy_desc": "Correct parent-child relationships between bones",
"Tools.standardize_fix_scale": "Fix Bone Scale",
"Tools.standardize_fix_scale_desc": "Normalize bone lengths to fix scale issues",
"Tools.standardize_warning": "This operation will modify your armature. Make a backup first!",
"Tools.standardize_success": "Armature successfully standardized",
"Tools.standardize_partial": "Armature partially standardized. Some issues remain.",
"Tools.standardize_already_valid": "Armature already meets standards. No changes needed.",
"Tools.standardize_issues_title": "Standardization Issues",
"Tools.standardize_issues_header": "Some issues still remain after standardization",
"Tools.standardize_issues_line1": "This could be because some bones on your avatar have unique names",
"Tools.standardize_issues_line2": "that aren't in our list of recognized non-standard bones.",
"Tools.standardize_issues_line3": "For example, if your hips bone is named 'THISISMYHIPS', we can't detect it.",
"Tools.standardize_issues_line4": "If your main skeleton bones aren't being recognized, please report this",
"Tools.standardize_issues_line5": "on our GitHub so we can add them to our database.",
"Tools.standardize_issues_line6": "Accessory bones (hair, clothing, etc.) must be renamed manually.",
"MMD.label": "MMD Tools",
"MMD.bone_standardization": "Bone Standardization",
"MMD.weight_processing": "Weight Processing",
"MMD.hierarchy": "Bone Hierarchy",
"MMD.cleanup": "Cleanup",
"MMD.no_armature": "No armature selected",
"MMD.no_meshes": "No meshes found",
"MMD.validation.rigify_unsupported": "Rigify armatures are not supported",
"MMD.validation.multi_user_mesh": "Multi-user mesh detected: {mesh}",
"MMD.bones_standardized": "Bones standardized successfully",
"MMD.weights_processed": "Weights processed successfully",
"MMD.hierarchy_fixed": "Bone hierarchy fixed successfully",
"MMD.hierarchy_validation_warning": "Some hierarchy relationships could not be validated",
"MMD.cleanup_completed": "Armature cleanup completed",
"MMD.process_twist_bones": "Process Twist Bones",
"MMD.process_twist_bones_desc": "Transfer weights from twist bones to their parent bones",
"MMD.connect_bones": "Connect Bones",
"MMD.connect_bones_desc": "Connect bones in chain where appropriate",
"UVTools.uv_title": "UV Tools",
"UVTools.too_many_vertices": "Error! You have too much stuff selected. Are you sure you're selecting two edges?",
"UVTools.need_line": "You need one line of selected UV points per selected object. Object \"{obj}\" does not meet this requirement!",
"UVTools.align_edges": "Align UV Edges to Target",
"UVTools.align_edges_desc": "Aligns a selected line of UV points on each selected mesh to the line of selected UV points on the active mesh. Useful for kitbashing textures of one model onto another. Uses distance from the 2D cursor to identify the start of the line of UV points on each mesh.",
"Visemes.panel_label": "Visemes",
"Visemes.shape_selection": "Shape Key Selection",
@@ -231,6 +299,8 @@
"Visemes.error.no_shapekeys": "Mesh has no shape keys",
"Visemes.error.select_shapekeys": "Please select shape keys for A, O and CH",
"Visemes.success": "Visemes created successfully",
"Visemes.mesh_select": "Select Mesh",
"Visemes.mesh_select_desc": "Select the mesh to create visemes on",
"EyeTracking.label": "Eye Tracking",
"EyeTracking.setup": "Eye Tracking Setup",
@@ -312,8 +382,15 @@
"EyeTracking.sdk_version": "SDK Version",
"EyeTracking.type.av3": "Avatar 3.0",
"EyeTracking.type.av3_desc": "VRChat Avatar 3.0 eye tracking setup",
"EyeTracking.type.sdk2": "SDK2 (Legacy)",
"EyeTracking.type.sdk2_desc": "VRChat SDK2 eye tracking setup",
"EyeTracking.type.sdk2": "Legacy (ChilloutVR",
"EyeTracking.type.sdk2_desc": "Legacy (SDK2) eye tracking setup",
"EyeTracking.adjust.label": "Adjust Eye Position",
"EyeTracking.adjust.desc": "Adjust the position of eye bones based on vertex groups",
"EyeTracking.sdk2_warning": "Legacy (SDK2) Eye Tracking Notice",
"EyeTracking.sdk2_warning_detail1": "This system SHOULD NOT BE USED FOR VRChat,",
"EyeTracking.sdk2_warning_detail2": "as eye tracking is now configured directly",
"EyeTracking.sdk2_warning_detail3": "in Unity. It remains for other platforms.",
"EyeTracking.sdk2_warning_detail4": "like ChilloutVR.",
"CustomPanel.label": "Custom Avatar Tools",
"CustomPanel.merge_mode": "Merge Mode",
@@ -376,6 +453,47 @@
"MergeArmature.cleanup_shape_keys": "Clean Shape Keys",
"MergeArmature.cleanup_shape_keys_desc": "Remove unused shape keys",
"TextureAtlas.atlas_completed": "Texture atlas creation completed",
"TextureAtlas.atlas_error": "An error occurred during texture atlas creation",
"TextureAtlas.atlas_materials": "Atlas Materials",
"TextureAtlas.atlas_materials_desc": "Atlas materials to optimize the model",
"TextureAtlas.label": "Texture Atlasing",
"TextureAtlas.loaded_list": "Loaded Texture Atlas Material List",
"TextureAtlas.material_list_label": "Texture Atlas Material List Material",
"TextureAtlas.reload_list": "Reload Texture Atlas Material List",
"TextureAtlas.error.label": "ERROR",
"TextureAtlas.none.label": "None",
"TextureAtlas.no_nodes_error.desc": "THIS MATERIAL DOES NOT USE NODES!",
"TextureAtlas.no_images_error.desc": "THIS MATERIAL HAS NO IMAGES!",
"TextureAtlas.texture_use_atlas.desc": "The texture that will be used for the {name} map atlas",
"TextureAtlas.albedo": "Albedo",
"TextureAtlas.normal": "Normal",
"TextureAtlas.emission": "Emission",
"TextureAtlas.ambient_occlusion": "Ambient Occlusion",
"TextureAtlas.height": "Height",
"TextureAtlas.roughness": "Roughness",
"TextureAtlas.description_1": "Create a single material with combined textures",
"TextureAtlas.description_2": "to optimize your avatar for better performance.",
"TextureAtlas.texture_maps": "Texture Maps",
"TextureAtlas.material_ready": "Material is ready for atlas",
"TextureAtlas.material_not_ready": "Material needs at least one texture",
"TextureAtlas.select_all_tooltip": "Select all materials for atlas",
"TextureAtlas.select_none_tooltip": "Deselect all materials",
"TextureAtlas.expand_all_tooltip": "Expand all material settings",
"TextureAtlas.collapse_all_tooltip": "Collapse all material settings",
"TextureAtlas.estimated_size": "Estimated Atlas Size",
"TextureAtlas.materials": "materials",
"TextureAtlas.no_materials_selected": "No materials selected",
"TextureAtlas.select_armature_first": "Please select an armature first",
"TextureAtlas.how_to_use_1": "1. Select an armature in the scene",
"TextureAtlas.how_to_use_2": "2. Click 'Load Materials' to begin",
"TextureAtlas.load_error": "Error loading materials. Check console for details.",
"TextureAtlas.material_not_included": "Material is not included in atlas",
"TextureAtlas.save_file_first": "Please save your Blender file before creating a texture atlas",
"TextureAtlas.save_file_instructions": "Use File > Save As... or click the button below:",
"TextureAtlas.save_file_button": "Save Blender File",
"TextureAtlas.save_file_required": "Save File Required",
"Settings.label": "Settings",
"Settings.language": "Language",
"Settings.language_desc": "Select interface language",
@@ -393,9 +511,13 @@
"Settings.enable_logging_desc": "Enable detailed debug logging for troubleshooting",
"Settings.logging_enabled": "Debug logging enabled",
"Settings.logging_disabled": "Debug logging disabled",
"Settings.highlight_problem_bones": "Highlight Problem Bones",
"Settings.highlight_problem_bones_desc": "Highlight bones with validation issues in the viewport",
"Settings.bone_highlighting": "Bone Highlighting",
"Language.auto": "Automatic",
"Language.en_US": "English",
"Language.ja_JP": "Japanese",
"Language.ko_KR": "Korean",
"Language.changed.title": "Language Changed",
"Language.changed.success": "Language changed successfully!",
"Language.changed.restart": "Some UI elements may require restarting Blender"
+502 -275
View File
@@ -1,298 +1,525 @@
{
"authors": ["Avatar Toolkit Team"],
"messages": {
"AutoVisemeButton.desc": "シェイプキーに基づいて自動的にビセムを作成",
"AutoVisemeButton.error.noShapekeys": "シェイプキーが見つかりません",
"AutoVisemeButton.error.selectShapekeys": "シェイプキーを選択してください",
"AutoVisemeButton.label": "ビセムを作成",
"AutoVisemeButton.success": "ビセムの作成に成功しました",
"AvatarToolkit.label": "Avatar Toolkit (アルファ版)",
"AvatarToolkit.desc1": "Avatar Toolkitは早期アクセス段階です",
"AvatarToolkit.desc2": "問題が発生する可能性があります。",
"AvatarToolkit.desc3": "問題を見つけた場合はGithubで報告してください。",
"Export.resonite.desc": "アニメーションとマテリアルを含むGLBをエクスポート。アニメーションデータについては:",
"Export.resonite.label": "Resoniteにエクスポート",
"Importer.export_resonite.desc": "GLTFとしてResoniteにエクスポート。Blenderでモデルのスケールを確認し、Resoniteではメートル単位でインポートしてください。",
"Importer.export_resonite.label": "Resoniteにエクスポート",
"Importer.export_vrchat.desc": "VRChatにエクスポート(ChilloutVRでも動作する可能性あり)。Catsのエクスポートに似ています。",
"Importer.export_vrchat.label": "VRChatにエクスポート",
"Importer.mmd_anim_importer.desc": "MMDアニメーション(.vmd)をインポート",
"Importer.mmd_anim_importer.label": "MMDアニメーション",
"Importing.importer_search_term": "https://search.brave.com/search?q=blender+{extension}+importer+addon&source=web",
"Importing.need_importer": "{extension}タイプに必要なインポーターがありません!インポーター検索用にウェブブラウザを開きます...",
"Language.auto": "自動",
"Language.en_US": "English",
"Language.ja_JP": "日本語",
"Optimization.applying_transforms": "トランスフォームを適用中...",
"Optimization.cleaning_material_names": "マテリアル名を整理中...",
"Optimization.cleaning_material_slots": "マテリアルスロットを整理中...",
"Optimization.clearing_unused_data": "未使用データを削除中...",
"Optimization.materials_optimization_report": "マテリアル最適化完了:{num_combined}個のマテリアルを結合、{num_cleaned_slots}個のマテリアルスロットを整理、{num_cleaned_names}個のマテリアル名を整理、{num_removed_data_blocks}個の未使用データブロックを削除しました",
"Optimization.combine_materials.desc": "描画コールを減らしパフォーマンスを向上させるため、類似したマテリアルを結合",
"Optimization.combine_materials.label": "マテリアルを結合",
"Optimization.consolidating_materials": "マテリアルを統合中...",
"Optimization.finalizing": "最終処理中...",
"Optimization.fixing_uv_coordinates": "UV座標を修正中...",
"Optimization.join_all_meshes.desc": "描画コールを減らすため、すべてのメッシュを1つのオブジェクトに結合",
"Optimization.join_all_meshes.label": "すべてのメッシュを結合",
"Optimization.join_error": "メッシュ結合中にエラーが発生",
"Optimization.join_operation_failed": "結合操作に失敗しました",
"Optimization.join_selected_meshes.desc": "選択したメッシュのみを1つのオブジェクトに結合",
"Optimization.join_selected_meshes.label": "選択したメッシュを結合",
"Optimization.joinmeshes.label": "メッシュの結合:",
"Optimization.joining_meshes": "メッシュを結合中...",
"Optimization.label": "最適化",
"Optimization.material_attribute_mismatch": "マテリアル{material_name}の属性が一致しません。スキップします",
"Optimization.materials_combined": "{num_combined}個のマテリアルを結合しました",
"Optimization.meshes_joined": "メッシュの結合に成功しました",
"Optimization.no_armature_selected": "アーマチュアが選択されていません",
"Optimization.no_mesh_selected": "メッシュオブジェクトが選択されていません",
"Optimization.no_meshes_found": "選択したアーマチュアにメッシュが見つかりません",
"Optimization.options.label": "最適化:",
"Optimization.preparing_meshes": "メッシュを準備中...",
"Optimization.processing_mesh_no_shapekeys": "シェイプキーのないメッシュ「{mesh_name}」を処理中",
"Optimization.processing_shapekey": "メッシュ「{mesh_name}」のシェイプキー「{shapekeyname}」を処理中",
"Optimization.remove_doubles_completed": "重複頂点の削除が完了しました",
"Optimization.remove_doubles_safely.desc": "口の形状などの重要な特徴を保持しながら重複頂点を削除します。\n素早い解決策ですが、動く頂点は結合しません。",
"Optimization.remove_doubles_safely.label": "安全に重複頂点を削除",
"Optimization.remove_doubles_safely_advanced.label": "高度な安全重複頂点削除",
"Optimization.remove_doubles_safely_advanced.desc": "口の形状などの重要な特徴を保持しながら重複頂点を削除します。\n基本版と異なり、動く頂点も結合しますがシェイプキーは保持します。\n例:唇を閉じることはありませんが、唇を構成する分割されたポリゴンは修正します。",
"UVTools.align_uv_to_target.warning.too_much": "エラー!選択が多すぎます。2つのエッジを選択していますか?",
"UVTools.align_uv_to_target.warning.need_a_line": "各選択オブジェクトにUVポイントの1行が必要です。オブジェクト「{obj}」がこの要件を満たしていません!",
"avatar_toolkit.align_uv_edges_to_target.label": "UVエッジをターゲットに合わせる",
"avatar_toolkit.align_uv_edges_to_target.desc": "選択された各メッシュのUVポイントの線をアクティブメッシュの選択されたUVポイントの線に合わせます。\nあるモデルのテクスチャを別のモデルに適用する際に便利です。\n2Dカーソルからの距離を使用して各メッシュのUVポイントの線の開始点を識別します。",
"Quick_Access.selected_armature.label": "選択されたアーマチュア",
"Quick_Access.selected_armature.desc": "Avatar Toolkitの操作対象となる現在の「ターゲット」アーマチュア",
"Quick_Access.export": "エクスポート",
"Quick_Access.export_fbx.desc": "モデルをFBXとしてエクスポート",
"Quick_Access.export_fbx.label": "FBXエクスポート",
"Quick_Access.export_menu.desc": "サポートされている形式にエクスポート",
"Quick_Access.export_menu.label": "エクスポートメニュー",
"Quick_Access.import": "インポート",
"Quick_Access.import_export.label": "インポート/エクスポート:",
"Quick_Access.import_menu.desc": "モデルをインポート",
"Quick_Access.import_menu.label": "インポートメニュー",
"Quick_Access.import_pmd": "PMDインポート",
"Quick_Access.import_pmd.desc": "MMD PMDモデルをインポート",
"Quick_Access.import_pmx": "PMXインポート",
"Quick_Access.import_pmx.desc": "MMD PMXモデルをインポート",
"Quick_Access.import_success": "モデルのインポートに成功しました",
"Quick_Access.label": "クイックアクセス",
"Quick_Access.options": "クイックアクセス:",
"Quick_Access.select_armature": "アーマチュアを選択:",
"Quick_Access.apply_armature_failed": "シェイプキーの結合段階でポーズをアーマチュアに適用できませんでした!",
"Quick_Access.apply_pose_as_rest.desc": "現在のポーズをデフォルトの休止ポーズにします。",
"Quick_Access.stop_pose_mode.desc": "ポーズモードを終了し、ポーズモードの全ての表示ボーンのポーズをクリアします。",
"Quick_Access.apply_pose_as_rest.label": "ポーズを休止ポーズとして適用",
"Quick_Access.apply_pose_as_shapekey.desc": "現在のポーズを後で有効化できるシェイプキーとして作成します。\n顎の開閉位置を顔の動きのシェイプキーとして適用する際に便利です。",
"Quick_Access.apply_pose_as_shapekey.label": "ポーズをシェイプキーとして適用",
"Quick_Access.stop_pose_mode.label": "ポーズモードを終了",
"Quick_Access.start_pose_mode.desc": "Avatar Toolkitのターゲットアーマチュアのポーズモードを開始します。",
"Quick_Access.start_pose_mode.label": "ポーズモードを開始",
"Quick_Access.select_export.label": "エクスポート方法を選択",
"Quick_Access.select_export_resonite.label": "Resonite",
"Settings.label": "設定",
"Settings.language.desc": "アドオンのUI言語を選択",
"Settings.language.label": "言語:",
"Settings.translation_restart_popup.description": "翻訳の更新について",
"Settings.translation_restart_popup.label": "翻訳の更新",
"Settings.translation_restart_popup.message1": "一部の翻訳はBlenderを再起動するまで",
"Settings.translation_restart_popup.message2": "適用されない場合があります。",
"TextureAtlas.atlas_completed": "テクスチャアトラスの作成が完了しました",
"TextureAtlas.atlas_error": "テクスチャアトラスの作成中にエラーが発生しました",
"TextureAtlas.atlas_materials": "マテリアルをアトラス化",
"TextureAtlas.atlas_materials_desc": "モデルを最適化するためにマテリアルをアトラス化",
"TextureAtlas.label": "テクスチャアトラス",
"TextureAtlas.loaded_list": "テクスチャアトラスマテリアルリストを読み込みました",
"TextureAtlas.material_list_label": "テクスチャアトラスマテリアルリストのマテリアル",
"TextureAtlas.reload_list": "テクスチャアトラスマテリアルリストを再読み込み",
"Tools.bones_translated_success": "すべてのボーンを正常にヒューマノイド名に変換しました",
"Tools.bones_translated_with_fails": "{translate_bone_fails}個のボーンをヒューマノイド名に変換できませんでした。名前に「<noik>」を追加します。",
"Tools.convert_to_resonite.desc": "モデルのボーン名をResonite互換の名前に変換",
"Tools.convert_to_resonite.label": "Resoniteに変換",
"Tools.create_digitigrade_legs.desc": "選択したボーンチェーンから獣脚を作成",
"Tools.create_digitigrade_legs.label": "獣脚を作成",
"Tools.digitigrade_legs.error.bone_format": "ボーンの形式が正しくありません!4つの連続したボーンのチェーンを選択してください!",
"Tools.digitigrade_legs.success": "獣脚の作成に成功しました",
"Tools.import_any_model.desc": "FBX、SMD、DMX、GLTF、PMD、PMXなど、サポートされているモデルをインポート",
"Tools.import_any_model.label": "モデルをインポート",
"Tools.label": "ツール",
"Tools.no_armature_selected": "アーマチュアが選択されていません",
"Tools.select_armature": "アーマチュアを選択してください",
"Tools.tools_title.label": "ツール:",
"Tools.separate_by.label": "分離方法:",
"Tools.separate_by_materials.label": "マテリアルで分離",
"Tools.separate_by_materials.desc": "選択したメッシュをマテリアルで分離",
"Tools.separate_by_materials.success": "メッシュをマテリアルで分離しました",
"Tools.separate_by_loose_parts.label": "分離パーツで分離",
"Tools.separate_by_loose_parts.desc": "選択したメッシュを分離パーツで分離",
"Tools.separate_by_loose_parts.success": "メッシュを分離パーツで分離しました",
"Tools.apply_transforms.label": "トランスフォームを適用",
"Tools.apply_transforms.desc": "アーマチュアとそのメッシュに位置、回転、スケールを適用",
"Tools.apply_transforms.invalid_armature": "無効なアーマチュアが選択されています",
"Tools.apply_transforms.success": "アーマチュアとメッシュにトランスフォームを適用しました",
"Tools.remove_unused_shapekeys.label": "未使用のシェイプキーを削除",
"Tools.remove_unused_shapekeys.tolerance.desc": "シェイプキーを保持する最小の頂点移動量\n(任意の座標での位置)",
"Tools.remove_unused_shapekeys.desc": "何も動かさないシェイプキーを削除します。\nカテゴリーシェイプキーは削除しません。\n(例:名前に「~」「-」「=」を含むもの)",
"Tools.remove_unused_shapekeys.tolerance.label": "位置の許容値",
"Tools.apply_shape_key.label": "シェイプキーをベースに適用",
"Tools.apply_shape_key.desc": "選択したシェイプキーをベースに適用し、デフォルトでオンにします。",
"Tools.apply_shape_key.error": "シェイプキーが何らかの理由でマージされませんでした!",
"Tools.remove_zero_weight_bones.success": "ウェイトのないボーンを削除しました",
"Tools.remove_zero_weight_bones.label": "ウェイトのないボーンを削除",
"Tools.remove_zero_weight_bones.desc": "閾値以下のウェイトを持つボーンをアーマチュアから削除します。",
"Tools.merge_bones_to_active.delete_old.desc": "マージ時に古いボーンを削除します。",
"Tools.merge_bones_to_active.delete_old.label": "古いボーンを削除",
"Tools.merge_bones_to_active.desc": "選択したボーンをアクティブなボーン(青または橙色で選択)にマージします。",
"Tools.merge_bones_to_active.label": "ボーンをアクティブなものにマージ",
"Tools.merge_bones_to_parents.delete_old.desc": "マージ時に古いボーンを削除します。",
"Tools.merge_bones_to_parents.delete_old.label": "古いボーンを削除",
"Tools.merge_bones_to_parents.desc": "選択した各ボーンをそれぞれの親ボーンにマージします。",
"Tools.merge_bones_to_parents.label": "ボーンを個別の親にマージ",
"Tools.remove_zero_weight_bones.threshold.label": "ウェイトの閾値",
"Tools.remove_zero_weight_bones.threshold.desc": "アーマチュア下のメッシュのどの部分にもこの閾値以上のウェイトがないボーンは削除されます",
"Tools.connect_bones.label": "ボーンを接続",
"Tools.bone_tools.label": "ボーンツール",
"Tools.additional_tools.label": "追加ツール",
"Tools.merge_twist_bones.label": "ツイストボーンをマージ",
"Tools.merge_twist_bones.desc": "ツイストボーンを親ボーンにマージ",
"Tools.connect_bones.desc": "ボーンをそれぞれの子ボーンと接続",
"Tools.connect_bones.invalid_armature": "無効なアーマチュアが選択されています",
"Tools.connect_bones.min_distance.label": "最小距離",
"Tools.connect_bones.min_distance.desc": "ボーンを接続する最小距離",
"Tools.connect_bones.success": "{bones_connected}個のボーンを接続しました",
"Tools.delete_bone_constraints.label": "ボーンの制約を削除",
"Tools.delete_bone_constraints.desc": "アーマチュアのボーンから全ての制約を削除",
"Tools.delete_bone_constraints.invalid_armature": "無効なアーマチュアが選択されています",
"Tools.delete_bone_constraints.success": "ボーンから{constraints_removed}個の制約を削除しました",
"Tools.convert_rigify_to_unity.label": "RigifyをUnityに変換",
"Tools.convert_rigify_to_unity.desc": "RigifyアーマチュアをUnityで使用できるように準備",
"Tools.convert_rigify_to_unity.success": "RigifyアーマチュアをUnity用に変換しました",
"VisemePanel.create_visemes": "ビセムを作成",
"VisemePanel.creating_viseme": "ビセムを作成中:{viseme_name}",
"VisemePanel.creating_viseme_detail": "ビセムを作成中:{viseme_name}",
"VisemePanel.creating_visemes": "ビセムを作成中...",
"VisemePanel.error.noArmature": "アーマチュアが選択されていません",
"VisemePanel.error.noMesh": "メッシュが選択されていません",
"VisemePanel.error.noShapekeys": "選択したメッシュにシェイプキーがありません",
"VisemePanel.error.selectMesh": "ビセムを作成するメッシュを選択してください",
"VisemePanel.info.selectMesh": "ビセムを作成するメッシュを選択してください",
"VisemePanel.label": "ビセム",
"VisemePanel.mixing_shape": "シェイプを混合中:{shape_name} 値:{value}",
"VisemePanel.mouth_a.desc": "'A'の口の形のシェイプキー",
"VisemePanel.mouth_a.label": "口 A",
"VisemePanel.mouth_ch.desc": "'CH'の口の形のシェイプキー",
"VisemePanel.mouth_ch.label": "口 CH",
"VisemePanel.mouth_o.desc": "'O'の口の形のシェイプキー",
"VisemePanel.mouth_o.label": "口 O",
"VisemePanel.removing_existing_viseme": "既存のビセムを削除中:{viseme_name}",
"VisemePanel.removing_existing_visemes": "既存のビセムを削除中...",
"VisemePanel.select_mesh": "メッシュを選択",
"VisemePanel.selected_mesh.label": "選択されたメッシュ",
"VisemePanel.selected_mesh.desc": "ビセム操作用に現在選択されているメッシュ",
"VisemePanel.selected_shapes": "選択されたシェイプ:A={shape_a}, O={shape_o}, CH={shape_ch}",
"VisemePanel.shape_intensity": "シェイプの強度",
"VisemePanel.shape_intensity_desc": "ビセムシェイプキーの強度",
"VisemePanel.sorting_shapekeys": "シェイプキーを並べ替え中...",
"VisemePanel.start_viseme_creation": "ビセム作成を開始...",
"VisemePanel.viseme_created_successfully": "ビセム{viseme_name}の作成に成功しました",
"VisemePanel.viseme_creation_completed": "ビセム作成が完了しました。",
"MergeArmatures.select_armature": "アーマチュアを選択してください",
"MergeArmatures.title.label": "アーマチュアのマージ:",
"MergeArmatures.label": "アーマチュアをマージ",
"MergeArmatures.selected_armature.label": "マージ元のアーマチュア",
"MergeArmatures.selected_armature.desc": "Avatar Toolkitのターゲットアーマチュアにマージされるアーマチュア",
"MergeArmatures.target_armature.label": "マージ先のアーマチュア",
"MergeArmatures.target_armature.desc": "アーマチュアのマージ先となるターゲットアーマチュア",
"MergeArmature.merge_armatures.label": "アーマチュアをマージ",
"MergeArmature.merge_armatures.desc": "{selected_armature_label}をAvatar Toolkitのターゲットアーマチュアにマージ",
"MergeArmature.merge_armatures.align_bones.label": "ボーンを整列",
"MergeArmature.merge_armatures.align_bones.desc": "マージ前にソースアーマチュアのボーンをターゲットアーマチュアに合わせて\nボーンを伸縮させます。",
"MergeArmature.merge_armatures.apply_transforms.label": "トランスフォームを適用",
"MergeArmature.merge_armatures.apply_transforms.desc": "マージ前にアーマチュアとそのメッシュにトランスフォームを適用します。",
"MMDOptions.optimize_armature.label": "アーマチュアを最適化",
"MMDOptions.optimize_armature.desc": "ボーンのロールの修正、ボーンの整列、ボーンの接続などでアーマチュアを最適化",
"MMDOptions.fixing_bone_rolls": "ボーンのロールを修正中",
"MMDOptions.aligning_bones": "ボーンを整列中",
"MMDOptions.connecting_bones": "ボーンを接続中",
"MMDOptions.deleting_bone_constraints": "ボーンの制約を削除中",
"MMDOptions.merging_bones_to_parents": "ボーンを親にマージ中",
"MMDOptions.reordering_bones": "ボーンを並べ替え中",
"MMDOptions.fixing_armature_names": "アーマチュア名を修正中",
"MMDOptions.renaming_bones": "ボーン名を変更中",
"MMDOptions.armature_optimization_complete": "アーマチュアの最適化が完了しました",
"MMDOptions.convert_materials.label": "マテリアルを変換",
"MMDOptions.convert_materials.desc": "マテリアルをPrincipled BSDFシェーダーを使用するように変換し、MMDとVRMシェーダーを修正",
"MMDOptions.converting_materials": "{name}のマテリアルを変換中",
"MMDOptions.title": "MMDオプション",
"MMDOptions.no_armature_selected": "アーマチュアが選択されていません",
"MMDOptions.label": "MMDオプション",
"MMDOptions.cleanup_mesh.label": "メッシュのクリーンアップ",
"MMDOptions.cleanup_mesh.desc": "空のオブジェクト、未使用の頂点グループ、未使用の頂点、空のシェイプキーを削除してメッシュをクリーンアップ",
"MMDOptions.removing_empty_objects": "空のオブジェクトを削除中",
"MMDOptions.removing_unused_vertex_groups": "未使用の頂点グループを削除中",
"MMDOptions.removing_unused_vertices": "未使用の頂点を削除中",
"MMDOptions.removing_empty_shape_keys": "空のシェイプキーを削除中",
"MMDOptions.optimize_weights.label": "ウェイトを最適化",
"MMDOptions.optimize_weights.desc": "頂点あたりのウェイト数を制限してウェイトを最適化",
"MMDOptions.max_weights.label": "最大ウェイト数",
"MMDOptions.max_weights.desc": "頂点あたりの最大ウェイト数",
"MMDOptions.merging_weights": "ウェイトを結合中",
"MMDOptions.removing_zero_weight_bones": "ウェイトのないボーンを削除中",
"MMDOptions.limiting_vertex_weights": "頂点ウェイトを制限中",
"MMDOptions.weight_optimization_complete": "ウェイトの最適化が完了しました",
"AvatarToolkit.label": "アバターツールキット (アルファ 0.2.1)",
"AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、",
"AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、",
"AvatarToolkit.desc3": "GitHubで報告してください。",
"Updater.label": "アップデーター",
"Updater.CheckForUpdateButton.label": "アップデートを確認",
"Updater.CheckForUpdateButton.label_alt": "利用可能なアップデートはありません",
"Updater.UpdateToLatestButton.label": "{name}にアップデート",
"Updater.UpdateToSelectedButton.label": "アップデート",
"Updater.currentVersion": "現在のバージョン{name}",
"Updater.currentVersion": "現在のバージョン: {name}",
"Updater.selectVersion": "バージョンを選択",
"Updater.CheckForUpdateButton.desc": "利用可能なアップデートを確認",
"UpdateToLatestButton.desc": "最新バージョンにアップデート",
"UpdateNotificationPopup.label": "アップデート通知",
"UpdateNotificationPopup.desc": "利用可能なアップデートについての通知",
"UpdateNotificationPopup.newUpdate": "新しいアップデートが利用可能{version}",
"UpdateNotificationPopup.newUpdate": "新しいアップデートが利用可能: {version}",
"RestartBlenderPopup.label": "Blenderを再起動",
"RestartBlenderPopup.desc": "アップデートを完了するためにBlenderを再起動",
"RestartBlenderPopup.message": "アップデート成功しました!Blenderを再起動してください。",
"RestartBlenderPopup.message": "アップデート成功!Blenderを再起動してください。",
"check_for_update.cantCheck": "アップデートを確認できません",
"download_file.cantConnect": "アップデートサーバーに接続できません",
"download_file.cantFindZip": "アップデートファイルが見つかりません",
"download_file.cantFindAvatarToolkit": "アップデートパッケージ内にAvatar Toolkitファイルが見つかりません",
"CreditsSupport.label": "クレジット&サポート",
"CreditsSupport.credits_title": "クレジット",
"CreditsSupport.credits_text1": "Avatar Toolkitは以下のNeonekoチームによって作成されました:",
"CreditsSupport.credits_text2": "YusarinaとOnan989",
"CreditsSupport.credits_text3": "一部のコードはCats Blender Pluginを参考にしています。",
"CreditsSupport.credits_text4": "元のプラグインの貢献者に感謝します。",
"CreditsSupport.support_text1": "私たちの活動を支援したい場合は、",
"CreditsSupport.support_text2": "pally.ggページで寄付/投げ銭ができます。",
"CreditsSupport.support_title": "ポートする",
"CreditsSupport.support_button": "ポートする",
"CreditsSupport.help_title": "ヘルプが必要ですか?",
"CreditsSupport.help_text1": "まずはWikiをご確認ください。さらなるサポート",
"CreditsSupport.help_text2": "求める前にWikiを読むことを強くお勧めします。",
"CreditsSupport.wiki_button": "Wiki",
"CreditsSupport.discord_button": "Discordに参加",
"TextureAtlas.include_in_atlas": "アトラスに含める",
"TextureAtlas.include_in_atlas_desc": "このマテリアルをテクスチャアトラスに含める",
"download_file.cantFindAvatarToolkit": "アップデートパッケージにアバターツールキットファイルが見つかりません",
"QuickAccess.label": "クイックアクセス",
"QuickAccess.select_armature": "アーマチュアを選択",
"QuickAccess.valid_armature": "有効なアーマチュア",
"QuickAccess.bones_count": "ボーン数: {count}",
"QuickAccess.pose_bones_available": "ポーズボーン: 利用可能",
"QuickAccess.pose_controls": "ポーズコントロール",
"QuickAccess.import_export": "インポート/エクスポート",
"QuickAccess.import": "インポート",
"QuickAccess.export": "エクスポート",
"QuickAccess.export_fbx": "FBXをエクスポート",
"QuickAccess.export_resonite": "Resoniteにエクスポート",
"QuickAccess.start_pose_mode.label": "ポーズモード開始",
"QuickAccess.start_pose_mode.desc": "選択したアーマチュアのポーズモードに入る",
"QuickAccess.stop_pose_mode.label": "ポーズモード終了",
"QuickAccess.stop_pose_mode.desc": "ポーズモードを終了し、変形をクリア",
"QuickAccess.apply_pose_as_shapekey.label": "ポーズをシェイプキーとして適用",
"QuickAccess.apply_pose_as_shapekey.desc": "現在のポーズから新しいシェイプキーを作成",
"QuickAccess.apply_pose_as_rest.label": "ポーズを静止ポーズとして適用",
"QuickAccess.apply_pose_as_rest.desc": "現在のポーズを静止ポーズとして適用",
"QuickAccess.apply_armature_failed": "アーマチュアの変更の適用に失敗しました",
"QuickAccess.validation_basic_warning": "限定的な検証がアクティブ",
"QuickAccess.validation_basic_details": "基本的なボーン構造のみが検証されています",
"QuickAccess.validation_none_warning": "検証が無効",
"QuickAccess.validation_none_details": "アーマチュアの検証チェックが実行されていません",
"Quick_Access.import_success": "インポート成功",
"PoseMode.error.start": "ポーズモードの開始に失敗: {error}",
"PoseMode.error.stop": "ポーズモードの終了に失敗: {error}",
"PoseMode.error.shapekey": "ポーズをシェイプキーとして適用に失敗: {error}",
"PoseMode.error.rest_pose": "ポーズを静止ポーズとして適用に失敗: {error}",
"PoseMode.shapekey.name": "シェイプキー名",
"PoseMode.shapekey.description": "新しいシェイプキーの名前",
"PoseMode.shapekey.default": "ポーズ_シェイプキー",
"PoseMode.skipped_meshes": "一部のメッシュがスキップされました:\n{message}",
"PoseMode.basis": "基本形",
"Armature.validation.no_armature": "アーマチュアが選択されていません",
"Armature.validation.not_armature": "選択されたオブジェクトはアーマチュアではありません",
"Armature.validation.no_bones": "アーマチュアにボーンがありません",
"Armature.validation.basic_check_failed": "基本的なアーマチュア検証に失敗しました",
"Armature.validation.missing_bones": "必須ボーンが不足しています: {bones}",
"Armature.validation.invalid_hierarchy": "{parent}と{child}の間のボーン階層が無効です",
"Armature.validation.asymmetric_bones": "{bone}の対称ボーンが不足しています",
"Armature.validation.asymmetric_hand_wrist": "手首/手のための対称ボーンが不足しています",
"Armature.validation.found_bones": "アーマチュアで見つかったボーン:\n- {bones}",
"Armature.validation.non_standard_bones": "非標準ボーンが見つかりました:\n- {bones}",
"Armature.validation.accessory_bones_note.line1": "髪のボーン、スカートのボーン、または他の",
"Armature.validation.accessory_bones_note.line2": "アクセサリーボーンがメインアーマチュアのボーンと",
"Armature.validation.accessory_bones_note.line3": "同様の名前(例:Head1、Head2)を持つ場合は、",
"Armature.validation.accessory_bones_note.line4": "Hair_1、Skirt_1などのより説明的な名前に変更してください。",
"Armature.validation.standardize_note.line1": "ツールセクションの「アーマチュアを標準化」",
"Armature.validation.standardize_note.line2": "ボタンを使用して、アーマチュアを",
"Armature.validation.standardize_note.line3": "自動的に標準化できます。",
"Validation.section.found_bones": "見つかったボーン",
"Validation.section.non_standard": "非標準ボーン",
"Validation.section.hierarchy": "階層の問題",
"Validation.status.failed": "検証に失敗しました",
"Validation.message.failed.line1": "アーマチュアの検証に失敗しました",
"Validation.message.failed.line2": "以下の問題を確認してください",
"Validation.message.failed.line3": "",
"Validation.highlight_problem_bones_desc": "検証に問題のあるボーンをビューポートで視覚的に強調表示",
"Validation.no_armature": "アーマチュアが選択されていません",
"Validation.no_issues": "強調表示する検証の問題が見つかりません",
"Validation.highlighting_complete": "問題のあるボーンが正常に強調表示されました",
"Validation.tpose.no_armature": "T-ポーズ検証用のアーマチュアが見つかりません",
"Validation.tpose.left_arm_not_horizontal": "左腕が水平なT-ポーズの位置にありません",
"Validation.tpose.right_arm_not_horizontal": "右腕が水平なT-ポーズの位置にありません",
"Validation.tpose.spine_not_vertical": "背骨が垂直な位置にありません",
"Validation.tpose.warning": "T-ポーズ検証の警告",
"Validation.tpose.recommendation": "Unityやほかのプラットフォームにインポートする前にT-ポーズを修正することをお勧めします",
"Validation.scale_issues": "異常なスケールを持つボーンが検出されました:",
"Validation.scale_issue.too_small": "ボーンが極端に小さい",
"Validation.scale_issue.too_large": "ボーンが極端に大きい",
"Validation.section.scale_issues": "スケールの問題",
"Validation.tpose.label": "T-ポーズを検証",
"Validation.no_scale_issues": "スケールの問題は検出されませんでした",
"Validation.no_hierarchy_issues": "階層の問題は検出されませんでした",
"Validation.no_non_standard_issues": "非標準ボーンの問題は検出されませんでした",
"Validation.tpose.valid": "T-ポーズの検証に成功しました",
"Validation.tpose.desc": "アーマチュアが適切なT-ポーズにあるかチェック",
"Validation.highlight_problem_bones": "問題のあるボーンを強調表示",
"Validation.clear_bone_highlighting": "ボーンの強調表示をクリア",
"Validation.clear_bone_highlighting_desc": "ボーンの強調表示を削除し、ボーンの色をデフォルトにリセット",
"Validation.highlighting_cleared": "ボーンの強調表示が正常にクリアされました",
"Mesh.validation.no_data": "メッシュデータがありません",
"Mesh.validation.no_vertex_groups": "頂点グループが見つかりません",
"Mesh.validation.no_armature_modifier": "アーマチュアモディファイアがありません",
"Mesh.validation.valid": "ポーズ操作に有効なメッシュ",
"Operation.pose_applied": "ポーズが正常に適用されました",
"Scene.avatar_toolkit_updater_version_list.name": "バージョンリスト",
"Scene.avatar_toolkit_updater_version_list.description": "アップデート可能なバージョンのリスト",
"TextureAtlas.albedo": "アルベド",
"TextureAtlas.normal": "法線",
"TextureAtlas.emission": "発光",
"TextureAtlas.ambient_occlusion": "アンビエントオクルージョン",
"TextureAtlas.height": "ハイト",
"TextureAtlas.roughness": "ラフネス",
"Scene.avatar_toolkit_updater_version_list.description": "利用可能なバージョンのリスト",
"Optimization.label": "最適化",
"Optimization.materials_title": "マテリアル",
"Optimization.cleanup_title": "メッシュクリーンアップ",
"Optimization.join_meshes_title": "メッシュ結合",
"Optimization.combine_materials": "マテリアルを結合",
"Optimization.combine_materials_desc": "描画コールを減らすために類似したマテリアルを結合",
"Optimization.remove_doubles": "重複頂点を削除",
"Optimization.remove_doubles_desc": "重複した頂点を削除",
"Optimization.remove_doubles_advanced": "高度な設定",
"Optimization.remove_doubles_advanced_desc": "高度なオプションで重複頂点を削除",
"Optimization.join_all_meshes": "すべて結合",
"Optimization.join_all_meshes_desc": "シーン内のすべてのメッシュを結合",
"Optimization.join_selected_meshes": "選択したものを結合",
"Optimization.join_selected_meshes_desc": "選択したメッシュのみを結合",
"Optimization.no_meshes": "最適化するメッシュが見つかりません",
"Optimization.materials_combined": "{combined}個のマテリアルを結合し、{cleaned}個のスロットをクリーンアップし、{removed}個の未使用データブロックを削除しました",
"Optimization.error.combine_materials": "マテリアルの結合に失敗: {error}",
"Optimization.materials_total": "合計マテリアル: {count}",
"Optimization.materials_duplicates": "潜在的な重複: {count}",
"Optimization.no_materials": "メッシュにマテリアルが見つかりません",
"Optimization.error.consolidation": "マテリアルの統合に失敗しました。詳細はコンソールを確認してください",
"Optimization.combining_materials": "類似したマテリアルを結合中...",
"Optimization.cleaning_slots": "マテリアルスロットをクリーニング中...",
"Optimization.removing_unused": "未使用のマテリアルを削除中...",
"Optimization.selecting_meshes": "メッシュを選択中...",
"Optimization.joining_meshes": "メッシュを結合中...",
"Optimization.applying_transforms": "変形を適用中...",
"Optimization.fixing_uvs": "UV座標を修正中...",
"Optimization.finalizing": "完了中...",
"Optimization.meshes_joined": "すべてのメッシュが正常に結合されました",
"Optimization.selected_meshes_joined": "選択したメッシュが正常に結合されました",
"Optimization.no_mesh_selected": "メッシュが選択されていません",
"Optimization.select_at_least_two": "少なくとも2つのメッシュを選択してください",
"Optimization.error.join_meshes": "メッシュの結合に失敗: {error}",
"Optimization.error.join_selected": "選択したメッシュの結合に失敗: {error}",
"Optimization.merge_distance": "結合距離",
"Optimization.merge_distance_desc": "頂点が結合される距離",
"Optimization.remove_doubles_warning": "このプロセスは時間がかかる場合があります",
"Optimization.remove_doubles_wait": "この操作中、Blenderが応答しなくなる場合があります",
"Optimization.error.remove_doubles": "重複頂点の削除に失敗: {error}",
"Optimization.no_armature": "アーマチュアが選択されていません",
"Optimization.processing_mesh": "メッシュを処理中: {name}",
"Optimization.processing_shapekey": "シェイプキーを処理中: {name}",
"Optimization.remove_doubles_completed": "重複頂点の削除が正常に完了しました",
"Tools.label": "ツール",
"Tools.general_title": "一般ツール",
"Tools.select_armature": "アーマチュアを選択",
"Tools.convert_resonite": "Resoniteに変換",
"Tools.convert_resonite_desc": "Resoniteで使用するためにモデルを変換",
"Tools.convert_resonite.operation": "Resoniteに変換中",
"Tools.separate_title": "分離ツール",
"Tools.separate_materials": "マテリアルで分離",
"Tools.separate_materials_desc": "マテリアルごとにメッシュを分離",
"Tools.separate_loose": "離れた部分で分離",
"Tools.separate_loose_desc": "メッシュを離れた部分に分離",
"Tools.separate_materials_success": "メッシュがマテリアルごとに正常に分離されました",
"Tools.separate_loose_success": "メッシュが離れた部分に正常に分離されました",
"Tools.bone_title": "ボーンツール",
"Tools.create_digitigrade": "デジティグレード脚を作成",
"Tools.create_digitigrade_desc": "脚をデジティグレード設定に変換",
"Tools.digitigrade": "デジティグレード脚を作成",
"Tools.digitigrade_desc": "選択した脚のボーンをデジティグレード設定に変換",
"Tools.digitigrade_error": "デジティグレード脚の作成に失敗: {error}",
"Tools.digitigrade_success": "デジティグレード脚の設定が正常に作成されました",
"Tools.processing_leg": "脚のボーンを処理中: {bone}",
"Tools.merge_twist_bones": "ツイストボーンを保持",
"Tools.merge_twist_bones_desc": "チェックすると、ウェイトがゼロでもツイストボーンが保持されます",
"Tools.clean_weights": "ゼロウェイトボーンを削除",
"Tools.clean_weights_desc": "頂点ウェイトのないボーンを削除",
"Tools.preserve_parent_bones": "親ボーンを保持",
"Tools.preserve_parent_bones_desc": "ウェイトがなくても子を持つボーンを保持",
"Tools.target_bone_type": "対象ボーンタイプ",
"Tools.target_bone_type_desc": "処理するボーンのタイプをフィルタリング",
"Tools.target_all_bones": "すべてのボーン",
"Tools.target_deform_bones": "変形ボーンのみ",
"Tools.target_non_deform_bones": "非変形ボーンのみ",
"Tools.list_only_mode": "リストモードのみ",
"Tools.list_only_mode_desc": "ゼロウェイトボーンを削除する代わりにリスト表示",
"Tools.zero_weight_bones_found": "ゼロウェイトボーンが見つかりました: {bones}",
"Tools.remove_selected_bones": "選択したボーンを削除",
"Tools.remove_selected_bones_desc": "選択したゼロウェイトボーンをアーマチュアから削除",
"Tools.bones_removed": "{count}個のボーンを削除しました",
"Tools.clean_constraints": "ボーン制約を削除",
"Tools.clean_constraints_desc": "アーマチュアからすべてのボーン制約を削除",
"Tools.clean_constraints_success": "{count}個のボーン制約を削除しました",
"Tools.processing_bone_constraints": "ボーンから制約を削除中: {bone}",
"Tools.clean_weights_success": "{count}個のゼロウェイトボーンを削除しました",
"Tools.clean_weights_threshold": "ウェイトしきい値",
"Tools.clean_weights_threshold_desc": "ボーンがウェイト付けされていると見なす最小ウェイト値",
"Tools.merge_title": "結合ツール",
"Tools.merge_to_active": "アクティブに結合",
"Tools.merge_to_active_desc": "選択したボーンをアクティブボーンに結合",
"Tools.merge_to_parent": "親に結合",
"Tools.merge_to_parent_desc": "ボーンをそれぞれの親に結合",
"Tools.connect_bones": "ボーンを接続",
"Tools.connect_bones_desc": "チェーン内の切断されたボーンを接続",
"Tools.additional_title": "追加ツール",
"Tools.apply_transforms": "変形を適用",
"Tools.apply_transforms_desc": "オブジェクトにすべての変形を適用",
"Tools.clean_shapekeys": "未使用のシェイプキーを削除",
"Tools.clean_shapekeys_desc": "メッシュから未使用のシェイプキーを削除",
"Tools.bones_translated_success": "すべてのボーンが正常に翻訳されました",
"Tools.bones_translated_with_fails": "翻訳が完了しましたが、{translate_bone_fails}個のボーンは翻訳されませんでした",
"Tools.storing_transforms": "ボーンの変形を保存中...",
"Tools.analyzing_weights": "頂点ウェイトを分析中...",
"Tools.removing_bones": "ウェイトのないボーンを削除中...",
"Tools.verifying_hierarchy": "ボーン階層を検証中...",
"Tools.connect_bones_min_distance": "最小距離",
"Tools.connect_bones_min_distance_desc": "接続を試みるボーン間の最小距離",
"Tools.connect_bones_success": "{count}個のボーンを接続しました",
"Tools.merge_weights_threshold": "ウェイト転送しきい値",
"Tools.merge_weights_threshold_desc": "ボーンを結合する際に転送する最小ウェイト値",
"Tools.no_bones_selected": "結合するボーンが選択されていません",
"Tools.no_bones_with_parent": "親を持つ選択されたボーンが見つかりません",
"Tools.merge_to_active_success": "{count}個のボーンをアクティブボーンに正常に結合しました",
"Tools.merge_to_parent_success": "{count}個のボーンをそれぞれの親に正常に結合しました",
"Tools.transforms_applied": "変形が正常に適用されました",
"Tools.shapekey_tolerance": "シェイプキー許容値",
"Tools.shapekey_tolerance_desc": "シェイプキーが使用されていると見なす最小差異",
"Tools.shapekeys_removed": "{count}個の未使用シェイプキーを削除しました",
"Tools.rigify_title": "Rigifyツール",
"Tools.convert_rigify_to_unity": "RigifyをUnityに変換",
"Tools.convert_rigify_to_unity_desc": "RigifyアーマチュアをUnity互換形式に変換",
"Tools.rigify_converted": "Rigifyアーマチュアが正常に変換されました",
"Tools.no_armature": "アーマチュアが選択されていません",
"Tools.standardize_title": "標準化",
"Tools.standardize_armature": "アーマチュアを標準化",
"Tools.standardize_armature_desc": "非標準アーマチュアをアバターツールキット標準に変換",
"Tools.standardize_fix_names": "ボーン名を修正",
"Tools.standardize_fix_names_desc": "ボーンの名前を標準的な命名規則に合わせて変更",
"Tools.standardize_fix_hierarchy": "ボーン階層を修正",
"Tools.standardize_fix_hierarchy_desc": "ボーン間の親子関係を修正",
"Tools.standardize_fix_scale": "ボーンスケールを修正",
"Tools.standardize_fix_scale_desc": "スケールの問題を修正するためにボーンの長さを正規化",
"Tools.standardize_warning": "この操作はアーマチュアを変更します。先にバックアップを作成してください!",
"Tools.standardize_success": "アーマチュアが正常に標準化されました",
"Tools.standardize_partial": "アーマチュアが部分的に標準化されました。一部の問題が残っています。",
"Tools.standardize_already_valid": "アーマチュアはすでに標準を満たしています。変更は必要ありません。",
"Tools.standardize_issues_title": "標準化の問題",
"Tools.standardize_issues_header": "標準化後もいくつかの問題が残っています",
"Tools.standardize_issues_line1": "これは、アバターのボーンに認識されない非標準ボーンの",
"Tools.standardize_issues_line2": "リストにない固有の名前があるためかもしれません。",
"Tools.standardize_issues_line3": "例えば、ヒップボーンが「THISISMYHIPS」という名前の場合、検出できません。",
"Tools.standardize_issues_line4": "メインスケルトンボーンが認識されない場合は、",
"Tools.standardize_issues_line5": "データベースに追加できるよう、GitHubで報告してください。",
"Tools.standardize_issues_line6": "アクセサリーボーン(髪、服など)は手動で名前を変更する必要があります。",
"UVTools.uv_title": "UVツール",
"UVTools.too_many_vertices": "エラー!選択項目が多すぎます。2つのエッジを選択していますか?",
"UVTools.need_line": "選択した各オブジェクトにUVポイントの線が1つ必要です。オブジェクト「{obj}」はこの要件を満たしていません!",
"UVTools.align_edges": "UVエッジをターゲットに合わせる",
"UVTools.align_edges_desc": "選択した各メッシュのUVポイントの線をアクティブメッシュの選択したUVポイントの線に合わせます。あるモデルのテクスチャを別のモデルに適用する際に便利です。2Dカーソルからの距離を使用して、各メッシュのUVポイントの線の開始点を識別します。",
"Visemes.panel_label": "口形素",
"Visemes.shape_selection": "シェイプキー選択",
"Visemes.controls": "口形素コントロール",
"Visemes.no_shapekeys": "シェイプキーを持つメッシュを選択",
"Visemes.mouth_a": "A形状",
"Visemes.mouth_a_desc": "'A'音のシェイプキー",
"Visemes.mouth_o": "O形状",
"Visemes.mouth_o_desc": "'O'音のシェイプキー",
"Visemes.mouth_ch": "CH形状",
"Visemes.mouth_ch_desc": "'CH'音のシェイプキー",
"Visemes.shape_intensity": "形状の強度",
"Visemes.shape_intensity_desc": "口形素形状の強度乗数",
"Visemes.start_preview": "プレビュー開始",
"Visemes.stop_preview": "プレビュー停止",
"Visemes.preview_mode_desc": "口形素プレビューモードを切り替え",
"Visemes.preview_selection": "プレビュー選択",
"Visemes.preview_selection_desc": "プレビューする口形素を選択",
"Visemes.preview_label": "口形素をプレビュー",
"Visemes.preview_desc": "ビューポートで口形素形状をプレビュー",
"Visemes.create_label": "口形素を作成",
"Visemes.create_desc": "VRC口形素シェイプキーを作成",
"Visemes.error.no_shapekeys": "メッシュにシェイプキーがありません",
"Visemes.error.select_shapekeys": "A、OおよびCHのシェイプキーを選択してください",
"Visemes.success": "口形素が正常に作成されました",
"Visemes.mesh_select": "メッシュを選択",
"Visemes.mesh_select_desc": "口形素を作成するメッシュを選択",
"EyeTracking.label": "アイトラッキング",
"EyeTracking.setup": "アイトラッキング設定",
"EyeTracking.mesh_select": "メッシュ選択",
"EyeTracking.bones": "ボーン選択",
"EyeTracking.head_bone": "頭部ボーン",
"EyeTracking.eye_left": "左目ボーン",
"EyeTracking.eye_right": "右目ボーン",
"EyeTracking.shapekeys": "シェイプキー選択",
"EyeTracking.options": "オプション",
"EyeTracking.rotation": "目の回転",
"EyeTracking.rotation.x": "垂直回転",
"EyeTracking.rotation.y": "水平回転",
"EyeTracking.adjust": "目の調整",
"EyeTracking.blinking": "まばたきコントロール",
"EyeTracking.no_shapekeys": "選択したメッシュにシェイプキーが見つかりません",
"EyeTracking.no_armature": "アーマチュアが選択されていません",
"EyeTracking.no_mesh": "メッシュが見つかりません",
"EyeTracking.create.label": "アイトラッキングを作成",
"EyeTracking.create.desc": "アイトラッキングボーンとシェイプキーを設定",
"EyeTracking.testing.start.label": "テスト開始",
"EyeTracking.testing.start.desc": "アイトラッキングテストモードに入る",
"EyeTracking.testing.stop.label": "テスト停止",
"EyeTracking.testing.stop.desc": "アイトラッキングテストモードを終了",
"EyeTracking.reset.label": "アイトラッキングをリセット",
"EyeTracking.reset.desc": "すべてのアイトラッキング設定をリセット",
"EyeTracking.rotate.label": "目のボーンを回転",
"EyeTracking.rotate.desc": "VRChat互換性のために目のボーンを回転",
"EyeTracking.iris.label": "虹彩の高さを調整",
"EyeTracking.iris.desc": "虹彩の頂点の高さを調整",
"EyeTracking.blink.test.label": "まばたきテスト",
"EyeTracking.blink.test.desc": "目のまばたきシェイプキーをテスト",
"EyeTracking.lowerlid.test.label": "下まぶたテスト",
"EyeTracking.lowerlid.test.desc": "下まぶたシェイプキーをテスト",
"EyeTracking.blink.reset.label": "まばたきテストをリセット",
"EyeTracking.blink.reset.desc": "まばたきテスト値をリセット",
"EyeTracking.validation.noArmature": "シーンにアーマチュアが見つかりません",
"EyeTracking.validation.noMesh": "メッシュ'{mesh}'が見つかりません",
"EyeTracking.validation.noShapekeys": "選択したメッシュにシェイプキーがありません",
"EyeTracking.validation.leftEye": "左目",
"EyeTracking.validation.rightEye": "右目",
"EyeTracking.validation.missingGroups": "不足している頂点グループ: {groups}",
"EyeTracking.validation.missingBones": "必要なボーンが不足しています: {bones}",
"EyeTracking.validation.success": "アイトラッキング設定が正常に検証されました",
"EyeTracking.error.noMesh": "アイトラッキング用のメッシュが選択されていません",
"EyeTracking.error.noVertexGroup": "ボーン用の頂点グループが見つかりません: {bone}",
"EyeTracking.error.noShapeSelected": "すべての必要なシェイプキーを選択してください",
"EyeTracking.success": "アイトラッキング設定が正常に完了しました",
"EyeTracking.mode_select": "モード選択",
"EyeTracking.mesh_setup": "メッシュ設定",
"EyeTracking.bone_setup": "ボーン設定",
"EyeTracking.shapekey_setup": "シェイプキー設定",
"EyeTracking.testing": "テストモード",
"EyeTracking.rotation_controls": "目の回転コントロール",
"EyeTracking.adjustments": "目の調整",
"EyeTracking.blink_testing": "まばたきテスト",
"EyeTracking.wink_left": "左ウィンク",
"EyeTracking.wink_right": "右ウィンク",
"EyeTracking.lowerlid_left": "左下まぶた",
"EyeTracking.lowerlid_right": "右下まぶた",
"EyeTracking.mode.creation": "作成モード",
"EyeTracking.mode.testing": "テストモード",
"EyeTracking.disable_blinking": "目のまばたきを無効化",
"EyeTracking.disable_movement": "目の動きを無効化",
"EyeTracking.distance": "目の距離",
"EyeTracking.distance_desc": "目の間の距離を調整",
"EyeTracking.mode": "アイトラッキングモード",
"EyeTracking.mesh_name": "メッシュ",
"EyeTracking.mesh_name_desc": "アイトラッキング用のメッシュを選択",
"EyeTracking.head_bone_desc": "頭部ボーンを選択",
"EyeTracking.eye_left_desc": "左目ボーンを選択",
"EyeTracking.eye_right_desc": "右目ボーンを選択",
"EyeTracking.type": "アイトラッキングタイプ",
"EyeTracking.type_desc": "作成するアイトラッキング設定のタイプを選択",
"EyeTracking.create.av3.label": "AV3アイトラッキングを作成",
"EyeTracking.create.av3.desc": "VRChatアバター3.0用のアイトラッキングを設定",
"EyeTracking.create.sdk2.label": "SDK2アイトラッキングを作成",
"EyeTracking.create.sdk2.desc": "VRChat SDK2用のアイトラッキングを設定",
"EyeTracking.sdk_version": "SDKバージョン",
"EyeTracking.type.av3": "アバター3.0",
"EyeTracking.type.av3_desc": "VRChatアバター3.0アイトラッキング設定",
"EyeTracking.type.sdk2": "レガシー (ChilloutVR)",
"EyeTracking.type.sdk2_desc": "レガシー (SDK2) アイトラッキング設定",
"EyeTracking.adjust.label": "目の位置を調整",
"EyeTracking.adjust.desc": "頂点グループに基づいて目のボーンの位置を調整",
"EyeTracking.sdk2_warning": "レガシー (SDK2) アイトラッキング注意",
"EyeTracking.sdk2_warning_detail1": "このシステムはVRChatには使用すべきではありません。",
"EyeTracking.sdk2_warning_detail2": "アイトラッキングは現在Unity内で",
"EyeTracking.sdk2_warning_detail3": "直接設定されるためです。ChilloutVRなどの",
"EyeTracking.sdk2_warning_detail4": "他のプラットフォーム用に残されています。",
"CustomPanel.label": "カスタムアバターツール",
"CustomPanel.merge_mode": "結合モード",
"CustomPanel.mesh_selection": "メッシュ選択",
"CustomPanel.select_mesh": "メッシュを選択",
"CustomPanel.select_bone": "ボーンを選択",
"CustomPanel.select_armature": "アーマチュアを選択",
"CustomPanel.mode.armature": "アーマチュア",
"CustomPanel.mode.armature_desc": "アーマチュアを一緒に結合",
"CustomPanel.mode.mesh": "メッシュ",
"CustomPanel.mode.mesh_desc": "メッシュをアーマチュアに取り付け",
"AttachMesh.label": "メッシュを取り付け",
"AttachMesh.desc": "自動ウェイト設定でメッシュをアーマチュアボーンに取り付け",
"AttachMesh.search_desc": "取り付けるメッシュを検索",
"AttachMesh.select": "取り付けるメッシュを選択",
"AttachMesh.select_desc": "アーマチュアに取り付けるメッシュを選択",
"AttachMesh.success": "メッシュが正常に取り付けられました",
"AttachMesh.warn_no_armature": "取り付けるアーマチュアとメッシュを選択",
"AttachMesh.validate_transforms": "メッシュの変形を検証中",
"AttachMesh.validate_name": "メッシュ名を検証中",
"AttachMesh.parent_mesh": "メッシュをアーマチュアの子に設定中",
"AttachMesh.setup_weights": "頂点ウェイトを設定中",
"AttachMesh.create_bone": "取り付けボーンを作成中",
"AttachMesh.position_bone": "ボーンを配置中",
"AttachMesh.add_modifier": "アーマチュアモディファイアを追加中",
"AttachMesh.error.bone_not_found": "取り付けボーン'{bone}'が見つかりません",
"AttachMesh.error.mesh_not_found": "メッシュが見つかりません",
"AttachMesh.error.non_uniform_scale": "メッシュに不均一なスケールがあります。スケールを適用してください",
"AttachBone.search_desc": "ターゲットボーンを検索",
"AttachBone.select": "ターゲットボーンを選択",
"AttachBone.select_desc": "メッシュを取り付けるボーンを選択",
"MergeArmature.label": "アーマチュアを結合",
"MergeArmature.desc": "2つのアーマチュアを一緒に結合",
"MergeArmature.options": "結合オプション",
"MergeArmature.warn_two": "結合するには少なくとも2つのアーマチュアが必要です",
"MergeArmature.into": "結合先",
"MergeArmature.into_desc": "結合先のターゲットアーマチュア",
"MergeArmature.into_search_desc": "ターゲットアーマチュアを検索",
"MergeArmature.from": "結合元",
"MergeArmature.from_desc": "結合元のソースアーマチュア",
"MergeArmature.from_search_desc": "ソースアーマチュアを検索",
"MergeArmature.error.not_found": "アーマチュア'{name}'が見つかりません",
"MergeArmature.error.transforms_not_aligned": "このアーマチュアを結合するには変形を適用する必要があります。手動で行うか、変形適用のチェックマークを使用してください",
"MergeArmature.error.check_transforms": "親の変形を確認してください",
"MergeArmature.error.fix_parents": "親子関係を修正してください",
"MergeArmature.progress.removing_rigidbodies": "剛体とジョイントを削除中",
"MergeArmature.progress.validating": "アーマチュアを検証中",
"MergeArmature.progress.merging": "アーマチュアを結合中",
"MergeArmature.success": "アーマチュアが正常に結合されました",
"MergeArmature.merge_all": "同名ボーンを結合",
"MergeArmature.merge_all_desc": "一致する名前を持つボーンを結合",
"MergeArmature.apply_transforms": "変形を適用",
"MergeArmature.apply_transforms_desc": "結合前にすべての変形を適用",
"MergeArmature.join_meshes": "メッシュを結合",
"MergeArmature.join_meshes_desc": "結合後にメッシュを結合",
"MergeArmature.remove_zero_weights": "ゼロウェイトを削除",
"MergeArmature.remove_zero_weights_desc": "ウェイトのない頂点グループを削除",
"MergeArmature.cleanup_shape_keys": "シェイプキーをクリーンアップ",
"MergeArmature.cleanup_shape_keys_desc": "未使用のシェイプキーを削除",
"TextureAtlas.atlas_completed": "テクスチャアトラス作成が完了しました",
"TextureAtlas.atlas_error": "テクスチャアトラス作成中にエラーが発生しました",
"TextureAtlas.atlas_materials": "アトラスマテリアル",
"TextureAtlas.atlas_materials_desc": "モデルを最適化するためのアトラスマテリアル",
"TextureAtlas.label": "テクスチャアトラス化",
"TextureAtlas.loaded_list": "読み込まれたテクスチャアトラスマテリアルリスト",
"TextureAtlas.material_list_label": "テクスチャアトラスマテリアルリストマテリアル",
"TextureAtlas.reload_list": "テクスチャアトラスマテリアルリストを再読み込み",
"TextureAtlas.error.label": "エラー",
"TextureAtlas.none.label": "なし",
"TextureAtlas.no_nodes_error.desc": "このマテリアルはノードを使用していません!",
"TextureAtlas.no_images_error.desc": "このマテリアルには画像がありません!",
"TextureAtlas.texture_use_atlas.desc": "{name}マップアトラスに使用るテクスチャ",
"TextureAtlas.no_materials_selected": "アトラス用のマテリアルが選択されていません",
"Optimization.select_armature": "アーマチュアを選択してください",
"CheckForUpdateButton.label": "アップデートを確認",
"CheckForUpdateButton.desc": "利用可能なアップデートを確認",
"UpdateToLatestButton.label": "最新バージョンにアップデート"
"TextureAtlas.texture_use_atlas.desc": "{name}マップアトラスに使用されるテクスチャ",
"TextureAtlas.albedo": "アルベド",
"TextureAtlas.normal": "法線",
"TextureAtlas.emission": "発光",
"TextureAtlas.ambient_occlusion": "アンビエントオクルージョン",
"TextureAtlas.height": "高さ",
"TextureAtlas.roughness": "粗さ",
"TextureAtlas.description_1": "テクスチャを組み合わせた単一のマテリアルを作成",
"TextureAtlas.description_2": "アバターのパフォーマンスを最適化します。",
"TextureAtlas.texture_maps": "テクスチャマップ",
"TextureAtlas.material_ready": "マテリアルはアトラス作成の準備ができています",
"TextureAtlas.material_not_ready": "マテリアルには少なくとも1つのテクスチャが必要です",
"TextureAtlas.select_all_tooltip": "すべてのマテリアルを選択",
"TextureAtlas.select_none_tooltip": "すべての選択を解除",
"TextureAtlas.expand_all_tooltip": "すべてのマテリアル設定を展開",
"TextureAtlas.collapse_all_tooltip": "すべてのマテリアル設定を折りたたむ",
"TextureAtlas.estimated_size": "推定アトラスサイズ",
"TextureAtlas.materials": "マテリアル",
"TextureAtlas.no_materials_selected": "マテリアルが選択されていません",
"TextureAtlas.select_armature_first": "最初にアーマチュアを選択してください",
"TextureAtlas.how_to_use_1": "1. シーン内のアーマチュアを選択",
"TextureAtlas.how_to_use_2": "2. 「マテリアルを読み込む」をクリックして開始",
"TextureAtlas.load_error": "マテリアルの読み込みエラー。詳細はコンソールを確認してください。",
"TextureAtlas.material_not_included": "マテリアルはアトラスに含まれていません",
"TextureAtlas.save_file_first": "テクスチャアトラスを作成する前に、Blenderファイルを保存してください",
"TextureAtlas.save_file_instructions": "ファイル > 名前を付けて保存... を使用するか、下のボタンをクリックしてください:",
"TextureAtlas.save_file_button": "Blenderファイルを保存",
"TextureAtlas.save_file_required": "ファイルの保存が必要です",
"Settings.label": "設定",
"Settings.language": "言語",
"Settings.language_desc": "インターフェース言語を選択",
"Settings.validation_mode": "検証モード",
"Settings.validation_mode_desc": "アーマチュアをどの程度厳密に検証するかを選択",
"Settings.validation_mode.strict": "厳格",
"Settings.validation_mode.strict_desc": "ボーン階層と対称性を含む完全な検証",
"Settings.validation_mode.basic": "基本",
"Settings.validation_mode.basic_desc": "必須ボーンのチェックのみ",
"Settings.validation_mode.none": "なし",
"Settings.validation_mode.none_desc": "アーマチュア検証なし",
"Settings.debug": "デバッグ設定",
"Settings.logging": "ログ記録",
"Settings.enable_logging": "デバッグログを有効化",
"Settings.enable_logging_desc": "トラブルシューティングのための詳細なデバッグログを有効化",
"Settings.logging_enabled": "デバッグログが有効になりました",
"Settings.logging_disabled": "デバッグログが無効になりました",
"Settings.highlight_problem_bones": "問題のあるボーンを強調表示",
"Settings.highlight_problem_bones_desc": "ビューポートで検証に問題のあるボーンを強調表示",
"Settings.bone_highlighting": "ボーンの強調表示",
"Language.auto": "自動",
"Language.en_US": "英語",
"Language.ja_JP": "日本語",
"Language.ko_KR": "韓国語",
"Language.changed.title": "言語が変更されました",
"Language.changed.success": "言語が正常に変更されました!",
"Language.changed.restart": "一部のUI要素はBlenderの再起動が必要な場合があります"
}
}
}
+525
View File
@@ -0,0 +1,525 @@
{
"authors": ["Avatar Toolkit Team"],
"messages": {
"AvatarToolkit.label": "아바타 툴킷 (알파 0.2.1)",
"AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로",
"AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면",
"AvatarToolkit.desc3": "Github에 보고해 주세요.",
"Updater.label": "업데이터",
"Updater.CheckForUpdateButton.label": "업데이트 확인",
"Updater.CheckForUpdateButton.label_alt": "사용 가능한 업데이트 없음",
"Updater.UpdateToLatestButton.label": "{name}으로 업데이트",
"Updater.UpdateToSelectedButton.label": "업데이트",
"Updater.currentVersion": "현재 버전: {name}",
"Updater.selectVersion": "버전 선택",
"Updater.CheckForUpdateButton.desc": "사용 가능한 업데이트 확인",
"UpdateToLatestButton.desc": "최신 버전으로 업데이트",
"UpdateNotificationPopup.label": "업데이트 알림",
"UpdateNotificationPopup.desc": "사용 가능한 업데이트에 대한 알림",
"UpdateNotificationPopup.newUpdate": "새 업데이트 사용 가능: {version}",
"RestartBlenderPopup.label": "블렌더 재시작",
"RestartBlenderPopup.desc": "업데이트를 완료하려면 블렌더를 재시작하세요",
"RestartBlenderPopup.message": "업데이트 성공! 블렌더를 재시작해 주세요.",
"check_for_update.cantCheck": "업데이트를 확인할 수 없습니다",
"download_file.cantConnect": "업데이트 서버에 연결할 수 없습니다",
"download_file.cantFindZip": "업데이트 파일을 찾을 수 없습니다",
"download_file.cantFindAvatarToolkit": "업데이트 패키지에서 아바타 툴킷 파일을 찾을 수 없습니다",
"QuickAccess.label": "빠른 접근",
"QuickAccess.select_armature": "아마추어 선택",
"QuickAccess.valid_armature": "유효한 아마추어",
"QuickAccess.bones_count": "본: {count}개",
"QuickAccess.pose_bones_available": "포즈 본: 사용 가능",
"QuickAccess.pose_controls": "포즈 컨트롤",
"QuickAccess.import_export": "가져오기/내보내기",
"QuickAccess.import": "가져오기",
"QuickAccess.export": "내보내기",
"QuickAccess.export_fbx": "FBX 내보내기",
"QuickAccess.export_resonite": "Resonite로 내보내기",
"QuickAccess.start_pose_mode.label": "포즈 모드 시작",
"QuickAccess.start_pose_mode.desc": "선택한 아마추어의 포즈 모드 진입",
"QuickAccess.stop_pose_mode.label": "포즈 모드 종료",
"QuickAccess.stop_pose_mode.desc": "포즈 모드 종료 및 변형 초기화",
"QuickAccess.apply_pose_as_shapekey.label": "포즈를 쉐이프 키로 적용",
"QuickAccess.apply_pose_as_shapekey.desc": "현재 포즈에서 새 쉐이프 키 생성",
"QuickAccess.apply_pose_as_rest.label": "포즈를 기본 포즈로 적용",
"QuickAccess.apply_pose_as_rest.desc": "현재 포즈를 기본 포즈로 적용",
"QuickAccess.apply_armature_failed": "아마추어 수정 적용 실패",
"QuickAccess.validation_basic_warning": "제한된 검증 활성화",
"QuickAccess.validation_basic_details": "필수 본 구조만 검증 중",
"QuickAccess.validation_none_warning": "검증 비활성화",
"QuickAccess.validation_none_details": "아마추어 검증 확인이 수행되지 않음",
"Quick_Access.import_success": "가져오기 성공",
"PoseMode.error.start": "포즈 모드 시작 실패: {error}",
"PoseMode.error.stop": "포즈 모드 종료 실패: {error}",
"PoseMode.error.shapekey": "포즈를 쉐이프 키로 적용 실패: {error}",
"PoseMode.error.rest_pose": "포즈를 기본 포즈로 적용 실패: {error}",
"PoseMode.shapekey.name": "쉐이프 키 이름",
"PoseMode.shapekey.description": "새 쉐이프 키의 이름",
"PoseMode.shapekey.default": "포즈_쉐이프키",
"PoseMode.skipped_meshes": "일부 메시가 건너뛰어졌습니다:\n{message}",
"PoseMode.basis": "기본",
"Armature.validation.no_armature": "선택된 아마추어 없음",
"Armature.validation.not_armature": "선택된 객체가 아마추어가 아님",
"Armature.validation.no_bones": "아마추어에 본이 없음",
"Armature.validation.basic_check_failed": "기본 아마추어 검증 실패",
"Armature.validation.missing_bones": "필수 본 누락: {bones}",
"Armature.validation.invalid_hierarchy": "{parent}와 {child} 사이의 유효하지 않은 본 계층 구조",
"Armature.validation.asymmetric_bones": "{bone}에 대한 대칭 본 누락",
"Armature.validation.asymmetric_hand_wrist": "손/손목에 대한 대칭 본 누락",
"Armature.validation.found_bones": "아마추어에서 발견된 본:\n- {bones}",
"Armature.validation.non_standard_bones": "비표준 본 발견:\n- {bones}",
"Armature.validation.accessory_bones_note.line1": "머리카락 본, 치마 본 또는 기타",
"Armature.validation.accessory_bones_note.line2": "액세서리 본의 이름이 주 아마추어",
"Armature.validation.accessory_bones_note.line3": "본과 유사하게 지정된 경우(예: Head1, Head2), 이름을",
"Armature.validation.accessory_bones_note.line4": "Hair_1, Skirt_1과 같은 더 설명적인 이름으로 변경하세요.",
"Armature.validation.standardize_note.line1": "도구 섹션의 '아마추어 표준화'",
"Armature.validation.standardize_note.line2": "버튼을 사용하여 아마추어를",
"Armature.validation.standardize_note.line3": "자동으로 표준화할 수 있습니다.",
"Validation.section.found_bones": "발견된 본",
"Validation.section.non_standard": "비표준 본",
"Validation.section.hierarchy": "계층 구조 문제",
"Validation.status.failed": "검증 실패",
"Validation.message.failed.line1": "아마추어 검증 실패",
"Validation.message.failed.line2": "아래에서 문제가 무엇인지",
"Validation.message.failed.line3": "확인하세요",
"Validation.highlight_problem_bones_desc": "뷰포트에서 검증 문제가 있는 본을 시각적으로 강조 표시",
"Validation.no_armature": "선택된 아마추어 없음",
"Validation.no_issues": "강조 표시할 검증 문제가 없음",
"Validation.highlighting_complete": "문제 본 강조 표시 성공",
"Validation.tpose.no_armature": "T-포즈 검증을 위한 아마추어를 찾을 수 없음",
"Validation.tpose.left_arm_not_horizontal": "왼쪽 팔이 수평 T-포즈 위치에 있지 않음",
"Validation.tpose.right_arm_not_horizontal": "오른쪽 팔이 수평 T-포즈 위치에 있지 않음",
"Validation.tpose.spine_not_vertical": "척추가 수직 위치에 있지 않음",
"Validation.tpose.warning": "T-포즈 검증 경고",
"Validation.tpose.recommendation": "Unity 또는 다른 플랫폼으로 가져오기 전에 T-포즈를 수정하는 것이 좋습니다",
"Validation.scale_issues": "비정상적인 크기의 본 감지:",
"Validation.scale_issue.too_small": "본이 매우 작음",
"Validation.scale_issue.too_large": "본이 매우 큼",
"Validation.section.scale_issues": "크기 문제",
"Validation.tpose.label": "T-포즈 검증",
"Validation.no_scale_issues": "크기 문제가 감지되지 않음",
"Validation.no_hierarchy_issues": "계층 구조 문제가 감지되지 않음",
"Validation.no_non_standard_issues": "비표준 본 문제가 감지되지 않음",
"Validation.tpose.valid": "T-포즈 검증 성공",
"Validation.tpose.desc": "아마추어가 적절한 T-포즈에 있는지 확인",
"Validation.highlight_problem_bones": "문제 본 강조 표시",
"Validation.clear_bone_highlighting": "본 강조 표시 지우기",
"Validation.clear_bone_highlighting_desc": "본 강조 표시를 제거하고 본 색상을 기본값으로 재설정",
"Validation.highlighting_cleared": "본 강조 표시 지우기 성공",
"Mesh.validation.no_data": "메시 데이터 없음",
"Mesh.validation.no_vertex_groups": "버텍스 그룹을 찾을 수 없음",
"Mesh.validation.no_armature_modifier": "아마추어 모디파이어 없음",
"Mesh.validation.valid": "포즈 작업에 유효한 메시",
"Operation.pose_applied": "포즈 적용 성공",
"Scene.avatar_toolkit_updater_version_list.name": "버전 목록",
"Scene.avatar_toolkit_updater_version_list.description": "사용 가능한 버전 목록",
"Optimization.label": "최적화",
"Optimization.materials_title": "재질",
"Optimization.cleanup_title": "메시 정리",
"Optimization.join_meshes_title": "메시 결합",
"Optimization.combine_materials": "재질 결합",
"Optimization.combine_materials_desc": "드로우 콜을 줄이기 위해 유사한 재질 결합",
"Optimization.remove_doubles": "중복 제거",
"Optimization.remove_doubles_desc": "중복 버텍스 제거",
"Optimization.remove_doubles_advanced": "고급",
"Optimization.remove_doubles_advanced_desc": "고급 옵션으로 중복 버텍스 제거",
"Optimization.join_all_meshes": "모두 결합",
"Optimization.join_all_meshes_desc": "씬의 모든 메시 결합",
"Optimization.join_selected_meshes": "선택 항목 결합",
"Optimization.join_selected_meshes_desc": "선택한 메시만 결합",
"Optimization.no_meshes": "최적화할 메시를 찾을 수 없음",
"Optimization.materials_combined": "{combined}개의 재질 결합, {cleaned}개의 슬롯 정리, {removed}개의 미사용 데이터 블록 제거",
"Optimization.error.combine_materials": "재질 결합 실패: {error}",
"Optimization.materials_total": "총 재질: {count}개",
"Optimization.materials_duplicates": "잠재적 중복: {count}개",
"Optimization.no_materials": "메시에서 재질을 찾을 수 없음",
"Optimization.error.consolidation": "재질 통합 실패. 자세한 내용은 콘솔을 확인하세요",
"Optimization.combining_materials": "유사한 재질 결합 중...",
"Optimization.cleaning_slots": "재질 슬롯 정리 중...",
"Optimization.removing_unused": "미사용 재질 제거 중...",
"Optimization.selecting_meshes": "메시 선택 중...",
"Optimization.joining_meshes": "메시 결합 중...",
"Optimization.applying_transforms": "변형 적용 중...",
"Optimization.fixing_uvs": "UV 좌표 수정 중...",
"Optimization.finalizing": "마무리 중...",
"Optimization.meshes_joined": "모든 메시 결합 성공",
"Optimization.selected_meshes_joined": "선택한 메시 결합 성공",
"Optimization.no_mesh_selected": "선택된 메시 없음",
"Optimization.select_at_least_two": "최소 두 개의 메시를 선택하세요",
"Optimization.error.join_meshes": "메시 결합 실패: {error}",
"Optimization.error.join_selected": "선택한 메시 결합 실패: {error}",
"Optimization.merge_distance": "병합 거리",
"Optimization.merge_distance_desc": "버텍스가 병합될 거리",
"Optimization.remove_doubles_warning": "이 과정은 시간이 오래 걸릴 수 있습니다",
"Optimization.remove_doubles_wait": "이 작업 중에는 블렌더가 응답하지 않는 것처럼 보일 수 있습니다",
"Optimization.error.remove_doubles": "중복 제거 실패: {error}",
"Optimization.no_armature": "선택된 아마추어 없음",
"Optimization.processing_mesh": "메시 처리 중: {name}",
"Optimization.processing_shapekey": "쉐이프 키 처리 중: {name}",
"Optimization.remove_doubles_completed": "중복 제거 완료 성공",
"Tools.label": "도구",
"Tools.general_title": "일반 도구",
"Tools.select_armature": "아마추어 선택",
"Tools.convert_resonite": "Resonite로 변환",
"Tools.convert_resonite_desc": "Resonite에서 사용하기 위해 모델 변환",
"Tools.convert_resonite.operation": "Resonite로 변환 중",
"Tools.separate_title": "분리 도구",
"Tools.separate_materials": "재질별",
"Tools.separate_materials_desc": "재질별로 메시 분리",
"Tools.separate_loose": "분리된 부분",
"Tools.separate_loose_desc": "메시를 분리된 부분으로 나누기",
"Tools.separate_materials_success": "메시가 재질별로 성공적으로 분리됨",
"Tools.separate_loose_success": "메시가 분리된 부분으로 성공적으로 나뉨",
"Tools.bone_title": "본 도구",
"Tools.create_digitigrade": "디지티그레이드 다리 생성",
"Tools.create_digitigrade_desc": "다리를 디지티그레이드 설정으로 변환",
"Tools.digitigrade": "디지티그레이드 다리 생성",
"Tools.digitigrade_desc": "선택한 다리 본을 디지티그레이드 설정으로 변환",
"Tools.digitigrade_error": "디지티그레이드 다리 생성 실패: {error}",
"Tools.digitigrade_success": "디지티그레이드 다리 설정 생성 성공",
"Tools.processing_leg": "다리 본 처리 중: {bone}",
"Tools.merge_twist_bones": "트위스트 본 유지",
"Tools.merge_twist_bones_desc": "체크하면 가중치가 0이더라도 트위스트 본이 유지됩니다",
"Tools.clean_weights": "가중치 0인 본 제거",
"Tools.clean_weights_desc": "버텍스 가중치가 없는 본 제거",
"Tools.preserve_parent_bones": "부모 본 보존",
"Tools.preserve_parent_bones_desc": "가중치가 없더라도 자식이 있는 본 유지",
"Tools.target_bone_type": "대상 본 유형",
"Tools.target_bone_type_desc": "처리할 본 유형 필터링",
"Tools.target_all_bones": "모든 본",
"Tools.target_deform_bones": "변형 본만",
"Tools.target_non_deform_bones": "비변형 본만",
"Tools.list_only_mode": "목록 모드만",
"Tools.list_only_mode_desc": "제거하는 대신 가중치 0인 본 나열",
"Tools.zero_weight_bones_found": "가중치 0인 본 발견: {bones}",
"Tools.remove_selected_bones": "선택한 본 제거",
"Tools.remove_selected_bones_desc": "아마추어에서 선택한 가중치 0인 본 제거",
"Tools.bones_removed": "{count}개의 본 제거됨",
"Tools.clean_constraints": "본 제약 조건 삭제",
"Tools.clean_constraints_desc": "아마추어에서 모든 본 제약 조건 제거",
"Tools.clean_constraints_success": "{count}개의 본 제약 조건 제거됨",
"Tools.processing_bone_constraints": "본에서 제약 조건 제거 중: {bone}",
"Tools.clean_weights_success": "{count}개의 가중치 0인 본 제거됨",
"Tools.clean_weights_threshold": "가중치 임계값",
"Tools.clean_weights_threshold_desc": "본이 가중치를 가진 것으로 간주하는 최소 가중치 값",
"Tools.merge_title": "병합 도구",
"Tools.merge_to_active": "활성 본으로 병합",
"Tools.merge_to_active_desc": "선택한 본을 활성 본으로 병합",
"Tools.merge_to_parent": "부모로 병합",
"Tools.merge_to_parent_desc": "본을 각각의 부모로 병합",
"Tools.connect_bones": "본 연결",
"Tools.connect_bones_desc": "체인에서 연결되지 않은 본 연결",
"Tools.additional_title": "추가 도구",
"Tools.apply_transforms": "변형 적용",
"Tools.apply_transforms_desc": "객체에 모든 변형 적용",
"Tools.clean_shapekeys": "미사용 쉐이프 키 제거",
"Tools.clean_shapekeys_desc": "메시에서 미사용 쉐이프 키 제거",
"Tools.bones_translated_success": "모든 본 번역 성공",
"Tools.bones_translated_with_fails": "번역 완료, {translate_bone_fails}개의 번역되지 않은 본",
"Tools.storing_transforms": "본 변형 저장 중...",
"Tools.analyzing_weights": "버텍스 가중치 분석 중...",
"Tools.removing_bones": "가중치 없는 본 제거 중...",
"Tools.verifying_hierarchy": "본 계층 구조 확인 중...",
"Tools.connect_bones_min_distance": "최소 거리",
"Tools.connect_bones_min_distance_desc": "연결을 시도할 본 사이의 최소 거리",
"Tools.connect_bones_success": "{count}개의 본 연결됨",
"Tools.merge_weights_threshold": "가중치 전송 임계값",
"Tools.merge_weights_threshold_desc": "본 병합 시 전송할 최소 가중치 값",
"Tools.no_bones_selected": "병합할 본이 선택되지 않음",
"Tools.no_bones_with_parent": "부모가 있는 선택된 본을 찾을 수 없음",
"Tools.merge_to_active_success": "{count}개의 본을 활성 본으로 성공적으로 병합",
"Tools.merge_to_parent_success": "{count}개의 본을 부모로 성공적으로 병합",
"Tools.transforms_applied": "변형 적용 성공",
"Tools.shapekey_tolerance": "쉐이프 키 허용 오차",
"Tools.shapekey_tolerance_desc": "쉐이프 키가 사용된 것으로 간주하는 최소 차이",
"Tools.shapekeys_removed": "{count}개의 미사용 쉐이프 키 제거됨",
"Tools.rigify_title": "Rigify 도구",
"Tools.convert_rigify_to_unity": "Rigify를 Unity로 변환",
"Tools.convert_rigify_to_unity_desc": "Rigify 아마추어를 Unity 호환 형식으로 변환",
"Tools.rigify_converted": "Rigify 아마추어 변환 성공",
"Tools.no_armature": "선택된 아마추어 없음",
"Tools.standardize_title": "표준화",
"Tools.standardize_armature": "아마추어 표준화",
"Tools.standardize_armature_desc": "비표준 아마추어를 아바타 툴킷 표준으로 변환",
"Tools.standardize_fix_names": "본 이름 수정",
"Tools.standardize_fix_names_desc": "본 이름을 표준 명명 규칙에 맞게 변경",
"Tools.standardize_fix_hierarchy": "본 계층 구조 수정",
"Tools.standardize_fix_hierarchy_desc": "본 사이의 부모-자식 관계 수정",
"Tools.standardize_fix_scale": "본 크기 수정",
"Tools.standardize_fix_scale_desc": "크기 문제를 해결하기 위해 본 길이 정규화",
"Tools.standardize_warning": "이 작업은 아마추어를 수정합니다. 먼저 백업을 만드세요!",
"Tools.standardize_success": "아마추어 표준화 성공",
"Tools.standardize_partial": "아마추어가 부분적으로 표준화되었습니다. 일부 문제가 남아 있습니다.",
"Tools.standardize_already_valid": "아마추어가 이미 표준을 충족합니다. 변경이 필요하지 않습니다.",
"Tools.standardize_issues_title": "표준화 문제",
"Tools.standardize_issues_header": "표준화 후에도 일부 문제가 남아 있습니다",
"Tools.standardize_issues_line1": "이는 아바타의 일부 본이 인식되지 않는 고유한 이름을 가지고 있기 때문일 수 있습니다",
"Tools.standardize_issues_line2": "우리의 비표준 본 인식 목록에 없는 이름입니다.",
"Tools.standardize_issues_line3": "예를 들어, 엉덩이 본의 이름이 'THISISMYHIPS'인 경우 감지할 수 없습니다.",
"Tools.standardize_issues_line4": "주요 골격 본이 인식되지 않는 경우 GitHub에 보고해 주세요",
"Tools.standardize_issues_line5": "데이터베이스에 추가할 수 있도록 합니다.",
"Tools.standardize_issues_line6": "액세서리 본(머리카락, 의류 등)은 수동으로 이름을 변경해야 합니다.",
"UVTools.uv_title": "UV 도구",
"UVTools.too_many_vertices": "오류! 너무 많은 항목이 선택되었습니다. 두 개의 엣지를 선택하고 있는지 확인하세요!",
"UVTools.need_line": "선택된 각 객체에 대해 UV 포인트의 한 줄이 필요합니다. 객체 \"{obj}\"는 이 요구 사항을 충족하지 않습니다!",
"UVTools.align_edges": "UV 엣지를 대상에 정렬",
"UVTools.align_edges_desc": "각 선택된 메시의 UV 포인트 선을 활성 메시의 선택된 UV 포인트 선에 정렬합니다. 한 모델의 텍스처를 다른 모델에 적용할 때 유용합니다. 각 메시에서 UV 포인트 선의 시작을 식별하기 위해 2D 커서로부터의 거리를 사용합니다.",
"Visemes.panel_label": "비셈",
"Visemes.shape_selection": "쉐이프 키 선택",
"Visemes.controls": "비셈 컨트롤",
"Visemes.no_shapekeys": "쉐이프 키가 있는 메시 선택",
"Visemes.mouth_a": "A 모양",
"Visemes.mouth_a_desc": "'A' 소리에 대한 쉐이프 키",
"Visemes.mouth_o": "O 모양",
"Visemes.mouth_o_desc": "'O' 소리에 대한 쉐이프 키",
"Visemes.mouth_ch": "CH 모양",
"Visemes.mouth_ch_desc": "'CH' 소리에 대한 쉐이프 키",
"Visemes.shape_intensity": "쉐이프 강도",
"Visemes.shape_intensity_desc": "비셈 쉐이프의 강도 배율",
"Visemes.start_preview": "미리보기 시작",
"Visemes.stop_preview": "미리보기 중지",
"Visemes.preview_mode_desc": "비셈 미리보기 모드 전환",
"Visemes.preview_selection": "미리보기 선택",
"Visemes.preview_selection_desc": "미리볼 비셈 선택",
"Visemes.preview_label": "비셈 미리보기",
"Visemes.preview_desc": "뷰포트에서 비셈 쉐이프 미리보기",
"Visemes.create_label": "비셈 생성",
"Visemes.create_desc": "VRC 비셈 쉐이프 키 생성",
"Visemes.error.no_shapekeys": "메시에 쉐이프 키가 없음",
"Visemes.error.select_shapekeys": "A, O 및 CH에 대한 쉐이프 키를 선택하세요",
"Visemes.success": "비셈 생성 성공",
"Visemes.mesh_select": "메시 선택",
"Visemes.mesh_select_desc": "비셈을 생성할 메시 선택",
"EyeTracking.label": "시선 추적",
"EyeTracking.setup": "시선 추적 설정",
"EyeTracking.mesh_select": "메시 선택",
"EyeTracking.bones": "본 선택",
"EyeTracking.head_bone": "머리 본",
"EyeTracking.eye_left": "왼쪽 눈 본",
"EyeTracking.eye_right": "오른쪽 눈 본",
"EyeTracking.shapekeys": "쉐이프 키 선택",
"EyeTracking.options": "옵션",
"EyeTracking.rotation": "눈 회전",
"EyeTracking.rotation.x": "수직 회전",
"EyeTracking.rotation.y": "수평 회전",
"EyeTracking.adjust": "눈 조정",
"EyeTracking.blinking": "깜빡임 컨트롤",
"EyeTracking.no_shapekeys": "선택한 메시에서 쉐이프 키를 찾을 수 없음",
"EyeTracking.no_armature": "선택된 아마추어 없음",
"EyeTracking.no_mesh": "메시를 찾을 수 없음",
"EyeTracking.create.label": "시선 추적 생성",
"EyeTracking.create.desc": "시선 추적 본 및 쉐이프 키 설정",
"EyeTracking.testing.start.label": "테스트 시작",
"EyeTracking.testing.start.desc": "시선 추적 테스트 모드 진입",
"EyeTracking.testing.stop.label": "테스트 중지",
"EyeTracking.testing.stop.desc": "시선 추적 테스트 모드 종료",
"EyeTracking.reset.label": "시선 추적 재설정",
"EyeTracking.reset.desc": "모든 시선 추적 설정 재설정",
"EyeTracking.rotate.label": "눈 본 회전",
"EyeTracking.rotate.desc": "VRChat 호환성을 위한 눈 본 회전",
"EyeTracking.iris.label": "홍채 높이 조정",
"EyeTracking.iris.desc": "홍채 버텍스의 높이 조정",
"EyeTracking.blink.test.label": "깜빡임 테스트",
"EyeTracking.blink.test.desc": "눈 깜빡임 쉐이프 키 테스트",
"EyeTracking.lowerlid.test.label": "아래 눈꺼풀 테스트",
"EyeTracking.lowerlid.test.desc": "아래 눈꺼풀 쉐이프 키 테스트",
"EyeTracking.blink.reset.label": "깜빡임 테스트 재설정",
"EyeTracking.blink.reset.desc": "깜빡임 테스트 값 재설정",
"EyeTracking.validation.noArmature": "씬에서 아마추어를 찾을 수 없음",
"EyeTracking.validation.noMesh": "메시 '{mesh}'를 찾을 수 없음",
"EyeTracking.validation.noShapekeys": "선택한 메시에 쉐이프 키가 없음",
"EyeTracking.validation.leftEye": "왼쪽 눈",
"EyeTracking.validation.rightEye": "오른쪽 눈",
"EyeTracking.validation.missingGroups": "누락된 버텍스 그룹: {groups}",
"EyeTracking.validation.missingBones": "필요한 본 누락: {bones}",
"EyeTracking.validation.success": "시선 추적 설정 검증 성공",
"EyeTracking.error.noMesh": "시선 추적을 위한 메시가 선택되지 않음",
"EyeTracking.error.noVertexGroup": "본에 대한 버텍스 그룹을 찾을 수 없음: {bone}",
"EyeTracking.error.noShapeSelected": "모든 필수 쉐이프 키를 선택하세요",
"EyeTracking.success": "시선 추적 설정 완료 성공",
"EyeTracking.mode_select": "모드 선택",
"EyeTracking.mesh_setup": "메시 설정",
"EyeTracking.bone_setup": "본 설정",
"EyeTracking.shapekey_setup": "쉐이프 키 설정",
"EyeTracking.testing": "테스트 모드",
"EyeTracking.rotation_controls": "눈 회전 컨트롤",
"EyeTracking.adjustments": "눈 조정",
"EyeTracking.blink_testing": "깜빡임 테스트",
"EyeTracking.wink_left": "왼쪽 윙크",
"EyeTracking.wink_right": "오른쪽 윙크",
"EyeTracking.lowerlid_left": "왼쪽 아래 눈꺼풀",
"EyeTracking.lowerlid_right": "오른쪽 아래 눈꺼풀",
"EyeTracking.mode.creation": "생성 모드",
"EyeTracking.mode.testing": "테스트 모드",
"EyeTracking.disable_blinking": "눈 깜빡임 비활성화",
"EyeTracking.disable_movement": "눈 움직임 비활성화",
"EyeTracking.distance": "눈 거리",
"EyeTracking.distance_desc": "눈 사이의 거리 조정",
"EyeTracking.mode": "시선 추적 모드",
"EyeTracking.mesh_name": "메시",
"EyeTracking.mesh_name_desc": "시선 추적을 위한 메시 선택",
"EyeTracking.head_bone_desc": "머리 본 선택",
"EyeTracking.eye_left_desc": "왼쪽 눈 본 선택",
"EyeTracking.eye_right_desc": "오른쪽 눈 본 선택",
"EyeTracking.type": "시선 추적 유형",
"EyeTracking.type_desc": "생성할 시선 추적 설정 유형 선택",
"EyeTracking.create.av3.label": "AV3 시선 추적 생성",
"EyeTracking.create.av3.desc": "VRChat 아바타 3.0용 시선 추적 설정",
"EyeTracking.create.sdk2.label": "SDK2 시선 추적 생성",
"EyeTracking.create.sdk2.desc": "VRChat SDK2용 시선 추적 설정",
"EyeTracking.sdk_version": "SDK 버전",
"EyeTracking.type.av3": "아바타 3.0",
"EyeTracking.type.av3_desc": "VRChat 아바타 3.0 시선 추적 설정",
"EyeTracking.type.sdk2": "레거시 (ChilloutVR)",
"EyeTracking.type.sdk2_desc": "레거시 (SDK2) 시선 추적 설정",
"EyeTracking.adjust.label": "눈 위치 조정",
"EyeTracking.adjust.desc": "버텍스 그룹을 기반으로 눈 본 위치 조정",
"EyeTracking.sdk2_warning": "레거시 (SDK2) 시선 추적 알림",
"EyeTracking.sdk2_warning_detail1": "이 시스템은 VRChat에 사용해서는 안 됩니다,",
"EyeTracking.sdk2_warning_detail2": "시선 추적은 이제 Unity에서 직접",
"EyeTracking.sdk2_warning_detail3": "구성됩니다. ChilloutVR과 같은 다른 플랫폼을",
"EyeTracking.sdk2_warning_detail4": "위해 남아 있습니다.",
"CustomPanel.label": "커스텀 아바타 도구",
"CustomPanel.merge_mode": "병합 모드",
"CustomPanel.mesh_selection": "메시 선택",
"CustomPanel.select_mesh": "메시 선택",
"CustomPanel.select_bone": "본 선택",
"CustomPanel.select_armature": "아마추어 선택",
"CustomPanel.mode.armature": "아마추어",
"CustomPanel.mode.armature_desc": "아마추어 병합",
"CustomPanel.mode.mesh": "메시",
"CustomPanel.mode.mesh_desc": "메시를 아마추어에 부착",
"AttachMesh.label": "메시 부착",
"AttachMesh.desc": "자동 가중치 설정으로 메시를 아마추어 본에 부착",
"AttachMesh.search_desc": "부착할 메시 검색",
"AttachMesh.select": "부착할 메시 선택",
"AttachMesh.select_desc": "아마추어에 부착할 메시 선택",
"AttachMesh.success": "메시 부착 성공",
"AttachMesh.warn_no_armature": "부착할 아마추어와 메시를 선택하세요",
"AttachMesh.validate_transforms": "메시 변형 검증 중",
"AttachMesh.validate_name": "메시 이름 검증 중",
"AttachMesh.parent_mesh": "메시를 아마추어에 부모 설정 중",
"AttachMesh.setup_weights": "버텍스 가중치 설정 중",
"AttachMesh.create_bone": "부착 본 생성 중",
"AttachMesh.position_bone": "본 위치 지정 중",
"AttachMesh.add_modifier": "아마추어 모디파이어 추가 중",
"AttachMesh.error.bone_not_found": "부착 본 '{bone}'을(를) 찾을 수 없음",
"AttachMesh.error.mesh_not_found": "메시를 찾을 수 없음",
"AttachMesh.error.non_uniform_scale": "메시의 크기가 균일하지 않습니다. 크기를 적용하세요",
"AttachBone.search_desc": "대상 본 검색",
"AttachBone.select": "대상 본 선택",
"AttachBone.select_desc": "메시를 부착할 본 선택",
"MergeArmature.label": "아마추어 병합",
"MergeArmature.desc": "두 아마추어 병합",
"MergeArmature.options": "병합 옵션",
"MergeArmature.warn_two": "병합하려면 최소 두 개의 아마추어가 필요합니다",
"MergeArmature.into": "병합 대상",
"MergeArmature.into_desc": "병합할 대상 아마추어",
"MergeArmature.into_search_desc": "대상 아마추어 검색",
"MergeArmature.from": "병합 소스",
"MergeArmature.from_desc": "병합할 소스 아마추어",
"MergeArmature.from_search_desc": "소스 아마추어 검색",
"MergeArmature.error.not_found": "아마추어 '{name}'을(를) 찾을 수 없음",
"MergeArmature.error.transforms_not_aligned": "이 아마추어를 병합하려면 변형을 적용해야 합니다. 수동 방법 또는 변형 적용 체크박스를 통해 이 작업을 수행하세요",
"MergeArmature.error.check_transforms": "부모 변형을 확인하세요",
"MergeArmature.error.fix_parents": "부모 관계를 수정하세요",
"MergeArmature.progress.removing_rigidbodies": "리지드 바디 및 조인트 제거 중",
"MergeArmature.progress.validating": "아마추어 검증 중",
"MergeArmature.progress.merging": "아마추어 병합 중",
"MergeArmature.success": "아마추어 병합 성공",
"MergeArmature.merge_all": "동일한 본 병합",
"MergeArmature.merge_all_desc": "일치하는 이름을 가진 본 병합",
"MergeArmature.apply_transforms": "변형 적용",
"MergeArmature.apply_transforms_desc": "병합 전 모든 변형 적용",
"MergeArmature.join_meshes": "메시 결합",
"MergeArmature.join_meshes_desc": "병합 후 메시 결합",
"MergeArmature.remove_zero_weights": "가중치 0 제거",
"MergeArmature.remove_zero_weights_desc": "가중치가 없는 버텍스 그룹 제거",
"MergeArmature.cleanup_shape_keys": "쉐이프 키 정리",
"MergeArmature.cleanup_shape_keys_desc": "미사용 쉐이프 키 제거",
"TextureAtlas.atlas_completed": "텍스처 아틀라스 생성 완료",
"TextureAtlas.atlas_error": "텍스처 아틀라스 생성 중 오류 발생",
"TextureAtlas.atlas_materials": "아틀라스 재질",
"TextureAtlas.atlas_materials_desc": "모델을 최적화하기 위한 아틀라스 재질",
"TextureAtlas.label": "텍스처 아틀라싱",
"TextureAtlas.loaded_list": "로드된 텍스처 아틀라스 재질 목록",
"TextureAtlas.material_list_label": "텍스처 아틀라스 재질 목록 재질",
"TextureAtlas.reload_list": "텍스처 아틀라스 재질 목록 다시 로드",
"TextureAtlas.error.label": "오류",
"TextureAtlas.none.label": "없음",
"TextureAtlas.no_nodes_error.desc": "이 재질은 노드를 사용하지 않습니다!",
"TextureAtlas.no_images_error.desc": "이 재질에는 이미지가 없습니다!",
"TextureAtlas.texture_use_atlas.desc": "{name} 맵 아틀라스에 사용될 텍스처",
"TextureAtlas.albedo": "알베도",
"TextureAtlas.normal": "노멀",
"TextureAtlas.emission": "이미션",
"TextureAtlas.ambient_occlusion": "앰비언트 오클루전",
"TextureAtlas.height": "높이",
"TextureAtlas.roughness": "거칠기",
"TextureAtlas.description_1": "텍스처를 결합한 단일 재질 생성",
"TextureAtlas.description_2": "아바타의 성능을 최적화합니다.",
"TextureAtlas.texture_maps": "텍스처 맵",
"TextureAtlas.material_ready": "재질이 아틀라스 생성 준비가 되었습니다",
"TextureAtlas.material_not_ready": "재질에는 최소 하나의 텍스처가 필요합니다",
"TextureAtlas.select_all_tooltip": "모든 재질 선택",
"TextureAtlas.select_none_tooltip": "모든 선택 해제",
"TextureAtlas.expand_all_tooltip": "모든 재질 설정 펼치기",
"TextureAtlas.collapse_all_tooltip": "모든 재질 설정 접기",
"TextureAtlas.estimated_size": "예상 아틀라스 크기",
"TextureAtlas.materials": "재질",
"TextureAtlas.no_materials_selected": "선택된 재질이 없습니다",
"TextureAtlas.select_armature_first": "먼저 아마추어를 선택해주세요",
"TextureAtlas.how_to_use_1": "1. 장면에서 아마추어 선택",
"TextureAtlas.how_to_use_2": "2. '재질 불러오기'를 클릭하여 시작",
"TextureAtlas.load_error": "재질 로딩 오류. 자세한 내용은 콘솔을 확인하세요.",
"TextureAtlas.material_not_included": "재질이 아틀라스에 포함되지 않았습니다",
"TextureAtlas.save_file_first": "텍스처 아틀라스를 만들기 전에 Blender 파일을 저장하세요",
"TextureAtlas.save_file_instructions": "파일 > 다른 이름으로 저장... 을 사용하거나 아래 버튼을 클릭하세요:",
"TextureAtlas.save_file_button": "Blender 파일 저장",
"TextureAtlas.save_file_required": "파일 저장 필요",
"Settings.label": "설정",
"Settings.language": "언어",
"Settings.language_desc": "인터페이스 언어 선택",
"Settings.validation_mode": "검증 모드",
"Settings.validation_mode_desc": "아마추어 검증 엄격성 선택",
"Settings.validation_mode.strict": "엄격",
"Settings.validation_mode.strict_desc": "본 계층 구조 및 대칭성을 포함한 전체 검증",
"Settings.validation_mode.basic": "기본",
"Settings.validation_mode.basic_desc": "필수 본 확인만",
"Settings.validation_mode.none": "없음",
"Settings.validation_mode.none_desc": "아마추어 검증 없음",
"Settings.debug": "디버그 설정",
"Settings.logging": "로깅",
"Settings.enable_logging": "디버그 로깅 활성화",
"Settings.enable_logging_desc": "문제 해결을 위한 상세 디버그 로깅 활성화",
"Settings.logging_enabled": "디버그 로깅 활성화됨",
"Settings.logging_disabled": "디버그 로깅 비활성화됨",
"Settings.highlight_problem_bones": "문제 본 강조 표시",
"Settings.highlight_problem_bones_desc": "뷰포트에서 검증 문제가 있는 본 강조 표시",
"Settings.bone_highlighting": "본 강조 표시",
"Language.auto": "자동",
"Language.en_US": "영어",
"Language.ja_JP": "일본어",
"Language.ko_KR": "한국어",
"Language.changed.title": "언어 변경됨",
"Language.changed.success": "언어가 성공적으로 변경되었습니다!",
"Language.changed.restart": "일부 UI 요소는 블렌더를 다시 시작해야 할 수 있습니다"
}
}
+293
View File
@@ -0,0 +1,293 @@
from bpy.types import UIList, Panel, UILayout, Object, Context, Material, Operator
import bpy
from math import sqrt
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..core.common import SceneMatClass, MaterialListBool, get_active_armature
from ..functions.atlas_materials import AvatarToolKit_OT_AtlasMaterials
from ..core.translations import t
from ..core.logging_setup import logger
class AvatarToolKit_OT_SelectAllMaterials(Operator):
bl_idname = 'avatar_toolkit.select_all_materials'
bl_label = "Select All"
bl_description = "Select all materials for atlas"
def execute(self, context):
for item in context.scene.avatar_toolkit.materials:
item.mat.include_in_atlas = True
return {'FINISHED'}
class AvatarToolKit_OT_SelectNoneMaterials(Operator):
bl_idname = 'avatar_toolkit.select_none_materials'
bl_label = "Select None"
bl_description = "Deselect all materials"
def execute(self, context):
for item in context.scene.avatar_toolkit.materials:
item.mat.include_in_atlas = False
return {'FINISHED'}
class AvatarToolKit_OT_ExpandAllMaterials(Operator):
bl_idname = 'avatar_toolkit.expand_all_materials'
bl_label = "Expand All"
bl_description = "Expand all material settings"
def execute(self, context):
for item in context.scene.avatar_toolkit.materials:
item.mat.material_expanded = True
return {'FINISHED'}
class AvatarToolKit_OT_CollapseAllMaterials(Operator):
bl_idname = 'avatar_toolkit.collapse_all_materials'
bl_label = "Collapse All"
bl_description = "Collapse all material settings"
def execute(self, context):
for item in context.scene.avatar_toolkit.materials:
item.mat.material_expanded = False
return {'FINISHED'}
class AvatarToolKit_OT_ExpandSectionMaterials(Operator):
bl_idname = 'avatar_toolkit.expand_section_materials'
bl_label = ""
bl_description = ""
@classmethod
def poll(cls, context: Context) -> bool:
return True
def execute(self, context: Context) -> set:
try:
if not context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown:
context.scene.avatar_toolkit.materials.clear()
newlist: list[Material] = []
logger.debug("Loading materials for texture atlas")
for obj in context.scene.objects:
if len(obj.material_slots) > 0:
for mat_slot in obj.material_slots:
if mat_slot.material:
if mat_slot.material not in newlist:
newlist.append(mat_slot.material)
newitem: SceneMatClass = context.scene.avatar_toolkit.materials.add()
newitem.mat = mat_slot.material
MaterialListBool.old_list[context.scene.name] = newlist
context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown = True
logger.info(f"Loaded {len(newlist)} materials for texture atlas")
else:
context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown = False
logger.debug("Hiding material list")
return {'FINISHED'}
except Exception as e:
logger.error(f"Error loading materials: {str(e)}", exc_info=True)
self.report({'ERROR'}, t("TextureAtlas.load_error"))
return {'CANCELLED'}
class AvatarToolKit_UL_MaterialTextureAtlasProperties(UIList):
bl_label = t("TextureAtlas.material_list_label")
bl_idname = "Material_UL_avatar_toolkit_texture_atlas_mat_list_mat"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
def draw_header(self, context):
layout = self.layout
row = layout.row(align=True)
row.scale_y = 1.2
row.operator("avatar_toolkit.select_all_materials", text="", icon='CHECKBOX_HLT',
emboss=True).tooltip = t("TextureAtlas.select_all_tooltip")
row.operator("avatar_toolkit.select_none_materials", text="", icon='CHECKBOX_DEHLT',
emboss=True).tooltip = t("TextureAtlas.select_none_tooltip")
row.separator(factor=0.5)
row.operator("avatar_toolkit.expand_all_materials", text="", icon='DISCLOSURE_TRI_DOWN',
emboss=True).tooltip = t("TextureAtlas.expand_all_tooltip")
row.operator("avatar_toolkit.collapse_all_materials", text="", icon='DISCLOSURE_TRI_RIGHT',
emboss=True).tooltip = t("TextureAtlas.collapse_all_tooltip")
row.separator(factor=1.0)
search_row = row.row()
search_row.scale_x = 2.0
search_row.prop(context.scene.avatar_toolkit, "material_search_filter", text="", icon='VIEWZOOM')
box = layout.box()
size_row = box.row()
size_row.alignment = 'CENTER'
size_text = self.calculate_atlas_size(context)
size_row.label(text=f"{t('TextureAtlas.estimated_size')}: {size_text}px", icon='TEXTURE')
def draw_item(self, context: Context, layout: UILayout, data: Object, item: SceneMatClass, icon, active_data, active_propname, index):
if context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown:
if (context.scene.avatar_toolkit.material_search_filter and
context.scene.avatar_toolkit.material_search_filter.lower() not in item.mat.name.lower()):
return
# Main material
row = layout.row()
row.prop(item.mat, "include_in_atlas", text="",
icon='CHECKBOX_HLT' if item.mat.include_in_atlas else 'CHECKBOX_DEHLT',
emboss=False)
# Material name
row.prop(item.mat, "material_expanded",
text=item.mat.name,
icon='DOWNARROW_HLT' if item.mat.material_expanded else 'RIGHTARROW',
emboss=False)
row.label(text="", icon='MATERIAL')
if item.mat.material_expanded:
box = layout.box()
col = box.column(align=True)
header_row = col.row()
header_row.alignment = 'CENTER'
header_row.label(text=t("TextureAtlas.texture_maps"), icon='IMAGE')
col.separator(factor=0.5)
self.draw_texture_row(col, item.mat, "texture_atlas_albedo", "IMAGE_RGB", t("TextureAtlas.albedo"))
self.draw_texture_row(col, item.mat, "texture_atlas_normal", "NORMALS_FACE", t("TextureAtlas.normal"))
self.draw_texture_row(col, item.mat, "texture_atlas_emission", "LIGHT", t("TextureAtlas.emission"))
self.draw_texture_row(col, item.mat, "texture_atlas_ambient_occlusion", "SHADING_SOLID", t("TextureAtlas.ambient_occlusion"))
self.draw_texture_row(col, item.mat, "texture_atlas_height", "IMAGE_ZDEPTH", t("TextureAtlas.height"))
self.draw_texture_row(col, item.mat, "texture_atlas_roughness", "MATERIAL", t("TextureAtlas.roughness"))
col.separator(factor=0.5)
status_row = col.row()
status_row.alignment = 'CENTER'
is_ready = self.is_material_ready(item.mat)
if item.mat.include_in_atlas:
status_text = t("TextureAtlas.material_ready") if is_ready else t("TextureAtlas.material_not_ready")
status_icon = 'CHECKMARK' if is_ready else 'ERROR'
else:
status_text = t("TextureAtlas.material_not_included")
status_icon = 'INFO'
status_row.label(text=status_text, icon=status_icon)
def draw_texture_row(self, layout, material, prop_name, icon, label_text):
row = layout.row(align=True)
icon_row = row.row()
icon_row.scale_x = 0.5
icon_row.label(text="", icon=icon)
# Texture selector
row.prop(material, prop_name, text=label_text)
status_row = row.row()
status_row.scale_x = 0.5
if getattr(material, prop_name):
status_row.label(text="", icon='CHECKMARK')
else:
status_row.label(text="", icon='X')
def is_material_ready(self, material):
return bool(material.texture_atlas_albedo or
material.texture_atlas_normal or
material.texture_atlas_emission)
def calculate_atlas_size(self, context):
total_size = 0
selected_count = 0
for mat in context.scene.avatar_toolkit.materials:
if mat.mat.include_in_atlas:
selected_count += 1
if mat.mat.texture_atlas_albedo:
img = bpy.data.images[mat.mat.texture_atlas_albedo]
total_size += img.size[0] * img.size[1]
if total_size == 0:
return f"0x0 ({t('TextureAtlas.no_materials_selected')})"
size = int(sqrt(total_size))
pot_size = 2 ** (size - 1).bit_length() # Next power of 2
return f"{pot_size}x{pot_size} ({selected_count} {t('TextureAtlas.materials')})"
class AvatarToolKit_PT_TextureAtlasPanel(Panel):
bl_label = t("TextureAtlas.label")
bl_idname = "OBJECT_PT_avatar_toolkit_texture_atlas"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 7
def draw(self, context: Context):
layout = self.layout
armature = get_active_armature(context)
if armature:
header_row = layout.row()
header_row.label(text=t("TextureAtlas.label"), icon='TEXTURE')
layout.separator(factor=0.5)
info_box = layout.box()
info_col = info_box.column()
info_col.scale_y = 0.9
info_col.label(text=t("TextureAtlas.description_1"), icon='INFO')
info_col.label(text=t("TextureAtlas.description_2"))
if not bpy.data.filepath:
warning_box = layout.box()
warning_col = warning_box.column()
warning_col.scale_y = 0.9
warning_col.alert = True
warning_col.label(text=t("TextureAtlas.save_file_first"), icon='ERROR')
warning_col.label(text=t("TextureAtlas.save_file_instructions"))
warning_col.operator("wm.save_as_mainfile", text=t("TextureAtlas.save_file_button"), icon='FILE_TICK')
layout.separator(factor=0.5)
layout.separator(factor=0.5)
box = layout.box()
row = box.row(align=True)
row.scale_y = 1.2
direction_icon = 'RIGHTARROW' if not context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown else 'DOWNARROW_HLT'
button_text = t("TextureAtlas.reload_list") if not context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown else t("TextureAtlas.loaded_list")
row.operator(AvatarToolKit_OT_ExpandSectionMaterials.bl_idname,
text=button_text,
icon=direction_icon)
# Material list expanded
if context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown:
row = box.row()
row.template_list(AvatarToolKit_UL_MaterialTextureAtlasProperties.bl_idname,
'material_list',
context.scene.avatar_toolkit,
'materials',
context.scene.avatar_toolkit,
'texture_atlas_material_index',
rows=12,
type='DEFAULT')
layout.separator(factor=1.0)
row = layout.row()
row.scale_y = 1.5
row.enabled = context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown
has_selected = False
if context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown:
for item in context.scene.avatar_toolkit.materials:
if item.mat.include_in_atlas:
has_selected = True
break
if not has_selected and context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown:
row.operator(AvatarToolKit_OT_AtlasMaterials.bl_idname,
text=t("TextureAtlas.no_materials_selected"),
icon='ERROR')
else:
row.operator(AvatarToolKit_OT_AtlasMaterials.bl_idname,
text=t("TextureAtlas.atlas_materials"),
icon='NODE_TEXTURE')
else:
layout.label(text=t("Tools.select_armature"), icon='ERROR')
box = layout.box()
col = box.column()
col.scale_y = 0.9
col.label(text=t("TextureAtlas.select_armature_first"), icon='INFO')
col.label(text=t("TextureAtlas.how_to_use_1"))
col.label(text=t("TextureAtlas.how_to_use_2"))
+51 -44
View File
@@ -1,20 +1,23 @@
import bpy
from typing import Set
from bpy.types import Panel, Context, UILayout, Operator
from typing import Set, List, Tuple, Any
from bpy.types import Panel, Context, UILayout, Operator, Event, WindowManager
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..functions.custom_tools.mesh_attachment import AvatarToolkit_OT_AttachMesh
from ..functions.custom_tools.armature_merging import AvatarToolkit_OT_MergeArmature
from ..core.translations import t
from ..core.common import (
get_active_armature,
get_all_meshes,
validate_armature,
get_armature_list
)
from ..core.armature_validation import validate_armature
class AvatarToolkit_OT_SearchMergeArmatureInto(Operator):
bl_idname = "avatar_toolkit.search_merge_armature_into"
bl_label = ""
bl_description = t('MergeArmature.into_search_desc')
bl_property = "search_merge_armature_into_enum"
"""Search operator for selecting target armature to merge into"""
bl_idname: str = "avatar_toolkit.search_merge_armature_into"
bl_label: str = ""
bl_description: str = t('MergeArmature.into_search_desc')
bl_property: str = "search_merge_armature_into_enum"
search_merge_armature_into_enum: bpy.props.EnumProperty(
name=t('MergeArmature.into'),
@@ -22,19 +25,20 @@ class AvatarToolkit_OT_SearchMergeArmatureInto(Operator):
items=get_armature_list
)
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
context.scene.avatar_toolkit.merge_armature_into = self.search_merge_armature_into_enum
return {'FINISHED'}
def invoke(self, context, event):
def invoke(self, context: Context, event: Event) -> Set[str]:
context.window_manager.invoke_search_popup(self)
return {'FINISHED'}
class AvatarToolkit_OT_SearchMergeArmature(Operator):
bl_idname = "avatar_toolkit.search_merge_armature"
bl_label = ""
bl_description = t('MergeArmature.from_search_desc')
bl_property = "search_merge_armature_enum"
"""Search operator for selecting source armature to merge from"""
bl_idname: str = "avatar_toolkit.search_merge_armature"
bl_label: str = ""
bl_description: str = t('MergeArmature.from_search_desc')
bl_property: str = "search_merge_armature_enum"
search_merge_armature_enum: bpy.props.EnumProperty(
name=t('MergeArmature.from'),
@@ -42,44 +46,46 @@ class AvatarToolkit_OT_SearchMergeArmature(Operator):
items=get_armature_list
)
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
context.scene.avatar_toolkit.merge_armature = self.search_merge_armature_enum
return {'FINISHED'}
def invoke(self, context, event):
def invoke(self, context: Context, event: Event) -> Set[str]:
context.window_manager.invoke_search_popup(self)
return {'FINISHED'}
class AvatarToolkit_OT_SearchAttachMesh(Operator):
bl_idname = "avatar_toolkit.search_attach_mesh"
bl_label = ""
bl_description = t('AttachMesh.search_desc')
bl_property = "search_attach_mesh_enum"
"""Search operator for selecting mesh to attach to armature"""
bl_idname: str = "avatar_toolkit.search_attach_mesh"
bl_label: str = ""
bl_description: str = t('AttachMesh.search_desc')
bl_property: str = "search_attach_mesh_enum"
search_attach_mesh_enum: bpy.props.EnumProperty(
name=t('AttachMesh.select'),
description=t('AttachMesh.select_desc'),
items=lambda self, context: [
(obj.name, obj.name, "")
(obj.name, obj.name, "")
for obj in bpy.data.objects
if obj.type == 'MESH'
and not any(mod.type == 'ARMATURE' for mod in obj.modifiers)
]
)
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
context.scene.avatar_toolkit.attach_mesh = self.search_attach_mesh_enum
return {'FINISHED'}
def invoke(self, context, event):
def invoke(self, context: Context, event: Event) -> Set[str]:
context.window_manager.invoke_search_popup(self)
return {'FINISHED'}
class AvatarToolkit_OT_SearchAttachBone(Operator):
bl_idname = "avatar_toolkit.search_attach_bone"
bl_label = ""
bl_description = t('AttachBone.search_desc')
bl_property = "search_attach_bone_enum"
"""Search operator for selecting bone to attach mesh to"""
bl_idname: str = "avatar_toolkit.search_attach_bone"
bl_label: str = ""
bl_description: str = t('AttachBone.search_desc')
bl_property: str = "search_attach_bone_enum"
search_attach_bone_enum: bpy.props.EnumProperty(
name=t('AttachBone.select'),
@@ -90,26 +96,27 @@ class AvatarToolkit_OT_SearchAttachBone(Operator):
] if get_active_armature(context) else []
)
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
context.scene.avatar_toolkit.attach_bone = self.search_attach_bone_enum
return {'FINISHED'}
def invoke(self, context, event):
def invoke(self, context: Context, event: Event) -> Set[str]:
context.window_manager.invoke_search_popup(self)
return {'FINISHED'}
class AvatarToolKit_PT_CustomPanel(Panel):
"""Panel containing tools for custom avatar creation and merging"""
bl_label = t('CustomPanel.label')
bl_idname = "VIEW3D_PT_avatar_toolkit_custom"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 4
bl_options = {'DEFAULT_CLOSED'}
bl_label: str = t('CustomPanel.label')
bl_idname: str = "VIEW3D_PT_avatar_toolkit_custom"
bl_space_type: str = 'VIEW_3D'
bl_region_type: str = 'UI'
bl_category: str = CATEGORY_NAME
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order: int = 4
bl_options: Set[str] = {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None:
"""Draw the custom avatar panel UI"""
layout: UILayout = self.layout
toolkit = context.scene.avatar_toolkit
@@ -129,6 +136,7 @@ class AvatarToolKit_PT_CustomPanel(Panel):
self.draw_mesh_tools(layout, context)
def draw_armature_tools(self, layout: UILayout, context: Context) -> None:
"""Draw the armature merging tools section"""
toolkit = context.scene.avatar_toolkit
# Merge Settings Box
@@ -148,13 +156,12 @@ class AvatarToolKit_PT_CustomPanel(Panel):
col.separator(factor=0.5)
# Group related options together
transform_col = col.column(align=True)
transform_col.prop(toolkit, "merge_all_bones")
transform_col: UILayout = col.column(align=True)
transform_col.prop(toolkit, "apply_transforms")
col.separator(factor=0.5)
cleanup_col = col.column(align=True)
cleanup_col: UILayout = col.column(align=True)
cleanup_col.prop(toolkit, "join_meshes")
cleanup_col.prop(toolkit, "remove_zero_weights")
cleanup_col.prop(toolkit, "cleanup_shape_keys")
@@ -178,12 +185,13 @@ class AvatarToolKit_PT_CustomPanel(Panel):
# Merge button with emphasis
merge_box: UILayout = layout.box()
col = merge_box.column(align=True)
row = col.row(align=True)
col: UILayout = merge_box.column(align=True)
row: UILayout = col.row(align=True)
row.scale_y = 1.5
row.operator("avatar_toolkit.merge_armatures", icon='ARMATURE_DATA')
def draw_mesh_tools(self, layout: UILayout, context: Context) -> None:
"""Draw the mesh attachment tools section"""
toolkit = context.scene.avatar_toolkit
# Mesh Tools Box
@@ -220,8 +228,7 @@ class AvatarToolKit_PT_CustomPanel(Panel):
# Attach button with emphasis
attach_box: UILayout = layout.box()
col = attach_box.column(align=True)
row = col.row(align=True)
col: UILayout = attach_box.column(align=True)
row: UILayout = col.row(align=True)
row.scale_y = 1.5
row.operator("avatar_toolkit.attach_mesh", icon='ARMATURE_DATA')
+53 -40
View File
@@ -1,6 +1,6 @@
import bpy
from typing import Set
from bpy.types import Panel, Context, UILayout, Operator
from bpy.types import Panel, Context, UILayout, Operator, Event, WindowManager
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..core.translations import t
from ..core.common import get_active_armature, get_all_meshes
@@ -20,32 +20,42 @@ from ..functions.eye_tracking import (
class AvatarToolKit_PT_EyeTrackingPanel(Panel):
"""Panel containing eye tracking setup and testing tools"""
bl_label = t("EyeTracking.label")
bl_idname = "VIEW3D_PT_avatar_toolkit_eye_tracking"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 6
bl_options = {'DEFAULT_CLOSED'}
bl_label: str = t("EyeTracking.label")
bl_idname: str = "VIEW3D_PT_avatar_toolkit_eye_tracking"
bl_space_type: str = 'VIEW_3D'
bl_region_type: str = 'UI'
bl_category: str = CATEGORY_NAME
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order: int = 6
bl_options: Set[str] = {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None:
"""Draw the eye tracking panel interface"""
layout = self.layout
layout: UILayout = self.layout
toolkit = context.scene.avatar_toolkit
# SDK Version Selection Box
sdk_box = layout.box()
col = sdk_box.column(align=True)
sdk_box: UILayout = layout.box()
col: UILayout = sdk_box.column(align=True)
col.label(text=t("EyeTracking.sdk_version"), icon='PRESET')
col.separator(factor=0.5)
row = col.row(align=True)
row: UILayout = col.row(align=True)
row.prop(toolkit, "eye_tracking_type", expand=True)
if toolkit.eye_tracking_type == 'SDK2':
# SDK2 Warning Box
warning_box: UILayout = layout.box()
col: UILayout = warning_box.column(align=True)
col.label(text=t("EyeTracking.sdk2_warning"), icon='INFO')
col.separator(factor=0.5)
col.label(text=t("EyeTracking.sdk2_warning_detail1"))
col.label(text=t("EyeTracking.sdk2_warning_detail2"))
col.label(text=t("EyeTracking.sdk2_warning_detail3"))
col.label(text=t("EyeTracking.sdk2_warning_detail4"))
# Mode Selection Box
mode_box = layout.box()
col = mode_box.column(align=True)
mode_box: UILayout = layout.box()
col: UILayout = mode_box.column(align=True)
col.label(text=t("EyeTracking.setup"), icon='TOOL_SETTINGS')
col.separator(factor=0.5)
col.prop(toolkit, "eye_mode", expand=True)
@@ -59,11 +69,12 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
self.draw_av3_setup(context, layout)
def draw_av3_setup(self, context: Context, layout: UILayout) -> None:
"""Draw the AV3 eye tracking setup interface"""
toolkit = context.scene.avatar_toolkit
# Bone Setup Box
bone_box = layout.box()
col = bone_box.column(align=True)
bone_box: UILayout = layout.box()
col: UILayout = bone_box.column(align=True)
col.label(text=t("EyeTracking.bone_setup"), icon='BONE_DATA')
col.separator(factor=0.5)
@@ -76,16 +87,17 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
col.label(text=t("EyeTracking.no_armature"), icon='ERROR')
# Create Button
row = layout.row(align=True)
row: UILayout = layout.row(align=True)
row.scale_y = 1.5
row.operator(CreateEyesAV3Button.bl_idname, icon='PLAY')
def draw_creation_mode(self, context: Context, layout: UILayout) -> None:
"""Draw the eye tracking creation mode interface"""
toolkit = context.scene.avatar_toolkit
# Bone Setup Box
bone_box = layout.box()
col = bone_box.column(align=True)
bone_box: UILayout = layout.box()
col: UILayout = bone_box.column(align=True)
col.label(text=t("EyeTracking.bone_setup"), icon='BONE_DATA')
col.separator(factor=0.5)
@@ -98,15 +110,15 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
col.label(text=t("EyeTracking.no_armature"), icon='ERROR')
# Mesh Setup Box
mesh_box = layout.box()
col = mesh_box.column(align=True)
mesh_box: UILayout = layout.box()
col: UILayout = mesh_box.column(align=True)
col.label(text=t("EyeTracking.mesh_setup"), icon='MESH_DATA')
col.separator(factor=0.5)
col.prop_search(toolkit, "mesh_name_eye", bpy.data, "objects", text="")
# Shape Key Setup Box
shape_box = layout.box()
col = shape_box.column(align=True)
shape_box: UILayout = layout.box()
col: UILayout = shape_box.column(align=True)
col.label(text=t("EyeTracking.shapekey_setup"), icon='SHAPEKEY_DATA')
col.separator(factor=0.5)
@@ -120,8 +132,8 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
col.label(text=t("EyeTracking.no_shapekeys"), icon='ERROR')
# Options Box
options_box = layout.box()
col = options_box.column(align=True)
options_box: UILayout = layout.box()
col: UILayout = options_box.column(align=True)
col.label(text=t("EyeTracking.options"), icon='SETTINGS')
col.separator(factor=0.5)
col.prop(toolkit, "disable_eye_blinking")
@@ -130,26 +142,27 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
col.prop(toolkit, "eye_distance")
# Create Button
row = layout.row(align=True)
row: UILayout = layout.row(align=True)
row.scale_y = 1.5
row.operator(CreateEyesSDK2Button.bl_idname, icon='PLAY')
def draw_testing_mode(self, context: Context, layout: UILayout) -> None:
"""Draw the eye tracking testing mode interface"""
toolkit = context.scene.avatar_toolkit
if context.mode != 'POSE':
# Testing Start Box
test_box = layout.box()
col = test_box.column(align=True)
test_box: UILayout = layout.box()
col: UILayout = test_box.column(align=True)
col.label(text=t("EyeTracking.testing"), icon='PLAY')
col.separator(factor=0.5)
row = col.row(align=True)
row: UILayout = col.row(align=True)
row.scale_y = 1.5
row.operator(StartTestingButton.bl_idname, icon='PLAY')
else:
# Eye Rotation Box
rotation_box = layout.box()
col = rotation_box.column(align=True)
rotation_box: UILayout = layout.box()
col: UILayout = rotation_box.column(align=True)
col.label(text=t("EyeTracking.rotation_controls"), icon='DRIVER_ROTATIONAL_DIFFERENCE')
col.separator(factor=0.5)
col.prop(toolkit, "eye_rotation_x", text=t("EyeTracking.rotation.x"))
@@ -157,31 +170,31 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
col.operator(ResetRotationButton.bl_idname, icon='LOOP_BACK')
# Eye Adjustment Box
adjust_box = layout.box()
col = adjust_box.column(align=True)
adjust_box: UILayout = layout.box()
col: UILayout = adjust_box.column(align=True)
col.label(text=t("EyeTracking.adjustments"), icon='MODIFIER')
col.separator(factor=0.5)
col.prop(toolkit, "eye_distance")
col.operator(AdjustEyesButton.bl_idname, icon='CON_TRACKTO')
# Blinking Test Box
blink_box = layout.box()
col = blink_box.column(align=True)
blink_box: UILayout = layout.box()
col: UILayout = blink_box.column(align=True)
col.label(text=t("EyeTracking.blink_testing"), icon='HIDE_OFF')
col.separator(factor=0.5)
row = col.row(align=True)
row: UILayout = col.row(align=True)
row.prop(toolkit, "eye_blink_shape")
row.operator(TestBlinking.bl_idname, icon='RESTRICT_VIEW_OFF')
row = col.row(align=True)
row: UILayout = col.row(align=True)
row.prop(toolkit, "eye_lowerlid_shape")
row.operator(TestLowerlid.bl_idname, icon='RESTRICT_VIEW_OFF')
col.operator(ResetBlinkTest.bl_idname, icon='LOOP_BACK')
# Stop Testing Button
row = layout.row(align=True)
row: UILayout = layout.row(align=True)
row.scale_y = 1.5
row.operator(StopTestingButton.bl_idname, icon='PAUSE')
# Reset Button
row = layout.row(align=True)
row: UILayout = layout.row(align=True)
row.operator(ResetEyeTrackingButton.bl_idname, icon='FILE_REFRESH')
-49
View File
@@ -1,49 +0,0 @@
# MMD Tools disabled for the time being unto it can be fixed.
# import bpy
# from typing import Set
# from bpy.types import Panel, Context, UILayout
# from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
# from ..core.translations import t
# class AvatarToolKit_PT_MMDPanel(Panel):
# """Panel containing MMD bone standardization and cleanup tools"""
# bl_label = t("MMD.label")
# bl_idname = "OBJECT_PT_avatar_toolkit_mmd"
# bl_space_type = 'VIEW_3D'
# bl_region_type = 'UI'
# bl_category = CATEGORY_NAME
# bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
# bl_order = 3
# bl_options = {'DEFAULT_CLOSED'}
# def draw(self, context: Context) -> None:
# layout: UILayout = self.layout
# toolkit = context.scene.avatar_toolkit
# Bone Settings Box
# bone_box: UILayout = layout.box()
# col: UILayout = bone_box.column(align=True)
# col.label(text=t("MMD.bone_settings"), icon='BONE_DATA')
# col.separator(factor=0.5)
# col.prop(toolkit, "keep_twist_bones")
# col.prop(toolkit, "keep_upper_chest")
# col.operator("avatar_toolkit.standardize_mmd", icon='BONE_DATA')
# Mesh Tools Box
# mesh_box: UILayout = layout.box()
# col = mesh_box.column(align=True)
# col.label(text=t("MMD.mesh_tools"), icon='MESH_DATA')
# col.separator(factor=0.5)
# row: UILayout = col.row(align=True)
# row.operator("avatar_toolkit.fix_meshes", icon='MODIFIER')
# row.operator("avatar_toolkit.validate_meshes", icon='CHECKMARK')
# Cleanup Box
# cleanup_box: UILayout = layout.box()
# col = cleanup_box.column(align=True)
# col.label(text=t("MMD.cleanup"), icon='BRUSH_DATA')
# col.separator(factor=0.5)
# col.operator("avatar_toolkit.cleanup_mmd", icon='SHADERFX')
# col.operator("avatar_toolkit.convert_mmd_morphs", icon='SHAPEKEY_DATA')
# col.operator("avatar_toolkit.reparent_meshes", icon='OUTLINER_OB_ARMATURE')
+122 -28
View File
@@ -14,27 +14,16 @@ from ..core.translations import t
from ..core.common import (
get_active_armature,
clear_default_objects,
validate_armature,
get_armature_list,
get_armature_stats
)
from ..core.importers.importer import import_types, imports
from ..functions.pose_mode import (
AvatarToolkit_OT_StartPoseMode,
AvatarToolkit_OT_StopPoseMode,
AvatarToolkit_OT_ApplyPoseAsShapekey,
AvatarToolkit_OT_ApplyPoseAsRest
)
class AvatarToolKit_OT_Import(Operator):
"""Import FBX files into Blender with Avatar Toolkit settings"""
bl_idname: str = "avatar_toolkit.import"
bl_label: str = t("QuickAccess.import")
def execute(self, context: Context) -> Set[str]:
clear_default_objects()
bpy.ops.import_scene.fbx('INVOKE_DEFAULT', filter_glob=imports)
return {'FINISHED'}
from ..core.armature_validation import validate_armature
class AvatarToolKit_OT_ExportFBX(Operator):
"""Export selected objects as FBX"""
@@ -81,6 +70,7 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
def draw(self, context: Context) -> None:
"""Draw the panel layout"""
layout: UILayout = self.layout
props = context.scene.avatar_toolkit
# Armature Selection Box
armature_box: UILayout = layout.box()
@@ -94,28 +84,134 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
# Armature Validation
active_armature: Optional[Object] = get_active_armature(context)
if active_armature:
is_valid: bool
messages: List[str]
is_valid, messages = validate_armature(active_armature)
is_valid, messages, is_acceptable, hierarchy_messages, scale_messages, non_standard_messages = validate_armature(active_armature, detailed_messages=True)
# Create info box for all validation information
info_box: UILayout = col.box()
info_box = col.box()
if is_valid:
row: UILayout = info_box.row()
split: UILayout = row.split(factor=0.6)
if not is_valid:
# Display non-standard bones and hierarchy issues
if len(messages) > 1:
# Found Bones section
validation_box = info_box.box()
row = validation_box.row()
row.prop(props, "show_found_bones", text=t("Validation.section.found_bones"), icon='TRIA_DOWN' if props.show_found_bones else 'TRIA_RIGHT', emboss=False)
if props.show_found_bones:
for line in messages[0].split('\n'):
validation_box.label(text=line)
# Main validation status
validation_box = info_box.box()
row = validation_box.row()
row.alert = True
row.label(text=t("Validation.status.failed"))
# Detailed validation message
validation_box = info_box.box()
row = validation_box.row()
row.alert = True
row.label(text=t("Validation.message.failed.line1"))
row = validation_box.row()
row.alert = True
row.label(text=t("Validation.message.failed.line2"))
row = validation_box.row()
row.alert = True
row.label(text=t("Validation.message.failed.line3"))
# Non-Standard Bones section
validation_box = info_box.box()
row = validation_box.row()
row.alert = True
row.prop(props, "show_non_standard", text=t("Validation.section.non_standard"),
icon='TRIA_DOWN' if props.show_non_standard else 'TRIA_RIGHT', emboss=False)
if props.show_non_standard:
if non_standard_messages:
for message in non_standard_messages:
for line in message.split('\n'):
sub_row = validation_box.row()
sub_row.alert = True
sub_row.label(text=line)
else:
sub_row = validation_box.row()
sub_row.label(text=t("Validation.no_non_standard_issues"))
# Hierarchy Issues section
validation_box = info_box.box()
row = validation_box.row()
row.alert = True
row.prop(props, "show_hierarchy", text=t("Validation.section.hierarchy"),
icon='TRIA_DOWN' if props.show_hierarchy else 'TRIA_RIGHT', emboss=False)
if props.show_hierarchy:
if hierarchy_messages:
for message in hierarchy_messages:
sub_row = validation_box.row()
sub_row.alert = True
sub_row.label(text=message)
else:
sub_row = validation_box.row()
sub_row.label(text=t("Validation.no_hierarchy_issues"))
# Scale Issues section
validation_box = info_box.box()
row = validation_box.row()
row.alert = True
row.prop(props, "show_scale_issues", text=t("Validation.section.scale_issues"),
icon='TRIA_DOWN' if props.show_scale_issues else 'TRIA_RIGHT', emboss=False)
if props.show_scale_issues:
if scale_messages:
for scale_msg in scale_messages:
sub_row = validation_box.row()
sub_row.alert = True
sub_row.label(text=scale_msg)
else:
sub_row = validation_box.row()
sub_row.label(text=t("Validation.no_scale_issues"))
pose_box = layout.box()
col = pose_box.column(align=True)
col.label(text=t("Validation.tpose.label"), icon='ARMATURE_DATA')
col.separator(factor=0.5)
col.operator("avatar_toolkit.validate_tpose", icon='CHECKMARK')
if props.show_tpose_validation:
validation_box = col.box()
if props.tpose_validation_result:
validation_box.label(text=t("Validation.tpose.valid"), icon='CHECKMARK')
else:
row = validation_box.row()
row.alert = True
row.label(text=t("Validation.tpose.warning"), icon='ERROR')
for msg in props.tpose_validation_messages:
row = validation_box.row()
row.alert = True
row.label(text=msg.name)
else:
# If no specific issues, show acceptable message
info_box.label(text=messages[0], icon='INFO')
info_box.label(text=messages[1])
info_box.label(text=messages[2])
elif is_valid and not is_acceptable:
row = info_box.row()
split = row.split(factor=0.6)
split.label(text=t("QuickAccess.valid_armature"), icon='CHECKMARK')
stats: Dict[str, int] = get_armature_stats(active_armature)
stats = get_armature_stats(active_armature)
split.label(text=t("QuickAccess.bones_count", count=stats['bone_count']))
if stats['has_pose']:
info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT')
else:
# Display validation failure messages
for message in messages:
info_box.label(text=message, icon='ERROR')
elif is_valid and is_acceptable:
# Show acceptable standard message
info_box.label(text=messages[0], icon='INFO')
info_box.label(text=messages[1])
info_box.label(text=messages[2])
# Add standardize button
standardize_box = info_box.box()
standardize_box.operator("avatar_toolkit.standardize_armature",
text=t("QuickAccess.standardize_armature"),
icon='MODIFIER')
# Validation Mode Warnings - always show in info box
# Validation Mode Warnings
validation_mode = context.scene.avatar_toolkit.validation_mode
if validation_mode == 'BASIC':
warning_row = info_box.box()
@@ -153,5 +249,3 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
button_row.scale_y = 1.5
button_row.operator("avatar_toolkit.import", text=t("QuickAccess.import"), icon='IMPORT')
button_row.operator("avatar_toolkit.export", text=t("QuickAccess.export"), icon='EXPORT')
+21 -7
View File
@@ -36,12 +36,13 @@ class AvatarToolKit_PT_SettingsPanel(Panel):
bl_region_type: str = 'UI'
bl_category: str = CATEGORY_NAME
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order: int = 7
bl_order: int = 8
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None:
"""Draw the settings panel layout with language selection"""
layout: UILayout = self.layout
props = context.scene.avatar_toolkit
# Language Settings
lang_box: UILayout = layout.box()
@@ -50,7 +51,7 @@ class AvatarToolKit_PT_SettingsPanel(Panel):
row.scale_y = 1.2
row.label(text=t("Settings.language"), icon='WORLD')
col.separator()
col.prop(context.scene.avatar_toolkit, "language", text="")
col.prop(props, "language", text="")
# Validation Settings
val_box: UILayout = layout.box()
@@ -59,18 +60,31 @@ class AvatarToolKit_PT_SettingsPanel(Panel):
row.scale_y = 1.2
row.label(text=t("Settings.validation_mode"), icon='CHECKMARK')
col.separator()
col.prop(context.scene.avatar_toolkit, "validation_mode", text="")
col.prop(props, "validation_mode", text="")
# Bone Highlighting Settings
bone_box: UILayout = layout.box()
col = bone_box.column(align=True)
row = col.row()
row.scale_y = 1.2
row.label(text=t("Settings.bone_highlighting"), icon='BONE_DATA')
col.separator()
col.prop(props, "highlight_problem_bones")
if props.highlight_problem_bones:
col.operator("avatar_toolkit.highlight_problem_bones", icon='COLOR')
else:
col.operator("avatar_toolkit.clear_bone_highlighting", icon='X')
# Debug Settings
debug_box = layout.box()
col = debug_box.column()
row = col.row(align=True)
row.prop(context.scene.avatar_toolkit, "debug_expand",
icon="TRIA_DOWN" if context.scene.avatar_toolkit.debug_expand
row.prop(props, "debug_expand",
icon="TRIA_DOWN" if props.debug_expand
else "TRIA_RIGHT",
icon_only=True, emboss=False)
row.label(text=t("Settings.debug"), icon='CONSOLE')
if context.scene.avatar_toolkit.debug_expand:
if props.debug_expand:
col = debug_box.column(align=True)
col.prop(context.scene.avatar_toolkit, "enable_logging")
col.prop(props, "enable_logging")
+45 -2
View File
@@ -1,9 +1,21 @@
import bpy
from typing import Set
from bpy.types import Panel, Context, UILayout, Operator
from bpy.types import Panel, Context, UILayout, Operator, UIList
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..core.translations import t
class AVATAR_TOOLKIT_UL_ZeroWeightBones(UIList):
"""UI List for displaying zero weight bones with selection options"""
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if self.layout_type in {'DEFAULT', 'COMPACT'}:
row = layout.row(align=True)
row.prop(item, "selected", text="")
row.label(text=item.name)
if item.has_children:
row.label(text="", icon='OUTLINER_OB_ARMATURE')
if item.is_deform:
row.label(text="", icon='MOD_ARMATURE')
class AvatarToolKit_PT_ToolsPanel(Panel):
"""Panel containing various tools for avatar customization and optimization"""
bl_label: str = t("Tools.label")
@@ -18,6 +30,7 @@ class AvatarToolKit_PT_ToolsPanel(Panel):
def draw(self, context: Context) -> None:
"""Draw the tools panel interface"""
layout: UILayout = self.layout
toolkit = context.scene.avatar_toolkit
# General Tools
tools_box: UILayout = layout.box()
@@ -42,10 +55,32 @@ class AvatarToolKit_PT_ToolsPanel(Panel):
col.separator(factor=0.5)
col.operator("avatar_toolkit.create_digitigrade", text=t("Tools.create_digitigrade"), icon='BONE_DATA')
# Standardization Tools
standardize_box: UILayout = bone_box.box()
col = standardize_box.column(align=True)
col.label(text=t("Tools.standardize_title"), icon='OUTLINER_OB_ARMATURE')
col.separator(factor=0.5)
col.operator("avatar_toolkit.standardize_armature", icon='CHECKMARK')
# Weight Tools
weight_box: UILayout = bone_box.box()
col = weight_box.column(align=True)
col.prop(context.scene.avatar_toolkit, "merge_twist_bones", text=t("Tools.merge_twist_bones"))
col.prop(toolkit, "merge_twist_bones", text=t("Tools.merge_twist_bones"))
col.prop(toolkit, "preserve_parent_bones")
col.prop(toolkit, "target_bone_type")
col.prop(toolkit, "list_only_mode")
if toolkit.list_only_mode and len(toolkit.zero_weight_bones) > 0:
box = weight_box.box()
row = box.row()
row.template_list("AVATAR_TOOLKIT_UL_ZeroWeightBones", "",
toolkit, "zero_weight_bones",
toolkit, "zero_weight_bones_index")
col = box.column(align=True)
col.operator("avatar_toolkit.remove_selected_bones",
text=t("Tools.remove_selected_bones"))
row = col.row(align=True)
row.operator("avatar_toolkit.clean_weights", text=t("Tools.clean_weights"), icon='GROUP_BONE')
row.operator("avatar_toolkit.clean_constraints", text=t("Tools.clean_constraints"), icon='CONSTRAINT_BONE')
@@ -67,3 +102,11 @@ class AvatarToolKit_PT_ToolsPanel(Panel):
col.separator(factor=0.5)
col.operator("avatar_toolkit.apply_transforms", text=t("Tools.apply_transforms"), icon='OBJECT_DATA')
col.operator("avatar_toolkit.clean_shapekeys", text=t("Tools.clean_shapekeys"), icon='SHAPEKEY_DATA')
# Rigify Tools
rigify_box: UILayout = layout.box()
col = rigify_box.column(align=True)
col.label(text=t("Tools.rigify_title"), icon='ARMATURE_DATA')
col.separator(factor=0.5)
col.operator("avatar_toolkit.convert_rigify_to_unity", icon='ARMATURE_DATA')
col.prop(context.scene.avatar_toolkit, "merge_twist_bones")
+21
View File
@@ -0,0 +1,21 @@
import bpy
from bpy.types import Panel, Context, UILayout
from ..core.translations import t
class AvatarToolKit_PT_UVPanel(Panel):
"""Main UV Tools panel for Avatar Toolkit"""
bl_label = t("AvatarToolkit.label")
bl_idname = "OBJECT_PT_avatar_toolkit_uv_main"
bl_space_type = 'IMAGE_EDITOR'
bl_region_type = 'UI'
bl_category = "Avatar Toolkit"
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
# Add title section
box: UILayout = layout.box()
col: UILayout = box.column(align=True)
row: UILayout = col.row()
row.scale_y = 1.2
row.label(text=t("AvatarToolkit.label"), icon='UV')
+27
View File
@@ -0,0 +1,27 @@
import bpy
from bpy.types import Panel, Context, UILayout
from ..core.translations import t
class AvatarToolKit_PT_UVTools(Panel):
"""UV Tools panel containing UV manipulation operators"""
bl_label = t("Tools.label")
bl_idname = "OBJECT_PT_avatar_toolkit_uv_tools"
bl_space_type = 'IMAGE_EDITOR'
bl_region_type = 'UI'
bl_category = "Avatar Toolkit"
bl_parent_id = "OBJECT_PT_avatar_toolkit_uv_main"
bl_order = 3
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
tools_box: UILayout = layout.box()
col: UILayout = tools_box.column(align=True)
col.label(text=t("Tools.uv_title"), icon='UV')
col.separator(factor=0.5)
row: UILayout = col.row(align=True)
row.operator("avatar_toolkit.align_uv_edges_to_target",
text=t("UVTools.align_edges"),
icon='GP_MULTIFRAME_EDITING')
+27 -12
View File
@@ -1,6 +1,8 @@
from bpy.types import Panel, Context, UILayout
import bpy
from bpy.types import Panel, Context, UILayout, Object, ShapeKey
from ..core.translations import t
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..core.common import get_active_armature
class AvatarToolKit_PT_VisemesPanel(Panel):
"""Panel containing viseme creation and preview tools"""
@@ -11,26 +13,39 @@ class AvatarToolKit_PT_VisemesPanel(Panel):
bl_category: str = CATEGORY_NAME
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order: int = 5
bl_options = {'DEFAULT_CLOSED'}
bl_options: set[str] = {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None:
"""Draw the visemes panel interface"""
"""Draw the visemes panel interface with shape key selection and preview controls"""
layout: UILayout = self.layout
props = context.scene.avatar_toolkit
# Check for valid mesh with shape keys
if not context.active_object or context.active_object.type != 'MESH' or not context.active_object.data.shape_keys:
# Mesh Selection Box
mesh_box: UILayout = layout.box()
col: UILayout = mesh_box.column(align=True)
col.label(text=t("Visemes.mesh_select"), icon='OUTLINER_OB_MESH')
col.separator(factor=0.5)
armature = get_active_armature(context)
if armature:
col.prop(props, "viseme_mesh", text="")
else:
col.label(text=t("Visemes.no_armature"), icon='ERROR')
# Get selected mesh
mesh_obj = bpy.data.objects.get(props.viseme_mesh)
if not mesh_obj or not mesh_obj.data or not mesh_obj.data.shape_keys:
layout.label(text=t("Visemes.no_shapekeys"))
return
# Shape Key Selection Box
# Shape Key Selection Box
shape_box: UILayout = layout.box()
col: UILayout = shape_box.column(align=True)
col.label(text=t("Visemes.shape_selection"), icon='SHAPEKEY_DATA')
col.separator(factor=0.5)
# Shape key selection with valid data
shape_keys = context.active_object.data.shape_keys
shape_keys: ShapeKey = mesh_obj.data.shape_keys
col.prop_search(props, "mouth_a", shape_keys, "key_blocks", text=t("Visemes.mouth_a"))
col.prop_search(props, "mouth_o", shape_keys, "key_blocks", text=t("Visemes.mouth_o"))
col.prop_search(props, "mouth_ch", shape_keys, "key_blocks", text=t("Visemes.mouth_ch"))
@@ -41,7 +56,7 @@ class AvatarToolKit_PT_VisemesPanel(Panel):
# Preview Box
preview_box: UILayout = layout.box()
col = preview_box.column(align=True)
col: UILayout = preview_box.column(align=True)
col.label(text=t("Visemes.preview_label"), icon='HIDE_OFF')
col.separator(factor=0.5)
@@ -49,12 +64,12 @@ class AvatarToolKit_PT_VisemesPanel(Panel):
col.prop(props, "viseme_preview_selection", text="")
col.separator()
preview_text = t("Visemes.stop_preview") if props.viseme_preview_mode else t("Visemes.start_preview")
preview_text: str = t("Visemes.stop_preview") if props.viseme_preview_mode else t("Visemes.start_preview")
col.operator("avatar_toolkit.preview_visemes", text=preview_text, icon='HIDE_OFF')
# Create Box
create_box: UILayout = layout.box()
col = create_box.column(align=True)
col: UILayout = create_box.column(align=True)
col.label(text=t("Visemes.create_label"), icon='ADD')
col.separator(factor=0.5)
col.operator("avatar_toolkit.create_visemes", icon='ADD')
Binary file not shown.
Binary file not shown.