Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eeb41dec40 | |||
| 10fb112de7 | |||
| c532e2a6a0 | |||
| 4df5369cbb | |||
| 36550a42e5 | |||
| 2dc3a19283 | |||
| 30115eeaac | |||
| 285c331f79 | |||
| 57ded41f2f | |||
| 948b1bb352 | |||
| 9104bfae67 | |||
| 6d71669849 | |||
| f043c6099e | |||
| 3eb0029b5e | |||
| 686bc0bda1 | |||
| 1482632405 | |||
| 2a7cb16fea | |||
| 1187949280 | |||
| d7cc8096b9 | |||
| 08f37d3202 | |||
| cb0abf3053 | |||
| 2bb1826346 | |||
| 9ad760bfb8 | |||
| 4d1a468db1 | |||
| bf5de6665c | |||
| b776ef78cb | |||
| cbc973b0be | |||
| 8665292c7b | |||
| c5d07892c2 |
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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__:
|
||||||
|
|||||||
+222
-176
@@ -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({
|
||||||
@@ -252,10 +288,10 @@ def apply_pose_as_rest(context: Context, armature_obj: Object, meshes: List[Obje
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error applying pose as rest: {str(e)}")
|
logger.error(f"Error applying pose as rest: {str(e)}")
|
||||||
return False, str(e)
|
return False, str(e)
|
||||||
|
|
||||||
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:
|
||||||
with context.temp_override(active_object=obj):
|
obj.data.uv_layers.active = uv_layer
|
||||||
bpy.ops.uv.select_all(action='SELECT')
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
bpy.ops.uv.average_islands_scale()
|
bpy.ops.mesh.select_all(action='SELECT')
|
||||||
|
|
||||||
|
with context.temp_override(active_object=obj):
|
||||||
|
bpy.ops.uv.select_all(action='SELECT')
|
||||||
|
bpy.ops.uv.pack_islands(margin=0.001)
|
||||||
|
bpy.ops.uv.average_islands_scale()
|
||||||
|
|
||||||
logger.debug(f"UV Fix - Successfully processed {obj.name}")
|
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'
|
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
for bone in armature.data.edit_bones:
|
||||||
|
if bone.length < 0.001:
|
||||||
|
bone.length = 0.001
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
# names - The first object will be the first one in the list. So the first one has to be the one that exists in the most models
|
def calculate_bone_orientation(mesh: Object, vertices: List[Any]) -> Tuple[Vector, float]:
|
||||||
# no_basis - If this is true the Basis will not be available in the list
|
"""Calculate optimal bone orientation based on mesh geometry"""
|
||||||
def get_shapekeys(context, names, is_mouth, no_basis, return_list):
|
if not vertices:
|
||||||
choices = []
|
return Vector((0, 0, 0.1)), 0.0
|
||||||
choices_simple = []
|
|
||||||
meshes_list = get_meshes_objects(check=False)
|
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')
|
|
||||||
for bone in armature.data.edit_bones:
|
|
||||||
if bone.length < 0.001:
|
|
||||||
bone.length = 0.001
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
|
def get_meshes_objects(check: bool = True) -> List[Object]:
|
||||||
|
"""Get all mesh objects in the scene"""
|
||||||
|
meshes: List[Object] = [obj for obj in bpy.data.objects if obj.type == 'MESH']
|
||||||
|
if check and not meshes:
|
||||||
|
return []
|
||||||
|
return meshes
|
||||||
|
|
||||||
def calculate_bone_orientation(mesh, vertices):
|
def get_objects() -> bpy.types.BlendData:
|
||||||
"""Calculate optimal bone orientation based on mesh geometry."""
|
"""Get all objects in the current Blender scene"""
|
||||||
|
return bpy.data.objects
|
||||||
if not vertices:
|
|
||||||
return Vector((0, 0, 0.1)), 0.0
|
|
||||||
|
|
||||||
coords = [mesh.data.vertices[v.index].co for v in vertices]
|
|
||||||
min_co = Vector(map(min, zip(*coords)))
|
|
||||||
max_co = Vector(map(max, zip(*coords)))
|
|
||||||
dimensions = max_co - min_co
|
|
||||||
|
|
||||||
roll_angle = 0.0
|
|
||||||
|
|
||||||
return dimensions, roll_angle
|
|
||||||
|
|
||||||
def add_armature_modifier(mesh: Object, armature: Object):
|
def duplicate_bone(bone: EditBone) -> EditBone:
|
||||||
"""Add armature modifier to mesh."""
|
"""Create a duplicate of the given bone"""
|
||||||
for mod in mesh.modifiers:
|
new_bone: EditBone = bone.id_data.edit_bones.new(bone.name + "_copy")
|
||||||
if mod.type == 'ARMATURE':
|
new_bone.head = bone.head.copy()
|
||||||
mesh.modifiers.remove(mod)
|
new_bone.tail = bone.tail.copy()
|
||||||
|
new_bone.roll = bone.roll
|
||||||
modifier = mesh.modifiers.new('Armature', 'ARMATURE')
|
new_bone.use_connect = bone.use_connect
|
||||||
modifier.object = armature
|
new_bone.use_local_location = bone.use_local_location
|
||||||
|
new_bone.use_inherit_rotation = bone.use_inherit_rotation
|
||||||
|
new_bone.use_inherit_scale = bone.use_inherit_scale
|
||||||
|
new_bone.use_deform = bone.use_deform
|
||||||
|
return new_bone
|
||||||
|
|
||||||
#Binary tools
|
#Binary tools
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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"""
|
||||||
@@ -17,8 +20,17 @@ def configure_logging(enabled: bool = False) -> None:
|
|||||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
handler.setFormatter(formatter)
|
handler.setFormatter(formatter)
|
||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
|
|
||||||
|
def error_with_traceback(msg, *args, **kwargs):
|
||||||
|
if kwargs.get('exc_info', False) or isinstance(msg, Exception):
|
||||||
|
full_msg = f"{msg}\n{traceback.format_exc()}"
|
||||||
|
_original_error(full_msg, *args, **{**kwargs, 'exc_info': False})
|
||||||
|
else:
|
||||||
|
_original_error(msg, *args, **kwargs)
|
||||||
|
|
||||||
|
logger.error = error_with_traceback
|
||||||
|
|
||||||
def update_logging_state(self, context) -> None:
|
def update_logging_state(self: Any, context: Context) -> None:
|
||||||
"""Update logging state based on user preference"""
|
"""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
|
||||||
|
|||||||
@@ -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
|
||||||
+122
-20
@@ -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):
|
||||||
@@ -133,13 +135,7 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
description=t("Visemes.preview_mode_desc"),
|
description=t("Visemes.preview_mode_desc"),
|
||||||
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")
|
||||||
|
|
||||||
|
|||||||
+77
-6
@@ -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()
|
||||||
@@ -195,11 +224,37 @@ def update_now(latest: bool = False) -> None:
|
|||||||
if not version_list:
|
if not version_list:
|
||||||
print("No version list available. Please check for updates first.")
|
print("No version list available. Please check for updates first.")
|
||||||
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,
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
+406
-276
@@ -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の再起動が必要な場合があります"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 요소는 블렌더 재시작이 필요할 수 있음"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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')
|
||||||
+48
-42
@@ -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,44 +44,46 @@ 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'),
|
||||||
description=t('AttachMesh.select_desc'),
|
description=t('AttachMesh.select_desc'),
|
||||||
items=lambda self, context: [
|
items=lambda self, context: [
|
||||||
(obj.name, obj.name, "")
|
(obj.name, obj.name, "")
|
||||||
for obj in bpy.data.objects
|
for obj in bpy.data.objects
|
||||||
if obj.type == 'MESH'
|
if obj.type == 'MESH'
|
||||||
and not any(mod.type == 'ARMATURE' for mod in obj.modifiers)
|
and not any(mod.type == 'ARMATURE' for mod in obj.modifiers)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context: Context) -> Set[str]:
|
||||||
context.scene.avatar_toolkit.attach_mesh = self.search_attach_mesh_enum
|
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
@@ -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')
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+27
-12
@@ -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,26 +13,39 @@ 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
|
||||||
|
|
||||||
# Shape Key Selection Box
|
# Shape Key Selection Box
|
||||||
shape_box: UILayout = layout.box()
|
shape_box: UILayout = layout.box()
|
||||||
col: UILayout = shape_box.column(align=True)
|
col: UILayout = shape_box.column(align=True)
|
||||||
col.label(text=t("Visemes.shape_selection"), icon='SHAPEKEY_DATA')
|
col.label(text=t("Visemes.shape_selection"), icon='SHAPEKEY_DATA')
|
||||||
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.
Reference in New Issue
Block a user