Merge branch 'Current-Dev' into patch-1
This commit is contained in:
@@ -6,9 +6,14 @@ from ..core.logging_setup import logger
|
||||
from bpy.types import AddonPreferences
|
||||
from typing import Any, Dict
|
||||
|
||||
# Get the directory of the current file
|
||||
PREFERENCES_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
PREFERENCES_FILE = os.path.join(PREFERENCES_DIR, "preferences.json")
|
||||
# Get the user preferences directory instead of addon directory
|
||||
def get_preferences_path():
|
||||
user_path = bpy.utils.resource_path('USER')
|
||||
addon_prefs_dir = os.path.join(user_path, "config", "avatar_toolkit_prefs")
|
||||
os.makedirs(addon_prefs_dir, exist_ok=True)
|
||||
return os.path.join(addon_prefs_dir, "preferences.json")
|
||||
|
||||
PREFERENCES_FILE = get_preferences_path()
|
||||
|
||||
def get_current_version():
|
||||
main_dir = os.path.dirname(os.path.dirname(__file__))
|
||||
@@ -59,4 +64,5 @@ def get_addon_preferences(context):
|
||||
if not os.path.exists(PREFERENCES_FILE):
|
||||
save_preference("language", 0) # Set default language to 0 (auto)
|
||||
save_preference("validation_mode", "STRICT") # Set default validation mode
|
||||
save_preference("enable_logging", False) # Set default logging mode
|
||||
save_preference("enable_logging", False) # Set default logging mode
|
||||
save_preference("highlight_problem_bones", True) # Set default bone highlighting
|
||||
|
||||
@@ -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'}
|
||||
+64
-67
@@ -1,6 +1,5 @@
|
||||
import os
|
||||
import bpy
|
||||
import sys
|
||||
import typing
|
||||
import inspect
|
||||
import pkgutil
|
||||
@@ -24,7 +23,6 @@ def init() -> None:
|
||||
global modules
|
||||
global ordered_classes
|
||||
|
||||
# Configure logging first
|
||||
from .logging_setup import configure_logging
|
||||
configure_logging(False)
|
||||
|
||||
@@ -32,14 +30,24 @@ def init() -> None:
|
||||
configure_logging(get_preference("enable_logging", False))
|
||||
|
||||
print("Auto-load init starting")
|
||||
modules = get_all_submodules(Path(__file__).parent.parent)
|
||||
|
||||
package_name = __package__.rsplit('.', 1)[0]
|
||||
directory = Path(__file__).parent.parent
|
||||
modules = get_all_submodules(directory, package_name)
|
||||
ordered_classes = get_ordered_classes_to_register(modules)
|
||||
print(f"Found modules: {modules}")
|
||||
print(f"Found classes: {ordered_classes}")
|
||||
|
||||
def register() -> None:
|
||||
"""Register all discovered classes and modules"""
|
||||
global modules, ordered_classes
|
||||
|
||||
print("Registering classes")
|
||||
|
||||
if not ordered_classes:
|
||||
print("Warning: No classes to register")
|
||||
ordered_classes = []
|
||||
|
||||
for cls in ordered_classes:
|
||||
print(f"Registering: {cls}")
|
||||
try:
|
||||
@@ -47,6 +55,10 @@ def register() -> None:
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if not modules:
|
||||
print("Warning: No modules to register")
|
||||
modules = []
|
||||
|
||||
for module in modules:
|
||||
if module.__name__ == __name__:
|
||||
continue
|
||||
@@ -67,44 +79,29 @@ def unregister() -> None:
|
||||
if hasattr(module, "unregister"):
|
||||
module.unregister()
|
||||
|
||||
def get_manifest_id() -> str:
|
||||
"""Get the addon ID from the manifest file"""
|
||||
manifest_path = Path(__file__).parent.parent / "blender_manifest.toml"
|
||||
with open(manifest_path, "rb") as f:
|
||||
manifest = tomllib.load(f)
|
||||
return manifest["id"]
|
||||
|
||||
def get_all_submodules(directory: Path) -> List[Any]:
|
||||
def get_all_submodules(directory: Path, package_name: str) -> List[Any]:
|
||||
"""Discover and import all submodules in the given directory"""
|
||||
modules = []
|
||||
addon_id = get_manifest_id()
|
||||
for root, dirs, files in os.walk(directory):
|
||||
if "__pycache__" in root:
|
||||
continue
|
||||
path = Path(root)
|
||||
if path == directory:
|
||||
package_name = f"bl_ext.user_default.{addon_id}"
|
||||
else:
|
||||
relative_path = path.relative_to(directory).as_posix().replace('/', '.')
|
||||
package_name = f"bl_ext.user_default.{addon_id}.{relative_path}"
|
||||
for name in sorted(iter_module_names(path)):
|
||||
modules.append(importlib.import_module(f".{name}", package_name))
|
||||
return modules
|
||||
return list(iter_submodules(directory, package_name))
|
||||
|
||||
def iter_submodules(path: Path, package_name: str) -> Generator[Any, None, None]:
|
||||
def iter_submodules(directory: Path, package_name: str) -> Generator[Any, None, None]:
|
||||
"""Iterate through submodules in a package"""
|
||||
for name in sorted(iter_module_names(path)):
|
||||
yield importlib.import_module("." + name, package_name)
|
||||
for name in sorted(iter_submodule_names(directory)):
|
||||
try:
|
||||
yield importlib.import_module("." + name, package_name)
|
||||
print(f"Successfully imported {name} from {package_name}")
|
||||
except ImportError as e:
|
||||
print(f"Error importing {name} from {package_name}: {e}")
|
||||
|
||||
def iter_module_names(path: Path) -> Generator[str, None, None]:
|
||||
def iter_submodule_names(path: Path, root: str = "") -> Generator[str, None, None]:
|
||||
"""Iterate through module names in a directory"""
|
||||
print(f"Scanning path: {path}")
|
||||
modules_list = list(pkgutil.iter_modules([str(path)]))
|
||||
print(f"Found these modules: {modules_list}")
|
||||
for _, module_name, is_pkg in modules_list:
|
||||
if not is_pkg:
|
||||
print(f"Found module: {module_name}")
|
||||
yield module_name
|
||||
for _, module_name, is_package in pkgutil.iter_modules([str(path)]):
|
||||
if is_package:
|
||||
sub_path = path / module_name
|
||||
sub_root = root + module_name + "."
|
||||
yield from iter_submodule_names(sub_path, sub_root)
|
||||
else:
|
||||
yield root + module_name
|
||||
|
||||
def get_ordered_classes_to_register(modules: List[Any]) -> List[Type]:
|
||||
"""Get a topologically sorted list of classes to register"""
|
||||
@@ -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]]:
|
||||
"""Get dependencies dictionary for class registration"""
|
||||
my_classes = set(iter_classes_to_register(modules))
|
||||
my_classes_by_idname = {cls.bl_idname: cls for cls in my_classes if hasattr(cls, "bl_idname")}
|
||||
|
||||
deps_dict = {}
|
||||
classes_to_register = set(iter_classes_to_register(modules))
|
||||
for cls in classes_to_register:
|
||||
deps_dict[cls] = set(iter_own_register_deps(cls, classes_to_register))
|
||||
for cls in my_classes:
|
||||
deps_dict[cls] = set()
|
||||
deps_dict[cls].update(iter_deps_from_annotations(cls, my_classes))
|
||||
deps_dict[cls].update(iter_deps_from_parent_id(cls, my_classes_by_idname))
|
||||
|
||||
return deps_dict
|
||||
|
||||
def iter_own_register_deps(cls: Type, classes_to_register: Set[Type]) -> Generator[Type, None, None]:
|
||||
"""Iterate through a class's own registration dependencies"""
|
||||
yield from (dep for dep in iter_register_deps(cls) if dep in classes_to_register)
|
||||
|
||||
def iter_register_deps(cls: Type) -> Generator[Type, None, None]:
|
||||
"""Iterate through all registration dependencies of a class"""
|
||||
def iter_deps_from_annotations(cls: Type, my_classes: Set[Type]) -> Generator[Type, None, None]:
|
||||
"""Iterate through dependencies from class annotations"""
|
||||
for value in typing.get_type_hints(cls, {}, {}).values():
|
||||
dependency = get_dependency_from_annotation(value)
|
||||
if dependency is not None:
|
||||
if dependency is not None and dependency in my_classes:
|
||||
yield dependency
|
||||
|
||||
def iter_deps_from_parent_id(cls: Type, my_classes_by_idname: Dict[str, Type]) -> Generator[Type, None, None]:
|
||||
"""Iterate through dependencies from panel parent IDs"""
|
||||
if bpy.types.Panel in cls.__bases__:
|
||||
parent_idname = getattr(cls, "bl_parent_id", None)
|
||||
if parent_idname is not None:
|
||||
parent_cls = my_classes_by_idname.get(parent_idname)
|
||||
if parent_cls is not None:
|
||||
yield parent_cls
|
||||
|
||||
def get_dependency_from_annotation(value: Any) -> Optional[Type]:
|
||||
"""Get dependency type from a type annotation"""
|
||||
if isinstance(value, tuple) and len(value) == 2:
|
||||
if value[0] in (bpy.props.PointerProperty, bpy.props.CollectionProperty):
|
||||
return value[1]["type"]
|
||||
if isinstance(value, bpy.props._PropertyDeferred):
|
||||
return value.keywords.get("type")
|
||||
return None
|
||||
|
||||
def iter_classes_to_register(modules: List[Any]) -> Generator[Type, None, None]:
|
||||
@@ -164,7 +170,8 @@ def get_register_base_types() -> Set[Type]:
|
||||
"Panel", "Operator", "PropertyGroup",
|
||||
"AddonPreferences", "Header", "Menu",
|
||||
"Node", "NodeSocket", "NodeTree",
|
||||
"UIList", "RenderEngine"
|
||||
"UIList", "RenderEngine",
|
||||
"Gizmo", "GizmoGroup",
|
||||
])
|
||||
|
||||
def toposort(deps_dict: Dict[Type, Set[Type]]) -> List[Type]:
|
||||
@@ -172,25 +179,15 @@ def toposort(deps_dict: Dict[Type, Set[Type]]) -> List[Type]:
|
||||
sorted_list = []
|
||||
sorted_values = set()
|
||||
|
||||
panels_to_sort = [(value, deps) for value, deps in deps_dict.items()
|
||||
if hasattr(value, 'bl_parent_id')]
|
||||
|
||||
base_panels = [(value, deps) for value, deps in deps_dict.items()
|
||||
if not hasattr(value, 'bl_parent_id')]
|
||||
|
||||
for value, deps in base_panels:
|
||||
if len(deps) == 0:
|
||||
sorted_list.append(value)
|
||||
sorted_values.add(value)
|
||||
|
||||
while len(deps_dict) > len(sorted_values):
|
||||
while len(deps_dict) > 0:
|
||||
unsorted = []
|
||||
for value, deps in deps_dict.items():
|
||||
if value not in sorted_values:
|
||||
if len(deps - sorted_values) == 0:
|
||||
sorted_list.append(value)
|
||||
sorted_values.add(value)
|
||||
else:
|
||||
unsorted.append(value)
|
||||
if len(deps) == 0:
|
||||
sorted_list.append(value)
|
||||
sorted_values.add(value)
|
||||
else:
|
||||
unsorted.append(value)
|
||||
|
||||
deps_dict = {value: deps_dict[value] - sorted_values for value in unsorted}
|
||||
|
||||
return sorted_list
|
||||
|
||||
+89
-115
@@ -10,7 +10,7 @@ import numpy.typing as npt
|
||||
|
||||
from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable, Union, Type
|
||||
from mathutils import Vector, Matrix
|
||||
from bpy.types import (Context, Object, Modifier, EditBone, Operator,
|
||||
from bpy.types import (Context, Object, Modifier, EditBone, Operator, Material,
|
||||
VertexGroup, ShapeKey, Bone, Mesh, Armature, PropertyGroup)
|
||||
from functools import lru_cache
|
||||
from bpy.props import PointerProperty, IntProperty, StringProperty
|
||||
@@ -20,6 +20,47 @@ from ..core.translations import t
|
||||
from ..core.dictionaries import bone_names
|
||||
from .dictionaries import reverse_bone_lookup, bone_names
|
||||
|
||||
class SceneMatClass(PropertyGroup):
|
||||
mat: PointerProperty(type=Material)
|
||||
|
||||
register_class(SceneMatClass)
|
||||
|
||||
class MaterialListBool:
|
||||
#For the love that is holy do not ever touch these. If this was java I would make these private
|
||||
#They should only be accessed via context.scene.texture_atlas_Has_Mat_List_Shown
|
||||
#This is so we know if the materials are up to date. messing with these variables directly will make the thing blow up.
|
||||
#The only exception to this is the ExpandSection_Materials operator which populates this with new data once the materials have changed and need reloading.
|
||||
old_list: dict[str,list[Material]] = {}
|
||||
bool_material_list_expand: dict[str,bool] = {}
|
||||
|
||||
def set_bool(self, value: bool) -> None:
|
||||
MaterialListBool.bool_material_list_expand[bpy.context.scene.name] = value
|
||||
if value == False:
|
||||
MaterialListBool.old_list[bpy.context.scene.name] = []
|
||||
|
||||
def get_bool(self) -> bool:
|
||||
newlist: list[Material] = []
|
||||
for obj in bpy.context.scene.objects:
|
||||
if len(obj.material_slots)>0:
|
||||
for mat_slot in obj.material_slots:
|
||||
if mat_slot.material:
|
||||
if mat_slot.material not in newlist:
|
||||
newlist.append(mat_slot.material)
|
||||
still_the_same: bool = True
|
||||
if bpy.context.scene.name in MaterialListBool.old_list:
|
||||
for item in newlist:
|
||||
if item not in MaterialListBool.old_list[bpy.context.scene.name]:
|
||||
still_the_same = False
|
||||
break
|
||||
for item in MaterialListBool.old_list[bpy.context.scene.name]:
|
||||
if item not in newlist:
|
||||
still_the_same = False
|
||||
break
|
||||
else:
|
||||
still_the_same = False
|
||||
MaterialListBool.bool_material_list_expand[bpy.context.scene.name] = still_the_same
|
||||
return MaterialListBool.bool_material_list_expand[bpy.context.scene.name]
|
||||
|
||||
class ProgressTracker:
|
||||
"""Universal progress tracking for Avatar Toolkit operations"""
|
||||
|
||||
@@ -67,89 +108,6 @@ def get_armature_list(self: Optional[Any] = None, context: Optional[Context] = N
|
||||
if not armatures:
|
||||
return [('NONE', t("Armature.validation.no_armature"), '')]
|
||||
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:
|
||||
"""Automatically select armature if only one exists in scene"""
|
||||
@@ -321,51 +279,67 @@ def validate_meshes(meshes: List[Object]) -> Tuple[bool, str]:
|
||||
return False, t("Optimization.non_mesh_objects")
|
||||
return True, ""
|
||||
|
||||
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"""
|
||||
try:
|
||||
# Store UV maps before joining
|
||||
uv_maps_data = {}
|
||||
for mesh in meshes:
|
||||
uv_maps_data[mesh.name] = {uv.name: uv.data.copy() for uv in mesh.data.uv_layers}
|
||||
def fast_uv_fix(obj: Object) -> None:
|
||||
"""Fast UV coordinate fixing for joined meshes"""
|
||||
if not obj or not obj.data or not obj.data.uv_layers:
|
||||
return
|
||||
|
||||
current_mode = bpy.context.mode
|
||||
|
||||
if current_mode != 'EDIT_MESH':
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
bpy.ops.mesh.select_all(action='SELECT')
|
||||
|
||||
# Process all UV layers at once
|
||||
bpy.ops.uv.select_all(action='SELECT')
|
||||
bpy.ops.uv.pack_islands(margin=0.001)
|
||||
|
||||
if current_mode != 'EDIT_MESH':
|
||||
bpy.ops.object.mode_set(mode=current_mode)
|
||||
|
||||
def join_mesh_objects(context: Context, meshes: List[Object], progress: Optional[ProgressTracker] = None) -> Optional[Object]:
|
||||
"""Combines multiple mesh objects into a single mesh with optimized performance"""
|
||||
try:
|
||||
if not meshes:
|
||||
return None
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
for mesh in meshes:
|
||||
# Create a list of valid meshes
|
||||
valid_meshes = [mesh for mesh in meshes if mesh.name in bpy.data.objects]
|
||||
if not valid_meshes:
|
||||
return None
|
||||
|
||||
for mesh in valid_meshes:
|
||||
mesh.select_set(True)
|
||||
|
||||
if context.selected_objects:
|
||||
context.view_layer.objects.active = context.selected_objects[0]
|
||||
context.view_layer.objects.active = valid_meshes[0]
|
||||
|
||||
if progress:
|
||||
progress.step(t("Optimization.joining_meshes"))
|
||||
|
||||
if progress:
|
||||
progress.step(t("Optimization.joining_meshes"))
|
||||
bpy.ops.object.join()
|
||||
bpy.ops.object.join()
|
||||
joined_mesh = context.active_object
|
||||
|
||||
if progress:
|
||||
progress.step(t("Optimization.applying_transforms"))
|
||||
|
||||
if progress:
|
||||
progress.step(t("Optimization.applying_transforms"))
|
||||
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
|
||||
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
|
||||
|
||||
if progress:
|
||||
progress.step(t("Optimization.fixing_uvs"))
|
||||
|
||||
if progress:
|
||||
progress.step(t("Optimization.fixing_uvs"))
|
||||
fix_uv_coordinates(context)
|
||||
|
||||
# Restore UV maps after joining
|
||||
joined_mesh = context.active_object
|
||||
for uv_name, uv_data in uv_maps_data.items():
|
||||
for map_name, map_data in uv_data.items():
|
||||
if map_name not in joined_mesh.data.uv_layers:
|
||||
joined_mesh.data.uv_layers.new(name=map_name)
|
||||
joined_mesh.data.uv_layers[map_name].data.foreach_set("uv", map_data)
|
||||
|
||||
return context.active_object
|
||||
|
||||
return None
|
||||
fast_uv_fix(joined_mesh)
|
||||
|
||||
return joined_mesh
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to join meshes: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
def fix_uv_coordinates(context: Context) -> None:
|
||||
"""Normalizes and fixes UV coordinates for the active mesh object"""
|
||||
obj: Object = context.object
|
||||
|
||||
+586
-2
@@ -354,8 +354,6 @@ resonite_translations = {
|
||||
'thumb_2_r': "thumb2.R",
|
||||
'thumb_3_r': "thumb3.R"
|
||||
}
|
||||
|
||||
|
||||
# Create reverse lookup dictionary (conversion/translation)
|
||||
reverse_bone_lookup = {}
|
||||
for preferred_name, name_list in bone_names.items():
|
||||
@@ -364,3 +362,589 @@ for preferred_name, name_list in bone_names.items():
|
||||
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -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 typing import Optional, Callable, Dict, List, Union, Set
|
||||
from ..common import clear_default_objects
|
||||
from .import_pmx import import_pmx
|
||||
from .import_pmd import import_pmd
|
||||
from ..translations import t
|
||||
|
||||
# Configure logging
|
||||
@@ -122,13 +120,6 @@ import_types: Dict[str, ImportMethod] = {
|
||||
method=lambda directory, filepath: bpy.ops.tuxedo.import_mmd_animation(directory=directory, filepath=filepath)
|
||||
),
|
||||
"vrm": lambda directory, files, filepath: bpy.ops.import_scene.vrm(filepath=filepath),
|
||||
"pmx": lambda directory, files, filepath: import_pmx(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)),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import logging
|
||||
import traceback
|
||||
from typing import Optional, Any
|
||||
from bpy.types import Context
|
||||
|
||||
logger = logging.getLogger('avatar_toolkit')
|
||||
_original_error = logger.error
|
||||
|
||||
def configure_logging(enabled: bool = False) -> None:
|
||||
"""Configure logging for Avatar Toolkit"""
|
||||
@@ -18,6 +20,15 @@ def configure_logging(enabled: bool = False) -> None:
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
def error_with_traceback(msg, *args, **kwargs):
|
||||
if kwargs.get('exc_info', False) or isinstance(msg, Exception):
|
||||
full_msg = f"{msg}\n{traceback.format_exc()}"
|
||||
_original_error(full_msg, *args, **{**kwargs, 'exc_info': False})
|
||||
else:
|
||||
_original_error(msg, *args, **kwargs)
|
||||
|
||||
logger.error = error_with_traceback
|
||||
|
||||
def update_logging_state(self: Any, context: Context) -> None:
|
||||
"""Update logging state based on user preference"""
|
||||
@@ -25,3 +36,10 @@ def update_logging_state(self: Any, context: Context) -> None:
|
||||
enabled = self.enable_logging
|
||||
save_preference("enable_logging", enabled)
|
||||
configure_logging(enabled)
|
||||
|
||||
def highlight_problem_bones(self: Any, context: Context) -> None:
|
||||
"""Log when problem bones are highlighted"""
|
||||
from .addon_preferences import save_preference
|
||||
enabled = self.highlight_problem_bones
|
||||
save_preference("highlight_problem_bones", enabled)
|
||||
logger.debug(f"Problem bone highlighting {'enabled' if enabled else 'disabled'}")
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# thank you https://stackoverflow.com/a/71432759
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
from typing import Optional
|
||||
from bpy.types import Image, Material
|
||||
|
||||
|
||||
# Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016 Jake Gordon and contributors
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
class Rectangle_Obj:
|
||||
x: int = 0
|
||||
y: int = 0
|
||||
w: int = 0
|
||||
h: int = 0
|
||||
down: Rectangle_Obj = None
|
||||
used: bool = False
|
||||
right: Rectangle_Obj = None
|
||||
|
||||
def __init__(self, x:int, y:int, w:int, h:int, down=None, used =False, right=None):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.w = w
|
||||
self.h = h
|
||||
self.down = down
|
||||
self.used = used
|
||||
self.right = right
|
||||
|
||||
def split(self, w, h) -> Rectangle_Obj:
|
||||
self.used = True
|
||||
self.down = Rectangle_Obj(x=self.x, y=self.y + h, w=self.w, h=self.h - h)
|
||||
self.right = Rectangle_Obj(x=self.x + w, y=self.y, w=self.w - w, h=h)
|
||||
return self
|
||||
|
||||
def find(self, w, h) -> Optional[Rectangle_Obj]:
|
||||
if self.used:
|
||||
return self.right.find(w, h) or self.down.find(w, h)
|
||||
elif (w <= self.w) and (h <= self.h):
|
||||
return self
|
||||
return None
|
||||
|
||||
class MaterialImageList:
|
||||
albedo: Image
|
||||
normal: Image
|
||||
emission: Image
|
||||
ambient_occlusion: Image
|
||||
height: Image
|
||||
roughness: Image
|
||||
fit: Rectangle_Obj
|
||||
material: Material
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
x: int = 0
|
||||
y: int = 0
|
||||
w: int = 0
|
||||
h: int = 0
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class BinPacker(object):
|
||||
root: Rectangle_Obj
|
||||
bin: list[MaterialImageList] = []
|
||||
def __init__(self, structure: list[MaterialImageList]):
|
||||
self.root = None
|
||||
self.bin = structure
|
||||
|
||||
def fit(self):
|
||||
structure = self.bin
|
||||
structure_len = len(self.bin)
|
||||
w: int = 0
|
||||
h: int = 0
|
||||
if structure_len > 0:
|
||||
w = structure[0].w
|
||||
h = structure[0].h
|
||||
self.root = Rectangle_Obj(x=0, y=0, w=w, h=h)
|
||||
for img in structure:
|
||||
w = img.w
|
||||
h = img.h
|
||||
node = self.root.find(w, h)
|
||||
if node:
|
||||
img.fit = node.split(w, h)
|
||||
else:
|
||||
img.fit = self.grow_node(w, h)
|
||||
return structure
|
||||
|
||||
def grow_node(self, w, h) -> Optional[Rectangle_Obj]:
|
||||
can_grow_right = (h <= self.root.h)
|
||||
can_grow_down = (w <= self.root.w)
|
||||
|
||||
should_grow_right = can_grow_right and (self.root.h >= (self.root.w + w))
|
||||
should_grow_down = can_grow_down and (self.root.w >= (self.root.h + h))
|
||||
|
||||
if should_grow_right:
|
||||
return self.grow_right(w, h)
|
||||
elif should_grow_down:
|
||||
return self.grow_down(w, h)
|
||||
elif can_grow_right:
|
||||
return self.grow_right(w, h)
|
||||
elif can_grow_down:
|
||||
return self.grow_down(w, h)
|
||||
return None
|
||||
|
||||
def grow_right(self, w, h) -> Optional[Rectangle_Obj]:
|
||||
self.root = Rectangle_Obj(
|
||||
used=True,
|
||||
x=0,
|
||||
y=0,
|
||||
w=self.root.w + w,
|
||||
h=self.root.h,
|
||||
down=self.root,
|
||||
right=Rectangle_Obj(x=self.root.w, y=0, w=w, h=self.root.h))
|
||||
node = self.root.find(w, h)
|
||||
if node:
|
||||
return node.split(w, h)
|
||||
return None
|
||||
|
||||
def grow_down(self, w, h) -> Optional[Rectangle_Obj]:
|
||||
self.root = Rectangle_Obj(
|
||||
used=True,
|
||||
x=0,
|
||||
y=0,
|
||||
w=self.root.w,
|
||||
h=self.root.h + h,
|
||||
down=Rectangle_Obj(x=0, y=self.root.h, w=self.root.w, h=h),
|
||||
right=self.root
|
||||
)
|
||||
node = self.root.find(w, h)
|
||||
if node:
|
||||
return node.split(w, h)
|
||||
return None
|
||||
+271
-53
@@ -14,15 +14,28 @@ from .logging_setup import logger
|
||||
from .translations import t, get_languages_list, update_language
|
||||
from .addon_preferences import get_preference, save_preference
|
||||
from .updater import get_version_list
|
||||
from .common import get_armature_list, get_active_armature, get_all_meshes
|
||||
from .common import get_armature_list, get_active_armature, get_all_meshes, SceneMatClass
|
||||
from ..functions.visemes import VisemePreview
|
||||
from ..functions.eye_tracking import set_rotation
|
||||
|
||||
class ValidationMessageItem(PropertyGroup):
|
||||
"""Property group for validation message items"""
|
||||
name: StringProperty(name="Message")
|
||||
|
||||
class ZeroWeightBoneItem(PropertyGroup):
|
||||
"""Property group for zero weight bone list items"""
|
||||
name: StringProperty(name="Bone Name")
|
||||
selected: BoolProperty(name="Selected", default=True)
|
||||
has_children: BoolProperty(name="Has Children", default=False)
|
||||
is_deform: BoolProperty(name="Is Deform Bone", default=False)
|
||||
|
||||
|
||||
def update_validation_mode(self: PropertyGroup, context: Context) -> None:
|
||||
"""Updates validation mode and saves preference"""
|
||||
logger.info(f"Updating validation mode to: {self.validation_mode}")
|
||||
save_preference("validation_mode", self.validation_mode)
|
||||
|
||||
|
||||
def update_logging_state(self: PropertyGroup, context: Context) -> None:
|
||||
"""Updates logging state and configures logging"""
|
||||
logger.info(f"Updating logging state to: {self.enable_logging}")
|
||||
@@ -30,13 +43,142 @@ def update_logging_state(self: PropertyGroup, context: Context) -> None:
|
||||
from .logging_setup import configure_logging
|
||||
configure_logging(self.enable_logging)
|
||||
|
||||
|
||||
def update_shape_intensity(self: PropertyGroup, context: Context) -> None:
|
||||
"""Updates shape key intensity and refreshes preview"""
|
||||
if self.viseme_preview_mode:
|
||||
VisemePreview.update_preview(context)
|
||||
|
||||
def highlight_problem_bones(self: PropertyGroup, context: Context) -> None:
|
||||
"""Updates problem bone highlighting state and saves preference"""
|
||||
logger.info(f"Updating problem bone highlighting to: {self.highlight_problem_bones}")
|
||||
save_preference("highlight_problem_bones", self.highlight_problem_bones)
|
||||
|
||||
def get_mesh_objects(self, context):
|
||||
meshes = [(obj.name, obj.name, "") for obj in bpy.data.objects if obj.type == 'MESH']
|
||||
if not meshes:
|
||||
return [('NONE', t("Visemes.no_meshes"), '')]
|
||||
return meshes
|
||||
|
||||
class AvatarToolkitSceneProperties(PropertyGroup):
|
||||
"""Property group containing Avatar Toolkit scene-level settings and properties"""
|
||||
|
||||
show_found_bones: BoolProperty(
|
||||
name="Show Found Bones",
|
||||
default=False
|
||||
)
|
||||
|
||||
show_non_standard: BoolProperty(
|
||||
name="Show Non-Standard Bones",
|
||||
default=False
|
||||
)
|
||||
|
||||
show_hierarchy: BoolProperty(
|
||||
name="Show Hierarchy Issues",
|
||||
default=False
|
||||
)
|
||||
|
||||
material_search_filter: StringProperty(
|
||||
name=t("TextureAtlas.search_materials"),
|
||||
description=t("TextureAtlas.search_materials_desc"),
|
||||
default=""
|
||||
)
|
||||
|
||||
def get_texture_node_list(self: Material, context: Context) -> list[tuple]:
|
||||
if self.use_nodes:
|
||||
Object.Enum = [((i.image.name if i.image else i.name+"_image"),
|
||||
(i.image.name if i.image else "node with no image..."),
|
||||
(i.image.name if i.image else i.name), index+1)
|
||||
for index, i in enumerate(self.node_tree.nodes)
|
||||
if i.bl_idname == "ShaderNodeTexImage"]
|
||||
if not len(Object.Enum):
|
||||
Object.Enum = [(t("TextureAtlas.error.label"),
|
||||
t("TextureAtlas.no_images_error.desc"),
|
||||
t("TextureAtlas.error.label"), 0)]
|
||||
else:
|
||||
Object.Enum = [(t("TextureAtlas.error.label"),
|
||||
t("TextureAtlas.no_nodes_error.desc"),
|
||||
t("TextureAtlas.error.label"), 0)]
|
||||
Object.Enum.append((t("TextureAtlas.none.label"),
|
||||
t("TextureAtlas.none.label"),
|
||||
t("TextureAtlas.none.label"), 0))
|
||||
return Object.Enum
|
||||
|
||||
Material.texture_atlas_albedo = EnumProperty(
|
||||
name=t("TextureAtlas.albedo"),
|
||||
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.albedo").lower()),
|
||||
default=0,
|
||||
items=get_texture_node_list
|
||||
)
|
||||
|
||||
Material.texture_atlas_normal = EnumProperty(
|
||||
name=t("TextureAtlas.normal"),
|
||||
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.normal").lower()),
|
||||
default=0,
|
||||
items=get_texture_node_list
|
||||
)
|
||||
|
||||
Material.texture_atlas_emission = EnumProperty(
|
||||
name=t("TextureAtlas.emission"),
|
||||
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.emission").lower()),
|
||||
default=0,
|
||||
items=get_texture_node_list
|
||||
)
|
||||
|
||||
Material.texture_atlas_ambient_occlusion = EnumProperty(
|
||||
name=t("TextureAtlas.ambient_occlusion"),
|
||||
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.ambient_occlusion").lower()),
|
||||
default=0,
|
||||
items=get_texture_node_list
|
||||
)
|
||||
|
||||
Material.texture_atlas_height = EnumProperty(
|
||||
name=t("TextureAtlas.height"),
|
||||
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.height").lower()),
|
||||
default=0,
|
||||
items=get_texture_node_list
|
||||
)
|
||||
|
||||
Material.texture_atlas_roughness = EnumProperty(
|
||||
name=t("TextureAtlas.roughness"),
|
||||
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.roughness").lower()),
|
||||
default=0,
|
||||
items=get_texture_node_list
|
||||
)
|
||||
|
||||
list_only_mode: BoolProperty(
|
||||
name=t("Tools.list_only_mode"),
|
||||
description=t("Tools.list_only_mode_desc"),
|
||||
default=False
|
||||
)
|
||||
|
||||
Material.include_in_atlas = BoolProperty(
|
||||
name=t("TextureAtlas.include_in_atlas"),
|
||||
description=t("TextureAtlas.include_in_atlas_desc"),
|
||||
default=False
|
||||
)
|
||||
|
||||
Material.material_expanded = BoolProperty(
|
||||
name=t("TextureAtlas.material_expanded"),
|
||||
description=t("TextureAtlas.material_expanded_desc"),
|
||||
default=False
|
||||
)
|
||||
|
||||
texture_atlas_Has_Mat_List_Shown: BoolProperty(
|
||||
name=t("TextureAtlas.list_shown"),
|
||||
description=t("TextureAtlas.list_shown_desc"),
|
||||
default=False
|
||||
)
|
||||
|
||||
texture_atlas_material_index: IntProperty(
|
||||
default=-1,
|
||||
get=lambda self: -1,
|
||||
set=lambda self, context: None
|
||||
)
|
||||
|
||||
materials: CollectionProperty(
|
||||
type=SceneMatClass
|
||||
)
|
||||
|
||||
avatar_toolkit_updater_version_list: EnumProperty(
|
||||
items=get_version_list,
|
||||
@@ -151,9 +293,10 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
||||
description=t("Visemes.mouth_ch_desc")
|
||||
)
|
||||
|
||||
viseme_mesh: StringProperty(
|
||||
viseme_mesh: EnumProperty(
|
||||
name=t("Visemes.mesh_select"),
|
||||
description=t("Visemes.mesh_select_desc"),
|
||||
items=get_mesh_objects
|
||||
)
|
||||
|
||||
shape_intensity: FloatProperty(
|
||||
@@ -167,38 +310,37 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
||||
)
|
||||
|
||||
viseme_preview_selection: EnumProperty(
|
||||
name=t("Visemes.preview_selection"),
|
||||
description=t("Visemes.preview_selection_desc"),
|
||||
items=[
|
||||
('vrc.v_aa', 'AA', 'A as in "bat"'),
|
||||
('vrc.v_ch', 'CH', 'Ch as in "choose"'),
|
||||
('vrc.v_dd', 'DD', 'D as in "dog"'),
|
||||
('vrc.v_ih', 'IH', 'I as in "bit"'),
|
||||
('vrc.v_ff', 'FF', 'F as in "fox"'),
|
||||
('vrc.v_e', 'E', 'E as in "bet"'),
|
||||
('vrc.v_kk', 'KK', 'K as in "cat"'),
|
||||
('vrc.v_nn', 'NN', 'N as in "net"'),
|
||||
('vrc.v_oh', 'OH', 'O as in "hot"'),
|
||||
('vrc.v_ou', 'OU', 'O as in "go"'),
|
||||
('vrc.v_pp', 'PP', 'P as in "pat"'),
|
||||
('vrc.v_rr', 'RR', 'R as in "red"'),
|
||||
('vrc.v_sil', 'SIL', 'Silence'),
|
||||
('vrc.v_ss', 'SS', 'S as in "sit"'),
|
||||
('vrc.v_th', 'TH', 'Th as in "think"')
|
||||
],
|
||||
update=lambda s, c: VisemePreview.update_preview(c)
|
||||
|
||||
)
|
||||
name=t("Visemes.preview_selection"),
|
||||
description=t("Visemes.preview_selection_desc"),
|
||||
items=[
|
||||
('vrc.v_aa', 'AA', 'A as in "bat"'),
|
||||
('vrc.v_ch', 'CH', 'Ch as in "choose"'),
|
||||
('vrc.v_dd', 'DD', 'D as in "dog"'),
|
||||
('vrc.v_ih', 'IH', 'I as in "bit"'),
|
||||
('vrc.v_ff', 'FF', 'F as in "fox"'),
|
||||
('vrc.v_e', 'E', 'E as in "bet"'),
|
||||
('vrc.v_kk', 'KK', 'K as in "cat"'),
|
||||
('vrc.v_nn', 'NN', 'N as in "net"'),
|
||||
('vrc.v_oh', 'OH', 'O as in "hot"'),
|
||||
('vrc.v_ou', 'OU', 'O as in "go"'),
|
||||
('vrc.v_pp', 'PP', 'P as in "pat"'),
|
||||
('vrc.v_rr', 'RR', 'R as in "red"'),
|
||||
('vrc.v_sil', 'SIL', 'Silence'),
|
||||
('vrc.v_ss', 'SS', 'S as in "sit"'),
|
||||
('vrc.v_th', 'TH', 'Th as in "think"')
|
||||
],
|
||||
update=lambda s, c: VisemePreview.update_preview(c)
|
||||
)
|
||||
|
||||
eye_tracking_type: EnumProperty(
|
||||
name=t("EyeTracking.type"),
|
||||
description=t("EyeTracking.type_desc"),
|
||||
items=[
|
||||
('AV3', t("EyeTracking.type.av3"), t("EyeTracking.type.av3_desc")),
|
||||
('SDK2', t("EyeTracking.type.sdk2"), t("EyeTracking.type.sdk2_desc"))
|
||||
],
|
||||
default='AV3'
|
||||
)
|
||||
name=t("EyeTracking.type"),
|
||||
description=t("EyeTracking.type_desc"),
|
||||
items=[
|
||||
('AV3', t("EyeTracking.type.av3"), t("EyeTracking.type.av3_desc")),
|
||||
('SDK2', t("EyeTracking.type.sdk2"), t("EyeTracking.type.sdk2_desc"))
|
||||
],
|
||||
default='AV3'
|
||||
)
|
||||
|
||||
eye_mode: EnumProperty(
|
||||
name=t("EyeTracking.mode"),
|
||||
@@ -337,12 +479,6 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
||||
default=""
|
||||
)
|
||||
|
||||
merge_all_bones: BoolProperty(
|
||||
name=t('MergeArmature.merge_all'),
|
||||
description=t('MergeArmature.merge_all_desc'),
|
||||
default=True
|
||||
)
|
||||
|
||||
apply_transforms: BoolProperty(
|
||||
name=t('MergeArmature.apply_transforms'),
|
||||
description=t('MergeArmature.apply_transforms_desc'),
|
||||
@@ -361,33 +497,115 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
||||
default=True
|
||||
)
|
||||
|
||||
preserve_parent_bones: BoolProperty(
|
||||
name=t("Tools.preserve_parent_bones"),
|
||||
description=t("Tools.preserve_parent_bones_desc"),
|
||||
default=True
|
||||
)
|
||||
|
||||
target_bone_type: EnumProperty(
|
||||
name=t("Tools.target_bone_type"),
|
||||
description=t("Tools.target_bone_type_desc"),
|
||||
items=[
|
||||
('ALL', t("Tools.target_all_bones"), ""),
|
||||
('DEFORM', t("Tools.target_deform_bones"), ""),
|
||||
('NON_DEFORM', t("Tools.target_non_deform_bones"), "")
|
||||
],
|
||||
default='ALL'
|
||||
)
|
||||
|
||||
zero_weight_bones: CollectionProperty(
|
||||
type=ZeroWeightBoneItem,
|
||||
name="Zero Weight Bones",
|
||||
description="List of bones with zero weights"
|
||||
)
|
||||
|
||||
zero_weight_bones_index: IntProperty(
|
||||
name="Zero Weight Bone Index",
|
||||
default=0
|
||||
)
|
||||
|
||||
list_only_mode: BoolProperty(
|
||||
name=t("Tools.list_only_mode"),
|
||||
description=t("Tools.list_only_mode_desc"),
|
||||
default=False
|
||||
)
|
||||
|
||||
cleanup_shape_keys: BoolProperty(
|
||||
name=t('MergeArmature.cleanup_shape_keys'),
|
||||
description=t('MergeArmature.cleanup_shape_keys_desc'),
|
||||
default=True
|
||||
)
|
||||
|
||||
merge_twist_bones: BoolProperty(
|
||||
name=t("Tools.merge_twist_bones"),
|
||||
description=t("Tools.merge_twist_bones_desc"),
|
||||
default=True
|
||||
)
|
||||
|
||||
highlight_problem_bones: BoolProperty(
|
||||
name=t("Settings.highlight_problem_bones"),
|
||||
description=t("Settings.highlight_problem_bones_desc"),
|
||||
default=get_preference("highlight_problem_bones", True),
|
||||
update=highlight_problem_bones
|
||||
)
|
||||
|
||||
show_scale_issues: BoolProperty(
|
||||
name="Show Scale Issues",
|
||||
default=False
|
||||
)
|
||||
|
||||
tpose_validation_result: BoolProperty(
|
||||
name="T-Pose Validation Result",
|
||||
default=True
|
||||
)
|
||||
|
||||
tpose_validation_messages: CollectionProperty(
|
||||
type=bpy.types.PropertyGroup,
|
||||
name="T-Pose Validation Messages"
|
||||
)
|
||||
|
||||
show_tpose_validation: BoolProperty(
|
||||
name="Show T-Pose Validation Results",
|
||||
default=False
|
||||
)
|
||||
|
||||
standardize_fix_names: BoolProperty(
|
||||
name=t("Tools.standardize_fix_names"),
|
||||
description=t("Tools.standardize_fix_names_desc"),
|
||||
default=True
|
||||
)
|
||||
|
||||
standardize_fix_hierarchy: BoolProperty(
|
||||
name=t("Tools.standardize_fix_hierarchy"),
|
||||
description=t("Tools.standardize_fix_hierarchy_desc"),
|
||||
default=True
|
||||
)
|
||||
|
||||
standardize_fix_scale: BoolProperty(
|
||||
name=t("Tools.standardize_fix_scale"),
|
||||
description=t("Tools.standardize_fix_scale_desc"),
|
||||
default=True
|
||||
)
|
||||
|
||||
def register() -> None:
|
||||
"""Register the Avatar Toolkit property group"""
|
||||
logger.info("Registering Avatar Toolkit properties")
|
||||
try:
|
||||
bpy.utils.register_class(AvatarToolkitSceneProperties)
|
||||
except ValueError:
|
||||
# Class already registered, we can continue
|
||||
pass
|
||||
|
||||
# Only register the property, not the classes (auto_load will handle that)
|
||||
bpy.types.Scene.avatar_toolkit = PointerProperty(type=AvatarToolkitSceneProperties)
|
||||
logger.debug("Properties registered successfully")
|
||||
|
||||
|
||||
def unregister() -> None:
|
||||
"""Unregister the Avatar Toolkit property group"""
|
||||
logger.info("Unregistering Avatar Toolkit properties")
|
||||
try:
|
||||
del bpy.types.Scene.avatar_toolkit
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
bpy.utils.unregister_class(AvatarToolkitSceneProperties)
|
||||
except RuntimeError:
|
||||
pass
|
||||
logger.debug("Properties unregistered successfully")
|
||||
|
||||
|
||||
# Remove the property
|
||||
if hasattr(bpy.types.Scene, "avatar_toolkit"):
|
||||
try:
|
||||
del bpy.types.Scene.avatar_toolkit
|
||||
logger.debug("Removed avatar_toolkit property")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to remove avatar_toolkit property: {e}")
|
||||
# Not fatal - continue
|
||||
|
||||
@@ -10,6 +10,7 @@ from bpy.types import Context, Operator
|
||||
from ..core.translations import t
|
||||
from ..core.dictionaries import bone_names, resonite_translations
|
||||
from ..core.logging_setup import logger
|
||||
from ..core.armature_validation import validate_armature
|
||||
|
||||
|
||||
from .resonite_loader import resonite_animx, resonite_types
|
||||
@@ -51,7 +52,7 @@ class AvatarToolkit_OT_ConvertResonite(Operator):
|
||||
armature = get_active_armature(context)
|
||||
if not armature:
|
||||
return False
|
||||
is_valid, _ = validate_armature(armature)
|
||||
is_valid, _, _ = validate_armature(armature)
|
||||
return is_valid
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
|
||||
+77
-6
@@ -17,11 +17,17 @@ from typing import Dict, List, Tuple, Optional, Set, Any
|
||||
|
||||
GITHUB_REPO = "teamneoneko/Avatar-Toolkit"
|
||||
|
||||
# Define which version series this installation can update to
|
||||
# For example: ["0.1"] means only look for 0.1.x updates
|
||||
# ["0.2", "0.3"] would look for both 0.2.x and 0.3.x updates
|
||||
ALLOWED_VERSION_SERIES = ["0.2"]
|
||||
|
||||
is_checking_for_update: bool = False
|
||||
update_needed: bool = False
|
||||
latest_version: Optional[str] = None
|
||||
latest_version_str: str = ''
|
||||
version_list: Optional[Dict[str, List[str]]] = None
|
||||
last_manual_check_time: float = 0
|
||||
|
||||
main_dir: str = os.path.dirname(os.path.dirname(__file__))
|
||||
downloads_dir: str = os.path.join(main_dir, "downloads")
|
||||
@@ -34,7 +40,9 @@ class AvatarToolkit_OT_CheckForUpdate(bpy.types.Operator):
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
global last_manual_check_time
|
||||
check_for_update_background()
|
||||
last_manual_check_time = time.time() # Reset the timer on manual check
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
@@ -80,7 +88,16 @@ class AvatarToolkit_PT_UpdaterPanel(bpy.types.Panel):
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
global last_manual_check_time
|
||||
layout = self.layout
|
||||
|
||||
# Auto-check for updates when panel is drawn, but not too frequently
|
||||
current_time = time.time()
|
||||
if current_time - last_manual_check_time > 300: # 5 minutes between auto-checks
|
||||
if not is_checking_for_update and not update_needed:
|
||||
check_for_update_background()
|
||||
last_manual_check_time = current_time
|
||||
|
||||
draw_updater_panel(context, layout)
|
||||
|
||||
|
||||
@@ -158,11 +175,23 @@ def get_github_releases() -> bool:
|
||||
return True
|
||||
|
||||
def check_for_update_available() -> bool:
|
||||
global latest_version, latest_version_str
|
||||
global latest_version, latest_version_str, version_list
|
||||
if not version_list:
|
||||
return False
|
||||
|
||||
latest_version = max(version_list.keys(), key=lambda v: [int(x) for x in v.split('.')])
|
||||
# Filter versions by allowed version series
|
||||
compatible_versions = {}
|
||||
for v, info in version_list.items():
|
||||
for prefix in ALLOWED_VERSION_SERIES:
|
||||
if v.startswith(prefix):
|
||||
compatible_versions[v] = info
|
||||
break
|
||||
|
||||
if not compatible_versions:
|
||||
print(f"No compatible versions found in series: {', '.join(ALLOWED_VERSION_SERIES)}")
|
||||
return False
|
||||
|
||||
latest_version = max(compatible_versions.keys(), key=lambda v: [int(x) for x in v.split('.')])
|
||||
latest_version_str = latest_version
|
||||
|
||||
current_version = get_current_version()
|
||||
@@ -195,11 +224,37 @@ def update_now(latest: bool = False) -> None:
|
||||
if not version_list:
|
||||
print("No version list available. Please check for updates first.")
|
||||
return
|
||||
|
||||
|
||||
if latest:
|
||||
update_link = version_list[latest_version_str][0]
|
||||
# Filter compatible versions
|
||||
compatible_versions = {}
|
||||
for v, info in version_list.items():
|
||||
for prefix in ALLOWED_VERSION_SERIES:
|
||||
if v.startswith(prefix):
|
||||
compatible_versions[v] = info
|
||||
break
|
||||
|
||||
if not compatible_versions:
|
||||
print(f"No compatible versions found in series: {', '.join(ALLOWED_VERSION_SERIES)}")
|
||||
return
|
||||
|
||||
latest_compatible = max(compatible_versions.keys(), key=lambda v: [int(x) for x in v.split('.')])
|
||||
update_link = version_list[latest_compatible][0]
|
||||
else:
|
||||
update_link = version_list[bpy.context.scene.avatar_toolkit_updater_version_list][0]
|
||||
selected_version = bpy.context.scene.avatar_toolkit_updater_version_list
|
||||
|
||||
# Check if selected version is compatible
|
||||
is_compatible = False
|
||||
for prefix in ALLOWED_VERSION_SERIES:
|
||||
if selected_version.startswith(prefix):
|
||||
is_compatible = True
|
||||
break
|
||||
|
||||
if not is_compatible:
|
||||
print(f"Selected version {selected_version} is not in allowed series: {', '.join(ALLOWED_VERSION_SERIES)}")
|
||||
return
|
||||
|
||||
update_link = version_list[selected_version][0]
|
||||
|
||||
download_file(update_link)
|
||||
ui_refresh()
|
||||
@@ -274,7 +329,17 @@ def finish_update(error: str = '') -> None:
|
||||
ui_refresh()
|
||||
|
||||
def get_version_list(self, context: bpy.types.Context) -> List[Tuple[str, str, str]]:
|
||||
return [(v, v, '') for v in version_list.keys()] if version_list else []
|
||||
if not version_list:
|
||||
return []
|
||||
|
||||
compatible_versions = []
|
||||
for v in version_list.keys():
|
||||
for prefix in ALLOWED_VERSION_SERIES:
|
||||
if v.startswith(prefix):
|
||||
compatible_versions.append(v)
|
||||
break
|
||||
|
||||
return [(v, v, '') for v in compatible_versions]
|
||||
|
||||
def draw_updater_panel(context: bpy.types.Context, layout: bpy.types.UILayout) -> None:
|
||||
box = layout.box()
|
||||
@@ -287,6 +352,12 @@ def draw_updater_panel(context: bpy.types.Context, layout: bpy.types.UILayout) -
|
||||
|
||||
col.separator()
|
||||
|
||||
# Show compatibility info
|
||||
col.label(text=f"Update series: {', '.join(s + '.x' for s in ALLOWED_VERSION_SERIES)}", icon='INFO')
|
||||
col.label(text=f"Blender version: {bpy.app.version_string}", icon='BLENDER')
|
||||
|
||||
col.separator()
|
||||
|
||||
# Update check/status section
|
||||
if is_checking_for_update:
|
||||
col.operator(AvatarToolkit_OT_CheckForUpdate.bl_idname,
|
||||
|
||||
Reference in New Issue
Block a user