Compare commits

...

29 Commits

Author SHA1 Message Date
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 6d71669849 Update README.md 2025-03-02 15:09:36 +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 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 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 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
26 changed files with 2425 additions and 802 deletions
+3
View File
@@ -11,6 +11,9 @@ 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-). 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. ## 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.1.0#what-is-avatar-toolkits-version-support-policy)
+31
View File
@@ -1,7 +1,38 @@
import bpy
from bpy.app.handlers import persistent
modules = None modules = None
ordered_classes = 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(): def register():
# Check Blender version first
version = bpy.app.version
if version[0] > 4 or (version[0] == 4 and version[1] > 3):
show_version_error_popup()
return
# Add wheel installation check
try:
import lz4
except ImportError:
import sys
import os
import site
import pip
wheels_dir = os.path.join(os.path.dirname(__file__), "wheels")
for wheel in os.listdir(wheels_dir):
if wheel.endswith(".whl"):
pip.main(['install', os.path.join(wheels_dir, wheel)])
site.addsitedir(site.getsitepackages()[0])
from .core import auto_load from .core import auto_load
print("Starting registration") print("Starting registration")
auto_load.init() auto_load.init()
+1 -1
View File
@@ -3,7 +3,7 @@
schema_version = "1.0.0" schema_version = "1.0.0"
id = "avatar_toolkit" id = "avatar_toolkit"
version = "0.1.0" version = "0.1.3"
name = "Avatar Toolkit" name = "Avatar Toolkit"
tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games." tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games."
maintainer = "Team NekoNeo" maintainer = "Team NekoNeo"
+4 -1
View File
@@ -56,7 +56,10 @@ def register() -> None:
def unregister() -> None: def unregister() -> None:
"""Unregister all classes and modules in reverse order""" """Unregister all classes and modules in reverse order"""
for cls in reversed(ordered_classes): for cls in reversed(ordered_classes):
bpy.utils.unregister_class(cls) try:
bpy.utils.unregister_class(cls)
except RuntimeError:
continue
for module in modules: for module in modules:
if module.__name__ == __name__: if module.__name__ == __name__:
+220 -174
View File
@@ -6,10 +6,12 @@ import webbrowser
import typing import typing
import struct import struct
from io import BytesIO from io import BytesIO
import numpy.typing as npt
from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable, Union, Type
from mathutils import Vector from mathutils import Vector, Matrix
from bpy.types import Context, Object, Modifier, EditBone, Operator from bpy.types import (Context, Object, Modifier, EditBone, Operator, Material,
VertexGroup, ShapeKey, Bone, Mesh, Armature, PropertyGroup)
from functools import lru_cache from functools import lru_cache
from bpy.props import PointerProperty, IntProperty, StringProperty from bpy.props import PointerProperty, IntProperty, StringProperty
from bpy.utils import register_class from bpy.utils import register_class
@@ -17,14 +19,58 @@ from ..core.logging_setup import logger
from ..core.translations import t from ..core.translations import t
from ..core.dictionaries import bone_names from ..core.dictionaries import 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: class ProgressTracker:
"""Universal progress tracking for Avatar Toolkit operations""" """Universal progress tracking for Avatar Toolkit operations"""
def __init__(self, context: Context, total_steps: int, operation_name: str = "Operation"): def __init__(self, context: Context, total_steps: int, operation_name: str = "Operation") -> None:
self.context = context self.context: Context = context
self.total = total_steps self.total: int = total_steps
self.current = 0 self.current: int = 0
self.operation_name = operation_name self.operation_name: str = operation_name
self.wm = context.window_manager self.wm = context.window_manager
def step(self, message: str = "") -> None: def step(self, message: str = "") -> None:
@@ -35,26 +81,28 @@ class ProgressTracker:
self.wm.progress_update(progress * 100) self.wm.progress_update(progress * 100)
logger.debug(f"{self.operation_name} - {progress:.1%}: {message}") logger.debug(f"{self.operation_name} - {progress:.1%}: {message}")
def __enter__(self): def __enter__(self) -> 'ProgressTracker':
logger.info(f"Starting {self.operation_name}") logger.info(f"Starting {self.operation_name}")
return self 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() self.wm.progress_end()
logger.info(f"Completed {self.operation_name}") 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""" """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': if armature_name and armature_name != 'NONE':
return bpy.data.objects.get(armature_name) return bpy.data.objects.get(armature_name)
return None 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""" """Set the active armature for Avatar Toolkit operations"""
context.scene.avatar_toolkit.active_armature = armature 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""" """Get list of all armature objects in the scene"""
if context is None: if context is None:
context = bpy.context context = bpy.context
@@ -63,25 +111,21 @@ def get_armature_list(self=None, context: bpy.types.Context = None) -> List[Tupl
return [('NONE', t("Armature.validation.no_armature"), '')] return [('NONE', t("Armature.validation.no_armature"), '')]
return armatures return armatures
def validate_armature(armature: bpy.types.Object) -> Tuple[bool, List[str]]: def validate_armature(armature: Object) -> Tuple[bool, List[str]]:
"""Enhanced armature validation with multiple validation modes""" """Enhanced armature validation with multiple validation modes"""
validation_mode = bpy.context.scene.avatar_toolkit.validation_mode validation_mode = bpy.context.scene.avatar_toolkit.validation_mode
messages: List[str] = []
# Skip validation if mode is NONE
if validation_mode == 'NONE': if validation_mode == 'NONE':
return True, [] return True, []
messages = []
# Basic checks always run if not NONE
if not armature or armature.type != 'ARMATURE' or not armature.data.bones: if not armature or armature.type != 'ARMATURE' or not armature.data.bones:
return False, [t("Armature.validation.basic_check_failed")] return False, [t("Armature.validation.basic_check_failed")]
found_bones = {bone.name.lower(): bone for bone in armature.data.bones} found_bones: Dict[str, Bone] = {bone.name.lower(): bone for bone in armature.data.bones}
essential_bones: Set[str] = {'hips', 'spine', 'chest', 'neck', 'head'}
# Essential bones check (BASIC and STRICT) missing_bones: List[str] = []
essential_bones = {'hips', 'spine', 'chest', 'neck', 'head'}
missing_bones = []
for bone in essential_bones: for bone in essential_bones:
if not any(alt_name in found_bones for alt_name in bone_names[bone]): if not any(alt_name in found_bones for alt_name in bone_names[bone]):
missing_bones.append(bone) missing_bones.append(bone)
@@ -89,41 +133,38 @@ def validate_armature(armature: bpy.types.Object) -> Tuple[bool, List[str]]:
if missing_bones: if missing_bones:
messages.append(t("Armature.validation.missing_bones", bones=", ".join(missing_bones))) messages.append(t("Armature.validation.missing_bones", bones=", ".join(missing_bones)))
# Additional checks for STRICT mode only
if validation_mode == 'STRICT': if validation_mode == 'STRICT':
# Hierarchy validation hierarchy: List[Tuple[str, str]] = [
hierarchy = [('hips', 'spine'), ('spine', 'chest'), ('chest', 'neck'), ('neck', 'head')] ('hips', 'spine'), ('spine', 'chest'),
('chest', 'neck'), ('neck', 'head')
]
for parent, child in hierarchy: for parent, child in hierarchy:
if not validate_bone_hierarchy(found_bones, parent, child): if not validate_bone_hierarchy(found_bones, parent, child):
messages.append(t("Armature.validation.invalid_hierarchy", parent=parent, child=child)) messages.append(t("Armature.validation.invalid_hierarchy",
parent=parent, child=child))
# Symmetry validation symmetry_pairs: List[Tuple[str, str, str]] = [('arm', 'l', 'r'), ('leg', 'l', 'r')]
symmetry_pairs = [('arm', 'l', 'r'), ('leg', 'l', 'r')]
for base, left, right in symmetry_pairs: for base, left, right in symmetry_pairs:
if not validate_symmetry(found_bones, base, left, right): if not validate_symmetry(found_bones, base, left, right):
messages.append(t("Armature.validation.asymmetric_bones", bone=base)) 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 if (not validate_symmetry(found_bones, 'hand', 'l', 'r') and
not validate_symmetry(found_bones, 'wrist', 'l', 'r')): not validate_symmetry(found_bones, 'wrist', 'l', 'r')):
messages.append(t("Armature.validation.asymmetric_hand_wrist")) messages.append(t("Armature.validation.asymmetric_hand_wrist"))
is_valid = len(messages) == 0 is_valid: bool = len(messages) == 0
return is_valid, messages return is_valid, messages
def validate_bone_hierarchy(bones: Dict[str, bpy.types.Bone], parent_name: str, child_name: str) -> bool: 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""" """Validate if there is a valid parent-child relationship between bones"""
# Find matching parent and child bones using bone_names dictionary parent_bone: Optional[Bone] = None
parent_bone = None child_bone: Optional[Bone] = None
child_bone = None
# Check for parent bone matches
for alt_name in bone_names[parent_name]: for alt_name in bone_names[parent_name]:
if alt_name in bones: if alt_name in bones:
parent_bone = bones[alt_name] parent_bone = bones[alt_name]
break break
# Check for child bone matches
for alt_name in bone_names[child_name]: for alt_name in bone_names[child_name]:
if alt_name in bones: if alt_name in bones:
child_bone = bones[alt_name] child_bone = bones[alt_name]
@@ -132,47 +173,42 @@ def validate_bone_hierarchy(bones: Dict[str, bpy.types.Bone], parent_name: str,
if not parent_bone or not child_bone: if not parent_bone or not child_bone:
return False return False
# Check if child's parent matches parent bone
return child_bone.parent == parent_bone return child_bone.parent == parent_bone
def validate_symmetry(bones: Dict[str, bpy.types.Bone], base: str, left: str, right: str) -> bool: 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"""
Validate if matching left and right bones exist for a given base bone name left_patterns: List[str] = [
"""
# Define common naming patterns
left_patterns = [
f"{base}.{left}", f"{base}.{left}",
f"{base}_{left}", f"{base}_{left}",
f"{left}_{base}" f"{left}_{base}"
] ]
right_patterns = [ right_patterns: List[str] = [
f"{base}.{right}", f"{base}.{right}",
f"{base}_{right}", f"{base}_{right}",
f"{right}_{base}" f"{right}_{base}"
] ]
# Check if any of the patterns exist in the bones dictionary left_exists: bool = any(pattern in bones for pattern in left_patterns)
left_exists = any(pattern in bones for pattern in left_patterns) right_exists: bool = any(pattern in bones for pattern in right_patterns)
right_exists = any(pattern in bones for pattern in right_patterns)
return left_exists and right_exists 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""" """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': if len(armatures) == 1 and armatures[0][0] != 'NONE':
toolkit = context.scene.avatar_toolkit toolkit = context.scene.avatar_toolkit
set_active_armature(context, armatures[0]) set_active_armature(context, armatures[0])
def clear_default_objects() -> None: def clear_default_objects() -> None:
"""Removes default Blender objects (cube, light, camera)""" """Removes default Blender objects"""
default_names: Set[str] = {'Cube', 'Light', 'Camera'} default_names: Set[str] = {'Cube', 'Light', 'Camera'}
for obj in bpy.data.objects: for obj in bpy.data.objects:
if obj.name.split('.')[0] in default_names: if obj.name.split('.')[0] in default_names:
bpy.data.objects.remove(obj, do_unlink=True) 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""" """Get statistics about the armature"""
return { return {
'bone_count': len(armature.data.bones), 'bone_count': len(armature.data.bones),
@@ -183,7 +219,7 @@ def get_armature_stats(armature: bpy.types.Object) -> dict:
def get_all_meshes(context: Context) -> List[Object]: def get_all_meshes(context: Context) -> List[Object]:
"""Get all mesh objects parented to the active armature""" """Get all mesh objects parented to the active armature"""
armature = get_active_armature(context) armature: Optional[Object] = get_active_armature(context)
if armature: if armature:
return [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == armature] return [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == armature]
return [] return []
@@ -196,26 +232,26 @@ def validate_mesh_for_pose(mesh_obj: Object) -> Tuple[bool, str]:
if not mesh_obj.vertex_groups: if not mesh_obj.vertex_groups:
return False, t("Mesh.validation.no_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: if not armature_mods:
return False, t("Mesh.validation.no_armature_modifier") return False, t("Mesh.validation.no_armature_modifier")
return True, t("Mesh.validation.valid") 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""" """Cache vertex positions for a mesh object"""
vertices = mesh_obj.data.vertices 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) vertices.foreach_get('co', positions)
return positions.reshape(-1, 3) 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""" """Apply cached vertex positions to mesh in batch"""
vertices.foreach_set('co', positions.flatten()) vertices.foreach_set('co', positions.flatten())
def process_armature_modifiers(mesh_obj: Object) -> List[Dict[str, Any]]: def process_armature_modifiers(mesh_obj: Object) -> List[Dict[str, Any]]:
"""Process and store armature modifier states""" """Process and store armature modifier states"""
modifier_states = [] modifier_states: List[Dict[str, Any]] = []
for mod in mesh_obj.modifiers: for mod in mesh_obj.modifiers:
if mod.type == 'ARMATURE': if mod.type == 'ARMATURE':
modifier_states.append({ modifier_states.append({
@@ -255,7 +291,7 @@ def apply_pose_as_rest(context: Context, armature_obj: Object, meshes: List[Obje
def apply_armature_to_mesh(armature_obj: Object, mesh_obj: Object) -> None: def apply_armature_to_mesh(armature_obj: Object, mesh_obj: Object) -> None:
"""Apply armature deformation to mesh""" """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 armature_mod.object = armature_obj
if bpy.app.version >= (3, 5): if bpy.app.version >= (3, 5):
@@ -269,13 +305,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: def apply_armature_to_mesh_with_shapekeys(armature_obj: Object, mesh_obj: Object, context: Context) -> None:
"""Apply armature deformation to mesh with shape keys""" """Apply armature deformation to mesh with shape keys"""
old_active_index = mesh_obj.active_shape_key_index old_active_index: int = mesh_obj.active_shape_key_index
old_show_only = mesh_obj.show_only_shape_key old_show_only: bool = mesh_obj.show_only_shape_key
mesh_obj.show_only_shape_key = True mesh_obj.show_only_shape_key = True
shape_keys = mesh_obj.data.shape_keys.key_blocks shape_keys: List[ShapeKey] = mesh_obj.data.shape_keys.key_blocks
vertex_groups = [] vertex_groups: List[str] = []
mutes = [] mutes: List[bool] = []
for sk in shape_keys: for sk in shape_keys:
vertex_groups.append(sk.vertex_group) vertex_groups.append(sk.vertex_group)
@@ -283,17 +319,17 @@ def apply_armature_to_mesh_with_shapekeys(armature_obj: Object, mesh_obj: Object
mutes.append(sk.mute) mutes.append(sk.mute)
sk.mute = False sk.mute = False
disabled_mods = [] disabled_mods: List[Modifier] = []
for mod in mesh_obj.modifiers: for mod in mesh_obj.modifiers:
if mod.show_viewport: if mod.show_viewport:
mod.show_viewport = False mod.show_viewport = False
disabled_mods.append(mod) 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 arm_mod.object = armature_obj
co_length = len(mesh_obj.data.vertices) * 3 co_length: int = len(mesh_obj.data.vertices) * 3
eval_cos = np.empty(co_length, dtype=np.single) eval_cos: npt.NDArray[np.float32] = np.empty(co_length, dtype=np.single)
for i, shape_key in enumerate(shape_keys): for i, shape_key in enumerate(shape_keys):
mesh_obj.active_shape_key_index = i mesh_obj.active_shape_key_index = i
@@ -331,6 +367,11 @@ def validate_meshes(meshes: List[Object]) -> Tuple[bool, str]:
def join_mesh_objects(context: Context, meshes: List[Object], progress: Optional[ProgressTracker] = None) -> Optional[Object]: 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 proper cleanup and UV fixing"""
try: try:
# Store UV maps before joining
uv_maps_data = {}
for mesh in meshes:
uv_maps_data[mesh.name] = {uv.name: uv.data.copy() for uv in mesh.data.uv_layers}
bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT') bpy.ops.object.select_all(action='DESELECT')
@@ -352,12 +393,17 @@ def join_mesh_objects(context: Context, meshes: List[Object], progress: Optional
progress.step(t("Optimization.fixing_uvs")) progress.step(t("Optimization.fixing_uvs"))
fix_uv_coordinates(context) fix_uv_coordinates(context)
# Return the joined mesh object # Restore UV maps after joining
joined_mesh = context.active_object
for uv_name, uv_data in uv_maps_data.items():
for map_name, map_data in uv_data.items():
if map_name not in joined_mesh.data.uv_layers:
joined_mesh.data.uv_layers.new(name=map_name)
joined_mesh.data.uv_layers[map_name].data.foreach_set("uv", map_data)
return context.active_object return context.active_object
else: return None
# No objects were selected, return None
return None
except Exception as e: except Exception as e:
logger.error(f"Failed to join meshes: {str(e)}") logger.error(f"Failed to join meshes: {str(e)}")
@@ -374,13 +420,17 @@ def fix_uv_coordinates(context: Context) -> None:
bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode='OBJECT')
obj.select_set(True) obj.select_set(True)
context.view_layer.objects.active = obj context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT') # 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): with context.temp_override(active_object=obj):
bpy.ops.uv.select_all(action='SELECT') bpy.ops.uv.select_all(action='SELECT')
bpy.ops.uv.average_islands_scale() bpy.ops.uv.pack_islands(margin=0.001)
bpy.ops.uv.average_islands_scale()
logger.debug(f"UV Fix - Successfully processed {obj.name}") logger.debug(f"UV Fix - Successfully processed {obj.name}")
@@ -392,13 +442,13 @@ def fix_uv_coordinates(context: Context) -> None:
for sel_obj in current_selected: for sel_obj in current_selected:
sel_obj.select_set(True) sel_obj.select_set(True)
context.view_layer.objects.active = current_active 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: def clear_unused_data_blocks() -> int:
"""Removes all unused data blocks from the current Blender file""" """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)) 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) 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)) if isinstance(getattr(bpy.data, attr), bpy.types.bpy_prop_collection))
return initial_count - final_count return initial_count - final_count
@@ -408,8 +458,8 @@ def simplify_bonename(name: str) -> str:
def duplicate_bone_chain(bones: List[EditBone]) -> List[EditBone]: def duplicate_bone_chain(bones: List[EditBone]) -> List[EditBone]:
"""Duplicate a chain of bones while preserving hierarchy""" """Duplicate a chain of bones while preserving hierarchy"""
new_bones = [] new_bones: List[EditBone] = []
parent_map = {} parent_map: Dict[EditBone, EditBone] = {}
for bone in bones: for bone in bones:
new_bone = duplicate_bone(bone) new_bone = duplicate_bone(bone)
@@ -429,37 +479,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]: def get_vertex_weights(mesh_obj: Object, group_name: str) -> Dict[int, float]:
"""Get vertex weights for a specific vertex group""" """Get vertex weights for a specific vertex group"""
weights = {} weights: Dict[int, float] = {}
group_index = mesh_obj.vertex_groups[group_name].index group_index: int = mesh_obj.vertex_groups[group_name].index
for vertex in mesh_obj.data.vertices: for vertex in mesh_obj.data.vertices:
for group in vertex.groups: for group in vertex.groups:
if group.group == group_index: if group.group == group_index:
weights[vertex.index] = group.weight weights[vertex.index] = group.weight
return weights return weights
def transfer_vertex_weights(mesh_obj: Object, def transfer_vertex_weights(mesh_obj: Object, source_name: str, target_name: str, threshold: float = 0.01) -> None:
source_name: str,
target_name: str,
threshold: float = 0.01) -> None:
"""Transfer vertex weights from source to target group""" """Transfer vertex weights from source to target group"""
if source_name not in mesh_obj.vertex_groups: if source_name not in mesh_obj.vertex_groups:
return return
source_group = mesh_obj.vertex_groups[source_name] source_group: VertexGroup = mesh_obj.vertex_groups[source_name]
target_group = mesh_obj.vertex_groups.get(target_name) target_group: Optional[VertexGroup] = mesh_obj.vertex_groups.get(target_name)
if not target_group: if not target_group:
target_group = mesh_obj.vertex_groups.new(name=target_name) target_group = mesh_obj.vertex_groups.new(name=target_name)
# Get source weights weights: Dict[int, float] = get_vertex_weights(mesh_obj, source_name)
weights = get_vertex_weights(mesh_obj, source_name)
# Transfer weights above threshold
for vertex_index, weight in weights.items(): for vertex_index, weight in weights.items():
if weight > threshold: if weight > threshold:
target_group.add([vertex_index], weight, 'ADD') target_group.add([vertex_index], weight, 'ADD')
# Remove source group
mesh_obj.vertex_groups.remove(source_group) mesh_obj.vertex_groups.remove(source_group)
def remove_unused_shapekeys(mesh_obj: Object, tolerance: float = 0.001) -> int: def remove_unused_shapekeys(mesh_obj: Object, tolerance: float = 0.001) -> int:
@@ -467,35 +511,30 @@ def remove_unused_shapekeys(mesh_obj: Object, tolerance: float = 0.001) -> int:
if not mesh_obj.data.shape_keys: if not mesh_obj.data.shape_keys:
return 0 return 0
key_blocks = mesh_obj.data.shape_keys.key_blocks key_blocks: List[ShapeKey] = mesh_obj.data.shape_keys.key_blocks
vertex_count = len(mesh_obj.data.vertices) vertex_count: int = len(mesh_obj.data.vertices)
removed_count = 0 removed_count: int = 0
# Cache for relative key locations cache: Dict[str, npt.NDArray[np.float32]] = {}
cache = {} locations: npt.NDArray[np.float32] = np.empty(3 * vertex_count, dtype=np.float32)
locations = np.empty(3 * vertex_count, dtype=np.float32) to_delete: List[str] = []
to_delete = []
for key in key_blocks: for key in key_blocks:
if key == key.relative_key: if key == key.relative_key:
continue continue
# Get current key locations
key.data.foreach_get("co", locations) key.data.foreach_get("co", locations)
# Get or calculate relative key locations
if key.relative_key.name not in cache: 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) key.relative_key.data.foreach_get("co", rel_locations)
cache[key.relative_key.name] = rel_locations cache[key.relative_key.name] = rel_locations
# Compare locations
locations -= cache[key.relative_key.name] locations -= cache[key.relative_key.name]
if (np.abs(locations) < tolerance).all(): 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) to_delete.append(key.name)
# Remove marked shape keys
for key_name in to_delete: for key_name in to_delete:
mesh_obj.shape_key_remove(key_blocks[key_name]) mesh_obj.shape_key_remove(key_blocks[key_name])
removed_count += 1 removed_count += 1
@@ -503,20 +542,52 @@ def remove_unused_shapekeys(mesh_obj: Object, tolerance: float = 0.001) -> int:
return removed_count return removed_count
def has_shapekeys(mesh_obj: Object) -> bool: def has_shapekeys(mesh_obj: Object) -> bool:
"""Check if mesh object has shape keys"""
return mesh_obj.data.shape_keys is not None return mesh_obj.data.shape_keys is not None
# Identifier to indicate that an EnumProperty is empty def fix_zero_length_bones(armature: Object) -> None:
# This is the default identifier used when a wrapped items function returns an empty list """Fix zero length bones by setting a minimum length"""
# This identifier needs to be something that should never normally be used, so as to avoid the possibility of if not armature:
# conflicting with an enum value that exists. return
_empty_enum_identifier = 'Cats_empty_enum_identifier'
# 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 bpy.ops.object.mode_set(mode='EDIT')
# no_basis - If this is true the Basis will not be available in the list for bone in armature.data.edit_bones:
def get_shapekeys(context, names, is_mouth, no_basis, return_list): if bone.length < 0.001:
choices = [] bone.length = 0.001
choices_simple = [] bpy.ops.object.mode_set(mode='OBJECT')
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 meshes_list:
if is_mouth: if is_mouth:
@@ -536,15 +607,12 @@ def get_shapekeys(context, names, is_mouth, no_basis, return_list):
continue continue
if no_basis and name == 'Basis': if no_basis and name == 'Basis':
continue 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.append((name, name, name))
choices_simple.append(name) choices_simple.append(name)
_sort_enum_choices_by_identifier_lower(choices) _sort_enum_choices_by_identifier_lower(choices)
choices2 = [] choices2: List[Tuple[str, str, str]] = []
for name in names: for name in names:
if name in choices_simple and len(choices) > 1 and choices[0][0] != name: if name in choices_simple and len(choices) > 1 and choices[0][0] != name:
continue continue
@@ -553,22 +621,16 @@ def get_shapekeys(context, names, is_mouth, no_basis, return_list):
choices2.extend(choices) choices2.extend(choices)
if return_list: if return_list:
shape_list = [] shape_list: List[str] = []
for choice in choices2: for choice in choices2:
shape_list.append(choice[0]) shape_list.append(choice[0])
return shape_list return shape_list
return choices2 return choices2
# Default sorting for dynamic EnumProperty items def _sort_enum_choices_by_identifier_lower(choices: List[Tuple[str, str, str]], in_place: bool = True) -> List[Tuple[str, str, str]]:
def _sort_enum_choices_by_identifier_lower(choices, in_place=True): """Sort a list of enum choices by the lowercase of their identifier"""
"""Sort a list of enum choices (items) by the lowercase of their identifier. def identifier_lower(choice: Tuple[str, str, str]) -> str:
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):
return choice[0].lower() return choice[0].lower()
if in_place: if in_place:
@@ -577,55 +639,39 @@ def _sort_enum_choices_by_identifier_lower(choices, in_place=True):
choices = sorted(choices, key=identifier_lower) choices = sorted(choices, key=identifier_lower)
return choices return choices
def is_enum_empty(string): def is_enum_empty(string: str) -> bool:
"""Returns True only if the tested string is the string that signifies that an EnumProperty is empty. """Returns True only if the tested string is the empty enum identifier"""
Returns False in all other cases."""
return _empty_enum_identifier == string return _empty_enum_identifier == string
def is_enum_non_empty(string: str) -> bool:
# This function isn't needed since you can 'not is_enum_empty(string)', but is included for code clarity and readability """Returns False only if the tested string is not the empty enum identifier"""
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."""
return _empty_enum_identifier != string return _empty_enum_identifier != string
def fix_zero_length_bones(armature: Object) -> None: _empty_enum_identifier: str = 'Cats_empty_enum_identifier'
"""Fix zero length bones by setting a minimum length"""
if not armature:
return
bpy.ops.object.mode_set(mode='EDIT') def get_meshes_objects(check: bool = True) -> List[Object]:
for bone in armature.data.edit_bones: """Get all mesh objects in the scene"""
if bone.length < 0.001: meshes: List[Object] = [obj for obj in bpy.data.objects if obj.type == 'MESH']
bone.length = 0.001 if check and not meshes:
bpy.ops.object.mode_set(mode='OBJECT') return []
return meshes
def get_objects() -> bpy.types.BlendData:
"""Get all objects in the current Blender scene"""
return bpy.data.objects
def calculate_bone_orientation(mesh, vertices): def duplicate_bone(bone: EditBone) -> EditBone:
"""Calculate optimal bone orientation based on mesh geometry.""" """Create a duplicate of the given bone"""
new_bone: EditBone = bone.id_data.edit_bones.new(bone.name + "_copy")
if not vertices: new_bone.head = bone.head.copy()
return Vector((0, 0, 0.1)), 0.0 new_bone.tail = bone.tail.copy()
new_bone.roll = bone.roll
coords = [mesh.data.vertices[v.index].co for v in vertices] new_bone.use_connect = bone.use_connect
min_co = Vector(map(min, zip(*coords))) new_bone.use_local_location = bone.use_local_location
max_co = Vector(map(max, zip(*coords))) new_bone.use_inherit_rotation = bone.use_inherit_rotation
dimensions = max_co - min_co new_bone.use_inherit_scale = bone.use_inherit_scale
new_bone.use_deform = bone.use_deform
roll_angle = 0.0 return new_bone
return dimensions, roll_angle
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
#Binary tools #Binary tools
+75 -1
View File
@@ -1,11 +1,15 @@
import bpy import bpy
import logging import logging
import os import os
import pathlib
import typing 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 typing import Optional, Callable, Dict, List, Union, Set
from ..common import clear_default_objects from ..common import clear_default_objects
from .import_pmx import import_pmx from .import_pmx import import_pmx
from .import_pmd import import_pmd from .import_pmd import import_pmd
from ..translations import t
# Configure logging # Configure logging
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
@@ -118,7 +122,12 @@ import_types: Dict[str, ImportMethod] = {
method=lambda directory, filepath: bpy.ops.tuxedo.import_mmd_animation(directory=directory, filepath=filepath) 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), "vrm": lambda directory, files, filepath: bpy.ops.import_scene.vrm(filepath=filepath),
"pmx": lambda directory, files, filepath: import_pmx(filepath), "pmx": lambda directory, files, filepath: import_pmx(bpy.context, filepath,
scale=1.0,
use_mipmap=True,
sph_blend_factor=1.0,
spa_blend_factor=1.0
),
"pmd": lambda directory, files, filepath: import_pmd(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)), "animx": (lambda directory, files, filepath : bpy.ops.avatar_toolkit.animx_importer(directory=directory,files=files,filepath=filepath)),
} }
@@ -128,3 +137,68 @@ def concat_imports_filter(imports: Dict[str, ImportMethod]) -> str:
return "".join(f"*.{importer};" for importer in imports.keys()) return "".join(f"*.{importer};" for importer in imports.keys())
imports: str = concat_imports_filter(import_types) 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'}
+14 -2
View File
@@ -1,7 +1,10 @@
import logging import logging
from typing import Optional import traceback
from typing import Optional, Any
from bpy.types import Context
logger = logging.getLogger('avatar_toolkit') logger = logging.getLogger('avatar_toolkit')
_original_error = logger.error
def configure_logging(enabled: bool = False) -> None: def configure_logging(enabled: bool = False) -> None:
"""Configure logging for Avatar Toolkit""" """Configure logging for Avatar Toolkit"""
@@ -18,7 +21,16 @@ def configure_logging(enabled: bool = False) -> None:
handler.setFormatter(formatter) handler.setFormatter(formatter)
logger.addHandler(handler) logger.addHandler(handler)
def update_logging_state(self, context) -> None: 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: Any, context: Context) -> None:
"""Update logging state based on user preference""" """Update logging state based on user preference"""
from .addon_preferences import save_preference from .addon_preferences import save_preference
enabled = self.enable_logging enabled = self.enable_logging
+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
+121 -19
View File
@@ -1,5 +1,5 @@
import bpy 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.types import PropertyGroup, Material, Scene, Object, Context
from bpy.props import ( from bpy.props import (
StringProperty, StringProperty,
@@ -14,23 +14,25 @@ from .logging_setup import logger
from .translations import t, get_languages_list, update_language from .translations import t, get_languages_list, update_language
from .addon_preferences import get_preference, save_preference from .addon_preferences import get_preference, save_preference
from .updater import get_version_list 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.visemes import VisemePreview
from ..functions.eye_tracking import set_rotation from ..functions.eye_tracking import set_rotation
def update_validation_mode(self, context): 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}") logger.info(f"Updating validation mode to: {self.validation_mode}")
save_preference("validation_mode", 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}") logger.info(f"Updating logging state to: {self.enable_logging}")
save_preference("enable_logging", self.enable_logging) save_preference("enable_logging", self.enable_logging)
from .logging_setup import configure_logging from .logging_setup import configure_logging
configure_logging(self.enable_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: if self.viseme_preview_mode:
from ..functions.visemes import VisemePreview
VisemePreview.update_preview(context) VisemePreview.update_preview(context)
class AvatarToolkitSceneProperties(PropertyGroup): class AvatarToolkitSceneProperties(PropertyGroup):
@@ -134,12 +136,6 @@ class AvatarToolkitSceneProperties(PropertyGroup):
default=False default=False
) )
viseme_preview_selection: StringProperty(
name=t("Visemes.preview_selection"),
description=t("Visemes.preview_selection_desc"),
default="vrc.v_aa"
)
mouth_a: StringProperty( mouth_a: StringProperty(
name=t("Visemes.mouth_a"), name=t("Visemes.mouth_a"),
description=t("Visemes.mouth_a_desc") description=t("Visemes.mouth_a_desc")
@@ -155,6 +151,11 @@ class AvatarToolkitSceneProperties(PropertyGroup):
description=t("Visemes.mouth_ch_desc") description=t("Visemes.mouth_ch_desc")
) )
viseme_mesh: StringProperty(
name=t("Visemes.mesh_select"),
description=t("Visemes.mesh_select_desc"),
)
shape_intensity: FloatProperty( shape_intensity: FloatProperty(
name=t("Visemes.shape_intensity"), name=t("Visemes.shape_intensity"),
description=t("Visemes.shape_intensity_desc"), description=t("Visemes.shape_intensity_desc"),
@@ -366,24 +367,125 @@ class AvatarToolkitSceneProperties(PropertyGroup):
default=True default=True
) )
attach_mesh: StringProperty( material_search_filter: StringProperty(
name=t("Tools.attach_mesh_select"), name=t("TextureAtlas.search_materials"),
description=t("Tools.attach_mesh_select_desc") description=t("TextureAtlas.search_materials_desc"),
default=""
) )
attach_bone: StringProperty( def get_texture_node_list(self: Material, context: Context) -> list[tuple]:
name=t("Tools.attach_bone_select"), if self.use_nodes:
description=t("Tools.attach_bone_select_desc") 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
)
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
)
def register() -> None: def register() -> None:
"""Register the Avatar Toolkit property group""" """Register the Avatar Toolkit property group"""
logger.info("Registering Avatar Toolkit properties") logger.info("Registering Avatar Toolkit properties")
try:
bpy.utils.register_class(AvatarToolkitSceneProperties)
except ValueError:
# Class already registered, we can continue
pass
bpy.types.Scene.avatar_toolkit = PointerProperty(type=AvatarToolkitSceneProperties) bpy.types.Scene.avatar_toolkit = PointerProperty(type=AvatarToolkitSceneProperties)
logger.debug("Properties registered successfully") logger.debug("Properties registered successfully")
def unregister() -> None: def unregister() -> None:
"""Unregister the Avatar Toolkit property group""" """Unregister the Avatar Toolkit property group"""
logger.info("Unregistering Avatar Toolkit properties") logger.info("Unregistering Avatar Toolkit properties")
del bpy.types.Scene.avatar_toolkit try:
del bpy.types.Scene.avatar_toolkit
except:
pass
try:
bpy.utils.unregister_class(AvatarToolkitSceneProperties)
except RuntimeError:
pass
logger.debug("Properties unregistered successfully") logger.debug("Properties unregistered successfully")
+76 -5
View File
@@ -17,11 +17,17 @@ from typing import Dict, List, Tuple, Optional, Set, Any
GITHUB_REPO = "teamneoneko/Avatar-Toolkit" 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.1"] # Change this based on which version you're building
is_checking_for_update: bool = False is_checking_for_update: bool = False
update_needed: bool = False update_needed: bool = False
latest_version: Optional[str] = None latest_version: Optional[str] = None
latest_version_str: str = '' latest_version_str: str = ''
version_list: Optional[Dict[str, List[str]]] = None version_list: Optional[Dict[str, List[str]]] = None
last_manual_check_time: float = 0
main_dir: str = os.path.dirname(os.path.dirname(__file__)) main_dir: str = os.path.dirname(os.path.dirname(__file__))
downloads_dir: str = os.path.join(main_dir, "downloads") downloads_dir: str = os.path.join(main_dir, "downloads")
@@ -34,7 +40,9 @@ class AvatarToolkit_OT_CheckForUpdate(bpy.types.Operator):
bl_options = {'INTERNAL'} bl_options = {'INTERNAL'}
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context: bpy.types.Context) -> Set[str]:
global last_manual_check_time
check_for_update_background() check_for_update_background()
last_manual_check_time = time.time() # Reset the timer on manual check
return {'FINISHED'} return {'FINISHED'}
@@ -80,7 +88,16 @@ class AvatarToolkit_PT_UpdaterPanel(bpy.types.Panel):
bl_options = {'DEFAULT_CLOSED'} bl_options = {'DEFAULT_CLOSED'}
def draw(self, context: bpy.types.Context) -> None: def draw(self, context: bpy.types.Context) -> None:
global last_manual_check_time
layout = self.layout 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) draw_updater_panel(context, layout)
@@ -158,11 +175,23 @@ def get_github_releases() -> bool:
return True return True
def check_for_update_available() -> bool: def check_for_update_available() -> bool:
global latest_version, latest_version_str global latest_version, latest_version_str, version_list
if not version_list: if not version_list:
return False 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 latest_version_str = latest_version
current_version = get_current_version() current_version = get_current_version()
@@ -197,9 +226,35 @@ def update_now(latest: bool = False) -> None:
return return
if latest: 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: 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) download_file(update_link)
ui_refresh() ui_refresh()
@@ -274,7 +329,17 @@ def finish_update(error: str = '') -> None:
ui_refresh() ui_refresh()
def get_version_list(self, context: bpy.types.Context) -> List[Tuple[str, str, str]]: 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: def draw_updater_panel(context: bpy.types.Context, layout: bpy.types.UILayout) -> None:
box = layout.box() box = layout.box()
@@ -287,6 +352,12 @@ def draw_updater_panel(context: bpy.types.Context, layout: bpy.types.UILayout) -
col.separator() 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 # Update check/status section
if is_checking_for_update: if is_checking_for_update:
col.operator(AvatarToolkit_OT_CheckForUpdate.bl_idname, col.operator(AvatarToolkit_OT_CheckForUpdate.bl_idname,
+290
View File
@@ -0,0 +1,290 @@
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:
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[:]
canvas.save(filepath=os.path.join(os.path.dirname(bpy.data.filepath),
new_image_name+".png"))
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}")
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
+89 -91
View File
@@ -1,7 +1,7 @@
import bpy import bpy
import numpy as np import numpy as np
from typing import List, Optional, Dict, Set from typing import List, Optional, Dict, Set, Tuple, Any
from bpy.types import Context, Object, Operator from bpy.types import Context, Object, Operator, ArmatureModifier, EditBone, VertexGroup, Mesh, ShapeKey
from ...core.logging_setup import logger from ...core.logging_setup import logger
from ...core.translations import t from ...core.translations import t
@@ -13,26 +13,27 @@ from ...core.common import (
remove_unused_shapekeys remove_unused_shapekeys
) )
class AvatarToolkit_OT_MergeArmature(Operator): class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
bl_idname = 'avatar_toolkit.merge_armatures' """Operator for merging two armatures together with their associated meshes"""
bl_label = t('MergeArmature.label') bl_idname: str = 'avatar_toolkit.merge_armatures'
bl_description = t('MergeArmature.desc') bl_label: str = t('MergeArmature.label')
bl_options = {'REGISTER', 'UNDO'} bl_description: str = t('MergeArmature.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: Context) -> bool:
return len(get_all_meshes(context)) > 1 return len(get_all_meshes(context)) > 1
def execute(self, context): def execute(self, context: Context) -> Set[str]:
try: try:
wm = context.window_manager wm = context.window_manager
wm.progress_begin(0, 100) wm.progress_begin(0, 100)
# Get both armatures # Get both armatures
base_armature_name = context.scene.merge_armature_into base_armature_name: str = context.scene.avatar_toolkit.merge_armature_into
merge_armature_name = context.scene.merge_armature merge_armature_name: str = context.scene.avatar_toolkit.merge_armature
base_armature = bpy.data.objects.get(base_armature_name) base_armature: Optional[Object] = bpy.data.objects.get(base_armature_name)
merge_armature = bpy.data.objects.get(merge_armature_name) merge_armature: Optional[Object] = bpy.data.objects.get(merge_armature_name)
if not base_armature or not merge_armature: if not base_armature or not merge_armature:
logger.error(f"Armature not found: {merge_armature_name}") logger.error(f"Armature not found: {merge_armature_name}")
@@ -51,15 +52,15 @@ class AvatarToolkit_OT_MergeArmature(Operator):
wm.progress_update(80) wm.progress_update(80)
# Get settings from scene properties # Get settings from scene properties
merge_all_bones = context.scene.avatar_toolkit.merge_all_bones merge_all_bones: bool = 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
merge_armatures( merge_armatures(
base_armature_name, base_armature_name,
merge_armature_name, merge_armature_name,
mesh_only=False, mesh_only=False,
merge_all_bones=context.scene.avatar_toolkit.merge_all_bones, merge_all_bones=merge_all_bones,
join_meshes=join_meshes, join_meshes=join_meshes,
operator=self operator=self
) )
@@ -76,10 +77,10 @@ class AvatarToolkit_OT_MergeArmature(Operator):
self.report({'ERROR'}, str(e)) self.report({'ERROR'}, str(e))
return {'CANCELLED'} return {'CANCELLED'}
def delete_rigidbodies_and_joints(armature: Object): def delete_rigidbodies_and_joints(armature: Object) -> None:
"""Delete rigid bodies and joints associated with the armature.""" """Delete rigid bodies and joints associated with an armature"""
to_delete = [] to_delete: List[Object] = []
parent = armature parent: Object = armature
while parent.parent: while parent.parent:
parent = parent.parent parent = parent.parent
@@ -94,9 +95,9 @@ def delete_rigidbodies_and_joints(armature: Object):
bpy.data.objects.remove(obj, do_unlink=True) bpy.data.objects.remove(obj, do_unlink=True)
def validate_parents_and_transforms(merge_armature: Object, base_armature: Object, context: Context) -> bool: def validate_parents_and_transforms(merge_armature: Object, base_armature: Object, context: Context) -> bool:
"""Validate parents and transformations of armatures before merging.""" """Validate parent relationships and transformations of armatures"""
merge_parent = merge_armature.parent merge_parent: Optional[Object] = merge_armature.parent
base_parent = base_armature.parent base_parent: Optional[Object] = base_armature.parent
if merge_parent or base_parent: if merge_parent or base_parent:
if context.scene.merge_all_bones: if context.scene.merge_all_bones:
@@ -112,21 +113,21 @@ def validate_parents_and_transforms(merge_armature: Object, base_armature: Objec
return True return True
def is_transform_clean(obj: Object) -> bool: 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): for i in range(3):
if obj.scale[i] != 1 or obj.location[i] != 0 or obj.rotation_euler[i] != 0: if obj.scale[i] != 1 or obj.location[i] != 0 or obj.rotation_euler[i] != 0:
return False return False
return True return True
def prepare_mesh_vertex_groups(mesh: Object): def prepare_mesh_vertex_groups(mesh: Object) -> None:
"""Prepare mesh by assigning all vertices to a new vertex group.""" """Initialize mesh vertex groups for merging process"""
if mesh.vertex_groups: if mesh.vertex_groups:
for vg in mesh.vertex_groups: for vg in mesh.vertex_groups:
mesh.vertex_groups.remove(vg) mesh.vertex_groups.remove(vg)
bpy.ops.object.mode_set(mode='EDIT') bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT') 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.vertex_group_assign()
bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode='OBJECT')
@@ -136,14 +137,14 @@ def merge_armatures(
mesh_only: bool, mesh_only: bool,
merge_all_bones: bool = False, merge_all_bones: bool = False,
join_meshes: bool = False, join_meshes: bool = False,
operator=None operator: Optional[Operator] = None
): ) -> None:
"""Main function to merge two armatures.""" """Main function to merge two armatures with their associated meshes and data"""
logger.info(f"Merging armatures: {merge_armature_name} into {base_armature_name}") 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) base_armature: Optional[Object] = bpy.data.objects.get(base_armature_name)
merge_armature = bpy.data.objects.get(merge_armature_name) merge_armature: Optional[Object] = bpy.data.objects.get(merge_armature_name)
if not base_armature or not merge_armature: if not base_armature or not merge_armature:
logger.error(f"Armature not found: {merge_armature_name}") logger.error(f"Armature not found: {merge_armature_name}")
@@ -172,12 +173,12 @@ def merge_armatures(
fix_zero_length_bones(merge_armature) fix_zero_length_bones(merge_armature)
# Store original parent relationships # Store original parent relationships
original_parents = {} original_parents: Dict[str, Optional[str]] = {}
for bone in merge_armature.data.bones: for bone in merge_armature.data.bones:
original_parents[bone.name] = bone.parent.name if bone.parent else None original_parents[bone.name] = bone.parent.name if bone.parent else None
# Get base bone names # 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}
# Switch to edit mode on merge armature and rename bones # Switch to edit mode on merge armature and rename bones
bpy.context.view_layer.objects.active = merge_armature bpy.context.view_layer.objects.active = merge_armature
@@ -206,11 +207,11 @@ def merge_armatures(
# Restore parent relationships # Restore parent relationships
bpy.ops.object.mode_set(mode='EDIT') bpy.ops.object.mode_set(mode='EDIT')
for bone in base_armature.data.edit_bones: for bone in base_armature.data.edit_bones:
base_name = bone.name.replace('.merge', '') base_name: str = bone.name.replace('.merge', '')
if base_name in original_parents: if base_name in original_parents:
parent_name = original_parents[base_name] parent_name: Optional[str] = original_parents[base_name]
if parent_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: if parent_bone:
bone.parent = parent_bone bone.parent = parent_bone
@@ -223,7 +224,7 @@ def merge_armatures(
# Process vertex groups if not mesh_only # Process vertex groups if not mesh_only
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) process_vertex_groups(meshes)
# Remove zero weight vertex groups if enabled # Remove zero weight vertex groups if enabled
@@ -235,9 +236,9 @@ def merge_armatures(
# Join meshes if requested # Join meshes if requested
if join_meshes: 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: 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: if joined_mesh:
logger.info(f"Joined meshes into {joined_mesh.name}") logger.info(f"Joined meshes into {joined_mesh.name}")
@@ -250,8 +251,8 @@ def merge_armatures(
# Remove any remaining .merge bones # Remove any remaining .merge bones
bpy.context.view_layer.objects.active = base_armature bpy.context.view_layer.objects.active = base_armature
bpy.ops.object.mode_set(mode='EDIT') bpy.ops.object.mode_set(mode='EDIT')
edit_bones = base_armature.data.edit_bones edit_bones: List[EditBone] = base_armature.data.edit_bones
bones_to_remove = [bone for bone in edit_bones if bone.name.endswith('.merge')] bones_to_remove: List[EditBone] = [bone for bone in edit_bones if bone.name.endswith('.merge')]
for bone in bones_to_remove: for bone in bones_to_remove:
edit_bones.remove(bone) edit_bones.remove(bone)
bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode='OBJECT')
@@ -259,14 +260,13 @@ def merge_armatures(
# Final cleanup # Final cleanup
clear_unused_data_blocks() clear_unused_data_blocks()
def validate_merge_armature_transforms( def validate_merge_armature_transforms(
base_armature: Object, base_armature: Object,
merge_armature: Object, merge_armature: Object,
mesh_merge: Optional[Object], mesh_merge: Optional[Object],
tolerance: float tolerance: float
) -> bool: ) -> bool:
"""Validate transforms of both armatures and mesh.""" """Validate transforms of both armatures and mesh"""
for i in [0, 1, 2]: for i in [0, 1, 2]:
if abs(base_armature.scale[i] - merge_armature.scale[i]) > tolerance: if abs(base_armature.scale[i] - merge_armature.scale[i]) > tolerance:
return False return False
@@ -280,10 +280,10 @@ def validate_merge_armature_transforms(
def adjust_merge_armature_transforms( def adjust_merge_armature_transforms(
merge_armature: Object, merge_armature: Object,
mesh_merge: Object mesh_merge: Object
): ) -> None:
"""Adjust transforms of the merge armature.""" """Adjust transforms of the merge armature"""
old_loc = list(merge_armature.location) old_loc: List[float] = list(merge_armature.location)
old_scale = list(merge_armature.scale) old_scale: List[float] = list(merge_armature.scale)
for i in [0, 1, 2]: for i in [0, 1, 2]:
merge_armature.location[i] = (mesh_merge.location[i] * old_scale[i]) + old_loc[i] merge_armature.location[i] = (mesh_merge.location[i] * old_scale[i]) + old_loc[i]
@@ -295,25 +295,24 @@ def adjust_merge_armature_transforms(
mesh_merge.rotation_euler[i] = 0 mesh_merge.rotation_euler[i] = 0
mesh_merge.scale[i] = 1 mesh_merge.scale[i] = 1
def detect_bones_to_merge( def detect_bones_to_merge(
base_edit_bones: bpy.types.ArmatureEditBones, base_edit_bones: bpy.types.ArmatureEditBones,
merge_edit_bones: bpy.types.ArmatureEditBones, merge_edit_bones: bpy.types.ArmatureEditBones,
tolerance: float, tolerance: float,
merge_all_bones: bool merge_all_bones: bool
) -> List[str]: ) -> List[str]:
"""Detect corresponding bones between base and merge armatures using smart detection and position tolerance.""" """Detect corresponding bones between base and merge armatures using smart detection and position tolerance"""
bones_to_merge = [] bones_to_merge: List[str] = []
# Cache base bone positions # 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 bone.name: np.array(bone.head) for bone in base_edit_bones
} }
# Smart bone detection # Smart bone detection
for merge_bone in merge_edit_bones: for merge_bone in merge_edit_bones:
merge_bone_position = np.array(merge_bone.head) merge_bone_position: np.ndarray = np.array(merge_bone.head)
found_match = False found_match: bool = False
if merge_all_bones and merge_bone.name in base_bones_positions: if merge_all_bones and merge_bone.name in base_bones_positions:
# If merging same bones by name # If merging same bones by name
@@ -333,17 +332,16 @@ def detect_bones_to_merge(
return bones_to_merge return bones_to_merge
def process_vertex_groups(meshes: List[Object]) -> None:
def process_vertex_groups(meshes: List[Object]): """Process vertex groups in meshes"""
"""Process vertex groups in meshes."""
for mesh in meshes: for mesh in meshes:
vg_names = {vg.name for vg in mesh.vertex_groups} vg_names: Set[str] = {vg.name for vg in mesh.vertex_groups}
merge_vg_names = [vg_name for vg_name in vg_names if vg_name.endswith('.merge')] 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: for vg_merge_name in merge_vg_names:
base_name = vg_merge_name[:-6] base_name: str = vg_merge_name[:-6]
vg_merge = mesh.vertex_groups.get(vg_merge_name) vg_merge: Optional[VertexGroup] = mesh.vertex_groups.get(vg_merge_name)
vg_base = mesh.vertex_groups.get(base_name) vg_base: Optional[VertexGroup] = mesh.vertex_groups.get(base_name)
if vg_merge is None: if vg_merge is None:
continue continue
@@ -353,20 +351,20 @@ def process_vertex_groups(meshes: List[Object]):
else: else:
vg_merge.name = base_name vg_merge.name = base_name
def mix_vertex_groups(mesh: Object, vg_from_name: str, vg_to_name: str): def mix_vertex_groups(mesh: Object, vg_from_name: str, vg_to_name: str) -> None:
"""Mix vertex group weights.""" """Mix vertex group weights"""
vg_from = mesh.vertex_groups.get(vg_from_name) vg_from: Optional[VertexGroup] = mesh.vertex_groups.get(vg_from_name)
vg_to = mesh.vertex_groups.get(vg_to_name) vg_to: Optional[VertexGroup] = mesh.vertex_groups.get(vg_to_name)
if not vg_from or not vg_to: if not vg_from or not vg_to:
return return
num_vertices = len(mesh.data.vertices) num_vertices: int = len(mesh.data.vertices)
weights_from = np.zeros(num_vertices) weights_from: np.ndarray = np.zeros(num_vertices)
weights_to = np.zeros(num_vertices) weights_to: np.ndarray = np.zeros(num_vertices)
idx_from = vg_from.index idx_from: int = vg_from.index
idx_to = vg_to.index idx_to: int = vg_to.index
for v in mesh.data.vertices: for v in mesh.data.vertices:
for g in v.groups: for g in v.groups:
@@ -375,14 +373,14 @@ def mix_vertex_groups(mesh: Object, vg_from_name: str, vg_to_name: str):
elif g.group == idx_to: elif g.group == idx_to:
weights_to[v.index] = g.weight 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') vg_to.add(range(num_vertices), weights_combined.tolist(), 'REPLACE')
mesh.vertex_groups.remove(vg_from) mesh.vertex_groups.remove(vg_from)
def remove_unused_vertex_groups(mesh: Object): def remove_unused_vertex_groups(mesh: Object) -> None:
"""Remove vertex groups with no weights.""" """Remove vertex groups with no weights"""
for vg in mesh.vertex_groups: for vg in mesh.vertex_groups:
has_weights = False has_weights: bool = False
for vert in mesh.data.vertices: for vert in mesh.data.vertices:
for group in vert.groups: for group in vert.groups:
if group.group == vg.index and group.weight > 0.001: if group.group == vg.index and group.weight > 0.001:
@@ -393,9 +391,9 @@ def remove_unused_vertex_groups(mesh: Object):
if not has_weights: if not has_weights:
mesh.vertex_groups.remove(vg) mesh.vertex_groups.remove(vg)
def apply_armature_to_mesh(armature: Object, mesh: Object): def apply_armature_to_mesh(armature: Object, mesh: Object) -> None:
"""Apply armature deformation to mesh.""" """Apply armature deformation to mesh"""
armature_mod = mesh.modifiers.new('PoseToRest', 'ARMATURE') armature_mod: ArmatureModifier = mesh.modifiers.new('PoseToRest', 'ARMATURE')
armature_mod.object = armature armature_mod.object = armature
if bpy.app.version >= (3, 5): if bpy.app.version >= (3, 5):
@@ -407,15 +405,15 @@ def apply_armature_to_mesh(armature: Object, mesh: Object):
with bpy.context.temp_override(object=mesh): with bpy.context.temp_override(object=mesh):
bpy.ops.object.modifier_apply(modifier=armature_mod.name) bpy.ops.object.modifier_apply(modifier=armature_mod.name)
def apply_armature_to_mesh_with_shapekeys(armature: Object, mesh: Object, context: Context): def apply_armature_to_mesh_with_shapekeys(armature: Object, mesh: Object, context: Context) -> None:
"""Apply armature deformation to mesh with shape keys.""" """Apply armature deformation to mesh with shape keys"""
old_active_index = mesh.active_shape_key_index old_active_index: int = mesh.active_shape_key_index
old_show_only = mesh.show_only_shape_key old_show_only: bool = mesh.show_only_shape_key
mesh.show_only_shape_key = True mesh.show_only_shape_key = True
shape_keys = mesh.data.shape_keys.key_blocks shape_keys: List[ShapeKey] = mesh.data.shape_keys.key_blocks
vertex_groups = [] vertex_groups: List[str] = []
mutes = [] mutes: List[bool] = []
for sk in shape_keys: for sk in shape_keys:
vertex_groups.append(sk.vertex_group) vertex_groups.append(sk.vertex_group)
@@ -423,23 +421,23 @@ def apply_armature_to_mesh_with_shapekeys(armature: Object, mesh: Object, contex
mutes.append(sk.mute) mutes.append(sk.mute)
sk.mute = False sk.mute = False
disabled_mods = [] disabled_mods: List[Any] = []
for mod in mesh.modifiers: for mod in mesh.modifiers:
if mod.show_viewport: if mod.show_viewport:
mod.show_viewport = False mod.show_viewport = False
disabled_mods.append(mod) disabled_mods.append(mod)
arm_mod = mesh.modifiers.new('PoseToRest', 'ARMATURE') arm_mod: ArmatureModifier = mesh.modifiers.new('PoseToRest', 'ARMATURE')
arm_mod.object = armature arm_mod.object = armature
co_length = len(mesh.data.vertices) * 3 co_length: int = len(mesh.data.vertices) * 3
eval_cos = np.empty(co_length, dtype=np.single) eval_cos: np.ndarray = np.empty(co_length, dtype=np.single)
for i, shape_key in enumerate(shape_keys): for i, shape_key in enumerate(shape_keys):
mesh.active_shape_key_index = i mesh.active_shape_key_index = i
depsgraph = context.evaluated_depsgraph_get() 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) eval_mesh.data.vertices.foreach_get('co', eval_cos)
shape_key.data.foreach_set('co', eval_cos) shape_key.data.foreach_set('co', eval_cos)
+29 -21
View File
@@ -1,7 +1,7 @@
import bpy import bpy
from bpy.types import Operator, Context, Object from bpy.types import Operator, Context, Object, ArmatureModifier, VertexGroup
from mathutils import Vector from mathutils import Vector
from typing import Set, Optional from typing import Set, Optional, List, Any
from ...core.logging_setup import logger from ...core.logging_setup import logger
from ...core.translations import t from ...core.translations import t
@@ -15,28 +15,34 @@ from ...core.common import (
) )
class AvatarToolkit_OT_AttachMesh(Operator): class AvatarToolkit_OT_AttachMesh(Operator):
"""Attach a mesh to an armature bone with automatic weight setup""" """Operator to attach a mesh to an armature bone with automatic weight setup"""
bl_idname = "avatar_toolkit.attach_mesh" bl_idname: str = "avatar_toolkit.attach_mesh"
bl_label = t("AttachMesh.label") bl_label: str = t("AttachMesh.label")
bl_description = t("AttachMesh.desc") bl_description: str = t("AttachMesh.desc")
bl_options = {'REGISTER', 'UNDO'} bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context: Context) -> bool:
armature = get_active_armature(context) """Check if operator can be executed"""
return armature is not None and context.mode == 'OBJECT' and len(get_all_meshes(context)) > 0 armature: Optional[Object] = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return is_valid
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
try: try:
logger.info("Starting mesh attachment process") logger.info("Starting mesh attachment process")
mesh_name = context.scene.avatar_toolkit.attach_mesh mesh_name: str = context.scene.avatar_toolkit.attach_mesh
armature = get_active_armature(context) armature: Object = get_active_armature(context)
attach_bone_name = context.scene.avatar_toolkit.attach_bone attach_bone_name: str = context.scene.avatar_toolkit.attach_bone
mesh = bpy.data.objects.get(mesh_name) mesh: Optional[Object] = bpy.data.objects.get(mesh_name)
with ProgressTracker(context, 10, "Attaching Mesh") as progress: with ProgressTracker(context, 10, "Attaching Mesh") as progress:
# Validation steps # Validation steps
is_valid: bool
error_msg: str
is_valid, error_msg = validate_mesh_transforms(mesh) is_valid, error_msg = validate_mesh_transforms(mesh)
if not is_valid: if not is_valid:
raise ValueError(error_msg) raise ValueError(error_msg)
@@ -63,7 +69,7 @@ class AvatarToolkit_OT_AttachMesh(Operator):
bpy.ops.object.mode_set(mode='EDIT') bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT') 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.vertex_group_assign()
bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode='OBJECT')
progress.step(t("AttachMesh.setup_weights")) progress.step(t("AttachMesh.setup_weights"))
@@ -83,12 +89,14 @@ class AvatarToolkit_OT_AttachMesh(Operator):
progress.step(t("AttachMesh.create_bone")) progress.step(t("AttachMesh.create_bone"))
# Calculate bone placement # 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] 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) dimensions, roll_angle = calculate_bone_orientation(mesh, verts_in_group)
# Set bone position and orientation # Set bone position and orientation
center = Vector((0, 0, 0)) center: Vector = Vector((0, 0, 0))
for v in verts_in_group: for v in verts_in_group:
center += mesh.data.vertices[v.index].co center += mesh.data.vertices[v.index].co
center /= len(verts_in_group) center /= len(verts_in_group)
@@ -111,20 +119,20 @@ class AvatarToolkit_OT_AttachMesh(Operator):
self.report({'ERROR'}, str(e)) self.report({'ERROR'}, str(e))
return {'CANCELLED'} return {'CANCELLED'}
def validate_mesh_transforms(mesh): def validate_mesh_transforms(mesh: Optional[Object]) -> tuple[bool, str]:
"""Validate mesh transforms are suitable for attaching.""" """Validate mesh transforms are suitable for attaching"""
if not mesh: if not mesh:
return False, "Mesh not found" return False, "Mesh not found"
# Check for non-uniform scale # 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: 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 False, "Mesh has non-uniform scale. Please apply scale (Ctrl+A)"
return True, "" return True, ""
def validate_mesh_name(armature, mesh_name): def validate_mesh_name(armature: Object, mesh_name: str) -> tuple[bool, str]:
"""Validate mesh name doesn't conflict with existing bones.""" """Validate mesh name doesn't conflict with existing bones"""
if mesh_name in armature.data.bones: if mesh_name in armature.data.bones:
return False, f"Bone named '{mesh_name}' already exists in armature" return False, f"Bone named '{mesh_name}' already exists in armature"
return True, "" return True, ""
+96 -80
View File
@@ -5,8 +5,8 @@ import math
import bmesh import bmesh
import mathutils import mathutils
import json import json
from bpy.types import Operator, Object, Context from bpy.types import Operator, Object, Context, UILayout, WindowManager, Event, ShapeKey, EditBone, PoseBone
from typing import Optional, Dict, Tuple, Set from typing import Optional, Dict, Tuple, Set, List, Any, Union, ClassVar
from collections import OrderedDict from collections import OrderedDict
from random import random from random import random
from itertools import chain from itertools import chain
@@ -24,19 +24,19 @@ from ..core.common import (
apply_vertex_positions apply_vertex_positions
) )
VALID_EYE_NAMES = { VALID_EYE_NAMES: Dict[str, List[str]] = {
'left': ['LeftEye', 'Eye_L', 'eye_L', 'eye.L', 'EyeLeft', 'left_eye', 'l_eye'], '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'] 'right': ['RightEye', 'Eye_R', 'eye_R', 'eye.R', 'EyeRight', 'right_eye', 'r_eye']
} }
class CreateEyesAV3Button(bpy.types.Operator): class CreateEyesAV3Button(bpy.types.Operator):
"""Create eye tracking setup for VRChat Avatar 3.0""" """Creates eye tracking setup compatible with VRChat Avatar 3.0 system"""
bl_idname = 'avatar_toolkit.create_eye_tracking_av3' bl_idname: str = 'avatar_toolkit.create_eye_tracking_av3'
bl_label = t('EyeTracking.create.av3.label') bl_label: str = t('EyeTracking.create.av3.label')
bl_description = t('EyeTracking.create.av3.desc') bl_description: str = t('EyeTracking.create.av3.desc')
bl_options = {'REGISTER', 'UNDO'} bl_options: Set[str] = {'REGISTER', 'UNDO'}
mesh = None mesh: Optional[Object] = None
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -109,13 +109,13 @@ class CreateEyesAV3Button(bpy.types.Operator):
return {'CANCELLED'} return {'CANCELLED'}
class CreateEyesSDK2Button(bpy.types.Operator): class CreateEyesSDK2Button(bpy.types.Operator):
"""Create eye tracking setup for VRChat SDK2""" """Creates eye tracking setup compatible with VRChat SDK2 system"""
bl_idname = 'avatar_toolkit.create_eye_tracking_sdk2' bl_idname: str = 'avatar_toolkit.create_eye_tracking_sdk2'
bl_label = t('EyeTracking.create.sdk2.label') bl_label: str = t('EyeTracking.create.sdk2.label')
bl_description = t('EyeTracking.create.sdk2.desc') bl_description: str = t('EyeTracking.create.sdk2.desc')
bl_options = {'REGISTER', 'UNDO'} bl_options: Set[str] = {'REGISTER', 'UNDO'}
mesh = None mesh: Optional[Object] = None
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -201,8 +201,9 @@ class CreateEyesSDK2Button(bpy.types.Operator):
return {'CANCELLED'} return {'CANCELLED'}
class EyeTrackingBackup: class EyeTrackingBackup:
def __init__(self): """Manages backup and restoration of eye bone positions"""
self.backup_path = os.path.join(bpy.app.tempdir, "eye_tracking_backup.json") 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]]] = {} self.bone_positions: Dict[str, Dict[str, Tuple[float, float, float]]] = {}
def store_bone_positions(self, armature) -> bool: def store_bone_positions(self, armature) -> bool:
@@ -247,8 +248,10 @@ class EyeTrackingBackup:
return False return False
class EyeTrackingValidator: class EyeTrackingValidator:
"""Validates eye tracking setup requirements and configurations"""
@staticmethod @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) mesh = bpy.data.objects.get(mesh_name)
if not mesh: if not mesh:
return None, None return None, None
@@ -265,7 +268,8 @@ class EyeTrackingValidator:
return left_group, right_group return left_group, right_group
@staticmethod @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) armature = get_active_armature(context)
if not armature: if not armature:
return False, t('EyeTracking.validation.noArmature') return False, t('EyeTracking.validation.noArmature')
@@ -299,10 +303,11 @@ class EyeTrackingValidator:
return True, t('EyeTracking.validation.success') return True, t('EyeTracking.validation.success')
class StartTestingButton(bpy.types.Operator): class StartTestingButton(bpy.types.Operator):
bl_idname = 'avatar_toolkit.start_eye_testing' """Initiates eye tracking testing mode"""
bl_label = t('EyeTracking.testing.start.label') bl_idname: str = 'avatar_toolkit.start_eye_testing'
bl_description = t('EyeTracking.testing.start.desc') bl_label: str = t('EyeTracking.testing.start.label')
bl_options = {'REGISTER', 'UNDO'} bl_description: str = t('EyeTracking.testing.start.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -351,10 +356,11 @@ class StartTestingButton(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
class StopTestingButton(bpy.types.Operator): class StopTestingButton(bpy.types.Operator):
bl_idname = 'avatar_toolkit.stop_eye_testing' """Terminates eye tracking testing mode"""
bl_label = t('EyeTracking.testing.stop.label') bl_idname: str = 'avatar_toolkit.stop_eye_testing'
bl_description = t('EyeTracking.testing.stop.desc') bl_label: str = t('EyeTracking.testing.stop.label')
bl_options = {'REGISTER', 'UNDO'} bl_description: str = t('EyeTracking.testing.stop.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
def execute(self, context): def execute(self, context):
global eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot 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'} return {'FINISHED'}
def set_rotation(self, context): def set_rotation(self, context):
"""Updates eye bone rotations based on current settings"""
global eye_left, eye_right, eye_left_rot, eye_right_rot global eye_left, eye_right, eye_left_rot, eye_right_rot
toolkit = context.scene.avatar_toolkit toolkit = context.scene.avatar_toolkit
@@ -414,10 +421,11 @@ def set_rotation(self, context):
return None return None
class ResetRotationButton(bpy.types.Operator): class ResetRotationButton(bpy.types.Operator):
bl_idname = 'avatar_toolkit.reset_eye_rotation' """Resets eye bone rotations to default values"""
bl_label = t('EyeTracking.reset.label') bl_idname: str = 'avatar_toolkit.reset_eye_rotation'
bl_description = t('EyeTracking.reset.desc') bl_label: str = t('EyeTracking.reset.label')
bl_options = {'REGISTER', 'UNDO'} bl_description: str = t('EyeTracking.reset.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -445,10 +453,11 @@ class ResetRotationButton(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
class AdjustEyesButton(bpy.types.Operator): class AdjustEyesButton(bpy.types.Operator):
bl_idname = 'avatar_toolkit.adjust_eyes' """Adjusts eye bone positions and orientations"""
bl_label = t('EyeTracking.adjust.label') bl_idname: str = 'avatar_toolkit.adjust_eyes'
bl_description = t('EyeTracking.adjust.desc') bl_label: str = t('EyeTracking.adjust.label')
bl_options = {'REGISTER', 'UNDO'} bl_description: str = t('EyeTracking.adjust.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -494,10 +503,11 @@ class AdjustEyesButton(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
class StartIrisHeightButton(bpy.types.Operator): class StartIrisHeightButton(bpy.types.Operator):
bl_idname = 'avatar_toolkit.adjust_iris_height' """Adjusts iris height for eye meshes"""
bl_label = t('EyeTracking.iris.label') bl_idname: str = 'avatar_toolkit.adjust_iris_height'
bl_description = t('EyeTracking.iris.desc') bl_label: str = t('EyeTracking.iris.label')
bl_options = {'REGISTER', 'UNDO'} bl_description: str = t('EyeTracking.iris.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -536,10 +546,11 @@ class StartIrisHeightButton(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
class TestBlinking(bpy.types.Operator): class TestBlinking(bpy.types.Operator):
bl_idname = 'avatar_toolkit.test_blinking' """Tests eye blinking animations"""
bl_label = t('EyeTracking.blink.test.label') bl_idname: str = 'avatar_toolkit.test_blinking'
bl_description = t('EyeTracking.blink.test.desc') bl_label: str = t('EyeTracking.blink.test.label')
bl_options = {'REGISTER', 'UNDO'} bl_description: str = t('EyeTracking.blink.test.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -559,10 +570,11 @@ class TestBlinking(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
class TestLowerlid(bpy.types.Operator): class TestLowerlid(bpy.types.Operator):
bl_idname = 'avatar_toolkit.test_lowerlid' """Tests lower eyelid movements"""
bl_label = t('EyeTracking.lowerlid.test.label') bl_idname: str = 'avatar_toolkit.test_lowerlid'
bl_description = t('EyeTracking.lowerlid.test.desc') bl_label: str = t('EyeTracking.lowerlid.test.label')
bl_options = {'REGISTER', 'UNDO'} bl_description: str = t('EyeTracking.lowerlid.test.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -584,10 +596,11 @@ class TestLowerlid(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
class ResetBlinkTest(bpy.types.Operator): class ResetBlinkTest(bpy.types.Operator):
bl_idname = 'avatar_toolkit.reset_blink_test' """Resets all eye blinking test values"""
bl_label = t('EyeTracking.blink.reset.label') bl_idname: str = 'avatar_toolkit.reset_blink_test'
bl_description = t('EyeTracking.blink.reset.desc') bl_label: str = t('EyeTracking.blink.reset.label')
bl_options = {'REGISTER', 'UNDO'} bl_description: str = t('EyeTracking.blink.reset.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
def execute(self, context): def execute(self, context):
toolkit = context.scene.avatar_toolkit toolkit = context.scene.avatar_toolkit
@@ -601,7 +614,8 @@ class ResetBlinkTest(bpy.types.Operator):
return {'FINISHED'} 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 toolkit = context.scene.avatar_toolkit
scale = -toolkit.eye_distance + 1 scale = -toolkit.eye_distance + 1
mesh = bpy.data.objects[toolkit.mesh_name_eye] mesh = bpy.data.objects[toolkit.mesh_name_eye]
@@ -637,8 +651,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[y_cord] = new_eye.head[y_cord]
new_eye.tail[z_cord] = new_eye.head[z_cord] + 0.1 new_eye.tail[z_cord] = new_eye.head[z_cord] + 0.1
def repair_shapekeys(mesh_name, vertex_group): def repair_shapekeys(mesh_name: str, vertex_group: str) -> None:
"""Fix VRC shape keys by slightly adjusting vertex positions""" """Repairs VRChat shape keys by adjusting vertex positions"""
armature = get_active_armature(bpy.context) armature = get_active_armature(bpy.context)
mesh = bpy.data.objects[mesh_name] mesh = bpy.data.objects[mesh_name]
mesh.select_set(True) mesh.select_set(True)
@@ -696,10 +710,12 @@ def repair_shapekeys(mesh_name, vertex_group):
logger.warning('Shape key repair failed, using random method') logger.warning('Shape key repair failed, using random method')
repair_shapekeys_mouth(mesh_name) repair_shapekeys_mouth(mesh_name)
def randBoolNumber(): def randBoolNumber() -> int:
"""Generates random boolean value as integer"""
return -1 if random() < 0.5 else 1 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 = bpy.data.objects[mesh_name]
mesh.select_set(True) mesh.select_set(True)
bpy.context.view_layer.objects.active = mesh bpy.context.view_layer.objects.active = mesh
@@ -730,12 +746,12 @@ def repair_shapekeys_mouth(mesh_name):
if not moved: if not moved:
logger.error('Random shape key repair failed') logger.error('Random shape key repair failed')
def get_bone_orientations(): def get_bone_orientations() -> Tuple[int, int, int]:
"""Get bone orientation axes""" """Returns standardized bone orientation axes"""
return (0, 1, 2) # x, y, z coordinates return (0, 1, 2) # x, y, z coordinates
def find_center_vector_of_vertex_group(mesh, group_name): def find_center_vector_of_vertex_group(mesh: Object, group_name: str) -> Union[mathutils.Vector, bool]:
"""Calculate center position of vertex group""" """Calculates center position of vertex group"""
group = mesh.vertex_groups.get(group_name) group = mesh.vertex_groups.get(group_name)
if not group: if not group:
return False return False
@@ -751,8 +767,8 @@ def find_center_vector_of_vertex_group(mesh, group_name):
return sum((v for v in vertices), mathutils.Vector()) / len(vertices) return sum((v for v in vertices), mathutils.Vector()) / len(vertices)
def vertex_group_exists(mesh_obj, group_name): def vertex_group_exists(mesh_obj: Object, group_name: str) -> bool:
"""Check if vertex group exists and has weights""" """Verifies existence and validity of vertex group"""
if not mesh_obj or group_name not in mesh_obj.vertex_groups: if not mesh_obj or group_name not in mesh_obj.vertex_groups:
return False return False
@@ -763,8 +779,8 @@ def vertex_group_exists(mesh_obj, group_name):
return True return True
return False return False
def copy_vertex_group(self, vertex_group, rename_to): def copy_vertex_group(self: Any, vertex_group: str, rename_to: str) -> None:
"""Copy vertex group with new name""" """Creates copy of vertex group with new name"""
vertex_group_index = 0 vertex_group_index = 0
# Select and make mesh active # Select and make mesh active
bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode='OBJECT')
@@ -781,8 +797,8 @@ def copy_vertex_group(self, vertex_group, rename_to):
vertex_group_index += 1 vertex_group_index += 1
def copy_shape_key(self, context, from_shape, new_names, new_index): def copy_shape_key(self: Any, context: Context, from_shape: str, new_names: List[str], new_index: int) -> str:
"""Copy shape key with new name""" """Creates copy of shape key with new name"""
blinking = not context.scene.avatar_toolkit.disable_eye_blinking blinking = not context.scene.avatar_toolkit.disable_eye_blinking
new_name = new_names[new_index - 1] new_name = new_names[new_index - 1]
@@ -847,11 +863,11 @@ class VertexGroupCache:
cls._cache.clear() cls._cache.clear()
class RotateEyeBonesForAv3Button(Operator): class RotateEyeBonesForAv3Button(Operator):
"""Reorient eye bones for proper VRChat eye tracking""" """Reorients eye bones for VRChat Avatar 3.0 compatibility"""
bl_idname = "avatar_toolkit.rotate_eye_bones" bl_idname: str = "avatar_toolkit.rotate_eye_bones"
bl_label = t("EyeTracking.rotate.label") bl_label: str = t("EyeTracking.rotate.label")
bl_description = t("EyeTracking.rotate.desc") bl_description: str = t("EyeTracking.rotate.desc")
bl_options = {'REGISTER', 'UNDO'} bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -874,11 +890,11 @@ class RotateEyeBonesForAv3Button(Operator):
return {'FINISHED'} return {'FINISHED'}
class ResetEyeTrackingButton(Operator): class ResetEyeTrackingButton(Operator):
"""Reset all eye tracking settings and state""" """Resets all eye tracking settings to default values"""
bl_idname = 'avatar_toolkit.reset_eye_tracking' bl_idname: str = 'avatar_toolkit.reset_eye_tracking'
bl_label = t('EyeTracking.reset.label') bl_label: str = t('EyeTracking.reset.label')
bl_description = t('EyeTracking.reset.desc') bl_description: str = t('EyeTracking.reset.desc')
bl_options = {'REGISTER', 'UNDO'} bl_options: Set[str] = {'REGISTER', 'UNDO'}
def execute(self, context): def execute(self, context):
global eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot global eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot
@@ -888,7 +904,7 @@ class ResetEyeTrackingButton(Operator):
return {'FINISHED'} return {'FINISHED'}
def validate_weights(mesh_obj: Object, vertex_group: str) -> bool: 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) group = mesh_obj.vertex_groups.get(vertex_group)
if not group: if not group:
return False return False
@@ -899,8 +915,8 @@ def validate_weights(mesh_obj: Object, vertex_group: str) -> bool:
return True return True
return False return False
def get_eye_bone_names(armature: Object) -> Dict[str, str]: def get_eye_bone_names(armature: Object) -> Dict[str, Optional[str]]:
"""Get standardized eye bone names""" """Retrieves standardized eye bone names from armature"""
eye_bones = {'left': None, 'right': None} eye_bones = {'left': None, 'right': None}
for bone in armature.data.bones: for bone in armature.data.bones:
@@ -912,7 +928,7 @@ def get_eye_bone_names(armature: Object) -> Dict[str, str]:
return eye_bones return eye_bones
def stop_testing(context: Context) -> None: 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 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]): if not all([eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot]):
@@ -27,6 +27,11 @@ def consolidate_nodes(node1: ShaderNodeTexImage, node2: ShaderNodeTexImage) -> N
"""Transfer properties from one texture node to another to ensure consistency""" """Transfer properties from one texture node to another to ensure consistency"""
node2.color_space = node1.color_space node2.color_space = node1.color_space
node2.coordinates = node1.coordinates 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: def consolidate_textures(node_tree1: NodeTree, node_tree2: NodeTree) -> None:
"""Synchronize texture nodes between two material node trees""" """Synchronize texture nodes between two material node trees"""
@@ -81,6 +86,9 @@ class AvatarToolkit_OT_CombineMaterials(Operator):
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context: Context) -> bool:
"""Check if the operator can be executed""" """Check if the operator can be executed"""
if context.mode != 'OBJECT':
return False
armature = get_active_armature(context) armature = get_active_armature(context)
if not armature: if not armature:
return False return False
+4
View File
@@ -134,6 +134,10 @@ class AvatarToolKit_OT_DeleteBoneConstraints(Operator):
def execute(self, context: Context) -> set[str]: def execute(self, context: Context) -> set[str]:
"""Execute the constraint removal operation""" """Execute the constraint removal operation"""
# Make sure we are in Object mode first or it will error
bpy.ops.object.mode_set(mode='OBJECT')
armature = get_active_armature(context) armature = get_active_armature(context)
# Select armature and make it active before changing mode # Select armature and make it active before changing mode
+41 -18
View File
@@ -1,9 +1,8 @@
# 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. # 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. # Didn't think it was necessary to re-make something that works well.
import bpy 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 bpy.types import Operator, Context, Object, ShapeKey
from collections import OrderedDict from collections import OrderedDict
from ..core.logging_setup import logger from ..core.logging_setup import logger
@@ -16,22 +15,24 @@ from ..core.common import (
) )
class VisemeCache: class VisemeCache:
"""Caches generated viseme shape data""" """Manages caching of generated viseme shape data for performance optimization"""
_cache: Dict = {} _cache: Dict[Tuple[str, Tuple[Tuple]], List] = {}
@classmethod @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)) cache_key = (key, tuple(tuple(x) for x in mix_data))
return cls._cache.get(cache_key) return cls._cache.get(cache_key)
@classmethod @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)) cache_key = (key, tuple(tuple(x) for x in mix_data))
cls._cache[cache_key] = shape_data cls._cache[cache_key] = shape_data
class VisemePreview: class VisemePreview:
"""Handles viseme preview functionality""" """Controls real-time preview functionality for viseme shapes"""
_preview_data: Dict = {} _preview_data: Dict[str, float] = {}
_active: bool = False _active: bool = False
_preview_shapes: Optional[OrderedDict] = None _preview_shapes: Optional[OrderedDict] = None
@@ -117,18 +118,29 @@ class VisemePreview:
cls._preview_shapes = None cls._preview_shapes = None
class ATOOLKIT_OT_preview_visemes(Operator): class ATOOLKIT_OT_preview_visemes(Operator):
bl_idname = "avatar_toolkit.preview_visemes" """Operator for previewing viseme shapes in real-time"""
bl_label = t("Visemes.preview_label") bl_idname: str = "avatar_toolkit.preview_visemes"
bl_description = t("Visemes.preview_desc") bl_label: str = t("Visemes.preview_label")
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} bl_description: str = t("Visemes.preview_desc")
bl_options: Set[str] = {'REGISTER', 'UNDO', 'INTERNAL'}
@classmethod @classmethod
def poll(cls, context: Context) -> bool: 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) armature = get_active_armature(context)
if not armature: if not armature:
return False return False
valid, _ = validate_armature(armature) valid, _ = validate_armature(armature)
return valid and context.active_object and context.active_object.type == 'MESH' return valid and mesh_obj and mesh_obj.type == 'MESH'
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
props = context.scene.avatar_toolkit props = context.scene.avatar_toolkit
@@ -165,18 +177,29 @@ def validate_deformation(mesh, mix_data):
return max_deform < (mesh_size * 0.4) return max_deform < (mesh_size * 0.4)
class ATOOLKIT_OT_create_visemes(Operator): class ATOOLKIT_OT_create_visemes(Operator):
bl_idname = "avatar_toolkit.create_visemes" """Operator for generating VRChat-compatible viseme shape keys"""
bl_label = t("Visemes.create_label") bl_idname: str = "avatar_toolkit.create_visemes"
bl_description = t("Visemes.create_desc") bl_label: str = t("Visemes.create_label")
bl_options = {'REGISTER', 'UNDO'} bl_description: str = t("Visemes.create_desc")
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod @classmethod
def poll(cls, context: Context) -> bool: 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) armature = get_active_armature(context)
if not armature: if not armature:
return False return False
valid, _ = validate_armature(armature) valid, _ = validate_armature(armature)
return valid and context.active_object and context.active_object.type == 'MESH' return valid and mesh_obj and mesh_obj.type == 'MESH'
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
props = context.scene.avatar_toolkit props = context.scene.avatar_toolkit
+26 -1
View File
@@ -1,7 +1,7 @@
{ {
"authors": ["Avatar Toolkit Team"], "authors": ["Avatar Toolkit Team"],
"messages": { "messages": {
"AvatarToolkit.label": "Avatar Toolkit (Alpha 0.1.0)", "AvatarToolkit.label": "Avatar Toolkit (Alpha 0.1.3)",
"AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there", "AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there",
"AvatarToolkit.desc2": "will be issues, if you find any issues,", "AvatarToolkit.desc2": "will be issues, if you find any issues,",
"AvatarToolkit.desc3": "please report it on our Github.", "AvatarToolkit.desc3": "please report it on our Github.",
@@ -231,6 +231,8 @@
"Visemes.error.no_shapekeys": "Mesh has no shape keys", "Visemes.error.no_shapekeys": "Mesh has no shape keys",
"Visemes.error.select_shapekeys": "Please select shape keys for A, O and CH", "Visemes.error.select_shapekeys": "Please select shape keys for A, O and CH",
"Visemes.success": "Visemes created successfully", "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.label": "Eye Tracking",
"EyeTracking.setup": "Eye Tracking Setup", "EyeTracking.setup": "Eye Tracking Setup",
@@ -314,6 +316,8 @@
"EyeTracking.type.av3_desc": "VRChat Avatar 3.0 eye tracking setup", "EyeTracking.type.av3_desc": "VRChat Avatar 3.0 eye tracking setup",
"EyeTracking.type.sdk2": "SDK2 (Legacy)", "EyeTracking.type.sdk2": "SDK2 (Legacy)",
"EyeTracking.type.sdk2_desc": "VRChat SDK2 eye tracking setup", "EyeTracking.type.sdk2_desc": "VRChat SDK2 eye tracking setup",
"EyeTracking.adjust.label": "Adjust Eye Position",
"EyeTracking.adjust.desc": "Adjust the position of eye bones based on vertex groups",
"CustomPanel.label": "Custom Avatar Tools", "CustomPanel.label": "Custom Avatar Tools",
"CustomPanel.merge_mode": "Merge Mode", "CustomPanel.merge_mode": "Merge Mode",
@@ -376,6 +380,26 @@
"MergeArmature.cleanup_shape_keys": "Clean Shape Keys", "MergeArmature.cleanup_shape_keys": "Clean Shape Keys",
"MergeArmature.cleanup_shape_keys_desc": "Remove unused 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",
"Settings.label": "Settings", "Settings.label": "Settings",
"Settings.language": "Language", "Settings.language": "Language",
"Settings.language_desc": "Select interface language", "Settings.language_desc": "Select interface language",
@@ -396,6 +420,7 @@
"Language.auto": "Automatic", "Language.auto": "Automatic",
"Language.en_US": "English", "Language.en_US": "English",
"Language.ja_JP": "Japanese", "Language.ja_JP": "Japanese",
"Language.ko_KR": "Korean",
"Language.changed.title": "Language Changed", "Language.changed.title": "Language Changed",
"Language.changed.success": "Language changed successfully!", "Language.changed.success": "Language changed successfully!",
"Language.changed.restart": "Some UI elements may require restarting Blender" "Language.changed.restart": "Some UI elements may require restarting Blender"
+405 -275
View File
@@ -1,298 +1,428 @@
{ {
"authors": ["Avatar Toolkit Team"], "authors": ["Avatar Toolkit Team"],
"messages": { "messages": {
"AutoVisemeButton.desc": "シェイプキーに基づいて自動的にビセムを作成", "AvatarToolkit.label": "アバターツールキット (Alpha 0.1.3)",
"AutoVisemeButton.error.noShapekeys": "シェイプキーが見つかりません", "AvatarToolkit.desc1": "アバターツールキットは早期アクセス中で",
"AutoVisemeButton.error.selectShapekeys": "シェイプキーを選択してください", "AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、",
"AutoVisemeButton.label": "ビセムを作成", "AvatarToolkit.desc3": "GitHubで報告してください。",
"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": "ウェイトの最適化が完了しました",
"Updater.label": "アップデーター", "Updater.label": "アップデーター",
"Updater.CheckForUpdateButton.label": "アップデートを確認", "Updater.CheckForUpdateButton.label": "アップデートを確認",
"Updater.CheckForUpdateButton.label_alt": "利用可能なアップデートはありません", "Updater.CheckForUpdateButton.label_alt": "利用可能なアップデートはありません",
"Updater.UpdateToLatestButton.label": "{name}にアップデート", "Updater.UpdateToLatestButton.label": "{name}にアップデート",
"Updater.UpdateToSelectedButton.label": "アップデート", "Updater.UpdateToSelectedButton.label": "アップデート",
"Updater.currentVersion": "現在のバージョン{name}", "Updater.currentVersion": "現在のバージョン: {name}",
"Updater.selectVersion": "バージョンを選択",
"Updater.CheckForUpdateButton.desc": "利用可能なアップデートを確認", "Updater.CheckForUpdateButton.desc": "利用可能なアップデートを確認",
"UpdateToLatestButton.desc": "最新バージョンにアップデート", "UpdateToLatestButton.desc": "最新バージョンにアップデート",
"UpdateNotificationPopup.label": "アップデート通知", "UpdateNotificationPopup.label": "アップデート通知",
"UpdateNotificationPopup.desc": "利用可能なアップデートについての通知", "UpdateNotificationPopup.desc": "利用可能なアップデートの通知",
"UpdateNotificationPopup.newUpdate": "新しいアップデートが利用可能{version}", "UpdateNotificationPopup.newUpdate": "新しいアップデートが利用可能: {version}",
"RestartBlenderPopup.label": "Blenderを再起動", "RestartBlenderPopup.label": "Blenderを再起動",
"RestartBlenderPopup.desc": "アップデートを完了するためにBlenderを再起動", "RestartBlenderPopup.desc": "アップデートを完了するためにBlenderを再起動",
"RestartBlenderPopup.message": "アップデート成功しました!Blenderを再起動してください。", "RestartBlenderPopup.message": "アップデート成功!Blenderを再起動してください。",
"check_for_update.cantCheck": "アップデートを確認できません", "check_for_update.cantCheck": "アップデートを確認できません",
"download_file.cantConnect": "アップデートサーバーに接続できません", "download_file.cantConnect": "アップデートサーバーに接続できません",
"download_file.cantFindZip": "アップデートファイルが見つかりません", "download_file.cantFindZip": "アップデートファイルが見つかりません",
"download_file.cantFindAvatarToolkit": "アップデートパッケージにAvatar Toolkitファイルが見つかりません", "download_file.cantFindAvatarToolkit": "アップデートパッケージにAvatarToolkitファイルが見つかりません",
"CreditsSupport.label": "クレジット&サポート",
"CreditsSupport.credits_title": "クレジット", "QuickAccess.label": "クイックアクセス",
"CreditsSupport.credits_text1": "Avatar Toolkitは以下のNeonekoチームによって作成されました:", "QuickAccess.select_armature": "アーマチュアを選択",
"CreditsSupport.credits_text2": "YusarinaとOnan989", "QuickAccess.valid_armature": "有効なアーマチュア",
"CreditsSupport.credits_text3": "一部のコードはCats Blender Pluginを参考にしています。", "QuickAccess.bones_count": "ボーン数: {count}",
"CreditsSupport.credits_text4": "元のプラグインの貢献者に感謝します。", "QuickAccess.pose_bones_available": "ポーズボーン: 利用可能",
"CreditsSupport.support_text1": "私たちの活動を支援したい場合は、", "QuickAccess.pose_controls": "ポーズコントロール",
"CreditsSupport.support_text2": "pally.ggページで寄付/投げ銭ができます。", "QuickAccess.import_export": "インポート/エクスポート",
"CreditsSupport.support_title": "ポートする", "QuickAccess.import": "インポート",
"CreditsSupport.support_button": "ポートする", "QuickAccess.export": "エクスポート",
"CreditsSupport.help_title": "ヘルプが必要ですか?", "QuickAccess.export_fbx": "FBXエクスポート",
"CreditsSupport.help_text1": "まずはWikiをご確認ください。さらなるサポート", "QuickAccess.export_resonite": "Resoniteにエクスポート",
"CreditsSupport.help_text2": "求める前にWikiを読むことを強くお勧めします。", "QuickAccess.start_pose_mode.label": "ポーズモード開始",
"CreditsSupport.wiki_button": "Wiki", "QuickAccess.start_pose_mode.desc": "選択したアーマチュアのポーズモードに入る",
"CreditsSupport.discord_button": "Discordに参加", "QuickAccess.stop_pose_mode.label": "ポーズモード終了",
"TextureAtlas.include_in_atlas": "アトラスに含める", "QuickAccess.stop_pose_mode.desc": "ポーズモードを終了し、変形をクリア",
"TextureAtlas.include_in_atlas_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": "アーマチュアの検証は行われていません",
"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": "手首/手のボーンの対称性が不足しています",
"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.name": "バージョンリスト",
"Scene.avatar_toolkit_updater_version_list.description": "アップデート可能なバージョンのリスト", "Scene.avatar_toolkit_updater_version_list.description": "利用可能なバージョンのリスト",
"TextureAtlas.albedo": "アルベド",
"TextureAtlas.normal": "法線", "Optimization.label": "最適化",
"TextureAtlas.emission": "発光", "Optimization.materials_title": "マテリアル",
"TextureAtlas.ambient_occlusion": "アンビエントオクルージョン", "Optimization.cleanup_title": "メッシュクリーンアップ",
"TextureAtlas.height": "ハイト", "Optimization.join_meshes_title": "メッシュ結合",
"TextureAtlas.roughness": "ラフネス", "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.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": "重みなしボーンを削除",
"Tools.clean_weights_desc": "頂点の重みがないボーンを削除",
"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}個の未使用シェイプキーを削除しました",
"MMD.label": "MMDツール",
"MMD.bone_standardization": "ボーン標準化",
"MMD.weight_processing": "ウェイト処理",
"MMD.hierarchy": "ボーン階層",
"MMD.cleanup": "クリーンアップ",
"MMD.no_armature": "アーマチュアが選択されていません",
"MMD.no_meshes": "メッシュが見つかりません",
"MMD.validation.rigify_unsupported": "Rigifyアーマチュアはサポートされていません",
"MMD.validation.multi_user_mesh": "複数ユーザーメッシュが検出されました: {mesh}",
"MMD.bones_standardized": "ボーンが正常に標準化されました",
"MMD.weights_processed": "ウェイトが正常に処理されました",
"MMD.hierarchy_fixed": "ボーン階層が正常に修正されました",
"MMD.hierarchy_validation_warning": "一部の階層関係を検証できませんでした",
"MMD.cleanup_completed": "アーマチュアのクリーンアップが完了しました",
"MMD.process_twist_bones": "ツイストボーンを処理",
"MMD.process_twist_bones_desc": "ツイストボーンの重みを親ボーンに転送",
"MMD.connect_bones": "ボーンを接続",
"MMD.connect_bones_desc": "適切な場所でボーンチェーンを接続",
"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 Avatar 3.0用のアイトラッキングを設定",
"EyeTracking.create.sdk2.label": "SDK2アイトラッキングを作成",
"EyeTracking.create.sdk2.desc": "VRChat SDK2用のアイトラッキングを設定",
"EyeTracking.sdk_version": "SDKバージョン",
"EyeTracking.type.av3": "Avatar 3.0",
"EyeTracking.type.av3_desc": "VRChat Avatar 3.0アイトラッキング設定",
"EyeTracking.type.sdk2": "SDK2(レガシー)",
"EyeTracking.type.sdk2_desc": "VRChat SDK2アイトラッキング設定",
"EyeTracking.adjust.label": "目の位置を調整",
"EyeTracking.adjust.desc": "頂点グループに基づいて目のボーンの位置を調整",
"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.error.label": "エラー",
"TextureAtlas.none.label": "なし", "TextureAtlas.none.label": "なし",
"TextureAtlas.no_nodes_error.desc": "このマテリアルはノードを使用していません!", "TextureAtlas.no_nodes_error.desc": "このマテリアルはノードを使用していません!",
"TextureAtlas.no_images_error.desc": "このマテリアルには画像がありません!", "TextureAtlas.no_images_error.desc": "このマテリアルには画像がありません!",
"TextureAtlas.texture_use_atlas.desc": "{name}マップアトラスに使用るテクスチャ", "TextureAtlas.texture_use_atlas.desc": "{name}マップアトラスに使用されるテクスチャ",
"TextureAtlas.no_materials_selected": "アトラス用のマテリアルが選択されていません", "TextureAtlas.albedo": "アルベド",
"Optimization.select_armature": "アーマチュアを選択してください", "TextureAtlas.normal": "法線",
"CheckForUpdateButton.label": "アップデートを確認", "TextureAtlas.emission": "発光",
"CheckForUpdateButton.desc": "利用可能なアップデートを確認", "TextureAtlas.ambient_occlusion": "アンビエントオクルージョン",
"UpdateToLatestButton.label": "最新バージョンにアップデート" "TextureAtlas.height": "高さ",
"TextureAtlas.roughness": "ラフネス",
"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": "デバッグログが無効になりました",
"Language.auto": "自動",
"Language.en_US": "英語",
"Language.ja_JP": "日本語",
"Language.ko_KR": "韓国語",
"Language.changed.title": "言語が変更されました",
"Language.changed.success": "言語が正常に変更されました!",
"Language.changed.restart": "一部のUI要素の更新にはBlenderの再起動が必要な場合があります"
} }
} }
+428
View File
@@ -0,0 +1,428 @@
{
"authors": ["Avatar Toolkit Team"],
"messages": {
"AvatarToolkit.label": "아바타 툴킷 (알파 0.1.3)",
"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": "아마추어 검증이 수행되지 않음",
"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": "손/손목의 대칭 본 누락",
"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.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.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}개의 미사용 쉐이프 키 제거됨",
"MMD.label": "MMD 도구",
"MMD.bone_standardization": "본 표준화",
"MMD.weight_processing": "가중치 처리",
"MMD.hierarchy": "본 계층 구조",
"MMD.cleanup": "정리",
"MMD.no_armature": "선택된 아마추어 없음",
"MMD.no_meshes": "메시를 찾을 수 없음",
"MMD.validation.rigify_unsupported": "Rigify 아마추어는 지원되지 않음",
"MMD.validation.multi_user_mesh": "다중 사용자 메시 감지됨: {mesh}",
"MMD.bones_standardized": "본이 성공적으로 표준화됨",
"MMD.weights_processed": "가중치가 성공적으로 처리됨",
"MMD.hierarchy_fixed": "본 계층 구조가 성공적으로 수정됨",
"MMD.hierarchy_validation_warning": "일부 계층 관계를 검증할 수 없음",
"MMD.cleanup_completed": "아마추어 정리 완료",
"MMD.process_twist_bones": "트위스트 본 처리",
"MMD.process_twist_bones_desc": "트위스트 본의 가중치를 부모 본으로 전송",
"MMD.connect_bones": "본 연결",
"MMD.connect_bones_desc": "적절한 경우 체인의 본 연결",
"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 Avatar 3.0용 시선 추적 설정",
"EyeTracking.create.sdk2.label": "SDK2 시선 추적 생성",
"EyeTracking.create.sdk2.desc": "VRChat SDK2용 시선 추적 설정",
"EyeTracking.sdk_version": "SDK 버전",
"EyeTracking.type.av3": "Avatar 3.0",
"EyeTracking.type.av3_desc": "VRChat Avatar 3.0 시선 추적 설정",
"EyeTracking.type.sdk2": "SDK2 (레거시)",
"EyeTracking.type.sdk2_desc": "VRChat SDK2 시선 추적 설정",
"EyeTracking.adjust.label": "눈 위치 조정",
"EyeTracking.adjust.desc": "버텍스 그룹을 기반으로 눈 본 위치 조정",
"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": "거칠기",
"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": "디버그 로깅이 비활성화됨",
"Language.auto": "자동",
"Language.en_US": "영어",
"Language.ja_JP": "일본어",
"Language.ko_KR": "한국어",
"Language.changed.title": "언어 변경됨",
"Language.changed.success": "언어가 성공적으로 변경됨!",
"Language.changed.restart": "일부 UI 요소는 블렌더 재시작이 필요할 수 있음"
}
}
+188
View File
@@ -0,0 +1,188 @@
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
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:
if not context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown:
context.scene.avatar_toolkit.materials.clear()
newlist: list[Material] = []
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
else:
context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown = False
return {'FINISHED'}
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.operator("avatar_toolkit.select_all_materials", text="", icon='CHECKBOX_HLT')
row.operator("avatar_toolkit.select_none_materials", text="", icon='CHECKBOX_DEHLT')
row.operator("avatar_toolkit.expand_all_materials", text="", icon='DISCLOSURE_TRI_DOWN')
row.operator("avatar_toolkit.collapse_all_materials", text="", icon='DISCLOSURE_TRI_RIGHT')
row.prop(context.scene.avatar_toolkit, "material_search_filter", text="", icon='VIEWZOOM')
box = layout.box()
row = box.row()
row.label(text=f"Estimated Atlas Size: {self.calculate_atlas_size(context)}px")
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
row = layout.row()
row.prop(item.mat, "include_in_atlas", text="", icon='CHECKBOX_HLT' if item.mat.include_in_atlas else 'CHECKBOX_DEHLT')
row.prop(item.mat, "material_expanded",
text=item.mat.name,
icon='DOWNARROW_HLT' if item.mat.material_expanded else 'RIGHTARROW',
emboss=False)
if item.mat.material_expanded and item.mat.include_in_atlas:
box = layout.box()
col = box.column(align=True)
self.draw_texture_row(col, item.mat, "texture_atlas_albedo", "IMAGE_RGB")
self.draw_texture_row(col, item.mat, "texture_atlas_normal", "NORMALS_FACE")
self.draw_texture_row(col, item.mat, "texture_atlas_emission", "LIGHT")
self.draw_texture_row(col, item.mat, "texture_atlas_ambient_occlusion", "SHADING_SOLID")
self.draw_texture_row(col, item.mat, "texture_atlas_height", "IMAGE_ZDEPTH")
self.draw_texture_row(col, item.mat, "texture_atlas_roughness", "MATERIAL")
col.separator(factor=0.5)
def draw_texture_row(self, layout, material, prop_name, icon):
row = layout.row()
row.prop(material, prop_name, icon=icon)
if getattr(material, prop_name):
row.label(text="", icon='CHECKMARK')
else:
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
for mat in context.scene.avatar_toolkit.materials:
if mat.mat.include_in_atlas:
if mat.mat.texture_atlas_albedo:
img = bpy.data.images[mat.mat.texture_atlas_albedo]
total_size += img.size[0] * img.size[1]
return f"{int(sqrt(total_size))}x{int(sqrt(total_size))}"
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 = 6
def draw(self, context: Context):
layout = self.layout
armature = get_active_armature(context)
if armature:
layout.label(text=t("TextureAtlas.label"), icon='TEXTURE')
layout.separator(factor=0.5)
box = layout.box()
row = box.row()
direction_icon = 'RIGHTARROW' if not context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown else 'DOWNARROW_HLT'
row.operator(AvatarToolKit_OT_ExpandSectionMaterials.bl_idname,
text=(t("TextureAtlas.reload_list") if not context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown else t("TextureAtlas.loaded_list")),
icon=direction_icon)
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.operator(AvatarToolKit_OT_AtlasMaterials.bl_idname,
text=t("TextureAtlas.atlas_materials"),
icon='NODE_TEXTURE')
else:
layout.label(text=t("Tools.select_armature"), icon='ERROR')
+47 -41
View File
@@ -1,6 +1,6 @@
import bpy import bpy
from typing import Set from typing import Set, List, Tuple, Any
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 .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..core.translations import t from ..core.translations import t
from ..core.common import ( from ..core.common import (
@@ -11,10 +11,11 @@ from ..core.common import (
) )
class AvatarToolkit_OT_SearchMergeArmatureInto(Operator): class AvatarToolkit_OT_SearchMergeArmatureInto(Operator):
bl_idname = "avatar_toolkit.search_merge_armature_into" """Search operator for selecting target armature to merge into"""
bl_label = "" bl_idname: str = "avatar_toolkit.search_merge_armature_into"
bl_description = t('MergeArmature.into_search_desc') bl_label: str = ""
bl_property = "search_merge_armature_into_enum" bl_description: str = t('MergeArmature.into_search_desc')
bl_property: str = "search_merge_armature_into_enum"
search_merge_armature_into_enum: bpy.props.EnumProperty( search_merge_armature_into_enum: bpy.props.EnumProperty(
name=t('MergeArmature.into'), name=t('MergeArmature.into'),
@@ -22,19 +23,20 @@ class AvatarToolkit_OT_SearchMergeArmatureInto(Operator):
items=get_armature_list 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 context.scene.avatar_toolkit.merge_armature_into = self.search_merge_armature_into_enum
return {'FINISHED'} return {'FINISHED'}
def invoke(self, context, event): def invoke(self, context: Context, event: Event) -> Set[str]:
context.window_manager.invoke_search_popup(self) context.window_manager.invoke_search_popup(self)
return {'FINISHED'} return {'FINISHED'}
class AvatarToolkit_OT_SearchMergeArmature(Operator): class AvatarToolkit_OT_SearchMergeArmature(Operator):
bl_idname = "avatar_toolkit.search_merge_armature" """Search operator for selecting source armature to merge from"""
bl_label = "" bl_idname: str = "avatar_toolkit.search_merge_armature"
bl_description = t('MergeArmature.from_search_desc') bl_label: str = ""
bl_property = "search_merge_armature_enum" bl_description: str = t('MergeArmature.from_search_desc')
bl_property: str = "search_merge_armature_enum"
search_merge_armature_enum: bpy.props.EnumProperty( search_merge_armature_enum: bpy.props.EnumProperty(
name=t('MergeArmature.from'), name=t('MergeArmature.from'),
@@ -42,19 +44,20 @@ class AvatarToolkit_OT_SearchMergeArmature(Operator):
items=get_armature_list 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 context.scene.avatar_toolkit.merge_armature = self.search_merge_armature_enum
return {'FINISHED'} return {'FINISHED'}
def invoke(self, context, event): def invoke(self, context: Context, event: Event) -> Set[str]:
context.window_manager.invoke_search_popup(self) context.window_manager.invoke_search_popup(self)
return {'FINISHED'} return {'FINISHED'}
class AvatarToolkit_OT_SearchAttachMesh(Operator): class AvatarToolkit_OT_SearchAttachMesh(Operator):
bl_idname = "avatar_toolkit.search_attach_mesh" """Search operator for selecting mesh to attach to armature"""
bl_label = "" bl_idname: str = "avatar_toolkit.search_attach_mesh"
bl_description = t('AttachMesh.search_desc') bl_label: str = ""
bl_property = "search_attach_mesh_enum" bl_description: str = t('AttachMesh.search_desc')
bl_property: str = "search_attach_mesh_enum"
search_attach_mesh_enum: bpy.props.EnumProperty( search_attach_mesh_enum: bpy.props.EnumProperty(
name=t('AttachMesh.select'), name=t('AttachMesh.select'),
@@ -67,19 +70,20 @@ class AvatarToolkit_OT_SearchAttachMesh(Operator):
] ]
) )
def execute(self, context): def execute(self, context: Context) -> Set[str]:
context.scene.avatar_toolkit.attach_mesh = self.search_attach_mesh_enum context.scene.avatar_toolkit.attach_mesh = self.search_attach_mesh_enum
return {'FINISHED'} return {'FINISHED'}
def invoke(self, context, event): def invoke(self, context: Context, event: Event) -> Set[str]:
context.window_manager.invoke_search_popup(self) context.window_manager.invoke_search_popup(self)
return {'FINISHED'} return {'FINISHED'}
class AvatarToolkit_OT_SearchAttachBone(Operator): class AvatarToolkit_OT_SearchAttachBone(Operator):
bl_idname = "avatar_toolkit.search_attach_bone" """Search operator for selecting bone to attach mesh to"""
bl_label = "" bl_idname: str = "avatar_toolkit.search_attach_bone"
bl_description = t('AttachBone.search_desc') bl_label: str = ""
bl_property = "search_attach_bone_enum" bl_description: str = t('AttachBone.search_desc')
bl_property: str = "search_attach_bone_enum"
search_attach_bone_enum: bpy.props.EnumProperty( search_attach_bone_enum: bpy.props.EnumProperty(
name=t('AttachBone.select'), name=t('AttachBone.select'),
@@ -90,26 +94,27 @@ class AvatarToolkit_OT_SearchAttachBone(Operator):
] if get_active_armature(context) else [] ] 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 context.scene.avatar_toolkit.attach_bone = self.search_attach_bone_enum
return {'FINISHED'} return {'FINISHED'}
def invoke(self, context, event): def invoke(self, context: Context, event: Event) -> Set[str]:
context.window_manager.invoke_search_popup(self) context.window_manager.invoke_search_popup(self)
return {'FINISHED'} return {'FINISHED'}
class AvatarToolKit_PT_CustomPanel(Panel): class AvatarToolKit_PT_CustomPanel(Panel):
"""Panel containing tools for custom avatar creation and merging""" """Panel containing tools for custom avatar creation and merging"""
bl_label = t('CustomPanel.label') bl_label: str = t('CustomPanel.label')
bl_idname = "VIEW3D_PT_avatar_toolkit_custom" bl_idname: str = "VIEW3D_PT_avatar_toolkit_custom"
bl_space_type = 'VIEW_3D' bl_space_type: str = 'VIEW_3D'
bl_region_type = 'UI' bl_region_type: str = 'UI'
bl_category = CATEGORY_NAME bl_category: str = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 4 bl_order: int = 4
bl_options = {'DEFAULT_CLOSED'} bl_options: Set[str] = {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None: def draw(self, context: Context) -> None:
"""Draw the custom avatar panel UI"""
layout: UILayout = self.layout layout: UILayout = self.layout
toolkit = context.scene.avatar_toolkit toolkit = context.scene.avatar_toolkit
@@ -129,6 +134,7 @@ class AvatarToolKit_PT_CustomPanel(Panel):
self.draw_mesh_tools(layout, context) self.draw_mesh_tools(layout, context)
def draw_armature_tools(self, layout: UILayout, context: Context) -> None: def draw_armature_tools(self, layout: UILayout, context: Context) -> None:
"""Draw the armature merging tools section"""
toolkit = context.scene.avatar_toolkit toolkit = context.scene.avatar_toolkit
# Merge Settings Box # Merge Settings Box
@@ -148,13 +154,13 @@ class AvatarToolKit_PT_CustomPanel(Panel):
col.separator(factor=0.5) col.separator(factor=0.5)
# Group related options together # Group related options together
transform_col = col.column(align=True) transform_col: UILayout = col.column(align=True)
transform_col.prop(toolkit, "merge_all_bones") transform_col.prop(toolkit, "merge_all_bones")
transform_col.prop(toolkit, "apply_transforms") transform_col.prop(toolkit, "apply_transforms")
col.separator(factor=0.5) 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, "join_meshes")
cleanup_col.prop(toolkit, "remove_zero_weights") cleanup_col.prop(toolkit, "remove_zero_weights")
cleanup_col.prop(toolkit, "cleanup_shape_keys") cleanup_col.prop(toolkit, "cleanup_shape_keys")
@@ -178,12 +184,13 @@ class AvatarToolKit_PT_CustomPanel(Panel):
# Merge button with emphasis # Merge button with emphasis
merge_box: UILayout = layout.box() merge_box: UILayout = layout.box()
col = merge_box.column(align=True) col: UILayout = merge_box.column(align=True)
row = col.row(align=True) row: UILayout = col.row(align=True)
row.scale_y = 1.5 row.scale_y = 1.5
row.operator("avatar_toolkit.merge_armatures", icon='ARMATURE_DATA') row.operator("avatar_toolkit.merge_armatures", icon='ARMATURE_DATA')
def draw_mesh_tools(self, layout: UILayout, context: Context) -> None: def draw_mesh_tools(self, layout: UILayout, context: Context) -> None:
"""Draw the mesh attachment tools section"""
toolkit = context.scene.avatar_toolkit toolkit = context.scene.avatar_toolkit
# Mesh Tools Box # Mesh Tools Box
@@ -220,8 +227,7 @@ class AvatarToolKit_PT_CustomPanel(Panel):
# Attach button with emphasis # Attach button with emphasis
attach_box: UILayout = layout.box() attach_box: UILayout = layout.box()
col = attach_box.column(align=True) col: UILayout = attach_box.column(align=True)
row = col.row(align=True) row: UILayout = col.row(align=True)
row.scale_y = 1.5 row.scale_y = 1.5
row.operator("avatar_toolkit.attach_mesh", icon='ARMATURE_DATA') row.operator("avatar_toolkit.attach_mesh", icon='ARMATURE_DATA')
+43 -40
View File
@@ -1,6 +1,6 @@
import bpy import bpy
from typing import Set 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 .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..core.translations import t from ..core.translations import t
from ..core.common import get_active_armature, get_all_meshes from ..core.common import get_active_armature, get_all_meshes
@@ -20,32 +20,32 @@ from ..functions.eye_tracking import (
class AvatarToolKit_PT_EyeTrackingPanel(Panel): class AvatarToolKit_PT_EyeTrackingPanel(Panel):
"""Panel containing eye tracking setup and testing tools""" """Panel containing eye tracking setup and testing tools"""
bl_label = t("EyeTracking.label") bl_label: str = t("EyeTracking.label")
bl_idname = "VIEW3D_PT_avatar_toolkit_eye_tracking" bl_idname: str = "VIEW3D_PT_avatar_toolkit_eye_tracking"
bl_space_type = 'VIEW_3D' bl_space_type: str = 'VIEW_3D'
bl_region_type = 'UI' bl_region_type: str = 'UI'
bl_category = CATEGORY_NAME bl_category: str = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 6 bl_order: int = 6
bl_options = {'DEFAULT_CLOSED'} bl_options: Set[str] = {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None: def draw(self, context: Context) -> None:
"""Draw the eye tracking panel interface""" """Draw the eye tracking panel interface"""
layout = self.layout layout: UILayout = self.layout
toolkit = context.scene.avatar_toolkit toolkit = context.scene.avatar_toolkit
# SDK Version Selection Box # SDK Version Selection Box
sdk_box = layout.box() sdk_box: UILayout = layout.box()
col = sdk_box.column(align=True) col: UILayout = sdk_box.column(align=True)
col.label(text=t("EyeTracking.sdk_version"), icon='PRESET') col.label(text=t("EyeTracking.sdk_version"), icon='PRESET')
col.separator(factor=0.5) col.separator(factor=0.5)
row = col.row(align=True) row: UILayout = col.row(align=True)
row.prop(toolkit, "eye_tracking_type", expand=True) row.prop(toolkit, "eye_tracking_type", expand=True)
if toolkit.eye_tracking_type == 'SDK2': if toolkit.eye_tracking_type == 'SDK2':
# Mode Selection Box # Mode Selection Box
mode_box = layout.box() mode_box: UILayout = layout.box()
col = mode_box.column(align=True) col: UILayout = mode_box.column(align=True)
col.label(text=t("EyeTracking.setup"), icon='TOOL_SETTINGS') col.label(text=t("EyeTracking.setup"), icon='TOOL_SETTINGS')
col.separator(factor=0.5) col.separator(factor=0.5)
col.prop(toolkit, "eye_mode", expand=True) col.prop(toolkit, "eye_mode", expand=True)
@@ -59,11 +59,12 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
self.draw_av3_setup(context, layout) self.draw_av3_setup(context, layout)
def draw_av3_setup(self, context: Context, layout: UILayout) -> None: def draw_av3_setup(self, context: Context, layout: UILayout) -> None:
"""Draw the AV3 eye tracking setup interface"""
toolkit = context.scene.avatar_toolkit toolkit = context.scene.avatar_toolkit
# Bone Setup Box # Bone Setup Box
bone_box = layout.box() bone_box: UILayout = layout.box()
col = bone_box.column(align=True) col: UILayout = bone_box.column(align=True)
col.label(text=t("EyeTracking.bone_setup"), icon='BONE_DATA') col.label(text=t("EyeTracking.bone_setup"), icon='BONE_DATA')
col.separator(factor=0.5) col.separator(factor=0.5)
@@ -76,16 +77,17 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
col.label(text=t("EyeTracking.no_armature"), icon='ERROR') col.label(text=t("EyeTracking.no_armature"), icon='ERROR')
# Create Button # Create Button
row = layout.row(align=True) row: UILayout = layout.row(align=True)
row.scale_y = 1.5 row.scale_y = 1.5
row.operator(CreateEyesAV3Button.bl_idname, icon='PLAY') row.operator(CreateEyesAV3Button.bl_idname, icon='PLAY')
def draw_creation_mode(self, context: Context, layout: UILayout) -> None: def draw_creation_mode(self, context: Context, layout: UILayout) -> None:
"""Draw the eye tracking creation mode interface"""
toolkit = context.scene.avatar_toolkit toolkit = context.scene.avatar_toolkit
# Bone Setup Box # Bone Setup Box
bone_box = layout.box() bone_box: UILayout = layout.box()
col = bone_box.column(align=True) col: UILayout = bone_box.column(align=True)
col.label(text=t("EyeTracking.bone_setup"), icon='BONE_DATA') col.label(text=t("EyeTracking.bone_setup"), icon='BONE_DATA')
col.separator(factor=0.5) col.separator(factor=0.5)
@@ -98,15 +100,15 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
col.label(text=t("EyeTracking.no_armature"), icon='ERROR') col.label(text=t("EyeTracking.no_armature"), icon='ERROR')
# Mesh Setup Box # Mesh Setup Box
mesh_box = layout.box() mesh_box: UILayout = layout.box()
col = mesh_box.column(align=True) col: UILayout = mesh_box.column(align=True)
col.label(text=t("EyeTracking.mesh_setup"), icon='MESH_DATA') col.label(text=t("EyeTracking.mesh_setup"), icon='MESH_DATA')
col.separator(factor=0.5) col.separator(factor=0.5)
col.prop_search(toolkit, "mesh_name_eye", bpy.data, "objects", text="") col.prop_search(toolkit, "mesh_name_eye", bpy.data, "objects", text="")
# Shape Key Setup Box # Shape Key Setup Box
shape_box = layout.box() shape_box: UILayout = layout.box()
col = shape_box.column(align=True) col: UILayout = shape_box.column(align=True)
col.label(text=t("EyeTracking.shapekey_setup"), icon='SHAPEKEY_DATA') col.label(text=t("EyeTracking.shapekey_setup"), icon='SHAPEKEY_DATA')
col.separator(factor=0.5) col.separator(factor=0.5)
@@ -120,8 +122,8 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
col.label(text=t("EyeTracking.no_shapekeys"), icon='ERROR') col.label(text=t("EyeTracking.no_shapekeys"), icon='ERROR')
# Options Box # Options Box
options_box = layout.box() options_box: UILayout = layout.box()
col = options_box.column(align=True) col: UILayout = options_box.column(align=True)
col.label(text=t("EyeTracking.options"), icon='SETTINGS') col.label(text=t("EyeTracking.options"), icon='SETTINGS')
col.separator(factor=0.5) col.separator(factor=0.5)
col.prop(toolkit, "disable_eye_blinking") col.prop(toolkit, "disable_eye_blinking")
@@ -130,26 +132,27 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
col.prop(toolkit, "eye_distance") col.prop(toolkit, "eye_distance")
# Create Button # Create Button
row = layout.row(align=True) row: UILayout = layout.row(align=True)
row.scale_y = 1.5 row.scale_y = 1.5
row.operator(CreateEyesSDK2Button.bl_idname, icon='PLAY') row.operator(CreateEyesSDK2Button.bl_idname, icon='PLAY')
def draw_testing_mode(self, context: Context, layout: UILayout) -> None: def draw_testing_mode(self, context: Context, layout: UILayout) -> None:
"""Draw the eye tracking testing mode interface"""
toolkit = context.scene.avatar_toolkit toolkit = context.scene.avatar_toolkit
if context.mode != 'POSE': if context.mode != 'POSE':
# Testing Start Box # Testing Start Box
test_box = layout.box() test_box: UILayout = layout.box()
col = test_box.column(align=True) col: UILayout = test_box.column(align=True)
col.label(text=t("EyeTracking.testing"), icon='PLAY') col.label(text=t("EyeTracking.testing"), icon='PLAY')
col.separator(factor=0.5) col.separator(factor=0.5)
row = col.row(align=True) row: UILayout = col.row(align=True)
row.scale_y = 1.5 row.scale_y = 1.5
row.operator(StartTestingButton.bl_idname, icon='PLAY') row.operator(StartTestingButton.bl_idname, icon='PLAY')
else: else:
# Eye Rotation Box # Eye Rotation Box
rotation_box = layout.box() rotation_box: UILayout = layout.box()
col = rotation_box.column(align=True) col: UILayout = rotation_box.column(align=True)
col.label(text=t("EyeTracking.rotation_controls"), icon='DRIVER_ROTATIONAL_DIFFERENCE') col.label(text=t("EyeTracking.rotation_controls"), icon='DRIVER_ROTATIONAL_DIFFERENCE')
col.separator(factor=0.5) col.separator(factor=0.5)
col.prop(toolkit, "eye_rotation_x", text=t("EyeTracking.rotation.x")) col.prop(toolkit, "eye_rotation_x", text=t("EyeTracking.rotation.x"))
@@ -157,31 +160,31 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
col.operator(ResetRotationButton.bl_idname, icon='LOOP_BACK') col.operator(ResetRotationButton.bl_idname, icon='LOOP_BACK')
# Eye Adjustment Box # Eye Adjustment Box
adjust_box = layout.box() adjust_box: UILayout = layout.box()
col = adjust_box.column(align=True) col: UILayout = adjust_box.column(align=True)
col.label(text=t("EyeTracking.adjustments"), icon='MODIFIER') col.label(text=t("EyeTracking.adjustments"), icon='MODIFIER')
col.separator(factor=0.5) col.separator(factor=0.5)
col.prop(toolkit, "eye_distance") col.prop(toolkit, "eye_distance")
col.operator(AdjustEyesButton.bl_idname, icon='CON_TRACKTO') col.operator(AdjustEyesButton.bl_idname, icon='CON_TRACKTO')
# Blinking Test Box # Blinking Test Box
blink_box = layout.box() blink_box: UILayout = layout.box()
col = blink_box.column(align=True) col: UILayout = blink_box.column(align=True)
col.label(text=t("EyeTracking.blink_testing"), icon='HIDE_OFF') col.label(text=t("EyeTracking.blink_testing"), icon='HIDE_OFF')
col.separator(factor=0.5) col.separator(factor=0.5)
row = col.row(align=True) row: UILayout = col.row(align=True)
row.prop(toolkit, "eye_blink_shape") row.prop(toolkit, "eye_blink_shape")
row.operator(TestBlinking.bl_idname, icon='RESTRICT_VIEW_OFF') 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.prop(toolkit, "eye_lowerlid_shape")
row.operator(TestLowerlid.bl_idname, icon='RESTRICT_VIEW_OFF') row.operator(TestLowerlid.bl_idname, icon='RESTRICT_VIEW_OFF')
col.operator(ResetBlinkTest.bl_idname, icon='LOOP_BACK') col.operator(ResetBlinkTest.bl_idname, icon='LOOP_BACK')
# Stop Testing Button # Stop Testing Button
row = layout.row(align=True) row: UILayout = layout.row(align=True)
row.scale_y = 1.5 row.scale_y = 1.5
row.operator(StopTestingButton.bl_idname, icon='PAUSE') row.operator(StopTestingButton.bl_idname, icon='PAUSE')
# Reset Button # Reset Button
row = layout.row(align=True) row: UILayout = layout.row(align=True)
row.operator(ResetEyeTrackingButton.bl_idname, icon='FILE_REFRESH') row.operator(ResetEyeTrackingButton.bl_idname, icon='FILE_REFRESH')
-13
View File
@@ -18,7 +18,6 @@ from ..core.common import (
get_armature_list, get_armature_list,
get_armature_stats get_armature_stats
) )
from ..core.importers.importer import import_types, imports
from ..functions.pose_mode import ( from ..functions.pose_mode import (
AvatarToolkit_OT_StartPoseMode, AvatarToolkit_OT_StartPoseMode,
AvatarToolkit_OT_StopPoseMode, AvatarToolkit_OT_StopPoseMode,
@@ -26,16 +25,6 @@ from ..functions.pose_mode import (
AvatarToolkit_OT_ApplyPoseAsRest 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'}
class AvatarToolKit_OT_ExportFBX(Operator): class AvatarToolKit_OT_ExportFBX(Operator):
"""Export selected objects as FBX""" """Export selected objects as FBX"""
bl_idname: str = "avatar_toolkit.export_fbx" bl_idname: str = "avatar_toolkit.export_fbx"
@@ -153,5 +142,3 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
button_row.scale_y = 1.5 button_row.scale_y = 1.5
button_row.operator("avatar_toolkit.import", text=t("QuickAccess.import"), icon='IMPORT') button_row.operator("avatar_toolkit.import", text=t("QuickAccess.import"), icon='IMPORT')
button_row.operator("avatar_toolkit.export", text=t("QuickAccess.export"), icon='EXPORT') button_row.operator("avatar_toolkit.export", text=t("QuickAccess.export"), icon='EXPORT')
+24 -9
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 ..core.translations import t
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..core.common import get_active_armature
class AvatarToolKit_PT_VisemesPanel(Panel): class AvatarToolKit_PT_VisemesPanel(Panel):
"""Panel containing viseme creation and preview tools""" """Panel containing viseme creation and preview tools"""
@@ -11,15 +13,28 @@ class AvatarToolKit_PT_VisemesPanel(Panel):
bl_category: str = CATEGORY_NAME bl_category: str = CATEGORY_NAME
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order: int = 5 bl_order: int = 5
bl_options = {'DEFAULT_CLOSED'} bl_options: set[str] = {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None: 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 layout: UILayout = self.layout
props = context.scene.avatar_toolkit props = context.scene.avatar_toolkit
# Check for valid mesh with shape keys # Mesh Selection Box
if not context.active_object or context.active_object.type != 'MESH' or not context.active_object.data.shape_keys: 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_search(props, "viseme_mesh", bpy.data, "objects", 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.shape_keys:
layout.label(text=t("Visemes.no_shapekeys")) layout.label(text=t("Visemes.no_shapekeys"))
return return
@@ -30,7 +45,7 @@ class AvatarToolKit_PT_VisemesPanel(Panel):
col.separator(factor=0.5) col.separator(factor=0.5)
# Shape key selection with valid data # 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_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_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")) 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
preview_box: UILayout = layout.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.label(text=t("Visemes.preview_label"), icon='HIDE_OFF')
col.separator(factor=0.5) col.separator(factor=0.5)
@@ -49,12 +64,12 @@ class AvatarToolKit_PT_VisemesPanel(Panel):
col.prop(props, "viseme_preview_selection", text="") col.prop(props, "viseme_preview_selection", text="")
col.separator() 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') col.operator("avatar_toolkit.preview_visemes", text=preview_text, icon='HIDE_OFF')
# Create Box # Create Box
create_box: UILayout = layout.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.label(text=t("Visemes.create_label"), icon='ADD')
col.separator(factor=0.5) col.separator(factor=0.5)
col.operator("avatar_toolkit.create_visemes", icon='ADD') col.operator("avatar_toolkit.create_visemes", icon='ADD')
Binary file not shown.