Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 82a7e67d7e | |||
| 87c40c02d6 | |||
| 2ad5393f06 | |||
| 12083c28c5 | |||
| c67f30fb97 | |||
| 5a43a9d66d | |||
| 1ca45ad901 | |||
| bc034c5308 | |||
| eba18d72a6 | |||
| 416fbe40e7 | |||
| d296d548e8 | |||
| feb2f5ac85 | |||
| 0ff2dc1c38 | |||
| a407e99ebd | |||
| ff5efc9639 | |||
| 345ba44463 | |||
| b67d94e89d | |||
| 64a78dbbb2 | |||
| 1e8784d0e4 | |||
| c0943e0d20 | |||
| af5b79e314 | |||
| 334f299e0e | |||
| 9a5f13f858 | |||
| 77b7b429a5 | |||
| 9a0521dad5 | |||
| d7fee2c961 | |||
| 357aa1b6d9 | |||
| 02c73ccd2a | |||
| 940854cade | |||
| 7a1531fbd6 | |||
| 672517a771 | |||
| 4a039421f5 | |||
| 15101fa887 | |||
| 5411acca45 | |||
| 23b4462f7e | |||
| dd3d21d9d5 | |||
| 73c2404010 | |||
| 546fec6039 | |||
| c90bf4e36c | |||
| 5b9acb496f | |||
| 08a65f9fa7 | |||
| ba85666d9f | |||
| 52a51ea6a2 | |||
| 47e3ea2d29 | |||
| 12d06638fe | |||
| 2a2c3d3973 | |||
| c65bed3ff4 | |||
| abc1fe955b | |||
| dac25e0dc0 | |||
| b946041ec1 | |||
| 07adaa590b | |||
| 855bb84e76 | |||
| fbb07aec10 | |||
| dd36ccaece | |||
| 017633696a | |||
| 71cba9a40f | |||
| 53cc5c28ae | |||
| 21ddc20119 | |||
| 2524634ef4 | |||
| bf6a32febb | |||
| 4576b27b53 | |||
| 6412b6f619 | |||
| a20a306582 | |||
| 4b59147649 | |||
| 6038177383 | |||
| 251c006498 | |||
| af9c597dd2 | |||
| fc9b1e42a2 | |||
| af311d7d2e | |||
| 239e212cf4 | |||
| 1333b4d2d4 | |||
| 7fb1b9a8a4 | |||
| 2283a44579 | |||
| c7318fbd0c | |||
| f376b06caf | |||
| 4b69832ca1 | |||
| 071b8186c9 | |||
| 146dec71f8 | |||
| 44593813b2 |
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
*.pyc
|
*.pyc
|
||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
|
core/preferences.json
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ If you like what we do and want to help support the development of cats you can
|
|||||||
|
|
||||||
## 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.2.0#what-is-avatar-toolkits-version-support-policy)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -25,8 +25,8 @@ See everything Avatar Toolkit has ot offer [here](https://avatartoolkit.xyz/wiki
|
|||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
1) Blender Version
|
1) Blender Version
|
||||||
- Blender 4.3 or newer is required
|
- Blender 4.4 or newer is required
|
||||||
- Blender 4.3 is the current recommended version
|
- Blender 4.4 is the current recommended version
|
||||||
|
|
||||||
|
|
||||||
2) Python Requirements
|
2) Python Requirements
|
||||||
@@ -35,13 +35,13 @@ See everything Avatar Toolkit has ot offer [here](https://avatartoolkit.xyz/wiki
|
|||||||
|
|
||||||
3) Recommended Setup
|
3) Recommended Setup
|
||||||
- Download Blender directly from https://blender.org
|
- Download Blender directly from https://blender.org
|
||||||
- Use Blender 4.3 for the best experience
|
- Use Blender 4.4 for the best experience
|
||||||
|
|
||||||
#### Additional Plugins Requirements.
|
#### Additional Plugins Requirements.
|
||||||
Currently None.
|
Currently None.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
You can find out how to install Avatar Toolkit [here](https://avatartoolkit.xyz/wiki.html?version=0.1.0#how-to-install-avatar-toolkit)
|
You can find out how to install Avatar Toolkit [here](https://avatartoolkit.xyz/wiki.html?version=0.2.0#how-to-install-avatar-toolkit)
|
||||||
|
|
||||||
## Help
|
## Help
|
||||||
|
|
||||||
|
|||||||
+17
-17
@@ -13,30 +13,30 @@ def show_version_error_popup():
|
|||||||
bpy.context.window_manager.popup_menu(draw, title="Avatar Toolkit Version Error", icon='ERROR')
|
bpy.context.window_manager.popup_menu(draw, title="Avatar Toolkit Version Error", icon='ERROR')
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
# Check Blender version first
|
import bpy
|
||||||
version = bpy.app.version
|
version = bpy.app.version
|
||||||
if version[0] > 4 or (version[0] == 4 and version[1] > 3):
|
if version[0] > 4 or (version[0] == 4 and version[1] >= 5):
|
||||||
show_version_error_popup()
|
show_version_error_popup()
|
||||||
return
|
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
|
|
||||||
print("Starting registration")
|
print("Starting registration")
|
||||||
|
|
||||||
|
# Import modules using relative imports
|
||||||
|
from . import core
|
||||||
|
from .core import auto_load
|
||||||
|
from .core.logging_setup import configure_logging
|
||||||
|
|
||||||
|
# Initialize logging
|
||||||
|
configure_logging(False)
|
||||||
|
|
||||||
auto_load.init()
|
auto_load.init()
|
||||||
auto_load.register()
|
auto_load.register()
|
||||||
|
|
||||||
|
# Verify property registration
|
||||||
|
if not hasattr(bpy.types.Scene, "avatar_toolkit"):
|
||||||
|
from .core.properties import register as register_properties
|
||||||
|
register_properties()
|
||||||
|
|
||||||
print("Registration complete")
|
print("Registration complete")
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
|
|||||||
@@ -3,21 +3,25 @@
|
|||||||
schema_version = "1.0.0"
|
schema_version = "1.0.0"
|
||||||
|
|
||||||
id = "avatar_toolkit"
|
id = "avatar_toolkit"
|
||||||
version = "0.1.3"
|
version = "0.2.1"
|
||||||
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"
|
||||||
type = "add-on"
|
type = "add-on"
|
||||||
|
|
||||||
blender_version_min = "4.3.0"
|
blender_version_min = "4.4.0"
|
||||||
|
|
||||||
license = [
|
license = [
|
||||||
"SPDX:GPL-3.0-or-later",
|
"SPDX:GPL-3.0-or-later",
|
||||||
]
|
]
|
||||||
|
|
||||||
wheels = [
|
wheels = [
|
||||||
"./wheels/lz4-4.3.3-cp311-cp311-macosx_11_0_arm64.whl",
|
"./wheels/lz4-4.4.3-cp311-cp311-macosx_11_0_arm64.whl",
|
||||||
"./wheels/lz4-4.3.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
"./wheels/lz4-4.3.3-cp311-cp311-macosx_10_9_x86_64.whl",
|
||||||
"./wheels/lz4-4.3.3-cp311-cp311-win_amd64.whl"
|
"./wheels/lz4-4.4.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
||||||
|
"./wheels/lz4-4.4.3-cp311-cp311-win_amd64.whl"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[permissions]
|
||||||
|
network = "For the auto updater to work, you need to allow network access"
|
||||||
|
files = "Import/Export files, saving atlas images, saving preferences"
|
||||||
|
|||||||
@@ -6,9 +6,14 @@ from ..core.logging_setup import logger
|
|||||||
from bpy.types import AddonPreferences
|
from bpy.types import AddonPreferences
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
# Get the directory of the current file
|
# Get the user preferences directory instead of addon directory
|
||||||
PREFERENCES_DIR = os.path.dirname(os.path.abspath(__file__))
|
def get_preferences_path():
|
||||||
PREFERENCES_FILE = os.path.join(PREFERENCES_DIR, "preferences.json")
|
user_path = bpy.utils.resource_path('USER')
|
||||||
|
addon_prefs_dir = os.path.join(user_path, "config", "avatar_toolkit_prefs")
|
||||||
|
os.makedirs(addon_prefs_dir, exist_ok=True)
|
||||||
|
return os.path.join(addon_prefs_dir, "preferences.json")
|
||||||
|
|
||||||
|
PREFERENCES_FILE = get_preferences_path()
|
||||||
|
|
||||||
def get_current_version():
|
def get_current_version():
|
||||||
main_dir = os.path.dirname(os.path.dirname(__file__))
|
main_dir = os.path.dirname(os.path.dirname(__file__))
|
||||||
@@ -60,3 +65,4 @@ if not os.path.exists(PREFERENCES_FILE):
|
|||||||
save_preference("language", 0) # Set default language to 0 (auto)
|
save_preference("language", 0) # Set default language to 0 (auto)
|
||||||
save_preference("validation_mode", "STRICT") # Set default validation mode
|
save_preference("validation_mode", "STRICT") # Set default validation mode
|
||||||
save_preference("enable_logging", False) # Set default logging mode
|
save_preference("enable_logging", False) # Set default logging mode
|
||||||
|
save_preference("highlight_problem_bones", True) # Set default bone highlighting
|
||||||
|
|||||||
@@ -0,0 +1,566 @@
|
|||||||
|
import bpy
|
||||||
|
import math
|
||||||
|
from mathutils import Vector, Color
|
||||||
|
from typing import Tuple, List, Dict, Set, Optional, Union
|
||||||
|
from bpy.types import Object, Bone, Operator
|
||||||
|
from ..core.common import get_armature_list, get_active_armature
|
||||||
|
from ..core.translations import t
|
||||||
|
from ..core.dictionaries import (
|
||||||
|
standard_bones,
|
||||||
|
bone_hierarchy,
|
||||||
|
finger_hierarchy,
|
||||||
|
acceptable_bone_hierarchy,
|
||||||
|
acceptable_bone_names
|
||||||
|
)
|
||||||
|
from ..core.logging_setup import logger
|
||||||
|
|
||||||
|
def validate_armature(armature: Object, detailed_messages: bool = False) -> Union[Tuple[bool, List[str], bool], Tuple[bool, List[str], bool, List[str], List[str], List[str]]]:
|
||||||
|
"""
|
||||||
|
Validates armature and returns validation results
|
||||||
|
"""
|
||||||
|
logger.debug(f"Validating armature: {armature.name if armature else 'None'}")
|
||||||
|
validation_mode = bpy.context.scene.avatar_toolkit.validation_mode
|
||||||
|
messages: List[str] = []
|
||||||
|
hierarchy_messages: List[str] = []
|
||||||
|
non_standard_messages: List[str] = []
|
||||||
|
scale_messages: List[str] = []
|
||||||
|
|
||||||
|
if validation_mode == 'NONE':
|
||||||
|
logger.debug("Validation mode is NONE, skipping validation")
|
||||||
|
if detailed_messages:
|
||||||
|
return True, [], False, [], [], []
|
||||||
|
else:
|
||||||
|
return True, [], False
|
||||||
|
|
||||||
|
if not armature or armature.type != 'ARMATURE' or not armature.data.bones:
|
||||||
|
logger.warning("Basic armature check failed")
|
||||||
|
if detailed_messages:
|
||||||
|
return False, [t("Armature.validation.basic_check_failed")], False, [], [], []
|
||||||
|
else:
|
||||||
|
return False, [t("Armature.validation.basic_check_failed")], False
|
||||||
|
|
||||||
|
found_bones: Dict[str, Bone] = {bone.name: bone for bone in armature.data.bones}
|
||||||
|
logger.debug(f"Found {len(found_bones)} bones in armature")
|
||||||
|
is_acceptable = check_acceptable_standards(found_bones)
|
||||||
|
|
||||||
|
# List all bones in armature
|
||||||
|
bone_list = "\n".join([f"- {bone}" for bone in found_bones.keys()])
|
||||||
|
messages.append(t("Armature.validation.found_bones", bones=bone_list))
|
||||||
|
|
||||||
|
# Basic validation for both STRICT and LIMITED modes
|
||||||
|
# Check for missing required bones
|
||||||
|
essential_bones = {standard_bones[key] for key in ['hips', 'spine', 'chest', 'neck', 'head']}
|
||||||
|
missing_bones = [bone for bone in essential_bones if bone not in found_bones]
|
||||||
|
|
||||||
|
if missing_bones:
|
||||||
|
missing_list = "\n".join([f"- {bone}" for bone in missing_bones])
|
||||||
|
logger.warning(f"Missing essential bones: {', '.join(missing_bones)}")
|
||||||
|
hierarchy_messages.append(t("Armature.validation.missing_bones", bones=missing_list))
|
||||||
|
|
||||||
|
if validation_mode == 'STRICT':
|
||||||
|
logger.debug("Performing strict validation")
|
||||||
|
# Add scale issue detection in STRICT mode
|
||||||
|
scale_issues = detect_scale_issues(found_bones)
|
||||||
|
if scale_issues:
|
||||||
|
logger.warning(f"Found {len(scale_issues)} scale issues")
|
||||||
|
# CHANGE: Don't combine into a single string, keep as separate items
|
||||||
|
scale_messages.extend(scale_issues)
|
||||||
|
|
||||||
|
# Validate bone hierarchy
|
||||||
|
for parent, child in bone_hierarchy:
|
||||||
|
if parent in found_bones and child in found_bones:
|
||||||
|
if not validate_bone_hierarchy(found_bones, parent, child):
|
||||||
|
logger.warning(f"Invalid hierarchy: {parent} -> {child}")
|
||||||
|
hierarchy_messages.append(t("Armature.validation.invalid_hierarchy",
|
||||||
|
parent=parent, child=child))
|
||||||
|
|
||||||
|
# Validate symmetry
|
||||||
|
logger.debug("Validating bone symmetry")
|
||||||
|
symmetry_pairs = [('arm', 'L', 'R'), ('leg', 'L', 'R')]
|
||||||
|
for base, left, right in symmetry_pairs:
|
||||||
|
if not validate_symmetry(found_bones, base, left, right):
|
||||||
|
logger.warning(f"Asymmetric bones found: {base}")
|
||||||
|
hierarchy_messages.append(t("Armature.validation.asymmetric_bones", bone=base))
|
||||||
|
|
||||||
|
if (not validate_symmetry(found_bones, 'hand', 'L', 'R') and
|
||||||
|
not validate_symmetry(found_bones, 'wrist', 'L', 'R')):
|
||||||
|
logger.warning("Asymmetric hand/wrist bones found")
|
||||||
|
hierarchy_messages.append(t("Armature.validation.asymmetric_hand_wrist"))
|
||||||
|
|
||||||
|
# Validate finger hierarchies
|
||||||
|
logger.debug("Validating finger hierarchies")
|
||||||
|
for side in ['left', 'right']:
|
||||||
|
for finger_chain in finger_hierarchy[side]:
|
||||||
|
if all(bone in found_bones for bone in finger_chain):
|
||||||
|
if not validate_finger_chain(found_bones, finger_chain):
|
||||||
|
logger.warning(f"Invalid finger hierarchy: {finger_chain[0]}")
|
||||||
|
hierarchy_messages.append(t("Armature.validation.invalid_finger", finger=finger_chain[0]))
|
||||||
|
|
||||||
|
# Non-standard bones check
|
||||||
|
non_standard_bones = []
|
||||||
|
required_patterns = [
|
||||||
|
'Hips', 'Spine', 'Chest', 'Neck', 'Head',
|
||||||
|
'Upper', 'Lower', 'Hand', 'Foot', 'Toe',
|
||||||
|
'Thumb', 'Index', 'Middle', 'Ring', 'Pinky',
|
||||||
|
'Eye'
|
||||||
|
]
|
||||||
|
|
||||||
|
for bone_name in found_bones:
|
||||||
|
if any(pattern in bone_name for pattern in required_patterns):
|
||||||
|
is_standard = bone_name in standard_bones.values()
|
||||||
|
is_acceptable_bone = any(bone_name in names for names in acceptable_bone_names.values())
|
||||||
|
if not (is_standard or is_acceptable_bone):
|
||||||
|
non_standard_bones.append(bone_name)
|
||||||
|
|
||||||
|
if non_standard_bones:
|
||||||
|
non_standard_list = "\n".join([f"- {bone}" for bone in non_standard_bones])
|
||||||
|
non_standard_messages.append(t("Armature.validation.non_standard_bones", bones=non_standard_list))
|
||||||
|
|
||||||
|
non_standard_messages.append(t("Armature.validation.accessory_bones_note.line1"))
|
||||||
|
non_standard_messages.append(t("Armature.validation.accessory_bones_note.line2"))
|
||||||
|
non_standard_messages.append(t("Armature.validation.accessory_bones_note.line3"))
|
||||||
|
non_standard_messages.append(t("Armature.validation.accessory_bones_note.line4"))
|
||||||
|
non_standard_messages.append("") # Add a blank line for spacing
|
||||||
|
non_standard_messages.append(t("Armature.validation.standardize_note.line1"))
|
||||||
|
non_standard_messages.append(t("Armature.validation.standardize_note.line2"))
|
||||||
|
non_standard_messages.append(t("Armature.validation.standardize_note.line3"))
|
||||||
|
|
||||||
|
# Combine messages in correct order
|
||||||
|
messages.extend(non_standard_messages)
|
||||||
|
|
||||||
|
is_valid = len(non_standard_messages) == 0 and len(hierarchy_messages) == 0 and len(scale_messages) == 0
|
||||||
|
|
||||||
|
if not is_valid and is_acceptable:
|
||||||
|
if non_standard_bones:
|
||||||
|
logger.info("Armature has non-standard bones but is acceptable")
|
||||||
|
if detailed_messages:
|
||||||
|
return False, messages, False, hierarchy_messages, scale_messages, non_standard_messages
|
||||||
|
else:
|
||||||
|
return False, messages, False
|
||||||
|
|
||||||
|
logger.info("Armature meets acceptable standards")
|
||||||
|
messages = [
|
||||||
|
t("Armature.validation.acceptable_standard.success"),
|
||||||
|
t("Armature.validation.acceptable_standard.note"),
|
||||||
|
t("Armature.validation.acceptable_standard.option")
|
||||||
|
]
|
||||||
|
if detailed_messages:
|
||||||
|
return True, messages, True, [], [], []
|
||||||
|
else:
|
||||||
|
return True, messages, True
|
||||||
|
|
||||||
|
logger.info(f"Armature validation complete. Valid: {is_valid}")
|
||||||
|
if detailed_messages:
|
||||||
|
return is_valid, messages, False, hierarchy_messages, scale_messages, non_standard_messages
|
||||||
|
else:
|
||||||
|
return is_valid, messages, False
|
||||||
|
|
||||||
|
def validate_bone_hierarchy(bones: Dict[str, Bone], parent_name: str, child_name: str) -> bool:
|
||||||
|
"""Validate if there is a valid parent-child relationship between bones"""
|
||||||
|
if parent_name not in bones or child_name not in bones:
|
||||||
|
return False
|
||||||
|
return bones[child_name].parent == bones[parent_name]
|
||||||
|
|
||||||
|
def validate_symmetry(bones: Dict[str, Bone], base: str, left: str, right: str) -> bool:
|
||||||
|
"""Validate if matching left and right bones exist for a given base bone name"""
|
||||||
|
# Extract left and right bone names from both hierarchies
|
||||||
|
left_bone_names = set()
|
||||||
|
right_bone_names = set()
|
||||||
|
|
||||||
|
# Add standard bones
|
||||||
|
for key, value in standard_bones.items():
|
||||||
|
if base in key.lower():
|
||||||
|
if '_l' in key.lower():
|
||||||
|
left_bone_names.add(value)
|
||||||
|
elif '_r' in key.lower():
|
||||||
|
right_bone_names.add(value)
|
||||||
|
|
||||||
|
# Add acceptable bones
|
||||||
|
for key, names in acceptable_bone_names.items():
|
||||||
|
if base in key.lower():
|
||||||
|
if '_l' in key.lower():
|
||||||
|
left_bone_names.update(names)
|
||||||
|
elif '_r' in key.lower():
|
||||||
|
right_bone_names.update(names)
|
||||||
|
|
||||||
|
# Check if at least one pair exists and matches
|
||||||
|
left_exists = any(name in bones for name in left_bone_names)
|
||||||
|
right_exists = any(name in bones for name in right_bone_names)
|
||||||
|
|
||||||
|
return left_exists == right_exists
|
||||||
|
|
||||||
|
def validate_finger_chain(bones: Dict[str, Bone], chain: Tuple[str, ...]) -> bool:
|
||||||
|
"""Validate if a finger bone chain has correct hierarchy"""
|
||||||
|
for i in range(len(chain) - 1):
|
||||||
|
if not validate_bone_hierarchy(bones, chain[i], chain[i + 1]):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_acceptable_standards(bones: Dict[str, Bone]) -> bool:
|
||||||
|
"""Check if armature matches acceptable non-standard hierarchy"""
|
||||||
|
logger.debug("Checking for acceptable standards")
|
||||||
|
# Check if bones exist in acceptable list
|
||||||
|
for bone_category, acceptable_names in acceptable_bone_names.items():
|
||||||
|
found = False
|
||||||
|
for name in acceptable_names:
|
||||||
|
if name in bones:
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
if not found:
|
||||||
|
logger.debug(f"Missing acceptable bone for category: {bone_category}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Validate acceptable hierarchy
|
||||||
|
for parent, child in acceptable_bone_hierarchy:
|
||||||
|
if parent in bones and child in bones:
|
||||||
|
if not validate_bone_hierarchy(bones, parent, child):
|
||||||
|
logger.debug(f"Invalid acceptable hierarchy: {parent} -> {child}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.debug("Armature meets acceptable standards")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def validate_tpose(armature):
|
||||||
|
"""Validate if armature is in a proper T-pose"""
|
||||||
|
logger.debug(f"Validating T-pose for armature: {armature.name if armature else 'None'}")
|
||||||
|
if not armature or armature.type != 'ARMATURE':
|
||||||
|
logger.warning("No valid armature for T-pose validation")
|
||||||
|
return False, [t("Validation.tpose.no_armature")]
|
||||||
|
|
||||||
|
issues = []
|
||||||
|
|
||||||
|
if armature.mode == 'POSE':
|
||||||
|
bones_collection = armature.pose.bones
|
||||||
|
get_direction = lambda bone: bone.matrix.to_3x3().col[1].normalized()
|
||||||
|
else:
|
||||||
|
bones_collection = armature.data.bones
|
||||||
|
get_direction = lambda bone: bone.y_axis
|
||||||
|
|
||||||
|
# Get left and right upper arm bones using standard bone names
|
||||||
|
left_arm = None
|
||||||
|
right_arm = None
|
||||||
|
|
||||||
|
left_arm_candidates = [standard_bones['left_arm']] # UpperArm.L
|
||||||
|
if 'arm_l' in acceptable_bone_names:
|
||||||
|
left_arm_candidates.extend(acceptable_bone_names['arm_l'])
|
||||||
|
|
||||||
|
right_arm_candidates = [standard_bones['right_arm']] # UpperArm.R
|
||||||
|
if 'arm_r' in acceptable_bone_names:
|
||||||
|
right_arm_candidates.extend(acceptable_bone_names['arm_r'])
|
||||||
|
|
||||||
|
for name in left_arm_candidates:
|
||||||
|
if name in armature.data.bones:
|
||||||
|
left_arm = armature.data.bones[name]
|
||||||
|
logger.debug(f"Found left arm bone: {name}")
|
||||||
|
break
|
||||||
|
|
||||||
|
for name in right_arm_candidates:
|
||||||
|
if name in armature.data.bones:
|
||||||
|
right_arm = armature.data.bones[name]
|
||||||
|
logger.debug(f"Found right arm bone: {name}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Check arm bones are horizontal
|
||||||
|
if left_arm:
|
||||||
|
direction = left_arm.y_axis
|
||||||
|
if abs(direction.x) < 0.7: # Not pointing mostly along X axis
|
||||||
|
logger.warning("Left arm is not horizontal")
|
||||||
|
issues.append(t("Validation.tpose.left_arm_not_horizontal"))
|
||||||
|
|
||||||
|
if right_arm:
|
||||||
|
direction = right_arm.y_axis
|
||||||
|
if abs(direction.x) < 0.7: # Not pointing mostly along X axis
|
||||||
|
logger.warning("Right arm is not horizontal")
|
||||||
|
issues.append(t("Validation.tpose.right_arm_not_horizontal"))
|
||||||
|
|
||||||
|
spine = None
|
||||||
|
spine_candidates = [standard_bones['spine']] # Spine
|
||||||
|
if 'spine' in acceptable_bone_names:
|
||||||
|
spine_candidates.extend(acceptable_bone_names['spine'])
|
||||||
|
|
||||||
|
for name in spine_candidates:
|
||||||
|
if name in armature.data.bones:
|
||||||
|
spine = armature.data.bones[name]
|
||||||
|
logger.debug(f"Found spine bone: {name}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if spine:
|
||||||
|
direction = spine.y_axis
|
||||||
|
if abs(direction.z) < 0.7: # Not pointing mostly along Z axis
|
||||||
|
logger.warning("Spine is not vertical")
|
||||||
|
issues.append(t("Validation.tpose.spine_not_vertical"))
|
||||||
|
|
||||||
|
if issues:
|
||||||
|
logger.warning(f"T-pose validation failed with {len(issues)} issues")
|
||||||
|
return False, issues
|
||||||
|
|
||||||
|
logger.info("T-pose validation successful")
|
||||||
|
return True, []
|
||||||
|
|
||||||
|
def detect_scale_issues(bones):
|
||||||
|
"""Detect bones with abnormal scale (too small or too large)"""
|
||||||
|
logger.debug("Detecting scale issues")
|
||||||
|
scale_issues = []
|
||||||
|
|
||||||
|
# Calculate median bone length for reference (more robust than average)
|
||||||
|
lengths = [bone.length for bone in bones.values()]
|
||||||
|
lengths.sort()
|
||||||
|
|
||||||
|
if not lengths:
|
||||||
|
logger.debug("No bones with length found")
|
||||||
|
return []
|
||||||
|
|
||||||
|
median_length = lengths[len(lengths) // 2]
|
||||||
|
|
||||||
|
# Filter out zero-length bones for standard deviation calculation
|
||||||
|
non_zero_lengths = [l for l in lengths if l > 0.0001]
|
||||||
|
|
||||||
|
if not non_zero_lengths:
|
||||||
|
logger.debug("No non-zero length bones found")
|
||||||
|
return []
|
||||||
|
|
||||||
|
mean = sum(non_zero_lengths) / len(non_zero_lengths)
|
||||||
|
variance = sum((l - mean) ** 2 for l in non_zero_lengths) / len(non_zero_lengths)
|
||||||
|
std_dev = math.sqrt(variance)
|
||||||
|
|
||||||
|
small_threshold = max(median_length * 0.05, mean - 3 * std_dev)
|
||||||
|
large_threshold = min(median_length * 15, mean + 5 * std_dev)
|
||||||
|
|
||||||
|
logger.debug(f"Scale thresholds - small: {small_threshold}, large: {large_threshold}")
|
||||||
|
|
||||||
|
# Get finger bones from standard and acceptable bone dictionaries
|
||||||
|
finger_bone_names = set()
|
||||||
|
|
||||||
|
for key in standard_bones:
|
||||||
|
if any(finger in key.lower() for finger in ['thumb', 'index', 'middle', 'ring', 'pinky', 'finger']):
|
||||||
|
finger_bone_names.add(standard_bones[key])
|
||||||
|
|
||||||
|
for key, names in acceptable_bone_names.items():
|
||||||
|
if any(finger in key.lower() for finger in ['thumb', 'index', 'middle', 'ring', 'pinky', 'finger']):
|
||||||
|
finger_bone_names.update(names)
|
||||||
|
|
||||||
|
for name, bone in bones.items():
|
||||||
|
is_finger = (name in finger_bone_names or
|
||||||
|
any(finger in name.lower() for finger in ['thumb', 'index', 'middle', 'ring', 'pinky', 'finger']))
|
||||||
|
|
||||||
|
if bone.length < small_threshold and not is_finger:
|
||||||
|
logger.debug(f"Bone {name} is too small: {bone.length}")
|
||||||
|
scale_issues.append(f"- {name}: {t('Validation.scale_issue.too_small')} ({bone.length:.4f})")
|
||||||
|
elif bone.length > large_threshold:
|
||||||
|
logger.debug(f"Bone {name} is too large: {bone.length}")
|
||||||
|
scale_issues.append(f"- {name}: {t('Validation.scale_issue.too_large')} ({bone.length:.4f})")
|
||||||
|
|
||||||
|
logger.debug(f"Found {len(scale_issues)} scale issues")
|
||||||
|
return scale_issues
|
||||||
|
|
||||||
|
def clear_bone_highlighting(armature: Object) -> None:
|
||||||
|
"""Clear bone highlighting by removing bone collections and resetting colors"""
|
||||||
|
logger.debug(f"Clearing bone highlighting for armature: {armature.name if armature else 'None'}")
|
||||||
|
if not armature or armature.type != 'ARMATURE':
|
||||||
|
logger.warning("No valid armature for clearing bone highlighting")
|
||||||
|
return
|
||||||
|
|
||||||
|
current_mode = bpy.context.mode
|
||||||
|
|
||||||
|
collection_name = "Problem Bones"
|
||||||
|
if collection_name in armature.data.collections:
|
||||||
|
problem_collection = armature.data.collections[collection_name]
|
||||||
|
armature.data.collections.remove(problem_collection)
|
||||||
|
logger.debug("Removed problem bones collection")
|
||||||
|
|
||||||
|
for bone in armature.data.bones:
|
||||||
|
bone.color.palette = 'DEFAULT'
|
||||||
|
|
||||||
|
if len(armature.data.collections) == 0:
|
||||||
|
armature.data.show_bone_colors = False
|
||||||
|
logger.debug("Disabled bone colors display")
|
||||||
|
|
||||||
|
logger.info("Bone highlighting cleared")
|
||||||
|
return
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_HighlightProblemBones(Operator):
|
||||||
|
"""Highlight bones that fail validation in the 3D viewport"""
|
||||||
|
bl_idname = "avatar_toolkit.highlight_problem_bones"
|
||||||
|
bl_label = t("Validation.highlight_problem_bones")
|
||||||
|
bl_description = t("Validation.highlight_problem_bones_desc")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return get_active_armature(context) is not None
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
if not armature:
|
||||||
|
logger.warning("No active armature found for highlighting problem bones")
|
||||||
|
self.report({'ERROR'}, t("Validation.no_armature"))
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
logger.info(f"Highlighting problem bones for armature: {armature.name}")
|
||||||
|
|
||||||
|
current_mode = context.mode
|
||||||
|
|
||||||
|
if current_mode != 'OBJECT':
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
|
context.view_layer.objects.active = armature
|
||||||
|
|
||||||
|
# First remove all bone collections
|
||||||
|
collection_name = "Problem Bones"
|
||||||
|
if collection_name in armature.data.collections:
|
||||||
|
problem_collection = armature.data.collections[collection_name]
|
||||||
|
armature.data.collections.remove(problem_collection)
|
||||||
|
logger.debug("Removed existing problem bones collection")
|
||||||
|
|
||||||
|
is_valid, messages, _ = validate_armature(armature)
|
||||||
|
|
||||||
|
if is_valid:
|
||||||
|
logger.info("No validation issues found")
|
||||||
|
self.report({'INFO'}, t("Validation.no_issues"))
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
problem_collection = armature.data.collections.new(name="Problem Bones")
|
||||||
|
logger.debug("Created new problem bones collection")
|
||||||
|
armature.data.show_bone_colors = True
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
|
||||||
|
# Extract bone names from validation messages
|
||||||
|
problem_bones = self._extract_problem_bones(messages)
|
||||||
|
|
||||||
|
# Assign bones to collection and set colors
|
||||||
|
highlighted_count = 0
|
||||||
|
for category, bone_names in problem_bones.items():
|
||||||
|
for bone_name in bone_names:
|
||||||
|
if bone_name in armature.data.edit_bones:
|
||||||
|
bone = armature.data.edit_bones[bone_name]
|
||||||
|
problem_collection.assign(bone)
|
||||||
|
|
||||||
|
if 'hierarchy' in category.lower():
|
||||||
|
bone.color.palette = 'THEME09' # Orange
|
||||||
|
elif 'scale' in category.lower():
|
||||||
|
bone.color.palette = 'THEME03' # Yellow
|
||||||
|
else:
|
||||||
|
bone.color.palette = 'THEME01' # Red
|
||||||
|
|
||||||
|
highlighted_count += 1
|
||||||
|
|
||||||
|
logger.info(f"Highlighted {highlighted_count} problem bones")
|
||||||
|
self.report({'INFO'}, t("Validation.highlighting_complete"))
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
def _extract_problem_bones(self, messages):
|
||||||
|
problem_bones = {
|
||||||
|
"Hierarchy Issues": [],
|
||||||
|
"Scale Issues": [],
|
||||||
|
"Missing Bones": []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract bone names from validation messages
|
||||||
|
for message in messages:
|
||||||
|
if isinstance(message, str):
|
||||||
|
# Parse message to extract bone names
|
||||||
|
for line in message.split('\n'):
|
||||||
|
if '- ' in line:
|
||||||
|
bone_name = line.split('- ')[1].strip()
|
||||||
|
if ':' in bone_name: # Handle "bone_name: message" format
|
||||||
|
bone_name = bone_name.split(':')[0].strip()
|
||||||
|
|
||||||
|
if 'hierarchy' in message.lower():
|
||||||
|
problem_bones["Hierarchy Issues"].append(bone_name)
|
||||||
|
elif 'scale' in message.lower():
|
||||||
|
problem_bones["Scale Issues"].append(bone_name)
|
||||||
|
else:
|
||||||
|
problem_bones["Missing Bones"].append(bone_name)
|
||||||
|
|
||||||
|
logger.debug(f"Extracted problem bones: {problem_bones}")
|
||||||
|
return problem_bones
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_ValidateTPose(Operator):
|
||||||
|
"""Validate if armature is in a proper T-pose"""
|
||||||
|
bl_idname = "avatar_toolkit.validate_tpose"
|
||||||
|
bl_label = t("Validation.tpose.label")
|
||||||
|
bl_description = t("Validation.tpose.desc")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return get_active_armature(context) is not None
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
if not armature:
|
||||||
|
logger.warning("No active armature found for T-pose validation")
|
||||||
|
self.report({'ERROR'}, t("Validation.no_armature"))
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
logger.info(f"Validating T-pose for armature: {armature.name}")
|
||||||
|
is_valid, messages = validate_tpose(armature)
|
||||||
|
props = context.scene.avatar_toolkit
|
||||||
|
props.tpose_validation_result = is_valid
|
||||||
|
props.tpose_validation_messages.clear()
|
||||||
|
|
||||||
|
for msg in messages:
|
||||||
|
item = props.tpose_validation_messages.add()
|
||||||
|
item.name = msg
|
||||||
|
|
||||||
|
props.show_tpose_validation = True
|
||||||
|
|
||||||
|
if is_valid:
|
||||||
|
logger.info("T-pose validation successful")
|
||||||
|
self.report({'INFO'}, t("Validation.tpose.valid"))
|
||||||
|
else:
|
||||||
|
for msg in messages:
|
||||||
|
self.report({'WARNING'}, msg)
|
||||||
|
logger.warning("T-pose validation failed")
|
||||||
|
self.report({'WARNING'}, t("Validation.tpose.warning"))
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_ClearBoneHighlighting(Operator):
|
||||||
|
"""Clear bone highlighting and reset bone colors"""
|
||||||
|
bl_idname = "avatar_toolkit.clear_bone_highlighting"
|
||||||
|
bl_label = t("Validation.clear_bone_highlighting")
|
||||||
|
bl_description = t("Validation.clear_bone_highlighting_desc")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return get_active_armature(context) is not None
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
if not armature:
|
||||||
|
logger.warning("No active armature found for clearing bone highlighting")
|
||||||
|
self.report({'ERROR'}, t("Validation.no_armature"))
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
logger.info(f"Clearing bone highlighting for armature: {armature.name}")
|
||||||
|
current_mode = context.mode
|
||||||
|
|
||||||
|
# Switch to object mode as collection editing is not possible in edit mode
|
||||||
|
if current_mode != 'OBJECT':
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
|
context.view_layer.objects.active = armature
|
||||||
|
|
||||||
|
collection_name = "Problem Bones"
|
||||||
|
if collection_name in armature.data.collections:
|
||||||
|
# Remove the collection
|
||||||
|
problem_collection = armature.data.collections[collection_name]
|
||||||
|
armature.data.collections.remove(problem_collection)
|
||||||
|
logger.debug("Removed problem bones collection")
|
||||||
|
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
|
||||||
|
# Reset all bone colors
|
||||||
|
for bone in armature.data.edit_bones:
|
||||||
|
bone.color.palette = 'DEFAULT'
|
||||||
|
|
||||||
|
# Turn off bone colors display if no other collections are using it
|
||||||
|
if len(armature.data.collections) == 0:
|
||||||
|
armature.data.show_bone_colors = False
|
||||||
|
logger.debug("Disabled bone colors display")
|
||||||
|
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
|
logger.info("Bone highlighting cleared")
|
||||||
|
self.report({'INFO'}, t("Validation.highlighting_cleared"))
|
||||||
|
return {'FINISHED'}
|
||||||
+59
-62
@@ -1,6 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
import bpy
|
import bpy
|
||||||
import sys
|
|
||||||
import typing
|
import typing
|
||||||
import inspect
|
import inspect
|
||||||
import pkgutil
|
import pkgutil
|
||||||
@@ -24,7 +23,6 @@ def init() -> None:
|
|||||||
global modules
|
global modules
|
||||||
global ordered_classes
|
global ordered_classes
|
||||||
|
|
||||||
# Configure logging first
|
|
||||||
from .logging_setup import configure_logging
|
from .logging_setup import configure_logging
|
||||||
configure_logging(False)
|
configure_logging(False)
|
||||||
|
|
||||||
@@ -32,14 +30,24 @@ def init() -> None:
|
|||||||
configure_logging(get_preference("enable_logging", False))
|
configure_logging(get_preference("enable_logging", False))
|
||||||
|
|
||||||
print("Auto-load init starting")
|
print("Auto-load init starting")
|
||||||
modules = get_all_submodules(Path(__file__).parent.parent)
|
|
||||||
|
package_name = __package__.rsplit('.', 1)[0]
|
||||||
|
directory = Path(__file__).parent.parent
|
||||||
|
modules = get_all_submodules(directory, package_name)
|
||||||
ordered_classes = get_ordered_classes_to_register(modules)
|
ordered_classes = get_ordered_classes_to_register(modules)
|
||||||
print(f"Found modules: {modules}")
|
print(f"Found modules: {modules}")
|
||||||
print(f"Found classes: {ordered_classes}")
|
print(f"Found classes: {ordered_classes}")
|
||||||
|
|
||||||
def register() -> None:
|
def register() -> None:
|
||||||
"""Register all discovered classes and modules"""
|
"""Register all discovered classes and modules"""
|
||||||
|
global modules, ordered_classes
|
||||||
|
|
||||||
print("Registering classes")
|
print("Registering classes")
|
||||||
|
|
||||||
|
if not ordered_classes:
|
||||||
|
print("Warning: No classes to register")
|
||||||
|
ordered_classes = []
|
||||||
|
|
||||||
for cls in ordered_classes:
|
for cls in ordered_classes:
|
||||||
print(f"Registering: {cls}")
|
print(f"Registering: {cls}")
|
||||||
try:
|
try:
|
||||||
@@ -47,6 +55,10 @@ def register() -> None:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if not modules:
|
||||||
|
print("Warning: No modules to register")
|
||||||
|
modules = []
|
||||||
|
|
||||||
for module in modules:
|
for module in modules:
|
||||||
if module.__name__ == __name__:
|
if module.__name__ == __name__:
|
||||||
continue
|
continue
|
||||||
@@ -67,44 +79,29 @@ def unregister() -> None:
|
|||||||
if hasattr(module, "unregister"):
|
if hasattr(module, "unregister"):
|
||||||
module.unregister()
|
module.unregister()
|
||||||
|
|
||||||
def get_manifest_id() -> str:
|
def get_all_submodules(directory: Path, package_name: str) -> List[Any]:
|
||||||
"""Get the addon ID from the manifest file"""
|
|
||||||
manifest_path = Path(__file__).parent.parent / "blender_manifest.toml"
|
|
||||||
with open(manifest_path, "rb") as f:
|
|
||||||
manifest = tomllib.load(f)
|
|
||||||
return manifest["id"]
|
|
||||||
|
|
||||||
def get_all_submodules(directory: Path) -> List[Any]:
|
|
||||||
"""Discover and import all submodules in the given directory"""
|
"""Discover and import all submodules in the given directory"""
|
||||||
modules = []
|
return list(iter_submodules(directory, package_name))
|
||||||
addon_id = get_manifest_id()
|
|
||||||
for root, dirs, files in os.walk(directory):
|
|
||||||
if "__pycache__" in root:
|
|
||||||
continue
|
|
||||||
path = Path(root)
|
|
||||||
if path == directory:
|
|
||||||
package_name = f"bl_ext.user_default.{addon_id}"
|
|
||||||
else:
|
|
||||||
relative_path = path.relative_to(directory).as_posix().replace('/', '.')
|
|
||||||
package_name = f"bl_ext.user_default.{addon_id}.{relative_path}"
|
|
||||||
for name in sorted(iter_module_names(path)):
|
|
||||||
modules.append(importlib.import_module(f".{name}", package_name))
|
|
||||||
return modules
|
|
||||||
|
|
||||||
def iter_submodules(path: Path, package_name: str) -> Generator[Any, None, None]:
|
def iter_submodules(directory: Path, package_name: str) -> Generator[Any, None, None]:
|
||||||
"""Iterate through submodules in a package"""
|
"""Iterate through submodules in a package"""
|
||||||
for name in sorted(iter_module_names(path)):
|
for name in sorted(iter_submodule_names(directory)):
|
||||||
|
try:
|
||||||
yield importlib.import_module("." + name, package_name)
|
yield importlib.import_module("." + name, package_name)
|
||||||
|
print(f"Successfully imported {name} from {package_name}")
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"Error importing {name} from {package_name}: {e}")
|
||||||
|
|
||||||
def iter_module_names(path: Path) -> Generator[str, None, None]:
|
def iter_submodule_names(path: Path, root: str = "") -> Generator[str, None, None]:
|
||||||
"""Iterate through module names in a directory"""
|
"""Iterate through module names in a directory"""
|
||||||
print(f"Scanning path: {path}")
|
print(f"Scanning path: {path}")
|
||||||
modules_list = list(pkgutil.iter_modules([str(path)]))
|
for _, module_name, is_package in pkgutil.iter_modules([str(path)]):
|
||||||
print(f"Found these modules: {modules_list}")
|
if is_package:
|
||||||
for _, module_name, is_pkg in modules_list:
|
sub_path = path / module_name
|
||||||
if not is_pkg:
|
sub_root = root + module_name + "."
|
||||||
print(f"Found module: {module_name}")
|
yield from iter_submodule_names(sub_path, sub_root)
|
||||||
yield module_name
|
else:
|
||||||
|
yield root + module_name
|
||||||
|
|
||||||
def get_ordered_classes_to_register(modules: List[Any]) -> List[Type]:
|
def get_ordered_classes_to_register(modules: List[Any]) -> List[Type]:
|
||||||
"""Get a topologically sorted list of classes to register"""
|
"""Get a topologically sorted list of classes to register"""
|
||||||
@@ -112,28 +109,37 @@ def get_ordered_classes_to_register(modules: List[Any]) -> List[Type]:
|
|||||||
|
|
||||||
def get_register_deps_dict(modules: List[Any]) -> Dict[Type, Set[Type]]:
|
def get_register_deps_dict(modules: List[Any]) -> Dict[Type, Set[Type]]:
|
||||||
"""Get dependencies dictionary for class registration"""
|
"""Get dependencies dictionary for class registration"""
|
||||||
|
my_classes = set(iter_classes_to_register(modules))
|
||||||
|
my_classes_by_idname = {cls.bl_idname: cls for cls in my_classes if hasattr(cls, "bl_idname")}
|
||||||
|
|
||||||
deps_dict = {}
|
deps_dict = {}
|
||||||
classes_to_register = set(iter_classes_to_register(modules))
|
for cls in my_classes:
|
||||||
for cls in classes_to_register:
|
deps_dict[cls] = set()
|
||||||
deps_dict[cls] = set(iter_own_register_deps(cls, classes_to_register))
|
deps_dict[cls].update(iter_deps_from_annotations(cls, my_classes))
|
||||||
|
deps_dict[cls].update(iter_deps_from_parent_id(cls, my_classes_by_idname))
|
||||||
|
|
||||||
return deps_dict
|
return deps_dict
|
||||||
|
|
||||||
def iter_own_register_deps(cls: Type, classes_to_register: Set[Type]) -> Generator[Type, None, None]:
|
def iter_deps_from_annotations(cls: Type, my_classes: Set[Type]) -> Generator[Type, None, None]:
|
||||||
"""Iterate through a class's own registration dependencies"""
|
"""Iterate through dependencies from class annotations"""
|
||||||
yield from (dep for dep in iter_register_deps(cls) if dep in classes_to_register)
|
|
||||||
|
|
||||||
def iter_register_deps(cls: Type) -> Generator[Type, None, None]:
|
|
||||||
"""Iterate through all registration dependencies of a class"""
|
|
||||||
for value in typing.get_type_hints(cls, {}, {}).values():
|
for value in typing.get_type_hints(cls, {}, {}).values():
|
||||||
dependency = get_dependency_from_annotation(value)
|
dependency = get_dependency_from_annotation(value)
|
||||||
if dependency is not None:
|
if dependency is not None and dependency in my_classes:
|
||||||
yield dependency
|
yield dependency
|
||||||
|
|
||||||
|
def iter_deps_from_parent_id(cls: Type, my_classes_by_idname: Dict[str, Type]) -> Generator[Type, None, None]:
|
||||||
|
"""Iterate through dependencies from panel parent IDs"""
|
||||||
|
if bpy.types.Panel in cls.__bases__:
|
||||||
|
parent_idname = getattr(cls, "bl_parent_id", None)
|
||||||
|
if parent_idname is not None:
|
||||||
|
parent_cls = my_classes_by_idname.get(parent_idname)
|
||||||
|
if parent_cls is not None:
|
||||||
|
yield parent_cls
|
||||||
|
|
||||||
def get_dependency_from_annotation(value: Any) -> Optional[Type]:
|
def get_dependency_from_annotation(value: Any) -> Optional[Type]:
|
||||||
"""Get dependency type from a type annotation"""
|
"""Get dependency type from a type annotation"""
|
||||||
if isinstance(value, tuple) and len(value) == 2:
|
if isinstance(value, bpy.props._PropertyDeferred):
|
||||||
if value[0] in (bpy.props.PointerProperty, bpy.props.CollectionProperty):
|
return value.keywords.get("type")
|
||||||
return value[1]["type"]
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def iter_classes_to_register(modules: List[Any]) -> Generator[Type, None, None]:
|
def iter_classes_to_register(modules: List[Any]) -> Generator[Type, None, None]:
|
||||||
@@ -164,7 +170,8 @@ def get_register_base_types() -> Set[Type]:
|
|||||||
"Panel", "Operator", "PropertyGroup",
|
"Panel", "Operator", "PropertyGroup",
|
||||||
"AddonPreferences", "Header", "Menu",
|
"AddonPreferences", "Header", "Menu",
|
||||||
"Node", "NodeSocket", "NodeTree",
|
"Node", "NodeSocket", "NodeTree",
|
||||||
"UIList", "RenderEngine"
|
"UIList", "RenderEngine",
|
||||||
|
"Gizmo", "GizmoGroup",
|
||||||
])
|
])
|
||||||
|
|
||||||
def toposort(deps_dict: Dict[Type, Set[Type]]) -> List[Type]:
|
def toposort(deps_dict: Dict[Type, Set[Type]]) -> List[Type]:
|
||||||
@@ -172,25 +179,15 @@ def toposort(deps_dict: Dict[Type, Set[Type]]) -> List[Type]:
|
|||||||
sorted_list = []
|
sorted_list = []
|
||||||
sorted_values = set()
|
sorted_values = set()
|
||||||
|
|
||||||
panels_to_sort = [(value, deps) for value, deps in deps_dict.items()
|
while len(deps_dict) > 0:
|
||||||
if hasattr(value, 'bl_parent_id')]
|
|
||||||
|
|
||||||
base_panels = [(value, deps) for value, deps in deps_dict.items()
|
|
||||||
if not hasattr(value, 'bl_parent_id')]
|
|
||||||
|
|
||||||
for value, deps in base_panels:
|
|
||||||
if len(deps) == 0:
|
|
||||||
sorted_list.append(value)
|
|
||||||
sorted_values.add(value)
|
|
||||||
|
|
||||||
while len(deps_dict) > len(sorted_values):
|
|
||||||
unsorted = []
|
unsorted = []
|
||||||
for value, deps in deps_dict.items():
|
for value, deps in deps_dict.items():
|
||||||
if value not in sorted_values:
|
if len(deps) == 0:
|
||||||
if len(deps - sorted_values) == 0:
|
|
||||||
sorted_list.append(value)
|
sorted_list.append(value)
|
||||||
sorted_values.add(value)
|
sorted_values.add(value)
|
||||||
else:
|
else:
|
||||||
unsorted.append(value)
|
unsorted.append(value)
|
||||||
|
|
||||||
|
deps_dict = {value: deps_dict[value] - sorted_values for value in unsorted}
|
||||||
|
|
||||||
return sorted_list
|
return sorted_list
|
||||||
|
|||||||
+47
-108
@@ -18,6 +18,7 @@ from bpy.utils import register_class
|
|||||||
from ..core.logging_setup import logger
|
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
|
||||||
|
from .dictionaries import reverse_bone_lookup, bone_names
|
||||||
|
|
||||||
class SceneMatClass(PropertyGroup):
|
class SceneMatClass(PropertyGroup):
|
||||||
mat: PointerProperty(type=Material)
|
mat: PointerProperty(type=Material)
|
||||||
@@ -28,7 +29,6 @@ class MaterialListBool:
|
|||||||
#For the love that is holy do not ever touch these. If this was java I would make these private
|
#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
|
#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.
|
#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.
|
#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]] = {}
|
old_list: dict[str,list[Material]] = {}
|
||||||
bool_material_list_expand: dict[str,bool] = {}
|
bool_material_list_expand: dict[str,bool] = {}
|
||||||
@@ -46,7 +46,6 @@ class MaterialListBool:
|
|||||||
if mat_slot.material:
|
if mat_slot.material:
|
||||||
if mat_slot.material not in newlist:
|
if mat_slot.material not in newlist:
|
||||||
newlist.append(mat_slot.material)
|
newlist.append(mat_slot.material)
|
||||||
|
|
||||||
still_the_same: bool = True
|
still_the_same: bool = True
|
||||||
if bpy.context.scene.name in MaterialListBool.old_list:
|
if bpy.context.scene.name in MaterialListBool.old_list:
|
||||||
for item in newlist:
|
for item in newlist:
|
||||||
@@ -60,7 +59,6 @@ class MaterialListBool:
|
|||||||
else:
|
else:
|
||||||
still_the_same = False
|
still_the_same = False
|
||||||
MaterialListBool.bool_material_list_expand[bpy.context.scene.name] = still_the_same
|
MaterialListBool.bool_material_list_expand[bpy.context.scene.name] = still_the_same
|
||||||
|
|
||||||
return MaterialListBool.bool_material_list_expand[bpy.context.scene.name]
|
return MaterialListBool.bool_material_list_expand[bpy.context.scene.name]
|
||||||
|
|
||||||
class ProgressTracker:
|
class ProgressTracker:
|
||||||
@@ -111,89 +109,6 @@ def get_armature_list(self: Optional[Any] = None, context: Optional[Context] = N
|
|||||||
return [('NONE', t("Armature.validation.no_armature"), '')]
|
return [('NONE', t("Armature.validation.no_armature"), '')]
|
||||||
return armatures
|
return armatures
|
||||||
|
|
||||||
def validate_armature(armature: Object) -> Tuple[bool, List[str]]:
|
|
||||||
"""Enhanced armature validation with multiple validation modes"""
|
|
||||||
validation_mode = bpy.context.scene.avatar_toolkit.validation_mode
|
|
||||||
messages: List[str] = []
|
|
||||||
|
|
||||||
if validation_mode == 'NONE':
|
|
||||||
return True, []
|
|
||||||
|
|
||||||
if not armature or armature.type != 'ARMATURE' or not armature.data.bones:
|
|
||||||
return False, [t("Armature.validation.basic_check_failed")]
|
|
||||||
|
|
||||||
found_bones: Dict[str, Bone] = {bone.name.lower(): bone for bone in armature.data.bones}
|
|
||||||
essential_bones: Set[str] = {'hips', 'spine', 'chest', 'neck', 'head'}
|
|
||||||
|
|
||||||
missing_bones: List[str] = []
|
|
||||||
for bone in essential_bones:
|
|
||||||
if not any(alt_name in found_bones for alt_name in bone_names[bone]):
|
|
||||||
missing_bones.append(bone)
|
|
||||||
|
|
||||||
if missing_bones:
|
|
||||||
messages.append(t("Armature.validation.missing_bones", bones=", ".join(missing_bones)))
|
|
||||||
|
|
||||||
if validation_mode == 'STRICT':
|
|
||||||
hierarchy: List[Tuple[str, str]] = [
|
|
||||||
('hips', 'spine'), ('spine', 'chest'),
|
|
||||||
('chest', 'neck'), ('neck', 'head')
|
|
||||||
]
|
|
||||||
for parent, child in hierarchy:
|
|
||||||
if not validate_bone_hierarchy(found_bones, parent, child):
|
|
||||||
messages.append(t("Armature.validation.invalid_hierarchy",
|
|
||||||
parent=parent, child=child))
|
|
||||||
|
|
||||||
symmetry_pairs: List[Tuple[str, str, str]] = [('arm', 'l', 'r'), ('leg', 'l', 'r')]
|
|
||||||
for base, left, right in symmetry_pairs:
|
|
||||||
if not validate_symmetry(found_bones, base, left, right):
|
|
||||||
messages.append(t("Armature.validation.asymmetric_bones", bone=base))
|
|
||||||
|
|
||||||
if (not validate_symmetry(found_bones, 'hand', 'l', 'r') and
|
|
||||||
not validate_symmetry(found_bones, 'wrist', 'l', 'r')):
|
|
||||||
messages.append(t("Armature.validation.asymmetric_hand_wrist"))
|
|
||||||
|
|
||||||
is_valid: bool = len(messages) == 0
|
|
||||||
return is_valid, messages
|
|
||||||
|
|
||||||
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"""
|
|
||||||
parent_bone: Optional[Bone] = None
|
|
||||||
child_bone: Optional[Bone] = None
|
|
||||||
|
|
||||||
for alt_name in bone_names[parent_name]:
|
|
||||||
if alt_name in bones:
|
|
||||||
parent_bone = bones[alt_name]
|
|
||||||
break
|
|
||||||
|
|
||||||
for alt_name in bone_names[child_name]:
|
|
||||||
if alt_name in bones:
|
|
||||||
child_bone = bones[alt_name]
|
|
||||||
break
|
|
||||||
|
|
||||||
if not parent_bone or not child_bone:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return child_bone.parent == parent_bone
|
|
||||||
|
|
||||||
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"""
|
|
||||||
left_patterns: List[str] = [
|
|
||||||
f"{base}.{left}",
|
|
||||||
f"{base}_{left}",
|
|
||||||
f"{left}_{base}"
|
|
||||||
]
|
|
||||||
|
|
||||||
right_patterns: List[str] = [
|
|
||||||
f"{base}.{right}",
|
|
||||||
f"{base}_{right}",
|
|
||||||
f"{right}_{base}"
|
|
||||||
]
|
|
||||||
|
|
||||||
left_exists: bool = any(pattern in bones for pattern in left_patterns)
|
|
||||||
right_exists: bool = any(pattern in bones for pattern in right_patterns)
|
|
||||||
|
|
||||||
return left_exists and right_exists
|
|
||||||
|
|
||||||
def auto_select_single_armature(context: 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: List[Tuple[str, str, str]] = get_armature_list(context)
|
armatures: List[Tuple[str, str, str]] = get_armature_list(context)
|
||||||
@@ -364,51 +279,67 @@ def validate_meshes(meshes: List[Object]) -> Tuple[bool, str]:
|
|||||||
return False, t("Optimization.non_mesh_objects")
|
return False, t("Optimization.non_mesh_objects")
|
||||||
return True, ""
|
return True, ""
|
||||||
|
|
||||||
|
def fast_uv_fix(obj: Object) -> None:
|
||||||
|
"""Fast UV coordinate fixing for joined meshes"""
|
||||||
|
if not obj or not obj.data or not obj.data.uv_layers:
|
||||||
|
return
|
||||||
|
|
||||||
|
current_mode = bpy.context.mode
|
||||||
|
|
||||||
|
if current_mode != 'EDIT_MESH':
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
|
||||||
|
bpy.ops.mesh.select_all(action='SELECT')
|
||||||
|
|
||||||
|
# Process all UV layers at once
|
||||||
|
bpy.ops.uv.select_all(action='SELECT')
|
||||||
|
bpy.ops.uv.pack_islands(margin=0.001)
|
||||||
|
|
||||||
|
if current_mode != 'EDIT_MESH':
|
||||||
|
bpy.ops.object.mode_set(mode=current_mode)
|
||||||
|
|
||||||
def join_mesh_objects(context: Context, meshes: List[Object], progress: Optional[ProgressTracker] = None) -> Optional[Object]:
|
def join_mesh_objects(context: Context, meshes: List[Object], progress: Optional[ProgressTracker] = None) -> Optional[Object]:
|
||||||
"""Combines multiple mesh objects into a single mesh with proper cleanup and UV fixing"""
|
"""Combines multiple mesh objects into a single mesh with optimized performance"""
|
||||||
try:
|
try:
|
||||||
# Store UV maps before joining
|
if not meshes:
|
||||||
uv_maps_data = {}
|
return None
|
||||||
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')
|
||||||
|
|
||||||
for mesh in meshes:
|
# Create a list of valid meshes
|
||||||
|
valid_meshes = [mesh for mesh in meshes if mesh.name in bpy.data.objects]
|
||||||
|
if not valid_meshes:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for mesh in valid_meshes:
|
||||||
mesh.select_set(True)
|
mesh.select_set(True)
|
||||||
|
|
||||||
if context.selected_objects:
|
context.view_layer.objects.active = valid_meshes[0]
|
||||||
context.view_layer.objects.active = context.selected_objects[0]
|
|
||||||
|
|
||||||
if progress:
|
if progress:
|
||||||
progress.step(t("Optimization.joining_meshes"))
|
progress.step(t("Optimization.joining_meshes"))
|
||||||
|
|
||||||
bpy.ops.object.join()
|
bpy.ops.object.join()
|
||||||
|
joined_mesh = context.active_object
|
||||||
|
|
||||||
if progress:
|
if progress:
|
||||||
progress.step(t("Optimization.applying_transforms"))
|
progress.step(t("Optimization.applying_transforms"))
|
||||||
|
|
||||||
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
|
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
|
||||||
|
|
||||||
if progress:
|
if progress:
|
||||||
progress.step(t("Optimization.fixing_uvs"))
|
progress.step(t("Optimization.fixing_uvs"))
|
||||||
fix_uv_coordinates(context)
|
|
||||||
|
|
||||||
# Restore UV maps after joining
|
fast_uv_fix(joined_mesh)
|
||||||
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 joined_mesh
|
||||||
|
|
||||||
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)}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def fix_uv_coordinates(context: Context) -> None:
|
def fix_uv_coordinates(context: Context) -> None:
|
||||||
"""Normalizes and fixes UV coordinates for the active mesh object"""
|
"""Normalizes and fixes UV coordinates for the active mesh object"""
|
||||||
obj: Object = context.object
|
obj: Object = context.object
|
||||||
@@ -452,9 +383,17 @@ def clear_unused_data_blocks() -> int:
|
|||||||
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
|
||||||
|
|
||||||
def simplify_bonename(name: str) -> str:
|
def identify_bones(arm_data: bpy.types.Armature, context: bpy.types.Context) -> Dict[str,str]:
|
||||||
"""Simplify bone name by removing spaces, underscores, dots and converting to lowercase"""
|
"""Identify bone names in an armature based on our reverse dictionary, so there is no confusion to what a bone is.
|
||||||
return name.lower().translate(dict.fromkeys(map(ord, u" _.")))
|
Essentially makes a dictionary of keys from dictionaries.bone_names like "hips", and the corosponding value is the bone that can be mapped to that key."""
|
||||||
|
returned: Dict[str,str] = {}
|
||||||
|
for bone in arm_data.bones:
|
||||||
|
|
||||||
|
simplified_name = simplify_bonename(bone.name)
|
||||||
|
|
||||||
|
if simplified_name in reverse_bone_lookup:
|
||||||
|
returned[reverse_bone_lookup[simplified_name]] = bone.name
|
||||||
|
return returned
|
||||||
|
|
||||||
def duplicate_bone_chain(bones: List[EditBone]) -> List[EditBone]:
|
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"""
|
||||||
|
|||||||
+624
-18
@@ -1,9 +1,14 @@
|
|||||||
# GPL Licence
|
# GPL Licence
|
||||||
|
|
||||||
|
|
||||||
# Bone names from https://github.com/triazo/immersive_scaler/
|
# Bone names from https://github.com/triazo/immersive_scaler/
|
||||||
# Note from @989onan: Please make sure to make your names are lowercase in this array. I banged my head metaphorically till I figured that out...
|
# Note from @989onan: Please make sure to make your names are lowercase in this array, or it will never find a match. I banged my head metaphorically till I figured that out...
|
||||||
|
# Note2: Remove all "_", ".", and " " (space) from your values array or it will also not ever find a match!!!!
|
||||||
# Taken from Tuxedo/Cats
|
# Taken from Tuxedo/Cats
|
||||||
|
|
||||||
|
def simplify_bonename(name: str) -> str:
|
||||||
|
"""Simplify bone name by removing spaces, underscores, dots and converting to lowercase"""
|
||||||
|
return name.lower().translate(dict.fromkeys(map(ord, u" _.")))
|
||||||
|
|
||||||
bone_names = {
|
bone_names = {
|
||||||
# Right side bones
|
# Right side bones
|
||||||
"right_shoulder": [
|
"right_shoulder": [
|
||||||
@@ -254,26 +259,26 @@ bone_names = {
|
|||||||
|
|
||||||
# Add VRM bone name variations
|
# Add VRM bone name variations
|
||||||
bone_names.update({
|
bone_names.update({
|
||||||
'hips': bone_names['hips'] + ['j_bip_c_hips', 'j_hips', 'vrm_hips'],
|
'hips': bone_names['hips'] + ['jbipchips', 'jhips', 'vrmhips'],
|
||||||
'spine': bone_names['spine'] + ['j_bip_c_spine', 'j_spine', 'vrm_spine'],
|
'spine': bone_names['spine'] + ['jbipcspine', 'jspine', 'vrmspine'],
|
||||||
'chest': bone_names['chest'] + ['j_bip_c_chest', 'j_chest', 'vrm_chest'],
|
'chest': bone_names['chest'] + ['jbipcchest', 'jchest', 'vrmchest'],
|
||||||
'upper_chest': bone_names['upper_chest'] + ['j_bip_c_upper_chest', 'j_upper_chest', 'vrm_upperchest'],
|
'upper_chest': bone_names['upper_chest'] + ['jbipcupperchest', 'jupperchest', 'vrmupperchest'],
|
||||||
'neck': bone_names['neck'] + ['j_bip_c_neck', 'j_neck', 'vrm_neck'],
|
'neck': bone_names['neck'] + ['jbipcneck', 'jneck', 'vrmneck'],
|
||||||
'head': bone_names['head'] + ['j_bip_c_head', 'j_head', 'vrm_head'],
|
'head': bone_names['head'] + ['jbipchead', 'jhead', 'vrmhead'],
|
||||||
|
|
||||||
# VRM specific finger naming
|
# VRM specific finger naming
|
||||||
'thumb_0_l': bone_names['thumb_0_l'] + ['thumb_metacarpal_l', 'j_thumb1_l'],
|
'thumb_0_l': bone_names['thumb_0_l'] + ['thumbmetacarpall', 'jthumb1l'],
|
||||||
'index_0_l': bone_names['index_0_l'] + ['index_metacarpal_l', 'j_index1_l'],
|
'index_0_l': bone_names['index_0_l'] + ['indexmetacarpall', 'jindex1l'],
|
||||||
'middle_0_l': bone_names['middle_0_l'] + ['middle_metacarpal_l', 'j_middle1_l'],
|
'middle_0_l': bone_names['middle_0_l'] + ['middlemetacarpall', 'jmiddle1l'],
|
||||||
'ring_0_l': bone_names['ring_0_l'] + ['ring_metacarpal_l', 'j_ring1_l'],
|
'ring_0_l': bone_names['ring_0_l'] + ['ringmetacarpall', 'jring1l'],
|
||||||
'pinkie_0_l': bone_names['pinkie_0_l'] + ['little_metacarpal_l', 'j_little1_l'],
|
'pinkie_0_l': bone_names['pinkie_0_l'] + ['littlemetacarpall', 'jlittle1l'],
|
||||||
|
|
||||||
# Mirror for right side
|
# Mirror for right side
|
||||||
'thumb_0_r': bone_names['thumb_0_r'] + ['thumb_metacarpal_r', 'j_thumb1_r'],
|
'thumb_0_r': bone_names['thumb_0_r'] + ['thumbmetacarpalr', 'jthumb1r'],
|
||||||
'index_0_r': bone_names['index_0_r'] + ['index_metacarpal_r', 'j_index1_r'],
|
'index_0_r': bone_names['index_0_r'] + ['indexmetacarpalr', 'jindex1r'],
|
||||||
'middle_0_r': bone_names['middle_0_r'] + ['middle_metacarpal_r', 'j_middle1_r'],
|
'middle_0_r': bone_names['middle_0_r'] + ['middlemetacarpalr', 'jmiddle1r'],
|
||||||
'ring_0_r': bone_names['ring_0_r'] + ['ring_metacarpal_r', 'j_ring1_r'],
|
'ring_0_r': bone_names['ring_0_r'] + ['ringmetacarpalr', 'jring1r'],
|
||||||
'pinkie_0_r': bone_names['pinkie_0_r'] + ['little_metacarpal_r', 'j_little1_r']
|
'pinkie_0_r': bone_names['pinkie_0_r'] + ['littlemetacarpalr', 'jlittle1r']
|
||||||
})
|
})
|
||||||
|
|
||||||
# array taken from cats
|
# array taken from cats
|
||||||
@@ -354,3 +359,604 @@ resonite_translations = {
|
|||||||
'thumb_2_r': "thumb2.R",
|
'thumb_2_r': "thumb2.R",
|
||||||
'thumb_3_r': "thumb3.R"
|
'thumb_3_r': "thumb3.R"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
standard_bones = {
|
||||||
|
# Core Structure
|
||||||
|
'hips': 'Hips',
|
||||||
|
'spine': 'Spine',
|
||||||
|
'chest': 'Chest',
|
||||||
|
'upper_chest': 'Chest.Up',
|
||||||
|
'neck': 'Neck',
|
||||||
|
'head': 'Head',
|
||||||
|
|
||||||
|
# Arms
|
||||||
|
'left_arm': 'UpperArm.L',
|
||||||
|
'left_elbow': 'LowerArm.L',
|
||||||
|
'left_wrist': 'Hand.L',
|
||||||
|
'right_arm': 'UpperArm.R',
|
||||||
|
'right_elbow': 'LowerArm.R',
|
||||||
|
'right_wrist': 'Hand.R',
|
||||||
|
|
||||||
|
# Legs
|
||||||
|
'left_leg': 'UpperLeg.L',
|
||||||
|
'left_knee': 'LowerLeg.L',
|
||||||
|
'left_ankle': 'Foot.L',
|
||||||
|
'left_toe': 'Toes.L',
|
||||||
|
'right_leg': 'UpperLeg.R',
|
||||||
|
'right_knee': 'LowerLeg.R',
|
||||||
|
'right_ankle': 'Foot.R',
|
||||||
|
'right_toe': 'Toes.R',
|
||||||
|
|
||||||
|
# Fingers Left
|
||||||
|
'thumb_1_l': 'Thumb1.L',
|
||||||
|
'thumb_2_l': 'Thumb2.L',
|
||||||
|
'thumb_3_l': 'Thumb3.L',
|
||||||
|
'index_1_l': 'Index1.L',
|
||||||
|
'index_2_l': 'Index2.L',
|
||||||
|
'index_3_l': 'Index3.L',
|
||||||
|
'middle_1_l': 'Middle1.L',
|
||||||
|
'middle_2_l': 'Middle2.L',
|
||||||
|
'middle_3_l': 'Middle3.L',
|
||||||
|
'ring_1_l': 'Ring1.L',
|
||||||
|
'ring_2_l': 'Ring2.L',
|
||||||
|
'ring_3_l': 'Ring3.L',
|
||||||
|
'pinkie_1_l': 'Pinky1.L',
|
||||||
|
'pinkie_2_l': 'Pinky2.L',
|
||||||
|
'pinkie_3_l': 'Pinky3.L',
|
||||||
|
|
||||||
|
# Fingers Right
|
||||||
|
'thumb_1_r': 'Thumb1.R',
|
||||||
|
'thumb_2_r': 'Thumb2.R',
|
||||||
|
'thumb_3_r': 'Thumb3.R',
|
||||||
|
'index_1_r': 'Index1.R',
|
||||||
|
'index_2_r': 'Index2.R',
|
||||||
|
'index_3_r': 'Index3.R',
|
||||||
|
'middle_1_r': 'Middle1.R',
|
||||||
|
'middle_2_r': 'Middle2.R',
|
||||||
|
'middle_3_r': 'Middle3.R',
|
||||||
|
'ring_1_r': 'Ring1.R',
|
||||||
|
'ring_2_r': 'Ring2.R',
|
||||||
|
'ring_3_r': 'Ring3.R',
|
||||||
|
'pinkie_1_r': 'Pinky1.R',
|
||||||
|
'pinkie_2_r': 'Pinky2.R',
|
||||||
|
'pinkie_3_r': 'Pinky3.R',
|
||||||
|
|
||||||
|
# Eyes
|
||||||
|
'left_eye': 'Eye.L',
|
||||||
|
'right_eye': 'Eye.R'
|
||||||
|
}
|
||||||
|
|
||||||
|
bone_hierarchy = [
|
||||||
|
('Hips', 'Spine'),
|
||||||
|
('Spine', 'Chest'),
|
||||||
|
('Chest', 'Chest.Up'),
|
||||||
|
('Chest.Up', 'Neck'),
|
||||||
|
('Neck', 'Head'),
|
||||||
|
('Head', 'Eye.L'),
|
||||||
|
('Head', 'Eye.R'),
|
||||||
|
|
||||||
|
# Left Arm Chain
|
||||||
|
('Chest.Up', 'UpperArm.L'),
|
||||||
|
('UpperArm.L', 'LowerArm.L'),
|
||||||
|
('LowerArm.L', 'Hand.L'),
|
||||||
|
|
||||||
|
# Right Arm Chain
|
||||||
|
('Chest.Up', 'UpperArm.R'),
|
||||||
|
('UpperArm.R', 'LowerArm.R'),
|
||||||
|
('LowerArm.R', 'Hand.R'),
|
||||||
|
|
||||||
|
# Left Leg Chain
|
||||||
|
('Hips', 'UpperLeg.L'),
|
||||||
|
('UpperLeg.L', 'LowerLeg.L'),
|
||||||
|
('LowerLeg.L', 'Foot.L'),
|
||||||
|
('Foot.L', 'Toes.L'),
|
||||||
|
|
||||||
|
# Right Leg Chain
|
||||||
|
('Hips', 'UpperLeg.R'),
|
||||||
|
('UpperLeg.R', 'LowerLeg.R'),
|
||||||
|
('LowerLeg.R', 'Foot.R'),
|
||||||
|
('Foot.R', 'Toes.R')
|
||||||
|
]
|
||||||
|
|
||||||
|
finger_hierarchy = {
|
||||||
|
'left': [
|
||||||
|
('Hand.L', 'Thumb1.L', 'Thumb2.L', 'Thumb3.L'),
|
||||||
|
('Hand.L', 'Index1.L', 'Index2.L', 'Index3.L'),
|
||||||
|
('Hand.L', 'Middle1.L', 'Middle2.L', 'Middle3.L'),
|
||||||
|
('Hand.L', 'Ring1.L', 'Ring2.L', 'Ring3.L'),
|
||||||
|
('Hand.L', 'Pinky1.L', 'Pinky2.L', 'Pinky3.L')
|
||||||
|
],
|
||||||
|
'right': [
|
||||||
|
('Hand.R', 'Thumb1.R', 'Thumb2.R', 'Thumb3.R'),
|
||||||
|
('Hand.R', 'Index1.R', 'Index2.R', 'Index3.R'),
|
||||||
|
('Hand.R', 'Middle1.R', 'Middle2.R', 'Middle3.R'),
|
||||||
|
('Hand.R', 'Ring1.R', 'Ring2.R', 'Ring3.R'),
|
||||||
|
('Hand.R', 'Pinky1.R', 'Pinky2.R', 'Pinky3.R')
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptable_bone_hierarchy = [
|
||||||
|
# Right side chain
|
||||||
|
('Hips', 'Chest'),
|
||||||
|
('Chest', 'Shoulder.R'),
|
||||||
|
('Shoulder.R', 'Arm.R'),
|
||||||
|
('Arm.R', 'Elbow.R'),
|
||||||
|
('Elbow.R', 'Wrist.R'),
|
||||||
|
('Hips', 'Leg.R'),
|
||||||
|
('Leg.R', 'Knee.R'),
|
||||||
|
('Knee.R', 'Foot.R'),
|
||||||
|
('Foot.R', 'Toes.R'),
|
||||||
|
|
||||||
|
# Left side chain
|
||||||
|
('Chest', 'Shoulder.L'),
|
||||||
|
('Shoulder.L', 'Arm.L'),
|
||||||
|
('Arm.L', 'Elbow.L'),
|
||||||
|
('Elbow.L', 'Wrist.L'),
|
||||||
|
('Hips', 'Leg.L'),
|
||||||
|
('Leg.L', 'Knee.L'),
|
||||||
|
('Knee.L', 'Foot.L'),
|
||||||
|
('Foot.L', 'Toes.L'),
|
||||||
|
|
||||||
|
# Head and Eyes
|
||||||
|
('Chest', 'Neck'),
|
||||||
|
('Neck', 'Head'),
|
||||||
|
('Head', 'Eye_L'),
|
||||||
|
('Head', 'Eye_R'),
|
||||||
|
('Head', 'LeftEye'),
|
||||||
|
('Head', 'RightEye'),
|
||||||
|
|
||||||
|
# Unity humanoid naming
|
||||||
|
('Hips', 'Spine'),
|
||||||
|
('Spine', 'Chest'),
|
||||||
|
('Chest', 'UpperChest'),
|
||||||
|
('UpperChest', 'Neck'),
|
||||||
|
('Neck', 'Head'),
|
||||||
|
('Head', 'LeftEye'),
|
||||||
|
('Head', 'RightEye'),
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
acceptable_bone_names = {
|
||||||
|
'hips': ['Hips', 'pelvis', 'root', 'Root', 'ROOT'],
|
||||||
|
'chest': ['Chest', 'spine1', 'Spine1', 'spine_01', 'SPINE1', 'Spine01'],
|
||||||
|
'neck': ['Neck', 'neck_01', 'Neck01'],
|
||||||
|
'head': ['Head', 'head_01', 'Head01'],
|
||||||
|
'eye_l': ['Eye_L', 'LeftEye', 'lefteye', 'eye_left', 'EyeLeft'],
|
||||||
|
'eye_r': ['Eye_R', 'RightEye', 'righteye', 'eye_right', 'EyeRight'],
|
||||||
|
|
||||||
|
'shoulder_r': ['Shoulder.R', 'clavicle_r', 'ClavicleRight', 'RightShoulder'],
|
||||||
|
'arm_r': ['Arm.R', 'upperarm_r', 'UpperArmRight', 'RightArm'],
|
||||||
|
'elbow_r': ['Elbow.R', 'lowerarm_r', 'ForearmRight', 'RightForeArm'],
|
||||||
|
'wrist_r': ['Wrist.R', 'hand_r', 'HandRight', 'RightHand'],
|
||||||
|
'leg_r': ['Leg.R', 'thigh_r', 'ThighRight', 'RightLeg', 'RightUpLeg'],
|
||||||
|
'knee_r': ['Knee.R', 'calf_r', 'CalfRight', 'RightShin', 'RightLowerLeg'],
|
||||||
|
'foot_r': ['Foot.R', 'foot_r', 'FootRight', 'RightFoot'],
|
||||||
|
'toes_r': ['Toes.R', 'ball_r', 'ToeRight', 'RightToeBase'],
|
||||||
|
|
||||||
|
'shoulder_l': ['Shoulder.L', 'clavicle_l', 'ClavicleLeft', 'LeftShoulder'],
|
||||||
|
'arm_l': ['Arm.L', 'upperarm_l', 'UpperArmLeft', 'LeftArm'],
|
||||||
|
'elbow_l': ['Elbow.L', 'lowerarm_l', 'ForearmLeft', 'LeftForeArm'],
|
||||||
|
'wrist_l': ['Wrist.L', 'hand_l', 'HandLeft', 'LeftHand'],
|
||||||
|
'leg_l': ['Leg.L', 'thigh_l', 'ThighLeft', 'LeftLeg', 'LeftUpLeg'],
|
||||||
|
'knee_l': ['Knee.L', 'calf_l', 'CalfLeft', 'LeftShin', 'LeftLowerLeg'],
|
||||||
|
'foot_l': ['Foot.L', 'foot_l', 'FootLeft', 'LeftFoot'],
|
||||||
|
'toes_l': ['Toes.L', 'ball_l', 'ToeLeft', 'LeftToeBase'],
|
||||||
|
|
||||||
|
# Add finger bones for left hand
|
||||||
|
'thumb_0_l': ['Thumb0_L'],
|
||||||
|
'thumb_1_l': ['Thumb1_L'],
|
||||||
|
'thumb_2_l': ['Thumb2_L'],
|
||||||
|
'index_1_l': ['IndexFinger1_L'],
|
||||||
|
'index_2_l': ['IndexFinger2_L'],
|
||||||
|
'index_3_l': ['IndexFinger3_L'],
|
||||||
|
'middle_1_l': ['MiddleFinger1_L'],
|
||||||
|
'middle_2_l': ['MiddleFinger2_L'],
|
||||||
|
'middle_3_l': ['MiddleFinger3_L'],
|
||||||
|
'ring_1_l': ['RingFinger1_L'],
|
||||||
|
'ring_2_l': ['RingFinger2_L'],
|
||||||
|
'ring_3_l': ['RingFinger3_L'],
|
||||||
|
|
||||||
|
# Add finger bones for right hand
|
||||||
|
'thumb_0_r': ['Thumb0_R', 'ThumbO_R'],
|
||||||
|
'thumb_1_r': ['Thumb1_R'],
|
||||||
|
'thumb_2_r': ['Thumb2_R'],
|
||||||
|
'index_1_r': ['IndexFinger1_R'],
|
||||||
|
'index_2_r': ['IndexFinger2_R'],
|
||||||
|
'index_3_r': ['IndexFinger3_R'],
|
||||||
|
'middle_1_r': ['MiddleFinger1_R'],
|
||||||
|
'middle_2_r': ['MiddleFinger2_R'],
|
||||||
|
'middle_3_r': ['MiddleFinger3_R'],
|
||||||
|
'ring_1_r': ['RingFinger1_R'],
|
||||||
|
'ring_2_r': ['RingFinger2_R'],
|
||||||
|
'ring_3_r': ['RingFinger3_R'],
|
||||||
|
|
||||||
|
'breast_upper_1_l': ['BreastUpper1_L'],
|
||||||
|
'breast_upper_2_l': ['BreastUpper2_L'],
|
||||||
|
'breast_upper_1_r': ['BreastUpper1_R'],
|
||||||
|
'breast_upper_2_r': ['BreastUpper2_R'],
|
||||||
|
|
||||||
|
'ear_upper_l': ['UpperEar.L', 'Upper Ear.L', 'Upper Ear_L'],
|
||||||
|
'ear_upper_r': ['UpperEar.R', 'Upper Ear.R', 'Upper Ear_R'],
|
||||||
|
'ear_lower_l': ['LowerEar.L', 'Lower Ear.L', 'Lower Ear_L'],
|
||||||
|
'ear_lower_r': ['LowerEar.R', 'Lower Ear.R', 'Lower Ear_R'],
|
||||||
|
|
||||||
|
'ears_upper': ['Ears Upper', 'EarsUpper', 'ears_upper'],
|
||||||
|
'ears_lower': ['Ears Lower', 'EarsLower', 'ears_lower']
|
||||||
|
}
|
||||||
|
|
||||||
|
rigify_unity_names = {
|
||||||
|
"DEF-spine": "Hips",
|
||||||
|
"DEF-spine.001": "Spine",
|
||||||
|
"DEF-spine.002": "Chest",
|
||||||
|
"DEF-spine.003": "UpperChest",
|
||||||
|
"DEF-neck": "Neck",
|
||||||
|
"DEF-head": "Head",
|
||||||
|
"DEF-shoulder.L": "LeftShoulder",
|
||||||
|
"DEF-upper_arm.L": "LeftUpperArm",
|
||||||
|
"DEF-forearm.L": "LeftLowerArm",
|
||||||
|
"DEF-hand.L": "LeftHand",
|
||||||
|
"DEF-shoulder.R": "RightShoulder",
|
||||||
|
"DEF-upper_arm.R": "RightUpperArm",
|
||||||
|
"DEF-forearm.R": "RightLowerArm",
|
||||||
|
"DEF-hand.R": "RightHand",
|
||||||
|
"DEF-thigh.L": "LeftUpperLeg",
|
||||||
|
"DEF-shin.L": "LeftLowerLeg",
|
||||||
|
"DEF-foot.L": "LeftFoot",
|
||||||
|
"DEF-toe.L": "LeftToes",
|
||||||
|
"DEF-thigh.R": "RightUpperLeg",
|
||||||
|
"DEF-shin.R": "RightLowerLeg",
|
||||||
|
"DEF-foot.R": "RightFoot",
|
||||||
|
"DEF-toe.R": "RightToes"
|
||||||
|
}
|
||||||
|
|
||||||
|
rigify_basic_unity_names = {
|
||||||
|
"spine": "Hips",
|
||||||
|
"spine.001": "Spine",
|
||||||
|
"spine.002": "Chest",
|
||||||
|
"spine.003": "UpperChest",
|
||||||
|
"neck": "Neck",
|
||||||
|
"head": "Head",
|
||||||
|
"shoulder.L": "LeftShoulder",
|
||||||
|
"upper_arm.L": "LeftUpperArm",
|
||||||
|
"forearm.L": "LeftLowerArm",
|
||||||
|
"hand.L": "LeftHand",
|
||||||
|
"shoulder.R": "RightShoulder",
|
||||||
|
"upper_arm.R": "RightUpperArm",
|
||||||
|
"forearm.R": "RightLowerArm",
|
||||||
|
"hand.R": "RightHand",
|
||||||
|
"thigh.L": "LeftUpperLeg",
|
||||||
|
"shin.L": "LeftLowerLeg",
|
||||||
|
"foot.L": "LeftFoot",
|
||||||
|
"toe.L": "LeftToes",
|
||||||
|
"thigh.R": "RightUpperLeg",
|
||||||
|
"shin.R": "RightLowerLeg",
|
||||||
|
"foot.R": "RightFoot",
|
||||||
|
"toe.R": "RightToes"
|
||||||
|
}
|
||||||
|
|
||||||
|
rigify_unnecessary_bones = [
|
||||||
|
'face',
|
||||||
|
'ear.l', 'ear.r',
|
||||||
|
'forehead',
|
||||||
|
'cheek.t.l', 'cheek.t.r',
|
||||||
|
'cheek.b.l', 'cheek.b.r',
|
||||||
|
'brow.t.l', 'brow.t.r',
|
||||||
|
'brow.b.l', 'brow.b.r',
|
||||||
|
'jaw',
|
||||||
|
'chin',
|
||||||
|
'nose',
|
||||||
|
'temple.l', 'temple.r',
|
||||||
|
'teeth',
|
||||||
|
'lip',
|
||||||
|
'lid',
|
||||||
|
'heel',
|
||||||
|
'pelvis.'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Non-standard bone mappings to standard bones
|
||||||
|
non_standard_mappings = {
|
||||||
|
'hips': [
|
||||||
|
'mixamorig:Hips', 'mixamorig_Hips',
|
||||||
|
'ORG-spine', 'spine', 'root',
|
||||||
|
'hip', 'pelvis'
|
||||||
|
],
|
||||||
|
'spine': [
|
||||||
|
'mixamorig:Spine', 'mixamorig_Spine',
|
||||||
|
'ORG-spine.001', 'spine.001',
|
||||||
|
'abdomenLower', 'lowerback'
|
||||||
|
],
|
||||||
|
'chest': [
|
||||||
|
'mixamorig:Spine1', 'mixamorig_Spine1',
|
||||||
|
'ORG-spine.002', 'spine.002',
|
||||||
|
'abdomenUpper', 'upperback', 'spine1'
|
||||||
|
],
|
||||||
|
'upper_chest': [
|
||||||
|
'mixamorig:Spine2', 'mixamorig_Spine2',
|
||||||
|
'ORG-spine.003', 'spine.003',
|
||||||
|
'chestLower', 'chest', 'spine2'
|
||||||
|
],
|
||||||
|
'neck': [
|
||||||
|
'mixamorig:Neck', 'mixamorig_Neck',
|
||||||
|
'ORG-spine.004', 'spine.004', 'neck',
|
||||||
|
'neckLower'
|
||||||
|
],
|
||||||
|
'head': [
|
||||||
|
'mixamorig:Head', 'mixamorig_Head',
|
||||||
|
'ORG-spine.005', 'spine.005', 'face', 'head'
|
||||||
|
],
|
||||||
|
|
||||||
|
'left_shoulder': [
|
||||||
|
'mixamorig:LeftShoulder', 'mixamorig_LeftShoulder',
|
||||||
|
'ORG-shoulder.L', 'shoulder.L',
|
||||||
|
'lCollar', 'lShldr', 'lClavicle'
|
||||||
|
],
|
||||||
|
'left_arm': [
|
||||||
|
'mixamorig:LeftArm', 'mixamorig_LeftArm',
|
||||||
|
'ORG-upper_arm.L', 'upper_arm.L',
|
||||||
|
'lShldrBend', 'lShldrTwist', 'lArm'
|
||||||
|
],
|
||||||
|
'left_elbow': [
|
||||||
|
'mixamorig:LeftForeArm', 'mixamorig_LeftForeArm',
|
||||||
|
'ORG-forearm.L', 'forearm.L',
|
||||||
|
'lForearmBend', 'lElbow', 'lForeArm'
|
||||||
|
],
|
||||||
|
'left_wrist': [
|
||||||
|
'mixamorig:LeftHand', 'mixamorig_LeftHand',
|
||||||
|
'ORG-hand.L', 'hand.L',
|
||||||
|
'lHand', 'lWrist'
|
||||||
|
],
|
||||||
|
|
||||||
|
'right_shoulder': [
|
||||||
|
'mixamorig:RightShoulder', 'mixamorig_RightShoulder',
|
||||||
|
'ORG-shoulder.R', 'shoulder.R',
|
||||||
|
'rCollar', 'rShldr', 'rClavicle'
|
||||||
|
],
|
||||||
|
'right_arm': [
|
||||||
|
'mixamorig:RightArm', 'mixamorig_RightArm',
|
||||||
|
'ORG-upper_arm.R', 'upper_arm.R',
|
||||||
|
'rShldrBend', 'rShldrTwist', 'rArm'
|
||||||
|
],
|
||||||
|
'right_elbow': [
|
||||||
|
'mixamorig:RightForeArm', 'mixamorig_RightForeArm',
|
||||||
|
'ORG-forearm.R', 'forearm.R',
|
||||||
|
'rForearmBend', 'rElbow', 'rForeArm'
|
||||||
|
],
|
||||||
|
'right_wrist': [
|
||||||
|
'mixamorig:RightHand', 'mixamorig_RightHand',
|
||||||
|
'ORG-hand.R', 'hand.R',
|
||||||
|
'rHand', 'rWrist'
|
||||||
|
],
|
||||||
|
|
||||||
|
'left_leg': [
|
||||||
|
'mixamorig:LeftUpLeg', 'mixamorig_LeftUpLeg',
|
||||||
|
'ORG-thigh.L', 'thigh.L',
|
||||||
|
'lThighBend', 'lThigh'
|
||||||
|
],
|
||||||
|
'left_knee': [
|
||||||
|
'mixamorig:LeftLeg', 'mixamorig_LeftLeg',
|
||||||
|
'ORG-shin.L', 'shin.L',
|
||||||
|
'lShin', 'lKnee', 'lLeg'
|
||||||
|
],
|
||||||
|
'left_ankle': [
|
||||||
|
'mixamorig:LeftFoot', 'mixamorig_LeftFoot',
|
||||||
|
'ORG-foot.L', 'foot.L',
|
||||||
|
'lFoot', 'lAnkle'
|
||||||
|
],
|
||||||
|
'left_toe': [
|
||||||
|
'mixamorig:LeftToeBase', 'mixamorig_LeftToeBase',
|
||||||
|
'ORG-toe.L', 'toe.L',
|
||||||
|
'lToe'
|
||||||
|
],
|
||||||
|
|
||||||
|
'right_leg': [
|
||||||
|
'mixamorig:RightUpLeg', 'mixamorig_RightUpLeg',
|
||||||
|
'ORG-thigh.R', 'thigh.R',
|
||||||
|
'rThighBend', 'rThigh'
|
||||||
|
],
|
||||||
|
'right_knee': [
|
||||||
|
'mixamorig:RightLeg', 'mixamorig_RightLeg',
|
||||||
|
'ORG-shin.R', 'shin.R',
|
||||||
|
'rShin', 'rKnee', 'rLeg'
|
||||||
|
],
|
||||||
|
'right_ankle': [
|
||||||
|
'mixamorig:RightFoot', 'mixamorig_RightFoot',
|
||||||
|
'ORG-foot.R', 'foot.R',
|
||||||
|
'rFoot', 'rAnkle'
|
||||||
|
],
|
||||||
|
'right_toe': [
|
||||||
|
'mixamorig:RightToeBase', 'mixamorig_RightToeBase',
|
||||||
|
'ORG-toe.R', 'toe.R',
|
||||||
|
'rToe'
|
||||||
|
],
|
||||||
|
|
||||||
|
'thumb_1_l': [
|
||||||
|
'mixamorig:LeftHandThumb1', 'mixamorig_LeftHandThumb1',
|
||||||
|
'ORG-thumb.01.L', 'thumb.01.L',
|
||||||
|
'lThumb1'
|
||||||
|
],
|
||||||
|
'thumb_2_l': [
|
||||||
|
'mixamorig:LeftHandThumb2', 'mixamorig_LeftHandThumb2',
|
||||||
|
'ORG-thumb.02.L', 'thumb.02.L',
|
||||||
|
'lThumb2'
|
||||||
|
],
|
||||||
|
'thumb_3_l': [
|
||||||
|
'mixamorig:LeftHandThumb3', 'mixamorig_LeftHandThumb3',
|
||||||
|
'ORG-thumb.03.L', 'thumb.03.L',
|
||||||
|
'lThumb3'
|
||||||
|
],
|
||||||
|
|
||||||
|
'index_1_l': [
|
||||||
|
'mixamorig:LeftHandIndex1', 'mixamorig_LeftHandIndex1',
|
||||||
|
'ORG-f_index.01.L', 'f_index.01.L',
|
||||||
|
'lIndex1'
|
||||||
|
],
|
||||||
|
'index_2_l': [
|
||||||
|
'mixamorig:LeftHandIndex2', 'mixamorig_LeftHandIndex2',
|
||||||
|
'ORG-f_index.02.L', 'f_index.02.L',
|
||||||
|
'lIndex2'
|
||||||
|
],
|
||||||
|
'index_3_l': [
|
||||||
|
'mixamorig:LeftHandIndex3', 'mixamorig_LeftHandIndex3',
|
||||||
|
'ORG-f_index.03.L', 'f_index.03.L',
|
||||||
|
'lIndex3'
|
||||||
|
],
|
||||||
|
|
||||||
|
'middle_1_l': [
|
||||||
|
'mixamorig:LeftHandMiddle1', 'mixamorig_LeftHandMiddle1',
|
||||||
|
'ORG-f_middle.01.L', 'f_middle.01.L',
|
||||||
|
'lMid1'
|
||||||
|
],
|
||||||
|
'middle_2_l': [
|
||||||
|
'mixamorig:LeftHandMiddle2', 'mixamorig_LeftHandMiddle2',
|
||||||
|
'ORG-f_middle.02.L', 'f_middle.02.L',
|
||||||
|
'lMid2'
|
||||||
|
],
|
||||||
|
'middle_3_l': [
|
||||||
|
'mixamorig:LeftHandMiddle3', 'mixamorig_LeftHandMiddle3',
|
||||||
|
'ORG-f_middle.03.L', 'f_middle.03.L',
|
||||||
|
'lMid3'
|
||||||
|
],
|
||||||
|
|
||||||
|
'ring_1_l': [
|
||||||
|
'mixamorig:LeftHandRing1', 'mixamorig_LeftHandRing1',
|
||||||
|
'ORG-f_ring.01.L', 'f_ring.01.L',
|
||||||
|
'lRing1'
|
||||||
|
],
|
||||||
|
'ring_2_l': [
|
||||||
|
'mixamorig:LeftHandRing2', 'mixamorig_LeftHandRing2',
|
||||||
|
'ORG-f_ring.02.L', 'f_ring.02.L',
|
||||||
|
'lRing2'
|
||||||
|
],
|
||||||
|
'ring_3_l': [
|
||||||
|
'mixamorig:LeftHandRing3', 'mixamorig_LeftHandRing3',
|
||||||
|
'ORG-f_ring.03.L', 'f_ring.03.L',
|
||||||
|
'lRing3'
|
||||||
|
],
|
||||||
|
|
||||||
|
'pinkie_1_l': [
|
||||||
|
'mixamorig:LeftHandPinky1', 'mixamorig_LeftHandPinky1',
|
||||||
|
'ORG-f_pinky.01.L', 'f_pinky.01.L',
|
||||||
|
'lPinky1'
|
||||||
|
],
|
||||||
|
'pinkie_2_l': [
|
||||||
|
'mixamorig:LeftHandPinky2', 'mixamorig_LeftHandPinky2',
|
||||||
|
'ORG-f_pinky.02.L', 'f_pinky.02.L',
|
||||||
|
'lPinky2'
|
||||||
|
],
|
||||||
|
'pinkie_3_l': [
|
||||||
|
'mixamorig:LeftHandPinky3', 'mixamorig_LeftHandPinky3',
|
||||||
|
'ORG-f_pinky.03.L', 'f_pinky.03.L',
|
||||||
|
'lPinky3'
|
||||||
|
],
|
||||||
|
|
||||||
|
'thumb_1_r': [
|
||||||
|
'mixamorig:RightHandThumb1', 'mixamorig_RightHandThumb1',
|
||||||
|
'ORG-thumb.01.R', 'thumb.01.R',
|
||||||
|
'rThumb1'
|
||||||
|
],
|
||||||
|
'thumb_2_r': [
|
||||||
|
'mixamorig:RightHandThumb2', 'mixamorig_RightHandThumb2',
|
||||||
|
'ORG-thumb.02.R', 'thumb.02.R',
|
||||||
|
'rThumb2'
|
||||||
|
],
|
||||||
|
'thumb_3_r': [
|
||||||
|
'mixamorig:RightHandThumb3', 'mixamorig_RightHandThumb3',
|
||||||
|
'ORG-thumb.03.R', 'thumb.03.R',
|
||||||
|
'rThumb3'
|
||||||
|
],
|
||||||
|
|
||||||
|
'index_1_r': [
|
||||||
|
'mixamorig:RightHandIndex1', 'mixamorig_RightHandIndex1',
|
||||||
|
'ORG-f_index.01.R', 'f_index.01.R',
|
||||||
|
'rIndex1'
|
||||||
|
],
|
||||||
|
'index_2_r': [
|
||||||
|
'mixamorig:RightHandIndex2', 'mixamorig_RightHandIndex2',
|
||||||
|
'ORG-f_index.02.R', 'f_index.02.R',
|
||||||
|
'rIndex2'
|
||||||
|
],
|
||||||
|
'index_3_r': [
|
||||||
|
'mixamorig:RightHandIndex3', 'mixamorig_RightHandIndex3',
|
||||||
|
'ORG-f_index.03.R', 'f_index.03.R',
|
||||||
|
'rIndex3'
|
||||||
|
],
|
||||||
|
|
||||||
|
'middle_1_r': [
|
||||||
|
'mixamorig:RightHandMiddle1', 'mixamorig_RightHandMiddle1',
|
||||||
|
'ORG-f_middle.01.R', 'f_middle.01.R',
|
||||||
|
'rMid1'
|
||||||
|
],
|
||||||
|
'middle_2_r': [
|
||||||
|
'mixamorig:RightHandMiddle2', 'mixamorig_RightHandMiddle2',
|
||||||
|
'ORG-f_middle.02.R', 'f_middle.02.R',
|
||||||
|
'rMid2'
|
||||||
|
],
|
||||||
|
'middle_3_r': [
|
||||||
|
'mixamorig:RightHandMiddle3', 'mixamorig_RightHandMiddle3',
|
||||||
|
'ORG-f_middle.03.R', 'f_middle.03.R',
|
||||||
|
'rMid3'
|
||||||
|
],
|
||||||
|
|
||||||
|
'ring_1_r': [
|
||||||
|
'mixamorig:RightHandRing1', 'mixamorig_RightHandRing1',
|
||||||
|
'ORG-f_ring.01.R', 'f_ring.01.R',
|
||||||
|
'rRing1'
|
||||||
|
],
|
||||||
|
'ring_2_r': [
|
||||||
|
'mixamorig:RightHandRing2', 'mixamorig_RightHandRing2',
|
||||||
|
'ORG-f_ring.02.R', 'f_ring.02.R',
|
||||||
|
'rRing2'
|
||||||
|
],
|
||||||
|
'ring_3_r': [
|
||||||
|
'mixamorig:RightHandRing3', 'mixamorig_RightHandRing3',
|
||||||
|
'ORG-f_ring.03.R', 'f_ring.03.R',
|
||||||
|
'rRing3'
|
||||||
|
],
|
||||||
|
|
||||||
|
'pinkie_1_r': [
|
||||||
|
'mixamorig:RightHandPinky1', 'mixamorig_RightHandPinky1',
|
||||||
|
'ORG-f_pinky.01.R', 'f_pinky.01.R',
|
||||||
|
'rPinky1'
|
||||||
|
],
|
||||||
|
'pinkie_2_r': [
|
||||||
|
'mixamorig:RightHandPinky2', 'mixamorig_RightHandPinky2',
|
||||||
|
'ORG-f_pinky.02.R', 'f_pinky.02.R',
|
||||||
|
'rPinky2'
|
||||||
|
],
|
||||||
|
'pinkie_3_r': [
|
||||||
|
'mixamorig:RightHandPinky3', 'mixamorig_RightHandPinky3',
|
||||||
|
'ORG-f_pinky.03.R', 'f_pinky.03.R',
|
||||||
|
'rPinky3'
|
||||||
|
],
|
||||||
|
|
||||||
|
'left_eye': [
|
||||||
|
'mixamorig:LeftEye', 'mixamorig_LeftEye',
|
||||||
|
'ORG-eye.L', 'eye.L',
|
||||||
|
'lEye'
|
||||||
|
],
|
||||||
|
'right_eye': [
|
||||||
|
'mixamorig:RightEye', 'mixamorig_RightEye',
|
||||||
|
'ORG-eye.R', 'eye.R',
|
||||||
|
'rEye'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
for category, mappings in non_standard_mappings.items():
|
||||||
|
if category in bone_names:
|
||||||
|
bone_names[category].extend(mappings)
|
||||||
|
else:
|
||||||
|
bone_names[category] = mappings
|
||||||
|
|
||||||
|
|
||||||
|
# Since data set is very poisoned by bone names that aren't simplified (And as such will not map properly using the function) we will just force convert them to the proper format at the end here. - @989onan
|
||||||
|
for standard, mappings in bone_names.items():
|
||||||
|
for i in range(len(mappings)):
|
||||||
|
bone_names[standard][i] = simplify_bonename(mappings[i])
|
||||||
|
|
||||||
|
# Create reverse lookup dictionary (conversion/translation)
|
||||||
|
reverse_bone_lookup = {}
|
||||||
|
for preferred_name, name_list in bone_names.items():
|
||||||
|
for name in name_list:
|
||||||
|
reverse_bone_lookup[name] = preferred_name
|
||||||
|
|||||||
@@ -1,271 +0,0 @@
|
|||||||
import bpy
|
|
||||||
import struct
|
|
||||||
import mathutils
|
|
||||||
import traceback
|
|
||||||
import os
|
|
||||||
|
|
||||||
from bpy.types import Material, Operator, Context, Object, Image, Mesh, MeshUVLoopLayer, Float2AttributeValue, ShaderNodeTexImage, ShaderNodeBsdfPrincipled, ShaderNodeOutputMaterial
|
|
||||||
|
|
||||||
def read_pmd_header(file):
|
|
||||||
# Read PMD header information
|
|
||||||
magic = file.read(3)
|
|
||||||
if magic != b'Pmd':
|
|
||||||
raise ValueError("Invalid PMD file")
|
|
||||||
|
|
||||||
version = struct.unpack('<f', file.read(4))[0]
|
|
||||||
|
|
||||||
# Read additional header fields
|
|
||||||
model_name = file.read(20).decode('shift-jis').rstrip('\0')
|
|
||||||
comment = file.read(256).decode('shift-jis').rstrip('\0')
|
|
||||||
|
|
||||||
return version, model_name, comment
|
|
||||||
|
|
||||||
def read_pmd_vertex(file):
|
|
||||||
# Read PMD vertex information
|
|
||||||
position = struct.unpack('<3f', file.read(12))
|
|
||||||
normal = struct.unpack('<3f', file.read(12))
|
|
||||||
uv = struct.unpack('<2f', file.read(8))
|
|
||||||
bone_indices = list(struct.unpack('<2H', file.read(4)))
|
|
||||||
bone_weights = struct.unpack('<b', file.read(1))[0] / 100
|
|
||||||
edge_flag = struct.unpack('<b', file.read(1))[0]
|
|
||||||
|
|
||||||
return position, normal, uv, bone_indices, bone_weights, edge_flag
|
|
||||||
|
|
||||||
def read_pmd_material(file):
|
|
||||||
# Read PMD material information
|
|
||||||
diffuse_color = struct.unpack('<4f', file.read(16))
|
|
||||||
specular_color = struct.unpack('<3f', file.read(12))
|
|
||||||
specular_intensity = struct.unpack('<f', file.read(4))[0]
|
|
||||||
ambient_color = struct.unpack('<3f', file.read(12))
|
|
||||||
toon_index = struct.unpack('<b', file.read(1))[0]
|
|
||||||
edge_flag = struct.unpack('<b', file.read(1))[0]
|
|
||||||
vertex_count = struct.unpack('<i', file.read(4))[0]
|
|
||||||
texture_file_name = file.read(20).decode('shift-jis').rstrip('\0')
|
|
||||||
|
|
||||||
return diffuse_color, specular_color, specular_intensity, ambient_color, toon_index, edge_flag, vertex_count, texture_file_name
|
|
||||||
|
|
||||||
def read_pmd_bone(file):
|
|
||||||
# Read PMD bone information
|
|
||||||
bone_name = file.read(20).decode('shift-jis').rstrip('\0')
|
|
||||||
parent_bone_index = struct.unpack('<h', file.read(2))[0]
|
|
||||||
tail_pos_bone_index = struct.unpack('<h', file.read(2))[0]
|
|
||||||
bone_type = struct.unpack('<b', file.read(1))[0]
|
|
||||||
ik_parent_bone_index = struct.unpack('<h', file.read(2))[0]
|
|
||||||
bone_head_pos = struct.unpack('<3f', file.read(12))
|
|
||||||
|
|
||||||
return bone_name, parent_bone_index, tail_pos_bone_index, bone_type, ik_parent_bone_index, bone_head_pos
|
|
||||||
|
|
||||||
def read_pmd_ik(file):
|
|
||||||
# Read PMD IK information
|
|
||||||
ik_bone_index = struct.unpack('<h', file.read(2))[0]
|
|
||||||
ik_target_bone_index = struct.unpack('<h', file.read(2))[0]
|
|
||||||
ik_chain_length = struct.unpack('<b', file.read(1))[0]
|
|
||||||
iterations = struct.unpack('<h', file.read(2))[0]
|
|
||||||
limit_angle = struct.unpack('<f', file.read(4))[0]
|
|
||||||
|
|
||||||
ik_child_bone_indices = []
|
|
||||||
for _ in range(ik_chain_length):
|
|
||||||
ik_child_bone_index = struct.unpack('<h', file.read(2))[0]
|
|
||||||
ik_child_bone_indices.append(ik_child_bone_index)
|
|
||||||
|
|
||||||
return ik_bone_index, ik_target_bone_index, ik_chain_length, iterations, limit_angle, ik_child_bone_indices
|
|
||||||
|
|
||||||
def read_pmd_morph(file):
|
|
||||||
# Read PMD morph information
|
|
||||||
morph_name = file.read(20).decode('shift-jis').rstrip('\0')
|
|
||||||
morph_vertex_count = struct.unpack('<i', file.read(4))[0]
|
|
||||||
morph_type = struct.unpack('<b', file.read(1))[0]
|
|
||||||
|
|
||||||
morph_vertices = []
|
|
||||||
for _ in range(morph_vertex_count):
|
|
||||||
morph_vertex_index = struct.unpack('<i', file.read(4))[0]
|
|
||||||
morph_vertex_pos = struct.unpack('<3f', file.read(12))
|
|
||||||
morph_vertices.append((morph_vertex_index, morph_vertex_pos))
|
|
||||||
|
|
||||||
return morph_name, morph_vertex_count, morph_type, morph_vertices
|
|
||||||
|
|
||||||
def import_pmd(filepath):
|
|
||||||
try:
|
|
||||||
with open(filepath, 'rb') as file:
|
|
||||||
version, model_name, comment = read_pmd_header(file)
|
|
||||||
|
|
||||||
# Read vertices
|
|
||||||
vertex_count = struct.unpack('<i', file.read(4))[0]
|
|
||||||
vertices = []
|
|
||||||
for _ in range(vertex_count):
|
|
||||||
position, normal, uv, bone_indices, bone_weights, edge_flag = read_pmd_vertex(file)
|
|
||||||
vertices.append((position, normal, uv, bone_indices, bone_weights, edge_flag))
|
|
||||||
|
|
||||||
# Read faces
|
|
||||||
face_count = struct.unpack('<i', file.read(4))[0]
|
|
||||||
faces = []
|
|
||||||
for _ in range(face_count // 3):
|
|
||||||
face_indices = struct.unpack('<3i', file.read(12))
|
|
||||||
faces.append(face_indices)
|
|
||||||
|
|
||||||
# Read materials
|
|
||||||
material_count = struct.unpack('<i', file.read(4))[0]
|
|
||||||
materials = []
|
|
||||||
for _ in range(material_count):
|
|
||||||
diffuse_color, specular_color, specular_intensity, ambient_color, toon_index, edge_flag, vertex_count, texture_file_name = read_pmd_material(file)
|
|
||||||
materials.append((diffuse_color, specular_color, specular_intensity, ambient_color, toon_index, edge_flag, vertex_count, texture_file_name))
|
|
||||||
|
|
||||||
# Read bones
|
|
||||||
bone_count = struct.unpack('<h', file.read(2))[0]
|
|
||||||
bones = []
|
|
||||||
for _ in range(bone_count):
|
|
||||||
bone_name, parent_bone_index, tail_pos_bone_index, bone_type, ik_parent_bone_index, bone_head_pos = read_pmd_bone(file)
|
|
||||||
bones.append((bone_name, parent_bone_index, tail_pos_bone_index, bone_type, ik_parent_bone_index, bone_head_pos))
|
|
||||||
|
|
||||||
# Read IKs
|
|
||||||
ik_count = struct.unpack('<h', file.read(2))[0]
|
|
||||||
iks = []
|
|
||||||
for _ in range(ik_count):
|
|
||||||
ik_bone_index, ik_target_bone_index, ik_chain_length, iterations, limit_angle, ik_child_bone_indices = read_pmd_ik(file)
|
|
||||||
iks.append((ik_bone_index, ik_target_bone_index, ik_chain_length, iterations, limit_angle, ik_child_bone_indices))
|
|
||||||
|
|
||||||
# Read morphs
|
|
||||||
morph_count = struct.unpack('<h', file.read(2))[0]
|
|
||||||
morphs = []
|
|
||||||
for _ in range(morph_count):
|
|
||||||
morph_name, morph_vertex_count, morph_type, morph_vertices = read_pmd_morph(file)
|
|
||||||
morphs.append((morph_name, morph_vertex_count, morph_type, morph_vertices))
|
|
||||||
|
|
||||||
# Create Blender objects and assign PMD data
|
|
||||||
mesh = bpy.data.meshes.new(model_name)
|
|
||||||
mesh.from_pydata([v[0] for v in vertices], [], faces)
|
|
||||||
mesh.update()
|
|
||||||
|
|
||||||
obj = bpy.data.objects.new(model_name, mesh)
|
|
||||||
bpy.context.collection.objects.link(obj)
|
|
||||||
|
|
||||||
# Assign vertex normals
|
|
||||||
for i, vertex in enumerate(vertices):
|
|
||||||
mesh.vertices[i].normal = vertex[1]
|
|
||||||
|
|
||||||
# Assign UV coordinates
|
|
||||||
uv_layer = mesh.uv_layers.new()
|
|
||||||
for i, vertex in enumerate(vertices):
|
|
||||||
uv_layer.data[i].uv = vertex[2]
|
|
||||||
|
|
||||||
# Assign materials
|
|
||||||
for material_data in materials:
|
|
||||||
material: bpy.types.Material
|
|
||||||
if f"Material_{len(mesh.materials)}" in bpy.data.materials:
|
|
||||||
material = bpy.data.materials[f"Material_{len(mesh.materials)}"]
|
|
||||||
else:
|
|
||||||
material = bpy.data.materials.new(f"Material_{len(mesh.materials)}")
|
|
||||||
|
|
||||||
material.use_nodes = True
|
|
||||||
for node in [node for node in material.node_tree.nodes]:
|
|
||||||
material.node_tree.nodes.remove(node)
|
|
||||||
|
|
||||||
principled_node: ShaderNodeBsdfPrincipled = material.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled")
|
|
||||||
principled_node.location.x = 7.29706335067749
|
|
||||||
principled_node.location.y = 298.918212890625
|
|
||||||
principled_node.inputs["Base Color"].default_value = material_data[0]
|
|
||||||
principled_node.inputs["Specular Tint"].default_value = [material_data[1][0],material_data[1][1],material_data[1][2],1.0]
|
|
||||||
principled_node.inputs["Specular IOR Level"].default_value = material_data[2]
|
|
||||||
|
|
||||||
output_node: ShaderNodeOutputMaterial = material.node_tree.nodes.new(type="ShaderNodeOutputMaterial")
|
|
||||||
output_node.location.x = 297.29705810546875
|
|
||||||
output_node.location.y = 298.918212890625
|
|
||||||
|
|
||||||
albedo_node: ShaderNodeTexImage = material.node_tree.nodes.new(type="ShaderNodeTexImage")
|
|
||||||
albedo_node.location.x = -588.6177978515625
|
|
||||||
albedo_node.location.y = 414.1948547363281
|
|
||||||
|
|
||||||
if texture_file_name in bpy.data.images:
|
|
||||||
albedo_node.image = bpy.data.images[texture_file_name]
|
|
||||||
else:
|
|
||||||
albedo_node.image = bpy.data.images.new(name=texture_file_name,width=32,height=32)
|
|
||||||
albedo_node.image.filepath = os.path.join(os.path.dirname(filepath),texture_file_name)
|
|
||||||
albedo_node.image.source = 'FILE'
|
|
||||||
albedo_node.image.reload()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
material.node_tree.links.new(principled_node.inputs["Base Color"], albedo_node.outputs["Color"])
|
|
||||||
material.node_tree.links.new(principled_node.inputs["Alpha"], albedo_node.outputs["Alpha"])
|
|
||||||
material.node_tree.links.new(output_node.inputs["Surface"], principled_node.outputs["BSDF"])
|
|
||||||
|
|
||||||
#material.ambient = material_data[5] #TODO: this doesn't exist
|
|
||||||
# Set other material properties based on the PMX data
|
|
||||||
if not (material.name in mesh.materials):
|
|
||||||
mesh.materials.append(material)
|
|
||||||
|
|
||||||
#surprised this works - @989onan
|
|
||||||
end: int = cur_polygon_index+material_data[15]-1
|
|
||||||
for face in mesh.polygons.items()[cur_polygon_index:end]:
|
|
||||||
face[1].material_index = mesh.materials.find(material.name)
|
|
||||||
|
|
||||||
cur_polygon_index = cur_polygon_index+material_data[15]
|
|
||||||
# Set other material properties based on the PMD data
|
|
||||||
|
|
||||||
# Create armature and assign bones
|
|
||||||
armature = bpy.data.armatures.new(model_name + "_Armature")
|
|
||||||
armature_obj = bpy.data.objects.new(model_name + "_Armature", armature)
|
|
||||||
bpy.context.collection.objects.link(armature_obj)
|
|
||||||
|
|
||||||
bpy.context.view_layer.objects.active = armature_obj
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
|
|
||||||
for bone_data in bones:
|
|
||||||
bone = armature.edit_bones.new(bone_data[0])
|
|
||||||
bone.head = bone_data[5]
|
|
||||||
|
|
||||||
if bone_data[1] != -1:
|
|
||||||
parent_bone = armature.edit_bones[bone_data[1]]
|
|
||||||
bone.parent = parent_bone
|
|
||||||
bone.tail = parent_bone.head
|
|
||||||
else:
|
|
||||||
bone.tail = bone.head + mathutils.Vector((0, 0.1, 0))
|
|
||||||
|
|
||||||
# Set other bone properties based on the PMD data
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
# Assign bone weights to the mesh
|
|
||||||
for i, vertex in enumerate(vertices):
|
|
||||||
for j in range(2):
|
|
||||||
if vertex[3][j] != 65535:
|
|
||||||
bone_name = bones[vertex[3][j]][0]
|
|
||||||
weight = vertex[4] if j == 0 else 1 - vertex[4]
|
|
||||||
|
|
||||||
vertex_group = obj.vertex_groups.get(bone_name)
|
|
||||||
if not vertex_group:
|
|
||||||
vertex_group = obj.vertex_groups.new(name=bone_name)
|
|
||||||
|
|
||||||
vertex_group.add([i], weight, 'REPLACE')
|
|
||||||
|
|
||||||
# Assign IK constraints to bones
|
|
||||||
for ik_data in iks:
|
|
||||||
ik_bone = armature.bones[bones[ik_data[0]][0]]
|
|
||||||
ik_target_bone = armature.bones[bones[ik_data[1]][0]]
|
|
||||||
|
|
||||||
ik_constraint = ik_bone.constraints.new('IK')
|
|
||||||
ik_constraint.target = armature_obj
|
|
||||||
ik_constraint.subtarget = ik_target_bone.name
|
|
||||||
ik_constraint.chain_count = ik_data[2]
|
|
||||||
ik_constraint.iterations = ik_data[3]
|
|
||||||
ik_constraint.limit_mode = 'LIMITDIST_INSIDE'
|
|
||||||
ik_constraint.limit_mode_max_x = ik_data[4]
|
|
||||||
|
|
||||||
# Assign morphs to the mesh
|
|
||||||
for morph_data in morphs:
|
|
||||||
morph_name = morph_data[0]
|
|
||||||
morph_type = morph_data[2]
|
|
||||||
|
|
||||||
if morph_type == 0: # Vertex morph
|
|
||||||
shape_key = obj.shape_key_add(name=morph_name)
|
|
||||||
for vertex_data in morph_data[3]:
|
|
||||||
vertex_index = vertex_data[0]
|
|
||||||
vertex_offset = vertex_data[1]
|
|
||||||
shape_key.data[vertex_index].co += mathutils.Vector(vertex_offset)
|
|
||||||
|
|
||||||
print(f"Successfully imported PMD file: {filepath}")
|
|
||||||
print(f"Model Name: {model_name}")
|
|
||||||
print(f"Comment: {comment}")
|
|
||||||
except Exception:
|
|
||||||
print(f"Error importing PMD file: {filepath}")
|
|
||||||
print(f"Error details: {traceback.format_exc()}")
|
|
||||||
@@ -1,861 +0,0 @@
|
|||||||
from io import BufferedReader
|
|
||||||
import os
|
|
||||||
import bpy
|
|
||||||
import struct
|
|
||||||
import traceback
|
|
||||||
import mathutils
|
|
||||||
from mathutils import Matrix, Vector
|
|
||||||
|
|
||||||
class PMXVertex:
|
|
||||||
def __init__(self, position, normal, uv, bone_indices, bone_weights, edge_scale, additional_uvs):
|
|
||||||
self.position = position
|
|
||||||
self.normal = normal
|
|
||||||
self.uv = uv
|
|
||||||
self.bone_indices = bone_indices
|
|
||||||
self.bone_weights = bone_weights
|
|
||||||
self.edge_scale = edge_scale
|
|
||||||
self.additional_uvs = additional_uvs
|
|
||||||
|
|
||||||
class PMXBone:
|
|
||||||
def __init__(self, name, english_name, position, parent_index, layer, flag,
|
|
||||||
tail_position, inherit_parent_index, inherit_influence,
|
|
||||||
fixed_axis, local_x, local_z, external_key,
|
|
||||||
ik_target_index, ik_loop_count, ik_limit_rad, ik_links):
|
|
||||||
self.name = name
|
|
||||||
self.english_name = english_name
|
|
||||||
self.position = position
|
|
||||||
self.parent_index = parent_index
|
|
||||||
self.layer = layer
|
|
||||||
self.flag = flag
|
|
||||||
self.tail_position = tail_position
|
|
||||||
self.inherit_parent_index = inherit_parent_index
|
|
||||||
self.inherit_influence = inherit_influence
|
|
||||||
self.fixed_axis = fixed_axis
|
|
||||||
self.local_x = local_x
|
|
||||||
self.local_z = local_z
|
|
||||||
self.external_key = external_key
|
|
||||||
self.ik_target_index = ik_target_index
|
|
||||||
self.ik_loop_count = ik_loop_count
|
|
||||||
self.ik_limit_rad = ik_limit_rad
|
|
||||||
self.ik_links = ik_links
|
|
||||||
|
|
||||||
class PMXMaterial:
|
|
||||||
def __init__(self, name, english_name, diffuse, specular, specular_strength,
|
|
||||||
ambient, flag, edge_color, edge_size, texture_index,
|
|
||||||
sphere_texture_index, sphere_mode, toon_sharing_flag,
|
|
||||||
toon_texture_index, comment, surface_count):
|
|
||||||
self.name = name
|
|
||||||
self.english_name = english_name
|
|
||||||
self.diffuse = diffuse
|
|
||||||
self.specular = specular
|
|
||||||
self.specular_strength = specular_strength
|
|
||||||
self.ambient = ambient
|
|
||||||
self.flag = flag
|
|
||||||
self.edge_color = edge_color
|
|
||||||
self.edge_size = edge_size
|
|
||||||
self.texture_index = texture_index
|
|
||||||
self.sphere_texture_index = sphere_texture_index
|
|
||||||
self.sphere_mode = sphere_mode
|
|
||||||
self.toon_sharing_flag = toon_sharing_flag
|
|
||||||
self.toon_texture_index = toon_texture_index
|
|
||||||
self.comment = comment
|
|
||||||
self.surface_count = surface_count
|
|
||||||
|
|
||||||
class PMXMorph:
|
|
||||||
def __init__(self, name, english_name, panel, morph_type, offsets):
|
|
||||||
self.name = name
|
|
||||||
self.english_name = english_name
|
|
||||||
self.panel = panel
|
|
||||||
self.morph_type = morph_type
|
|
||||||
self.offsets = offsets
|
|
||||||
|
|
||||||
class PMXRigidBody:
|
|
||||||
def __init__(self, name, bone_index, group, shape_type, size, position, rotation, mass, linear_damping, angular_damping, restitution, friction, mode):
|
|
||||||
self.name = name
|
|
||||||
self.bone_index = bone_index
|
|
||||||
self.group = group
|
|
||||||
self.shape_type = shape_type
|
|
||||||
self.size = size
|
|
||||||
self.position = position
|
|
||||||
self.rotation = rotation
|
|
||||||
self.mass = mass
|
|
||||||
self.linear_damping = linear_damping
|
|
||||||
self.angular_damping = angular_damping
|
|
||||||
self.restitution = restitution
|
|
||||||
self.friction = friction
|
|
||||||
self.mode = mode
|
|
||||||
|
|
||||||
class PMXJoint:
|
|
||||||
def __init__(self, name, joint_type, rigid_body_a, rigid_body_b, position, rotation, linear_limit_min, linear_limit_max, angular_limit_min, angular_limit_max, spring_constant_translation, spring_constant_rotation):
|
|
||||||
self.name = name
|
|
||||||
self.joint_type = joint_type
|
|
||||||
self.rigid_body_a = rigid_body_a
|
|
||||||
self.rigid_body_b = rigid_body_b
|
|
||||||
self.position = position
|
|
||||||
self.rotation = rotation
|
|
||||||
self.linear_limit_min = linear_limit_min
|
|
||||||
self.linear_limit_max = linear_limit_max
|
|
||||||
self.angular_limit_min = angular_limit_min
|
|
||||||
self.angular_limit_max = angular_limit_max
|
|
||||||
self.spring_constant_translation = spring_constant_translation
|
|
||||||
self.spring_constant_rotation = spring_constant_rotation
|
|
||||||
|
|
||||||
def read_pmx_header(file: BufferedReader):
|
|
||||||
magic = file.read(4)
|
|
||||||
if magic != b'PMX ':
|
|
||||||
raise ValueError("Invalid PMX file")
|
|
||||||
|
|
||||||
version = struct.unpack('<f', file.read(4))[0]
|
|
||||||
data_size = struct.unpack('<b', file.read(1))[0]
|
|
||||||
encoding = struct.unpack('<b', file.read(1))[0]
|
|
||||||
additional_uvs = struct.unpack('<b', file.read(1))[0]
|
|
||||||
vertex_index_size = struct.unpack('<b', file.read(1))[0]
|
|
||||||
texture_index_size = struct.unpack('<b', file.read(1))[0]
|
|
||||||
material_index_size = struct.unpack('<b', file.read(1))[0]
|
|
||||||
bone_index_size = struct.unpack('<b', file.read(1))[0]
|
|
||||||
morph_index_size = struct.unpack('<b', file.read(1))[0]
|
|
||||||
rigid_body_index_size = struct.unpack('<b', file.read(1))[0]
|
|
||||||
|
|
||||||
model_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
|
|
||||||
model_english_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
|
|
||||||
model_comment = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
|
|
||||||
model_english_comment = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
|
|
||||||
|
|
||||||
return (version, encoding, additional_uvs, vertex_index_size, texture_index_size,
|
|
||||||
material_index_size, bone_index_size, morph_index_size, rigid_body_index_size,
|
|
||||||
model_name, model_english_name, model_comment, model_english_comment)
|
|
||||||
|
|
||||||
def read_index_size(index, types):
|
|
||||||
struct_format = "<??"
|
|
||||||
byte_size = 0
|
|
||||||
if index == 1:
|
|
||||||
struct_format = replace_char(struct_format, 2, types[0])
|
|
||||||
byte_size = 1
|
|
||||||
elif index == 2:
|
|
||||||
struct_format = replace_char(struct_format, 2, types[1])
|
|
||||||
byte_size = 2
|
|
||||||
else:
|
|
||||||
struct_format = replace_char(struct_format, 2, types[2])
|
|
||||||
byte_size = 4
|
|
||||||
|
|
||||||
return struct_format, byte_size
|
|
||||||
|
|
||||||
def replace_char(string, index, character):
|
|
||||||
temp = list(string)
|
|
||||||
temp[index] = character
|
|
||||||
return "".join(temp)
|
|
||||||
|
|
||||||
def read_morph(file: BufferedReader, vertex_struct, vertex_size):
|
|
||||||
try:
|
|
||||||
name_length = struct.unpack('<i', file.read(4))[0]
|
|
||||||
name = str(file.read(name_length), 'utf-16-le', errors='replace')
|
|
||||||
|
|
||||||
english_name_length = struct.unpack('<i', file.read(4))[0]
|
|
||||||
english_name = str(file.read(english_name_length), 'utf-16-le', errors='replace')
|
|
||||||
|
|
||||||
panel = int.from_bytes(file.read(1), byteorder='little', signed=True)
|
|
||||||
morph_type = int.from_bytes(file.read(1), byteorder='little', signed=True)
|
|
||||||
|
|
||||||
# Read offset count with error checking
|
|
||||||
offset_count_bytes = file.read(4)
|
|
||||||
if len(offset_count_bytes) != 4:
|
|
||||||
return PMXMorph(name, english_name, panel, morph_type, [])
|
|
||||||
|
|
||||||
offset_count = struct.unpack('<i', offset_count_bytes)[0]
|
|
||||||
|
|
||||||
offsets = []
|
|
||||||
if morph_type == 1: # Vertex morph
|
|
||||||
for _ in range(offset_count):
|
|
||||||
vertex_index = struct.unpack(replace_char(vertex_struct, 1, '1'), file.read(vertex_size))[0]
|
|
||||||
offset = struct.unpack('<3f', file.read(12))
|
|
||||||
offsets.append((vertex_index, offset))
|
|
||||||
|
|
||||||
return PMXMorph(name, english_name, panel, morph_type, offsets)
|
|
||||||
except:
|
|
||||||
return PMXMorph("", "", 0, 0, [])
|
|
||||||
|
|
||||||
def validate_pmx_data(header_data, vertices, faces, materials, bones):
|
|
||||||
"""Validate PMX data integrity"""
|
|
||||||
if not vertices:
|
|
||||||
raise ValueError("No vertices found in PMX file")
|
|
||||||
if not faces:
|
|
||||||
raise ValueError("No faces found in PMX file")
|
|
||||||
if not materials:
|
|
||||||
raise ValueError("No materials found in PMX file")
|
|
||||||
if not bones:
|
|
||||||
raise ValueError("No bones found in PMX file")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def handle_import_error(context, error_msg):
|
|
||||||
"""Handle import errors with user feedback"""
|
|
||||||
context.window_manager.progress_end()
|
|
||||||
bpy.ops.ui.popup_menu(message=error_msg)
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
def read_vertex(file: BufferedReader, string_build, byte_size, additional_uvs):
|
|
||||||
position = struct.unpack('<3f', file.read(12))
|
|
||||||
normal = struct.unpack('<3f', file.read(12))
|
|
||||||
uv = struct.unpack('<2f', file.read(8))
|
|
||||||
uv = [uv[0], (1.0-uv[1])-1.0]
|
|
||||||
|
|
||||||
additional_uv_read = []
|
|
||||||
for _ in range(additional_uvs):
|
|
||||||
additional_uv_read.append(struct.unpack('<4f', file.read(16)))
|
|
||||||
|
|
||||||
weight_deform_type = struct.unpack('<B', file.read(1))[0]
|
|
||||||
|
|
||||||
bone_indices = []
|
|
||||||
bone_weights = []
|
|
||||||
|
|
||||||
if weight_deform_type == 0: # BDEF1
|
|
||||||
string_build = replace_char(string_build, 1, '1')
|
|
||||||
bone_indices = list(struct.unpack(string_build, file.read(byte_size*1)))
|
|
||||||
bone_weights = [1.0]
|
|
||||||
elif weight_deform_type == 1: # BDEF2
|
|
||||||
string_build = replace_char(string_build, 1, '2')
|
|
||||||
bone_indices = list(struct.unpack(string_build, file.read(byte_size*2)))
|
|
||||||
weight = struct.unpack('<f', file.read(4))[0]
|
|
||||||
bone_weights = [weight, 1.0-weight]
|
|
||||||
elif weight_deform_type == 2: # BDEF4
|
|
||||||
string_build = replace_char(string_build, 1, '4')
|
|
||||||
bone_indices = list(struct.unpack(string_build, file.read(byte_size*4)))
|
|
||||||
bone_weights = list(struct.unpack('<4f', file.read(16)))
|
|
||||||
elif weight_deform_type == 3: # SDEF
|
|
||||||
string_build = replace_char(string_build, 1, '2')
|
|
||||||
bone_indices = list(struct.unpack(string_build, file.read(byte_size*2)))
|
|
||||||
weight = struct.unpack('<f', file.read(4))[0]
|
|
||||||
bone_weights = [weight, 1.0-weight]
|
|
||||||
# Skip SDEF data as we don't use it
|
|
||||||
file.read(36) # 3 vectors of 3 floats each (C, R0, R1)
|
|
||||||
elif weight_deform_type == 4: # QDEF
|
|
||||||
string_build = replace_char(string_build, 1, '4')
|
|
||||||
bone_indices = list(struct.unpack(string_build, file.read(byte_size*4)))
|
|
||||||
bone_weights = list(struct.unpack('<4f', file.read(16)))
|
|
||||||
|
|
||||||
edge_scale = struct.unpack('<f', file.read(4))[0]
|
|
||||||
|
|
||||||
return PMXVertex(position, normal, uv, bone_indices, bone_weights, edge_scale, additional_uv_read)
|
|
||||||
|
|
||||||
def read_material(file: BufferedReader, string_build, byte_size):
|
|
||||||
material_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
|
|
||||||
material_english_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
|
|
||||||
|
|
||||||
diffuse_color = struct.unpack('<4f', file.read(16))
|
|
||||||
specular_color = struct.unpack('<3f', file.read(12))
|
|
||||||
specular_strength = struct.unpack('<f', file.read(4))[0]
|
|
||||||
ambient_color = struct.unpack('<3f', file.read(12))
|
|
||||||
|
|
||||||
flag = struct.unpack('<b', file.read(1))[0]
|
|
||||||
edge_color = struct.unpack('<4f', file.read(16))
|
|
||||||
edge_size = struct.unpack('<f', file.read(4))[0]
|
|
||||||
|
|
||||||
texture_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
|
|
||||||
sphere_texture_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
|
|
||||||
sphere_mode = struct.unpack('<b', file.read(1))[0]
|
|
||||||
toon_sharing_flag = struct.unpack('<b', file.read(1))[0]
|
|
||||||
|
|
||||||
if toon_sharing_flag == 0:
|
|
||||||
toon_texture_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
|
|
||||||
else:
|
|
||||||
toon_texture_index = struct.unpack('<b', file.read(1))[0]
|
|
||||||
|
|
||||||
comment = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
|
|
||||||
surface_count = int(struct.unpack('<i', file.read(4))[0]/3)
|
|
||||||
|
|
||||||
return PMXMaterial(material_name, material_english_name, diffuse_color, specular_color,
|
|
||||||
specular_strength, ambient_color, flag, edge_color, edge_size,
|
|
||||||
texture_index, sphere_texture_index, sphere_mode,
|
|
||||||
toon_sharing_flag, toon_texture_index, comment, surface_count)
|
|
||||||
|
|
||||||
def create_material_nodes(material: bpy.types.Material, texture_path: str, diffuse_color, specular_color, specular_strength, toon_texture_path=None):
|
|
||||||
material.use_nodes = True
|
|
||||||
nodes = material.node_tree.nodes
|
|
||||||
links = material.node_tree.links
|
|
||||||
|
|
||||||
nodes.clear()
|
|
||||||
|
|
||||||
principled = nodes.new("ShaderNodeBsdfPrincipled")
|
|
||||||
principled.location = (0, 0)
|
|
||||||
principled.inputs["Base Color"].default_value = diffuse_color
|
|
||||||
principled.inputs["Specular IOR Level"].default_value = specular_strength
|
|
||||||
principled.inputs["Specular Tint"].default_value = (*specular_color, 1.0)
|
|
||||||
|
|
||||||
# Handle transparency
|
|
||||||
if diffuse_color[3] < 1.0:
|
|
||||||
material.blend_method = 'HASHED'
|
|
||||||
principled.inputs["Alpha"].default_value = diffuse_color[3]
|
|
||||||
|
|
||||||
output = nodes.new("ShaderNodeOutputMaterial")
|
|
||||||
output.location = (300, 0)
|
|
||||||
|
|
||||||
# Main texture
|
|
||||||
if texture_path and os.path.exists(texture_path):
|
|
||||||
texture = nodes.new("ShaderNodeTexImage")
|
|
||||||
texture.location = (-300, 0)
|
|
||||||
texture.image = bpy.data.images.load(texture_path)
|
|
||||||
links.new(texture.outputs["Color"], principled.inputs["Base Color"])
|
|
||||||
links.new(texture.outputs["Alpha"], principled.inputs["Alpha"])
|
|
||||||
|
|
||||||
# Toon texture
|
|
||||||
if toon_texture_path and os.path.exists(toon_texture_path):
|
|
||||||
toon = nodes.new("ShaderNodeTexImage")
|
|
||||||
toon.location = (-300, -300)
|
|
||||||
toon.image = bpy.data.images.load(toon_texture_path)
|
|
||||||
mix = nodes.new("ShaderNodeMixRGB")
|
|
||||||
mix.location = (-50, -150)
|
|
||||||
mix.blend_type = 'MULTIPLY'
|
|
||||||
links.new(toon.outputs["Color"], mix.inputs[2])
|
|
||||||
links.new(mix.outputs["Color"], principled.inputs["Base Color"])
|
|
||||||
|
|
||||||
links.new(principled.outputs["BSDF"], output.inputs["Surface"])
|
|
||||||
|
|
||||||
def read_bone(file: BufferedReader, string_build, byte_size):
|
|
||||||
bone_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
|
|
||||||
bone_english_name = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
|
|
||||||
|
|
||||||
position = struct.unpack('<3f', file.read(12))
|
|
||||||
parent_bone_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
|
|
||||||
layer = struct.unpack('<i', file.read(4))[0]
|
|
||||||
flag = struct.unpack('<H', file.read(2))[0]
|
|
||||||
|
|
||||||
tail_position = [None, None, None]
|
|
||||||
inherit_bone_parent_index = 0
|
|
||||||
inherit_bone_parent_influence = 0.0
|
|
||||||
fixed_axis = [0.0, 0.0, 0.0]
|
|
||||||
local_x_vector = [0.0, 0.0, 0.0]
|
|
||||||
local_z_vector = [0.0, 0.0, 0.0]
|
|
||||||
external_key = 0
|
|
||||||
ik_target_bone_index = 0
|
|
||||||
ik_loop_count = -1
|
|
||||||
ik_limit_radian = 0.0
|
|
||||||
ik_links = []
|
|
||||||
|
|
||||||
if not (flag & 0x0001):
|
|
||||||
tail_position = struct.unpack('<3f', file.read(12))
|
|
||||||
else:
|
|
||||||
tail_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
|
|
||||||
|
|
||||||
if flag & 0x0100 or flag & 0x0200:
|
|
||||||
inherit_bone_parent_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
|
|
||||||
inherit_bone_parent_influence = struct.unpack('<f', file.read(4))[0]
|
|
||||||
|
|
||||||
if flag & 0x0400:
|
|
||||||
fixed_axis = struct.unpack('<3f', file.read(12))
|
|
||||||
|
|
||||||
if flag & 0x0800:
|
|
||||||
local_x_vector = struct.unpack('<3f', file.read(12))
|
|
||||||
local_z_vector = struct.unpack('<3f', file.read(12))
|
|
||||||
|
|
||||||
if flag & 0x2000:
|
|
||||||
external_key = struct.unpack('<i', file.read(4))[0]
|
|
||||||
|
|
||||||
if flag & 0x0020:
|
|
||||||
ik_target_bone_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
|
|
||||||
ik_loop_count = struct.unpack('<i', file.read(4))[0]
|
|
||||||
ik_limit_radian = struct.unpack('<f', file.read(4))[0]
|
|
||||||
ik_link_count = struct.unpack('<i', file.read(4))[0]
|
|
||||||
|
|
||||||
for _ in range(ik_link_count):
|
|
||||||
ik_link_bone_index = struct.unpack(replace_char(string_build, 1, '1'), file.read(byte_size))[0]
|
|
||||||
ik_link_limit = struct.unpack('<b', file.read(1))[0]
|
|
||||||
if ik_link_limit == 1:
|
|
||||||
angle_limit = (struct.unpack('<3f', file.read(12)), struct.unpack('<3f', file.read(12)))
|
|
||||||
ik_links.append((ik_link_bone_index, True, angle_limit))
|
|
||||||
else:
|
|
||||||
ik_links.append((ik_link_bone_index, False, None))
|
|
||||||
|
|
||||||
return PMXBone(bone_name, bone_english_name, position, parent_bone_index, layer,
|
|
||||||
flag, tail_position, inherit_bone_parent_index, inherit_bone_parent_influence,
|
|
||||||
fixed_axis, local_x_vector, local_z_vector, external_key,
|
|
||||||
ik_target_bone_index, ik_loop_count, ik_limit_radian, ik_links)
|
|
||||||
|
|
||||||
def create_bone_constraints(armature_obj: bpy.types.Object, bones: list[PMXBone]):
|
|
||||||
bpy.context.view_layer.objects.active = armature_obj
|
|
||||||
bpy.ops.object.mode_set(mode='POSE')
|
|
||||||
|
|
||||||
# Clear existing constraints
|
|
||||||
for pose_bone in armature_obj.pose.bones:
|
|
||||||
while pose_bone.constraints:
|
|
||||||
pose_bone.constraints.remove(pose_bone.constraints[0])
|
|
||||||
|
|
||||||
# Handle rotation inheritance first
|
|
||||||
for bone_data in bones:
|
|
||||||
pose_bone = armature_obj.pose.bones.get(bone_data.name)
|
|
||||||
if not pose_bone or bone_data.parent_index < 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Check if bone has vertex groups
|
|
||||||
if not pose_bone.bone.use_deform:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if bone_data.flag & 0x0100: # Rotation inheritance
|
|
||||||
if bone_data.inherit_parent_index >= 0:
|
|
||||||
constraint = pose_bone.constraints.new('COPY_ROTATION')
|
|
||||||
constraint.name = "MMD Rotation"
|
|
||||||
constraint.target = armature_obj
|
|
||||||
constraint.subtarget = bones[bone_data.inherit_parent_index].name
|
|
||||||
constraint.influence = bone_data.inherit_influence
|
|
||||||
constraint.target_space = 'LOCAL'
|
|
||||||
constraint.owner_space = 'LOCAL'
|
|
||||||
|
|
||||||
# Then handle IK constraints
|
|
||||||
for bone_data in bones:
|
|
||||||
pose_bone = armature_obj.pose.bones.get(bone_data.name)
|
|
||||||
if not pose_bone:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Skip non-deforming bones
|
|
||||||
if not pose_bone.bone.use_deform:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if bone_data.flag & 0x0020: # IK
|
|
||||||
if bone_data.ik_target_index >= 0:
|
|
||||||
constraint = pose_bone.constraints.new('IK')
|
|
||||||
constraint.name = "MMD IK"
|
|
||||||
constraint.target = armature_obj
|
|
||||||
constraint.subtarget = bones[bone_data.ik_target_index].name
|
|
||||||
constraint.chain_count = min(len(bone_data.ik_links), 3)
|
|
||||||
constraint.iterations = min(bone_data.ik_loop_count, 8)
|
|
||||||
constraint.use_tail = False
|
|
||||||
constraint.use_stretch = False
|
|
||||||
|
|
||||||
# Configure IK chain
|
|
||||||
for link_bone_index, has_limits, angle_limits in bone_data.ik_links:
|
|
||||||
link_pose_bone = armature_obj.pose.bones.get(bones[link_bone_index].name)
|
|
||||||
if link_pose_bone and link_pose_bone.bone.use_deform:
|
|
||||||
link_pose_bone.rotation_mode = 'XYZ'
|
|
||||||
link_pose_bone.use_ik_limit_x = True
|
|
||||||
link_pose_bone.use_ik_limit_y = True
|
|
||||||
link_pose_bone.use_ik_limit_z = True
|
|
||||||
|
|
||||||
if has_limits and angle_limits:
|
|
||||||
min_angles, max_angles = angle_limits
|
|
||||||
link_pose_bone.ik_min_x = max(-1.4, min_angles[0])
|
|
||||||
link_pose_bone.ik_max_x = min(1.4, max_angles[0])
|
|
||||||
link_pose_bone.ik_min_y = max(-1.4, min_angles[1])
|
|
||||||
link_pose_bone.ik_max_y = min(1.4, max_angles[1])
|
|
||||||
link_pose_bone.ik_min_z = max(-1.4, min_angles[2])
|
|
||||||
link_pose_bone.ik_max_z = min(1.4, max_angles[2])
|
|
||||||
|
|
||||||
# Reset pose to default state
|
|
||||||
bpy.ops.pose.select_all(action='SELECT')
|
|
||||||
bpy.ops.pose.transforms_clear()
|
|
||||||
bpy.ops.pose.select_all(action='DESELECT')
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
def setup_physics(obj: bpy.types.Object, armature_obj: bpy.types.Object, rigid_bodies: list[PMXRigidBody], joints: list[PMXJoint]):
|
|
||||||
"""Set up physics for PMX model"""
|
|
||||||
# Create rigid body collection if it doesn't exist
|
|
||||||
if 'RigidBodies' not in bpy.data.collections:
|
|
||||||
rigid_body_collection = bpy.data.collections.new('RigidBodies')
|
|
||||||
bpy.context.scene.collection.children.link(rigid_body_collection)
|
|
||||||
else:
|
|
||||||
rigid_body_collection = bpy.data.collections['RigidBodies']
|
|
||||||
|
|
||||||
# Create rigid bodies
|
|
||||||
for rb in rigid_bodies:
|
|
||||||
# Create mesh based on shape type
|
|
||||||
if rb.shape_type == 0: # Sphere
|
|
||||||
bpy.ops.mesh.primitive_uv_sphere_add(radius=rb.size[0])
|
|
||||||
elif rb.shape_type == 1: # Box
|
|
||||||
bpy.ops.mesh.primitive_cube_add()
|
|
||||||
bpy.context.active_object.scale = rb.size
|
|
||||||
elif rb.shape_type == 2: # Capsule
|
|
||||||
bpy.ops.mesh.primitive_cylinder_add(radius=rb.size[0], depth=rb.size[1])
|
|
||||||
|
|
||||||
rb_obj = bpy.context.active_object
|
|
||||||
rb_obj.name = f"RB_{rb.name}"
|
|
||||||
rb_obj.location = rb.position
|
|
||||||
rb_obj.rotation_euler = rb.rotation
|
|
||||||
|
|
||||||
# Set up rigid body physics
|
|
||||||
rb_obj.rigid_body.type = 'ACTIVE' if rb.mode == 0 else 'PASSIVE'
|
|
||||||
rb_obj.rigid_body.mass = rb.mass
|
|
||||||
rb_obj.rigid_body.linear_damping = rb.linear_damping
|
|
||||||
rb_obj.rigid_body.angular_damping = rb.angular_damping
|
|
||||||
rb_obj.rigid_body.restitution = rb.restitution
|
|
||||||
rb_obj.rigid_body.friction = rb.friction
|
|
||||||
|
|
||||||
# Parent to bone if specified
|
|
||||||
if rb.bone_index >= 0:
|
|
||||||
rb_obj.parent = armature_obj
|
|
||||||
rb_obj.parent_type = 'BONE'
|
|
||||||
rb_obj.parent_bone = bones[rb.bone_index].name
|
|
||||||
|
|
||||||
# Move to rigid body collection
|
|
||||||
rigid_body_collection.objects.link(rb_obj)
|
|
||||||
bpy.context.scene.collection.objects.unlink(rb_obj)
|
|
||||||
|
|
||||||
# Create joints
|
|
||||||
for joint in joints:
|
|
||||||
empty = bpy.data.objects.new(f"Joint_{joint.name}", None)
|
|
||||||
empty.empty_display_type = 'ARROWS'
|
|
||||||
empty.location = joint.position
|
|
||||||
empty.rotation_euler = joint.rotation
|
|
||||||
bpy.context.scene.collection.objects.link(empty)
|
|
||||||
|
|
||||||
# Set up constraint
|
|
||||||
constraint = empty.constraints.new('RIGID_BODY_JOINT')
|
|
||||||
constraint.target = rigid_bodies[joint.rigid_body_a]
|
|
||||||
constraint.child = rigid_bodies[joint.rigid_body_b]
|
|
||||||
constraint.use_limit_lin_x = True
|
|
||||||
constraint.use_limit_lin_y = True
|
|
||||||
constraint.use_limit_lin_z = True
|
|
||||||
constraint.use_limit_ang_x = True
|
|
||||||
constraint.use_limit_ang_y = True
|
|
||||||
constraint.use_limit_ang_z = True
|
|
||||||
|
|
||||||
# Set limits
|
|
||||||
constraint.limit_lin_x_lower = joint.linear_limit_min[0]
|
|
||||||
constraint.limit_lin_x_upper = joint.linear_limit_max[0]
|
|
||||||
constraint.limit_lin_y_lower = joint.linear_limit_min[1]
|
|
||||||
constraint.limit_lin_y_upper = joint.linear_limit_max[1]
|
|
||||||
constraint.limit_lin_z_lower = joint.linear_limit_min[2]
|
|
||||||
constraint.limit_lin_z_upper = joint.linear_limit_max[2]
|
|
||||||
constraint.limit_ang_x_lower = joint.angular_limit_min[0]
|
|
||||||
constraint.limit_ang_x_upper = joint.angular_limit_max[0]
|
|
||||||
constraint.limit_ang_y_lower = joint.angular_limit_min[1]
|
|
||||||
constraint.limit_ang_y_upper = joint.angular_limit_max[1]
|
|
||||||
constraint.limit_ang_z_lower = joint.angular_limit_min[2]
|
|
||||||
constraint.limit_ang_z_upper = joint.angular_limit_max[2]
|
|
||||||
|
|
||||||
def create_armature(model_name: str, bones: list[PMXBone]) -> bpy.types.Object:
|
|
||||||
# Handle CJK characters in model name
|
|
||||||
if isinstance(model_name, bytes):
|
|
||||||
try:
|
|
||||||
model_name = model_name.decode('gbk') # Try Chinese encoding first
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
try:
|
|
||||||
model_name = model_name.decode('utf-8')
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
try:
|
|
||||||
model_name = model_name.decode('shift-jis')
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
model_name = model_name.decode('latin1')
|
|
||||||
|
|
||||||
armature = bpy.data.armatures.new(f"{model_name}_Armature")
|
|
||||||
armature_obj = bpy.data.objects.new(f"{model_name}_Armature", armature)
|
|
||||||
bpy.context.collection.objects.link(armature_obj)
|
|
||||||
|
|
||||||
bpy.context.view_layer.objects.active = armature_obj
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
|
|
||||||
# First pass: Create bones with proper names and types
|
|
||||||
edit_bones = []
|
|
||||||
for i, bone_data in enumerate(bones):
|
|
||||||
bone_name = bone_data.name if bone_data.name else bone_data.english_name
|
|
||||||
if not bone_name:
|
|
||||||
bone_name = f"bone_{i}"
|
|
||||||
|
|
||||||
edit_bone = armature.edit_bones.new(bone_name)
|
|
||||||
edit_bone.head = Vector(bone_data.position)
|
|
||||||
|
|
||||||
# Handle different bone types based on flags and names
|
|
||||||
is_expression = bool(bone_data.flag & 0x0004)
|
|
||||||
is_rotation_influenced = bool(bone_data.flag & 0x0100)
|
|
||||||
is_ik = bool(bone_data.flag & 0x0020)
|
|
||||||
is_twist = "twist" in bone_name.lower()
|
|
||||||
|
|
||||||
if is_twist:
|
|
||||||
# Twist bones need specific handling
|
|
||||||
parent_pos = bones[bone_data.parent_index].position if bone_data.parent_index >= 0 else None
|
|
||||||
if parent_pos:
|
|
||||||
direction = Vector(bone_data.position) - Vector(parent_pos)
|
|
||||||
if direction.length > 0.001:
|
|
||||||
edit_bone.tail = edit_bone.head + direction.normalized() * 0.1
|
|
||||||
else:
|
|
||||||
edit_bone.tail = edit_bone.head + Vector((0, 0.05, 0))
|
|
||||||
else:
|
|
||||||
edit_bone.tail = edit_bone.head + Vector((0, 0.05, 0))
|
|
||||||
|
|
||||||
elif is_expression:
|
|
||||||
edit_bone.tail = edit_bone.head + Vector((0, 0.02, 0))
|
|
||||||
edit_bone.use_deform = False
|
|
||||||
|
|
||||||
elif is_ik:
|
|
||||||
if bone_data.ik_links:
|
|
||||||
target_pos = bones[bone_data.ik_links[0][0]].position
|
|
||||||
direction = Vector(target_pos) - Vector(edit_bone.head)
|
|
||||||
if direction.length > 0.001:
|
|
||||||
edit_bone.tail = edit_bone.head + direction.normalized() * 0.1
|
|
||||||
else:
|
|
||||||
edit_bone.tail = edit_bone.head + Vector((0, 0.1, 0))
|
|
||||||
else:
|
|
||||||
edit_bone.tail = edit_bone.head + Vector((0, 0.1, 0))
|
|
||||||
|
|
||||||
elif is_rotation_influenced:
|
|
||||||
# Handle rotation influenced bones
|
|
||||||
if bone_data.inherit_parent_index >= 0:
|
|
||||||
target_pos = bones[bone_data.inherit_parent_index].position
|
|
||||||
direction = Vector(target_pos) - Vector(edit_bone.head)
|
|
||||||
if direction.length > 0.001:
|
|
||||||
edit_bone.tail = edit_bone.head + direction.normalized() * 0.08
|
|
||||||
else:
|
|
||||||
edit_bone.tail = edit_bone.head + Vector((0, 0.08, 0))
|
|
||||||
else:
|
|
||||||
edit_bone.tail = edit_bone.head + Vector((0, 0.08, 0))
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Standard bones
|
|
||||||
if bone_data.tail_position[0] is not None:
|
|
||||||
edit_bone.tail = Vector(bone_data.tail_position)
|
|
||||||
else:
|
|
||||||
child_positions = [bones[j].position for j in range(len(bones))
|
|
||||||
if bones[j].parent_index == i]
|
|
||||||
if child_positions:
|
|
||||||
avg_child_pos = Vector((0, 0, 0))
|
|
||||||
for pos in child_positions:
|
|
||||||
avg_child_pos += Vector(pos)
|
|
||||||
avg_child_pos /= len(child_positions)
|
|
||||||
edit_bone.tail = avg_child_pos
|
|
||||||
else:
|
|
||||||
bone_length = 0.1 if bone_data.layer == 0 else 0.05
|
|
||||||
edit_bone.tail = edit_bone.head + Vector((0, bone_length, 0))
|
|
||||||
|
|
||||||
edit_bones.append(edit_bone)
|
|
||||||
|
|
||||||
# Second pass: Set up hierarchy and orientations
|
|
||||||
for i, bone_data in enumerate(bones):
|
|
||||||
edit_bone = edit_bones[i]
|
|
||||||
|
|
||||||
# Parent bones
|
|
||||||
if bone_data.parent_index >= 0:
|
|
||||||
parent_bone = edit_bones[bone_data.parent_index]
|
|
||||||
edit_bone.parent = parent_bone
|
|
||||||
|
|
||||||
# Connect bones only if they should be connected
|
|
||||||
if (Vector(bone_data.position) - Vector(parent_bone.tail)).length < 0.01:
|
|
||||||
edit_bone.use_connect = True
|
|
||||||
|
|
||||||
# Handle bone orientation
|
|
||||||
if bone_data.fixed_axis != [0.0, 0.0, 0.0]:
|
|
||||||
edit_bone.align_roll(Vector(bone_data.fixed_axis))
|
|
||||||
elif bone_data.local_x != [0.0, 0.0, 0.0]:
|
|
||||||
x_axis = Vector(bone_data.local_x).normalized()
|
|
||||||
z_axis = Vector(bone_data.local_z).normalized()
|
|
||||||
y_axis = z_axis.cross(x_axis)
|
|
||||||
|
|
||||||
# Create and apply orientation matrix
|
|
||||||
matrix_3x3 = Matrix((x_axis, y_axis, z_axis)).to_3x3()
|
|
||||||
matrix_4x4 = matrix_3x3.to_4x4()
|
|
||||||
edit_bone.matrix = matrix_4x4
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
return armature_obj
|
|
||||||
|
|
||||||
|
|
||||||
def assign_vertex_weights(obj: bpy.types.Object, vertices: list[PMXVertex], bones: list[PMXBone]):
|
|
||||||
# Pre-create vertex groups
|
|
||||||
vertex_groups = {}
|
|
||||||
for bone in bones:
|
|
||||||
vertex_groups[bone.name] = obj.vertex_groups.new(name=bone.name)
|
|
||||||
|
|
||||||
# Batch assign weights
|
|
||||||
for vertex_index, vertex in enumerate(vertices):
|
|
||||||
for bone_idx, weight in zip(vertex.bone_indices, vertex.bone_weights):
|
|
||||||
if bone_idx != -1 and weight > 0:
|
|
||||||
vertex_groups[bones[bone_idx].name].add([vertex_index], weight, 'REPLACE')
|
|
||||||
|
|
||||||
def assign_materials(obj: bpy.types.Object, materials: list[PMXMaterial], textures: list[str], base_path: str):
|
|
||||||
current_face_index = 0
|
|
||||||
|
|
||||||
for material in materials:
|
|
||||||
# Create or get material
|
|
||||||
mat_name = material.name or f"Material_{len(obj.data.materials)}"
|
|
||||||
if mat_name in bpy.data.materials:
|
|
||||||
mat = bpy.data.materials[mat_name]
|
|
||||||
else:
|
|
||||||
mat = bpy.data.materials.new(name=mat_name)
|
|
||||||
|
|
||||||
# Set up material nodes
|
|
||||||
texture_path = None
|
|
||||||
if material.texture_index >= 0 and material.texture_index < len(textures):
|
|
||||||
texture_path = os.path.join(base_path, textures[material.texture_index])
|
|
||||||
|
|
||||||
create_material_nodes(mat, texture_path, material.diffuse, material.specular,
|
|
||||||
material.specular_strength)
|
|
||||||
|
|
||||||
# Assign material to mesh
|
|
||||||
if mat.name not in obj.data.materials:
|
|
||||||
obj.data.materials.append(mat)
|
|
||||||
|
|
||||||
# Assign faces to material
|
|
||||||
mat_index = obj.data.materials.find(mat.name)
|
|
||||||
for face in obj.data.polygons[current_face_index:current_face_index + material.surface_count]:
|
|
||||||
face.material_index = mat_index
|
|
||||||
|
|
||||||
current_face_index += material.surface_count
|
|
||||||
|
|
||||||
def import_pmx(filepath: str):
|
|
||||||
wm = bpy.context.window_manager
|
|
||||||
wm.progress_begin(0, 100)
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(filepath, 'rb') as file:
|
|
||||||
# Read header (5%)
|
|
||||||
wm.progress_update(5)
|
|
||||||
header_data = read_pmx_header(file)
|
|
||||||
version, encoding, additional_uvs, vertex_index_size, texture_index_size, \
|
|
||||||
material_index_size, bone_index_size, morph_index_size, rigid_body_index_size, \
|
|
||||||
model_name, model_english_name, model_comment, model_english_comment = header_data
|
|
||||||
|
|
||||||
# Set up index size formats (10%)
|
|
||||||
wm.progress_update(10)
|
|
||||||
vertex_struct, vertex_size = read_index_size(vertex_index_size, 'BHi')
|
|
||||||
bone_struct, bone_size = read_index_size(bone_index_size, 'bhi')
|
|
||||||
texture_struct, texture_size = read_index_size(texture_index_size, 'bhi')
|
|
||||||
|
|
||||||
# Read vertices (25%)
|
|
||||||
vertex_count = struct.unpack('<i', file.read(4))[0]
|
|
||||||
vertices = []
|
|
||||||
for i in range(vertex_count):
|
|
||||||
vertices.append(read_vertex(file, bone_struct, bone_size, additional_uvs))
|
|
||||||
if i % 1000 == 0:
|
|
||||||
wm.progress_update(10 + (i/vertex_count * 15))
|
|
||||||
|
|
||||||
# Read faces (35%)
|
|
||||||
wm.progress_update(35)
|
|
||||||
face_count = struct.unpack('<i', file.read(4))[0] // 3
|
|
||||||
faces = []
|
|
||||||
for _ in range(face_count):
|
|
||||||
if vertex_index_size == 1:
|
|
||||||
faces.append(struct.unpack('<3B', file.read(3)))
|
|
||||||
elif vertex_index_size == 2:
|
|
||||||
faces.append(struct.unpack('<3H', file.read(6)))
|
|
||||||
else:
|
|
||||||
faces.append(struct.unpack('<3i', file.read(12)))
|
|
||||||
|
|
||||||
# Read textures (45%)
|
|
||||||
wm.progress_update(45)
|
|
||||||
texture_count = struct.unpack('<i', file.read(4))[0]
|
|
||||||
textures = []
|
|
||||||
for _ in range(texture_count):
|
|
||||||
texture_path = str(file.read(struct.unpack('<i', file.read(4))[0]), 'utf-16-le', errors='replace')
|
|
||||||
textures.append(texture_path)
|
|
||||||
|
|
||||||
# Read materials (55%)
|
|
||||||
wm.progress_update(55)
|
|
||||||
material_count = struct.unpack('<i', file.read(4))[0]
|
|
||||||
materials = []
|
|
||||||
for _ in range(material_count):
|
|
||||||
materials.append(read_material(file, texture_struct, texture_size))
|
|
||||||
|
|
||||||
# Read bones (65%)
|
|
||||||
wm.progress_update(65)
|
|
||||||
bone_count = struct.unpack('<i', file.read(4))[0]
|
|
||||||
bones = []
|
|
||||||
for _ in range(bone_count):
|
|
||||||
bones.append(read_bone(file, bone_struct, bone_size))
|
|
||||||
|
|
||||||
# Read morphs (75%)
|
|
||||||
wm.progress_update(75)
|
|
||||||
morph_count = struct.unpack('<i', file.read(4))[0]
|
|
||||||
morphs = []
|
|
||||||
for _ in range(morph_count):
|
|
||||||
morphs.append(read_morph(file, vertex_struct, vertex_size))
|
|
||||||
|
|
||||||
# Read rigid bodies (85%)
|
|
||||||
wm.progress_update(85)
|
|
||||||
try:
|
|
||||||
rigid_body_count_bytes = file.read(4)
|
|
||||||
if len(rigid_body_count_bytes) == 4:
|
|
||||||
rigid_body_count = struct.unpack('<i', rigid_body_count_bytes)[0]
|
|
||||||
rigid_bodies = []
|
|
||||||
for _ in range(rigid_body_count):
|
|
||||||
rigid_bodies.append(read_rigid_body(file, bone_struct, bone_size))
|
|
||||||
else:
|
|
||||||
rigid_bodies = []
|
|
||||||
except:
|
|
||||||
rigid_bodies = []
|
|
||||||
|
|
||||||
# Read joints (90%)
|
|
||||||
wm.progress_update(90)
|
|
||||||
try:
|
|
||||||
joint_count_bytes = file.read(4)
|
|
||||||
if len(joint_count_bytes) == 4:
|
|
||||||
joint_count = struct.unpack('<i', joint_count_bytes)[0]
|
|
||||||
joints = []
|
|
||||||
for _ in range(joint_count):
|
|
||||||
joints.append(read_joint(file, rigid_body_struct, rigid_body_size))
|
|
||||||
else:
|
|
||||||
joints = []
|
|
||||||
except:
|
|
||||||
joints = []
|
|
||||||
|
|
||||||
# Validate data (92%)
|
|
||||||
wm.progress_update(92)
|
|
||||||
validate_pmx_data(header_data, vertices, faces, materials, bones)
|
|
||||||
|
|
||||||
# Create mesh and object (94%)
|
|
||||||
wm.progress_update(94)
|
|
||||||
mesh = bpy.data.meshes.new(model_name)
|
|
||||||
mesh.from_pydata([v.position for v in vertices], [], faces)
|
|
||||||
mesh.update()
|
|
||||||
|
|
||||||
obj = bpy.data.objects.new(model_name, mesh)
|
|
||||||
bpy.context.collection.objects.link(obj)
|
|
||||||
|
|
||||||
# Create and set up armature (96%)
|
|
||||||
wm.progress_update(96)
|
|
||||||
armature_obj = create_armature(model_name, bones)
|
|
||||||
obj.parent = armature_obj
|
|
||||||
|
|
||||||
# Create shape keys (97%)
|
|
||||||
wm.progress_update(97)
|
|
||||||
for morph in morphs:
|
|
||||||
if morph.morph_type == 1:
|
|
||||||
if not obj.data.shape_keys:
|
|
||||||
obj.shape_key_add(name='Basis')
|
|
||||||
shape_key = obj.shape_key_add(name=morph.name)
|
|
||||||
for vertex_index, offset in morph.offsets:
|
|
||||||
shape_key.data[vertex_index].co = (
|
|
||||||
vertices[vertex_index].position[0] + offset[0],
|
|
||||||
vertices[vertex_index].position[1] + offset[1],
|
|
||||||
vertices[vertex_index].position[2] + offset[2]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set up physics (98%)
|
|
||||||
wm.progress_update(98)
|
|
||||||
setup_physics(obj, armature_obj, rigid_bodies, joints)
|
|
||||||
|
|
||||||
# Final setup (99%)
|
|
||||||
wm.progress_update(99)
|
|
||||||
base_path = os.path.dirname(filepath)
|
|
||||||
assign_materials(obj, materials, textures, base_path)
|
|
||||||
assign_vertex_weights(obj, vertices, bones)
|
|
||||||
|
|
||||||
# Add armature modifier
|
|
||||||
mod = obj.modifiers.new(name="Armature", type='ARMATURE')
|
|
||||||
mod.object = armature_obj
|
|
||||||
|
|
||||||
# Set proper scale and orientation
|
|
||||||
armature_obj.scale = (0.08, 0.08, 0.08)
|
|
||||||
armature_obj.rotation_euler = (1.5708, 0, 0)
|
|
||||||
|
|
||||||
# Select objects and set active
|
|
||||||
armature_obj.select_set(True)
|
|
||||||
obj.select_set(True)
|
|
||||||
bpy.context.view_layer.objects.active = armature_obj
|
|
||||||
|
|
||||||
# Disable automatic mirroring
|
|
||||||
armature_obj.data.use_mirror_x = False
|
|
||||||
|
|
||||||
# Add constraints
|
|
||||||
create_bone_constraints(armature_obj, bones)
|
|
||||||
|
|
||||||
# Apply transforms
|
|
||||||
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
|
|
||||||
|
|
||||||
# Ensure object mode
|
|
||||||
bpy.context.view_layer.objects.active = armature_obj
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
wm.progress_end()
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
wm.progress_end()
|
|
||||||
error_msg = f"PMX Import Error: {str(e)}\n{traceback.format_exc()}"
|
|
||||||
print(error_msg) # Console output for debugging
|
|
||||||
return {'CANCELLED'}
|
|
||||||
@@ -7,8 +7,6 @@ from bpy.types import Operator, Context
|
|||||||
from bpy_extras.io_utils import ImportHelper
|
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_pmd import import_pmd
|
|
||||||
from ..translations import t
|
from ..translations import t
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
@@ -122,13 +120,6 @@ 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(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),
|
|
||||||
"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)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,3 +36,10 @@ def update_logging_state(self: Any, context: Context) -> None:
|
|||||||
enabled = self.enable_logging
|
enabled = self.enable_logging
|
||||||
save_preference("enable_logging", enabled)
|
save_preference("enable_logging", enabled)
|
||||||
configure_logging(enabled)
|
configure_logging(enabled)
|
||||||
|
|
||||||
|
def highlight_problem_bones(self: Any, context: Context) -> None:
|
||||||
|
"""Log when problem bones are highlighted"""
|
||||||
|
from .addon_preferences import save_preference
|
||||||
|
enabled = self.highlight_problem_bones
|
||||||
|
save_preference("highlight_problem_bones", enabled)
|
||||||
|
logger.debug(f"Problem bone highlighting {'enabled' if enabled else 'disabled'}")
|
||||||
|
|||||||
+225
-105
@@ -18,11 +18,24 @@ from .common import get_armature_list, get_active_armature, get_all_meshes, Scen
|
|||||||
from ..functions.visemes import VisemePreview
|
from ..functions.visemes import VisemePreview
|
||||||
from ..functions.eye_tracking import set_rotation
|
from ..functions.eye_tracking import set_rotation
|
||||||
|
|
||||||
|
class ValidationMessageItem(PropertyGroup):
|
||||||
|
"""Property group for validation message items"""
|
||||||
|
name: StringProperty(name="Message")
|
||||||
|
|
||||||
|
class ZeroWeightBoneItem(PropertyGroup):
|
||||||
|
"""Property group for zero weight bone list items"""
|
||||||
|
name: StringProperty(name="Bone Name")
|
||||||
|
selected: BoolProperty(name="Selected", default=True)
|
||||||
|
has_children: BoolProperty(name="Has Children", default=False)
|
||||||
|
is_deform: BoolProperty(name="Is Deform Bone", default=False)
|
||||||
|
|
||||||
|
|
||||||
def update_validation_mode(self: PropertyGroup, context: Context) -> None:
|
def update_validation_mode(self: PropertyGroup, context: Context) -> None:
|
||||||
"""Updates validation mode and saves preference"""
|
"""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: PropertyGroup, context: Context) -> None:
|
def update_logging_state(self: PropertyGroup, context: Context) -> None:
|
||||||
"""Updates logging state and configures logging"""
|
"""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}")
|
||||||
@@ -30,14 +43,143 @@ def update_logging_state(self: PropertyGroup, context: Context) -> None:
|
|||||||
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: PropertyGroup, context: Context) -> None:
|
def update_shape_intensity(self: PropertyGroup, context: Context) -> None:
|
||||||
"""Updates shape key intensity and refreshes preview"""
|
"""Updates shape key intensity and refreshes preview"""
|
||||||
if self.viseme_preview_mode:
|
if self.viseme_preview_mode:
|
||||||
VisemePreview.update_preview(context)
|
VisemePreview.update_preview(context)
|
||||||
|
|
||||||
|
def highlight_problem_bones(self: PropertyGroup, context: Context) -> None:
|
||||||
|
"""Updates problem bone highlighting state and saves preference"""
|
||||||
|
logger.info(f"Updating problem bone highlighting to: {self.highlight_problem_bones}")
|
||||||
|
save_preference("highlight_problem_bones", self.highlight_problem_bones)
|
||||||
|
|
||||||
|
def get_mesh_objects(self, context):
|
||||||
|
meshes = [(obj.name, obj.name, "") for obj in bpy.data.objects if obj.type == 'MESH']
|
||||||
|
if not meshes:
|
||||||
|
return [('NONE', t("Visemes.no_meshes"), '')]
|
||||||
|
return meshes
|
||||||
|
|
||||||
class AvatarToolkitSceneProperties(PropertyGroup):
|
class AvatarToolkitSceneProperties(PropertyGroup):
|
||||||
"""Property group containing Avatar Toolkit scene-level settings and properties"""
|
"""Property group containing Avatar Toolkit scene-level settings and properties"""
|
||||||
|
|
||||||
|
show_found_bones: BoolProperty(
|
||||||
|
name="Show Found Bones",
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
show_non_standard: BoolProperty(
|
||||||
|
name="Show Non-Standard Bones",
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
show_hierarchy: BoolProperty(
|
||||||
|
name="Show Hierarchy Issues",
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
material_search_filter: StringProperty(
|
||||||
|
name=t("TextureAtlas.search_materials"),
|
||||||
|
description=t("TextureAtlas.search_materials_desc"),
|
||||||
|
default=""
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_texture_node_list(self: Material, context: Context) -> list[tuple]:
|
||||||
|
if self.use_nodes:
|
||||||
|
Object.Enum = [((i.image.name if i.image else i.name+"_image"),
|
||||||
|
(i.image.name if i.image else "node with no image..."),
|
||||||
|
(i.image.name if i.image else i.name), index+1)
|
||||||
|
for index, i in enumerate(self.node_tree.nodes)
|
||||||
|
if i.bl_idname == "ShaderNodeTexImage"]
|
||||||
|
if not len(Object.Enum):
|
||||||
|
Object.Enum = [(t("TextureAtlas.error.label"),
|
||||||
|
t("TextureAtlas.no_images_error.desc"),
|
||||||
|
t("TextureAtlas.error.label"), 0)]
|
||||||
|
else:
|
||||||
|
Object.Enum = [(t("TextureAtlas.error.label"),
|
||||||
|
t("TextureAtlas.no_nodes_error.desc"),
|
||||||
|
t("TextureAtlas.error.label"), 0)]
|
||||||
|
Object.Enum.append((t("TextureAtlas.none.label"),
|
||||||
|
t("TextureAtlas.none.label"),
|
||||||
|
t("TextureAtlas.none.label"), 0))
|
||||||
|
return Object.Enum
|
||||||
|
|
||||||
|
Material.texture_atlas_albedo = EnumProperty(
|
||||||
|
name=t("TextureAtlas.albedo"),
|
||||||
|
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.albedo").lower()),
|
||||||
|
default=0,
|
||||||
|
items=get_texture_node_list
|
||||||
|
)
|
||||||
|
|
||||||
|
Material.texture_atlas_normal = EnumProperty(
|
||||||
|
name=t("TextureAtlas.normal"),
|
||||||
|
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.normal").lower()),
|
||||||
|
default=0,
|
||||||
|
items=get_texture_node_list
|
||||||
|
)
|
||||||
|
|
||||||
|
Material.texture_atlas_emission = EnumProperty(
|
||||||
|
name=t("TextureAtlas.emission"),
|
||||||
|
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.emission").lower()),
|
||||||
|
default=0,
|
||||||
|
items=get_texture_node_list
|
||||||
|
)
|
||||||
|
|
||||||
|
Material.texture_atlas_ambient_occlusion = EnumProperty(
|
||||||
|
name=t("TextureAtlas.ambient_occlusion"),
|
||||||
|
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.ambient_occlusion").lower()),
|
||||||
|
default=0,
|
||||||
|
items=get_texture_node_list
|
||||||
|
)
|
||||||
|
|
||||||
|
Material.texture_atlas_height = EnumProperty(
|
||||||
|
name=t("TextureAtlas.height"),
|
||||||
|
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.height").lower()),
|
||||||
|
default=0,
|
||||||
|
items=get_texture_node_list
|
||||||
|
)
|
||||||
|
|
||||||
|
Material.texture_atlas_roughness = EnumProperty(
|
||||||
|
name=t("TextureAtlas.roughness"),
|
||||||
|
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.roughness").lower()),
|
||||||
|
default=0,
|
||||||
|
items=get_texture_node_list
|
||||||
|
)
|
||||||
|
|
||||||
|
list_only_mode: BoolProperty(
|
||||||
|
name=t("Tools.list_only_mode"),
|
||||||
|
description=t("Tools.list_only_mode_desc"),
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
Material.include_in_atlas = BoolProperty(
|
||||||
|
name=t("TextureAtlas.include_in_atlas"),
|
||||||
|
description=t("TextureAtlas.include_in_atlas_desc"),
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
Material.material_expanded = BoolProperty(
|
||||||
|
name=t("TextureAtlas.material_expanded"),
|
||||||
|
description=t("TextureAtlas.material_expanded_desc"),
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
texture_atlas_Has_Mat_List_Shown: BoolProperty(
|
||||||
|
name=t("TextureAtlas.list_shown"),
|
||||||
|
description=t("TextureAtlas.list_shown_desc"),
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
texture_atlas_material_index: IntProperty(
|
||||||
|
default=-1,
|
||||||
|
get=lambda self: -1,
|
||||||
|
set=lambda self, context: None
|
||||||
|
)
|
||||||
|
|
||||||
|
materials: CollectionProperty(
|
||||||
|
type=SceneMatClass
|
||||||
|
)
|
||||||
|
|
||||||
avatar_toolkit_updater_version_list: EnumProperty(
|
avatar_toolkit_updater_version_list: EnumProperty(
|
||||||
items=get_version_list,
|
items=get_version_list,
|
||||||
name=t("Scene.avatar_toolkit_updater_version_list.name"),
|
name=t("Scene.avatar_toolkit_updater_version_list.name"),
|
||||||
@@ -151,9 +293,10 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
description=t("Visemes.mouth_ch_desc")
|
description=t("Visemes.mouth_ch_desc")
|
||||||
)
|
)
|
||||||
|
|
||||||
viseme_mesh: StringProperty(
|
viseme_mesh: EnumProperty(
|
||||||
name=t("Visemes.mesh_select"),
|
name=t("Visemes.mesh_select"),
|
||||||
description=t("Visemes.mesh_select_desc"),
|
description=t("Visemes.mesh_select_desc"),
|
||||||
|
items=get_mesh_objects
|
||||||
)
|
)
|
||||||
|
|
||||||
shape_intensity: FloatProperty(
|
shape_intensity: FloatProperty(
|
||||||
@@ -187,8 +330,7 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
('vrc.v_th', 'TH', 'Th as in "think"')
|
('vrc.v_th', 'TH', 'Th as in "think"')
|
||||||
],
|
],
|
||||||
update=lambda s, c: VisemePreview.update_preview(c)
|
update=lambda s, c: VisemePreview.update_preview(c)
|
||||||
|
)
|
||||||
)
|
|
||||||
|
|
||||||
eye_tracking_type: EnumProperty(
|
eye_tracking_type: EnumProperty(
|
||||||
name=t("EyeTracking.type"),
|
name=t("EyeTracking.type"),
|
||||||
@@ -198,7 +340,7 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
('SDK2', t("EyeTracking.type.sdk2"), t("EyeTracking.type.sdk2_desc"))
|
('SDK2', t("EyeTracking.type.sdk2"), t("EyeTracking.type.sdk2_desc"))
|
||||||
],
|
],
|
||||||
default='AV3'
|
default='AV3'
|
||||||
)
|
)
|
||||||
|
|
||||||
eye_mode: EnumProperty(
|
eye_mode: EnumProperty(
|
||||||
name=t("EyeTracking.mode"),
|
name=t("EyeTracking.mode"),
|
||||||
@@ -337,12 +479,6 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
default=""
|
default=""
|
||||||
)
|
)
|
||||||
|
|
||||||
merge_all_bones: BoolProperty(
|
|
||||||
name=t('MergeArmature.merge_all'),
|
|
||||||
description=t('MergeArmature.merge_all_desc'),
|
|
||||||
default=True
|
|
||||||
)
|
|
||||||
|
|
||||||
apply_transforms: BoolProperty(
|
apply_transforms: BoolProperty(
|
||||||
name=t('MergeArmature.apply_transforms'),
|
name=t('MergeArmature.apply_transforms'),
|
||||||
description=t('MergeArmature.apply_transforms_desc'),
|
description=t('MergeArmature.apply_transforms_desc'),
|
||||||
@@ -361,131 +497,115 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
default=True
|
default=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
preserve_parent_bones: BoolProperty(
|
||||||
|
name=t("Tools.preserve_parent_bones"),
|
||||||
|
description=t("Tools.preserve_parent_bones_desc"),
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
target_bone_type: EnumProperty(
|
||||||
|
name=t("Tools.target_bone_type"),
|
||||||
|
description=t("Tools.target_bone_type_desc"),
|
||||||
|
items=[
|
||||||
|
('ALL', t("Tools.target_all_bones"), ""),
|
||||||
|
('DEFORM', t("Tools.target_deform_bones"), ""),
|
||||||
|
('NON_DEFORM', t("Tools.target_non_deform_bones"), "")
|
||||||
|
],
|
||||||
|
default='ALL'
|
||||||
|
)
|
||||||
|
|
||||||
|
zero_weight_bones: CollectionProperty(
|
||||||
|
type=ZeroWeightBoneItem,
|
||||||
|
name="Zero Weight Bones",
|
||||||
|
description="List of bones with zero weights"
|
||||||
|
)
|
||||||
|
|
||||||
|
zero_weight_bones_index: IntProperty(
|
||||||
|
name="Zero Weight Bone Index",
|
||||||
|
default=0
|
||||||
|
)
|
||||||
|
|
||||||
|
list_only_mode: BoolProperty(
|
||||||
|
name=t("Tools.list_only_mode"),
|
||||||
|
description=t("Tools.list_only_mode_desc"),
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
cleanup_shape_keys: BoolProperty(
|
cleanup_shape_keys: BoolProperty(
|
||||||
name=t('MergeArmature.cleanup_shape_keys'),
|
name=t('MergeArmature.cleanup_shape_keys'),
|
||||||
description=t('MergeArmature.cleanup_shape_keys_desc'),
|
description=t('MergeArmature.cleanup_shape_keys_desc'),
|
||||||
default=True
|
default=True
|
||||||
)
|
)
|
||||||
|
|
||||||
material_search_filter: StringProperty(
|
merge_twist_bones: BoolProperty(
|
||||||
name=t("TextureAtlas.search_materials"),
|
name=t("Tools.merge_twist_bones"),
|
||||||
description=t("TextureAtlas.search_materials_desc"),
|
description=t("Tools.merge_twist_bones_desc"),
|
||||||
default=""
|
default=True
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_texture_node_list(self: Material, context: Context) -> list[tuple]:
|
highlight_problem_bones: BoolProperty(
|
||||||
if self.use_nodes:
|
name=t("Settings.highlight_problem_bones"),
|
||||||
Object.Enum = [((i.image.name if i.image else i.name+"_image"),
|
description=t("Settings.highlight_problem_bones_desc"),
|
||||||
(i.image.name if i.image else "node with no image..."),
|
default=get_preference("highlight_problem_bones", True),
|
||||||
(i.image.name if i.image else i.name),index+1)
|
update=highlight_problem_bones
|
||||||
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(
|
show_scale_issues: BoolProperty(
|
||||||
name=t("TextureAtlas.normal"),
|
name="Show Scale Issues",
|
||||||
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
|
default=False
|
||||||
)
|
)
|
||||||
|
|
||||||
Material.material_expanded = BoolProperty(
|
tpose_validation_result: BoolProperty(
|
||||||
name=t("TextureAtlas.material_expanded"),
|
name="T-Pose Validation Result",
|
||||||
description=t("TextureAtlas.material_expanded_desc"),
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
tpose_validation_messages: CollectionProperty(
|
||||||
|
type=bpy.types.PropertyGroup,
|
||||||
|
name="T-Pose Validation Messages"
|
||||||
|
)
|
||||||
|
|
||||||
|
show_tpose_validation: BoolProperty(
|
||||||
|
name="Show T-Pose Validation Results",
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
|
||||||
texture_atlas_Has_Mat_List_Shown: BoolProperty(
|
standardize_fix_names: BoolProperty(
|
||||||
name=t("TextureAtlas.list_shown"),
|
name=t("Tools.standardize_fix_names"),
|
||||||
description=t("TextureAtlas.list_shown_desc"),
|
description=t("Tools.standardize_fix_names_desc"),
|
||||||
default=False
|
default=True
|
||||||
)
|
)
|
||||||
|
|
||||||
texture_atlas_material_index: IntProperty(
|
standardize_fix_hierarchy: BoolProperty(
|
||||||
default=-1,
|
name=t("Tools.standardize_fix_hierarchy"),
|
||||||
get=lambda self: -1,
|
description=t("Tools.standardize_fix_hierarchy_desc"),
|
||||||
set=lambda self, context: None
|
default=True
|
||||||
)
|
)
|
||||||
|
|
||||||
materials: CollectionProperty(
|
standardize_fix_scale: BoolProperty(
|
||||||
type=SceneMatClass
|
name=t("Tools.standardize_fix_scale"),
|
||||||
|
description=t("Tools.standardize_fix_scale_desc"),
|
||||||
|
default=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
# Only register the property, not the classes (auto_load will handle that)
|
||||||
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")
|
||||||
|
|
||||||
|
# Remove the property
|
||||||
|
if hasattr(bpy.types.Scene, "avatar_toolkit"):
|
||||||
try:
|
try:
|
||||||
del bpy.types.Scene.avatar_toolkit
|
del bpy.types.Scene.avatar_toolkit
|
||||||
except:
|
logger.debug("Removed avatar_toolkit property")
|
||||||
pass
|
except Exception as e:
|
||||||
try:
|
logger.warning(f"Failed to remove avatar_toolkit property: {e}")
|
||||||
bpy.utils.unregister_class(AvatarToolkitSceneProperties)
|
# Not fatal - continue
|
||||||
except RuntimeError:
|
|
||||||
pass
|
|
||||||
logger.debug("Properties unregistered successfully")
|
|
||||||
|
|
||||||
|
|||||||
+13
-20
@@ -3,14 +3,16 @@ import bpy
|
|||||||
import bpy_extras
|
import bpy_extras
|
||||||
from numpy import double
|
from numpy import double
|
||||||
from typing import Set, Dict
|
from typing import Set, Dict
|
||||||
|
import re
|
||||||
|
|
||||||
from .common import get_active_armature, simplify_bonename, validate_armature, ProgressTracker
|
from .common import get_active_armature, simplify_bonename, validate_armature, ProgressTracker, identify_bones
|
||||||
from bpy.types import Context, Operator
|
from bpy.types import Context, Operator
|
||||||
from ..core.translations import t
|
from ..core.translations import t
|
||||||
from ..core.dictionaries import bone_names, resonite_translations
|
from ..core.dictionaries import bone_names, resonite_translations
|
||||||
from ..core.logging_setup import logger
|
from ..core.logging_setup import logger
|
||||||
|
from ..core.armature_validation import validate_armature
|
||||||
|
|
||||||
|
|
||||||
import re
|
|
||||||
from .resonite_loader import resonite_animx, resonite_types
|
from .resonite_loader import resonite_animx, resonite_types
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -50,7 +52,7 @@ class AvatarToolkit_OT_ConvertResonite(Operator):
|
|||||||
armature = get_active_armature(context)
|
armature = get_active_armature(context)
|
||||||
if not armature:
|
if not armature:
|
||||||
return False
|
return False
|
||||||
is_valid, _ = validate_armature(armature)
|
is_valid, _, _ = validate_armature(armature)
|
||||||
return is_valid
|
return is_valid
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
@@ -64,30 +66,21 @@ class AvatarToolkit_OT_ConvertResonite(Operator):
|
|||||||
untranslated_bones: Set[str] = set()
|
untranslated_bones: Set[str] = set()
|
||||||
simplified_names: Dict[str, str] = {}
|
simplified_names: Dict[str, str] = {}
|
||||||
|
|
||||||
# Create reverse lookup dictionary
|
|
||||||
reverse_bone_lookup = {}
|
|
||||||
for preferred_name, name_list in bone_names.items():
|
|
||||||
for name in name_list:
|
|
||||||
reverse_bone_lookup[name] = preferred_name
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
context.view_layer.objects.active = armature
|
context.view_layer.objects.active = armature
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
arm_data: bpy.types.Armature = armature.data
|
||||||
# Cache simplified bone names
|
# Cache simplified bone names
|
||||||
for bone in armature.data.bones:
|
for bone in arm_data.bones:
|
||||||
simplified_names[bone.name] = simplify_bonename(bone.name)
|
|
||||||
|
|
||||||
total_bones = len(armature.data.bones)
|
|
||||||
with ProgressTracker(context, total_bones, t("Tools.convert_resonite.operation")) as progress:
|
|
||||||
for bone in armature.data.bones:
|
|
||||||
# Remove any existing "<noik>" tags
|
|
||||||
bone.name = re.compile(re.escape("<noik>"), re.IGNORECASE).sub("", bone.name)
|
bone.name = re.compile(re.escape("<noik>"), re.IGNORECASE).sub("", bone.name)
|
||||||
simplified_name = simplified_names[bone.name]
|
|
||||||
|
|
||||||
if simplified_name in reverse_bone_lookup and reverse_bone_lookup[simplified_name] in resonite_translations:
|
total_bones = len(arm_data.bones)
|
||||||
new_name = resonite_translations[reverse_bone_lookup[simplified_name]]
|
with ProgressTracker(context, total_bones, t("Tools.convert_resonite.operation")) as progress:
|
||||||
|
for key_simple,bone_name in identify_bones(arm_data,context).items():
|
||||||
|
|
||||||
|
if key_simple in resonite_translations:
|
||||||
|
new_name = resonite_translations[key_simple]
|
||||||
logger.debug(f"Translating bone: {bone.name} -> {new_name}")
|
logger.debug(f"Translating bone: {bone.name} -> {new_name}")
|
||||||
bone.name = new_name
|
bone.name = new_name
|
||||||
else:
|
else:
|
||||||
|
|||||||
+2
-2
@@ -20,7 +20,7 @@ GITHUB_REPO = "teamneoneko/Avatar-Toolkit"
|
|||||||
# Define which version series this installation can update to
|
# Define which version series this installation can update to
|
||||||
# For example: ["0.1"] means only look for 0.1.x updates
|
# 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
|
# ["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
|
ALLOWED_VERSION_SERIES = ["0.2"]
|
||||||
|
|
||||||
is_checking_for_update: bool = False
|
is_checking_for_update: bool = False
|
||||||
update_needed: bool = False
|
update_needed: bool = False
|
||||||
@@ -84,7 +84,7 @@ class AvatarToolkit_PT_UpdaterPanel(bpy.types.Panel):
|
|||||||
bl_region_type = 'UI'
|
bl_region_type = 'UI'
|
||||||
bl_category = CATEGORY_NAME
|
bl_category = CATEGORY_NAME
|
||||||
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
||||||
bl_order = 8
|
bl_order = 9
|
||||||
bl_options = {'DEFAULT_CLOSED'}
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
def draw(self, context: bpy.types.Context) -> None:
|
def draw(self, context: bpy.types.Context) -> None:
|
||||||
|
|||||||
@@ -137,6 +137,10 @@ class AvatarToolKit_OT_AtlasMaterials(Operator):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context: Context) -> bool:
|
||||||
|
# Only allow operation if the file is saved and materials are selected.
|
||||||
|
if not bpy.data.filepath:
|
||||||
|
cls.poll_message_set(t("TextureAtlas.save_file_first"))
|
||||||
|
return False
|
||||||
return context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown
|
return context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown
|
||||||
|
|
||||||
def execute(self, context: Context) -> set:
|
def execute(self, context: Context) -> set:
|
||||||
@@ -208,8 +212,14 @@ class AvatarToolKit_OT_AtlasMaterials(Operator):
|
|||||||
image_pixels[int(((k*w)+i)*4)+channel]
|
image_pixels[int(((k*w)+i)*4)+channel]
|
||||||
|
|
||||||
canvas.pixels[:] = canvas_pixels[:]
|
canvas.pixels[:] = canvas_pixels[:]
|
||||||
canvas.save(filepath=os.path.join(os.path.dirname(bpy.data.filepath),
|
|
||||||
new_image_name+".png"))
|
try:
|
||||||
|
save_dir = os.path.dirname(bpy.data.filepath)
|
||||||
|
canvas.save(filepath=os.path.join(save_dir, new_image_name+".png"))
|
||||||
|
except Exception as save_error:
|
||||||
|
logger.error(f"Failed to save atlas texture: {str(save_error)}")
|
||||||
|
self.report({'WARNING'}, f"Could not save texture to disk, This may be due to a lack of permissions.")
|
||||||
|
|
||||||
setattr(atlased_mat, type_name, canvas)
|
setattr(atlased_mat, type_name, canvas)
|
||||||
progress.step(f"Created {type_name} atlas")
|
progress.step(f"Created {type_name} atlas")
|
||||||
|
|
||||||
@@ -280,6 +290,17 @@ class AvatarToolKit_OT_AtlasMaterials(Operator):
|
|||||||
mesh.materials[i] = atlased_mat.material
|
mesh.materials[i] = atlased_mat.material
|
||||||
progress.step(f"Updated materials for {obj.name}")
|
progress.step(f"Updated materials for {obj.name}")
|
||||||
|
|
||||||
|
MaterialListBool.old_list.pop(context.scene.name, None)
|
||||||
|
was_open = context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown
|
||||||
|
context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown = False
|
||||||
|
|
||||||
|
if was_open:
|
||||||
|
bpy.ops.avatar_toolkit.expand_section_materials()
|
||||||
|
|
||||||
|
for area in context.screen.areas:
|
||||||
|
if area.type == 'VIEW_3D':
|
||||||
|
area.tag_redraw()
|
||||||
|
|
||||||
logger.info("Material atlas creation completed successfully")
|
logger.info("Material atlas creation completed successfully")
|
||||||
self.report({'INFO'}, t("TextureAtlas.atlas_completed"))
|
self.report({'INFO'}, t("TextureAtlas.atlas_completed"))
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import bpy
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from typing import List, Optional, Dict, Set, Tuple, Any
|
from typing import List, Optional, Dict, Set, Tuple, Any
|
||||||
from bpy.types import Context, Object, Operator, ArmatureModifier, EditBone, VertexGroup, Mesh, ShapeKey
|
from bpy.types import Context, Object, Operator, ArmatureModifier, EditBone, VertexGroup, Mesh, ShapeKey
|
||||||
|
from ...core.dictionaries import bone_names
|
||||||
from ...core.logging_setup import logger
|
from ...core.logging_setup import logger
|
||||||
from ...core.translations import t
|
from ...core.translations import t
|
||||||
from ...core.common import (
|
from ...core.common import (
|
||||||
@@ -10,8 +10,9 @@ from ...core.common import (
|
|||||||
fix_zero_length_bones,
|
fix_zero_length_bones,
|
||||||
clear_unused_data_blocks,
|
clear_unused_data_blocks,
|
||||||
join_mesh_objects,
|
join_mesh_objects,
|
||||||
remove_unused_shapekeys
|
remove_unused_shapekeys,
|
||||||
)
|
)
|
||||||
|
from ...core.dictionaries import simplify_bonename
|
||||||
|
|
||||||
class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
|
class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
|
||||||
"""Operator for merging two armatures together with their associated meshes"""
|
"""Operator for merging two armatures together with their associated meshes"""
|
||||||
@@ -52,7 +53,6 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
|
|||||||
wm.progress_update(80)
|
wm.progress_update(80)
|
||||||
|
|
||||||
# Get settings from scene properties
|
# Get settings from scene properties
|
||||||
merge_all_bones: bool = context.scene.avatar_toolkit.merge_all_bones
|
|
||||||
join_meshes: bool = context.scene.avatar_toolkit.join_meshes
|
join_meshes: bool = context.scene.avatar_toolkit.join_meshes
|
||||||
|
|
||||||
# Merge armatures
|
# Merge armatures
|
||||||
@@ -60,7 +60,6 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
|
|||||||
base_armature_name,
|
base_armature_name,
|
||||||
merge_armature_name,
|
merge_armature_name,
|
||||||
mesh_only=False,
|
mesh_only=False,
|
||||||
merge_all_bones=merge_all_bones,
|
|
||||||
join_meshes=join_meshes,
|
join_meshes=join_meshes,
|
||||||
operator=self
|
operator=self
|
||||||
)
|
)
|
||||||
@@ -100,16 +99,12 @@ def validate_parents_and_transforms(merge_armature: Object, base_armature: Objec
|
|||||||
base_parent: Optional[Object] = 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:
|
|
||||||
for armature, parent in [(merge_armature, merge_parent), (base_armature, base_parent)]:
|
for armature, parent in [(merge_armature, merge_parent), (base_armature, base_parent)]:
|
||||||
if parent:
|
if parent:
|
||||||
if not is_transform_clean(parent):
|
if not is_transform_clean(parent):
|
||||||
logger.error("Parent transforms are not clean")
|
logger.error("Parent transforms are not clean")
|
||||||
return False
|
return False
|
||||||
bpy.data.objects.remove(parent, do_unlink=True)
|
bpy.data.objects.remove(parent, do_unlink=True)
|
||||||
else:
|
|
||||||
logger.error("Parent relationships need fixing")
|
|
||||||
return False
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def is_transform_clean(obj: Object) -> bool:
|
def is_transform_clean(obj: Object) -> bool:
|
||||||
@@ -135,7 +130,6 @@ def merge_armatures(
|
|||||||
base_armature_name: str,
|
base_armature_name: str,
|
||||||
merge_armature_name: str,
|
merge_armature_name: str,
|
||||||
mesh_only: bool,
|
mesh_only: bool,
|
||||||
merge_all_bones: bool = False,
|
|
||||||
join_meshes: bool = False,
|
join_meshes: bool = False,
|
||||||
operator: Optional[Operator] = None
|
operator: Optional[Operator] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -152,6 +146,9 @@ def merge_armatures(
|
|||||||
operator.report({'ERROR'}, t('MergeArmature.error.notFound', name=merge_armature_name))
|
operator.report({'ERROR'}, t('MergeArmature.error.notFound', name=merge_armature_name))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Store meshes that need to be reparented
|
||||||
|
meshes_to_reparent = [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == merge_armature]
|
||||||
|
|
||||||
# Check transforms early
|
# Check transforms early
|
||||||
if not validate_merge_armature_transforms(base_armature, merge_armature, None, tolerance):
|
if not validate_merge_armature_transforms(base_armature, merge_armature, None, tolerance):
|
||||||
if not bpy.context.scene.avatar_toolkit.apply_transforms:
|
if not bpy.context.scene.avatar_toolkit.apply_transforms:
|
||||||
@@ -174,25 +171,49 @@ def merge_armatures(
|
|||||||
|
|
||||||
# Store original parent relationships
|
# Store original parent relationships
|
||||||
original_parents: Dict[str, Optional[str]] = {}
|
original_parents: Dict[str, Optional[str]] = {}
|
||||||
for bone in merge_armature.data.bones:
|
merge_armature_data: bpy.types.Armature = merge_armature.data
|
||||||
|
for bone in merge_armature_data.bones:
|
||||||
original_parents[bone.name] = bone.parent.name if bone.parent else None
|
original_parents[bone.name] = bone.parent.name if bone.parent else None
|
||||||
|
|
||||||
|
#create reverse lookup
|
||||||
|
reverse_bone_lookup = {}
|
||||||
|
for preferred_name, name_list in bone_names.items():
|
||||||
|
for name in name_list:
|
||||||
|
reverse_bone_lookup[name] = preferred_name
|
||||||
|
|
||||||
# Get base bone names
|
# Get base bone names
|
||||||
base_bone_names: Set[str] = {bone.name for bone in base_armature.data.bones}
|
base_bone_names: Set[str] = {bone.name for bone in base_armature.data.bones}
|
||||||
|
|
||||||
|
base_armature_standards: Dict[str,Optional[str]] = {}
|
||||||
|
for bone in base_bone_names:
|
||||||
|
if simplify_bonename(bone) in reverse_bone_lookup:
|
||||||
|
base_armature_standards[reverse_bone_lookup[simplify_bonename(bone)]] = bone
|
||||||
|
|
||||||
# Switch to edit mode on merge armature and rename bones
|
# 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
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
|
||||||
# Handle bone renaming based on merge_all_bones setting
|
# Handle bone renaming/removing to target armature.
|
||||||
for bone in merge_armature.data.edit_bones:
|
bone_names_source: list[str] = [bone.name for bone in merge_armature_data.edit_bones]
|
||||||
if not merge_all_bones:
|
for bone in bone_names_source:
|
||||||
# Only rename bones that don't exist in base armature
|
bone_name = bone
|
||||||
if bone.name not in base_bone_names:
|
if bone_name not in base_bone_names: #not auto mergable to original
|
||||||
bone.name += '.merge'
|
|
||||||
|
if simplify_bonename(bone_name) in reverse_bone_lookup: #if is a standard bone through standard translation.
|
||||||
|
if reverse_bone_lookup[simplify_bonename(bone_name)] in base_armature_standards: #if this bone equals for example, "hips", does a bone that should be "hips" exist on our target armature?
|
||||||
|
#if so, rename this bone to that one
|
||||||
|
merge_armature_data.edit_bones[bone_name].name = base_armature_standards[reverse_bone_lookup[simplify_bonename(bone_name)]]
|
||||||
|
bone_name = merge_armature_data.edit_bones[bone_name].name
|
||||||
|
#adjust original parents list to point to the new name.
|
||||||
|
for child_bone in merge_armature_data.edit_bones[bone_name]:
|
||||||
|
original_parents[child_bone.name] = bone_name
|
||||||
|
#then remove so it doesn't clash when merged.
|
||||||
|
merge_armature_data.edit_bones.remove(merge_armature_data.edit_bones[bone_name])
|
||||||
|
continue
|
||||||
|
|
||||||
|
#if it really doesn't have a counter part, just don't bother.
|
||||||
else:
|
else:
|
||||||
# Rename all bones from merge armature
|
merge_armature_data.edit_bones.remove(merge_armature_data.edit_bones[bone_name])
|
||||||
bone.name += '.merge'
|
|
||||||
|
|
||||||
# Return to object mode
|
# Return to object mode
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
@@ -204,23 +225,28 @@ def merge_armatures(
|
|||||||
bpy.context.view_layer.objects.active = base_armature
|
bpy.context.view_layer.objects.active = base_armature
|
||||||
bpy.ops.object.join()
|
bpy.ops.object.join()
|
||||||
|
|
||||||
|
# Explicitly set active object after join
|
||||||
|
bpy.context.view_layer.objects.active = base_armature
|
||||||
|
base_armature_data: bpy.types.Armature = base_armature.data
|
||||||
|
|
||||||
# Restore parent relationships
|
# 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: str = bone.name.replace('.merge', '')
|
if bone.name in original_parents:
|
||||||
if base_name in original_parents:
|
parent_name: Optional[str] = original_parents[bone.name]
|
||||||
parent_name: Optional[str] = original_parents[base_name]
|
|
||||||
if parent_name:
|
if parent_name:
|
||||||
parent_bone: Optional[EditBone] = 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
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
# Update mesh parenting
|
for mesh_obj in meshes_to_reparent:
|
||||||
for obj in bpy.data.objects:
|
if mesh_obj and mesh_obj.name in bpy.data.objects:
|
||||||
if obj.type == 'MESH' and obj.parent == merge_armature:
|
mesh_obj.parent = base_armature
|
||||||
obj.parent = base_armature
|
for mod in mesh_obj.modifiers:
|
||||||
|
if mod.type == 'ARMATURE':
|
||||||
|
mod.object = base_armature
|
||||||
|
|
||||||
# Process vertex groups if not mesh_only
|
# Process vertex groups if not mesh_only
|
||||||
if not mesh_only:
|
if not mesh_only:
|
||||||
@@ -241,6 +267,8 @@ def merge_armatures(
|
|||||||
joined_mesh: Optional[Object] = 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}")
|
||||||
|
# Ensure the joined mesh is properly parented
|
||||||
|
joined_mesh.parent = base_armature
|
||||||
|
|
||||||
# Clean up shape keys if enabled
|
# Clean up shape keys if enabled
|
||||||
if bpy.context.scene.avatar_toolkit.cleanup_shape_keys:
|
if bpy.context.scene.avatar_toolkit.cleanup_shape_keys:
|
||||||
@@ -250,11 +278,6 @@ 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')
|
|
||||||
edit_bones: List[EditBone] = base_armature.data.edit_bones
|
|
||||||
bones_to_remove: List[EditBone] = [bone for bone in edit_bones if bone.name.endswith('.merge')]
|
|
||||||
for bone in bones_to_remove:
|
|
||||||
edit_bones.remove(bone)
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
# Final cleanup
|
# Final cleanup
|
||||||
@@ -298,8 +321,7 @@ def adjust_merge_armature_transforms(
|
|||||||
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
|
|
||||||
) -> 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: List[str] = []
|
bones_to_merge: List[str] = []
|
||||||
@@ -314,7 +336,7 @@ def detect_bones_to_merge(
|
|||||||
merge_bone_position: np.ndarray = np.array(merge_bone.head)
|
merge_bone_position: np.ndarray = np.array(merge_bone.head)
|
||||||
found_match: bool = False
|
found_match: bool = False
|
||||||
|
|
||||||
if merge_all_bones and merge_bone.name in base_bones_positions:
|
if merge_bone.name in base_bones_positions:
|
||||||
# If merging same bones by name
|
# If merging same bones by name
|
||||||
bones_to_merge.append(merge_bone.name)
|
bones_to_merge.append(merge_bone.name)
|
||||||
found_match = True
|
found_match = True
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ from ...core.logging_setup import logger
|
|||||||
from ...core.translations import t
|
from ...core.translations import t
|
||||||
from ...core.common import (
|
from ...core.common import (
|
||||||
get_active_armature,
|
get_active_armature,
|
||||||
validate_armature,
|
|
||||||
get_all_meshes,
|
get_all_meshes,
|
||||||
ProgressTracker,
|
ProgressTracker,
|
||||||
calculate_bone_orientation,
|
calculate_bone_orientation,
|
||||||
add_armature_modifier
|
add_armature_modifier
|
||||||
)
|
)
|
||||||
|
from ...core.armature_validation import validate_armature
|
||||||
|
|
||||||
class AvatarToolkit_OT_AttachMesh(Operator):
|
class AvatarToolkit_OT_AttachMesh(Operator):
|
||||||
"""Operator to attach a mesh to an armature bone with automatic weight setup"""
|
"""Operator to attach a mesh to an armature bone with automatic weight setup"""
|
||||||
@@ -27,8 +27,8 @@ class AvatarToolkit_OT_AttachMesh(Operator):
|
|||||||
armature: Optional[Object] = get_active_armature(context)
|
armature: Optional[Object] = get_active_armature(context)
|
||||||
if not armature:
|
if not armature:
|
||||||
return False
|
return False
|
||||||
is_valid, _ = validate_armature(armature)
|
valid, _, _ = validate_armature(armature)
|
||||||
return is_valid
|
return valid
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ from ..core.common import (
|
|||||||
get_active_armature,
|
get_active_armature,
|
||||||
get_all_meshes,
|
get_all_meshes,
|
||||||
get_armature_list,
|
get_armature_list,
|
||||||
validate_armature,
|
|
||||||
validate_mesh_for_pose,
|
validate_mesh_for_pose,
|
||||||
cache_vertex_positions,
|
cache_vertex_positions,
|
||||||
apply_vertex_positions
|
apply_vertex_positions
|
||||||
)
|
)
|
||||||
|
from ..core.armature_validation import validate_armature
|
||||||
|
|
||||||
VALID_EYE_NAMES: Dict[str, List[str]] = {
|
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'],
|
||||||
@@ -406,6 +406,14 @@ def set_rotation(self, context):
|
|||||||
StartTestingButton.execute(StartTestingButton, context)
|
StartTestingButton.execute(StartTestingButton, context)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Check if rotation data is available
|
||||||
|
if not eye_left_rot or len(eye_left_rot) < 3 or not eye_right_rot or len(eye_right_rot) < 3:
|
||||||
|
# Initialize rotation data if missing
|
||||||
|
eye_left.rotation_mode = 'XYZ'
|
||||||
|
eye_left_rot = list(eye_left.rotation_euler)
|
||||||
|
eye_right.rotation_mode = 'XYZ'
|
||||||
|
eye_right_rot = list(eye_right.rotation_euler)
|
||||||
|
|
||||||
eye_left.rotation_mode = 'XYZ'
|
eye_left.rotation_mode = 'XYZ'
|
||||||
eye_right.rotation_mode = 'XYZ'
|
eye_right.rotation_mode = 'XYZ'
|
||||||
|
|
||||||
@@ -898,9 +906,24 @@ class ResetEyeTrackingButton(Operator):
|
|||||||
|
|
||||||
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
|
||||||
eye_left = eye_right = eye_left_data = eye_right_data = None
|
|
||||||
eye_left_rot = eye_right_rot = []
|
|
||||||
context.scene.avatar_toolkit.eye_mode = 'CREATION'
|
context.scene.avatar_toolkit.eye_mode = 'CREATION'
|
||||||
|
context.scene.avatar_toolkit.eye_rotation_x = 0
|
||||||
|
context.scene.avatar_toolkit.eye_rotation_y = 0
|
||||||
|
|
||||||
|
eye_left = None
|
||||||
|
eye_right = None
|
||||||
|
eye_left_data = None
|
||||||
|
eye_right_data = None
|
||||||
|
eye_left_rot = []
|
||||||
|
eye_right_rot = []
|
||||||
|
|
||||||
|
mesh_name = context.scene.avatar_toolkit.mesh_name_eye
|
||||||
|
mesh = bpy.data.objects.get(mesh_name)
|
||||||
|
if mesh and mesh.data.shape_keys:
|
||||||
|
for shape_key in mesh.data.shape_keys.key_blocks:
|
||||||
|
shape_key.value = 0
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
def validate_weights(mesh_obj: Object, vertex_group: str) -> bool:
|
def validate_weights(mesh_obj: Object, vertex_group: str) -> bool:
|
||||||
|
|||||||
@@ -1,792 +0,0 @@
|
|||||||
import bpy
|
|
||||||
from mathutils import Vector
|
|
||||||
from typing import Dict, List, Tuple, Set, Optional
|
|
||||||
from bpy.types import Object, Armature, EditBone, Bone, Operator, Context
|
|
||||||
from ..core.logging_setup import logger
|
|
||||||
from ..core.common import (
|
|
||||||
ProgressTracker,
|
|
||||||
get_active_armature,
|
|
||||||
validate_armature,
|
|
||||||
get_vertex_weights,
|
|
||||||
transfer_vertex_weights,
|
|
||||||
get_all_meshes
|
|
||||||
)
|
|
||||||
from ..core.translations import t
|
|
||||||
from ..core.dictionaries import bone_names, dont_delete_these_main_bones
|
|
||||||
|
|
||||||
class AVATAR_TOOLKIT_OT_StandardizeMmd(Operator):
|
|
||||||
"""MMD Bone standardization system"""
|
|
||||||
bl_idname = "avatar_toolkit.standardize_mmd"
|
|
||||||
bl_label = t("MMD.standardize")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.bone_mapping: Dict[str, str] = {}
|
|
||||||
self.processed_bones: Set[str] = set()
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
|
||||||
self.armature = get_active_armature(context)
|
|
||||||
|
|
||||||
if not self.armature:
|
|
||||||
self.report({'ERROR'}, t("MMD.no_armature"))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
try:
|
|
||||||
with ProgressTracker(context, 5, "MMD Standardization") as progress:
|
|
||||||
# Step 1: Process bone names
|
|
||||||
self.process_bone_names(context)
|
|
||||||
progress.step("Processed bone names")
|
|
||||||
|
|
||||||
# Step 2: Fix bone structure
|
|
||||||
self.fix_bone_structure(context)
|
|
||||||
progress.step("Fixed bone structure")
|
|
||||||
|
|
||||||
# Step 3: Process weights
|
|
||||||
self.process_weights(context)
|
|
||||||
progress.step("Processed weights")
|
|
||||||
|
|
||||||
# Step 4: Clean up
|
|
||||||
self.cleanup_armature(context)
|
|
||||||
progress.step("Cleaned up armature")
|
|
||||||
|
|
||||||
# Step 5: Final validation
|
|
||||||
self.validate_results(context)
|
|
||||||
progress.step("Validated results")
|
|
||||||
|
|
||||||
self.report({'INFO'}, t("MMD.standardization_complete"))
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"MMD Standardization failed: {str(e)}")
|
|
||||||
self.report({'ERROR'}, str(e))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
def process_bone_names(self, context: Context) -> None:
|
|
||||||
"""Process and standardize bone names"""
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
edit_bones = self.armature.data.edit_bones
|
|
||||||
|
|
||||||
# First pass - handle IK bones
|
|
||||||
ik_bones = [bone for bone in edit_bones if 'IK' in bone.name or 'IK' in bone.name]
|
|
||||||
for bone in ik_bones:
|
|
||||||
new_name = f"ik_{self.standardize_bone_name(bone.name.replace('IK', '').replace('IK', ''))}"
|
|
||||||
self.bone_mapping[bone.name] = new_name
|
|
||||||
bone.name = new_name
|
|
||||||
|
|
||||||
# Second pass - standard bones
|
|
||||||
for bone in edit_bones:
|
|
||||||
if bone not in ik_bones:
|
|
||||||
new_name = self.standardize_bone_name(bone.name)
|
|
||||||
if new_name != bone.name:
|
|
||||||
self.bone_mapping[bone.name] = new_name
|
|
||||||
bone.name = new_name
|
|
||||||
|
|
||||||
def translate_japanese_bone_name(self, name: str) -> str:
|
|
||||||
"""Translate Japanese bone names to English standardized names"""
|
|
||||||
name_lower = name.lower()
|
|
||||||
|
|
||||||
for bone_category, variations in bone_names.items():
|
|
||||||
for variation in variations:
|
|
||||||
if variation in name_lower:
|
|
||||||
return bone_category
|
|
||||||
|
|
||||||
return name
|
|
||||||
|
|
||||||
def standardize_bone_name(self, name: str) -> str:
|
|
||||||
"""Standardize individual bone names"""
|
|
||||||
result = self.translate_japanese_bone_name(name)
|
|
||||||
|
|
||||||
prefixes = ['ValveBiped_', 'Bip01_', 'MMD_', 'Armature|']
|
|
||||||
for prefix in prefixes:
|
|
||||||
if result.lower().startswith(prefix.lower()):
|
|
||||||
result = result[len(prefix):]
|
|
||||||
|
|
||||||
if result.endswith('_L') or result.endswith('.L'):
|
|
||||||
result = f"{result[:-2]}.L"
|
|
||||||
elif result.endswith('_R') or result.endswith('.R'):
|
|
||||||
result = f"{result[:-2]}.R"
|
|
||||||
|
|
||||||
return result
|
|
||||||
return result
|
|
||||||
|
|
||||||
def fix_bone_structure(self, context: Context) -> None:
|
|
||||||
"""Fix bone hierarchy and orientations"""
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
edit_bones = self.armature.data.edit_bones
|
|
||||||
|
|
||||||
self.process_spine_chain(context)
|
|
||||||
self.fix_bone_orientations(context)
|
|
||||||
self.connect_bones(context)
|
|
||||||
|
|
||||||
def process_weights(self, context: Context) -> None:
|
|
||||||
"""Process and clean up vertex weights"""
|
|
||||||
for mesh in self.get_associated_meshes(context):
|
|
||||||
# Transfer weights based on bone mapping
|
|
||||||
for old_name, new_name in self.bone_mapping.items():
|
|
||||||
if old_name != new_name:
|
|
||||||
transfer_vertex_weights(mesh, old_name, new_name)
|
|
||||||
|
|
||||||
# Clean up zero weights
|
|
||||||
self.cleanup_vertex_groups(mesh, context)
|
|
||||||
|
|
||||||
def cleanup_armature(self, context: Context) -> None:
|
|
||||||
"""Perform final cleanup operations"""
|
|
||||||
self.remove_unused_bones(context)
|
|
||||||
self.cleanup_constraints(context)
|
|
||||||
self.fix_zero_length_bones(context)
|
|
||||||
|
|
||||||
def get_associated_meshes(self, context: Context) -> List[Object]:
|
|
||||||
"""Get all mesh objects associated with the armature"""
|
|
||||||
return [obj for obj in bpy.data.objects
|
|
||||||
if obj.type == 'MESH'
|
|
||||||
and obj.parent == self.armature]
|
|
||||||
|
|
||||||
def process_spine_chain(self, context: Context) -> None:
|
|
||||||
"""Process and fix spine bone chain hierarchy"""
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
edit_bones = self.armature.data.edit_bones
|
|
||||||
spine_bones = {
|
|
||||||
'hips': None,
|
|
||||||
'spine': None,
|
|
||||||
'chest': None,
|
|
||||||
'upper_chest': None,
|
|
||||||
'neck': None,
|
|
||||||
'head': None
|
|
||||||
}
|
|
||||||
|
|
||||||
# Find spine bones using bone_names dictionary
|
|
||||||
for bone in edit_bones:
|
|
||||||
for spine_part, _ in spine_bones.items():
|
|
||||||
if any(alt_name in bone.name.lower() for alt_name in bone_names[spine_part]):
|
|
||||||
spine_bones[spine_part] = bone
|
|
||||||
break
|
|
||||||
|
|
||||||
# Set up spine hierarchy
|
|
||||||
hierarchy = [
|
|
||||||
('hips', 'spine'),
|
|
||||||
('spine', 'chest'),
|
|
||||||
('chest', 'neck'),
|
|
||||||
('neck', 'head')
|
|
||||||
]
|
|
||||||
|
|
||||||
for parent_name, child_name in hierarchy:
|
|
||||||
parent = spine_bones.get(parent_name)
|
|
||||||
child = spine_bones.get(child_name)
|
|
||||||
if parent and child:
|
|
||||||
child.parent = parent
|
|
||||||
child.use_connect = True
|
|
||||||
|
|
||||||
def fix_bone_orientations(self, context: Context) -> None:
|
|
||||||
"""Fix bone orientations for standard pose compatibility"""
|
|
||||||
edit_bones = self.armature.data.edit_bones
|
|
||||||
|
|
||||||
# Define standardized roll values for key bones
|
|
||||||
roll_values = {
|
|
||||||
'upper_arm.L': -0.1,
|
|
||||||
'upper_arm.R': 0.1,
|
|
||||||
'forearm.L': -0.1,
|
|
||||||
'forearm.R': 0.1,
|
|
||||||
'thigh.L': 0.0,
|
|
||||||
'thigh.R': 0.0,
|
|
||||||
'shin.L': 0.0,
|
|
||||||
'shin.R': 0.0,
|
|
||||||
'foot.L': 0.0,
|
|
||||||
'foot.R': 0.0,
|
|
||||||
'spine': 0.0,
|
|
||||||
'chest': 0.0,
|
|
||||||
'neck': 0.0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Apply roll corrections
|
|
||||||
for bone in edit_bones:
|
|
||||||
if bone.name.lower() in roll_values:
|
|
||||||
bone.roll = roll_values[bone.name.lower()]
|
|
||||||
|
|
||||||
# Process arm chains
|
|
||||||
arm_pairs = [
|
|
||||||
('upper_arm', 'forearm'),
|
|
||||||
('forearm', 'hand')
|
|
||||||
]
|
|
||||||
|
|
||||||
for side in ['.L', '.R']:
|
|
||||||
for parent, child in arm_pairs:
|
|
||||||
parent_bone = next((b for b in edit_bones if b.name.lower().startswith(parent) and b.name.endswith(side)), None)
|
|
||||||
child_bone = next((b for b in edit_bones if b.name.lower().startswith(child) and b.name.endswith(side)), None)
|
|
||||||
|
|
||||||
if parent_bone and child_bone:
|
|
||||||
child_bone.use_connect = True
|
|
||||||
child_bone.use_inherit_rotation = True
|
|
||||||
|
|
||||||
# Process leg chains
|
|
||||||
leg_pairs = [
|
|
||||||
('thigh', 'shin'),
|
|
||||||
('shin', 'foot')
|
|
||||||
]
|
|
||||||
|
|
||||||
for side in ['.L', '.R']:
|
|
||||||
for parent, child in leg_pairs:
|
|
||||||
parent_bone = next((b for b in edit_bones if b.name.lower().startswith(parent) and b.name.endswith(side)), None)
|
|
||||||
child_bone = next((b for b in edit_bones if b.name.lower().startswith(child) and b.name.endswith(side)), None)
|
|
||||||
|
|
||||||
if parent_bone and child_bone:
|
|
||||||
child_bone.use_connect = True
|
|
||||||
child_bone.use_inherit_rotation = True
|
|
||||||
|
|
||||||
# Align twist bones if present
|
|
||||||
twist_bones = [b for b in edit_bones if 'twist' in b.name.lower()]
|
|
||||||
for twist_bone in twist_bones:
|
|
||||||
if twist_bone.parent:
|
|
||||||
twist_bone.roll = twist_bone.parent.roll
|
|
||||||
|
|
||||||
def remove_unused_bones(self, context: Context) -> None:
|
|
||||||
"""Remove unused and unnecessary bones from the armature"""
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
edit_bones = self.armature.data.edit_bones
|
|
||||||
|
|
||||||
# Get list of bones that have vertex weights
|
|
||||||
used_bones = set()
|
|
||||||
for mesh in self.get_associated_meshes(context):
|
|
||||||
for group in mesh.vertex_groups:
|
|
||||||
used_bones.add(group.name)
|
|
||||||
|
|
||||||
# Get list of essential bones to always keep
|
|
||||||
essential_bones = {
|
|
||||||
'hips', 'spine', 'chest', 'upper_chest', 'neck', 'head',
|
|
||||||
'left_leg', 'right_leg', 'left_knee', 'right_knee',
|
|
||||||
'left_ankle', 'right_ankle', 'left_toe', 'right_toe'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add any additional bones you want to preserve
|
|
||||||
essential_bones.update(dont_delete_these_main_bones)
|
|
||||||
|
|
||||||
# Remove unused bones
|
|
||||||
for bone in edit_bones:
|
|
||||||
# Skip if bone is essential
|
|
||||||
if bone.name.lower() in essential_bones:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Skip if bone has weights
|
|
||||||
if bone.name in used_bones:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Remove the bone
|
|
||||||
edit_bones.remove(bone)
|
|
||||||
|
|
||||||
|
|
||||||
def connect_bones(self, context: Context) -> None:
|
|
||||||
"""Connect bones that should be connected in the hierarchy"""
|
|
||||||
edit_bones = self.armature.data.edit_bones
|
|
||||||
|
|
||||||
connect_chains = [
|
|
||||||
['hips', 'spine', 'chest', 'neck', 'head'],
|
|
||||||
['shoulder.L', 'upper_arm.L', 'forearm.L', 'hand.L'],
|
|
||||||
['shoulder.R', 'upper_arm.R', 'forearm.R', 'hand.R'],
|
|
||||||
['thigh.L', 'shin.L', 'foot.L', 'toe.L'],
|
|
||||||
['thigh.R', 'shin.R', 'foot.R', 'toe.R']
|
|
||||||
]
|
|
||||||
|
|
||||||
for chain in connect_chains:
|
|
||||||
prev_bone = None
|
|
||||||
for bone_name in chain:
|
|
||||||
bone = next((b for b in edit_bones if b.name.lower().endswith(bone_name.lower())), None)
|
|
||||||
if bone and prev_bone:
|
|
||||||
bone.parent = prev_bone
|
|
||||||
bone.use_connect = True
|
|
||||||
prev_bone = bone
|
|
||||||
|
|
||||||
def cleanup_vertex_groups(self, mesh_obj: Object, context: Context) -> None:
|
|
||||||
"""Clean up vertex groups by removing zero weights and merging similar groups"""
|
|
||||||
threshold = context.scene.avatar_toolkit.merge_weights_threshold
|
|
||||||
|
|
||||||
vertex_groups = mesh_obj.vertex_groups
|
|
||||||
|
|
||||||
groups_to_remove = set()
|
|
||||||
|
|
||||||
for group in vertex_groups:
|
|
||||||
weights = get_vertex_weights(mesh_obj, group.name)
|
|
||||||
|
|
||||||
if not any(weight > threshold for weight in weights.values()):
|
|
||||||
groups_to_remove.add(group.name)
|
|
||||||
|
|
||||||
for group_name in groups_to_remove:
|
|
||||||
group = vertex_groups.get(group_name)
|
|
||||||
if group:
|
|
||||||
vertex_groups.remove(group)
|
|
||||||
|
|
||||||
def validate_results(self, context: Context) -> None:
|
|
||||||
"""Validate the results of standardization"""
|
|
||||||
valid, messages = validate_armature(self.armature)
|
|
||||||
if not valid:
|
|
||||||
raise ValueError("\n".join(messages))
|
|
||||||
|
|
||||||
def cleanup_constraints(self, context: Context) -> None:
|
|
||||||
"""Remove all constraints from the armature."""
|
|
||||||
bpy.ops.object.mode_set(mode='POSE')
|
|
||||||
|
|
||||||
for pose_bone in self.armature.pose.bones:
|
|
||||||
constraints_to_remove = [constraint for constraint in pose_bone.constraints]
|
|
||||||
for constraint in constraints_to_remove:
|
|
||||||
pose_bone.constraints.remove(constraint)
|
|
||||||
|
|
||||||
def fix_zero_length_bones(self, context: Context) -> None:
|
|
||||||
"""Fix zero-length bones by setting minimal length"""
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
edit_bones = self.armature.data.edit_bones
|
|
||||||
|
|
||||||
min_length = 0.01 # Minimum bone length in Blender units
|
|
||||||
|
|
||||||
for bone in edit_bones:
|
|
||||||
bone_length = (bone.tail - bone.head).length
|
|
||||||
|
|
||||||
if bone_length < min_length:
|
|
||||||
if bone.parent:
|
|
||||||
direction = bone.parent.tail - bone.parent.head
|
|
||||||
direction.normalize()
|
|
||||||
else:
|
|
||||||
direction = Vector((0, 0, 1))
|
|
||||||
|
|
||||||
bone.tail = bone.head + (direction * min_length)
|
|
||||||
|
|
||||||
|
|
||||||
class ReparentMeshesOperator(bpy.types.Operator):
|
|
||||||
bl_idname = "avatar_toolkit.reparent_meshes"
|
|
||||||
bl_label = t("MMD.reparent_meshes")
|
|
||||||
bl_description = t("MMD.reparent_meshes_desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
armature = get_active_armature(context)
|
|
||||||
return armature is not None and get_all_meshes(context)
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
armature = get_active_armature(context)
|
|
||||||
if not armature:
|
|
||||||
self.report({'ERROR'}, t("MMD.no_armature"))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
meshes = get_all_meshes(context)
|
|
||||||
if not meshes:
|
|
||||||
self.report({'ERROR'}, t("MMD.no_meshes"))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
try:
|
|
||||||
with ProgressTracker(context, len(meshes) + 1, "Reparenting Meshes") as progress:
|
|
||||||
# Get or create main collection
|
|
||||||
main_collection = self._get_main_collection(context)
|
|
||||||
progress.step("Setting up collections")
|
|
||||||
|
|
||||||
# Process each mesh
|
|
||||||
for mesh in meshes:
|
|
||||||
progress.step(f"Processing {mesh.name}")
|
|
||||||
self._process_mesh(mesh, armature, main_collection)
|
|
||||||
|
|
||||||
self.report({'INFO'}, t("MMD.reparenting_complete"))
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error reparenting meshes: {str(e)}")
|
|
||||||
self.report({'ERROR'}, str(e))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
def _get_main_collection(self, context) -> bpy.types.Collection:
|
|
||||||
"""Get or create the main collection for the armature"""
|
|
||||||
if hasattr(context.scene, 'collection'):
|
|
||||||
return context.scene.collection
|
|
||||||
return context.scene.collection
|
|
||||||
|
|
||||||
def _process_mesh(self, mesh: bpy.types.Object,
|
|
||||||
armature: bpy.types.Object,
|
|
||||||
main_collection: bpy.types.Collection) -> None:
|
|
||||||
"""Process individual mesh parenting and collection management"""
|
|
||||||
# Unlink from other collections
|
|
||||||
for col in mesh.users_collection:
|
|
||||||
if col != main_collection:
|
|
||||||
col.objects.unlink(mesh)
|
|
||||||
|
|
||||||
# Ensure mesh is in main collection
|
|
||||||
if mesh.name not in main_collection.objects:
|
|
||||||
main_collection.objects.link(mesh)
|
|
||||||
|
|
||||||
# Set parent to armature
|
|
||||||
mesh.parent = armature
|
|
||||||
if not mesh.parent_type == 'ARMATURE':
|
|
||||||
mesh.parent_type = 'ARMATURE'
|
|
||||||
|
|
||||||
class AVATAR_TOOLKIT_OT_ConvertMmdMorphs(Operator):
|
|
||||||
"""Convert MMD morph data to shape keys"""
|
|
||||||
bl_idname = "avatar_toolkit.convert_mmd_morphs"
|
|
||||||
bl_label = t("MMD.convert_morphs")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
armature = get_active_armature(context)
|
|
||||||
return armature is not None and get_all_meshes(context)
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
armature = get_active_armature(context)
|
|
||||||
if not armature:
|
|
||||||
self.report({'ERROR'}, t("MMD.no_armature"))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
try:
|
|
||||||
with ProgressTracker(context, 3, "Converting MMD Morphs") as progress:
|
|
||||||
# Convert bone morphs to shape keys
|
|
||||||
if hasattr(armature, 'mmd_root') and armature.mmd_root.bone_morphs:
|
|
||||||
self.process_bone_morphs(context, armature, progress)
|
|
||||||
|
|
||||||
progress.step("Processed bone morphs")
|
|
||||||
|
|
||||||
# Clean up unused data
|
|
||||||
self.cleanup_unused_data(context)
|
|
||||||
progress.step("Cleaned up data")
|
|
||||||
|
|
||||||
# Validate results
|
|
||||||
self.validate_results(context)
|
|
||||||
progress.step("Validated results")
|
|
||||||
|
|
||||||
self.report({'INFO'}, t("MMD.conversion_complete"))
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error converting MMD morphs: {str(e)}")
|
|
||||||
self.report({'ERROR'}, str(e))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
def process_bone_morphs(self, context, armature, progress):
|
|
||||||
"""Process bone morphs into shape keys"""
|
|
||||||
for morph in armature.mmd_root.bone_morphs:
|
|
||||||
for mesh in get_all_meshes(context):
|
|
||||||
# Create armature modifier
|
|
||||||
mod = mesh.modifiers.new(morph.name, 'ARMATURE')
|
|
||||||
mod.object = armature
|
|
||||||
|
|
||||||
# Apply as shape key
|
|
||||||
with context.temp_override(object=mesh):
|
|
||||||
bpy.ops.object.modifier_apply(modifier=mod.name)
|
|
||||||
|
|
||||||
class AVATAR_TOOLKIT_OT_CleanupMmdModel(Operator):
|
|
||||||
"""Clean up MMD model by removing unused data and fixing display settings"""
|
|
||||||
bl_idname = "avatar_toolkit.cleanup_mmd"
|
|
||||||
bl_label = t("MMD.cleanup")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
armature = get_active_armature(context)
|
|
||||||
if not armature:
|
|
||||||
self.report({'ERROR'}, t("MMD.no_armature"))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
try:
|
|
||||||
with ProgressTracker(context, 4, "Cleaning MMD Model") as progress:
|
|
||||||
# Remove rigid bodies and joints
|
|
||||||
self.remove_physics_objects(armature)
|
|
||||||
progress.step("Removed physics objects")
|
|
||||||
|
|
||||||
# Clean up collections and hierarchy
|
|
||||||
self.cleanup_hierarchy(context, armature)
|
|
||||||
progress.step("Cleaned hierarchy")
|
|
||||||
|
|
||||||
# Fix viewport settings
|
|
||||||
self.fix_viewport_settings(context)
|
|
||||||
progress.step("Fixed viewport")
|
|
||||||
|
|
||||||
# Final cleanup
|
|
||||||
clear_unused_data_blocks()
|
|
||||||
progress.step("Cleared unused data")
|
|
||||||
|
|
||||||
self.report({'INFO'}, t("MMD.cleanup_complete"))
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error cleaning MMD model: {str(e)}")
|
|
||||||
self.report({'ERROR'}, str(e))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
def remove_physics_objects(self, armature):
|
|
||||||
"""Remove physics-related objects"""
|
|
||||||
to_delete = []
|
|
||||||
for child in armature.children:
|
|
||||||
if any(x in child.name.lower() for x in ['rigidbodies', 'joints', 'physics']):
|
|
||||||
to_delete.append(child)
|
|
||||||
|
|
||||||
for obj in to_delete:
|
|
||||||
bpy.data.objects.remove(obj, do_unlink=True)
|
|
||||||
|
|
||||||
def cleanup_hierarchy(self, context, armature):
|
|
||||||
"""Clean up object hierarchy and collections"""
|
|
||||||
meshes = get_all_meshes(context)
|
|
||||||
for mesh in meshes:
|
|
||||||
# Ensure proper parenting
|
|
||||||
mesh.parent = armature
|
|
||||||
mesh.parent_type = 'ARMATURE'
|
|
||||||
|
|
||||||
# Clean up collections
|
|
||||||
for col in mesh.users_collection:
|
|
||||||
if col != context.scene.collection:
|
|
||||||
col.objects.unlink(mesh)
|
|
||||||
|
|
||||||
if mesh.name not in context.scene.collection.objects:
|
|
||||||
context.scene.collection.objects.link(mesh)
|
|
||||||
|
|
||||||
def fix_viewport_settings(self, context):
|
|
||||||
"""Fix viewport display settings"""
|
|
||||||
# Set armature display
|
|
||||||
armature = get_active_armature(context)
|
|
||||||
armature.data.display_type = 'OCTAHEDRAL'
|
|
||||||
armature.show_in_front = True
|
|
||||||
|
|
||||||
# Set viewport shading
|
|
||||||
for area in context.screen.areas:
|
|
||||||
if area.type == 'VIEW_3D':
|
|
||||||
space = area.spaces[0]
|
|
||||||
space.shading.type = 'MATERIAL'
|
|
||||||
space.clip_start = 0.01
|
|
||||||
space.clip_end = 300
|
|
||||||
|
|
||||||
class AVATAR_TOOLKIT_OT_FixMeshes(Operator):
|
|
||||||
"""Clean up and optimize mesh materials, shading, and shape keys"""
|
|
||||||
bl_idname = "avatar_toolkit.fix_meshes"
|
|
||||||
bl_label = t("Optimization.fix_meshes")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
armature = get_active_armature(context)
|
|
||||||
return armature is not None and get_all_meshes(context)
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
try:
|
|
||||||
meshes = get_all_meshes(context)
|
|
||||||
if not meshes:
|
|
||||||
self.report({'ERROR'}, t("Optimization.no_meshes"))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
with ProgressTracker(context, len(meshes), "Fixing Meshes") as progress:
|
|
||||||
for mesh in meshes:
|
|
||||||
self.process_mesh(context, mesh)
|
|
||||||
progress.step(f"Processed {mesh.name}")
|
|
||||||
|
|
||||||
self.report({'INFO'}, t("Optimization.meshes_fixed"))
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error fixing meshes: {str(e)}")
|
|
||||||
self.report({'ERROR'}, str(e))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
def process_mesh(self, context: Context, mesh: Object) -> None:
|
|
||||||
"""Process and fix individual mesh"""
|
|
||||||
# Unlock transforms
|
|
||||||
for i in range(3):
|
|
||||||
mesh.lock_location[i] = False
|
|
||||||
mesh.lock_rotation[i] = False
|
|
||||||
mesh.lock_scale[i] = False
|
|
||||||
|
|
||||||
# Process shape keys
|
|
||||||
if mesh.data.shape_keys:
|
|
||||||
self.fix_shape_keys(mesh)
|
|
||||||
|
|
||||||
# Process materials
|
|
||||||
self.fix_materials(context, mesh)
|
|
||||||
|
|
||||||
def fix_shape_keys(self, mesh: Object) -> None:
|
|
||||||
"""Fix and clean up shape keys"""
|
|
||||||
if not mesh.data.shape_keys:
|
|
||||||
return
|
|
||||||
|
|
||||||
shape_keys = mesh.data.shape_keys.key_blocks
|
|
||||||
|
|
||||||
# Rename basis
|
|
||||||
if shape_keys[0].name != "Basis":
|
|
||||||
shape_keys[0].name = "Basis"
|
|
||||||
|
|
||||||
# Clean up names
|
|
||||||
for key in shape_keys:
|
|
||||||
# Remove common prefixes/suffixes
|
|
||||||
clean_name = key.name
|
|
||||||
for prefix in ['Face.M F00 000 Fcl ', 'Face.M F00 000 00 Fcl ']:
|
|
||||||
clean_name = clean_name.replace(prefix, '')
|
|
||||||
|
|
||||||
# Replace underscores with spaces
|
|
||||||
clean_name = clean_name.replace('_', ' ')
|
|
||||||
key.name = clean_name
|
|
||||||
|
|
||||||
# Sort shape keys by category
|
|
||||||
categories = ['MTH', 'EYE', 'BRW', 'ALL']
|
|
||||||
|
|
||||||
# Create sorted list of shape key names
|
|
||||||
ordered_names = []
|
|
||||||
|
|
||||||
# Add categorized keys first
|
|
||||||
for category in categories:
|
|
||||||
category_keys = [key.name for key in shape_keys if key.name.startswith(category)]
|
|
||||||
ordered_names.extend(sorted(category_keys))
|
|
||||||
|
|
||||||
# Add remaining keys
|
|
||||||
remaining = [key.name for key in shape_keys if not any(key.name.startswith(c) for c in categories)]
|
|
||||||
ordered_names.extend(sorted(remaining))
|
|
||||||
|
|
||||||
# Reorder using context override
|
|
||||||
with bpy.context.temp_override(active_object=mesh, selected_objects=[mesh]):
|
|
||||||
for idx, name in enumerate(ordered_names):
|
|
||||||
mesh.active_shape_key_index = shape_keys.find(name)
|
|
||||||
while mesh.active_shape_key_index > idx:
|
|
||||||
bpy.ops.object.shape_key_move(type='UP')
|
|
||||||
|
|
||||||
|
|
||||||
def fix_materials(self, context: Context, mesh: Object) -> None:
|
|
||||||
"""Fix and optimize materials"""
|
|
||||||
for slot in mesh.material_slots:
|
|
||||||
if not slot.material:
|
|
||||||
continue
|
|
||||||
|
|
||||||
material = slot.material
|
|
||||||
|
|
||||||
# Set up basic material properties
|
|
||||||
material.use_backface_culling = True
|
|
||||||
material.blend_method = 'HASHED'
|
|
||||||
material.shadow_method = 'HASHED'
|
|
||||||
|
|
||||||
# Clean up material name
|
|
||||||
material.name = self.clean_material_name(material.name)
|
|
||||||
|
|
||||||
# Consolidate similar materials
|
|
||||||
for other_slot in mesh.material_slots:
|
|
||||||
if other_slot.material and other_slot.material != material:
|
|
||||||
if materials_match(material, other_slot.material):
|
|
||||||
other_slot.material = material
|
|
||||||
|
|
||||||
def clean_material_name(self, name: str) -> str:
|
|
||||||
"""Clean up material name"""
|
|
||||||
# Remove common prefixes/suffixes
|
|
||||||
prefixes = ['material', 'mat', 'mtl', 'material.']
|
|
||||||
for prefix in prefixes:
|
|
||||||
if name.lower().startswith(prefix):
|
|
||||||
name = name[len(prefix):]
|
|
||||||
|
|
||||||
# Remove numbers at end
|
|
||||||
while name and name[-1].isdigit():
|
|
||||||
name = name[:-1]
|
|
||||||
|
|
||||||
return name.strip()
|
|
||||||
|
|
||||||
class AVATAR_TOOLKIT_OT_ValidateMeshes(Operator):
|
|
||||||
"""Validate meshes and UV maps for common issues"""
|
|
||||||
bl_idname = "avatar_toolkit.validate_meshes"
|
|
||||||
bl_label = t("Validation.check_meshes")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
armature = get_active_armature(context)
|
|
||||||
if not armature:
|
|
||||||
self.report({'ERROR'}, t("Validation.no_armature"))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
try:
|
|
||||||
with ProgressTracker(context, 3, "Validating Meshes") as progress:
|
|
||||||
# Check bone hierarchy
|
|
||||||
hierarchy_issues = self.validate_bone_hierarchy(armature)
|
|
||||||
progress.step("Checked bone hierarchy")
|
|
||||||
|
|
||||||
# Check UV coordinates
|
|
||||||
uv_issues = self.validate_uv_maps(context)
|
|
||||||
progress.step("Checked UV maps")
|
|
||||||
|
|
||||||
# Generate report
|
|
||||||
self.generate_validation_report(context, hierarchy_issues, uv_issues)
|
|
||||||
progress.step("Generated report")
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error validating meshes: {str(e)}")
|
|
||||||
self.report({'ERROR'}, str(e))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
def validate_bone_hierarchy(self, armature: Object) -> List[str]:
|
|
||||||
"""Validate bone hierarchy against standard structure"""
|
|
||||||
issues = []
|
|
||||||
|
|
||||||
# Define expected hierarchy
|
|
||||||
hierarchy = [
|
|
||||||
['hips', 'spine', 'chest', 'neck', 'head'],
|
|
||||||
['hips', 'left_leg', 'left_knee', 'left_ankle'],
|
|
||||||
['hips', 'right_leg', 'right_knee', 'right_ankle'],
|
|
||||||
['chest', 'left_shoulder', 'left_arm', 'left_elbow', 'left_wrist'],
|
|
||||||
['chest', 'right_shoulder', 'right_arm', 'right_elbow', 'right_wrist']
|
|
||||||
]
|
|
||||||
|
|
||||||
for chain in hierarchy:
|
|
||||||
previous = None
|
|
||||||
for bone_name in chain:
|
|
||||||
# Check if bone exists
|
|
||||||
bone = None
|
|
||||||
for alt_name in bone_names[bone_name]:
|
|
||||||
if alt_name in armature.data.bones:
|
|
||||||
bone = armature.data.bones[alt_name]
|
|
||||||
break
|
|
||||||
|
|
||||||
if not bone:
|
|
||||||
issues.append(t("Validation.missing_bone", bone=bone_name))
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Check parent relationship
|
|
||||||
if previous:
|
|
||||||
if not bone.parent:
|
|
||||||
issues.append(t("Validation.no_parent", bone=bone.name))
|
|
||||||
elif bone.parent.name != previous.name:
|
|
||||||
issues.append(t("Validation.wrong_parent",
|
|
||||||
bone=bone.name,
|
|
||||||
expected=previous.name,
|
|
||||||
actual=bone.parent.name))
|
|
||||||
previous = bone
|
|
||||||
|
|
||||||
return issues
|
|
||||||
|
|
||||||
def validate_uv_maps(self, context: Context) -> Dict[str, int]:
|
|
||||||
"""Check UV maps for issues"""
|
|
||||||
issues = {'nan_coords': 0, 'missing_uvs': 0}
|
|
||||||
|
|
||||||
for mesh in get_all_meshes(context):
|
|
||||||
if not mesh.data.uv_layers:
|
|
||||||
issues['missing_uvs'] += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
for uv_layer in mesh.data.uv_layers:
|
|
||||||
for uv in uv_layer.data:
|
|
||||||
if math.isnan(uv.uv.x):
|
|
||||||
uv.uv.x = 0
|
|
||||||
issues['nan_coords'] += 1
|
|
||||||
if math.isnan(uv.uv.y):
|
|
||||||
uv.uv.y = 0
|
|
||||||
issues['nan_coords'] += 1
|
|
||||||
|
|
||||||
return issues
|
|
||||||
|
|
||||||
def generate_validation_report(self, context: Context,
|
|
||||||
hierarchy_issues: List[str],
|
|
||||||
uv_issues: Dict[str, int]) -> None:
|
|
||||||
"""Generate and display validation report"""
|
|
||||||
report_lines = []
|
|
||||||
|
|
||||||
# Add hierarchy issues
|
|
||||||
if hierarchy_issues:
|
|
||||||
report_lines.append(t("Validation.hierarchy_issues"))
|
|
||||||
report_lines.extend(hierarchy_issues)
|
|
||||||
|
|
||||||
# Add UV issues
|
|
||||||
if uv_issues['nan_coords'] > 0:
|
|
||||||
report_lines.append(t("Validation.uv_nan_coords",
|
|
||||||
count=uv_issues['nan_coords']))
|
|
||||||
|
|
||||||
if uv_issues['missing_uvs'] > 0:
|
|
||||||
report_lines.append(t("Validation.missing_uvs",
|
|
||||||
count=uv_issues['missing_uvs']))
|
|
||||||
|
|
||||||
# Show report
|
|
||||||
if report_lines:
|
|
||||||
self.report({'WARNING'}, "\n".join(report_lines))
|
|
||||||
else:
|
|
||||||
self.report({'INFO'}, t("Validation.no_issues"))
|
|
||||||
@@ -14,10 +14,10 @@ from ...core.translations import t
|
|||||||
from ...core.common import (
|
from ...core.common import (
|
||||||
get_active_armature,
|
get_active_armature,
|
||||||
get_all_meshes,
|
get_all_meshes,
|
||||||
validate_armature,
|
|
||||||
clear_unused_data_blocks,
|
clear_unused_data_blocks,
|
||||||
ProgressTracker
|
ProgressTracker
|
||||||
)
|
)
|
||||||
|
from ...core.armature_validation import validate_armature
|
||||||
|
|
||||||
def textures_match(tex1: ShaderNodeTexImage, tex2: ShaderNodeTexImage) -> bool:
|
def textures_match(tex1: ShaderNodeTexImage, tex2: ShaderNodeTexImage) -> bool:
|
||||||
"""Compare two texture nodes for matching properties and image data"""
|
"""Compare two texture nodes for matching properties and image data"""
|
||||||
@@ -92,7 +92,7 @@ class AvatarToolkit_OT_CombineMaterials(Operator):
|
|||||||
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
|
return valid
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ from ...core.translations import t
|
|||||||
from ...core.common import (
|
from ...core.common import (
|
||||||
get_active_armature,
|
get_active_armature,
|
||||||
get_all_meshes,
|
get_all_meshes,
|
||||||
validate_armature,
|
|
||||||
validate_meshes,
|
validate_meshes,
|
||||||
join_mesh_objects,
|
join_mesh_objects,
|
||||||
ProgressTracker
|
ProgressTracker
|
||||||
)
|
)
|
||||||
|
from ...core.armature_validation import validate_armature
|
||||||
|
|
||||||
class AvatarToolkit_OT_JoinAllMeshes(Operator):
|
class AvatarToolkit_OT_JoinAllMeshes(Operator):
|
||||||
"""Operator to join all meshes in the scene"""
|
"""Operator to join all meshes in the scene"""
|
||||||
@@ -25,7 +25,7 @@ class AvatarToolkit_OT_JoinAllMeshes(Operator):
|
|||||||
if not armature:
|
if not armature:
|
||||||
return False
|
return False
|
||||||
valid: bool
|
valid: bool
|
||||||
valid, _ = validate_armature(armature)
|
valid, _, _ = validate_armature(armature)
|
||||||
return valid
|
return valid
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
@@ -69,7 +69,7 @@ class AvatarToolkit_OT_JoinSelectedMeshes(Operator):
|
|||||||
if not armature:
|
if not armature:
|
||||||
return False
|
return False
|
||||||
valid: bool
|
valid: bool
|
||||||
valid, _ = validate_armature(armature)
|
valid, _, _ = validate_armature(armature)
|
||||||
return (valid and
|
return (valid and
|
||||||
context.mode == 'OBJECT' and
|
context.mode == 'OBJECT' and
|
||||||
len([obj for obj in context.selected_objects if obj.type == 'MESH']) > 1)
|
len([obj for obj in context.selected_objects if obj.type == 'MESH']) > 1)
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ from ...core.translations import t
|
|||||||
from ...core.common import (
|
from ...core.common import (
|
||||||
get_active_armature,
|
get_active_armature,
|
||||||
get_all_meshes,
|
get_all_meshes,
|
||||||
validate_armature
|
|
||||||
)
|
)
|
||||||
|
from ...core.armature_validation import validate_armature
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
MERGE_ITERATION_COUNT = 20
|
MERGE_ITERATION_COUNT = 20
|
||||||
@@ -54,6 +54,28 @@ def process_vertex_merging(mesh_data: bpy.types.Mesh, vertices_original: dict[in
|
|||||||
|
|
||||||
return merged_vertices
|
return merged_vertices
|
||||||
|
|
||||||
|
def vertex_moves(mesh_data: bpy.types.Mesh, vertex: int) -> bool:
|
||||||
|
|
||||||
|
for shapekey in mesh_data.shape_keys.key_blocks:
|
||||||
|
data: bpy.types.ShapeKey = shapekey
|
||||||
|
|
||||||
|
if data.points[vertex].co.xyz != mesh_data.vertices[vertex].co.xyz:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def merge_vertex_at_index(mesh_data: bpy.types.Mesh, index: int, distance: float):
|
||||||
|
|
||||||
|
select_target_vertex = [False]*len(mesh_data.vertices)
|
||||||
|
select_target_vertex[index] = True
|
||||||
|
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
mesh_data.vertices.foreach_set("select",select_target_vertex)
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
for _ in range(0,20): #for some reason, if using merge to unselected on a vertex, the vertex will only merge to 1 other vertex. so we gotta spam it to fix it.
|
||||||
|
bpy.ops.mesh.remove_doubles(threshold=distance, use_unselected=True, use_sharp_edge_from_normals=False)
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
class AvatarToolkit_OT_RemoveDoublesAdvanced(Operator):
|
class AvatarToolkit_OT_RemoveDoublesAdvanced(Operator):
|
||||||
bl_idname = "avatar_toolkit.remove_doubles_advanced"
|
bl_idname = "avatar_toolkit.remove_doubles_advanced"
|
||||||
bl_label = t("Optimization.remove_doubles_advanced")
|
bl_label = t("Optimization.remove_doubles_advanced")
|
||||||
@@ -66,7 +88,7 @@ class AvatarToolkit_OT_RemoveDoublesAdvanced(Operator):
|
|||||||
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
|
return valid
|
||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
def execute(self, context: Context) -> set[str]:
|
||||||
@@ -89,7 +111,7 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
|
|||||||
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
|
return valid
|
||||||
|
|
||||||
def draw(self, context: Context) -> None:
|
def draw(self, context: Context) -> None:
|
||||||
@@ -168,7 +190,7 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in modify_mesh: {str(e)}")
|
logger.error(f"Error in modify_mesh: {str(e)}")
|
||||||
|
|
||||||
def modify_mesh_advanced(self, context: Context, mesh_entry: MeshEntry) -> bool:
|
def modify_mesh_advanced(self, context: Context, mesh_entry: MeshEntry) -> int:
|
||||||
"""Advanced mesh modification with shape key handling"""
|
"""Advanced mesh modification with shape key handling"""
|
||||||
try:
|
try:
|
||||||
final_merged_vertex_group = []
|
final_merged_vertex_group = []
|
||||||
@@ -179,26 +201,28 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
|
|||||||
duplicate = create_duplicate_for_merge(context, mesh_entry["mesh"], shapekey_name)
|
duplicate = create_duplicate_for_merge(context, mesh_entry["mesh"], shapekey_name)
|
||||||
vertices_original = {i: v.co.xyz for i, v in enumerate(duplicate.data.vertices)}
|
vertices_original = {i: v.co.xyz for i, v in enumerate(duplicate.data.vertices)}
|
||||||
|
|
||||||
|
|
||||||
|
merge_vertex_at_index(duplicate.data, mesh_entry["cur_vertex_pass"], merge_distance) #merge the vertex at our pass to find vertices that would merge to our vertex at this shapekey.
|
||||||
|
|
||||||
# Process merging
|
# Process merging
|
||||||
merged_vertices = process_vertex_merging(duplicate.data, vertices_original, mesh_entry["cur_vertex_pass"])
|
merged_vertices = process_vertex_merging(duplicate.data, vertices_original, mesh_entry["cur_vertex_pass"]) # find what vertices actually merged.
|
||||||
|
|
||||||
if not initialized_final:
|
if not initialized_final:
|
||||||
final_merged_vertex_group = merged_vertices.copy()
|
final_merged_vertex_group = merged_vertices.copy()
|
||||||
initialized_final = True
|
initialized_final = True
|
||||||
else:
|
else:
|
||||||
final_merged_vertex_group = [v for v in final_merged_vertex_group if v in merged_vertices]
|
final_merged_vertex_group = [v for v in final_merged_vertex_group if v in merged_vertices] # remove vertices that merged from the list if they didn't merge during this shapkey.
|
||||||
|
|
||||||
bpy.ops.object.delete()
|
bpy.ops.object.delete()
|
||||||
|
|
||||||
# Apply final merging
|
# Apply final merging
|
||||||
if final_merged_vertex_group:
|
if final_merged_vertex_group:
|
||||||
self.apply_final_merging(context, mesh_entry, final_merged_vertex_group, merge_distance)
|
self.apply_final_merging(context, mesh_entry, final_merged_vertex_group, merge_distance) # merge all vertices that merged on every shapekey no matter the shapekey during the loop.
|
||||||
|
|
||||||
return not (len(final_merged_vertex_group) > 1)
|
return len(final_merged_vertex_group)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in modify_mesh_advanced: {str(e)}")
|
logger.error(f"Error in modify_mesh_advanced: {str(e)}")
|
||||||
return True
|
return 1
|
||||||
|
|
||||||
def apply_final_merging(self, context: Context, mesh_entry: MeshEntry, vertex_group: list[int], merge_distance: float) -> None:
|
def apply_final_merging(self, context: Context, mesh_entry: MeshEntry, vertex_group: list[int], merge_distance: float) -> None:
|
||||||
"""Apply final vertex merging operations"""
|
"""Apply final vertex merging operations"""
|
||||||
@@ -232,8 +256,6 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
|
|||||||
def finish_mesh_processing(self, context: Context, mesh: MeshEntry, advanced: bool, merge_distance: float) -> None:
|
def finish_mesh_processing(self, context: Context, mesh: MeshEntry, advanced: bool, merge_distance: float) -> None:
|
||||||
"""Complete the mesh processing by performing final merge operations"""
|
"""Complete the mesh processing by performing final merge operations"""
|
||||||
logger.debug("Finishing mesh processing")
|
logger.debug("Finishing mesh processing")
|
||||||
|
|
||||||
if not advanced:
|
|
||||||
mesh["mesh"].select_set(True)
|
mesh["mesh"].select_set(True)
|
||||||
context.view_layer.objects.active = mesh["mesh"]
|
context.view_layer.objects.active = mesh["mesh"]
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
@@ -266,10 +288,21 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
|
|||||||
self.process_simple_mesh(context, mesh, merge_distance)
|
self.process_simple_mesh(context, mesh, merge_distance)
|
||||||
self.objects_to_do.pop(0)
|
self.objects_to_do.pop(0)
|
||||||
|
|
||||||
elif not (mesh["cur_vertex_pass"] > mesh["vertices"]) and advanced:
|
elif not (mesh["cur_vertex_pass"] > mesh["vertices"]) and advanced: #advanced merging vertex by vertex
|
||||||
if self.modify_mesh_advanced(context, mesh):
|
if(mesh["cur_vertex_pass"] < 0): #make sure it doesn't go below 0 and explode when advancing backwards from a previous step
|
||||||
|
mesh["cur_vertex_pass"] = 0
|
||||||
|
|
||||||
|
if vertex_moves(mesh["mesh"].data, mesh["cur_vertex_pass"]): # do not do advanced merging for vertices that don't move
|
||||||
|
mesh["cur_vertex_pass"] -= self.modify_mesh_advanced(context, mesh)-2 #advance forward or backwards based on how many vertices actually got merged, changing the list size.
|
||||||
|
#if above returns 1 (no vertices other than this one being merged to ourselves), advance by 1. else don't advance or go backwards. Makes sure all vertices get merged in the end.
|
||||||
|
else:
|
||||||
mesh["cur_vertex_pass"] += 1
|
mesh["cur_vertex_pass"] += 1
|
||||||
|
|
||||||
|
elif (mesh["cur_vertex_pass"] > mesh["vertices"]) and advanced and len(mesh['shapekeys']) > 0: #after advanced merging has gone past all the moving vertices, now we need to merge non moving vertices.
|
||||||
|
shapekeyname = mesh['shapekeys'].pop(0)
|
||||||
|
mesh["mesh"].active_shape_key_index = mesh_data.shape_keys.key_blocks.find(shapekeyname)
|
||||||
|
logger.debug(f"Processing shapekey {shapekeyname}")
|
||||||
|
self.modify_mesh(context, mesh)
|
||||||
else:
|
else:
|
||||||
self.finish_mesh_processing(context, mesh, advanced, merge_distance)
|
self.finish_mesh_processing(context, mesh, advanced, merge_distance)
|
||||||
self.objects_to_do.pop(0)
|
self.objects_to_do.pop(0)
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ from ..core.common import (
|
|||||||
get_active_armature,
|
get_active_armature,
|
||||||
get_all_meshes,
|
get_all_meshes,
|
||||||
apply_pose_as_rest,
|
apply_pose_as_rest,
|
||||||
validate_armature,
|
|
||||||
cache_vertex_positions,
|
cache_vertex_positions,
|
||||||
apply_vertex_positions,
|
apply_vertex_positions,
|
||||||
validate_mesh_for_pose,
|
validate_mesh_for_pose,
|
||||||
process_armature_modifiers,
|
process_armature_modifiers,
|
||||||
ProgressTracker
|
ProgressTracker
|
||||||
)
|
)
|
||||||
|
from ..core.armature_validation import validate_armature
|
||||||
|
|
||||||
class BatchPoseOperationMixin:
|
class BatchPoseOperationMixin:
|
||||||
"""Base class for batch pose operations"""
|
"""Base class for batch pose operations"""
|
||||||
@@ -23,7 +23,7 @@ class BatchPoseOperationMixin:
|
|||||||
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.mode == 'POSE'
|
return valid and context.mode == 'POSE'
|
||||||
|
|
||||||
def validate_meshes(self, meshes: List[Object]) -> List[Tuple[Object, str]]:
|
def validate_meshes(self, meshes: List[Object]) -> List[Tuple[Object, str]]:
|
||||||
@@ -46,7 +46,7 @@ class AvatarToolkit_OT_StartPoseMode(Operator):
|
|||||||
armature = get_active_armature(context)
|
armature = get_active_armature(context)
|
||||||
if not armature or context.mode == "POSE":
|
if not armature or context.mode == "POSE":
|
||||||
return False
|
return False
|
||||||
valid, _ = validate_armature(armature)
|
valid, _, _ = validate_armature(armature)
|
||||||
return valid
|
return valid
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ from bpy.types import Operator, Context
|
|||||||
from typing import Set
|
from typing import Set
|
||||||
from ...core.translations import t
|
from ...core.translations import t
|
||||||
from ...core.logging_setup import logger
|
from ...core.logging_setup import logger
|
||||||
from ...core.common import get_active_armature, get_all_meshes, validate_armature, remove_unused_shapekeys
|
from ...core.common import get_active_armature, get_all_meshes, remove_unused_shapekeys
|
||||||
|
from ...core.armature_validation import validate_armature
|
||||||
|
|
||||||
class AvatarToolkit_OT_ApplyTransforms(Operator):
|
class AvatarToolkit_OT_ApplyTransforms(Operator):
|
||||||
"""Apply all transformations to armature and associated meshes"""
|
"""Apply all transformations to armature and associated meshes"""
|
||||||
@@ -18,8 +19,8 @@ class AvatarToolkit_OT_ApplyTransforms(Operator):
|
|||||||
armature = get_active_armature(context)
|
armature = get_active_armature(context)
|
||||||
if not armature:
|
if not armature:
|
||||||
return False
|
return False
|
||||||
is_valid, _ = validate_armature(armature)
|
valid, _, _ = validate_armature(armature)
|
||||||
return is_valid and context.mode == 'OBJECT'
|
return valid and context.mode == 'OBJECT'
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
try:
|
try:
|
||||||
@@ -66,8 +67,8 @@ class AvatarToolkit_OT_CleanShapekeys(Operator):
|
|||||||
armature = get_active_armature(context)
|
armature = get_active_armature(context)
|
||||||
if not armature:
|
if not armature:
|
||||||
return False
|
return False
|
||||||
is_valid, _ = validate_armature(armature)
|
valid, _, _ = validate_armature(armature)
|
||||||
return is_valid and context.mode == 'OBJECT' and len(get_all_meshes(context)) > 0
|
return valid and context.mode == 'OBJECT' and len(get_all_meshes(context)) > 0
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -5,12 +5,11 @@ from typing import Optional, Dict, Any, List, Tuple
|
|||||||
from ...core.translations import t
|
from ...core.translations import t
|
||||||
from ...core.common import (
|
from ...core.common import (
|
||||||
get_active_armature,
|
get_active_armature,
|
||||||
validate_armature,
|
|
||||||
get_all_meshes,
|
get_all_meshes,
|
||||||
ProgressTracker,
|
ProgressTracker,
|
||||||
validate_bone_hierarchy,
|
|
||||||
restore_bone_transforms
|
restore_bone_transforms
|
||||||
)
|
)
|
||||||
|
from ...core.armature_validation import validate_armature, validate_bone_hierarchy
|
||||||
|
|
||||||
def duplicate_bone(bone: EditBone) -> EditBone:
|
def duplicate_bone(bone: EditBone) -> EditBone:
|
||||||
"""Create a duplicate of the given bone"""
|
"""Create a duplicate of the given bone"""
|
||||||
@@ -35,8 +34,8 @@ class AvatarToolKit_OT_CreateDigitigradeLegs(Operator):
|
|||||||
armature = get_active_armature(context)
|
armature = get_active_armature(context)
|
||||||
if not armature:
|
if not armature:
|
||||||
return False
|
return False
|
||||||
is_valid, _ = validate_armature(armature)
|
valid, _, _ = validate_armature(armature)
|
||||||
return (is_valid and
|
return (valid and
|
||||||
context.mode == 'EDIT_ARMATURE' and
|
context.mode == 'EDIT_ARMATURE' and
|
||||||
context.selected_editable_bones is not None and
|
context.selected_editable_bones is not None and
|
||||||
len(context.selected_editable_bones) == 2)
|
len(context.selected_editable_bones) == 2)
|
||||||
@@ -129,22 +128,16 @@ class AvatarToolKit_OT_DeleteBoneConstraints(Operator):
|
|||||||
armature = get_active_armature(context)
|
armature = get_active_armature(context)
|
||||||
if not armature:
|
if not armature:
|
||||||
return False
|
return False
|
||||||
is_valid, _ = validate_armature(armature)
|
valid, _, _ = validate_armature(armature)
|
||||||
return is_valid
|
return valid
|
||||||
|
|
||||||
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')
|
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
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
armature.select_set(True)
|
armature.select_set(True)
|
||||||
context.view_layer.objects.active = armature
|
context.view_layer.objects.active = armature
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='POSE')
|
bpy.ops.object.mode_set(mode='POSE')
|
||||||
|
|
||||||
constraints_removed = 0
|
constraints_removed = 0
|
||||||
@@ -157,7 +150,6 @@ class AvatarToolKit_OT_DeleteBoneConstraints(Operator):
|
|||||||
self.report({'INFO'}, t("Tools.clean_constraints_success", count=constraints_removed))
|
self.report({'INFO'}, t("Tools.clean_constraints_success", count=constraints_removed))
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
|
class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
|
||||||
"""Operator to remove bones with no vertex weights"""
|
"""Operator to remove bones with no vertex weights"""
|
||||||
bl_idname = "avatar_toolkit.clean_weights"
|
bl_idname = "avatar_toolkit.clean_weights"
|
||||||
@@ -167,10 +159,37 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
|
|||||||
|
|
||||||
def should_preserve_bone(self, bone_name: str, context: Context) -> bool:
|
def should_preserve_bone(self, bone_name: str, context: Context) -> bool:
|
||||||
"""Check if bone should be preserved based on settings"""
|
"""Check if bone should be preserved based on settings"""
|
||||||
if context.scene.avatar_toolkit.merge_twist_bones:
|
toolkit = context.scene.avatar_toolkit
|
||||||
return "twist" in bone_name.lower()
|
bone = context.active_object.data.bones.get(bone_name)
|
||||||
|
|
||||||
|
if not bone:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if toolkit.preserve_parent_bones and bone.children:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if toolkit.target_bone_type == 'DEFORM' and not bone.use_deform:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if toolkit.target_bone_type == 'NON_DEFORM' and bone.use_deform:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def populate_bone_list(self, context: Context, zero_weight_bones: List[str]) -> None:
|
||||||
|
"""Populate the zero weight bones list"""
|
||||||
|
toolkit = context.scene.avatar_toolkit
|
||||||
|
toolkit.zero_weight_bones.clear()
|
||||||
|
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
for bone_name in zero_weight_bones:
|
||||||
|
bone = armature.data.bones.get(bone_name)
|
||||||
|
if bone:
|
||||||
|
item = toolkit.zero_weight_bones.add()
|
||||||
|
item.name = bone_name
|
||||||
|
item.has_children = len(bone.children) > 0
|
||||||
|
item.is_deform = bone.use_deform
|
||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
def execute(self, context: Context) -> set[str]:
|
||||||
"""Execute the zero weight bone removal operation"""
|
"""Execute the zero weight bone removal operation"""
|
||||||
armature = get_active_armature(context)
|
armature = get_active_armature(context)
|
||||||
@@ -192,6 +211,7 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
|
|||||||
# Get weighted bones
|
# Get weighted bones
|
||||||
weighted_bones: List[str] = []
|
weighted_bones: List[str] = []
|
||||||
meshes = get_all_meshes(context)
|
meshes = get_all_meshes(context)
|
||||||
|
zero_weight_bones: List[str] = []
|
||||||
|
|
||||||
for mesh in meshes:
|
for mesh in meshes:
|
||||||
mesh_data: Mesh = mesh.data
|
mesh_data: Mesh = mesh.data
|
||||||
@@ -209,6 +229,10 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
|
|||||||
if (bone.name not in weighted_bones and
|
if (bone.name not in weighted_bones and
|
||||||
not self.should_preserve_bone(bone.name, context)):
|
not self.should_preserve_bone(bone.name, context)):
|
||||||
|
|
||||||
|
if context.scene.avatar_toolkit.list_only_mode:
|
||||||
|
zero_weight_bones.append(bone.name)
|
||||||
|
continue
|
||||||
|
|
||||||
# Store children data
|
# Store children data
|
||||||
children = bone.children
|
children = bone.children
|
||||||
children_data = {child.name: initial_transforms[child.name] for child in children}
|
children_data = {child.name: initial_transforms[child.name] for child in children}
|
||||||
@@ -227,11 +251,38 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
|
|||||||
for child_name, data in children_data.items():
|
for child_name, data in children_data.items():
|
||||||
if child_name in armature_data.edit_bones:
|
if child_name in armature_data.edit_bones:
|
||||||
child = armature_data.edit_bones[child_name]
|
child = armature_data.edit_bones[child_name]
|
||||||
child.head = data['head']
|
restore_bone_transforms(child, data)
|
||||||
child.tail = data['tail']
|
|
||||||
child.roll = data['roll']
|
|
||||||
child.matrix = data['matrix']
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
|
if context.scene.avatar_toolkit.list_only_mode:
|
||||||
|
self.populate_bone_list(context, zero_weight_bones)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
self.report({'INFO'}, t("Tools.clean_weights_success", count=removed_count))
|
self.report({'INFO'}, t("Tools.clean_weights_success", count=removed_count))
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class AvatarToolKit_OT_RemoveSelectedBones(Operator):
|
||||||
|
"""Operator to remove selected bones from the zero weight bones list"""
|
||||||
|
bl_idname = "avatar_toolkit.remove_selected_bones"
|
||||||
|
bl_label = t("Tools.remove_selected_bones")
|
||||||
|
bl_description = t("Tools.remove_selected_bones_desc")
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> set[str]:
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
toolkit = context.scene.avatar_toolkit
|
||||||
|
|
||||||
|
selected_bones = [item.name for item in toolkit.zero_weight_bones
|
||||||
|
if item.selected]
|
||||||
|
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
for bone_name in selected_bones:
|
||||||
|
if bone_name in armature.data.edit_bones:
|
||||||
|
armature.data.edit_bones.remove(armature.data.edit_bones[bone_name])
|
||||||
|
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
toolkit.zero_weight_bones.clear()
|
||||||
|
|
||||||
|
self.report({'INFO'}, t("Tools.bones_removed", count=len(selected_bones)))
|
||||||
|
return {'FINISHED'}
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import bpy
|
|
||||||
import re
|
|
||||||
from typing import Set, Dict, Optional
|
|
||||||
from bpy.types import Operator, Context
|
|
||||||
from ...core.translations import t
|
|
||||||
from ...core.logging_setup import logger
|
|
||||||
from ...core.common import get_active_armature, simplify_bonename, validate_armature, ProgressTracker
|
|
||||||
from ...core.dictionaries import bone_names, resonite_translations
|
|
||||||
|
|
||||||
class AvatarToolkit_OT_ConvertResonite(Operator):
|
|
||||||
"""Convert armature bone names to Resonite format with progress tracking and validation"""
|
|
||||||
bl_idname = "avatar_toolkit.convert_resonite"
|
|
||||||
bl_label = t("Tools.convert_resonite")
|
|
||||||
bl_description = t("Tools.convert_resonite_desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context: Context) -> bool:
|
|
||||||
armature = get_active_armature(context)
|
|
||||||
if not armature:
|
|
||||||
return False
|
|
||||||
is_valid, _ = validate_armature(armature)
|
|
||||||
return is_valid
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
|
||||||
armature = get_active_armature(context)
|
|
||||||
if not armature:
|
|
||||||
logger.warning("No armature selected for Resonite conversion")
|
|
||||||
self.report({'WARNING'}, t("Armature.validation.no_armature"))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
translate_bone_fails: int = 0
|
|
||||||
untranslated_bones: Set[str] = set()
|
|
||||||
simplified_names: Dict[str, str] = {}
|
|
||||||
|
|
||||||
# Create reverse lookup dictionary
|
|
||||||
reverse_bone_lookup = {}
|
|
||||||
for preferred_name, name_list in bone_names.items():
|
|
||||||
for name in name_list:
|
|
||||||
reverse_bone_lookup[name] = preferred_name
|
|
||||||
|
|
||||||
try:
|
|
||||||
context.view_layer.objects.active = armature
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
# Cache simplified bone names
|
|
||||||
for bone in armature.data.bones:
|
|
||||||
simplified_names[bone.name] = simplify_bonename(bone.name)
|
|
||||||
|
|
||||||
total_bones = len(armature.data.bones)
|
|
||||||
with ProgressTracker(context, total_bones, t("Tools.convert_resonite.operation")) as progress:
|
|
||||||
for bone in armature.data.bones:
|
|
||||||
# Remove any existing "<noik>" tags
|
|
||||||
bone.name = re.compile(re.escape("<noik>"), re.IGNORECASE).sub("", bone.name)
|
|
||||||
simplified_name = simplified_names[bone.name]
|
|
||||||
|
|
||||||
if simplified_name in reverse_bone_lookup and reverse_bone_lookup[simplified_name] in resonite_translations:
|
|
||||||
new_name = resonite_translations[reverse_bone_lookup[simplified_name]]
|
|
||||||
logger.debug(f"Translating bone: {bone.name} -> {new_name}")
|
|
||||||
bone.name = new_name
|
|
||||||
else:
|
|
||||||
untranslated_bones.add(bone.name)
|
|
||||||
bone.name = bone.name + "<noik>"
|
|
||||||
translate_bone_fails += 1
|
|
||||||
logger.debug(f"Failed to translate bone: {bone.name}")
|
|
||||||
|
|
||||||
progress.step(t("Tools.convert_resonite.processing", name=bone.name))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error during Resonite conversion: {str(e)}")
|
|
||||||
self.report({'ERROR'}, str(e))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
finally:
|
|
||||||
try:
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Error returning to object mode: {str(e)}")
|
|
||||||
|
|
||||||
if translate_bone_fails > 0:
|
|
||||||
logger.info(f"Conversion completed with {translate_bone_fails} untranslated bones")
|
|
||||||
logger.debug(f"Untranslated bones: {untranslated_bones}")
|
|
||||||
self.report({'INFO'}, t("Tools.bones_translated_with_fails", translate_bone_fails=translate_bone_fails))
|
|
||||||
else:
|
|
||||||
logger.info("All bones translated successfully")
|
|
||||||
self.report({'INFO'}, t("Tools.bones_translated_success"))
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
@@ -4,7 +4,8 @@ from typing import Set, List
|
|||||||
from bpy.types import Operator, Context, Armature, EditBone
|
from bpy.types import Operator, Context, Armature, EditBone
|
||||||
from ...core.translations import t
|
from ...core.translations import t
|
||||||
from ...core.logging_setup import logger
|
from ...core.logging_setup import logger
|
||||||
from ...core.common import get_active_armature, get_all_meshes, get_vertex_weights, transfer_vertex_weights, validate_armature
|
from ...core.common import get_active_armature, get_all_meshes, get_vertex_weights, transfer_vertex_weights
|
||||||
|
from ...core.armature_validation import validate_armature
|
||||||
|
|
||||||
class AvatarToolkit_OT_ConnectBones(Operator):
|
class AvatarToolkit_OT_ConnectBones(Operator):
|
||||||
"""Connect disconnected bones in chain"""
|
"""Connect disconnected bones in chain"""
|
||||||
@@ -18,8 +19,8 @@ class AvatarToolkit_OT_ConnectBones(Operator):
|
|||||||
armature = get_active_armature(context)
|
armature = get_active_armature(context)
|
||||||
if not armature:
|
if not armature:
|
||||||
return False
|
return False
|
||||||
is_valid, _ = validate_armature(armature)
|
valid, _, _ = validate_armature(armature)
|
||||||
return is_valid
|
return valid
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import bpy
|
import bpy
|
||||||
from bpy.types import Operator, Context
|
from bpy.types import Operator, Context
|
||||||
from ...core.translations import t
|
from ...core.translations import t
|
||||||
from ...core.common import get_active_armature, validate_armature
|
from ...core.common import get_active_armature
|
||||||
|
from ...core.armature_validation import validate_armature
|
||||||
|
|
||||||
class AvatarToolKit_OT_SeparateByMaterials(Operator):
|
class AvatarToolKit_OT_SeparateByMaterials(Operator):
|
||||||
"""Operator to separate mesh by materials"""
|
"""Operator to separate mesh by materials"""
|
||||||
@@ -16,10 +17,10 @@ class AvatarToolKit_OT_SeparateByMaterials(Operator):
|
|||||||
armature = get_active_armature(context)
|
armature = get_active_armature(context)
|
||||||
if not armature:
|
if not armature:
|
||||||
return False
|
return False
|
||||||
is_valid, _ = validate_armature(armature)
|
valid, _, _ = validate_armature(armature)
|
||||||
return (context.active_object and
|
return (context.active_object and
|
||||||
context.active_object.type == 'MESH' and
|
context.active_object.type == 'MESH' and
|
||||||
is_valid)
|
valid)
|
||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
def execute(self, context: Context) -> set[str]:
|
||||||
"""Execute the separation operation"""
|
"""Execute the separation operation"""
|
||||||
@@ -48,10 +49,10 @@ class AvatarToolKit_OT_SeparateByLooseParts(Operator):
|
|||||||
armature = get_active_armature(context)
|
armature = get_active_armature(context)
|
||||||
if not armature:
|
if not armature:
|
||||||
return False
|
return False
|
||||||
is_valid, _ = validate_armature(armature)
|
valid, _, _ = validate_armature(armature)
|
||||||
return (context.active_object and
|
return (context.active_object and
|
||||||
context.active_object.type == 'MESH' and
|
context.active_object.type == 'MESH' and
|
||||||
is_valid)
|
valid)
|
||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
def execute(self, context: Context) -> set[str]:
|
||||||
"""Execute the separation operation"""
|
"""Execute the separation operation"""
|
||||||
|
|||||||
@@ -0,0 +1,256 @@
|
|||||||
|
import bpy
|
||||||
|
from typing import Dict, List, Set, Optional, Tuple, Any
|
||||||
|
from bpy.types import Operator, Context, Object, PoseBone, EditBone, Bone, Constraint
|
||||||
|
from ...core.common import get_active_armature
|
||||||
|
from ...core.logging_setup import logger
|
||||||
|
from ...core.translations import t
|
||||||
|
from ...core.dictionaries import rigify_unity_names, rigify_basic_unity_names, rigify_unnecessary_bones
|
||||||
|
from ...core.armature_validation import validate_armature
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_ConvertRigifyToUnity(Operator):
|
||||||
|
"""Convert Rigify armature to Unity-compatible format"""
|
||||||
|
bl_idname = "avatar_toolkit.convert_rigify_to_unity"
|
||||||
|
bl_label = t("Tools.convert_rigify_to_unity")
|
||||||
|
bl_description = t("Tools.convert_rigify_to_unity_desc")
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context: Context) -> bool:
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
if not armature:
|
||||||
|
return False
|
||||||
|
return ("DEF-spine" in armature.data.bones or
|
||||||
|
"spine" in armature.data.bones and "metarig" in armature.name.lower())
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
try:
|
||||||
|
logger.info("Starting Rigify to Unity conversion")
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
if not armature:
|
||||||
|
logger.error("No armature found")
|
||||||
|
self.report({'ERROR'}, t("Tools.no_armature"))
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
logger.debug(f"Converting armature: {armature.name}")
|
||||||
|
armature.name = "Armature"
|
||||||
|
armature.data.name = "Armature"
|
||||||
|
logger.debug("Renamed armature to 'Armature'")
|
||||||
|
|
||||||
|
if "DEF-spine" in armature.data.bones:
|
||||||
|
logger.info("Processing DEF bones")
|
||||||
|
self.move_def_bones(armature)
|
||||||
|
self.rename_bones_for_unity(armature)
|
||||||
|
else:
|
||||||
|
logger.info("Processing basic bones")
|
||||||
|
self.cleanup_extra_bones(armature)
|
||||||
|
self.rename_basic_bones_for_unity(armature)
|
||||||
|
|
||||||
|
logger.debug("Cleaning up bone collections")
|
||||||
|
self.cleanup_bone_collections(armature)
|
||||||
|
|
||||||
|
if context.scene.avatar_toolkit.merge_twist_bones:
|
||||||
|
logger.info("Merging twist bones")
|
||||||
|
self.handle_twist_bones(armature)
|
||||||
|
|
||||||
|
logger.info("Successfully converted Rigify armature to Unity format")
|
||||||
|
self.report({'INFO'}, t("Tools.rigify_converted"))
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to convert Rigify: {str(e)}", exc_info=True)
|
||||||
|
self.report({'ERROR'}, str(e))
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
def cleanup_extra_bones(self, armature: Object) -> None:
|
||||||
|
"""Remove unnecessary bones and merge neck bones"""
|
||||||
|
logger.debug("Starting cleanup of extra bones")
|
||||||
|
|
||||||
|
# Set armature as active object before mode switch
|
||||||
|
bpy.context.view_layer.objects.active = armature
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
|
||||||
|
bones_to_remove: List[str] = []
|
||||||
|
for bone in armature.data.edit_bones:
|
||||||
|
if any(pattern in bone.name.lower() for pattern in rigify_unnecessary_bones):
|
||||||
|
bones_to_remove.append(bone.name)
|
||||||
|
|
||||||
|
for bone_name in bones_to_remove:
|
||||||
|
if bone_name in armature.data.edit_bones:
|
||||||
|
logger.debug(f"Removing bone: {bone_name}")
|
||||||
|
armature.data.edit_bones.remove(armature.data.edit_bones[bone_name])
|
||||||
|
|
||||||
|
if 'spine.004' in armature.data.edit_bones and 'spine.005' in armature.data.edit_bones:
|
||||||
|
logger.debug("Merging neck bones")
|
||||||
|
neck_start = armature.data.edit_bones['spine.004']
|
||||||
|
neck_end = armature.data.edit_bones['spine.005']
|
||||||
|
neck_start.tail = neck_end.tail
|
||||||
|
armature.data.edit_bones.remove(neck_end)
|
||||||
|
neck_start.name = "Neck"
|
||||||
|
|
||||||
|
if 'spine.006' in armature.data.edit_bones:
|
||||||
|
logger.debug("Renaming head bone")
|
||||||
|
head_bone = armature.data.edit_bones['spine.006']
|
||||||
|
head_bone.name = "Head"
|
||||||
|
|
||||||
|
def move_def_bones(self, armature: Object) -> None:
|
||||||
|
"""Move DEF bones to their correct positions"""
|
||||||
|
logger.debug("Moving DEF bones to correct positions")
|
||||||
|
|
||||||
|
# Set armature as active object
|
||||||
|
bpy.context.view_layer.objects.active = armature
|
||||||
|
remap: Dict[str, str] = self.get_org_remap(armature)
|
||||||
|
remap.update(self.get_special_remap())
|
||||||
|
|
||||||
|
remove_bones_in_chain: List[str] = [
|
||||||
|
'DEF-upper_arm.L.001', 'DEF-forearm.L.001',
|
||||||
|
'DEF-upper_arm.R.001', 'DEF-forearm.R.001',
|
||||||
|
'DEF-thigh.L.001', 'DEF-shin.L.001',
|
||||||
|
'DEF-thigh.R.001', 'DEF-shin.R.001'
|
||||||
|
]
|
||||||
|
|
||||||
|
transform_copies: List[str] = self.get_transform_copies(armature)
|
||||||
|
|
||||||
|
logger.debug("Setting up transform copies")
|
||||||
|
bpy.ops.object.mode_set(mode='POSE')
|
||||||
|
for bone_name in transform_copies:
|
||||||
|
bone = armature.pose.bones[bone_name]
|
||||||
|
org_name = 'ORG-' + self.get_proto_name(bone_name)
|
||||||
|
if org_name in armature.pose.bones:
|
||||||
|
constraint = bone.constraints.new('COPY_TRANSFORMS')
|
||||||
|
constraint.target = armature
|
||||||
|
constraint.subtarget = org_name
|
||||||
|
constr_count = len(bone.constraints)
|
||||||
|
if constr_count > 1:
|
||||||
|
bone.constraints.move(constr_count-1, 0)
|
||||||
|
|
||||||
|
logger.debug("Remapping bone parents")
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
for remap_key in remap:
|
||||||
|
if remap_key in armature.data.edit_bones and remap[remap_key] in armature.data.edit_bones:
|
||||||
|
armature.data.edit_bones[remap_key].parent = armature.data.edit_bones[remap[remap_key]]
|
||||||
|
|
||||||
|
logger.debug("Processing bone chain removal")
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
for bone_name in remove_bones_in_chain:
|
||||||
|
if bone_name in armature.data.bones:
|
||||||
|
armature.data.bones[bone_name].use_deform = False
|
||||||
|
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
for bone_name in remove_bones_in_chain:
|
||||||
|
if bone_name in armature.data.bones:
|
||||||
|
remove_bone = armature.data.edit_bones[bone_name]
|
||||||
|
parent_bone = remove_bone.parent
|
||||||
|
parent_bone.tail = remove_bone.tail
|
||||||
|
retarget_bones = list(remove_bone.children)
|
||||||
|
for bone in retarget_bones:
|
||||||
|
bone.parent = parent_bone
|
||||||
|
armature.data.edit_bones.remove(remove_bone)
|
||||||
|
|
||||||
|
def rename_bones_for_unity(self, armature: Object) -> None:
|
||||||
|
"""Rename bones to Unity-compatible names"""
|
||||||
|
logger.debug("Renaming bones to Unity format")
|
||||||
|
for old_name, new_name in rigify_unity_names.items():
|
||||||
|
bone = armature.pose.bones.get(old_name)
|
||||||
|
if bone:
|
||||||
|
logger.debug(f"Renaming bone: {old_name} -> {new_name}")
|
||||||
|
bone.name = new_name
|
||||||
|
|
||||||
|
def rename_basic_bones_for_unity(self, armature: Object) -> None:
|
||||||
|
"""Rename basic metarig bones to Unity-compatible names"""
|
||||||
|
logger.debug("Renaming basic metarig bones")
|
||||||
|
for old_name, new_name in rigify_basic_unity_names.items():
|
||||||
|
bone = armature.pose.bones.get(old_name)
|
||||||
|
if bone:
|
||||||
|
logger.debug(f"Renaming basic bone: {old_name} -> {new_name}")
|
||||||
|
bone.name = new_name
|
||||||
|
|
||||||
|
def cleanup_bone_collections(self, armature: Object) -> None:
|
||||||
|
"""Remove all bone collections since they're not needed for Unity"""
|
||||||
|
logger.debug("Cleaning up bone collections")
|
||||||
|
if hasattr(armature.data, 'collections') and armature.data.collections:
|
||||||
|
while len(armature.data.collections) > 0:
|
||||||
|
collection = armature.data.collections[0]
|
||||||
|
armature.data.collections.remove(collection)
|
||||||
|
|
||||||
|
while len(armature.data.collections) > 1:
|
||||||
|
collection = armature.data.collections[1]
|
||||||
|
armature.data.collections.remove(collection)
|
||||||
|
|
||||||
|
def handle_twist_bones(self, armature: Object) -> None:
|
||||||
|
"""Handle twist bones during conversion"""
|
||||||
|
logger.debug("Processing twist bones")
|
||||||
|
twist_bones: List[Tuple[str, str]] = [
|
||||||
|
("DEF-upper_arm_twist.L", "DEF-upper_arm.L"),
|
||||||
|
("DEF-upper_arm_twist.R", "DEF-upper_arm.R"),
|
||||||
|
("DEF-forearm_twist.L", "DEF-forearm.L"),
|
||||||
|
("DEF-forearm_twist.R", "DEF-forearm.R"),
|
||||||
|
("DEF-thigh_twist.L", "DEF-thigh.L"),
|
||||||
|
("DEF-thigh_twist.R", "DEF-thigh.R")
|
||||||
|
]
|
||||||
|
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
for twist_bone, parent_bone in twist_bones:
|
||||||
|
if twist_bone in armature.data.edit_bones and parent_bone in armature.data.edit_bones:
|
||||||
|
logger.debug(f"Merging twist bone: {twist_bone} into {parent_bone}")
|
||||||
|
twist = armature.data.edit_bones[twist_bone]
|
||||||
|
parent = armature.data.edit_bones[parent_bone]
|
||||||
|
parent.tail = twist.tail
|
||||||
|
for child in twist.children:
|
||||||
|
child.parent = parent
|
||||||
|
armature.data.edit_bones.remove(twist)
|
||||||
|
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
|
def get_org_remap(self, armature: Object) -> Dict[str, str]:
|
||||||
|
"""Get original bone remapping"""
|
||||||
|
logger.debug("Getting original bone remapping")
|
||||||
|
remap: Dict[str, str] = {}
|
||||||
|
for bone in armature.data.bones:
|
||||||
|
if self.is_def_bone(bone.name):
|
||||||
|
name = self.get_proto_name(bone.name)
|
||||||
|
parent = bone.parent
|
||||||
|
while parent:
|
||||||
|
parent_name = self.get_proto_name(parent.name)
|
||||||
|
if parent_name != name:
|
||||||
|
if ('DEF-' + parent_name) in armature.data.bones:
|
||||||
|
remap[bone.name] = 'DEF-' + parent_name
|
||||||
|
break
|
||||||
|
parent = parent.parent
|
||||||
|
return remap
|
||||||
|
|
||||||
|
def get_special_remap(self) -> Dict[str, str]:
|
||||||
|
"""Get special bone remapping cases"""
|
||||||
|
logger.debug("Getting special bone remapping")
|
||||||
|
return {
|
||||||
|
'DEF-thigh.L': 'DEF-pelvis.L',
|
||||||
|
'DEF-thigh.R': 'DEF-pelvis.R',
|
||||||
|
'DEF-upper_arm.L': 'DEF-shoulder.L',
|
||||||
|
'DEF-upper_arm.R': 'DEF-shoulder.R',
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_transform_copies(self, armature: Object) -> List[str]:
|
||||||
|
"""Get bones that need transform copies"""
|
||||||
|
logger.debug("Getting transform copy bones")
|
||||||
|
result: List[str] = []
|
||||||
|
for bone in armature.pose.bones:
|
||||||
|
if self.is_def_bone(bone.name) and not self.has_transform_copies(bone):
|
||||||
|
result.append(bone.name)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def has_transform_copies(self, bone: PoseBone) -> bool:
|
||||||
|
"""Check if bone has transform copy constraints"""
|
||||||
|
return any(constraint.type == 'COPY_TRANSFORMS' for constraint in bone.constraints)
|
||||||
|
|
||||||
|
def is_def_bone(self, bone_name: str) -> bool:
|
||||||
|
"""Check if bone is a DEF bone"""
|
||||||
|
return bone_name.startswith('DEF-')
|
||||||
|
|
||||||
|
def is_org_bone(self, bone_name: str) -> bool:
|
||||||
|
"""Check if bone is an ORG bone"""
|
||||||
|
return bone_name.startswith('ORG-')
|
||||||
|
|
||||||
|
def get_proto_name(self, bone_name: str) -> str:
|
||||||
|
"""Get the prototype name of a bone"""
|
||||||
|
if self.is_def_bone(bone_name) or self.is_org_bone(bone_name):
|
||||||
|
return bone_name[4:]
|
||||||
|
return bone_name
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
import bpy
|
||||||
|
import math
|
||||||
|
from typing import Dict, List, Set, Tuple, Optional, Any, Union
|
||||||
|
from bpy.types import Operator, Context, Object, EditBone, Bone
|
||||||
|
from ...core.translations import t
|
||||||
|
from ...core.logging_setup import logger
|
||||||
|
from ...core.common import get_active_armature, ProgressTracker
|
||||||
|
from ...core.armature_validation import validate_armature
|
||||||
|
from ...core.dictionaries import (
|
||||||
|
standard_bones,
|
||||||
|
bone_names,
|
||||||
|
bone_hierarchy,
|
||||||
|
acceptable_bone_names,
|
||||||
|
acceptable_bone_hierarchy,
|
||||||
|
non_standard_mappings
|
||||||
|
)
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_StandardizeArmature(Operator):
|
||||||
|
"""Standardize armature bone names and hierarchy to match Avatar Toolkit requirements"""
|
||||||
|
bl_idname: str = "avatar_toolkit.standardize_armature"
|
||||||
|
bl_label: str = t("Tools.standardize_armature")
|
||||||
|
bl_description: str = t("Tools.standardize_armature_desc")
|
||||||
|
bl_options: Set[str] = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context: Context) -> bool:
|
||||||
|
armature: Optional[Object] = get_active_armature(context)
|
||||||
|
return armature is not None and context.mode in {'OBJECT', 'EDIT_ARMATURE'}
|
||||||
|
|
||||||
|
def invoke(self, context: Context, event: Any) -> Set[str]:
|
||||||
|
logger.debug("Invoking standardize armature dialog")
|
||||||
|
return context.window_manager.invoke_props_dialog(self)
|
||||||
|
|
||||||
|
def draw(self, context: Context) -> None:
|
||||||
|
layout = self.layout
|
||||||
|
toolkit = context.scene.avatar_toolkit
|
||||||
|
|
||||||
|
layout.prop(toolkit, "standardize_fix_names")
|
||||||
|
layout.prop(toolkit, "standardize_fix_hierarchy")
|
||||||
|
layout.prop(toolkit, "standardize_fix_scale")
|
||||||
|
layout.separator()
|
||||||
|
layout.label(text=t("Tools.standardize_warning"), icon='ERROR')
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
armature: Optional[Object] = get_active_armature(context)
|
||||||
|
toolkit = context.scene.avatar_toolkit
|
||||||
|
|
||||||
|
if not armature:
|
||||||
|
logger.warning("No active armature found for standardization")
|
||||||
|
self.report({'ERROR'}, t("Validation.no_armature"))
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
logger.info(f"Starting armature standardization for {armature.name}")
|
||||||
|
|
||||||
|
is_valid, _, _ = validate_armature(armature)
|
||||||
|
if is_valid:
|
||||||
|
logger.info("Armature already meets standards, no changes needed")
|
||||||
|
self.report({'INFO'}, t("Tools.standardize_already_valid"))
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
original_mode: str = context.mode
|
||||||
|
logger.debug(f"Original mode: {original_mode}")
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
context.view_layer.objects.active = armature
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
|
||||||
|
try:
|
||||||
|
with ProgressTracker(context, 3, "Standardizing Armature") as progress:
|
||||||
|
# Step 1: Fix bone names
|
||||||
|
if toolkit.standardize_fix_names:
|
||||||
|
progress.step("Fixing bone names")
|
||||||
|
renamed_bones: Dict[str, str] = self.standardize_bone_names(armature)
|
||||||
|
logger.info(f"Renamed {len(renamed_bones)} bones")
|
||||||
|
for old_name, new_name in renamed_bones.items():
|
||||||
|
logger.debug(f"Renamed bone: {old_name} -> {new_name}")
|
||||||
|
|
||||||
|
# Step 2: Fix hierarchy
|
||||||
|
if toolkit.standardize_fix_hierarchy:
|
||||||
|
progress.step("Fixing bone hierarchy")
|
||||||
|
fixed_hierarchy: int = self.standardize_bone_hierarchy(armature)
|
||||||
|
logger.info(f"Fixed {fixed_hierarchy} hierarchy relationships")
|
||||||
|
|
||||||
|
# Step 3: Fix scale issues
|
||||||
|
if toolkit.standardize_fix_scale:
|
||||||
|
progress.step("Fixing bone scale")
|
||||||
|
fixed_scale: int = self.standardize_bone_scale(armature)
|
||||||
|
logger.info(f"Fixed {fixed_scale} scale issues")
|
||||||
|
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
is_valid, messages, _ = validate_armature(armature)
|
||||||
|
|
||||||
|
if is_valid:
|
||||||
|
logger.info("Armature successfully standardized")
|
||||||
|
self.report({'INFO'}, t("Tools.standardize_success"))
|
||||||
|
else:
|
||||||
|
logger.warning(f"Armature partially standardized. {len(messages)} issues remain")
|
||||||
|
bpy.ops.avatar_toolkit.standardize_issues_popup('INVOKE_DEFAULT')
|
||||||
|
self.report({'WARNING'}, t("Tools.standardize_partial"))
|
||||||
|
|
||||||
|
if original_mode == 'EDIT_ARMATURE':
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to standardize armature: {str(e)}")
|
||||||
|
self.report({'ERROR'}, str(e))
|
||||||
|
|
||||||
|
try:
|
||||||
|
if original_mode == 'EDIT_ARMATURE':
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
else:
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
except Exception as restore_error:
|
||||||
|
logger.error(f"Failed to restore original mode: {str(restore_error)}")
|
||||||
|
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
def standardize_bone_names(self, armature: Object) -> Dict[str, str]:
|
||||||
|
"""Rename bones to match standard naming conventions"""
|
||||||
|
logger.debug("Starting bone name standardization")
|
||||||
|
renamed_bones: Dict[str, str] = {}
|
||||||
|
edit_bones = armature.data.edit_bones
|
||||||
|
|
||||||
|
# First, check which standard bones already exist
|
||||||
|
existing_standard_bones: Set[str] = set()
|
||||||
|
for bone in edit_bones:
|
||||||
|
if bone.name in standard_bones.values():
|
||||||
|
existing_standard_bones.add(bone.name)
|
||||||
|
logger.debug(f"Found existing standard bone: {bone.name}")
|
||||||
|
|
||||||
|
# Build a mapping of non-standard bone names to standard names
|
||||||
|
name_mapping: Dict[str, str] = {}
|
||||||
|
for category, standard_name in standard_bones.items():
|
||||||
|
# Skip if this standard bone already exists
|
||||||
|
if standard_name in existing_standard_bones:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get all variants for this category
|
||||||
|
if category in non_standard_mappings:
|
||||||
|
for variant in non_standard_mappings[category]:
|
||||||
|
name_mapping[variant.lower()] = standard_name
|
||||||
|
|
||||||
|
# First pass: identify bones to rename
|
||||||
|
bones_to_rename: Dict[str, str] = {}
|
||||||
|
for bone in edit_bones:
|
||||||
|
original_name: str = bone.name
|
||||||
|
|
||||||
|
# Skip if this is already a standard bone name
|
||||||
|
if original_name in standard_bones.values():
|
||||||
|
continue
|
||||||
|
|
||||||
|
simplified_name: str = original_name.lower().replace(' ', '').replace('_', '').replace('.', '')
|
||||||
|
|
||||||
|
# Check if this bone matches any known pattern
|
||||||
|
for variant, standard_name in name_mapping.items():
|
||||||
|
# More precise matching - exact match or with common separators
|
||||||
|
if (variant == simplified_name or
|
||||||
|
variant == original_name.lower() or
|
||||||
|
f"{variant}_" in simplified_name or
|
||||||
|
f"{variant}." in simplified_name):
|
||||||
|
|
||||||
|
if original_name != standard_name:
|
||||||
|
bones_to_rename[original_name] = standard_name
|
||||||
|
logger.debug(f"Identified bone to rename: {original_name} -> {standard_name}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Special case for spine/chest hierarchy
|
||||||
|
# If we don't have an upper chest, don't rename chest to upper chest because it will break hierarchy
|
||||||
|
has_chest: bool = False
|
||||||
|
has_upper_chest: bool = False
|
||||||
|
|
||||||
|
for bone_name in edit_bones.keys():
|
||||||
|
if bone_name == standard_bones['chest']:
|
||||||
|
has_chest = True
|
||||||
|
elif bone_name == standard_bones['upper_chest']:
|
||||||
|
has_upper_chest = True
|
||||||
|
|
||||||
|
# If we have a chest but no upper chest, don't rename anything to upper chest
|
||||||
|
if has_chest and not has_upper_chest:
|
||||||
|
for original_name, new_name in list(bones_to_rename.items()):
|
||||||
|
if new_name == standard_bones['upper_chest']:
|
||||||
|
logger.debug(f"Skipping upper chest rename for {original_name} as chest already exists")
|
||||||
|
del bones_to_rename[original_name]
|
||||||
|
|
||||||
|
# Second pass: rename bones (in reverse to avoid naming conflicts)
|
||||||
|
for original_name, new_name in sorted(bones_to_rename.items(), reverse=True):
|
||||||
|
if original_name in edit_bones:
|
||||||
|
temp_name: str = f"TEMP_{original_name}"
|
||||||
|
edit_bones[original_name].name = temp_name
|
||||||
|
renamed_bones[original_name] = new_name
|
||||||
|
logger.debug(f"Temporarily renamed: {original_name} -> {temp_name}")
|
||||||
|
|
||||||
|
# Third pass: apply final names
|
||||||
|
for original_name, new_name in renamed_bones.items():
|
||||||
|
temp_name: str = f"TEMP_{original_name}"
|
||||||
|
if temp_name in edit_bones:
|
||||||
|
edit_bones[temp_name].name = new_name
|
||||||
|
logger.debug(f"Applied final rename: {temp_name} -> {new_name}")
|
||||||
|
|
||||||
|
logger.info(f"Standardized {len(renamed_bones)} bone names")
|
||||||
|
return renamed_bones
|
||||||
|
|
||||||
|
def standardize_bone_hierarchy(self, armature: Object) -> int:
|
||||||
|
"""Fix bone hierarchy to match standard relationships"""
|
||||||
|
logger.debug("Starting bone hierarchy standardization")
|
||||||
|
edit_bones = armature.data.edit_bones
|
||||||
|
fixed_count: int = 0
|
||||||
|
|
||||||
|
# Build a mapping of standard bone names to their expected parents
|
||||||
|
hierarchy_map: Dict[str, str] = {}
|
||||||
|
for parent, child in bone_hierarchy:
|
||||||
|
if parent in edit_bones and child in edit_bones:
|
||||||
|
hierarchy_map[child] = parent
|
||||||
|
logger.debug(f"Found standard hierarchy: {parent} -> {child}")
|
||||||
|
|
||||||
|
for parent, child in acceptable_bone_hierarchy:
|
||||||
|
if parent in edit_bones and child in edit_bones:
|
||||||
|
# Only add if not already in the map
|
||||||
|
if child not in hierarchy_map:
|
||||||
|
hierarchy_map[child] = parent
|
||||||
|
logger.debug(f"Found acceptable hierarchy: {parent} -> {child}")
|
||||||
|
|
||||||
|
for child_name, parent_name in hierarchy_map.items():
|
||||||
|
if child_name in edit_bones and parent_name in edit_bones:
|
||||||
|
child_bone: EditBone = edit_bones[child_name]
|
||||||
|
parent_bone: EditBone = edit_bones[parent_name]
|
||||||
|
|
||||||
|
if child_bone.parent != parent_bone:
|
||||||
|
logger.debug(f"Fixing hierarchy: {child_name} parent was {child_bone.parent.name if child_bone.parent else 'None'}, setting to {parent_name}")
|
||||||
|
child_bone.parent = parent_bone
|
||||||
|
fixed_count += 1
|
||||||
|
|
||||||
|
logger.info(f"Fixed {fixed_count} bone hierarchy relationships")
|
||||||
|
return fixed_count
|
||||||
|
|
||||||
|
def standardize_bone_scale(self, armature: Object) -> int:
|
||||||
|
"""Fix bone scale issues by normalizing bone lengths"""
|
||||||
|
logger.debug("Starting bone scale standardization")
|
||||||
|
edit_bones = armature.data.edit_bones
|
||||||
|
fixed_count: int = 0
|
||||||
|
|
||||||
|
# Calculate median bone length for reference
|
||||||
|
lengths: List[float] = [bone.length for bone in edit_bones if bone.length > 0.0001]
|
||||||
|
if not lengths:
|
||||||
|
logger.warning("No valid bone lengths found for scale standardization")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
lengths.sort()
|
||||||
|
median_length: float = lengths[len(lengths) // 2]
|
||||||
|
logger.debug(f"Median bone length: {median_length}")
|
||||||
|
|
||||||
|
# Calculate mean and standard deviation
|
||||||
|
mean: float = sum(lengths) / len(lengths)
|
||||||
|
variance: float = sum((l - mean) ** 2 for l in lengths) / len(lengths)
|
||||||
|
std_dev: float = math.sqrt(variance)
|
||||||
|
logger.debug(f"Mean bone length: {mean}, Standard deviation: {std_dev}")
|
||||||
|
|
||||||
|
small_threshold: float = max(median_length * 0.05, mean - 3 * std_dev)
|
||||||
|
large_threshold: float = min(median_length * 15, mean + 5 * std_dev)
|
||||||
|
logger.debug(f"Scale thresholds - small: {small_threshold}, large: {large_threshold}")
|
||||||
|
|
||||||
|
for bone in edit_bones:
|
||||||
|
is_finger: bool = any(finger in bone.name.lower() for finger in ['thumb', 'index', 'middle', 'ring', 'pinky', 'finger'])
|
||||||
|
|
||||||
|
if bone.length < small_threshold and not is_finger:
|
||||||
|
old_length: float = bone.length
|
||||||
|
bone.length = small_threshold
|
||||||
|
logger.debug(f"Fixed small bone {bone.name}: {old_length} -> {bone.length}")
|
||||||
|
fixed_count += 1
|
||||||
|
elif bone.length > large_threshold:
|
||||||
|
old_length: float = bone.length
|
||||||
|
bone.length = large_threshold
|
||||||
|
logger.debug(f"Fixed large bone {bone.name}: {old_length} -> {bone.length}")
|
||||||
|
fixed_count += 1
|
||||||
|
|
||||||
|
logger.info(f"Fixed {fixed_count} bone scale issues")
|
||||||
|
return fixed_count
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_StandardizeIssuesPopup(Operator):
|
||||||
|
"""Display information about remaining issues after standardization"""
|
||||||
|
bl_idname: str = "avatar_toolkit.standardize_issues_popup"
|
||||||
|
bl_label: str = t("Tools.standardize_issues_title")
|
||||||
|
bl_options: Set[str] = {'INTERNAL'}
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
def invoke(self, context: Context, event: Any) -> Set[str]:
|
||||||
|
logger.debug("Showing standardization issues popup")
|
||||||
|
return context.window_manager.invoke_props_dialog(self, width=400)
|
||||||
|
|
||||||
|
def draw(self, context: Context) -> None:
|
||||||
|
layout = self.layout
|
||||||
|
col = layout.column(align=True)
|
||||||
|
|
||||||
|
col.label(text=t("Tools.standardize_issues_header"), icon='INFO')
|
||||||
|
col.separator()
|
||||||
|
|
||||||
|
col.label(text=t("Tools.standardize_issues_line1"))
|
||||||
|
col.label(text=t("Tools.standardize_issues_line2"))
|
||||||
|
col.label(text=t("Tools.standardize_issues_line3"))
|
||||||
|
col.separator()
|
||||||
|
col.label(text=t("Tools.standardize_issues_line4"))
|
||||||
|
col.label(text=t("Tools.standardize_issues_line5"))
|
||||||
|
col.separator()
|
||||||
|
col.label(text=t("Tools.standardize_issues_line6"))
|
||||||
|
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
from typing import TypedDict, Set, Dict, List, Optional, Any, Tuple
|
||||||
|
import bpy
|
||||||
|
from bpy.types import Operator, Object, Context, Mesh, MeshUVLoopLayer
|
||||||
|
import bmesh
|
||||||
|
import numpy as np
|
||||||
|
import math
|
||||||
|
from ...core.translations import t
|
||||||
|
from ...core.logging_setup import logger
|
||||||
|
|
||||||
|
class GenerateLoopTreeResult(TypedDict):
|
||||||
|
tree: Dict[str, Set[str]]
|
||||||
|
selected_loops: Dict[str, List[int]]
|
||||||
|
selected_verts: Dict[str, int]
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_AlignUVEdgesToTarget(Operator):
|
||||||
|
"""Operator to align selected UV edges to target edge"""
|
||||||
|
bl_idname = "avatar_toolkit.align_uv_edges_to_target"
|
||||||
|
bl_label = t("UVTools.align_edges")
|
||||||
|
bl_description = t("UVTools.align_edges_desc")
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
#all selected objects need to be meshes for this to work - @989onan
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context: Context) -> bool:
|
||||||
|
if not ((context.view_layer.objects.active is not None) and (len(context.view_layer.objects.selected) > 0)):
|
||||||
|
return False
|
||||||
|
if context.mode != "EDIT_MESH":
|
||||||
|
return False
|
||||||
|
for obj in context.view_layer.objects.selected:
|
||||||
|
if obj.type != "MESH":
|
||||||
|
return False
|
||||||
|
if not context.space_data:
|
||||||
|
return False
|
||||||
|
if not context.space_data.show_uvedit:
|
||||||
|
return False
|
||||||
|
if context.scene.tool_settings.use_uv_select_sync:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
target: str = context.view_layer.objects.active.name #The object which we want to align every other selected object's selected UV vertex line to
|
||||||
|
sources: List[str] = [i.name for i in context.view_layer.objects.selected] #The objects which we want to align their selected UV lines to the target's UV line
|
||||||
|
|
||||||
|
prev_mode: str = bpy.context.object.mode
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
|
def generate_loop_tree(obj_name: str) -> GenerateLoopTreeResult:
|
||||||
|
logger.debug(f"Finding selected line for: {obj_name}")
|
||||||
|
|
||||||
|
vert_target_loops: Dict[str, List[int]] = {}
|
||||||
|
vert_target_verts: Dict[str, int] = {}
|
||||||
|
|
||||||
|
me: Mesh = bpy.data.objects[obj_name].data
|
||||||
|
uv_lay: MeshUVLoopLayer = me.uv_layers.active
|
||||||
|
bm: bmesh.types.BMesh = bmesh.new()
|
||||||
|
bm.from_mesh(me)
|
||||||
|
bm.verts.ensure_lookup_table()
|
||||||
|
|
||||||
|
# To explain:
|
||||||
|
# So loops in UV maps are X polygons that make up a face (So a MeshLoop represent a face and each vertex on that face is in order)
|
||||||
|
#
|
||||||
|
# For some preknowledge:
|
||||||
|
# When a mesh is UV unwrapped, if a vertice is shared by two different faces on the model in the viewport and the vertice of both faces are in
|
||||||
|
# the same position on the UV map, then it considers it one point and the user can move it
|
||||||
|
# (is why the uv map doesn't split apart when you try to move a vertex because that would be annoying)
|
||||||
|
#
|
||||||
|
# The problem:
|
||||||
|
# The problem is that the data for whether the uv corners of two faces that share a vertex physically being connected and selected as one vertex on the uv map does not exist
|
||||||
|
# Though thankfully, blender forcibly (whether you like it or not) merges vertices of a uv map if the vertex of two different faces are actually shared in the UI,
|
||||||
|
# allowing for the moving of vertices of 4 faces connected by a single vertex. Behavior every normal blender user is familiar with.
|
||||||
|
#
|
||||||
|
# The solution
|
||||||
|
# We can use this to our advantage, by finding vertices on the uv map that share the same coridinate as another vertex that is also selected.
|
||||||
|
# that way we can group each pair shared in a line as the same vertex, and identify the line using these pairs and using the data that says for certain
|
||||||
|
# that two vertices share the same face loop, and therefore are connected.
|
||||||
|
|
||||||
|
#hmmm real stupid grimlin hours with this one. Using a string as the index of a dictionary of loop corners that end up on the same coordinate
|
||||||
|
for k,i in enumerate(uv_lay.vertex_selection):
|
||||||
|
if (i.value == True) and (bm.verts[me.loops[k].vertex_index].select == True) and (bm.verts[me.loops[k].vertex_index].hide == False):
|
||||||
|
key = np.array(uv_lay.uv[k].vector[:])
|
||||||
|
key = key.round(decimals=5)
|
||||||
|
|
||||||
|
if str(key) not in vert_target_loops:
|
||||||
|
vert_target_loops[str(key)] = []
|
||||||
|
vert_target_loops[str(key)].append(k)
|
||||||
|
vert_target_verts[str(key)] = me.loops[k].vertex_index
|
||||||
|
|
||||||
|
if len(vert_target_loops) > 4000:
|
||||||
|
self.report({'WARNING'}, t("UVTools.too_many_vertices"))
|
||||||
|
return {"tree": {}, "selected_loops": {}, "selected_verts": {}}
|
||||||
|
|
||||||
|
logger.debug(f"Finding connections on line for {obj_name}")
|
||||||
|
me.validate()
|
||||||
|
|
||||||
|
bm = bmesh.new()
|
||||||
|
bm.from_mesh(me)
|
||||||
|
|
||||||
|
tree: Dict[str, Set[str]] = {}
|
||||||
|
selected_verts = np.hstack(list(vert_target_loops.values()))
|
||||||
|
bm.verts.ensure_lookup_table()
|
||||||
|
|
||||||
|
for uvcoordsstr in vert_target_loops:
|
||||||
|
uv_lay = me.uv_layers.active
|
||||||
|
|
||||||
|
#before this section, each vert_target_loops is just groupings of vertices that share coordinates.
|
||||||
|
# Using the data that determines UV face corners (uvloops) that are associated with the real vertex,
|
||||||
|
# and the uv face corners (loops) that are on the same faces as the vertices that share coordinates in
|
||||||
|
# vert_target_loops, we can now identify them
|
||||||
|
#TL;DR: pairs of vertices that share cooridinates (chain links) find their buddies (make chain connected)
|
||||||
|
|
||||||
|
# Someone explain this better than me if you can please - @989onan
|
||||||
|
extension_loops = []
|
||||||
|
loops = bm.verts[vert_target_verts[uvcoordsstr]].link_loops
|
||||||
|
loops_indexes = [i.index for i in loops]
|
||||||
|
for loop in vert_target_loops[uvcoordsstr]:
|
||||||
|
if loop in loops_indexes:
|
||||||
|
loop_obj = loops[loops_indexes.index(loop)]
|
||||||
|
extension_loops.append(loop_obj.link_loop_next.index)
|
||||||
|
extension_loops.append(loop_obj.link_loop_prev.index)
|
||||||
|
|
||||||
|
#make a tree out of the vertices we identified as sharing faces with the vertices in vert_target_loops, and then link them together in a dictionary.
|
||||||
|
#the order of this dictionary is unknown.
|
||||||
|
# Someone explain this better than me if you can please - @989onan
|
||||||
|
tree[uvcoordsstr] = set()
|
||||||
|
|
||||||
|
for i in extension_loops:
|
||||||
|
if i in selected_verts:
|
||||||
|
key = np.array(uv_lay.uv[i].vector[:])
|
||||||
|
key = key.round(decimals=5)
|
||||||
|
tree[uvcoordsstr].add(str(key))
|
||||||
|
|
||||||
|
if uvcoordsstr in tree:
|
||||||
|
if len(tree[uvcoordsstr]) > 2:
|
||||||
|
self.report({'WARNING'}, t("UVTools.need_line", obj=obj_name))
|
||||||
|
return {"tree": {}, "selected_loops": {}, "selected_verts": {}}
|
||||||
|
|
||||||
|
uv_lay = me.uv_layers.active
|
||||||
|
for uvcoordstr in vert_target_loops:
|
||||||
|
for loop in vert_target_loops[uvcoordstr]:
|
||||||
|
uv_lay.vertex_selection[loop].value = True
|
||||||
|
|
||||||
|
bm.free()
|
||||||
|
me.validate()
|
||||||
|
logger.debug(f"Found UV line connections for {obj_name}")
|
||||||
|
|
||||||
|
return {"tree": tree, "selected_loops": vert_target_loops, "selected_verts": vert_target_verts}
|
||||||
|
|
||||||
|
def sort_uv_tree(originaltree: Dict[str, Set[str]], obj_name: str) -> List[str]:
|
||||||
|
sortedtree: Dict[str, Set[str]] = originaltree.copy()
|
||||||
|
startpoints: List[str] = []
|
||||||
|
for i in sortedtree:
|
||||||
|
if len(sortedtree[i]) < 2:
|
||||||
|
startpoints.append(i)
|
||||||
|
|
||||||
|
if len(startpoints) != 2:
|
||||||
|
self.report({'WARNING'}, t("UVTools.need_line", obj=obj_name))
|
||||||
|
return []
|
||||||
|
|
||||||
|
uvcoords1 = [float(x) for x in startpoints[0].replace("[","").replace("]","").split()]
|
||||||
|
uvcoords2 = [float(x) for x in startpoints[1].replace("[","").replace("]","").split()]
|
||||||
|
|
||||||
|
cursor = context.space_data.cursor_location
|
||||||
|
|
||||||
|
startpoint = startpoints[0] if math.sqrt((uvcoords1[0] - cursor[0])**2 + (uvcoords1[1] - cursor[1])**2) > math.sqrt((uvcoords2[0] - cursor[0])**2 + (uvcoords2[1] - cursor[1])**2) else startpoints[1]
|
||||||
|
|
||||||
|
#Wew my first actual recursive sort! - @989onan
|
||||||
|
def recursive_sort_uv_tree(point: str, sortedfinal: List[str]) -> List[str]:
|
||||||
|
#print("appending "+point)
|
||||||
|
sortedfinal.append(point)
|
||||||
|
|
||||||
|
new_point: str = ""
|
||||||
|
for i in sortedtree:
|
||||||
|
if point in sortedtree[i]:
|
||||||
|
new_point = i
|
||||||
|
removed_value = sortedtree.pop(i)
|
||||||
|
#print(removed_value)
|
||||||
|
break
|
||||||
|
|
||||||
|
if new_point == "":
|
||||||
|
logger.debug("Sorting complete, remaining tree:")
|
||||||
|
logger.debug(sortedtree)
|
||||||
|
return sortedfinal
|
||||||
|
|
||||||
|
return recursive_sort_uv_tree(new_point, sortedfinal)
|
||||||
|
|
||||||
|
sortedtree.pop(startpoint)
|
||||||
|
return recursive_sort_uv_tree(startpoint, [])
|
||||||
|
|
||||||
|
def lerp(v0: float, v1: float, t: float) -> float:
|
||||||
|
return v0 + t * (v1 - v0)
|
||||||
|
|
||||||
|
target_data = generate_loop_tree(target)
|
||||||
|
sorted_target_tree = sort_uv_tree(target_data["tree"], target)
|
||||||
|
logger.debug("Sorted target tree")
|
||||||
|
|
||||||
|
for source in sources:
|
||||||
|
if source == target:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
source_data = generate_loop_tree(source)
|
||||||
|
sorted_source_tree = sort_uv_tree(source_data["tree"], source)
|
||||||
|
logger.debug(f"Sorted source {source}")
|
||||||
|
|
||||||
|
vertex_factor = float(len(sorted_target_tree)-1) / float(len(sorted_source_tree)-1)
|
||||||
|
logger.debug(f"Vertex factor: {vertex_factor}")
|
||||||
|
|
||||||
|
for k, i in enumerate(sorted_source_tree):
|
||||||
|
try:
|
||||||
|
#find where we are on the target edges, to interpolate the current point we're placing along the target point's line.
|
||||||
|
progress_along_edge = float(k) * vertex_factor
|
||||||
|
previous_vertex_index = math.floor(progress_along_edge)
|
||||||
|
next_vertex_index = math.ceil(progress_along_edge)
|
||||||
|
|
||||||
|
#find the uv coordinates of the previous and next points on the target uv line.
|
||||||
|
previous_point = [float(x) for x in sorted_target_tree[previous_vertex_index].replace("[","").replace("]","").split()]
|
||||||
|
next_point = [float(x) for x in sorted_target_tree[next_vertex_index].replace("[","").replace("]","").split()]
|
||||||
|
|
||||||
|
#create a point between these two values that represents a decimal 0-1 going where we are to where we are going between the two current points on the edge we are targeting this whole shebang with.
|
||||||
|
progress_between_points = progress_along_edge - int(progress_along_edge)
|
||||||
|
lerped_point = [
|
||||||
|
lerp(previous_point[0], next_point[0], progress_between_points),
|
||||||
|
lerp(previous_point[1], next_point[1], progress_between_points)
|
||||||
|
]
|
||||||
|
|
||||||
|
#grab our uv face corners for each uv coord that we saved.
|
||||||
|
#Since each face is considered separate internally, we have to treat each connected face to a vertex in a uv map as separate entities/vertexes.
|
||||||
|
#basically pretend they are split apart.
|
||||||
|
uv_face_corners = source_data["selected_loops"][i]
|
||||||
|
|
||||||
|
me = bpy.data.objects[source].data
|
||||||
|
me.validate()
|
||||||
|
bm = bmesh.new()
|
||||||
|
bm.from_mesh(me)
|
||||||
|
uv_lay = me.uv_layers.active
|
||||||
|
bm.verts.ensure_lookup_table()
|
||||||
|
|
||||||
|
for corner in uv_face_corners:
|
||||||
|
uv_lay.uv[corner].vector = lerped_point
|
||||||
|
|
||||||
|
except:
|
||||||
|
#This is probably fine? - @989onan
|
||||||
|
#TODO: What happened here? The magic of making code so complex you forget if this is even an issue. - @989onan
|
||||||
|
pass
|
||||||
|
|
||||||
|
logger.info(f"Finished mesh {source} for UV's")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing source {source}: {str(e)}")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
bpy.ops.object.mode_set(mode=prev_mode)
|
||||||
|
return {'FINISHED'}
|
||||||
+24
-14
@@ -9,10 +9,10 @@ from ..core.logging_setup import logger
|
|||||||
from ..core.translations import t
|
from ..core.translations import t
|
||||||
from ..core.common import (
|
from ..core.common import (
|
||||||
get_active_armature,
|
get_active_armature,
|
||||||
validate_armature,
|
|
||||||
get_all_meshes,
|
get_all_meshes,
|
||||||
validate_mesh_for_pose
|
validate_mesh_for_pose
|
||||||
)
|
)
|
||||||
|
from ..core.armature_validation import validate_armature
|
||||||
|
|
||||||
class VisemeCache:
|
class VisemeCache:
|
||||||
"""Manages caching of generated viseme shape data for performance optimization"""
|
"""Manages caching of generated viseme shape data for performance optimization"""
|
||||||
@@ -35,6 +35,7 @@ class VisemePreview:
|
|||||||
_preview_data: Dict[str, float] = {}
|
_preview_data: Dict[str, float] = {}
|
||||||
_active: bool = False
|
_active: bool = False
|
||||||
_preview_shapes: Optional[OrderedDict] = None
|
_preview_shapes: Optional[OrderedDict] = None
|
||||||
|
_mesh_name: str = ""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def start_preview(cls, context: Context, mesh: Object, shapes: List[str]) -> bool:
|
def start_preview(cls, context: Context, mesh: Object, shapes: List[str]) -> bool:
|
||||||
@@ -43,6 +44,7 @@ class VisemePreview:
|
|||||||
|
|
||||||
cls._active = True
|
cls._active = True
|
||||||
cls._preview_data = {}
|
cls._preview_data = {}
|
||||||
|
cls._mesh_name = mesh.name
|
||||||
|
|
||||||
# Store original values
|
# Store original values
|
||||||
for shape_key in mesh.data.shape_keys.key_blocks:
|
for shape_key in mesh.data.shape_keys.key_blocks:
|
||||||
@@ -79,7 +81,11 @@ class VisemePreview:
|
|||||||
if not cls._active or not cls._preview_shapes:
|
if not cls._active or not cls._preview_shapes:
|
||||||
return
|
return
|
||||||
|
|
||||||
mesh = context.active_object
|
# Get the mesh by name instead of using active object
|
||||||
|
mesh = bpy.data.objects.get(cls._mesh_name)
|
||||||
|
if not mesh:
|
||||||
|
return
|
||||||
|
|
||||||
props = context.scene.avatar_toolkit
|
props = context.scene.avatar_toolkit
|
||||||
viseme_data = cls._preview_shapes.get(props.viseme_preview_selection)
|
viseme_data = cls._preview_shapes.get(props.viseme_preview_selection)
|
||||||
if viseme_data:
|
if viseme_data:
|
||||||
@@ -116,6 +122,7 @@ class VisemePreview:
|
|||||||
cls._active = False
|
cls._active = False
|
||||||
cls._preview_data.clear()
|
cls._preview_data.clear()
|
||||||
cls._preview_shapes = None
|
cls._preview_shapes = None
|
||||||
|
cls._mesh_name = ""
|
||||||
|
|
||||||
class ATOOLKIT_OT_preview_visemes(Operator):
|
class ATOOLKIT_OT_preview_visemes(Operator):
|
||||||
"""Operator for previewing viseme shapes in real-time"""
|
"""Operator for previewing viseme shapes in real-time"""
|
||||||
@@ -126,7 +133,6 @@ class ATOOLKIT_OT_preview_visemes(Operator):
|
|||||||
|
|
||||||
@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':
|
if context.mode != 'OBJECT':
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -138,19 +144,18 @@ class ATOOLKIT_OT_preview_visemes(Operator):
|
|||||||
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 mesh_obj and mesh_obj.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
|
||||||
mesh = context.active_object
|
mesh = bpy.data.objects.get(props.viseme_mesh)
|
||||||
|
|
||||||
if props.viseme_preview_mode:
|
if props.viseme_preview_mode:
|
||||||
VisemePreview.end_preview(mesh)
|
VisemePreview.end_preview(mesh)
|
||||||
props.viseme_preview_mode = False
|
props.viseme_preview_mode = False
|
||||||
else:
|
else:
|
||||||
if not mesh.data.shape_keys:
|
if not mesh or not mesh.data.shape_keys:
|
||||||
self.report({'ERROR'}, t("Visemes.error.no_shapekeys"))
|
self.report({'ERROR'}, t("Visemes.error.no_shapekeys"))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
@@ -197,15 +202,14 @@ class ATOOLKIT_OT_create_visemes(Operator):
|
|||||||
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 mesh_obj and mesh_obj.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
|
||||||
mesh = context.active_object
|
mesh = bpy.data.objects.get(props.viseme_mesh) # Changed from context.active_object
|
||||||
|
|
||||||
if not mesh.data.shape_keys:
|
if not mesh or not mesh.data.shape_keys:
|
||||||
self.report({'ERROR'}, t("Visemes.error.no_shapekeys"))
|
self.report({'ERROR'}, t("Visemes.error.no_shapekeys"))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
@@ -280,7 +284,7 @@ class ATOOLKIT_OT_create_visemes(Operator):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Create new shape key
|
# Create new shape key
|
||||||
self.mix_shapekey(context, renamed_shapes, data['mix'], key)
|
self.mix_shapekey(context, renamed_shapes, data['mix'], key, mesh) # Added mesh parameter
|
||||||
|
|
||||||
# Cache the new shape key data
|
# Cache the new shape key data
|
||||||
shape_data = [v.co.copy() for v in mesh.data.shape_keys.key_blocks[key].data]
|
shape_data = [v.co.copy() for v in mesh.data.shape_keys.key_blocks[key].data]
|
||||||
@@ -293,14 +297,16 @@ class ATOOLKIT_OT_create_visemes(Operator):
|
|||||||
mesh.active_shape_key_index = 0
|
mesh.active_shape_key_index = 0
|
||||||
wm.progress_end()
|
wm.progress_end()
|
||||||
|
|
||||||
def mix_shapekey(self, context: Context, shapes: List[str], mix_data: List, new_name: str) -> None:
|
def mix_shapekey(self, context: Context, shapes: List[str], mix_data: List, new_name: str, mesh: Object) -> None: # Added mesh parameter
|
||||||
"""Creates a new shape key by mixing existing ones"""
|
"""Creates a new shape key by mixing existing ones"""
|
||||||
mesh = context.active_object
|
|
||||||
|
|
||||||
# Remove existing shape key if it exists
|
# Remove existing shape key if it exists
|
||||||
if new_name in mesh.data.shape_keys.key_blocks:
|
if new_name in mesh.data.shape_keys.key_blocks:
|
||||||
mesh.active_shape_key_index = mesh.data.shape_keys.key_blocks.find(new_name)
|
mesh.active_shape_key_index = mesh.data.shape_keys.key_blocks.find(new_name)
|
||||||
|
old_active = context.view_layer.objects.active
|
||||||
|
context.view_layer.objects.active = mesh
|
||||||
bpy.ops.object.shape_key_remove()
|
bpy.ops.object.shape_key_remove()
|
||||||
|
context.view_layer.objects.active = old_active
|
||||||
|
|
||||||
# Reset all shape keys
|
# Reset all shape keys
|
||||||
for shapekey in mesh.data.shape_keys.key_blocks:
|
for shapekey in mesh.data.shape_keys.key_blocks:
|
||||||
@@ -313,7 +319,10 @@ class ATOOLKIT_OT_create_visemes(Operator):
|
|||||||
shapekey.value = value
|
shapekey.value = value
|
||||||
|
|
||||||
# Create mixed shape key
|
# Create mixed shape key
|
||||||
|
old_active = context.view_layer.objects.active
|
||||||
|
context.view_layer.objects.active = mesh
|
||||||
mesh.shape_key_add(name=new_name, from_mix=True)
|
mesh.shape_key_add(name=new_name, from_mix=True)
|
||||||
|
context.view_layer.objects.active = old_active
|
||||||
|
|
||||||
# Reset values and restore shape key settings
|
# Reset values and restore shape key settings
|
||||||
for shapekey in mesh.data.shape_keys.key_blocks:
|
for shapekey in mesh.data.shape_keys.key_blocks:
|
||||||
@@ -356,3 +365,4 @@ class ATOOLKIT_OT_create_visemes(Operator):
|
|||||||
props.mouth_a = current_names[0]
|
props.mouth_a = current_names[0]
|
||||||
props.mouth_o = current_names[1]
|
props.mouth_o = current_names[1]
|
||||||
props.mouth_ch = current_names[2]
|
props.mouth_ch = current_names[2]
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"authors": ["Avatar Toolkit Team"],
|
"authors": ["Avatar Toolkit Team"],
|
||||||
"messages": {
|
"messages": {
|
||||||
"AvatarToolkit.label": "Avatar Toolkit (Alpha 0.1.3)",
|
"AvatarToolkit.label": "Avatar Toolkit (Alpha 0.2.1)",
|
||||||
"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.",
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
"QuickAccess.validation_basic_details": "Only essential bone structure is being validated",
|
"QuickAccess.validation_basic_details": "Only essential bone structure is being validated",
|
||||||
"QuickAccess.validation_none_warning": "Validation Disabled",
|
"QuickAccess.validation_none_warning": "Validation Disabled",
|
||||||
"QuickAccess.validation_none_details": "No armature validation checks are being performed",
|
"QuickAccess.validation_none_details": "No armature validation checks are being performed",
|
||||||
|
"Quick_Access.import_success": "Import successful",
|
||||||
|
|
||||||
"PoseMode.error.start": "Failed to start pose mode: {error}",
|
"PoseMode.error.start": "Failed to start pose mode: {error}",
|
||||||
"PoseMode.error.stop": "Failed to stop pose mode: {error}",
|
"PoseMode.error.stop": "Failed to stop pose mode: {error}",
|
||||||
@@ -69,6 +70,46 @@
|
|||||||
"Armature.validation.invalid_hierarchy": "Invalid bone hierarchy between {parent} and {child}",
|
"Armature.validation.invalid_hierarchy": "Invalid bone hierarchy between {parent} and {child}",
|
||||||
"Armature.validation.asymmetric_bones": "Missing symmetric bones for {bone}",
|
"Armature.validation.asymmetric_bones": "Missing symmetric bones for {bone}",
|
||||||
"Armature.validation.asymmetric_hand_wrist": "Missing symmetric bones for hands/wrists",
|
"Armature.validation.asymmetric_hand_wrist": "Missing symmetric bones for hands/wrists",
|
||||||
|
"Armature.validation.found_bones": "Found bones in armature:\n- {bones}",
|
||||||
|
"Armature.validation.non_standard_bones": "Non-standard bones found:\n- {bones}",
|
||||||
|
"Armature.validation.accessory_bones_note.line1": "If you have hair bones, skirt bones, or other",
|
||||||
|
"Armature.validation.accessory_bones_note.line2": "accessorybones named similarly to main armature",
|
||||||
|
"Armature.validation.accessory_bones_note.line3": "bones (e.g., Head1, Head2), please rename them to",
|
||||||
|
"Armature.validation.accessory_bones_note.line4": "more descriptive names like Hair_1, Skirt_1.",
|
||||||
|
"Armature.validation.standardize_note.line1": "You can standardize your armature",
|
||||||
|
"Armature.validation.standardize_note.line2": "automatically by using the 'Standardize Armature'",
|
||||||
|
"Armature.validation.standardize_note.line3": "button in the Tools section.",
|
||||||
|
"Validation.section.found_bones": "Found Bones",
|
||||||
|
"Validation.section.non_standard": "Non-Standard Bones",
|
||||||
|
"Validation.section.hierarchy": "Hierarchy Issues",
|
||||||
|
"Validation.status.failed": "Validation has failed",
|
||||||
|
"Validation.message.failed.line1": "Armature validation has failed",
|
||||||
|
"Validation.message.failed.line2": "Please check below what the",
|
||||||
|
"Validation.message.failed.line3": "issues are",
|
||||||
|
"Validation.highlight_problem_bones_desc": "Visually highlight bones that have validation issues in the viewport",
|
||||||
|
"Validation.no_armature": "No armature selected",
|
||||||
|
"Validation.no_issues": "No validation issues found to highlight",
|
||||||
|
"Validation.highlighting_complete": "Problem bones highlighted successfully",
|
||||||
|
"Validation.tpose.no_armature": "No armature found for T-pose validation",
|
||||||
|
"Validation.tpose.left_arm_not_horizontal": "Left arm is not in a horizontal T-pose position",
|
||||||
|
"Validation.tpose.right_arm_not_horizontal": "Right arm is not in a horizontal T-pose position",
|
||||||
|
"Validation.tpose.spine_not_vertical": "Spine is not in a vertical position",
|
||||||
|
"Validation.tpose.warning": "T-Pose Validation Warning",
|
||||||
|
"Validation.tpose.recommendation": "We recommend fixing the T-pose before importing into Unity or other platforms",
|
||||||
|
"Validation.scale_issues": "Bones with abnormal scale detected:",
|
||||||
|
"Validation.scale_issue.too_small": "Bone is extremely small",
|
||||||
|
"Validation.scale_issue.too_large": "Bone is extremely large",
|
||||||
|
"Validation.section.scale_issues": "Scale Issues",
|
||||||
|
"Validation.tpose.label": "Validate T-Pose",
|
||||||
|
"Validation.no_scale_issues": "No scale issues detected",
|
||||||
|
"Validation.no_hierarchy_issues": "No hierarchy issues detected",
|
||||||
|
"Validation.no_non_standard_issues": "No non-standard bone issues detected",
|
||||||
|
"Validation.tpose.valid": "T-Pose validation passed successfully",
|
||||||
|
"Validation.tpose.desc": "Check if armature is in a proper T-pose",
|
||||||
|
"Validation.highlight_problem_bones": "Highlight Problem Bones",
|
||||||
|
"Validation.clear_bone_highlighting": "Clear Bone Highlighting",
|
||||||
|
"Validation.clear_bone_highlighting_desc": "Remove bone highlighting and reset bone colors to default",
|
||||||
|
"Validation.highlighting_cleared": "Bone highlighting cleared successfully",
|
||||||
|
|
||||||
"Mesh.validation.no_data": "No mesh data",
|
"Mesh.validation.no_data": "No mesh data",
|
||||||
"Mesh.validation.no_vertex_groups": "No vertex groups found",
|
"Mesh.validation.no_vertex_groups": "No vertex groups found",
|
||||||
@@ -127,6 +168,7 @@
|
|||||||
|
|
||||||
"Tools.label": "Tools",
|
"Tools.label": "Tools",
|
||||||
"Tools.general_title": "General Tools",
|
"Tools.general_title": "General Tools",
|
||||||
|
"Tools.select_armature": "Select an Armature",
|
||||||
"Tools.convert_resonite": "Convert to Resonite",
|
"Tools.convert_resonite": "Convert to Resonite",
|
||||||
"Tools.convert_resonite_desc": "Convert model for use in Resonite",
|
"Tools.convert_resonite_desc": "Convert model for use in Resonite",
|
||||||
"Tools.convert_resonite.operation": "Converting to Resonite",
|
"Tools.convert_resonite.operation": "Converting to Resonite",
|
||||||
@@ -149,6 +191,19 @@
|
|||||||
"Tools.merge_twist_bones_desc": "When checked, twist bones will be kept, even if there are zero-weight",
|
"Tools.merge_twist_bones_desc": "When checked, twist bones will be kept, even if there are zero-weight",
|
||||||
"Tools.clean_weights": "Remove Zero Weight Bones",
|
"Tools.clean_weights": "Remove Zero Weight Bones",
|
||||||
"Tools.clean_weights_desc": "Remove bones with no vertex weights",
|
"Tools.clean_weights_desc": "Remove bones with no vertex weights",
|
||||||
|
"Tools.preserve_parent_bones": "Preserve Parent Bones",
|
||||||
|
"Tools.preserve_parent_bones_desc": "Keep bones that have children even if they have no weights",
|
||||||
|
"Tools.target_bone_type": "Target Bone Type",
|
||||||
|
"Tools.target_bone_type_desc": "Filter which types of bones to process",
|
||||||
|
"Tools.target_all_bones": "All Bones",
|
||||||
|
"Tools.target_deform_bones": "Deform Bones Only",
|
||||||
|
"Tools.target_non_deform_bones": "Non-Deform Bones Only",
|
||||||
|
"Tools.list_only_mode": "List Mode Only",
|
||||||
|
"Tools.list_only_mode_desc": "List zero weight bones instead of removing them",
|
||||||
|
"Tools.zero_weight_bones_found": "Zero weight bones found: {bones}",
|
||||||
|
"Tools.remove_selected_bones": "Remove Selected Bones",
|
||||||
|
"Tools.remove_selected_bones_desc": "Remove selected zero weight bones from armature",
|
||||||
|
"Tools.bones_removed": "Removed {count} bones",
|
||||||
"Tools.clean_constraints": "Delete Bone Constraints",
|
"Tools.clean_constraints": "Delete Bone Constraints",
|
||||||
"Tools.clean_constraints_desc": "Remove all bone constraints from armature",
|
"Tools.clean_constraints_desc": "Remove all bone constraints from armature",
|
||||||
"Tools.clean_constraints_success": "Removed {count} bone constraints",
|
"Tools.clean_constraints_success": "Removed {count} bone constraints",
|
||||||
@@ -187,25 +242,38 @@
|
|||||||
"Tools.shapekey_tolerance": "Shape Key Tolerance",
|
"Tools.shapekey_tolerance": "Shape Key Tolerance",
|
||||||
"Tools.shapekey_tolerance_desc": "Minimum difference to consider a shape key as used",
|
"Tools.shapekey_tolerance_desc": "Minimum difference to consider a shape key as used",
|
||||||
"Tools.shapekeys_removed": "Removed {count} unused shape keys",
|
"Tools.shapekeys_removed": "Removed {count} unused shape keys",
|
||||||
|
"Tools.rigify_title": "Rigify Tools",
|
||||||
|
"Tools.convert_rigify_to_unity": "Convert Rigify to Unity",
|
||||||
|
"Tools.convert_rigify_to_unity_desc": "Convert Rigify armature to Unity-compatible format",
|
||||||
|
"Tools.rigify_converted": "Rigify armature converted successfully",
|
||||||
|
"Tools.no_armature": "No armature selected",
|
||||||
|
"Tools.standardize_title": "Standardization",
|
||||||
|
"Tools.standardize_armature": "Standardize Armature",
|
||||||
|
"Tools.standardize_armature_desc": "Convert non-standard armature to Avatar Toolkit standards",
|
||||||
|
"Tools.standardize_fix_names": "Fix Bone Names",
|
||||||
|
"Tools.standardize_fix_names_desc": "Rename bones to match standard naming conventions",
|
||||||
|
"Tools.standardize_fix_hierarchy": "Fix Bone Hierarchy",
|
||||||
|
"Tools.standardize_fix_hierarchy_desc": "Correct parent-child relationships between bones",
|
||||||
|
"Tools.standardize_fix_scale": "Fix Bone Scale",
|
||||||
|
"Tools.standardize_fix_scale_desc": "Normalize bone lengths to fix scale issues",
|
||||||
|
"Tools.standardize_warning": "This operation will modify your armature. Make a backup first!",
|
||||||
|
"Tools.standardize_success": "Armature successfully standardized",
|
||||||
|
"Tools.standardize_partial": "Armature partially standardized. Some issues remain.",
|
||||||
|
"Tools.standardize_already_valid": "Armature already meets standards. No changes needed.",
|
||||||
|
"Tools.standardize_issues_title": "Standardization Issues",
|
||||||
|
"Tools.standardize_issues_header": "Some issues still remain after standardization",
|
||||||
|
"Tools.standardize_issues_line1": "This could be because some bones on your avatar have unique names",
|
||||||
|
"Tools.standardize_issues_line2": "that aren't in our list of recognized non-standard bones.",
|
||||||
|
"Tools.standardize_issues_line3": "For example, if your hips bone is named 'THISISMYHIPS', we can't detect it.",
|
||||||
|
"Tools.standardize_issues_line4": "If your main skeleton bones aren't being recognized, please report this",
|
||||||
|
"Tools.standardize_issues_line5": "on our GitHub so we can add them to our database.",
|
||||||
|
"Tools.standardize_issues_line6": "Accessory bones (hair, clothing, etc.) must be renamed manually.",
|
||||||
|
|
||||||
"MMD.label": "MMD Tools",
|
"UVTools.uv_title": "UV Tools",
|
||||||
"MMD.bone_standardization": "Bone Standardization",
|
"UVTools.too_many_vertices": "Error! You have too much stuff selected. Are you sure you're selecting two edges?",
|
||||||
"MMD.weight_processing": "Weight Processing",
|
"UVTools.need_line": "You need one line of selected UV points per selected object. Object \"{obj}\" does not meet this requirement!",
|
||||||
"MMD.hierarchy": "Bone Hierarchy",
|
"UVTools.align_edges": "Align UV Edges to Target",
|
||||||
"MMD.cleanup": "Cleanup",
|
"UVTools.align_edges_desc": "Aligns a selected line of UV points on each selected mesh to the line of selected UV points on the active mesh. Useful for kitbashing textures of one model onto another. Uses distance from the 2D cursor to identify the start of the line of UV points on each mesh.",
|
||||||
"MMD.no_armature": "No armature selected",
|
|
||||||
"MMD.no_meshes": "No meshes found",
|
|
||||||
"MMD.validation.rigify_unsupported": "Rigify armatures are not supported",
|
|
||||||
"MMD.validation.multi_user_mesh": "Multi-user mesh detected: {mesh}",
|
|
||||||
"MMD.bones_standardized": "Bones standardized successfully",
|
|
||||||
"MMD.weights_processed": "Weights processed successfully",
|
|
||||||
"MMD.hierarchy_fixed": "Bone hierarchy fixed successfully",
|
|
||||||
"MMD.hierarchy_validation_warning": "Some hierarchy relationships could not be validated",
|
|
||||||
"MMD.cleanup_completed": "Armature cleanup completed",
|
|
||||||
"MMD.process_twist_bones": "Process Twist Bones",
|
|
||||||
"MMD.process_twist_bones_desc": "Transfer weights from twist bones to their parent bones",
|
|
||||||
"MMD.connect_bones": "Connect Bones",
|
|
||||||
"MMD.connect_bones_desc": "Connect bones in chain where appropriate",
|
|
||||||
|
|
||||||
"Visemes.panel_label": "Visemes",
|
"Visemes.panel_label": "Visemes",
|
||||||
"Visemes.shape_selection": "Shape Key Selection",
|
"Visemes.shape_selection": "Shape Key Selection",
|
||||||
@@ -314,10 +382,15 @@
|
|||||||
"EyeTracking.sdk_version": "SDK Version",
|
"EyeTracking.sdk_version": "SDK Version",
|
||||||
"EyeTracking.type.av3": "Avatar 3.0",
|
"EyeTracking.type.av3": "Avatar 3.0",
|
||||||
"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": "Legacy (ChilloutVR",
|
||||||
"EyeTracking.type.sdk2_desc": "VRChat SDK2 eye tracking setup",
|
"EyeTracking.type.sdk2_desc": "Legacy (SDK2) eye tracking setup",
|
||||||
"EyeTracking.adjust.label": "Adjust Eye Position",
|
"EyeTracking.adjust.label": "Adjust Eye Position",
|
||||||
"EyeTracking.adjust.desc": "Adjust the position of eye bones based on vertex groups",
|
"EyeTracking.adjust.desc": "Adjust the position of eye bones based on vertex groups",
|
||||||
|
"EyeTracking.sdk2_warning": "Legacy (SDK2) Eye Tracking Notice",
|
||||||
|
"EyeTracking.sdk2_warning_detail1": "This system SHOULD NOT BE USED FOR VRChat,",
|
||||||
|
"EyeTracking.sdk2_warning_detail2": "as eye tracking is now configured directly",
|
||||||
|
"EyeTracking.sdk2_warning_detail3": "in Unity. It remains for other platforms.",
|
||||||
|
"EyeTracking.sdk2_warning_detail4": "like ChilloutVR.",
|
||||||
|
|
||||||
"CustomPanel.label": "Custom Avatar Tools",
|
"CustomPanel.label": "Custom Avatar Tools",
|
||||||
"CustomPanel.merge_mode": "Merge Mode",
|
"CustomPanel.merge_mode": "Merge Mode",
|
||||||
@@ -399,6 +472,27 @@
|
|||||||
"TextureAtlas.ambient_occlusion": "Ambient Occlusion",
|
"TextureAtlas.ambient_occlusion": "Ambient Occlusion",
|
||||||
"TextureAtlas.height": "Height",
|
"TextureAtlas.height": "Height",
|
||||||
"TextureAtlas.roughness": "Roughness",
|
"TextureAtlas.roughness": "Roughness",
|
||||||
|
"TextureAtlas.description_1": "Create a single material with combined textures",
|
||||||
|
"TextureAtlas.description_2": "to optimize your avatar for better performance.",
|
||||||
|
"TextureAtlas.texture_maps": "Texture Maps",
|
||||||
|
"TextureAtlas.material_ready": "Material is ready for atlas",
|
||||||
|
"TextureAtlas.material_not_ready": "Material needs at least one texture",
|
||||||
|
"TextureAtlas.select_all_tooltip": "Select all materials for atlas",
|
||||||
|
"TextureAtlas.select_none_tooltip": "Deselect all materials",
|
||||||
|
"TextureAtlas.expand_all_tooltip": "Expand all material settings",
|
||||||
|
"TextureAtlas.collapse_all_tooltip": "Collapse all material settings",
|
||||||
|
"TextureAtlas.estimated_size": "Estimated Atlas Size",
|
||||||
|
"TextureAtlas.materials": "materials",
|
||||||
|
"TextureAtlas.no_materials_selected": "No materials selected",
|
||||||
|
"TextureAtlas.select_armature_first": "Please select an armature first",
|
||||||
|
"TextureAtlas.how_to_use_1": "1. Select an armature in the scene",
|
||||||
|
"TextureAtlas.how_to_use_2": "2. Click 'Load Materials' to begin",
|
||||||
|
"TextureAtlas.load_error": "Error loading materials. Check console for details.",
|
||||||
|
"TextureAtlas.material_not_included": "Material is not included in atlas",
|
||||||
|
"TextureAtlas.save_file_first": "Please save your Blender file before creating a texture atlas",
|
||||||
|
"TextureAtlas.save_file_instructions": "Use File > Save As... or click the button below:",
|
||||||
|
"TextureAtlas.save_file_button": "Save Blender File",
|
||||||
|
"TextureAtlas.save_file_required": "Save File Required",
|
||||||
|
|
||||||
"Settings.label": "Settings",
|
"Settings.label": "Settings",
|
||||||
"Settings.language": "Language",
|
"Settings.language": "Language",
|
||||||
@@ -417,6 +511,9 @@
|
|||||||
"Settings.enable_logging_desc": "Enable detailed debug logging for troubleshooting",
|
"Settings.enable_logging_desc": "Enable detailed debug logging for troubleshooting",
|
||||||
"Settings.logging_enabled": "Debug logging enabled",
|
"Settings.logging_enabled": "Debug logging enabled",
|
||||||
"Settings.logging_disabled": "Debug logging disabled",
|
"Settings.logging_disabled": "Debug logging disabled",
|
||||||
|
"Settings.highlight_problem_bones": "Highlight Problem Bones",
|
||||||
|
"Settings.highlight_problem_bones_desc": "Highlight bones with validation issues in the viewport",
|
||||||
|
"Settings.bone_highlighting": "Bone Highlighting",
|
||||||
"Language.auto": "Automatic",
|
"Language.auto": "Automatic",
|
||||||
"Language.en_US": "English",
|
"Language.en_US": "English",
|
||||||
"Language.ja_JP": "Japanese",
|
"Language.ja_JP": "Japanese",
|
||||||
|
|||||||
+247
-150
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"authors": ["Avatar Toolkit Team"],
|
"authors": ["Avatar Toolkit Team"],
|
||||||
"messages": {
|
"messages": {
|
||||||
"AvatarToolkit.label": "アバターツールキット (Alpha 0.1.3)",
|
"AvatarToolkit.label": "アバターツールキット (アルファ 0.2.1)",
|
||||||
"AvatarToolkit.desc1": "アバターツールキットは早期アクセス中で",
|
"AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、",
|
||||||
"AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、",
|
"AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、",
|
||||||
"AvatarToolkit.desc3": "GitHubで報告してください。",
|
"AvatarToolkit.desc3": "GitHubで報告してください。",
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"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を再起動",
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"check_for_update.cantCheck": "アップデートを確認できません",
|
"check_for_update.cantCheck": "アップデートを確認できません",
|
||||||
"download_file.cantConnect": "アップデートサーバーに接続できません",
|
"download_file.cantConnect": "アップデートサーバーに接続できません",
|
||||||
"download_file.cantFindZip": "アップデートファイルが見つかりません",
|
"download_file.cantFindZip": "アップデートファイルが見つかりません",
|
||||||
"download_file.cantFindAvatarToolkit": "アップデートパッケージにAvatarToolkitファイルが見つかりません",
|
"download_file.cantFindAvatarToolkit": "アップデートパッケージにアバターツールキットファイルが見つかりません",
|
||||||
|
|
||||||
"QuickAccess.label": "クイックアクセス",
|
"QuickAccess.label": "クイックアクセス",
|
||||||
"QuickAccess.select_armature": "アーマチュアを選択",
|
"QuickAccess.select_armature": "アーマチュアを選択",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"QuickAccess.import_export": "インポート/エクスポート",
|
"QuickAccess.import_export": "インポート/エクスポート",
|
||||||
"QuickAccess.import": "インポート",
|
"QuickAccess.import": "インポート",
|
||||||
"QuickAccess.export": "エクスポート",
|
"QuickAccess.export": "エクスポート",
|
||||||
"QuickAccess.export_fbx": "FBXエクスポート",
|
"QuickAccess.export_fbx": "FBXをエクスポート",
|
||||||
"QuickAccess.export_resonite": "Resoniteにエクスポート",
|
"QuickAccess.export_resonite": "Resoniteにエクスポート",
|
||||||
"QuickAccess.start_pose_mode.label": "ポーズモード開始",
|
"QuickAccess.start_pose_mode.label": "ポーズモード開始",
|
||||||
"QuickAccess.start_pose_mode.desc": "選択したアーマチュアのポーズモードに入る",
|
"QuickAccess.start_pose_mode.desc": "選択したアーマチュアのポーズモードに入る",
|
||||||
@@ -43,37 +43,78 @@
|
|||||||
"QuickAccess.stop_pose_mode.desc": "ポーズモードを終了し、変形をクリア",
|
"QuickAccess.stop_pose_mode.desc": "ポーズモードを終了し、変形をクリア",
|
||||||
"QuickAccess.apply_pose_as_shapekey.label": "ポーズをシェイプキーとして適用",
|
"QuickAccess.apply_pose_as_shapekey.label": "ポーズをシェイプキーとして適用",
|
||||||
"QuickAccess.apply_pose_as_shapekey.desc": "現在のポーズから新しいシェイプキーを作成",
|
"QuickAccess.apply_pose_as_shapekey.desc": "現在のポーズから新しいシェイプキーを作成",
|
||||||
"QuickAccess.apply_pose_as_rest.label": "ポーズを初期位置として適用",
|
"QuickAccess.apply_pose_as_rest.label": "ポーズを静止ポーズとして適用",
|
||||||
"QuickAccess.apply_pose_as_rest.desc": "現在のポーズを初期位置として適用",
|
"QuickAccess.apply_pose_as_rest.desc": "現在のポーズを静止ポーズとして適用",
|
||||||
"QuickAccess.apply_armature_failed": "アーマチュアの修正の適用に失敗しました",
|
"QuickAccess.apply_armature_failed": "アーマチュアの変更の適用に失敗しました",
|
||||||
"QuickAccess.validation_basic_warning": "基本的な検証のみ有効",
|
"QuickAccess.validation_basic_warning": "限定的な検証がアクティブ",
|
||||||
"QuickAccess.validation_basic_details": "基本的なボーン構造のみ検証しています",
|
"QuickAccess.validation_basic_details": "基本的なボーン構造のみが検証されています",
|
||||||
"QuickAccess.validation_none_warning": "検証無効",
|
"QuickAccess.validation_none_warning": "検証が無効",
|
||||||
"QuickAccess.validation_none_details": "アーマチュアの検証は行われていません",
|
"QuickAccess.validation_none_details": "アーマチュアの検証チェックが実行されていません",
|
||||||
|
"Quick_Access.import_success": "インポート成功",
|
||||||
|
|
||||||
"PoseMode.error.start": "ポーズモードの開始に失敗: {error}",
|
"PoseMode.error.start": "ポーズモードの開始に失敗: {error}",
|
||||||
"PoseMode.error.stop": "ポーズモードの終了に失敗: {error}",
|
"PoseMode.error.stop": "ポーズモードの終了に失敗: {error}",
|
||||||
"PoseMode.error.shapekey": "ポーズをシェイプキーとして適用に失敗: {error}",
|
"PoseMode.error.shapekey": "ポーズをシェイプキーとして適用に失敗: {error}",
|
||||||
"PoseMode.error.rest_pose": "ポーズを初期位置として適用に失敗: {error}",
|
"PoseMode.error.rest_pose": "ポーズを静止ポーズとして適用に失敗: {error}",
|
||||||
"PoseMode.shapekey.name": "シェイプキー名",
|
"PoseMode.shapekey.name": "シェイプキー名",
|
||||||
"PoseMode.shapekey.description": "新しいシェイプキーの名前",
|
"PoseMode.shapekey.description": "新しいシェイプキーの名前",
|
||||||
"PoseMode.shapekey.default": "ポーズ_シェイプキー",
|
"PoseMode.shapekey.default": "ポーズ_シェイプキー",
|
||||||
"PoseMode.skipped_meshes": "一部のメッシュがスキップされました:\n{message}",
|
"PoseMode.skipped_meshes": "一部のメッシュがスキップされました:\n{message}",
|
||||||
"PoseMode.basis": "基準",
|
"PoseMode.basis": "基本形",
|
||||||
|
|
||||||
"Armature.validation.no_armature": "アーマチュアが選択されていません",
|
"Armature.validation.no_armature": "アーマチュアが選択されていません",
|
||||||
"Armature.validation.not_armature": "選択されたオブジェクトはアーマチュアではありません",
|
"Armature.validation.not_armature": "選択されたオブジェクトはアーマチュアではありません",
|
||||||
"Armature.validation.no_bones": "アーマチュアにボーンがありません",
|
"Armature.validation.no_bones": "アーマチュアにボーンがありません",
|
||||||
"Armature.validation.basic_check_failed": "基本的なアーマチュアの検証に失敗しました",
|
"Armature.validation.basic_check_failed": "基本的なアーマチュア検証に失敗しました",
|
||||||
"Armature.validation.missing_bones": "必須ボーンが不足: {bones}",
|
"Armature.validation.missing_bones": "必須ボーンが不足しています: {bones}",
|
||||||
"Armature.validation.invalid_hierarchy": "{parent}と{child}の間のボーン階層が無効です",
|
"Armature.validation.invalid_hierarchy": "{parent}と{child}の間のボーン階層が無効です",
|
||||||
"Armature.validation.asymmetric_bones": "{bone}の対称ボーンが不足しています",
|
"Armature.validation.asymmetric_bones": "{bone}の対称ボーンが不足しています",
|
||||||
"Armature.validation.asymmetric_hand_wrist": "手首/手のボーンの対称性が不足しています",
|
"Armature.validation.asymmetric_hand_wrist": "手首/手のための対称ボーンが不足しています",
|
||||||
|
"Armature.validation.found_bones": "アーマチュアで見つかったボーン:\n- {bones}",
|
||||||
|
"Armature.validation.non_standard_bones": "非標準ボーンが見つかりました:\n- {bones}",
|
||||||
|
"Armature.validation.accessory_bones_note.line1": "髪のボーン、スカートのボーン、または他の",
|
||||||
|
"Armature.validation.accessory_bones_note.line2": "アクセサリーボーンがメインアーマチュアのボーンと",
|
||||||
|
"Armature.validation.accessory_bones_note.line3": "同様の名前(例:Head1、Head2)を持つ場合は、",
|
||||||
|
"Armature.validation.accessory_bones_note.line4": "Hair_1、Skirt_1などのより説明的な名前に変更してください。",
|
||||||
|
"Armature.validation.standardize_note.line1": "ツールセクションの「アーマチュアを標準化」",
|
||||||
|
"Armature.validation.standardize_note.line2": "ボタンを使用して、アーマチュアを",
|
||||||
|
"Armature.validation.standardize_note.line3": "自動的に標準化できます。",
|
||||||
|
"Validation.section.found_bones": "見つかったボーン",
|
||||||
|
"Validation.section.non_standard": "非標準ボーン",
|
||||||
|
"Validation.section.hierarchy": "階層の問題",
|
||||||
|
"Validation.status.failed": "検証に失敗しました",
|
||||||
|
"Validation.message.failed.line1": "アーマチュアの検証に失敗しました",
|
||||||
|
"Validation.message.failed.line2": "以下の問題を確認してください",
|
||||||
|
"Validation.message.failed.line3": "",
|
||||||
|
"Validation.highlight_problem_bones_desc": "検証に問題のあるボーンをビューポートで視覚的に強調表示",
|
||||||
|
"Validation.no_armature": "アーマチュアが選択されていません",
|
||||||
|
"Validation.no_issues": "強調表示する検証の問題が見つかりません",
|
||||||
|
"Validation.highlighting_complete": "問題のあるボーンが正常に強調表示されました",
|
||||||
|
"Validation.tpose.no_armature": "T-ポーズ検証用のアーマチュアが見つかりません",
|
||||||
|
"Validation.tpose.left_arm_not_horizontal": "左腕が水平なT-ポーズの位置にありません",
|
||||||
|
"Validation.tpose.right_arm_not_horizontal": "右腕が水平なT-ポーズの位置にありません",
|
||||||
|
"Validation.tpose.spine_not_vertical": "背骨が垂直な位置にありません",
|
||||||
|
"Validation.tpose.warning": "T-ポーズ検証の警告",
|
||||||
|
"Validation.tpose.recommendation": "Unityやほかのプラットフォームにインポートする前にT-ポーズを修正することをお勧めします",
|
||||||
|
"Validation.scale_issues": "異常なスケールを持つボーンが検出されました:",
|
||||||
|
"Validation.scale_issue.too_small": "ボーンが極端に小さい",
|
||||||
|
"Validation.scale_issue.too_large": "ボーンが極端に大きい",
|
||||||
|
"Validation.section.scale_issues": "スケールの問題",
|
||||||
|
"Validation.tpose.label": "T-ポーズを検証",
|
||||||
|
"Validation.no_scale_issues": "スケールの問題は検出されませんでした",
|
||||||
|
"Validation.no_hierarchy_issues": "階層の問題は検出されませんでした",
|
||||||
|
"Validation.no_non_standard_issues": "非標準ボーンの問題は検出されませんでした",
|
||||||
|
"Validation.tpose.valid": "T-ポーズの検証に成功しました",
|
||||||
|
"Validation.tpose.desc": "アーマチュアが適切なT-ポーズにあるかチェック",
|
||||||
|
"Validation.highlight_problem_bones": "問題のあるボーンを強調表示",
|
||||||
|
"Validation.clear_bone_highlighting": "ボーンの強調表示をクリア",
|
||||||
|
"Validation.clear_bone_highlighting_desc": "ボーンの強調表示を削除し、ボーンの色をデフォルトにリセット",
|
||||||
|
"Validation.highlighting_cleared": "ボーンの強調表示が正常にクリアされました",
|
||||||
|
|
||||||
"Mesh.validation.no_data": "メッシュデータがありません",
|
"Mesh.validation.no_data": "メッシュデータがありません",
|
||||||
"Mesh.validation.no_vertex_groups": "頂点グループが見つかりません",
|
"Mesh.validation.no_vertex_groups": "頂点グループが見つかりません",
|
||||||
"Mesh.validation.no_armature_modifier": "アーマチュアモディファイアがありません",
|
"Mesh.validation.no_armature_modifier": "アーマチュアモディファイアがありません",
|
||||||
"Mesh.validation.valid": "ポーズ操作に有効なメッシュです",
|
"Mesh.validation.valid": "ポーズ操作に有効なメッシュ",
|
||||||
|
|
||||||
"Operation.pose_applied": "ポーズが正常に適用されました",
|
"Operation.pose_applied": "ポーズが正常に適用されました",
|
||||||
|
|
||||||
@@ -85,22 +126,22 @@
|
|||||||
"Optimization.cleanup_title": "メッシュクリーンアップ",
|
"Optimization.cleanup_title": "メッシュクリーンアップ",
|
||||||
"Optimization.join_meshes_title": "メッシュ結合",
|
"Optimization.join_meshes_title": "メッシュ結合",
|
||||||
"Optimization.combine_materials": "マテリアルを結合",
|
"Optimization.combine_materials": "マテリアルを結合",
|
||||||
"Optimization.combine_materials_desc": "類似したマテリアルを結合してドローコールを減らす",
|
"Optimization.combine_materials_desc": "描画コールを減らすために類似したマテリアルを結合",
|
||||||
"Optimization.remove_doubles": "重複頂点を削除",
|
"Optimization.remove_doubles": "重複頂点を削除",
|
||||||
"Optimization.remove_doubles_desc": "重複した頂点を削除",
|
"Optimization.remove_doubles_desc": "重複した頂点を削除",
|
||||||
"Optimization.remove_doubles_advanced": "高度な設定",
|
"Optimization.remove_doubles_advanced": "高度な設定",
|
||||||
"Optimization.remove_doubles_advanced_desc": "高度なオプションで重複頂点を削除",
|
"Optimization.remove_doubles_advanced_desc": "高度なオプションで重複頂点を削除",
|
||||||
"Optimization.join_all_meshes": "すべて結合",
|
"Optimization.join_all_meshes": "すべて結合",
|
||||||
"Optimization.join_all_meshes_desc": "シーン内のすべてのメッシュを結合",
|
"Optimization.join_all_meshes_desc": "シーン内のすべてのメッシュを結合",
|
||||||
"Optimization.join_selected_meshes": "選択を結合",
|
"Optimization.join_selected_meshes": "選択したものを結合",
|
||||||
"Optimization.join_selected_meshes_desc": "選択したメッシュのみを結合",
|
"Optimization.join_selected_meshes_desc": "選択したメッシュのみを結合",
|
||||||
"Optimization.no_meshes": "最適化するメッシュが見つかりません",
|
"Optimization.no_meshes": "最適化するメッシュが見つかりません",
|
||||||
"Optimization.materials_combined": "{combined}個のマテリアルを結合し、{cleaned}個のスロットをクリーンアップし、{removed}個の未使用データブロックを削除しました",
|
"Optimization.materials_combined": "{combined}個のマテリアルを結合し、{cleaned}個のスロットをクリーンアップし、{removed}個の未使用データブロックを削除しました",
|
||||||
"Optimization.error.combine_materials": "マテリアルの結合に失敗: {error}",
|
"Optimization.error.combine_materials": "マテリアルの結合に失敗: {error}",
|
||||||
"Optimization.materials_total": "合計マテリアル数: {count}",
|
"Optimization.materials_total": "合計マテリアル: {count}",
|
||||||
"Optimization.materials_duplicates": "重複の可能性: {count}",
|
"Optimization.materials_duplicates": "潜在的な重複: {count}",
|
||||||
"Optimization.no_materials": "メッシュにマテリアルが見つかりません",
|
"Optimization.no_materials": "メッシュにマテリアルが見つかりません",
|
||||||
"Optimization.error.consolidation": "マテリアルの統合に失敗しました。コンソールで詳細を確認してください",
|
"Optimization.error.consolidation": "マテリアルの統合に失敗しました。詳細はコンソールを確認してください",
|
||||||
"Optimization.combining_materials": "類似したマテリアルを結合中...",
|
"Optimization.combining_materials": "類似したマテリアルを結合中...",
|
||||||
"Optimization.cleaning_slots": "マテリアルスロットをクリーニング中...",
|
"Optimization.cleaning_slots": "マテリアルスロットをクリーニング中...",
|
||||||
"Optimization.removing_unused": "未使用のマテリアルを削除中...",
|
"Optimization.removing_unused": "未使用のマテリアルを削除中...",
|
||||||
@@ -108,7 +149,7 @@
|
|||||||
"Optimization.joining_meshes": "メッシュを結合中...",
|
"Optimization.joining_meshes": "メッシュを結合中...",
|
||||||
"Optimization.applying_transforms": "変形を適用中...",
|
"Optimization.applying_transforms": "変形を適用中...",
|
||||||
"Optimization.fixing_uvs": "UV座標を修正中...",
|
"Optimization.fixing_uvs": "UV座標を修正中...",
|
||||||
"Optimization.finalizing": "完了処理中...",
|
"Optimization.finalizing": "完了中...",
|
||||||
"Optimization.meshes_joined": "すべてのメッシュが正常に結合されました",
|
"Optimization.meshes_joined": "すべてのメッシュが正常に結合されました",
|
||||||
"Optimization.selected_meshes_joined": "選択したメッシュが正常に結合されました",
|
"Optimization.selected_meshes_joined": "選択したメッシュが正常に結合されました",
|
||||||
"Optimization.no_mesh_selected": "メッシュが選択されていません",
|
"Optimization.no_mesh_selected": "メッシュが選択されていません",
|
||||||
@@ -116,101 +157,128 @@
|
|||||||
"Optimization.error.join_meshes": "メッシュの結合に失敗: {error}",
|
"Optimization.error.join_meshes": "メッシュの結合に失敗: {error}",
|
||||||
"Optimization.error.join_selected": "選択したメッシュの結合に失敗: {error}",
|
"Optimization.error.join_selected": "選択したメッシュの結合に失敗: {error}",
|
||||||
"Optimization.merge_distance": "結合距離",
|
"Optimization.merge_distance": "結合距離",
|
||||||
"Optimization.merge_distance_desc": "頂点を結合する距離の閾値",
|
"Optimization.merge_distance_desc": "頂点が結合される距離",
|
||||||
"Optimization.remove_doubles_warning": "この処理には時間がかかる場合があります",
|
"Optimization.remove_doubles_warning": "このプロセスは時間がかかる場合があります",
|
||||||
"Optimization.remove_doubles_wait": "この操作中、Blenderは応答しないように見える場合があります",
|
"Optimization.remove_doubles_wait": "この操作中、Blenderが応答しなくなる場合があります",
|
||||||
"Optimization.error.remove_doubles": "重複頂点の削除に失敗: {error}",
|
"Optimization.error.remove_doubles": "重複頂点の削除に失敗: {error}",
|
||||||
"Optimization.no_armature": "アーマチュアが選択されていません",
|
"Optimization.no_armature": "アーマチュアが選択されていません",
|
||||||
"Optimization.processing_mesh": "メッシュ処理中: {name}",
|
"Optimization.processing_mesh": "メッシュを処理中: {name}",
|
||||||
"Optimization.processing_shapekey": "シェイプキー処理中: {name}",
|
"Optimization.processing_shapekey": "シェイプキーを処理中: {name}",
|
||||||
"Optimization.remove_doubles_completed": "重複頂点の削除が正常に完了しました",
|
"Optimization.remove_doubles_completed": "重複頂点の削除が正常に完了しました",
|
||||||
|
|
||||||
"Tools.label": "ツール",
|
"Tools.label": "ツール",
|
||||||
"Tools.general_title": "一般ツール",
|
"Tools.general_title": "一般ツール",
|
||||||
|
"Tools.select_armature": "アーマチュアを選択",
|
||||||
"Tools.convert_resonite": "Resoniteに変換",
|
"Tools.convert_resonite": "Resoniteに変換",
|
||||||
"Tools.convert_resonite_desc": "Resonite用にモデルを変換",
|
"Tools.convert_resonite_desc": "Resoniteで使用するためにモデルを変換",
|
||||||
"Tools.convert_resonite.operation": "Resoniteに変換中",
|
"Tools.convert_resonite.operation": "Resoniteに変換中",
|
||||||
"Tools.separate_title": "分離ツール",
|
"Tools.separate_title": "分離ツール",
|
||||||
"Tools.separate_materials": "マテリアルで分離",
|
"Tools.separate_materials": "マテリアルで分離",
|
||||||
"Tools.separate_materials_desc": "マテリアルごとにメッシュを分離",
|
"Tools.separate_materials_desc": "マテリアルごとにメッシュを分離",
|
||||||
"Tools.separate_loose": "分離パーツ",
|
"Tools.separate_loose": "離れた部分で分離",
|
||||||
"Tools.separate_loose_desc": "メッシュを分離パーツに分割",
|
"Tools.separate_loose_desc": "メッシュを離れた部分に分離",
|
||||||
"Tools.separate_materials_success": "メッシュをマテリアルごとに正常に分離しました",
|
"Tools.separate_materials_success": "メッシュがマテリアルごとに正常に分離されました",
|
||||||
"Tools.separate_loose_success": "メッシュを分離パーツに正常に分割しました",
|
"Tools.separate_loose_success": "メッシュが離れた部分に正常に分離されました",
|
||||||
"Tools.bone_title": "ボーンツール",
|
"Tools.bone_title": "ボーンツール",
|
||||||
"Tools.create_digitigrade": "デジタイグレード脚を作成",
|
"Tools.create_digitigrade": "デジティグレード脚を作成",
|
||||||
"Tools.create_digitigrade_desc": "脚をデジタイグレード設定に変換",
|
"Tools.create_digitigrade_desc": "脚をデジティグレード設定に変換",
|
||||||
"Tools.digitigrade": "デジタイグレード脚を作成",
|
"Tools.digitigrade": "デジティグレード脚を作成",
|
||||||
"Tools.digitigrade_desc": "選択した脚のボーンをデジタイグレード設定に変換",
|
"Tools.digitigrade_desc": "選択した脚のボーンをデジティグレード設定に変換",
|
||||||
"Tools.digitigrade_error": "デジタイグレード脚の作成に失敗: {error}",
|
"Tools.digitigrade_error": "デジティグレード脚の作成に失敗: {error}",
|
||||||
"Tools.digitigrade_success": "デジタイグレード脚の設定が正常に作成されました",
|
"Tools.digitigrade_success": "デジティグレード脚の設定が正常に作成されました",
|
||||||
"Tools.processing_leg": "脚のボーン処理中: {bone}",
|
"Tools.processing_leg": "脚のボーンを処理中: {bone}",
|
||||||
"Tools.merge_twist_bones": "ツイストボーンを保持",
|
"Tools.merge_twist_bones": "ツイストボーンを保持",
|
||||||
"Tools.merge_twist_bones_desc": "チェックすると、重みが0でもツイストボーンを保持します",
|
"Tools.merge_twist_bones_desc": "チェックすると、ウェイトがゼロでもツイストボーンが保持されます",
|
||||||
"Tools.clean_weights": "重みなしボーンを削除",
|
"Tools.clean_weights": "ゼロウェイトボーンを削除",
|
||||||
"Tools.clean_weights_desc": "頂点の重みがないボーンを削除",
|
"Tools.clean_weights_desc": "頂点ウェイトのないボーンを削除",
|
||||||
"Tools.clean_constraints": "ボーンのコンストレイントを削除",
|
"Tools.preserve_parent_bones": "親ボーンを保持",
|
||||||
"Tools.clean_constraints_desc": "アーマチュアからすべてのボーンコンストレイントを削除",
|
"Tools.preserve_parent_bones_desc": "ウェイトがなくても子を持つボーンを保持",
|
||||||
"Tools.clean_constraints_success": "{count}個のボーンコンストレイントを削除しました",
|
"Tools.target_bone_type": "対象ボーンタイプ",
|
||||||
"Tools.processing_bone_constraints": "ボーンのコンストレイント削除中: {bone}",
|
"Tools.target_bone_type_desc": "処理するボーンのタイプをフィルタリング",
|
||||||
"Tools.clean_weights_success": "{count}個の重みなしボーンを削除しました",
|
"Tools.target_all_bones": "すべてのボーン",
|
||||||
"Tools.clean_weights_threshold": "重みの閾値",
|
"Tools.target_deform_bones": "変形ボーンのみ",
|
||||||
"Tools.clean_weights_threshold_desc": "ボーンに重みがあると判断する最小値",
|
"Tools.target_non_deform_bones": "非変形ボーンのみ",
|
||||||
|
"Tools.list_only_mode": "リストモードのみ",
|
||||||
|
"Tools.list_only_mode_desc": "ゼロウェイトボーンを削除する代わりにリスト表示",
|
||||||
|
"Tools.zero_weight_bones_found": "ゼロウェイトボーンが見つかりました: {bones}",
|
||||||
|
"Tools.remove_selected_bones": "選択したボーンを削除",
|
||||||
|
"Tools.remove_selected_bones_desc": "選択したゼロウェイトボーンをアーマチュアから削除",
|
||||||
|
"Tools.bones_removed": "{count}個のボーンを削除しました",
|
||||||
|
"Tools.clean_constraints": "ボーン制約を削除",
|
||||||
|
"Tools.clean_constraints_desc": "アーマチュアからすべてのボーン制約を削除",
|
||||||
|
"Tools.clean_constraints_success": "{count}個のボーン制約を削除しました",
|
||||||
|
"Tools.processing_bone_constraints": "ボーンから制約を削除中: {bone}",
|
||||||
|
"Tools.clean_weights_success": "{count}個のゼロウェイトボーンを削除しました",
|
||||||
|
"Tools.clean_weights_threshold": "ウェイトしきい値",
|
||||||
|
"Tools.clean_weights_threshold_desc": "ボーンがウェイト付けされていると見なす最小ウェイト値",
|
||||||
"Tools.merge_title": "結合ツール",
|
"Tools.merge_title": "結合ツール",
|
||||||
"Tools.merge_to_active": "アクティブに結合",
|
"Tools.merge_to_active": "アクティブに結合",
|
||||||
"Tools.merge_to_active_desc": "選択したボーンをアクティブなボーンに結合",
|
"Tools.merge_to_active_desc": "選択したボーンをアクティブボーンに結合",
|
||||||
"Tools.merge_to_parent": "親に結合",
|
"Tools.merge_to_parent": "親に結合",
|
||||||
"Tools.merge_to_parent_desc": "ボーンをそれぞれの親ボーンに結合",
|
"Tools.merge_to_parent_desc": "ボーンをそれぞれの親に結合",
|
||||||
"Tools.connect_bones": "ボーンを接続",
|
"Tools.connect_bones": "ボーンを接続",
|
||||||
"Tools.connect_bones_desc": "チェーン内の未接続のボーンを接続",
|
"Tools.connect_bones_desc": "チェーン内の切断されたボーンを接続",
|
||||||
"Tools.additional_title": "追加ツール",
|
"Tools.additional_title": "追加ツール",
|
||||||
"Tools.apply_transforms": "変形を適用",
|
"Tools.apply_transforms": "変形を適用",
|
||||||
"Tools.apply_transforms_desc": "オブジェクトのすべての変形を適用",
|
"Tools.apply_transforms_desc": "オブジェクトにすべての変形を適用",
|
||||||
"Tools.clean_shapekeys": "未使用のシェイプキーを削除",
|
"Tools.clean_shapekeys": "未使用のシェイプキーを削除",
|
||||||
"Tools.clean_shapekeys_desc": "メッシュから未使用のシェイプキーを削除",
|
"Tools.clean_shapekeys_desc": "メッシュから未使用のシェイプキーを削除",
|
||||||
"Tools.bones_translated_success": "すべてのボーンが正常に変換されました",
|
"Tools.bones_translated_success": "すべてのボーンが正常に翻訳されました",
|
||||||
"Tools.bones_translated_with_fails": "変換完了({translate_bone_fails}個のボーンは未変換)",
|
"Tools.bones_translated_with_fails": "翻訳が完了しましたが、{translate_bone_fails}個のボーンは翻訳されませんでした",
|
||||||
"Tools.storing_transforms": "ボーンの変形を保存中...",
|
"Tools.storing_transforms": "ボーンの変形を保存中...",
|
||||||
"Tools.analyzing_weights": "頂点の重みを分析中...",
|
"Tools.analyzing_weights": "頂点ウェイトを分析中...",
|
||||||
"Tools.removing_bones": "重みのないボーンを削除中...",
|
"Tools.removing_bones": "ウェイトのないボーンを削除中...",
|
||||||
"Tools.verifying_hierarchy": "ボーン階層を検証中...",
|
"Tools.verifying_hierarchy": "ボーン階層を検証中...",
|
||||||
"Tools.connect_bones_min_distance": "最小距離",
|
"Tools.connect_bones_min_distance": "最小距離",
|
||||||
"Tools.connect_bones_min_distance_desc": "ボーンを接続する最小距離",
|
"Tools.connect_bones_min_distance_desc": "接続を試みるボーン間の最小距離",
|
||||||
"Tools.connect_bones_success": "{count}個のボーンを接続しました",
|
"Tools.connect_bones_success": "{count}個のボーンを接続しました",
|
||||||
"Tools.merge_weights_threshold": "重み転送閾値",
|
"Tools.merge_weights_threshold": "ウェイト転送しきい値",
|
||||||
"Tools.merge_weights_threshold_desc": "ボーン結合時に転送する最小重み値",
|
"Tools.merge_weights_threshold_desc": "ボーンを結合する際に転送する最小ウェイト値",
|
||||||
"Tools.no_bones_selected": "結合するボーンが選択されていません",
|
"Tools.no_bones_selected": "結合するボーンが選択されていません",
|
||||||
"Tools.no_bones_with_parent": "親を持つ選択ボーンが見つかりません",
|
"Tools.no_bones_with_parent": "親を持つ選択されたボーンが見つかりません",
|
||||||
"Tools.merge_to_active_success": "{count}個のボーンをアクティブボーンに正常に結合しました",
|
"Tools.merge_to_active_success": "{count}個のボーンをアクティブボーンに正常に結合しました",
|
||||||
"Tools.merge_to_parent_success": "{count}個のボーンを親ボーンに正常に結合しました",
|
"Tools.merge_to_parent_success": "{count}個のボーンをそれぞれの親に正常に結合しました",
|
||||||
"Tools.transforms_applied": "変形が正常に適用されました",
|
"Tools.transforms_applied": "変形が正常に適用されました",
|
||||||
"Tools.shapekey_tolerance": "シェイプキーの許容値",
|
"Tools.shapekey_tolerance": "シェイプキー許容値",
|
||||||
"Tools.shapekey_tolerance_desc": "シェイプキーを使用済みと判断する最小差分",
|
"Tools.shapekey_tolerance_desc": "シェイプキーが使用されていると見なす最小差異",
|
||||||
"Tools.shapekeys_removed": "{count}個の未使用シェイプキーを削除しました",
|
"Tools.shapekeys_removed": "{count}個の未使用シェイプキーを削除しました",
|
||||||
|
"Tools.rigify_title": "Rigifyツール",
|
||||||
|
"Tools.convert_rigify_to_unity": "RigifyをUnityに変換",
|
||||||
|
"Tools.convert_rigify_to_unity_desc": "RigifyアーマチュアをUnity互換形式に変換",
|
||||||
|
"Tools.rigify_converted": "Rigifyアーマチュアが正常に変換されました",
|
||||||
|
"Tools.no_armature": "アーマチュアが選択されていません",
|
||||||
|
"Tools.standardize_title": "標準化",
|
||||||
|
"Tools.standardize_armature": "アーマチュアを標準化",
|
||||||
|
"Tools.standardize_armature_desc": "非標準アーマチュアをアバターツールキット標準に変換",
|
||||||
|
"Tools.standardize_fix_names": "ボーン名を修正",
|
||||||
|
"Tools.standardize_fix_names_desc": "ボーンの名前を標準的な命名規則に合わせて変更",
|
||||||
|
"Tools.standardize_fix_hierarchy": "ボーン階層を修正",
|
||||||
|
"Tools.standardize_fix_hierarchy_desc": "ボーン間の親子関係を修正",
|
||||||
|
"Tools.standardize_fix_scale": "ボーンスケールを修正",
|
||||||
|
"Tools.standardize_fix_scale_desc": "スケールの問題を修正するためにボーンの長さを正規化",
|
||||||
|
"Tools.standardize_warning": "この操作はアーマチュアを変更します。先にバックアップを作成してください!",
|
||||||
|
"Tools.standardize_success": "アーマチュアが正常に標準化されました",
|
||||||
|
"Tools.standardize_partial": "アーマチュアが部分的に標準化されました。一部の問題が残っています。",
|
||||||
|
"Tools.standardize_already_valid": "アーマチュアはすでに標準を満たしています。変更は必要ありません。",
|
||||||
|
"Tools.standardize_issues_title": "標準化の問題",
|
||||||
|
"Tools.standardize_issues_header": "標準化後もいくつかの問題が残っています",
|
||||||
|
"Tools.standardize_issues_line1": "これは、アバターのボーンに認識されない非標準ボーンの",
|
||||||
|
"Tools.standardize_issues_line2": "リストにない固有の名前があるためかもしれません。",
|
||||||
|
"Tools.standardize_issues_line3": "例えば、ヒップボーンが「THISISMYHIPS」という名前の場合、検出できません。",
|
||||||
|
"Tools.standardize_issues_line4": "メインスケルトンボーンが認識されない場合は、",
|
||||||
|
"Tools.standardize_issues_line5": "データベースに追加できるよう、GitHubで報告してください。",
|
||||||
|
"Tools.standardize_issues_line6": "アクセサリーボーン(髪、服など)は手動で名前を変更する必要があります。",
|
||||||
|
|
||||||
"MMD.label": "MMDツール",
|
"UVTools.uv_title": "UVツール",
|
||||||
"MMD.bone_standardization": "ボーン標準化",
|
"UVTools.too_many_vertices": "エラー!選択項目が多すぎます。2つのエッジを選択していますか?",
|
||||||
"MMD.weight_processing": "ウェイト処理",
|
"UVTools.need_line": "選択した各オブジェクトにUVポイントの線が1つ必要です。オブジェクト「{obj}」はこの要件を満たしていません!",
|
||||||
"MMD.hierarchy": "ボーン階層",
|
"UVTools.align_edges": "UVエッジをターゲットに合わせる",
|
||||||
"MMD.cleanup": "クリーンアップ",
|
"UVTools.align_edges_desc": "選択した各メッシュのUVポイントの線をアクティブメッシュの選択したUVポイントの線に合わせます。あるモデルのテクスチャを別のモデルに適用する際に便利です。2Dカーソルからの距離を使用して、各メッシュのUVポイントの線の開始点を識別します。",
|
||||||
"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.panel_label": "口形素",
|
||||||
"Visemes.shape_selection": "シェイプキー選択",
|
"Visemes.shape_selection": "シェイプキー選択",
|
||||||
"Visemes.controls": "ビセームコントロール",
|
"Visemes.controls": "口形素コントロール",
|
||||||
"Visemes.no_shapekeys": "シェイプキーのあるメッシュを選択してください",
|
"Visemes.no_shapekeys": "シェイプキーを持つメッシュを選択",
|
||||||
"Visemes.mouth_a": "A形状",
|
"Visemes.mouth_a": "A形状",
|
||||||
"Visemes.mouth_a_desc": "'A'音のシェイプキー",
|
"Visemes.mouth_a_desc": "'A'音のシェイプキー",
|
||||||
"Visemes.mouth_o": "O形状",
|
"Visemes.mouth_o": "O形状",
|
||||||
@@ -218,21 +286,21 @@
|
|||||||
"Visemes.mouth_ch": "CH形状",
|
"Visemes.mouth_ch": "CH形状",
|
||||||
"Visemes.mouth_ch_desc": "'CH'音のシェイプキー",
|
"Visemes.mouth_ch_desc": "'CH'音のシェイプキー",
|
||||||
"Visemes.shape_intensity": "形状の強度",
|
"Visemes.shape_intensity": "形状の強度",
|
||||||
"Visemes.shape_intensity_desc": "ビセーム形状の強度乗数",
|
"Visemes.shape_intensity_desc": "口形素形状の強度乗数",
|
||||||
"Visemes.start_preview": "プレビュー開始",
|
"Visemes.start_preview": "プレビュー開始",
|
||||||
"Visemes.stop_preview": "プレビュー停止",
|
"Visemes.stop_preview": "プレビュー停止",
|
||||||
"Visemes.preview_mode_desc": "ビセームプレビューモードの切り替え",
|
"Visemes.preview_mode_desc": "口形素プレビューモードを切り替え",
|
||||||
"Visemes.preview_selection": "プレビュー選択",
|
"Visemes.preview_selection": "プレビュー選択",
|
||||||
"Visemes.preview_selection_desc": "プレビューするビセームを選択",
|
"Visemes.preview_selection_desc": "プレビューする口形素を選択",
|
||||||
"Visemes.preview_label": "ビセームプレビュー",
|
"Visemes.preview_label": "口形素をプレビュー",
|
||||||
"Visemes.preview_desc": "ビューポートでビセーム形状をプレビュー",
|
"Visemes.preview_desc": "ビューポートで口形素形状をプレビュー",
|
||||||
"Visemes.create_label": "ビセームを作成",
|
"Visemes.create_label": "口形素を作成",
|
||||||
"Visemes.create_desc": "VRCビセームシェイプキーを作成",
|
"Visemes.create_desc": "VRC口形素シェイプキーを作成",
|
||||||
"Visemes.error.no_shapekeys": "メッシュにシェイプキーがありません",
|
"Visemes.error.no_shapekeys": "メッシュにシェイプキーがありません",
|
||||||
"Visemes.error.select_shapekeys": "A、O、CHのシェイプキーを選択してください",
|
"Visemes.error.select_shapekeys": "A、OおよびCHのシェイプキーを選択してください",
|
||||||
"Visemes.success": "ビセームが正常に作成されました",
|
"Visemes.success": "口形素が正常に作成されました",
|
||||||
"Visemes.mesh_select": "メッシュを選択",
|
"Visemes.mesh_select": "メッシュを選択",
|
||||||
"Visemes.mesh_select_desc": "ビセームを作成するメッシュを選択",
|
"Visemes.mesh_select_desc": "口形素を作成するメッシュを選択",
|
||||||
|
|
||||||
"EyeTracking.label": "アイトラッキング",
|
"EyeTracking.label": "アイトラッキング",
|
||||||
"EyeTracking.setup": "アイトラッキング設定",
|
"EyeTracking.setup": "アイトラッキング設定",
|
||||||
@@ -252,9 +320,9 @@
|
|||||||
"EyeTracking.no_armature": "アーマチュアが選択されていません",
|
"EyeTracking.no_armature": "アーマチュアが選択されていません",
|
||||||
"EyeTracking.no_mesh": "メッシュが見つかりません",
|
"EyeTracking.no_mesh": "メッシュが見つかりません",
|
||||||
"EyeTracking.create.label": "アイトラッキングを作成",
|
"EyeTracking.create.label": "アイトラッキングを作成",
|
||||||
"EyeTracking.create.desc": "アイトラッキングのボーンとシェイプキーを設定",
|
"EyeTracking.create.desc": "アイトラッキングボーンとシェイプキーを設定",
|
||||||
"EyeTracking.testing.start.label": "テスト開始",
|
"EyeTracking.testing.start.label": "テスト開始",
|
||||||
"EyeTracking.testing.start.desc": "アイトラッキングテストモードを開始",
|
"EyeTracking.testing.start.desc": "アイトラッキングテストモードに入る",
|
||||||
"EyeTracking.testing.stop.label": "テスト停止",
|
"EyeTracking.testing.stop.label": "テスト停止",
|
||||||
"EyeTracking.testing.stop.desc": "アイトラッキングテストモードを終了",
|
"EyeTracking.testing.stop.desc": "アイトラッキングテストモードを終了",
|
||||||
"EyeTracking.reset.label": "アイトラッキングをリセット",
|
"EyeTracking.reset.label": "アイトラッキングをリセット",
|
||||||
@@ -264,22 +332,22 @@
|
|||||||
"EyeTracking.iris.label": "虹彩の高さを調整",
|
"EyeTracking.iris.label": "虹彩の高さを調整",
|
||||||
"EyeTracking.iris.desc": "虹彩の頂点の高さを調整",
|
"EyeTracking.iris.desc": "虹彩の頂点の高さを調整",
|
||||||
"EyeTracking.blink.test.label": "まばたきテスト",
|
"EyeTracking.blink.test.label": "まばたきテスト",
|
||||||
"EyeTracking.blink.test.desc": "まばたきのシェイプキーをテスト",
|
"EyeTracking.blink.test.desc": "目のまばたきシェイプキーをテスト",
|
||||||
"EyeTracking.lowerlid.test.label": "下まぶたテスト",
|
"EyeTracking.lowerlid.test.label": "下まぶたテスト",
|
||||||
"EyeTracking.lowerlid.test.desc": "下まぶたのシェイプキーをテスト",
|
"EyeTracking.lowerlid.test.desc": "下まぶたシェイプキーをテスト",
|
||||||
"EyeTracking.blink.reset.label": "まばたきテストをリセット",
|
"EyeTracking.blink.reset.label": "まばたきテストをリセット",
|
||||||
"EyeTracking.blink.reset.desc": "まばたきテストの値をリセット",
|
"EyeTracking.blink.reset.desc": "まばたきテスト値をリセット",
|
||||||
"EyeTracking.validation.noArmature": "シーンにアーマチュアが見つかりません",
|
"EyeTracking.validation.noArmature": "シーンにアーマチュアが見つかりません",
|
||||||
"EyeTracking.validation.noMesh": "メッシュ'{mesh}'が見つかりません",
|
"EyeTracking.validation.noMesh": "メッシュ'{mesh}'が見つかりません",
|
||||||
"EyeTracking.validation.noShapekeys": "選択したメッシュにシェイプキーがありません",
|
"EyeTracking.validation.noShapekeys": "選択したメッシュにシェイプキーがありません",
|
||||||
"EyeTracking.validation.leftEye": "左目",
|
"EyeTracking.validation.leftEye": "左目",
|
||||||
"EyeTracking.validation.rightEye": "右目",
|
"EyeTracking.validation.rightEye": "右目",
|
||||||
"EyeTracking.validation.missingGroups": "不足している頂点グループ: {groups}",
|
"EyeTracking.validation.missingGroups": "不足している頂点グループ: {groups}",
|
||||||
"EyeTracking.validation.missingBones": "必要なボーンが不足: {bones}",
|
"EyeTracking.validation.missingBones": "必要なボーンが不足しています: {bones}",
|
||||||
"EyeTracking.validation.success": "アイトラッキング設定が正常に検証されました",
|
"EyeTracking.validation.success": "アイトラッキング設定が正常に検証されました",
|
||||||
"EyeTracking.error.noMesh": "アイトラッキング用のメッシュが選択されていません",
|
"EyeTracking.error.noMesh": "アイトラッキング用のメッシュが選択されていません",
|
||||||
"EyeTracking.error.noVertexGroup": "ボーン用の頂点グループが見つかりません: {bone}",
|
"EyeTracking.error.noVertexGroup": "ボーン用の頂点グループが見つかりません: {bone}",
|
||||||
"EyeTracking.error.noShapeSelected": "必要なすべてのシェイプキーを選択してください",
|
"EyeTracking.error.noShapeSelected": "すべての必要なシェイプキーを選択してください",
|
||||||
"EyeTracking.success": "アイトラッキング設定が正常に完了しました",
|
"EyeTracking.success": "アイトラッキング設定が正常に完了しました",
|
||||||
"EyeTracking.mode_select": "モード選択",
|
"EyeTracking.mode_select": "モード選択",
|
||||||
"EyeTracking.mesh_setup": "メッシュ設定",
|
"EyeTracking.mesh_setup": "メッシュ設定",
|
||||||
@@ -289,13 +357,13 @@
|
|||||||
"EyeTracking.rotation_controls": "目の回転コントロール",
|
"EyeTracking.rotation_controls": "目の回転コントロール",
|
||||||
"EyeTracking.adjustments": "目の調整",
|
"EyeTracking.adjustments": "目の調整",
|
||||||
"EyeTracking.blink_testing": "まばたきテスト",
|
"EyeTracking.blink_testing": "まばたきテスト",
|
||||||
"EyeTracking.wink_left": "左目のウィンク",
|
"EyeTracking.wink_left": "左ウィンク",
|
||||||
"EyeTracking.wink_right": "右目のウィンク",
|
"EyeTracking.wink_right": "右ウィンク",
|
||||||
"EyeTracking.lowerlid_left": "左下まぶた",
|
"EyeTracking.lowerlid_left": "左下まぶた",
|
||||||
"EyeTracking.lowerlid_right": "右下まぶた",
|
"EyeTracking.lowerlid_right": "右下まぶた",
|
||||||
"EyeTracking.mode.creation": "作成モード",
|
"EyeTracking.mode.creation": "作成モード",
|
||||||
"EyeTracking.mode.testing": "テストモード",
|
"EyeTracking.mode.testing": "テストモード",
|
||||||
"EyeTracking.disable_blinking": "まばたきを無効化",
|
"EyeTracking.disable_blinking": "目のまばたきを無効化",
|
||||||
"EyeTracking.disable_movement": "目の動きを無効化",
|
"EyeTracking.disable_movement": "目の動きを無効化",
|
||||||
"EyeTracking.distance": "目の距離",
|
"EyeTracking.distance": "目の距離",
|
||||||
"EyeTracking.distance_desc": "目の間の距離を調整",
|
"EyeTracking.distance_desc": "目の間の距離を調整",
|
||||||
@@ -303,21 +371,26 @@
|
|||||||
"EyeTracking.mesh_name": "メッシュ",
|
"EyeTracking.mesh_name": "メッシュ",
|
||||||
"EyeTracking.mesh_name_desc": "アイトラッキング用のメッシュを選択",
|
"EyeTracking.mesh_name_desc": "アイトラッキング用のメッシュを選択",
|
||||||
"EyeTracking.head_bone_desc": "頭部ボーンを選択",
|
"EyeTracking.head_bone_desc": "頭部ボーンを選択",
|
||||||
"EyeTracking.eye_left_desc": "左目のボーンを選択",
|
"EyeTracking.eye_left_desc": "左目ボーンを選択",
|
||||||
"EyeTracking.eye_right_desc": "右目のボーンを選択",
|
"EyeTracking.eye_right_desc": "右目ボーンを選択",
|
||||||
"EyeTracking.type": "アイトラッキングタイプ",
|
"EyeTracking.type": "アイトラッキングタイプ",
|
||||||
"EyeTracking.type_desc": "作成するアイトラッキング設定のタイプを選択",
|
"EyeTracking.type_desc": "作成するアイトラッキング設定のタイプを選択",
|
||||||
"EyeTracking.create.av3.label": "AV3アイトラッキングを作成",
|
"EyeTracking.create.av3.label": "AV3アイトラッキングを作成",
|
||||||
"EyeTracking.create.av3.desc": "VRChat Avatar 3.0用のアイトラッキングを設定",
|
"EyeTracking.create.av3.desc": "VRChatアバター3.0用のアイトラッキングを設定",
|
||||||
"EyeTracking.create.sdk2.label": "SDK2アイトラッキングを作成",
|
"EyeTracking.create.sdk2.label": "SDK2アイトラッキングを作成",
|
||||||
"EyeTracking.create.sdk2.desc": "VRChat SDK2用のアイトラッキングを設定",
|
"EyeTracking.create.sdk2.desc": "VRChat SDK2用のアイトラッキングを設定",
|
||||||
"EyeTracking.sdk_version": "SDKバージョン",
|
"EyeTracking.sdk_version": "SDKバージョン",
|
||||||
"EyeTracking.type.av3": "Avatar 3.0",
|
"EyeTracking.type.av3": "アバター3.0",
|
||||||
"EyeTracking.type.av3_desc": "VRChat Avatar 3.0アイトラッキング設定",
|
"EyeTracking.type.av3_desc": "VRChatアバター3.0アイトラッキング設定",
|
||||||
"EyeTracking.type.sdk2": "SDK2(レガシー)",
|
"EyeTracking.type.sdk2": "レガシー (ChilloutVR)",
|
||||||
"EyeTracking.type.sdk2_desc": "VRChat SDK2アイトラッキング設定",
|
"EyeTracking.type.sdk2_desc": "レガシー (SDK2) アイトラッキング設定",
|
||||||
"EyeTracking.adjust.label": "目の位置を調整",
|
"EyeTracking.adjust.label": "目の位置を調整",
|
||||||
"EyeTracking.adjust.desc": "頂点グループに基づいて目のボーンの位置を調整",
|
"EyeTracking.adjust.desc": "頂点グループに基づいて目のボーンの位置を調整",
|
||||||
|
"EyeTracking.sdk2_warning": "レガシー (SDK2) アイトラッキング注意",
|
||||||
|
"EyeTracking.sdk2_warning_detail1": "このシステムはVRChatには使用すべきではありません。",
|
||||||
|
"EyeTracking.sdk2_warning_detail2": "アイトラッキングは現在Unity内で",
|
||||||
|
"EyeTracking.sdk2_warning_detail3": "直接設定されるためです。ChilloutVRなどの",
|
||||||
|
"EyeTracking.sdk2_warning_detail4": "他のプラットフォーム用に残されています。",
|
||||||
|
|
||||||
"CustomPanel.label": "カスタムアバターツール",
|
"CustomPanel.label": "カスタムアバターツール",
|
||||||
"CustomPanel.merge_mode": "結合モード",
|
"CustomPanel.merge_mode": "結合モード",
|
||||||
@@ -326,41 +399,41 @@
|
|||||||
"CustomPanel.select_bone": "ボーンを選択",
|
"CustomPanel.select_bone": "ボーンを選択",
|
||||||
"CustomPanel.select_armature": "アーマチュアを選択",
|
"CustomPanel.select_armature": "アーマチュアを選択",
|
||||||
"CustomPanel.mode.armature": "アーマチュア",
|
"CustomPanel.mode.armature": "アーマチュア",
|
||||||
"CustomPanel.mode.armature_desc": "アーマチュアを結合",
|
"CustomPanel.mode.armature_desc": "アーマチュアを一緒に結合",
|
||||||
"CustomPanel.mode.mesh": "メッシュ",
|
"CustomPanel.mode.mesh": "メッシュ",
|
||||||
"CustomPanel.mode.mesh_desc": "メッシュをアーマチュアに接続",
|
"CustomPanel.mode.mesh_desc": "メッシュをアーマチュアに取り付け",
|
||||||
|
|
||||||
"AttachMesh.label": "メッシュを接続",
|
"AttachMesh.label": "メッシュを取り付け",
|
||||||
"AttachMesh.desc": "自動ウェイト設定でメッシュをアーマチュアボーンに接続",
|
"AttachMesh.desc": "自動ウェイト設定でメッシュをアーマチュアボーンに取り付け",
|
||||||
"AttachMesh.search_desc": "接続するメッシュを検索",
|
"AttachMesh.search_desc": "取り付けるメッシュを検索",
|
||||||
"AttachMesh.select": "接続するメッシュを選択",
|
"AttachMesh.select": "取り付けるメッシュを選択",
|
||||||
"AttachMesh.select_desc": "アーマチュアに接続するメッシュを選択",
|
"AttachMesh.select_desc": "アーマチュアに取り付けるメッシュを選択",
|
||||||
"AttachMesh.success": "メッシュが正常に接続されました",
|
"AttachMesh.success": "メッシュが正常に取り付けられました",
|
||||||
"AttachMesh.warn_no_armature": "アーマチュアとメッシュを選択してください",
|
"AttachMesh.warn_no_armature": "取り付けるアーマチュアとメッシュを選択",
|
||||||
"AttachMesh.validate_transforms": "メッシュの変形を検証中",
|
"AttachMesh.validate_transforms": "メッシュの変形を検証中",
|
||||||
"AttachMesh.validate_name": "メッシュ名を検証中",
|
"AttachMesh.validate_name": "メッシュ名を検証中",
|
||||||
"AttachMesh.parent_mesh": "メッシュをアーマチュアの子に設定中",
|
"AttachMesh.parent_mesh": "メッシュをアーマチュアの子に設定中",
|
||||||
"AttachMesh.setup_weights": "頂点ウェイトを設定中",
|
"AttachMesh.setup_weights": "頂点ウェイトを設定中",
|
||||||
"AttachMesh.create_bone": "接続用ボーンを作成中",
|
"AttachMesh.create_bone": "取り付けボーンを作成中",
|
||||||
"AttachMesh.position_bone": "ボーンを配置中",
|
"AttachMesh.position_bone": "ボーンを配置中",
|
||||||
"AttachMesh.add_modifier": "アーマチュアモディファイアを追加中",
|
"AttachMesh.add_modifier": "アーマチュアモディファイアを追加中",
|
||||||
"AttachMesh.error.bone_not_found": "接続ボーン'{bone}'が見つかりません",
|
"AttachMesh.error.bone_not_found": "取り付けボーン'{bone}'が見つかりません",
|
||||||
"AttachMesh.error.mesh_not_found": "メッシュが見つかりません",
|
"AttachMesh.error.mesh_not_found": "メッシュが見つかりません",
|
||||||
"AttachMesh.error.non_uniform_scale": "メッシュに不均一なスケールがあります。スケールを適用してください",
|
"AttachMesh.error.non_uniform_scale": "メッシュに不均一なスケールがあります。スケールを適用してください",
|
||||||
"AttachBone.search_desc": "対象のボーンを検索",
|
"AttachBone.search_desc": "ターゲットボーンを検索",
|
||||||
"AttachBone.select": "対象のボーンを選択",
|
"AttachBone.select": "ターゲットボーンを選択",
|
||||||
"AttachBone.select_desc": "メッシュを接続するボーンを選択",
|
"AttachBone.select_desc": "メッシュを取り付けるボーンを選択",
|
||||||
|
|
||||||
"MergeArmature.label": "アーマチュアの結合",
|
"MergeArmature.label": "アーマチュアを結合",
|
||||||
"MergeArmature.desc": "2つのアーマチュアを結合",
|
"MergeArmature.desc": "2つのアーマチュアを一緒に結合",
|
||||||
"MergeArmature.options": "結合オプション",
|
"MergeArmature.options": "結合オプション",
|
||||||
"MergeArmature.warn_two": "結合には少なくとも2つのアーマチュアが必要です",
|
"MergeArmature.warn_two": "結合するには少なくとも2つのアーマチュアが必要です",
|
||||||
"MergeArmature.into": "結合先",
|
"MergeArmature.into": "結合先",
|
||||||
"MergeArmature.into_desc": "結合先のターゲットアーマチュア",
|
"MergeArmature.into_desc": "結合先のターゲットアーマチュア",
|
||||||
"MergeArmature.into_search_desc": "結合先のアーマチュアを検索",
|
"MergeArmature.into_search_desc": "ターゲットアーマチュアを検索",
|
||||||
"MergeArmature.from": "結合元",
|
"MergeArmature.from": "結合元",
|
||||||
"MergeArmature.from_desc": "結合元のソースアーマチュア",
|
"MergeArmature.from_desc": "結合元のソースアーマチュア",
|
||||||
"MergeArmature.from_search_desc": "結合元のアーマチュアを検索",
|
"MergeArmature.from_search_desc": "ソースアーマチュアを検索",
|
||||||
"MergeArmature.error.not_found": "アーマチュア'{name}'が見つかりません",
|
"MergeArmature.error.not_found": "アーマチュア'{name}'が見つかりません",
|
||||||
"MergeArmature.error.transforms_not_aligned": "このアーマチュアを結合するには変形を適用する必要があります。手動で行うか、変形適用のチェックマークを使用してください",
|
"MergeArmature.error.transforms_not_aligned": "このアーマチュアを結合するには変形を適用する必要があります。手動で行うか、変形適用のチェックマークを使用してください",
|
||||||
"MergeArmature.error.check_transforms": "親の変形を確認してください",
|
"MergeArmature.error.check_transforms": "親の変形を確認してください",
|
||||||
@@ -370,23 +443,23 @@
|
|||||||
"MergeArmature.progress.merging": "アーマチュアを結合中",
|
"MergeArmature.progress.merging": "アーマチュアを結合中",
|
||||||
"MergeArmature.success": "アーマチュアが正常に結合されました",
|
"MergeArmature.success": "アーマチュアが正常に結合されました",
|
||||||
"MergeArmature.merge_all": "同名ボーンを結合",
|
"MergeArmature.merge_all": "同名ボーンを結合",
|
||||||
"MergeArmature.merge_all_desc": "名前が一致するボーンを結合",
|
"MergeArmature.merge_all_desc": "一致する名前を持つボーンを結合",
|
||||||
"MergeArmature.apply_transforms": "変形を適用",
|
"MergeArmature.apply_transforms": "変形を適用",
|
||||||
"MergeArmature.apply_transforms_desc": "結合前にすべての変形を適用",
|
"MergeArmature.apply_transforms_desc": "結合前にすべての変形を適用",
|
||||||
"MergeArmature.join_meshes": "メッシュを結合",
|
"MergeArmature.join_meshes": "メッシュを結合",
|
||||||
"MergeArmature.join_meshes_desc": "結合後にメッシュを結合",
|
"MergeArmature.join_meshes_desc": "結合後にメッシュを結合",
|
||||||
"MergeArmature.remove_zero_weights": "重みなしを削除",
|
"MergeArmature.remove_zero_weights": "ゼロウェイトを削除",
|
||||||
"MergeArmature.remove_zero_weights_desc": "重みのない頂点グループを削除",
|
"MergeArmature.remove_zero_weights_desc": "ウェイトのない頂点グループを削除",
|
||||||
"MergeArmature.cleanup_shape_keys": "シェイプキーをクリーン",
|
"MergeArmature.cleanup_shape_keys": "シェイプキーをクリーンアップ",
|
||||||
"MergeArmature.cleanup_shape_keys_desc": "未使用のシェイプキーを削除",
|
"MergeArmature.cleanup_shape_keys_desc": "未使用のシェイプキーを削除",
|
||||||
|
|
||||||
"TextureAtlas.atlas_completed": "テクスチャアトラスの作成が完了しました",
|
"TextureAtlas.atlas_completed": "テクスチャアトラス作成が完了しました",
|
||||||
"TextureAtlas.atlas_error": "テクスチャアトラスの作成中にエラーが発生しました",
|
"TextureAtlas.atlas_error": "テクスチャアトラス作成中にエラーが発生しました",
|
||||||
"TextureAtlas.atlas_materials": "マテリアルをアトラス化",
|
"TextureAtlas.atlas_materials": "アトラスマテリアル",
|
||||||
"TextureAtlas.atlas_materials_desc": "モデルを最適化するためにマテリアルをアトラス化",
|
"TextureAtlas.atlas_materials_desc": "モデルを最適化するためのアトラスマテリアル",
|
||||||
"TextureAtlas.label": "テクスチャアトラス化",
|
"TextureAtlas.label": "テクスチャアトラス化",
|
||||||
"TextureAtlas.loaded_list": "テクスチャアトラスマテリアルリストを読み込み済み",
|
"TextureAtlas.loaded_list": "読み込まれたテクスチャアトラスマテリアルリスト",
|
||||||
"TextureAtlas.material_list_label": "テクスチャアトラスマテリアルリスト",
|
"TextureAtlas.material_list_label": "テクスチャアトラスマテリアルリストマテリアル",
|
||||||
"TextureAtlas.reload_list": "テクスチャアトラスマテリアルリストを再読み込み",
|
"TextureAtlas.reload_list": "テクスチャアトラスマテリアルリストを再読み込み",
|
||||||
"TextureAtlas.error.label": "エラー",
|
"TextureAtlas.error.label": "エラー",
|
||||||
"TextureAtlas.none.label": "なし",
|
"TextureAtlas.none.label": "なし",
|
||||||
@@ -398,31 +471,55 @@
|
|||||||
"TextureAtlas.emission": "発光",
|
"TextureAtlas.emission": "発光",
|
||||||
"TextureAtlas.ambient_occlusion": "アンビエントオクルージョン",
|
"TextureAtlas.ambient_occlusion": "アンビエントオクルージョン",
|
||||||
"TextureAtlas.height": "高さ",
|
"TextureAtlas.height": "高さ",
|
||||||
"TextureAtlas.roughness": "ラフネス",
|
"TextureAtlas.roughness": "粗さ",
|
||||||
|
"TextureAtlas.description_1": "テクスチャを組み合わせた単一のマテリアルを作成",
|
||||||
|
"TextureAtlas.description_2": "アバターのパフォーマンスを最適化します。",
|
||||||
|
"TextureAtlas.texture_maps": "テクスチャマップ",
|
||||||
|
"TextureAtlas.material_ready": "マテリアルはアトラス作成の準備ができています",
|
||||||
|
"TextureAtlas.material_not_ready": "マテリアルには少なくとも1つのテクスチャが必要です",
|
||||||
|
"TextureAtlas.select_all_tooltip": "すべてのマテリアルを選択",
|
||||||
|
"TextureAtlas.select_none_tooltip": "すべての選択を解除",
|
||||||
|
"TextureAtlas.expand_all_tooltip": "すべてのマテリアル設定を展開",
|
||||||
|
"TextureAtlas.collapse_all_tooltip": "すべてのマテリアル設定を折りたたむ",
|
||||||
|
"TextureAtlas.estimated_size": "推定アトラスサイズ",
|
||||||
|
"TextureAtlas.materials": "マテリアル",
|
||||||
|
"TextureAtlas.no_materials_selected": "マテリアルが選択されていません",
|
||||||
|
"TextureAtlas.select_armature_first": "最初にアーマチュアを選択してください",
|
||||||
|
"TextureAtlas.how_to_use_1": "1. シーン内のアーマチュアを選択",
|
||||||
|
"TextureAtlas.how_to_use_2": "2. 「マテリアルを読み込む」をクリックして開始",
|
||||||
|
"TextureAtlas.load_error": "マテリアルの読み込みエラー。詳細はコンソールを確認してください。",
|
||||||
|
"TextureAtlas.material_not_included": "マテリアルはアトラスに含まれていません",
|
||||||
|
"TextureAtlas.save_file_first": "テクスチャアトラスを作成する前に、Blenderファイルを保存してください",
|
||||||
|
"TextureAtlas.save_file_instructions": "ファイル > 名前を付けて保存... を使用するか、下のボタンをクリックしてください:",
|
||||||
|
"TextureAtlas.save_file_button": "Blenderファイルを保存",
|
||||||
|
"TextureAtlas.save_file_required": "ファイルの保存が必要です",
|
||||||
|
|
||||||
"Settings.label": "設定",
|
"Settings.label": "設定",
|
||||||
"Settings.language": "言語",
|
"Settings.language": "言語",
|
||||||
"Settings.language_desc": "インターフェース言語を選択",
|
"Settings.language_desc": "インターフェース言語を選択",
|
||||||
"Settings.validation_mode": "検証モード",
|
"Settings.validation_mode": "検証モード",
|
||||||
"Settings.validation_mode_desc": "アーマチュアの検証の厳密さを選択",
|
"Settings.validation_mode_desc": "アーマチュアをどの程度厳密に検証するかを選択",
|
||||||
"Settings.validation_mode.strict": "厳密",
|
"Settings.validation_mode.strict": "厳格",
|
||||||
"Settings.validation_mode.strict_desc": "ボーン階層と対称性を含む完全な検証",
|
"Settings.validation_mode.strict_desc": "ボーン階層と対称性を含む完全な検証",
|
||||||
"Settings.validation_mode.basic": "基本",
|
"Settings.validation_mode.basic": "基本",
|
||||||
"Settings.validation_mode.basic_desc": "必須ボーンのみチェック",
|
"Settings.validation_mode.basic_desc": "必須ボーンのチェックのみ",
|
||||||
"Settings.validation_mode.none": "なし",
|
"Settings.validation_mode.none": "なし",
|
||||||
"Settings.validation_mode.none_desc": "アーマチュアの検証を行わない",
|
"Settings.validation_mode.none_desc": "アーマチュア検証なし",
|
||||||
"Settings.debug": "デバッグ設定",
|
"Settings.debug": "デバッグ設定",
|
||||||
"Settings.logging": "ログ記録",
|
"Settings.logging": "ログ記録",
|
||||||
"Settings.enable_logging": "デバッグログを有効化",
|
"Settings.enable_logging": "デバッグログを有効化",
|
||||||
"Settings.enable_logging_desc": "トラブルシューティング用の詳細ログを有効化",
|
"Settings.enable_logging_desc": "トラブルシューティングのための詳細なデバッグログを有効化",
|
||||||
"Settings.logging_enabled": "デバッグログが有効になりました",
|
"Settings.logging_enabled": "デバッグログが有効になりました",
|
||||||
"Settings.logging_disabled": "デバッグログが無効になりました",
|
"Settings.logging_disabled": "デバッグログが無効になりました",
|
||||||
|
"Settings.highlight_problem_bones": "問題のあるボーンを強調表示",
|
||||||
|
"Settings.highlight_problem_bones_desc": "ビューポートで検証に問題のあるボーンを強調表示",
|
||||||
|
"Settings.bone_highlighting": "ボーンの強調表示",
|
||||||
"Language.auto": "自動",
|
"Language.auto": "自動",
|
||||||
"Language.en_US": "英語",
|
"Language.en_US": "英語",
|
||||||
"Language.ja_JP": "日本語",
|
"Language.ja_JP": "日本語",
|
||||||
"Language.ko_KR": "韓国語",
|
"Language.ko_KR": "韓国語",
|
||||||
"Language.changed.title": "言語が変更されました",
|
"Language.changed.title": "言語が変更されました",
|
||||||
"Language.changed.success": "言語が正常に変更されました!",
|
"Language.changed.success": "言語が正常に変更されました!",
|
||||||
"Language.changed.restart": "一部のUI要素の更新にはBlenderの再起動が必要な場合があります"
|
"Language.changed.restart": "一部のUI要素はBlenderの再起動が必要な場合があります"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+213
-116
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"authors": ["Avatar Toolkit Team"],
|
"authors": ["Avatar Toolkit Team"],
|
||||||
"messages": {
|
"messages": {
|
||||||
"AvatarToolkit.label": "아바타 툴킷 (알파 0.1.3)",
|
"AvatarToolkit.label": "아바타 툴킷 (알파 0.2.1)",
|
||||||
"AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계입니다",
|
"AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로",
|
||||||
"AvatarToolkit.desc2": "문제가 발생할 수 있으며, 문제를 발견하시면",
|
"AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면",
|
||||||
"AvatarToolkit.desc3": "Github에 보고해 주시기 바랍니다.",
|
"AvatarToolkit.desc3": "Github에 보고해 주세요.",
|
||||||
|
|
||||||
"Updater.label": "업데이터",
|
"Updater.label": "업데이터",
|
||||||
"Updater.CheckForUpdateButton.label": "업데이트 확인",
|
"Updater.CheckForUpdateButton.label": "업데이트 확인",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"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": "블렌더 재시작",
|
"RestartBlenderPopup.label": "블렌더 재시작",
|
||||||
"RestartBlenderPopup.desc": "업데이트를 완료하려면 블렌더를 재시작하세요",
|
"RestartBlenderPopup.desc": "업데이트를 완료하려면 블렌더를 재시작하세요",
|
||||||
@@ -42,40 +42,81 @@
|
|||||||
"QuickAccess.stop_pose_mode.label": "포즈 모드 종료",
|
"QuickAccess.stop_pose_mode.label": "포즈 모드 종료",
|
||||||
"QuickAccess.stop_pose_mode.desc": "포즈 모드 종료 및 변형 초기화",
|
"QuickAccess.stop_pose_mode.desc": "포즈 모드 종료 및 변형 초기화",
|
||||||
"QuickAccess.apply_pose_as_shapekey.label": "포즈를 쉐이프 키로 적용",
|
"QuickAccess.apply_pose_as_shapekey.label": "포즈를 쉐이프 키로 적용",
|
||||||
"QuickAccess.apply_pose_as_shapekey.desc": "현재 포즈로 새 쉐이프 키 생성",
|
"QuickAccess.apply_pose_as_shapekey.desc": "현재 포즈에서 새 쉐이프 키 생성",
|
||||||
"QuickAccess.apply_pose_as_rest.label": "포즈를 기본 자세로 적용",
|
"QuickAccess.apply_pose_as_rest.label": "포즈를 기본 포즈로 적용",
|
||||||
"QuickAccess.apply_pose_as_rest.desc": "현재 포즈를 기본 자세로 적용",
|
"QuickAccess.apply_pose_as_rest.desc": "현재 포즈를 기본 포즈로 적용",
|
||||||
"QuickAccess.apply_armature_failed": "아마추어 수정 적용 실패",
|
"QuickAccess.apply_armature_failed": "아마추어 수정 적용 실패",
|
||||||
"QuickAccess.validation_basic_warning": "제한된 검증 활성화됨",
|
"QuickAccess.validation_basic_warning": "제한된 검증 활성화",
|
||||||
"QuickAccess.validation_basic_details": "필수 본 구조만 검증됨",
|
"QuickAccess.validation_basic_details": "필수 본 구조만 검증 중",
|
||||||
"QuickAccess.validation_none_warning": "검증 비활성화됨",
|
"QuickAccess.validation_none_warning": "검증 비활성화",
|
||||||
"QuickAccess.validation_none_details": "아마추어 검증이 수행되지 않음",
|
"QuickAccess.validation_none_details": "아마추어 검증 확인이 수행되지 않음",
|
||||||
|
"Quick_Access.import_success": "가져오기 성공",
|
||||||
|
|
||||||
"PoseMode.error.start": "포즈 모드 시작 실패: {error}",
|
"PoseMode.error.start": "포즈 모드 시작 실패: {error}",
|
||||||
"PoseMode.error.stop": "포즈 모드 종료 실패: {error}",
|
"PoseMode.error.stop": "포즈 모드 종료 실패: {error}",
|
||||||
"PoseMode.error.shapekey": "포즈를 쉐이프 키로 적용 실패: {error}",
|
"PoseMode.error.shapekey": "포즈를 쉐이프 키로 적용 실패: {error}",
|
||||||
"PoseMode.error.rest_pose": "포즈를 기본 자세로 적용 실패: {error}",
|
"PoseMode.error.rest_pose": "포즈를 기본 포즈로 적용 실패: {error}",
|
||||||
"PoseMode.shapekey.name": "쉐이프 키 이름",
|
"PoseMode.shapekey.name": "쉐이프 키 이름",
|
||||||
"PoseMode.shapekey.description": "새 쉐이프 키의 이름",
|
"PoseMode.shapekey.description": "새 쉐이프 키의 이름",
|
||||||
"PoseMode.shapekey.default": "포즈_쉐이프키",
|
"PoseMode.shapekey.default": "포즈_쉐이프키",
|
||||||
"PoseMode.skipped_meshes": "일부 메시가 건너뛰어짐:\n{message}",
|
"PoseMode.skipped_meshes": "일부 메시가 건너뛰어졌습니다:\n{message}",
|
||||||
"PoseMode.basis": "기본",
|
"PoseMode.basis": "기본",
|
||||||
|
|
||||||
"Armature.validation.no_armature": "선택된 아마추어 없음",
|
"Armature.validation.no_armature": "선택된 아마추어 없음",
|
||||||
"Armature.validation.not_armature": "선택된 오브젝트가 아마추어가 아님",
|
"Armature.validation.not_armature": "선택된 객체가 아마추어가 아님",
|
||||||
"Armature.validation.no_bones": "아마추어에 본이 없음",
|
"Armature.validation.no_bones": "아마추어에 본이 없음",
|
||||||
"Armature.validation.basic_check_failed": "기본 아마추어 검증 실패",
|
"Armature.validation.basic_check_failed": "기본 아마추어 검증 실패",
|
||||||
"Armature.validation.missing_bones": "필수 본 누락: {bones}",
|
"Armature.validation.missing_bones": "필수 본 누락: {bones}",
|
||||||
"Armature.validation.invalid_hierarchy": "{parent}와 {child} 사이의 잘못된 본 계층 구조",
|
"Armature.validation.invalid_hierarchy": "{parent}와 {child} 사이의 유효하지 않은 본 계층 구조",
|
||||||
"Armature.validation.asymmetric_bones": "{bone}의 대칭 본 누락",
|
"Armature.validation.asymmetric_bones": "{bone}에 대한 대칭 본 누락",
|
||||||
"Armature.validation.asymmetric_hand_wrist": "손/손목의 대칭 본 누락",
|
"Armature.validation.asymmetric_hand_wrist": "손/손목에 대한 대칭 본 누락",
|
||||||
|
"Armature.validation.found_bones": "아마추어에서 발견된 본:\n- {bones}",
|
||||||
|
"Armature.validation.non_standard_bones": "비표준 본 발견:\n- {bones}",
|
||||||
|
"Armature.validation.accessory_bones_note.line1": "머리카락 본, 치마 본 또는 기타",
|
||||||
|
"Armature.validation.accessory_bones_note.line2": "액세서리 본의 이름이 주 아마추어",
|
||||||
|
"Armature.validation.accessory_bones_note.line3": "본과 유사하게 지정된 경우(예: Head1, Head2), 이름을",
|
||||||
|
"Armature.validation.accessory_bones_note.line4": "Hair_1, Skirt_1과 같은 더 설명적인 이름으로 변경하세요.",
|
||||||
|
"Armature.validation.standardize_note.line1": "도구 섹션의 '아마추어 표준화'",
|
||||||
|
"Armature.validation.standardize_note.line2": "버튼을 사용하여 아마추어를",
|
||||||
|
"Armature.validation.standardize_note.line3": "자동으로 표준화할 수 있습니다.",
|
||||||
|
"Validation.section.found_bones": "발견된 본",
|
||||||
|
"Validation.section.non_standard": "비표준 본",
|
||||||
|
"Validation.section.hierarchy": "계층 구조 문제",
|
||||||
|
"Validation.status.failed": "검증 실패",
|
||||||
|
"Validation.message.failed.line1": "아마추어 검증 실패",
|
||||||
|
"Validation.message.failed.line2": "아래에서 문제가 무엇인지",
|
||||||
|
"Validation.message.failed.line3": "확인하세요",
|
||||||
|
"Validation.highlight_problem_bones_desc": "뷰포트에서 검증 문제가 있는 본을 시각적으로 강조 표시",
|
||||||
|
"Validation.no_armature": "선택된 아마추어 없음",
|
||||||
|
"Validation.no_issues": "강조 표시할 검증 문제가 없음",
|
||||||
|
"Validation.highlighting_complete": "문제 본 강조 표시 성공",
|
||||||
|
"Validation.tpose.no_armature": "T-포즈 검증을 위한 아마추어를 찾을 수 없음",
|
||||||
|
"Validation.tpose.left_arm_not_horizontal": "왼쪽 팔이 수평 T-포즈 위치에 있지 않음",
|
||||||
|
"Validation.tpose.right_arm_not_horizontal": "오른쪽 팔이 수평 T-포즈 위치에 있지 않음",
|
||||||
|
"Validation.tpose.spine_not_vertical": "척추가 수직 위치에 있지 않음",
|
||||||
|
"Validation.tpose.warning": "T-포즈 검증 경고",
|
||||||
|
"Validation.tpose.recommendation": "Unity 또는 다른 플랫폼으로 가져오기 전에 T-포즈를 수정하는 것이 좋습니다",
|
||||||
|
"Validation.scale_issues": "비정상적인 크기의 본 감지:",
|
||||||
|
"Validation.scale_issue.too_small": "본이 매우 작음",
|
||||||
|
"Validation.scale_issue.too_large": "본이 매우 큼",
|
||||||
|
"Validation.section.scale_issues": "크기 문제",
|
||||||
|
"Validation.tpose.label": "T-포즈 검증",
|
||||||
|
"Validation.no_scale_issues": "크기 문제가 감지되지 않음",
|
||||||
|
"Validation.no_hierarchy_issues": "계층 구조 문제가 감지되지 않음",
|
||||||
|
"Validation.no_non_standard_issues": "비표준 본 문제가 감지되지 않음",
|
||||||
|
"Validation.tpose.valid": "T-포즈 검증 성공",
|
||||||
|
"Validation.tpose.desc": "아마추어가 적절한 T-포즈에 있는지 확인",
|
||||||
|
"Validation.highlight_problem_bones": "문제 본 강조 표시",
|
||||||
|
"Validation.clear_bone_highlighting": "본 강조 표시 지우기",
|
||||||
|
"Validation.clear_bone_highlighting_desc": "본 강조 표시를 제거하고 본 색상을 기본값으로 재설정",
|
||||||
|
"Validation.highlighting_cleared": "본 강조 표시 지우기 성공",
|
||||||
|
|
||||||
"Mesh.validation.no_data": "메시 데이터 없음",
|
"Mesh.validation.no_data": "메시 데이터 없음",
|
||||||
"Mesh.validation.no_vertex_groups": "버텍스 그룹 없음",
|
"Mesh.validation.no_vertex_groups": "버텍스 그룹을 찾을 수 없음",
|
||||||
"Mesh.validation.no_armature_modifier": "아마추어 모디파이어 없음",
|
"Mesh.validation.no_armature_modifier": "아마추어 모디파이어 없음",
|
||||||
"Mesh.validation.valid": "포즈 작업에 유효한 메시",
|
"Mesh.validation.valid": "포즈 작업에 유효한 메시",
|
||||||
|
|
||||||
"Operation.pose_applied": "포즈가 성공적으로 적용됨",
|
"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": "사용 가능한 버전 목록",
|
||||||
@@ -87,20 +128,20 @@
|
|||||||
"Optimization.combine_materials": "재질 결합",
|
"Optimization.combine_materials": "재질 결합",
|
||||||
"Optimization.combine_materials_desc": "드로우 콜을 줄이기 위해 유사한 재질 결합",
|
"Optimization.combine_materials_desc": "드로우 콜을 줄이기 위해 유사한 재질 결합",
|
||||||
"Optimization.remove_doubles": "중복 제거",
|
"Optimization.remove_doubles": "중복 제거",
|
||||||
"Optimization.remove_doubles_desc": "중복된 버텍스 제거",
|
"Optimization.remove_doubles_desc": "중복 버텍스 제거",
|
||||||
"Optimization.remove_doubles_advanced": "고급",
|
"Optimization.remove_doubles_advanced": "고급",
|
||||||
"Optimization.remove_doubles_advanced_desc": "고급 옵션으로 중복 버텍스 제거",
|
"Optimization.remove_doubles_advanced_desc": "고급 옵션으로 중복 버텍스 제거",
|
||||||
"Optimization.join_all_meshes": "전체 결합",
|
"Optimization.join_all_meshes": "모두 결합",
|
||||||
"Optimization.join_all_meshes_desc": "씬의 모든 메시 결합",
|
"Optimization.join_all_meshes_desc": "씬의 모든 메시 결합",
|
||||||
"Optimization.join_selected_meshes": "선택 결합",
|
"Optimization.join_selected_meshes": "선택 항목 결합",
|
||||||
"Optimization.join_selected_meshes_desc": "선택된 메시만 결합",
|
"Optimization.join_selected_meshes_desc": "선택한 메시만 결합",
|
||||||
"Optimization.no_meshes": "최적화할 메시를 찾을 수 없음",
|
"Optimization.no_meshes": "최적화할 메시를 찾을 수 없음",
|
||||||
"Optimization.materials_combined": "{combined}개의 재질 결합, {cleaned}개의 슬롯 정리, {removed}개의 미사용 데이터 블록 제거됨",
|
"Optimization.materials_combined": "{combined}개의 재질 결합, {cleaned}개의 슬롯 정리, {removed}개의 미사용 데이터 블록 제거",
|
||||||
"Optimization.error.combine_materials": "재질 결합 실패: {error}",
|
"Optimization.error.combine_materials": "재질 결합 실패: {error}",
|
||||||
"Optimization.materials_total": "전체 재질: {count}개",
|
"Optimization.materials_total": "총 재질: {count}개",
|
||||||
"Optimization.materials_duplicates": "잠재적 중복: {count}개",
|
"Optimization.materials_duplicates": "잠재적 중복: {count}개",
|
||||||
"Optimization.no_materials": "메시에서 재질을 찾을 수 없음",
|
"Optimization.no_materials": "메시에서 재질을 찾을 수 없음",
|
||||||
"Optimization.error.consolidation": "재질 통합 실패. 콘솔에서 세부 정보 확인",
|
"Optimization.error.consolidation": "재질 통합 실패. 자세한 내용은 콘솔을 확인하세요",
|
||||||
"Optimization.combining_materials": "유사한 재질 결합 중...",
|
"Optimization.combining_materials": "유사한 재질 결합 중...",
|
||||||
"Optimization.cleaning_slots": "재질 슬롯 정리 중...",
|
"Optimization.cleaning_slots": "재질 슬롯 정리 중...",
|
||||||
"Optimization.removing_unused": "미사용 재질 제거 중...",
|
"Optimization.removing_unused": "미사용 재질 제거 중...",
|
||||||
@@ -109,114 +150,141 @@
|
|||||||
"Optimization.applying_transforms": "변형 적용 중...",
|
"Optimization.applying_transforms": "변형 적용 중...",
|
||||||
"Optimization.fixing_uvs": "UV 좌표 수정 중...",
|
"Optimization.fixing_uvs": "UV 좌표 수정 중...",
|
||||||
"Optimization.finalizing": "마무리 중...",
|
"Optimization.finalizing": "마무리 중...",
|
||||||
"Optimization.meshes_joined": "모든 메시가 성공적으로 결합됨",
|
"Optimization.meshes_joined": "모든 메시 결합 성공",
|
||||||
"Optimization.selected_meshes_joined": "선택된 메시가 성공적으로 결합됨",
|
"Optimization.selected_meshes_joined": "선택한 메시 결합 성공",
|
||||||
"Optimization.no_mesh_selected": "선택된 메시 없음",
|
"Optimization.no_mesh_selected": "선택된 메시 없음",
|
||||||
"Optimization.select_at_least_two": "최소 두 개의 메시를 선택하세요",
|
"Optimization.select_at_least_two": "최소 두 개의 메시를 선택하세요",
|
||||||
"Optimization.error.join_meshes": "메시 결합 실패: {error}",
|
"Optimization.error.join_meshes": "메시 결합 실패: {error}",
|
||||||
"Optimization.error.join_selected": "선택된 메시 결합 실패: {error}",
|
"Optimization.error.join_selected": "선택한 메시 결합 실패: {error}",
|
||||||
"Optimization.merge_distance": "병합 거리",
|
"Optimization.merge_distance": "병합 거리",
|
||||||
"Optimization.merge_distance_desc": "버텍스를 병합할 거리",
|
"Optimization.merge_distance_desc": "버텍스가 병합될 거리",
|
||||||
"Optimization.remove_doubles_warning": "이 과정은 시간이 오래 걸릴 수 있습니다",
|
"Optimization.remove_doubles_warning": "이 과정은 시간이 오래 걸릴 수 있습니다",
|
||||||
"Optimization.remove_doubles_wait": "이 작업 중에는 블렌더가 응답하지 않을 수 있습니다",
|
"Optimization.remove_doubles_wait": "이 작업 중에는 블렌더가 응답하지 않는 것처럼 보일 수 있습니다",
|
||||||
"Optimization.error.remove_doubles": "중복 제거 실패: {error}",
|
"Optimization.error.remove_doubles": "중복 제거 실패: {error}",
|
||||||
"Optimization.no_armature": "선택된 아마추어 없음",
|
"Optimization.no_armature": "선택된 아마추어 없음",
|
||||||
"Optimization.processing_mesh": "메시 처리 중: {name}",
|
"Optimization.processing_mesh": "메시 처리 중: {name}",
|
||||||
"Optimization.processing_shapekey": "쉐이프 키 처리 중: {name}",
|
"Optimization.processing_shapekey": "쉐이프 키 처리 중: {name}",
|
||||||
"Optimization.remove_doubles_completed": "중복 제거가 성공적으로 완료됨",
|
"Optimization.remove_doubles_completed": "중복 제거 완료 성공",
|
||||||
|
|
||||||
"Tools.label": "도구",
|
"Tools.label": "도구",
|
||||||
"Tools.general_title": "일반 도구",
|
"Tools.general_title": "일반 도구",
|
||||||
|
"Tools.select_armature": "아마추어 선택",
|
||||||
"Tools.convert_resonite": "Resonite로 변환",
|
"Tools.convert_resonite": "Resonite로 변환",
|
||||||
"Tools.convert_resonite_desc": "Resonite에서 사용할 모델 변환",
|
"Tools.convert_resonite_desc": "Resonite에서 사용하기 위해 모델 변환",
|
||||||
"Tools.convert_resonite.operation": "Resonite로 변환 중",
|
"Tools.convert_resonite.operation": "Resonite로 변환 중",
|
||||||
"Tools.separate_title": "분리 도구",
|
"Tools.separate_title": "분리 도구",
|
||||||
"Tools.separate_materials": "재질별",
|
"Tools.separate_materials": "재질별",
|
||||||
"Tools.separate_materials_desc": "재질별로 메시 분리",
|
"Tools.separate_materials_desc": "재질별로 메시 분리",
|
||||||
"Tools.separate_loose": "분리된 부분",
|
"Tools.separate_loose": "분리된 부분",
|
||||||
"Tools.separate_loose_desc": "분리된 부분으로 메시 분리",
|
"Tools.separate_loose_desc": "메시를 분리된 부분으로 나누기",
|
||||||
"Tools.separate_materials_success": "메시가 재질별로 성공적으로 분리됨",
|
"Tools.separate_materials_success": "메시가 재질별로 성공적으로 분리됨",
|
||||||
"Tools.separate_loose_success": "메시가 분리된 부분으로 성공적으로 분리됨",
|
"Tools.separate_loose_success": "메시가 분리된 부분으로 성공적으로 나뉨",
|
||||||
"Tools.bone_title": "본 도구",
|
"Tools.bone_title": "본 도구",
|
||||||
"Tools.create_digitigrade": "디지티그레이드 다리 생성",
|
"Tools.create_digitigrade": "디지티그레이드 다리 생성",
|
||||||
"Tools.create_digitigrade_desc": "다리를 디지티그레이드 설정으로 변환",
|
"Tools.create_digitigrade_desc": "다리를 디지티그레이드 설정으로 변환",
|
||||||
"Tools.digitigrade": "디지티그레이드 다리 생성",
|
"Tools.digitigrade": "디지티그레이드 다리 생성",
|
||||||
"Tools.digitigrade_desc": "선택된 다리 본을 디지티그레이드 설정으로 변환",
|
"Tools.digitigrade_desc": "선택한 다리 본을 디지티그레이드 설정으로 변환",
|
||||||
"Tools.digitigrade_error": "디지티그레이드 다리 생성 실패: {error}",
|
"Tools.digitigrade_error": "디지티그레이드 다리 생성 실패: {error}",
|
||||||
"Tools.digitigrade_success": "디지티그레이드 다리 설정 생성 성공",
|
"Tools.digitigrade_success": "디지티그레이드 다리 설정 생성 성공",
|
||||||
"Tools.processing_leg": "다리 본 처리 중: {bone}",
|
"Tools.processing_leg": "다리 본 처리 중: {bone}",
|
||||||
"Tools.merge_twist_bones": "트위스트 본 유지",
|
"Tools.merge_twist_bones": "트위스트 본 유지",
|
||||||
"Tools.merge_twist_bones_desc": "체크하면 가중치가 0이어도 트위스트 본 유지",
|
"Tools.merge_twist_bones_desc": "체크하면 가중치가 0이더라도 트위스트 본이 유지됩니다",
|
||||||
"Tools.clean_weights": "0 가중치 본 제거",
|
"Tools.clean_weights": "가중치 0인 본 제거",
|
||||||
"Tools.clean_weights_desc": "버텍스 가중치가 없는 본 제거",
|
"Tools.clean_weights_desc": "버텍스 가중치가 없는 본 제거",
|
||||||
|
"Tools.preserve_parent_bones": "부모 본 보존",
|
||||||
|
"Tools.preserve_parent_bones_desc": "가중치가 없더라도 자식이 있는 본 유지",
|
||||||
|
"Tools.target_bone_type": "대상 본 유형",
|
||||||
|
"Tools.target_bone_type_desc": "처리할 본 유형 필터링",
|
||||||
|
"Tools.target_all_bones": "모든 본",
|
||||||
|
"Tools.target_deform_bones": "변형 본만",
|
||||||
|
"Tools.target_non_deform_bones": "비변형 본만",
|
||||||
|
"Tools.list_only_mode": "목록 모드만",
|
||||||
|
"Tools.list_only_mode_desc": "제거하는 대신 가중치 0인 본 나열",
|
||||||
|
"Tools.zero_weight_bones_found": "가중치 0인 본 발견: {bones}",
|
||||||
|
"Tools.remove_selected_bones": "선택한 본 제거",
|
||||||
|
"Tools.remove_selected_bones_desc": "아마추어에서 선택한 가중치 0인 본 제거",
|
||||||
|
"Tools.bones_removed": "{count}개의 본 제거됨",
|
||||||
"Tools.clean_constraints": "본 제약 조건 삭제",
|
"Tools.clean_constraints": "본 제약 조건 삭제",
|
||||||
"Tools.clean_constraints_desc": "아마추어에서 모든 본 제약 조건 제거",
|
"Tools.clean_constraints_desc": "아마추어에서 모든 본 제약 조건 제거",
|
||||||
"Tools.clean_constraints_success": "{count}개의 본 제약 조건 제거됨",
|
"Tools.clean_constraints_success": "{count}개의 본 제약 조건 제거됨",
|
||||||
"Tools.processing_bone_constraints": "본의 제약 조건 제거 중: {bone}",
|
"Tools.processing_bone_constraints": "본에서 제약 조건 제거 중: {bone}",
|
||||||
"Tools.clean_weights_success": "{count}개의 0 가중치 본 제거됨",
|
"Tools.clean_weights_success": "{count}개의 가중치 0인 본 제거됨",
|
||||||
"Tools.clean_weights_threshold": "가중치 임계값",
|
"Tools.clean_weights_threshold": "가중치 임계값",
|
||||||
"Tools.clean_weights_threshold_desc": "본이 가중치를 가진 것으로 간주할 최소값",
|
"Tools.clean_weights_threshold_desc": "본이 가중치를 가진 것으로 간주하는 최소 가중치 값",
|
||||||
"Tools.merge_title": "병합 도구",
|
"Tools.merge_title": "병합 도구",
|
||||||
"Tools.merge_to_active": "활성 본으로 병합",
|
"Tools.merge_to_active": "활성 본으로 병합",
|
||||||
"Tools.merge_to_active_desc": "선택된 본을 활성 본으로 병합",
|
"Tools.merge_to_active_desc": "선택한 본을 활성 본으로 병합",
|
||||||
"Tools.merge_to_parent": "부모로 병합",
|
"Tools.merge_to_parent": "부모로 병합",
|
||||||
"Tools.merge_to_parent_desc": "본을 각각의 부모로 병합",
|
"Tools.merge_to_parent_desc": "본을 각각의 부모로 병합",
|
||||||
"Tools.connect_bones": "본 연결",
|
"Tools.connect_bones": "본 연결",
|
||||||
"Tools.connect_bones_desc": "체인에서 연결되지 않은 본 연결",
|
"Tools.connect_bones_desc": "체인에서 연결되지 않은 본 연결",
|
||||||
"Tools.additional_title": "추가 도구",
|
"Tools.additional_title": "추가 도구",
|
||||||
"Tools.apply_transforms": "변형 적용",
|
"Tools.apply_transforms": "변형 적용",
|
||||||
"Tools.apply_transforms_desc": "오브젝트에 모든 변형 적용",
|
"Tools.apply_transforms_desc": "객체에 모든 변형 적용",
|
||||||
"Tools.clean_shapekeys": "미사용 쉐이프키 제거",
|
"Tools.clean_shapekeys": "미사용 쉐이프 키 제거",
|
||||||
"Tools.clean_shapekeys_desc": "메시에서 미사용 쉐이프 키 제거",
|
"Tools.clean_shapekeys_desc": "메시에서 미사용 쉐이프 키 제거",
|
||||||
"Tools.bones_translated_success": "모든 본이 성공적으로 변환됨",
|
"Tools.bones_translated_success": "모든 본 번역 성공",
|
||||||
"Tools.bones_translated_with_fails": "변환 완료됨 (변환되지 않은 본 {translate_bone_fails}개)",
|
"Tools.bones_translated_with_fails": "번역 완료, {translate_bone_fails}개의 번역되지 않은 본",
|
||||||
"Tools.storing_transforms": "본 변형 저장 중...",
|
"Tools.storing_transforms": "본 변형 저장 중...",
|
||||||
"Tools.analyzing_weights": "버텍스 가중치 분석 중...",
|
"Tools.analyzing_weights": "버텍스 가중치 분석 중...",
|
||||||
"Tools.removing_bones": "가중치 없는 본 제거 중...",
|
"Tools.removing_bones": "가중치 없는 본 제거 중...",
|
||||||
"Tools.verifying_hierarchy": "본 계층 구조 확인 중...",
|
"Tools.verifying_hierarchy": "본 계층 구조 확인 중...",
|
||||||
"Tools.connect_bones_min_distance": "최소 거리",
|
"Tools.connect_bones_min_distance": "최소 거리",
|
||||||
"Tools.connect_bones_min_distance_desc": "본 연결을 시도할 최소 거리",
|
"Tools.connect_bones_min_distance_desc": "연결을 시도할 본 사이의 최소 거리",
|
||||||
"Tools.connect_bones_success": "{count}개의 본 연결됨",
|
"Tools.connect_bones_success": "{count}개의 본 연결됨",
|
||||||
"Tools.merge_weights_threshold": "가중치 전송 임계값",
|
"Tools.merge_weights_threshold": "가중치 전송 임계값",
|
||||||
"Tools.merge_weights_threshold_desc": "본 병합 시 전송할 최소 가중치 값",
|
"Tools.merge_weights_threshold_desc": "본 병합 시 전송할 최소 가중치 값",
|
||||||
"Tools.no_bones_selected": "병합할 본이 선택되지 않음",
|
"Tools.no_bones_selected": "병합할 본이 선택되지 않음",
|
||||||
"Tools.no_bones_with_parent": "부모가 있는 선택된 본을 찾을 수 없음",
|
"Tools.no_bones_with_parent": "부모가 있는 선택된 본을 찾을 수 없음",
|
||||||
"Tools.merge_to_active_success": "{count}개의 본을 활성 본으로 성공적으로 병합함",
|
"Tools.merge_to_active_success": "{count}개의 본을 활성 본으로 성공적으로 병합",
|
||||||
"Tools.merge_to_parent_success": "{count}개의 본을 부모로 성공적으로 병합함",
|
"Tools.merge_to_parent_success": "{count}개의 본을 부모로 성공적으로 병합",
|
||||||
"Tools.transforms_applied": "변형이 성공적으로 적용됨",
|
"Tools.transforms_applied": "변형 적용 성공",
|
||||||
"Tools.shapekey_tolerance": "쉐이프 키 허용 오차",
|
"Tools.shapekey_tolerance": "쉐이프 키 허용 오차",
|
||||||
"Tools.shapekey_tolerance_desc": "쉐이프 키를 사용된 것으로 간주할 최소 차이",
|
"Tools.shapekey_tolerance_desc": "쉐이프 키가 사용된 것으로 간주하는 최소 차이",
|
||||||
"Tools.shapekeys_removed": "{count}개의 미사용 쉐이프 키 제거됨",
|
"Tools.shapekeys_removed": "{count}개의 미사용 쉐이프 키 제거됨",
|
||||||
|
"Tools.rigify_title": "Rigify 도구",
|
||||||
|
"Tools.convert_rigify_to_unity": "Rigify를 Unity로 변환",
|
||||||
|
"Tools.convert_rigify_to_unity_desc": "Rigify 아마추어를 Unity 호환 형식으로 변환",
|
||||||
|
"Tools.rigify_converted": "Rigify 아마추어 변환 성공",
|
||||||
|
"Tools.no_armature": "선택된 아마추어 없음",
|
||||||
|
"Tools.standardize_title": "표준화",
|
||||||
|
"Tools.standardize_armature": "아마추어 표준화",
|
||||||
|
"Tools.standardize_armature_desc": "비표준 아마추어를 아바타 툴킷 표준으로 변환",
|
||||||
|
"Tools.standardize_fix_names": "본 이름 수정",
|
||||||
|
"Tools.standardize_fix_names_desc": "본 이름을 표준 명명 규칙에 맞게 변경",
|
||||||
|
"Tools.standardize_fix_hierarchy": "본 계층 구조 수정",
|
||||||
|
"Tools.standardize_fix_hierarchy_desc": "본 사이의 부모-자식 관계 수정",
|
||||||
|
"Tools.standardize_fix_scale": "본 크기 수정",
|
||||||
|
"Tools.standardize_fix_scale_desc": "크기 문제를 해결하기 위해 본 길이 정규화",
|
||||||
|
"Tools.standardize_warning": "이 작업은 아마추어를 수정합니다. 먼저 백업을 만드세요!",
|
||||||
|
"Tools.standardize_success": "아마추어 표준화 성공",
|
||||||
|
"Tools.standardize_partial": "아마추어가 부분적으로 표준화되었습니다. 일부 문제가 남아 있습니다.",
|
||||||
|
"Tools.standardize_already_valid": "아마추어가 이미 표준을 충족합니다. 변경이 필요하지 않습니다.",
|
||||||
|
"Tools.standardize_issues_title": "표준화 문제",
|
||||||
|
"Tools.standardize_issues_header": "표준화 후에도 일부 문제가 남아 있습니다",
|
||||||
|
"Tools.standardize_issues_line1": "이는 아바타의 일부 본이 인식되지 않는 고유한 이름을 가지고 있기 때문일 수 있습니다",
|
||||||
|
"Tools.standardize_issues_line2": "우리의 비표준 본 인식 목록에 없는 이름입니다.",
|
||||||
|
"Tools.standardize_issues_line3": "예를 들어, 엉덩이 본의 이름이 'THISISMYHIPS'인 경우 감지할 수 없습니다.",
|
||||||
|
"Tools.standardize_issues_line4": "주요 골격 본이 인식되지 않는 경우 GitHub에 보고해 주세요",
|
||||||
|
"Tools.standardize_issues_line5": "데이터베이스에 추가할 수 있도록 합니다.",
|
||||||
|
"Tools.standardize_issues_line6": "액세서리 본(머리카락, 의류 등)은 수동으로 이름을 변경해야 합니다.",
|
||||||
|
|
||||||
"MMD.label": "MMD 도구",
|
"UVTools.uv_title": "UV 도구",
|
||||||
"MMD.bone_standardization": "본 표준화",
|
"UVTools.too_many_vertices": "오류! 너무 많은 항목이 선택되었습니다. 두 개의 엣지를 선택하고 있는지 확인하세요!",
|
||||||
"MMD.weight_processing": "가중치 처리",
|
"UVTools.need_line": "선택된 각 객체에 대해 UV 포인트의 한 줄이 필요합니다. 객체 \"{obj}\"는 이 요구 사항을 충족하지 않습니다!",
|
||||||
"MMD.hierarchy": "본 계층 구조",
|
"UVTools.align_edges": "UV 엣지를 대상에 정렬",
|
||||||
"MMD.cleanup": "정리",
|
"UVTools.align_edges_desc": "각 선택된 메시의 UV 포인트 선을 활성 메시의 선택된 UV 포인트 선에 정렬합니다. 한 모델의 텍스처를 다른 모델에 적용할 때 유용합니다. 각 메시에서 UV 포인트 선의 시작을 식별하기 위해 2D 커서로부터의 거리를 사용합니다.",
|
||||||
"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.panel_label": "비셈",
|
||||||
"Visemes.shape_selection": "쉐이프 키 선택",
|
"Visemes.shape_selection": "쉐이프 키 선택",
|
||||||
"Visemes.controls": "비셈 컨트롤",
|
"Visemes.controls": "비셈 컨트롤",
|
||||||
"Visemes.no_shapekeys": "쉐이프 키가 있는 메시 선택",
|
"Visemes.no_shapekeys": "쉐이프 키가 있는 메시 선택",
|
||||||
"Visemes.mouth_a": "A 모양",
|
"Visemes.mouth_a": "A 모양",
|
||||||
"Visemes.mouth_a_desc": "'A' 소리를 위한 쉐이프 키",
|
"Visemes.mouth_a_desc": "'A' 소리에 대한 쉐이프 키",
|
||||||
"Visemes.mouth_o": "O 모양",
|
"Visemes.mouth_o": "O 모양",
|
||||||
"Visemes.mouth_o_desc": "'O' 소리를 위한 쉐이프 키",
|
"Visemes.mouth_o_desc": "'O' 소리에 대한 쉐이프 키",
|
||||||
"Visemes.mouth_ch": "CH 모양",
|
"Visemes.mouth_ch": "CH 모양",
|
||||||
"Visemes.mouth_ch_desc": "'CH' 소리를 위한 쉐이프 키",
|
"Visemes.mouth_ch_desc": "'CH' 소리에 대한 쉐이프 키",
|
||||||
"Visemes.shape_intensity": "쉐이프 강도",
|
"Visemes.shape_intensity": "쉐이프 강도",
|
||||||
"Visemes.shape_intensity_desc": "비셈 쉐이프의 강도 배율",
|
"Visemes.shape_intensity_desc": "비셈 쉐이프의 강도 배율",
|
||||||
"Visemes.start_preview": "미리보기 시작",
|
"Visemes.start_preview": "미리보기 시작",
|
||||||
@@ -229,8 +297,8 @@
|
|||||||
"Visemes.create_label": "비셈 생성",
|
"Visemes.create_label": "비셈 생성",
|
||||||
"Visemes.create_desc": "VRC 비셈 쉐이프 키 생성",
|
"Visemes.create_desc": "VRC 비셈 쉐이프 키 생성",
|
||||||
"Visemes.error.no_shapekeys": "메시에 쉐이프 키가 없음",
|
"Visemes.error.no_shapekeys": "메시에 쉐이프 키가 없음",
|
||||||
"Visemes.error.select_shapekeys": "A, O, CH 쉐이프 키를 선택하세요",
|
"Visemes.error.select_shapekeys": "A, O 및 CH에 대한 쉐이프 키를 선택하세요",
|
||||||
"Visemes.success": "비셈이 성공적으로 생성됨",
|
"Visemes.success": "비셈 생성 성공",
|
||||||
"Visemes.mesh_select": "메시 선택",
|
"Visemes.mesh_select": "메시 선택",
|
||||||
"Visemes.mesh_select_desc": "비셈을 생성할 메시 선택",
|
"Visemes.mesh_select_desc": "비셈을 생성할 메시 선택",
|
||||||
|
|
||||||
@@ -248,17 +316,17 @@
|
|||||||
"EyeTracking.rotation.y": "수평 회전",
|
"EyeTracking.rotation.y": "수평 회전",
|
||||||
"EyeTracking.adjust": "눈 조정",
|
"EyeTracking.adjust": "눈 조정",
|
||||||
"EyeTracking.blinking": "깜빡임 컨트롤",
|
"EyeTracking.blinking": "깜빡임 컨트롤",
|
||||||
"EyeTracking.no_shapekeys": "선택된 메시에서 쉐이프 키를 찾을 수 없음",
|
"EyeTracking.no_shapekeys": "선택한 메시에서 쉐이프 키를 찾을 수 없음",
|
||||||
"EyeTracking.no_armature": "선택된 아마추어 없음",
|
"EyeTracking.no_armature": "선택된 아마추어 없음",
|
||||||
"EyeTracking.no_mesh": "메시를 찾을 수 없음",
|
"EyeTracking.no_mesh": "메시를 찾을 수 없음",
|
||||||
"EyeTracking.create.label": "시선 추적 생성",
|
"EyeTracking.create.label": "시선 추적 생성",
|
||||||
"EyeTracking.create.desc": "시선 추적 본과 쉐이프 키 설정",
|
"EyeTracking.create.desc": "시선 추적 본 및 쉐이프 키 설정",
|
||||||
"EyeTracking.testing.start.label": "테스트 시작",
|
"EyeTracking.testing.start.label": "테스트 시작",
|
||||||
"EyeTracking.testing.start.desc": "시선 추적 테스트 모드 진입",
|
"EyeTracking.testing.start.desc": "시선 추적 테스트 모드 진입",
|
||||||
"EyeTracking.testing.stop.label": "테스트 중지",
|
"EyeTracking.testing.stop.label": "테스트 중지",
|
||||||
"EyeTracking.testing.stop.desc": "시선 추적 테스트 모드 종료",
|
"EyeTracking.testing.stop.desc": "시선 추적 테스트 모드 종료",
|
||||||
"EyeTracking.reset.label": "시선 추적 초기화",
|
"EyeTracking.reset.label": "시선 추적 재설정",
|
||||||
"EyeTracking.reset.desc": "모든 시선 추적 설정 초기화",
|
"EyeTracking.reset.desc": "모든 시선 추적 설정 재설정",
|
||||||
"EyeTracking.rotate.label": "눈 본 회전",
|
"EyeTracking.rotate.label": "눈 본 회전",
|
||||||
"EyeTracking.rotate.desc": "VRChat 호환성을 위한 눈 본 회전",
|
"EyeTracking.rotate.desc": "VRChat 호환성을 위한 눈 본 회전",
|
||||||
"EyeTracking.iris.label": "홍채 높이 조정",
|
"EyeTracking.iris.label": "홍채 높이 조정",
|
||||||
@@ -267,20 +335,20 @@
|
|||||||
"EyeTracking.blink.test.desc": "눈 깜빡임 쉐이프 키 테스트",
|
"EyeTracking.blink.test.desc": "눈 깜빡임 쉐이프 키 테스트",
|
||||||
"EyeTracking.lowerlid.test.label": "아래 눈꺼풀 테스트",
|
"EyeTracking.lowerlid.test.label": "아래 눈꺼풀 테스트",
|
||||||
"EyeTracking.lowerlid.test.desc": "아래 눈꺼풀 쉐이프 키 테스트",
|
"EyeTracking.lowerlid.test.desc": "아래 눈꺼풀 쉐이프 키 테스트",
|
||||||
"EyeTracking.blink.reset.label": "깜빡임 테스트 초기화",
|
"EyeTracking.blink.reset.label": "깜빡임 테스트 재설정",
|
||||||
"EyeTracking.blink.reset.desc": "깜빡임 테스트 값 초기화",
|
"EyeTracking.blink.reset.desc": "깜빡임 테스트 값 재설정",
|
||||||
"EyeTracking.validation.noArmature": "씬에서 아마추어를 찾을 수 없음",
|
"EyeTracking.validation.noArmature": "씬에서 아마추어를 찾을 수 없음",
|
||||||
"EyeTracking.validation.noMesh": "메시 '{mesh}'를 찾을 수 없음",
|
"EyeTracking.validation.noMesh": "메시 '{mesh}'를 찾을 수 없음",
|
||||||
"EyeTracking.validation.noShapekeys": "선택된 메시에 쉐이프 키가 없음",
|
"EyeTracking.validation.noShapekeys": "선택한 메시에 쉐이프 키가 없음",
|
||||||
"EyeTracking.validation.leftEye": "왼쪽 눈",
|
"EyeTracking.validation.leftEye": "왼쪽 눈",
|
||||||
"EyeTracking.validation.rightEye": "오른쪽 눈",
|
"EyeTracking.validation.rightEye": "오른쪽 눈",
|
||||||
"EyeTracking.validation.missingGroups": "누락된 버텍스 그룹: {groups}",
|
"EyeTracking.validation.missingGroups": "누락된 버텍스 그룹: {groups}",
|
||||||
"EyeTracking.validation.missingBones": "필요한 본 누락: {bones}",
|
"EyeTracking.validation.missingBones": "필요한 본 누락: {bones}",
|
||||||
"EyeTracking.validation.success": "시선 추적 설정이 성공적으로 검증됨",
|
"EyeTracking.validation.success": "시선 추적 설정 검증 성공",
|
||||||
"EyeTracking.error.noMesh": "시선 추적을 위한 메시가 선택되지 않음",
|
"EyeTracking.error.noMesh": "시선 추적을 위한 메시가 선택되지 않음",
|
||||||
"EyeTracking.error.noVertexGroup": "본을 위한 버텍스 그룹을 찾을 수 없음: {bone}",
|
"EyeTracking.error.noVertexGroup": "본에 대한 버텍스 그룹을 찾을 수 없음: {bone}",
|
||||||
"EyeTracking.error.noShapeSelected": "필요한 모든 쉐이프 키를 선택하세요",
|
"EyeTracking.error.noShapeSelected": "모든 필수 쉐이프 키를 선택하세요",
|
||||||
"EyeTracking.success": "시선 추적 설정이 성공적으로 완료됨",
|
"EyeTracking.success": "시선 추적 설정 완료 성공",
|
||||||
"EyeTracking.mode_select": "모드 선택",
|
"EyeTracking.mode_select": "모드 선택",
|
||||||
"EyeTracking.mesh_setup": "메시 설정",
|
"EyeTracking.mesh_setup": "메시 설정",
|
||||||
"EyeTracking.bone_setup": "본 설정",
|
"EyeTracking.bone_setup": "본 설정",
|
||||||
@@ -308,16 +376,21 @@
|
|||||||
"EyeTracking.type": "시선 추적 유형",
|
"EyeTracking.type": "시선 추적 유형",
|
||||||
"EyeTracking.type_desc": "생성할 시선 추적 설정 유형 선택",
|
"EyeTracking.type_desc": "생성할 시선 추적 설정 유형 선택",
|
||||||
"EyeTracking.create.av3.label": "AV3 시선 추적 생성",
|
"EyeTracking.create.av3.label": "AV3 시선 추적 생성",
|
||||||
"EyeTracking.create.av3.desc": "VRChat Avatar 3.0용 시선 추적 설정",
|
"EyeTracking.create.av3.desc": "VRChat 아바타 3.0용 시선 추적 설정",
|
||||||
"EyeTracking.create.sdk2.label": "SDK2 시선 추적 생성",
|
"EyeTracking.create.sdk2.label": "SDK2 시선 추적 생성",
|
||||||
"EyeTracking.create.sdk2.desc": "VRChat SDK2용 시선 추적 설정",
|
"EyeTracking.create.sdk2.desc": "VRChat SDK2용 시선 추적 설정",
|
||||||
"EyeTracking.sdk_version": "SDK 버전",
|
"EyeTracking.sdk_version": "SDK 버전",
|
||||||
"EyeTracking.type.av3": "Avatar 3.0",
|
"EyeTracking.type.av3": "아바타 3.0",
|
||||||
"EyeTracking.type.av3_desc": "VRChat Avatar 3.0 시선 추적 설정",
|
"EyeTracking.type.av3_desc": "VRChat 아바타 3.0 시선 추적 설정",
|
||||||
"EyeTracking.type.sdk2": "SDK2 (레거시)",
|
"EyeTracking.type.sdk2": "레거시 (ChilloutVR)",
|
||||||
"EyeTracking.type.sdk2_desc": "VRChat SDK2 시선 추적 설정",
|
"EyeTracking.type.sdk2_desc": "레거시 (SDK2) 시선 추적 설정",
|
||||||
"EyeTracking.adjust.label": "눈 위치 조정",
|
"EyeTracking.adjust.label": "눈 위치 조정",
|
||||||
"EyeTracking.adjust.desc": "버텍스 그룹을 기반으로 눈 본 위치 조정",
|
"EyeTracking.adjust.desc": "버텍스 그룹을 기반으로 눈 본 위치 조정",
|
||||||
|
"EyeTracking.sdk2_warning": "레거시 (SDK2) 시선 추적 알림",
|
||||||
|
"EyeTracking.sdk2_warning_detail1": "이 시스템은 VRChat에 사용해서는 안 됩니다,",
|
||||||
|
"EyeTracking.sdk2_warning_detail2": "시선 추적은 이제 Unity에서 직접",
|
||||||
|
"EyeTracking.sdk2_warning_detail3": "구성됩니다. ChilloutVR과 같은 다른 플랫폼을",
|
||||||
|
"EyeTracking.sdk2_warning_detail4": "위해 남아 있습니다.",
|
||||||
|
|
||||||
"CustomPanel.label": "커스텀 아바타 도구",
|
"CustomPanel.label": "커스텀 아바타 도구",
|
||||||
"CustomPanel.merge_mode": "병합 모드",
|
"CustomPanel.merge_mode": "병합 모드",
|
||||||
@@ -326,7 +399,7 @@
|
|||||||
"CustomPanel.select_bone": "본 선택",
|
"CustomPanel.select_bone": "본 선택",
|
||||||
"CustomPanel.select_armature": "아마추어 선택",
|
"CustomPanel.select_armature": "아마추어 선택",
|
||||||
"CustomPanel.mode.armature": "아마추어",
|
"CustomPanel.mode.armature": "아마추어",
|
||||||
"CustomPanel.mode.armature_desc": "아마추어 함께 병합",
|
"CustomPanel.mode.armature_desc": "아마추어 병합",
|
||||||
"CustomPanel.mode.mesh": "메시",
|
"CustomPanel.mode.mesh": "메시",
|
||||||
"CustomPanel.mode.mesh_desc": "메시를 아마추어에 부착",
|
"CustomPanel.mode.mesh_desc": "메시를 아마추어에 부착",
|
||||||
|
|
||||||
@@ -335,18 +408,18 @@
|
|||||||
"AttachMesh.search_desc": "부착할 메시 검색",
|
"AttachMesh.search_desc": "부착할 메시 검색",
|
||||||
"AttachMesh.select": "부착할 메시 선택",
|
"AttachMesh.select": "부착할 메시 선택",
|
||||||
"AttachMesh.select_desc": "아마추어에 부착할 메시 선택",
|
"AttachMesh.select_desc": "아마추어에 부착할 메시 선택",
|
||||||
"AttachMesh.success": "메시가 성공적으로 부착됨",
|
"AttachMesh.success": "메시 부착 성공",
|
||||||
"AttachMesh.warn_no_armature": "부착할 아마추어와 메시를 선택하세요",
|
"AttachMesh.warn_no_armature": "부착할 아마추어와 메시를 선택하세요",
|
||||||
"AttachMesh.validate_transforms": "메시 변형 검증 중",
|
"AttachMesh.validate_transforms": "메시 변형 검증 중",
|
||||||
"AttachMesh.validate_name": "메시 이름 검증 중",
|
"AttachMesh.validate_name": "메시 이름 검증 중",
|
||||||
"AttachMesh.parent_mesh": "메시를 아마추어에 페어런팅",
|
"AttachMesh.parent_mesh": "메시를 아마추어에 부모 설정 중",
|
||||||
"AttachMesh.setup_weights": "버텍스 가중치 설정 중",
|
"AttachMesh.setup_weights": "버텍스 가중치 설정 중",
|
||||||
"AttachMesh.create_bone": "부착 본 생성 중",
|
"AttachMesh.create_bone": "부착 본 생성 중",
|
||||||
"AttachMesh.position_bone": "본 위치 지정 중",
|
"AttachMesh.position_bone": "본 위치 지정 중",
|
||||||
"AttachMesh.add_modifier": "아마추어 모디파이어 추가 중",
|
"AttachMesh.add_modifier": "아마추어 모디파이어 추가 중",
|
||||||
"AttachMesh.error.bone_not_found": "부착 본 '{bone}'을(를) 찾을 수 없음",
|
"AttachMesh.error.bone_not_found": "부착 본 '{bone}'을(를) 찾을 수 없음",
|
||||||
"AttachMesh.error.mesh_not_found": "메시를 찾을 수 없음",
|
"AttachMesh.error.mesh_not_found": "메시를 찾을 수 없음",
|
||||||
"AttachMesh.error.non_uniform_scale": "메시에 비균일 스케일이 있습니다. 스케일을 적용하세요",
|
"AttachMesh.error.non_uniform_scale": "메시의 크기가 균일하지 않습니다. 크기를 적용하세요",
|
||||||
"AttachBone.search_desc": "대상 본 검색",
|
"AttachBone.search_desc": "대상 본 검색",
|
||||||
"AttachBone.select": "대상 본 선택",
|
"AttachBone.select": "대상 본 선택",
|
||||||
"AttachBone.select_desc": "메시를 부착할 본 선택",
|
"AttachBone.select_desc": "메시를 부착할 본 선택",
|
||||||
@@ -362,51 +435,72 @@
|
|||||||
"MergeArmature.from_desc": "병합할 소스 아마추어",
|
"MergeArmature.from_desc": "병합할 소스 아마추어",
|
||||||
"MergeArmature.from_search_desc": "소스 아마추어 검색",
|
"MergeArmature.from_search_desc": "소스 아마추어 검색",
|
||||||
"MergeArmature.error.not_found": "아마추어 '{name}'을(를) 찾을 수 없음",
|
"MergeArmature.error.not_found": "아마추어 '{name}'을(를) 찾을 수 없음",
|
||||||
"MergeArmature.error.transforms_not_aligned": "이 아마추어를 병합하려면 변형을 적용해야 합니다. 수동 방법이나 변형 적용 체크박스를 통해 수행하세요",
|
"MergeArmature.error.transforms_not_aligned": "이 아마추어를 병합하려면 변형을 적용해야 합니다. 수동 방법 또는 변형 적용 체크박스를 통해 이 작업을 수행하세요",
|
||||||
"MergeArmature.error.check_transforms": "부모 변형을 확인하세요",
|
"MergeArmature.error.check_transforms": "부모 변형을 확인하세요",
|
||||||
"MergeArmature.error.fix_parents": "부모 관계를 수정하세요",
|
"MergeArmature.error.fix_parents": "부모 관계를 수정하세요",
|
||||||
"MergeArmature.progress.removing_rigidbodies": "강체와 조인트 제거 중",
|
"MergeArmature.progress.removing_rigidbodies": "리지드 바디 및 조인트 제거 중",
|
||||||
"MergeArmature.progress.validating": "아마추어 검증 중",
|
"MergeArmature.progress.validating": "아마추어 검증 중",
|
||||||
"MergeArmature.progress.merging": "아마추어 병합 중",
|
"MergeArmature.progress.merging": "아마추어 병합 중",
|
||||||
"MergeArmature.success": "아마추어가 성공적으로 병합됨",
|
"MergeArmature.success": "아마추어 병합 성공",
|
||||||
"MergeArmature.merge_all": "동일한 본 병합",
|
"MergeArmature.merge_all": "동일한 본 병합",
|
||||||
"MergeArmature.merge_all_desc": "일치하는 이름의 본 병합",
|
"MergeArmature.merge_all_desc": "일치하는 이름을 가진 본 병합",
|
||||||
"MergeArmature.apply_transforms": "변형 적용",
|
"MergeArmature.apply_transforms": "변형 적용",
|
||||||
"MergeArmature.apply_transforms_desc": "병합 전 모든 변형 적용",
|
"MergeArmature.apply_transforms_desc": "병합 전 모든 변형 적용",
|
||||||
"MergeArmature.join_meshes": "메시 결합",
|
"MergeArmature.join_meshes": "메시 결합",
|
||||||
"MergeArmature.join_meshes_desc": "병합 후 메시 결합",
|
"MergeArmature.join_meshes_desc": "병합 후 메시 결합",
|
||||||
"MergeArmature.remove_zero_weights": "0 가중치 제거",
|
"MergeArmature.remove_zero_weights": "가중치 0 제거",
|
||||||
"MergeArmature.remove_zero_weights_desc": "가중치가 없는 버텍스 그룹 제거",
|
"MergeArmature.remove_zero_weights_desc": "가중치가 없는 버텍스 그룹 제거",
|
||||||
"MergeArmature.cleanup_shape_keys": "쉐이프 키 정리",
|
"MergeArmature.cleanup_shape_keys": "쉐이프 키 정리",
|
||||||
"MergeArmature.cleanup_shape_keys_desc": "미사용 쉐이프 키 제거",
|
"MergeArmature.cleanup_shape_keys_desc": "미사용 쉐이프 키 제거",
|
||||||
|
|
||||||
"TextureAtlas.atlas_completed": "텍스처 아틀라스 생성이 완료되었습니다",
|
"TextureAtlas.atlas_completed": "텍스처 아틀라스 생성 완료",
|
||||||
"TextureAtlas.atlas_error": "텍스처 아틀라스 생성 중 오류가 발생했습니다",
|
"TextureAtlas.atlas_error": "텍스처 아틀라스 생성 중 오류 발생",
|
||||||
"TextureAtlas.atlas_materials": "재질 아틀라스화",
|
"TextureAtlas.atlas_materials": "아틀라스 재질",
|
||||||
"TextureAtlas.atlas_materials_desc": "모델을 최적화하기 위해 재질을 아틀라스화",
|
"TextureAtlas.atlas_materials_desc": "모델을 최적화하기 위한 아틀라스 재질",
|
||||||
"TextureAtlas.label": "텍스처 아틀라스화",
|
"TextureAtlas.label": "텍스처 아틀라싱",
|
||||||
"TextureAtlas.loaded_list": "텍스처 아틀라스 재질 목록 로드됨",
|
"TextureAtlas.loaded_list": "로드된 텍스처 아틀라스 재질 목록",
|
||||||
"TextureAtlas.material_list_label": "텍스처 아틀라스 재질 목록",
|
"TextureAtlas.material_list_label": "텍스처 아틀라스 재질 목록 재질",
|
||||||
"TextureAtlas.reload_list": "텍스처 아틀라스 재질 목록 새로고침",
|
"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.albedo": "알베도",
|
"TextureAtlas.albedo": "알베도",
|
||||||
"TextureAtlas.normal": "노말",
|
"TextureAtlas.normal": "노멀",
|
||||||
"TextureAtlas.emission": "이미션",
|
"TextureAtlas.emission": "이미션",
|
||||||
"TextureAtlas.ambient_occlusion": "앰비언트 오클루전",
|
"TextureAtlas.ambient_occlusion": "앰비언트 오클루전",
|
||||||
"TextureAtlas.height": "높이",
|
"TextureAtlas.height": "높이",
|
||||||
"TextureAtlas.roughness": "거칠기",
|
"TextureAtlas.roughness": "거칠기",
|
||||||
|
"TextureAtlas.description_1": "텍스처를 결합한 단일 재질 생성",
|
||||||
|
"TextureAtlas.description_2": "아바타의 성능을 최적화합니다.",
|
||||||
|
"TextureAtlas.texture_maps": "텍스처 맵",
|
||||||
|
"TextureAtlas.material_ready": "재질이 아틀라스 생성 준비가 되었습니다",
|
||||||
|
"TextureAtlas.material_not_ready": "재질에는 최소 하나의 텍스처가 필요합니다",
|
||||||
|
"TextureAtlas.select_all_tooltip": "모든 재질 선택",
|
||||||
|
"TextureAtlas.select_none_tooltip": "모든 선택 해제",
|
||||||
|
"TextureAtlas.expand_all_tooltip": "모든 재질 설정 펼치기",
|
||||||
|
"TextureAtlas.collapse_all_tooltip": "모든 재질 설정 접기",
|
||||||
|
"TextureAtlas.estimated_size": "예상 아틀라스 크기",
|
||||||
|
"TextureAtlas.materials": "재질",
|
||||||
|
"TextureAtlas.no_materials_selected": "선택된 재질이 없습니다",
|
||||||
|
"TextureAtlas.select_armature_first": "먼저 아마추어를 선택해주세요",
|
||||||
|
"TextureAtlas.how_to_use_1": "1. 장면에서 아마추어 선택",
|
||||||
|
"TextureAtlas.how_to_use_2": "2. '재질 불러오기'를 클릭하여 시작",
|
||||||
|
"TextureAtlas.load_error": "재질 로딩 오류. 자세한 내용은 콘솔을 확인하세요.",
|
||||||
|
"TextureAtlas.material_not_included": "재질이 아틀라스에 포함되지 않았습니다",
|
||||||
|
"TextureAtlas.save_file_first": "텍스처 아틀라스를 만들기 전에 Blender 파일을 저장하세요",
|
||||||
|
"TextureAtlas.save_file_instructions": "파일 > 다른 이름으로 저장... 을 사용하거나 아래 버튼을 클릭하세요:",
|
||||||
|
"TextureAtlas.save_file_button": "Blender 파일 저장",
|
||||||
|
"TextureAtlas.save_file_required": "파일 저장 필요",
|
||||||
|
|
||||||
"Settings.label": "설정",
|
"Settings.label": "설정",
|
||||||
"Settings.language": "언어",
|
"Settings.language": "언어",
|
||||||
"Settings.language_desc": "인터페이스 언어 선택",
|
"Settings.language_desc": "인터페이스 언어 선택",
|
||||||
"Settings.validation_mode": "검증 모드",
|
"Settings.validation_mode": "검증 모드",
|
||||||
"Settings.validation_mode_desc": "아마추어 검증의 엄격성 선택",
|
"Settings.validation_mode_desc": "아마추어 검증 엄격성 선택",
|
||||||
"Settings.validation_mode.strict": "엄격",
|
"Settings.validation_mode.strict": "엄격",
|
||||||
"Settings.validation_mode.strict_desc": "본 계층 구조와 대칭성을 포함한 전체 검증",
|
"Settings.validation_mode.strict_desc": "본 계층 구조 및 대칭성을 포함한 전체 검증",
|
||||||
"Settings.validation_mode.basic": "기본",
|
"Settings.validation_mode.basic": "기본",
|
||||||
"Settings.validation_mode.basic_desc": "필수 본 확인만",
|
"Settings.validation_mode.basic_desc": "필수 본 확인만",
|
||||||
"Settings.validation_mode.none": "없음",
|
"Settings.validation_mode.none": "없음",
|
||||||
@@ -415,14 +509,17 @@
|
|||||||
"Settings.logging": "로깅",
|
"Settings.logging": "로깅",
|
||||||
"Settings.enable_logging": "디버그 로깅 활성화",
|
"Settings.enable_logging": "디버그 로깅 활성화",
|
||||||
"Settings.enable_logging_desc": "문제 해결을 위한 상세 디버그 로깅 활성화",
|
"Settings.enable_logging_desc": "문제 해결을 위한 상세 디버그 로깅 활성화",
|
||||||
"Settings.logging_enabled": "디버그 로깅이 활성화됨",
|
"Settings.logging_enabled": "디버그 로깅 활성화됨",
|
||||||
"Settings.logging_disabled": "디버그 로깅이 비활성화됨",
|
"Settings.logging_disabled": "디버그 로깅 비활성화됨",
|
||||||
|
"Settings.highlight_problem_bones": "문제 본 강조 표시",
|
||||||
|
"Settings.highlight_problem_bones_desc": "뷰포트에서 검증 문제가 있는 본 강조 표시",
|
||||||
|
"Settings.bone_highlighting": "본 강조 표시",
|
||||||
"Language.auto": "자동",
|
"Language.auto": "자동",
|
||||||
"Language.en_US": "영어",
|
"Language.en_US": "영어",
|
||||||
"Language.ja_JP": "일본어",
|
"Language.ja_JP": "일본어",
|
||||||
"Language.ko_KR": "한국어",
|
"Language.ko_KR": "한국어",
|
||||||
"Language.changed.title": "언어 변경됨",
|
"Language.changed.title": "언어 변경됨",
|
||||||
"Language.changed.success": "언어가 성공적으로 변경됨!",
|
"Language.changed.success": "언어가 성공적으로 변경되었습니다!",
|
||||||
"Language.changed.restart": "일부 UI 요소는 블렌더 재시작이 필요할 수 있음"
|
"Language.changed.restart": "일부 UI 요소는 블렌더를 다시 시작해야 할 수 있습니다"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
+133
-28
@@ -5,6 +5,7 @@ from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
|||||||
from ..core.common import SceneMatClass, MaterialListBool, get_active_armature
|
from ..core.common import SceneMatClass, MaterialListBool, get_active_armature
|
||||||
from ..functions.atlas_materials import AvatarToolKit_OT_AtlasMaterials
|
from ..functions.atlas_materials import AvatarToolKit_OT_AtlasMaterials
|
||||||
from ..core.translations import t
|
from ..core.translations import t
|
||||||
|
from ..core.logging_setup import logger
|
||||||
|
|
||||||
class AvatarToolKit_OT_SelectAllMaterials(Operator):
|
class AvatarToolKit_OT_SelectAllMaterials(Operator):
|
||||||
bl_idname = 'avatar_toolkit.select_all_materials'
|
bl_idname = 'avatar_toolkit.select_all_materials'
|
||||||
@@ -56,9 +57,12 @@ class AvatarToolKit_OT_ExpandSectionMaterials(Operator):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def execute(self, context: Context) -> set:
|
def execute(self, context: Context) -> set:
|
||||||
|
try:
|
||||||
if not context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown:
|
if not context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown:
|
||||||
context.scene.avatar_toolkit.materials.clear()
|
context.scene.avatar_toolkit.materials.clear()
|
||||||
newlist: list[Material] = []
|
newlist: list[Material] = []
|
||||||
|
|
||||||
|
logger.debug("Loading materials for texture atlas")
|
||||||
for obj in context.scene.objects:
|
for obj in context.scene.objects:
|
||||||
if len(obj.material_slots) > 0:
|
if len(obj.material_slots) > 0:
|
||||||
for mat_slot in obj.material_slots:
|
for mat_slot in obj.material_slots:
|
||||||
@@ -67,11 +71,19 @@ class AvatarToolKit_OT_ExpandSectionMaterials(Operator):
|
|||||||
newlist.append(mat_slot.material)
|
newlist.append(mat_slot.material)
|
||||||
newitem: SceneMatClass = context.scene.avatar_toolkit.materials.add()
|
newitem: SceneMatClass = context.scene.avatar_toolkit.materials.add()
|
||||||
newitem.mat = mat_slot.material
|
newitem.mat = mat_slot.material
|
||||||
|
|
||||||
MaterialListBool.old_list[context.scene.name] = newlist
|
MaterialListBool.old_list[context.scene.name] = newlist
|
||||||
context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown = True
|
context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown = True
|
||||||
|
logger.info(f"Loaded {len(newlist)} materials for texture atlas")
|
||||||
else:
|
else:
|
||||||
context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown = False
|
context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown = False
|
||||||
|
logger.debug("Hiding material list")
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error loading materials: {str(e)}", exc_info=True)
|
||||||
|
self.report({'ERROR'}, t("TextureAtlas.load_error"))
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
class AvatarToolKit_UL_MaterialTextureAtlasProperties(UIList):
|
class AvatarToolKit_UL_MaterialTextureAtlasProperties(UIList):
|
||||||
bl_label = t("TextureAtlas.material_list_label")
|
bl_label = t("TextureAtlas.material_list_label")
|
||||||
@@ -81,17 +93,30 @@ class AvatarToolKit_UL_MaterialTextureAtlasProperties(UIList):
|
|||||||
|
|
||||||
def draw_header(self, context):
|
def draw_header(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
row = layout.row(align=True)
|
|
||||||
|
|
||||||
row.operator("avatar_toolkit.select_all_materials", text="", icon='CHECKBOX_HLT')
|
row = layout.row(align=True)
|
||||||
row.operator("avatar_toolkit.select_none_materials", text="", icon='CHECKBOX_DEHLT')
|
row.scale_y = 1.2
|
||||||
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.operator("avatar_toolkit.select_all_materials", text="", icon='CHECKBOX_HLT',
|
||||||
row.prop(context.scene.avatar_toolkit, "material_search_filter", text="", icon='VIEWZOOM')
|
emboss=True).tooltip = t("TextureAtlas.select_all_tooltip")
|
||||||
|
row.operator("avatar_toolkit.select_none_materials", text="", icon='CHECKBOX_DEHLT',
|
||||||
|
emboss=True).tooltip = t("TextureAtlas.select_none_tooltip")
|
||||||
|
row.separator(factor=0.5)
|
||||||
|
row.operator("avatar_toolkit.expand_all_materials", text="", icon='DISCLOSURE_TRI_DOWN',
|
||||||
|
emboss=True).tooltip = t("TextureAtlas.expand_all_tooltip")
|
||||||
|
row.operator("avatar_toolkit.collapse_all_materials", text="", icon='DISCLOSURE_TRI_RIGHT',
|
||||||
|
emboss=True).tooltip = t("TextureAtlas.collapse_all_tooltip")
|
||||||
|
|
||||||
|
row.separator(factor=1.0)
|
||||||
|
search_row = row.row()
|
||||||
|
search_row.scale_x = 2.0
|
||||||
|
search_row.prop(context.scene.avatar_toolkit, "material_search_filter", text="", icon='VIEWZOOM')
|
||||||
|
|
||||||
box = layout.box()
|
box = layout.box()
|
||||||
row = box.row()
|
size_row = box.row()
|
||||||
row.label(text=f"Estimated Atlas Size: {self.calculate_atlas_size(context)}px")
|
size_row.alignment = 'CENTER'
|
||||||
|
size_text = self.calculate_atlas_size(context)
|
||||||
|
size_row.label(text=f"{t('TextureAtlas.estimated_size')}: {size_text}px", icon='TEXTURE')
|
||||||
|
|
||||||
def draw_item(self, context: Context, layout: UILayout, data: Object, item: SceneMatClass, icon, active_data, active_propname, index):
|
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.texture_atlas_Has_Mat_List_Shown:
|
||||||
@@ -99,34 +124,64 @@ class AvatarToolKit_UL_MaterialTextureAtlasProperties(UIList):
|
|||||||
context.scene.avatar_toolkit.material_search_filter.lower() not in item.mat.name.lower()):
|
context.scene.avatar_toolkit.material_search_filter.lower() not in item.mat.name.lower()):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Main material
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
|
row.prop(item.mat, "include_in_atlas", text="",
|
||||||
|
icon='CHECKBOX_HLT' if item.mat.include_in_atlas else 'CHECKBOX_DEHLT',
|
||||||
|
emboss=False)
|
||||||
|
|
||||||
row.prop(item.mat, "include_in_atlas", text="", icon='CHECKBOX_HLT' if item.mat.include_in_atlas else 'CHECKBOX_DEHLT')
|
# Material name
|
||||||
|
|
||||||
row.prop(item.mat, "material_expanded",
|
row.prop(item.mat, "material_expanded",
|
||||||
text=item.mat.name,
|
text=item.mat.name,
|
||||||
icon='DOWNARROW_HLT' if item.mat.material_expanded else 'RIGHTARROW',
|
icon='DOWNARROW_HLT' if item.mat.material_expanded else 'RIGHTARROW',
|
||||||
emboss=False)
|
emboss=False)
|
||||||
|
|
||||||
if item.mat.material_expanded and item.mat.include_in_atlas:
|
row.label(text="", icon='MATERIAL')
|
||||||
|
|
||||||
|
if item.mat.material_expanded:
|
||||||
box = layout.box()
|
box = layout.box()
|
||||||
col = box.column(align=True)
|
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")
|
header_row = col.row()
|
||||||
self.draw_texture_row(col, item.mat, "texture_atlas_emission", "LIGHT")
|
header_row.alignment = 'CENTER'
|
||||||
self.draw_texture_row(col, item.mat, "texture_atlas_ambient_occlusion", "SHADING_SOLID")
|
header_row.label(text=t("TextureAtlas.texture_maps"), icon='IMAGE')
|
||||||
self.draw_texture_row(col, item.mat, "texture_atlas_height", "IMAGE_ZDEPTH")
|
col.separator(factor=0.5)
|
||||||
self.draw_texture_row(col, item.mat, "texture_atlas_roughness", "MATERIAL")
|
self.draw_texture_row(col, item.mat, "texture_atlas_albedo", "IMAGE_RGB", t("TextureAtlas.albedo"))
|
||||||
|
self.draw_texture_row(col, item.mat, "texture_atlas_normal", "NORMALS_FACE", t("TextureAtlas.normal"))
|
||||||
|
self.draw_texture_row(col, item.mat, "texture_atlas_emission", "LIGHT", t("TextureAtlas.emission"))
|
||||||
|
self.draw_texture_row(col, item.mat, "texture_atlas_ambient_occlusion", "SHADING_SOLID", t("TextureAtlas.ambient_occlusion"))
|
||||||
|
self.draw_texture_row(col, item.mat, "texture_atlas_height", "IMAGE_ZDEPTH", t("TextureAtlas.height"))
|
||||||
|
self.draw_texture_row(col, item.mat, "texture_atlas_roughness", "MATERIAL", t("TextureAtlas.roughness"))
|
||||||
|
|
||||||
col.separator(factor=0.5)
|
col.separator(factor=0.5)
|
||||||
|
|
||||||
def draw_texture_row(self, layout, material, prop_name, icon):
|
status_row = col.row()
|
||||||
row = layout.row()
|
status_row.alignment = 'CENTER'
|
||||||
row.prop(material, prop_name, icon=icon)
|
is_ready = self.is_material_ready(item.mat)
|
||||||
if getattr(material, prop_name):
|
|
||||||
row.label(text="", icon='CHECKMARK')
|
if item.mat.include_in_atlas:
|
||||||
|
status_text = t("TextureAtlas.material_ready") if is_ready else t("TextureAtlas.material_not_ready")
|
||||||
|
status_icon = 'CHECKMARK' if is_ready else 'ERROR'
|
||||||
else:
|
else:
|
||||||
row.label(text="", icon='X')
|
status_text = t("TextureAtlas.material_not_included")
|
||||||
|
status_icon = 'INFO'
|
||||||
|
|
||||||
|
status_row.label(text=status_text, icon=status_icon)
|
||||||
|
|
||||||
|
def draw_texture_row(self, layout, material, prop_name, icon, label_text):
|
||||||
|
row = layout.row(align=True)
|
||||||
|
icon_row = row.row()
|
||||||
|
icon_row.scale_x = 0.5
|
||||||
|
icon_row.label(text="", icon=icon)
|
||||||
|
|
||||||
|
# Texture selector
|
||||||
|
row.prop(material, prop_name, text=label_text)
|
||||||
|
status_row = row.row()
|
||||||
|
status_row.scale_x = 0.5
|
||||||
|
if getattr(material, prop_name):
|
||||||
|
status_row.label(text="", icon='CHECKMARK')
|
||||||
|
else:
|
||||||
|
status_row.label(text="", icon='X')
|
||||||
|
|
||||||
def is_material_ready(self, material):
|
def is_material_ready(self, material):
|
||||||
return bool(material.texture_atlas_albedo or
|
return bool(material.texture_atlas_albedo or
|
||||||
@@ -135,12 +190,21 @@ class AvatarToolKit_UL_MaterialTextureAtlasProperties(UIList):
|
|||||||
|
|
||||||
def calculate_atlas_size(self, context):
|
def calculate_atlas_size(self, context):
|
||||||
total_size = 0
|
total_size = 0
|
||||||
|
selected_count = 0
|
||||||
|
|
||||||
for mat in context.scene.avatar_toolkit.materials:
|
for mat in context.scene.avatar_toolkit.materials:
|
||||||
if mat.mat.include_in_atlas:
|
if mat.mat.include_in_atlas:
|
||||||
|
selected_count += 1
|
||||||
if mat.mat.texture_atlas_albedo:
|
if mat.mat.texture_atlas_albedo:
|
||||||
img = bpy.data.images[mat.mat.texture_atlas_albedo]
|
img = bpy.data.images[mat.mat.texture_atlas_albedo]
|
||||||
total_size += img.size[0] * img.size[1]
|
total_size += img.size[0] * img.size[1]
|
||||||
return f"{int(sqrt(total_size))}x{int(sqrt(total_size))}"
|
|
||||||
|
if total_size == 0:
|
||||||
|
return f"0x0 ({t('TextureAtlas.no_materials_selected')})"
|
||||||
|
size = int(sqrt(total_size))
|
||||||
|
pot_size = 2 ** (size - 1).bit_length() # Next power of 2
|
||||||
|
|
||||||
|
return f"{pot_size}x{pot_size} ({selected_count} {t('TextureAtlas.materials')})"
|
||||||
|
|
||||||
class AvatarToolKit_PT_TextureAtlasPanel(Panel):
|
class AvatarToolKit_PT_TextureAtlasPanel(Panel):
|
||||||
bl_label = t("TextureAtlas.label")
|
bl_label = t("TextureAtlas.label")
|
||||||
@@ -149,23 +213,43 @@ class AvatarToolKit_PT_TextureAtlasPanel(Panel):
|
|||||||
bl_region_type = 'UI'
|
bl_region_type = 'UI'
|
||||||
bl_category = CATEGORY_NAME
|
bl_category = CATEGORY_NAME
|
||||||
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
||||||
bl_order = 6
|
bl_order = 7
|
||||||
|
|
||||||
def draw(self, context: Context):
|
def draw(self, context: Context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
armature = get_active_armature(context)
|
armature = get_active_armature(context)
|
||||||
|
|
||||||
if armature:
|
if armature:
|
||||||
layout.label(text=t("TextureAtlas.label"), icon='TEXTURE')
|
header_row = layout.row()
|
||||||
|
header_row.label(text=t("TextureAtlas.label"), icon='TEXTURE')
|
||||||
|
layout.separator(factor=0.5)
|
||||||
|
info_box = layout.box()
|
||||||
|
info_col = info_box.column()
|
||||||
|
info_col.scale_y = 0.9
|
||||||
|
info_col.label(text=t("TextureAtlas.description_1"), icon='INFO')
|
||||||
|
info_col.label(text=t("TextureAtlas.description_2"))
|
||||||
|
|
||||||
|
if not bpy.data.filepath:
|
||||||
|
warning_box = layout.box()
|
||||||
|
warning_col = warning_box.column()
|
||||||
|
warning_col.scale_y = 0.9
|
||||||
|
warning_col.alert = True
|
||||||
|
warning_col.label(text=t("TextureAtlas.save_file_first"), icon='ERROR')
|
||||||
|
warning_col.label(text=t("TextureAtlas.save_file_instructions"))
|
||||||
|
warning_col.operator("wm.save_as_mainfile", text=t("TextureAtlas.save_file_button"), icon='FILE_TICK')
|
||||||
layout.separator(factor=0.5)
|
layout.separator(factor=0.5)
|
||||||
|
|
||||||
|
layout.separator(factor=0.5)
|
||||||
box = layout.box()
|
box = layout.box()
|
||||||
row = box.row()
|
row = box.row(align=True)
|
||||||
|
row.scale_y = 1.2
|
||||||
direction_icon = 'RIGHTARROW' if not context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown else 'DOWNARROW_HLT'
|
direction_icon = 'RIGHTARROW' if not context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown else 'DOWNARROW_HLT'
|
||||||
|
button_text = t("TextureAtlas.reload_list") if not context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown else t("TextureAtlas.loaded_list")
|
||||||
row.operator(AvatarToolKit_OT_ExpandSectionMaterials.bl_idname,
|
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")),
|
text=button_text,
|
||||||
icon=direction_icon)
|
icon=direction_icon)
|
||||||
|
|
||||||
|
# Material list expanded
|
||||||
if context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown:
|
if context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown:
|
||||||
row = box.row()
|
row = box.row()
|
||||||
row.template_list(AvatarToolKit_UL_MaterialTextureAtlasProperties.bl_idname,
|
row.template_list(AvatarToolKit_UL_MaterialTextureAtlasProperties.bl_idname,
|
||||||
@@ -181,8 +265,29 @@ class AvatarToolKit_PT_TextureAtlasPanel(Panel):
|
|||||||
|
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
row.scale_y = 1.5
|
row.scale_y = 1.5
|
||||||
|
row.enabled = context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown
|
||||||
|
|
||||||
|
has_selected = False
|
||||||
|
if context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown:
|
||||||
|
for item in context.scene.avatar_toolkit.materials:
|
||||||
|
if item.mat.include_in_atlas:
|
||||||
|
has_selected = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not has_selected and context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown:
|
||||||
|
row.operator(AvatarToolKit_OT_AtlasMaterials.bl_idname,
|
||||||
|
text=t("TextureAtlas.no_materials_selected"),
|
||||||
|
icon='ERROR')
|
||||||
|
else:
|
||||||
row.operator(AvatarToolKit_OT_AtlasMaterials.bl_idname,
|
row.operator(AvatarToolKit_OT_AtlasMaterials.bl_idname,
|
||||||
text=t("TextureAtlas.atlas_materials"),
|
text=t("TextureAtlas.atlas_materials"),
|
||||||
icon='NODE_TEXTURE')
|
icon='NODE_TEXTURE')
|
||||||
else:
|
else:
|
||||||
layout.label(text=t("Tools.select_armature"), icon='ERROR')
|
layout.label(text=t("Tools.select_armature"), icon='ERROR')
|
||||||
|
|
||||||
|
box = layout.box()
|
||||||
|
col = box.column()
|
||||||
|
col.scale_y = 0.9
|
||||||
|
col.label(text=t("TextureAtlas.select_armature_first"), icon='INFO')
|
||||||
|
col.label(text=t("TextureAtlas.how_to_use_1"))
|
||||||
|
col.label(text=t("TextureAtlas.how_to_use_2"))
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ import bpy
|
|||||||
from typing import Set, List, Tuple, Any
|
from typing import Set, List, Tuple, Any
|
||||||
from bpy.types import Panel, Context, UILayout, Operator, Event, WindowManager
|
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 ..functions.custom_tools.mesh_attachment import AvatarToolkit_OT_AttachMesh
|
||||||
|
from ..functions.custom_tools.armature_merging import AvatarToolkit_OT_MergeArmature
|
||||||
from ..core.translations import t
|
from ..core.translations import t
|
||||||
from ..core.common import (
|
from ..core.common import (
|
||||||
get_active_armature,
|
get_active_armature,
|
||||||
get_all_meshes,
|
get_all_meshes,
|
||||||
validate_armature,
|
|
||||||
get_armature_list
|
get_armature_list
|
||||||
)
|
)
|
||||||
|
from ..core.armature_validation import validate_armature
|
||||||
|
|
||||||
class AvatarToolkit_OT_SearchMergeArmatureInto(Operator):
|
class AvatarToolkit_OT_SearchMergeArmatureInto(Operator):
|
||||||
"""Search operator for selecting target armature to merge into"""
|
"""Search operator for selecting target armature to merge into"""
|
||||||
@@ -155,7 +157,6 @@ class AvatarToolKit_PT_CustomPanel(Panel):
|
|||||||
|
|
||||||
# Group related options together
|
# Group related options together
|
||||||
transform_col: UILayout = col.column(align=True)
|
transform_col: UILayout = col.column(align=True)
|
||||||
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)
|
||||||
|
|||||||
@@ -43,6 +43,16 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
|
|||||||
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':
|
||||||
|
# SDK2 Warning Box
|
||||||
|
warning_box: UILayout = layout.box()
|
||||||
|
col: UILayout = warning_box.column(align=True)
|
||||||
|
col.label(text=t("EyeTracking.sdk2_warning"), icon='INFO')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
col.label(text=t("EyeTracking.sdk2_warning_detail1"))
|
||||||
|
col.label(text=t("EyeTracking.sdk2_warning_detail2"))
|
||||||
|
col.label(text=t("EyeTracking.sdk2_warning_detail3"))
|
||||||
|
col.label(text=t("EyeTracking.sdk2_warning_detail4"))
|
||||||
|
|
||||||
# Mode Selection Box
|
# Mode Selection Box
|
||||||
mode_box: UILayout = layout.box()
|
mode_box: UILayout = layout.box()
|
||||||
col: UILayout = mode_box.column(align=True)
|
col: UILayout = mode_box.column(align=True)
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
# MMD Tools disabled for the time being unto it can be fixed.
|
|
||||||
|
|
||||||
# import bpy
|
|
||||||
# from typing import Set
|
|
||||||
# from bpy.types import Panel, Context, UILayout
|
|
||||||
# from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
|
||||||
# from ..core.translations import t
|
|
||||||
|
|
||||||
# class AvatarToolKit_PT_MMDPanel(Panel):
|
|
||||||
# """Panel containing MMD bone standardization and cleanup tools"""
|
|
||||||
# bl_label = t("MMD.label")
|
|
||||||
# bl_idname = "OBJECT_PT_avatar_toolkit_mmd"
|
|
||||||
# bl_space_type = 'VIEW_3D'
|
|
||||||
# bl_region_type = 'UI'
|
|
||||||
# bl_category = CATEGORY_NAME
|
|
||||||
# bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
|
||||||
# bl_order = 3
|
|
||||||
# bl_options = {'DEFAULT_CLOSED'}
|
|
||||||
|
|
||||||
# def draw(self, context: Context) -> None:
|
|
||||||
# layout: UILayout = self.layout
|
|
||||||
# toolkit = context.scene.avatar_toolkit
|
|
||||||
|
|
||||||
# Bone Settings Box
|
|
||||||
# bone_box: UILayout = layout.box()
|
|
||||||
# col: UILayout = bone_box.column(align=True)
|
|
||||||
# col.label(text=t("MMD.bone_settings"), icon='BONE_DATA')
|
|
||||||
# col.separator(factor=0.5)
|
|
||||||
# col.prop(toolkit, "keep_twist_bones")
|
|
||||||
# col.prop(toolkit, "keep_upper_chest")
|
|
||||||
# col.operator("avatar_toolkit.standardize_mmd", icon='BONE_DATA')
|
|
||||||
|
|
||||||
# Mesh Tools Box
|
|
||||||
# mesh_box: UILayout = layout.box()
|
|
||||||
# col = mesh_box.column(align=True)
|
|
||||||
# col.label(text=t("MMD.mesh_tools"), icon='MESH_DATA')
|
|
||||||
# col.separator(factor=0.5)
|
|
||||||
# row: UILayout = col.row(align=True)
|
|
||||||
# row.operator("avatar_toolkit.fix_meshes", icon='MODIFIER')
|
|
||||||
# row.operator("avatar_toolkit.validate_meshes", icon='CHECKMARK')
|
|
||||||
|
|
||||||
# Cleanup Box
|
|
||||||
# cleanup_box: UILayout = layout.box()
|
|
||||||
# col = cleanup_box.column(align=True)
|
|
||||||
# col.label(text=t("MMD.cleanup"), icon='BRUSH_DATA')
|
|
||||||
# col.separator(factor=0.5)
|
|
||||||
# col.operator("avatar_toolkit.cleanup_mmd", icon='SHADERFX')
|
|
||||||
# col.operator("avatar_toolkit.convert_mmd_morphs", icon='SHAPEKEY_DATA')
|
|
||||||
# col.operator("avatar_toolkit.reparent_meshes", icon='OUTLINER_OB_ARMATURE')
|
|
||||||
+122
-15
@@ -14,7 +14,6 @@ from ..core.translations import t
|
|||||||
from ..core.common import (
|
from ..core.common import (
|
||||||
get_active_armature,
|
get_active_armature,
|
||||||
clear_default_objects,
|
clear_default_objects,
|
||||||
validate_armature,
|
|
||||||
get_armature_list,
|
get_armature_list,
|
||||||
get_armature_stats
|
get_armature_stats
|
||||||
)
|
)
|
||||||
@@ -24,6 +23,7 @@ from ..functions.pose_mode import (
|
|||||||
AvatarToolkit_OT_ApplyPoseAsShapekey,
|
AvatarToolkit_OT_ApplyPoseAsShapekey,
|
||||||
AvatarToolkit_OT_ApplyPoseAsRest
|
AvatarToolkit_OT_ApplyPoseAsRest
|
||||||
)
|
)
|
||||||
|
from ..core.armature_validation import validate_armature
|
||||||
|
|
||||||
class AvatarToolKit_OT_ExportFBX(Operator):
|
class AvatarToolKit_OT_ExportFBX(Operator):
|
||||||
"""Export selected objects as FBX"""
|
"""Export selected objects as FBX"""
|
||||||
@@ -70,6 +70,7 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
|
|||||||
def draw(self, context: Context) -> None:
|
def draw(self, context: Context) -> None:
|
||||||
"""Draw the panel layout"""
|
"""Draw the panel layout"""
|
||||||
layout: UILayout = self.layout
|
layout: UILayout = self.layout
|
||||||
|
props = context.scene.avatar_toolkit
|
||||||
|
|
||||||
# Armature Selection Box
|
# Armature Selection Box
|
||||||
armature_box: UILayout = layout.box()
|
armature_box: UILayout = layout.box()
|
||||||
@@ -83,28 +84,134 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
|
|||||||
# Armature Validation
|
# Armature Validation
|
||||||
active_armature: Optional[Object] = get_active_armature(context)
|
active_armature: Optional[Object] = get_active_armature(context)
|
||||||
if active_armature:
|
if active_armature:
|
||||||
is_valid: bool
|
is_valid, messages, is_acceptable, hierarchy_messages, scale_messages, non_standard_messages = validate_armature(active_armature, detailed_messages=True)
|
||||||
messages: List[str]
|
|
||||||
is_valid, messages = validate_armature(active_armature)
|
|
||||||
|
|
||||||
# Create info box for all validation information
|
info_box = col.box()
|
||||||
info_box: UILayout = col.box()
|
|
||||||
|
|
||||||
if is_valid:
|
if not is_valid:
|
||||||
row: UILayout = info_box.row()
|
# Display non-standard bones and hierarchy issues
|
||||||
split: UILayout = row.split(factor=0.6)
|
if len(messages) > 1:
|
||||||
|
# Found Bones section
|
||||||
|
validation_box = info_box.box()
|
||||||
|
row = validation_box.row()
|
||||||
|
row.prop(props, "show_found_bones", text=t("Validation.section.found_bones"), icon='TRIA_DOWN' if props.show_found_bones else 'TRIA_RIGHT', emboss=False)
|
||||||
|
if props.show_found_bones:
|
||||||
|
for line in messages[0].split('\n'):
|
||||||
|
validation_box.label(text=line)
|
||||||
|
|
||||||
|
# Main validation status
|
||||||
|
validation_box = info_box.box()
|
||||||
|
row = validation_box.row()
|
||||||
|
row.alert = True
|
||||||
|
row.label(text=t("Validation.status.failed"))
|
||||||
|
|
||||||
|
# Detailed validation message
|
||||||
|
validation_box = info_box.box()
|
||||||
|
row = validation_box.row()
|
||||||
|
row.alert = True
|
||||||
|
row.label(text=t("Validation.message.failed.line1"))
|
||||||
|
row = validation_box.row()
|
||||||
|
row.alert = True
|
||||||
|
row.label(text=t("Validation.message.failed.line2"))
|
||||||
|
row = validation_box.row()
|
||||||
|
row.alert = True
|
||||||
|
row.label(text=t("Validation.message.failed.line3"))
|
||||||
|
|
||||||
|
# Non-Standard Bones section
|
||||||
|
validation_box = info_box.box()
|
||||||
|
row = validation_box.row()
|
||||||
|
row.alert = True
|
||||||
|
row.prop(props, "show_non_standard", text=t("Validation.section.non_standard"),
|
||||||
|
icon='TRIA_DOWN' if props.show_non_standard else 'TRIA_RIGHT', emboss=False)
|
||||||
|
if props.show_non_standard:
|
||||||
|
if non_standard_messages:
|
||||||
|
for message in non_standard_messages:
|
||||||
|
for line in message.split('\n'):
|
||||||
|
sub_row = validation_box.row()
|
||||||
|
sub_row.alert = True
|
||||||
|
sub_row.label(text=line)
|
||||||
|
else:
|
||||||
|
sub_row = validation_box.row()
|
||||||
|
sub_row.label(text=t("Validation.no_non_standard_issues"))
|
||||||
|
|
||||||
|
# Hierarchy Issues section
|
||||||
|
validation_box = info_box.box()
|
||||||
|
row = validation_box.row()
|
||||||
|
row.alert = True
|
||||||
|
row.prop(props, "show_hierarchy", text=t("Validation.section.hierarchy"),
|
||||||
|
icon='TRIA_DOWN' if props.show_hierarchy else 'TRIA_RIGHT', emboss=False)
|
||||||
|
if props.show_hierarchy:
|
||||||
|
if hierarchy_messages:
|
||||||
|
for message in hierarchy_messages:
|
||||||
|
sub_row = validation_box.row()
|
||||||
|
sub_row.alert = True
|
||||||
|
sub_row.label(text=message)
|
||||||
|
else:
|
||||||
|
sub_row = validation_box.row()
|
||||||
|
sub_row.label(text=t("Validation.no_hierarchy_issues"))
|
||||||
|
|
||||||
|
# Scale Issues section
|
||||||
|
validation_box = info_box.box()
|
||||||
|
row = validation_box.row()
|
||||||
|
row.alert = True
|
||||||
|
row.prop(props, "show_scale_issues", text=t("Validation.section.scale_issues"),
|
||||||
|
icon='TRIA_DOWN' if props.show_scale_issues else 'TRIA_RIGHT', emboss=False)
|
||||||
|
if props.show_scale_issues:
|
||||||
|
if scale_messages:
|
||||||
|
for scale_msg in scale_messages:
|
||||||
|
sub_row = validation_box.row()
|
||||||
|
sub_row.alert = True
|
||||||
|
sub_row.label(text=scale_msg)
|
||||||
|
else:
|
||||||
|
sub_row = validation_box.row()
|
||||||
|
sub_row.label(text=t("Validation.no_scale_issues"))
|
||||||
|
|
||||||
|
pose_box = layout.box()
|
||||||
|
col = pose_box.column(align=True)
|
||||||
|
col.label(text=t("Validation.tpose.label"), icon='ARMATURE_DATA')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
col.operator("avatar_toolkit.validate_tpose", icon='CHECKMARK')
|
||||||
|
|
||||||
|
if props.show_tpose_validation:
|
||||||
|
validation_box = col.box()
|
||||||
|
if props.tpose_validation_result:
|
||||||
|
validation_box.label(text=t("Validation.tpose.valid"), icon='CHECKMARK')
|
||||||
|
else:
|
||||||
|
row = validation_box.row()
|
||||||
|
row.alert = True
|
||||||
|
row.label(text=t("Validation.tpose.warning"), icon='ERROR')
|
||||||
|
|
||||||
|
for msg in props.tpose_validation_messages:
|
||||||
|
row = validation_box.row()
|
||||||
|
row.alert = True
|
||||||
|
row.label(text=msg.name)
|
||||||
|
else:
|
||||||
|
# If no specific issues, show acceptable message
|
||||||
|
info_box.label(text=messages[0], icon='INFO')
|
||||||
|
info_box.label(text=messages[1])
|
||||||
|
info_box.label(text=messages[2])
|
||||||
|
elif is_valid and not is_acceptable:
|
||||||
|
row = info_box.row()
|
||||||
|
split = row.split(factor=0.6)
|
||||||
split.label(text=t("QuickAccess.valid_armature"), icon='CHECKMARK')
|
split.label(text=t("QuickAccess.valid_armature"), icon='CHECKMARK')
|
||||||
stats: Dict[str, int] = get_armature_stats(active_armature)
|
stats = get_armature_stats(active_armature)
|
||||||
split.label(text=t("QuickAccess.bones_count", count=stats['bone_count']))
|
split.label(text=t("QuickAccess.bones_count", count=stats['bone_count']))
|
||||||
|
|
||||||
if stats['has_pose']:
|
if stats['has_pose']:
|
||||||
info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT')
|
info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT')
|
||||||
else:
|
elif is_valid and is_acceptable:
|
||||||
# Display validation failure messages
|
# Show acceptable standard message
|
||||||
for message in messages:
|
info_box.label(text=messages[0], icon='INFO')
|
||||||
info_box.label(text=message, icon='ERROR')
|
info_box.label(text=messages[1])
|
||||||
|
info_box.label(text=messages[2])
|
||||||
|
|
||||||
# Validation Mode Warnings - always show in info box
|
# Add standardize button
|
||||||
|
standardize_box = info_box.box()
|
||||||
|
standardize_box.operator("avatar_toolkit.standardize_armature",
|
||||||
|
text=t("QuickAccess.standardize_armature"),
|
||||||
|
icon='MODIFIER')
|
||||||
|
|
||||||
|
# Validation Mode Warnings
|
||||||
validation_mode = context.scene.avatar_toolkit.validation_mode
|
validation_mode = context.scene.avatar_toolkit.validation_mode
|
||||||
if validation_mode == 'BASIC':
|
if validation_mode == 'BASIC':
|
||||||
warning_row = info_box.box()
|
warning_row = info_box.box()
|
||||||
|
|||||||
+21
-7
@@ -36,12 +36,13 @@ class AvatarToolKit_PT_SettingsPanel(Panel):
|
|||||||
bl_region_type: str = 'UI'
|
bl_region_type: str = 'UI'
|
||||||
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 = 7
|
bl_order: int = 8
|
||||||
bl_options = {'DEFAULT_CLOSED'}
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
def draw(self, context: Context) -> None:
|
def draw(self, context: Context) -> None:
|
||||||
"""Draw the settings panel layout with language selection"""
|
"""Draw the settings panel layout with language selection"""
|
||||||
layout: UILayout = self.layout
|
layout: UILayout = self.layout
|
||||||
|
props = context.scene.avatar_toolkit
|
||||||
|
|
||||||
# Language Settings
|
# Language Settings
|
||||||
lang_box: UILayout = layout.box()
|
lang_box: UILayout = layout.box()
|
||||||
@@ -50,7 +51,7 @@ class AvatarToolKit_PT_SettingsPanel(Panel):
|
|||||||
row.scale_y = 1.2
|
row.scale_y = 1.2
|
||||||
row.label(text=t("Settings.language"), icon='WORLD')
|
row.label(text=t("Settings.language"), icon='WORLD')
|
||||||
col.separator()
|
col.separator()
|
||||||
col.prop(context.scene.avatar_toolkit, "language", text="")
|
col.prop(props, "language", text="")
|
||||||
|
|
||||||
# Validation Settings
|
# Validation Settings
|
||||||
val_box: UILayout = layout.box()
|
val_box: UILayout = layout.box()
|
||||||
@@ -59,18 +60,31 @@ class AvatarToolKit_PT_SettingsPanel(Panel):
|
|||||||
row.scale_y = 1.2
|
row.scale_y = 1.2
|
||||||
row.label(text=t("Settings.validation_mode"), icon='CHECKMARK')
|
row.label(text=t("Settings.validation_mode"), icon='CHECKMARK')
|
||||||
col.separator()
|
col.separator()
|
||||||
col.prop(context.scene.avatar_toolkit, "validation_mode", text="")
|
col.prop(props, "validation_mode", text="")
|
||||||
|
|
||||||
|
# Bone Highlighting Settings
|
||||||
|
bone_box: UILayout = layout.box()
|
||||||
|
col = bone_box.column(align=True)
|
||||||
|
row = col.row()
|
||||||
|
row.scale_y = 1.2
|
||||||
|
row.label(text=t("Settings.bone_highlighting"), icon='BONE_DATA')
|
||||||
|
col.separator()
|
||||||
|
col.prop(props, "highlight_problem_bones")
|
||||||
|
if props.highlight_problem_bones:
|
||||||
|
col.operator("avatar_toolkit.highlight_problem_bones", icon='COLOR')
|
||||||
|
else:
|
||||||
|
col.operator("avatar_toolkit.clear_bone_highlighting", icon='X')
|
||||||
|
|
||||||
# Debug Settings
|
# Debug Settings
|
||||||
debug_box = layout.box()
|
debug_box = layout.box()
|
||||||
col = debug_box.column()
|
col = debug_box.column()
|
||||||
row = col.row(align=True)
|
row = col.row(align=True)
|
||||||
row.prop(context.scene.avatar_toolkit, "debug_expand",
|
row.prop(props, "debug_expand",
|
||||||
icon="TRIA_DOWN" if context.scene.avatar_toolkit.debug_expand
|
icon="TRIA_DOWN" if props.debug_expand
|
||||||
else "TRIA_RIGHT",
|
else "TRIA_RIGHT",
|
||||||
icon_only=True, emboss=False)
|
icon_only=True, emboss=False)
|
||||||
row.label(text=t("Settings.debug"), icon='CONSOLE')
|
row.label(text=t("Settings.debug"), icon='CONSOLE')
|
||||||
|
|
||||||
if context.scene.avatar_toolkit.debug_expand:
|
if props.debug_expand:
|
||||||
col = debug_box.column(align=True)
|
col = debug_box.column(align=True)
|
||||||
col.prop(context.scene.avatar_toolkit, "enable_logging")
|
col.prop(props, "enable_logging")
|
||||||
|
|||||||
+45
-2
@@ -1,9 +1,21 @@
|
|||||||
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, UIList
|
||||||
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
|
||||||
|
|
||||||
|
class AVATAR_TOOLKIT_UL_ZeroWeightBones(UIList):
|
||||||
|
"""UI List for displaying zero weight bones with selection options"""
|
||||||
|
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||||
|
if self.layout_type in {'DEFAULT', 'COMPACT'}:
|
||||||
|
row = layout.row(align=True)
|
||||||
|
row.prop(item, "selected", text="")
|
||||||
|
row.label(text=item.name)
|
||||||
|
if item.has_children:
|
||||||
|
row.label(text="", icon='OUTLINER_OB_ARMATURE')
|
||||||
|
if item.is_deform:
|
||||||
|
row.label(text="", icon='MOD_ARMATURE')
|
||||||
|
|
||||||
class AvatarToolKit_PT_ToolsPanel(Panel):
|
class AvatarToolKit_PT_ToolsPanel(Panel):
|
||||||
"""Panel containing various tools for avatar customization and optimization"""
|
"""Panel containing various tools for avatar customization and optimization"""
|
||||||
bl_label: str = t("Tools.label")
|
bl_label: str = t("Tools.label")
|
||||||
@@ -18,6 +30,7 @@ class AvatarToolKit_PT_ToolsPanel(Panel):
|
|||||||
def draw(self, context: Context) -> None:
|
def draw(self, context: Context) -> None:
|
||||||
"""Draw the tools panel interface"""
|
"""Draw the tools panel interface"""
|
||||||
layout: UILayout = self.layout
|
layout: UILayout = self.layout
|
||||||
|
toolkit = context.scene.avatar_toolkit
|
||||||
|
|
||||||
# General Tools
|
# General Tools
|
||||||
tools_box: UILayout = layout.box()
|
tools_box: UILayout = layout.box()
|
||||||
@@ -42,10 +55,32 @@ class AvatarToolKit_PT_ToolsPanel(Panel):
|
|||||||
col.separator(factor=0.5)
|
col.separator(factor=0.5)
|
||||||
col.operator("avatar_toolkit.create_digitigrade", text=t("Tools.create_digitigrade"), icon='BONE_DATA')
|
col.operator("avatar_toolkit.create_digitigrade", text=t("Tools.create_digitigrade"), icon='BONE_DATA')
|
||||||
|
|
||||||
|
# Standardization Tools
|
||||||
|
standardize_box: UILayout = bone_box.box()
|
||||||
|
col = standardize_box.column(align=True)
|
||||||
|
col.label(text=t("Tools.standardize_title"), icon='OUTLINER_OB_ARMATURE')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
col.operator("avatar_toolkit.standardize_armature", icon='CHECKMARK')
|
||||||
|
|
||||||
# Weight Tools
|
# Weight Tools
|
||||||
weight_box: UILayout = bone_box.box()
|
weight_box: UILayout = bone_box.box()
|
||||||
col = weight_box.column(align=True)
|
col = weight_box.column(align=True)
|
||||||
col.prop(context.scene.avatar_toolkit, "merge_twist_bones", text=t("Tools.merge_twist_bones"))
|
col.prop(toolkit, "merge_twist_bones", text=t("Tools.merge_twist_bones"))
|
||||||
|
col.prop(toolkit, "preserve_parent_bones")
|
||||||
|
col.prop(toolkit, "target_bone_type")
|
||||||
|
col.prop(toolkit, "list_only_mode")
|
||||||
|
|
||||||
|
if toolkit.list_only_mode and len(toolkit.zero_weight_bones) > 0:
|
||||||
|
box = weight_box.box()
|
||||||
|
row = box.row()
|
||||||
|
row.template_list("AVATAR_TOOLKIT_UL_ZeroWeightBones", "",
|
||||||
|
toolkit, "zero_weight_bones",
|
||||||
|
toolkit, "zero_weight_bones_index")
|
||||||
|
|
||||||
|
col = box.column(align=True)
|
||||||
|
col.operator("avatar_toolkit.remove_selected_bones",
|
||||||
|
text=t("Tools.remove_selected_bones"))
|
||||||
|
|
||||||
row = col.row(align=True)
|
row = col.row(align=True)
|
||||||
row.operator("avatar_toolkit.clean_weights", text=t("Tools.clean_weights"), icon='GROUP_BONE')
|
row.operator("avatar_toolkit.clean_weights", text=t("Tools.clean_weights"), icon='GROUP_BONE')
|
||||||
row.operator("avatar_toolkit.clean_constraints", text=t("Tools.clean_constraints"), icon='CONSTRAINT_BONE')
|
row.operator("avatar_toolkit.clean_constraints", text=t("Tools.clean_constraints"), icon='CONSTRAINT_BONE')
|
||||||
@@ -67,3 +102,11 @@ class AvatarToolKit_PT_ToolsPanel(Panel):
|
|||||||
col.separator(factor=0.5)
|
col.separator(factor=0.5)
|
||||||
col.operator("avatar_toolkit.apply_transforms", text=t("Tools.apply_transforms"), icon='OBJECT_DATA')
|
col.operator("avatar_toolkit.apply_transforms", text=t("Tools.apply_transforms"), icon='OBJECT_DATA')
|
||||||
col.operator("avatar_toolkit.clean_shapekeys", text=t("Tools.clean_shapekeys"), icon='SHAPEKEY_DATA')
|
col.operator("avatar_toolkit.clean_shapekeys", text=t("Tools.clean_shapekeys"), icon='SHAPEKEY_DATA')
|
||||||
|
|
||||||
|
# Rigify Tools
|
||||||
|
rigify_box: UILayout = layout.box()
|
||||||
|
col = rigify_box.column(align=True)
|
||||||
|
col.label(text=t("Tools.rigify_title"), icon='ARMATURE_DATA')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
col.operator("avatar_toolkit.convert_rigify_to_unity", icon='ARMATURE_DATA')
|
||||||
|
col.prop(context.scene.avatar_toolkit, "merge_twist_bones")
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import bpy
|
||||||
|
from bpy.types import Panel, Context, UILayout
|
||||||
|
from ..core.translations import t
|
||||||
|
|
||||||
|
class AvatarToolKit_PT_UVPanel(Panel):
|
||||||
|
"""Main UV Tools panel for Avatar Toolkit"""
|
||||||
|
bl_label = t("AvatarToolkit.label")
|
||||||
|
bl_idname = "OBJECT_PT_avatar_toolkit_uv_main"
|
||||||
|
bl_space_type = 'IMAGE_EDITOR'
|
||||||
|
bl_region_type = 'UI'
|
||||||
|
bl_category = "Avatar Toolkit"
|
||||||
|
|
||||||
|
def draw(self, context: Context) -> None:
|
||||||
|
layout: UILayout = self.layout
|
||||||
|
|
||||||
|
# Add title section
|
||||||
|
box: UILayout = layout.box()
|
||||||
|
col: UILayout = box.column(align=True)
|
||||||
|
row: UILayout = col.row()
|
||||||
|
row.scale_y = 1.2
|
||||||
|
row.label(text=t("AvatarToolkit.label"), icon='UV')
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import bpy
|
||||||
|
from bpy.types import Panel, Context, UILayout
|
||||||
|
from ..core.translations import t
|
||||||
|
|
||||||
|
class AvatarToolKit_PT_UVTools(Panel):
|
||||||
|
"""UV Tools panel containing UV manipulation operators"""
|
||||||
|
bl_label = t("Tools.label")
|
||||||
|
bl_idname = "OBJECT_PT_avatar_toolkit_uv_tools"
|
||||||
|
bl_space_type = 'IMAGE_EDITOR'
|
||||||
|
bl_region_type = 'UI'
|
||||||
|
bl_category = "Avatar Toolkit"
|
||||||
|
bl_parent_id = "OBJECT_PT_avatar_toolkit_uv_main"
|
||||||
|
bl_order = 3
|
||||||
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
|
def draw(self, context: Context) -> None:
|
||||||
|
layout: UILayout = self.layout
|
||||||
|
|
||||||
|
tools_box: UILayout = layout.box()
|
||||||
|
col: UILayout = tools_box.column(align=True)
|
||||||
|
col.label(text=t("Tools.uv_title"), icon='UV')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
|
||||||
|
row: UILayout = col.row(align=True)
|
||||||
|
row.operator("avatar_toolkit.align_uv_edges_to_target",
|
||||||
|
text=t("UVTools.align_edges"),
|
||||||
|
icon='GP_MULTIFRAME_EDITING')
|
||||||
+2
-2
@@ -28,13 +28,13 @@ class AvatarToolKit_PT_VisemesPanel(Panel):
|
|||||||
|
|
||||||
armature = get_active_armature(context)
|
armature = get_active_armature(context)
|
||||||
if armature:
|
if armature:
|
||||||
col.prop_search(props, "viseme_mesh", bpy.data, "objects", text="")
|
col.prop(props, "viseme_mesh", text="")
|
||||||
else:
|
else:
|
||||||
col.label(text=t("Visemes.no_armature"), icon='ERROR')
|
col.label(text=t("Visemes.no_armature"), icon='ERROR')
|
||||||
|
|
||||||
# Get selected mesh
|
# Get selected mesh
|
||||||
mesh_obj = bpy.data.objects.get(props.viseme_mesh)
|
mesh_obj = bpy.data.objects.get(props.viseme_mesh)
|
||||||
if not mesh_obj or not mesh_obj.data.shape_keys:
|
if not mesh_obj or not mesh_obj.data or not mesh_obj.data.shape_keys:
|
||||||
layout.label(text=t("Visemes.no_shapekeys"))
|
layout.label(text=t("Visemes.no_shapekeys"))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user