Compare commits

..

42 Commits

Author SHA1 Message Date
Yusarina 659f3eb91e Update version label in Korean translation 2025-11-22 15:26:19 +00:00
Yusarina ff19a895dc Update AvatarToolkit label version to 0.5.2 2025-11-22 15:26:07 +00:00
Yusarina e6e5a98e58 Update Avatar Toolkit version to Alpha 0.5.2 2025-11-22 15:25:55 +00:00
Yusarina 3fe00da569 Bump version from 0.5.1 to 0.5.2 2025-11-22 15:25:17 +00:00
Yusarina 108f9d3bc8 Merge pull request #214 from Yusarina/Current
Fix to translation service
2025-11-22 14:11:16 +00:00
Yusarina 1847628dc8 Fix to translation service 2025-11-22 13:12:48 +00:00
Yusarina 25a43afdbc Merge pull request #213 from Yusarina/atk-next
Logging Fix
2025-11-20 03:22:12 +00:00
Yusarina baaf4049f6 Logging Fix 2025-11-20 03:21:31 +00:00
Yusarina 7ef86b68fa Update AvatarToolkit label version in Korean translation 2025-11-19 06:48:11 +00:00
Yusarina 27e18b5656 Update AvatarToolkit label version in Japanese translation 2025-11-19 06:47:59 +00:00
Yusarina b61283b9d5 Update Avatar Toolkit version in translations 2025-11-19 06:47:49 +00:00
Yusarina fbcf709ffc Downgrade version from 0.6.0 to 0.5.1 2025-11-19 06:47:25 +00:00
Yusarina 299800e5c2 Update allowed version series to 0.6 2025-11-19 06:41:43 +00:00
Yusarina f6197ccbbf Merge pull request #210 from teamneoneko/Current
Bring Next Up To Speed
2025-11-19 06:41:05 +00:00
Yusarina fd01c39cf9 Merge branch 'atk-next' into Current 2025-11-19 06:40:58 +00:00
Yusarina 117ce4f41d Merge pull request #209 from Yusarina/Current
Fixed Updater
2025-11-19 06:39:06 +00:00
Yusarina f11e9d35fb Fixed Updater 2025-11-19 06:38:44 +00:00
Yusarina 7f1decc644 Merge pull request #208 from Yusarina/Current
Fixed to PMX Import
2025-11-19 06:36:25 +00:00
Yusarina a929f68ad4 Holy shit this was a pain
- Truly fixes PMX Import lol, i messed up completely
- Updated MMD Tools to use Cats One
2025-11-19 06:35:06 +00:00
Yusarina f0bda259d3 Fix PMX import for Blender 5.0 - remove deprecated UV texture APIs
- Replace mesh.uv_textures with mesh.uv_layers
- Remove deprecated UV selection properties
- Add compatibility helpers for UV vertex selection
- Fix morph operators UV handling
2025-11-19 05:06:13 +00:00
Yusarina f4d93a8180 Merge pull request #207 from Yusarina/atk-next
Translation Strings Fixes
2025-11-19 04:36:38 +00:00
Yusarina 303707adf7 Translation String Fix 2025-11-19 04:34:32 +00:00
Yusarina ef84478af7 Translation Strings Fix 2025-11-19 04:32:34 +00:00
Yusarina 56005c5d37 Translations Strings Fixes 2025-11-19 04:30:57 +00:00
Yusarina fe122f9f13 Merge pull request #206 from Yusarina/atk-next
panel order and default open logic
2025-11-16 18:52:13 +00:00
Yusarina 17fb0fcadd panel order and default open logic
Replaced hardcoded panel order and default open/closed options with dynamic values using get_panel_order and should_open_by_default from panel_layout.
2025-11-16 18:50:50 +00:00
Yusarina 1d9c186613 How? 2025-11-16 18:35:55 +00:00
Yusarina 49f5bf7063 Merge pull request #205 from Yusarina/atk-next
improve UI consistency and reduce code duplication
2025-11-16 18:33:31 +00:00
Yusarina daef1298d4 improve UI consistency and reduce code duplication
- Add ui_utils.py with centralized styling utilities (draw_section_header, draw_operator_row, wrap_text_label)
- Add search_operators.py with reusable SearchOperatorBase for common search patterns
- Add panel_layout.py for centralized panel ordering configuration
- Refactor 6 panels to use new utilities (optimization, tools, settings, eye_tracking, main, quick_access)
- Consolidate multi-label warnings into single wrapped text (eye tracking panel)
- Combine single-button rows into compact operator rows
- Standardize button scaling with UIStyle constants
- Add help text to validation settings
- Reduce duplicate code by ~200 lines
- Improve information density by 25-40% through better layout organization
2025-11-16 18:31:54 +00:00
Yusarina 86406efc6b Merge pull request #204 from Yusarina/atk-next
Update lz4 wheels
2025-11-16 01:58:05 +00:00
Yusarina 734d5fe401 Updae lz4 wheels 2025-11-16 01:57:00 +00:00
Yusarina 5029ba8724 Merge pull request #203 from Yusarina/atk-next
overhaul armature validation system to be opt-in by default
2025-11-16 01:49:44 +00:00
Yusarina 3545951fae refactor: overhaul armature validation system to be opt-in by default
- Change default validation mode from STRICT to NONE (disabled)
- Move validation from automatic panel draw to explicit "Validate Now" button
- Hide validation results when mode is changed to NONE
- Fix PMX/MMD model detection to check mmd_type value, not just attribute existence
- Add new validation result collapsible sections
- Improve UI presentation with better visual hierarchy
- Add translation strings for new validation UI elements
2025-11-16 01:47:21 +00:00
Yusarina 0b5bff9222 Update ko_KR.json 2025-11-15 17:21:04 +00:00
Yusarina 862849c032 Update AvatarToolkit label to version 0.5.0 2025-11-15 17:20:56 +00:00
Yusarina e060186716 Update Avatar Toolkit version to Alpha 0.5.0 2025-11-15 17:20:44 +00:00
Onan Chew 07c4dd501f Merge pull request #202 from Yusarina/Current
Migrate to Blender 5.0 API
2025-11-14 22:47:30 -05:00
Yusarina e80c0c034d Version Change
- Min Blender version is 5.0
- ATK version is 0.5.0
2025-11-15 02:52:04 +00:00
Yusarina f40b2faacb Migrate to Blender 5.0 API
- Replaced action.fcurves with channelbag system
- Updated EEVEE_NEXT to EEVEE render engine
- Removed deprecated material.use_nodes and use_shadeless
- Fixed bone selection/hide API for Pose mode
2025-11-15 02:45:37 +00:00
Onan Chew d2b98716ff Merge pull request #201 from Yusarina/Current
Fix texture atlas crash caused by premature image removal
2025-11-12 15:20:01 -05:00
Yusarina e4f3cdbf17 Fix texture atlas crash caused by premature image removal
- Changed image replacement logic to reuse existing placeholder images instead of deleting and recreating them. This should prevents ReferenceError when multiple materials reference the same replacement image
2025-11-12 16:44:07 +00:00
Onan Chew 1d34ac2dd8 Merge pull request #200 from teamneoneko/Alpha-4
Alpha 4
Unbreak things
2025-10-29 12:15:05 -04:00
76 changed files with 5521 additions and 3177 deletions
+6 -6
View File
@@ -3,23 +3,23 @@
schema_version = "1.0.0" schema_version = "1.0.0"
id = "avatar_toolkit" id = "avatar_toolkit"
version = "0.4.0" version = "0.5.2"
name = "Avatar Toolkit" name = "Avatar Toolkit"
tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games." tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games."
maintainer = "Team NekoNeo" maintainer = "Team NekoNeo"
type = "add-on" type = "add-on"
blender_version_min = "4.5.0" blender_version_min = "5.0.0"
license = [ license = [
"SPDX:GPL-3.0-or-later", "SPDX:GPL-3.0-or-later",
] ]
wheels = [ wheels = [
"./wheels/lz4-4.4.3-cp311-cp311-macosx_11_0_arm64.whl", "./wheels/lz4-4.4.5-cp311-cp311-macosx_11_0_arm64.whl",
"./wheels/lz4-4.3.3-cp311-cp311-macosx_10_9_x86_64.whl", "./wheels/lz4-4.4.5-cp311-cp311-macosx_10_9_x86_64.whl",
"./wheels/lz4-4.4.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", "./wheels/lz4-4.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
"./wheels/lz4-4.4.3-cp311-cp311-win_amd64.whl" "./wheels/lz4-4.4.5-cp311-cp311-win_amd64.whl"
] ]
[permissions] [permissions]
+1 -1
View File
@@ -63,6 +63,6 @@ def get_addon_preferences(context):
# Initialize preferences if the file doesn't exist # Initialize preferences if the file doesn't exist
if not os.path.exists(PREFERENCES_FILE): if not os.path.exists(PREFERENCES_FILE):
save_preference("language", 0) # Set default language to 0 (auto) save_preference("language", 0) # Set default language to 0 (auto)
save_preference("validation_mode", "STRICT") # Set default validation mode save_preference("validation_mode", "NONE") # Set default validation mode to NONE (off by default)
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 save_preference("highlight_problem_bones", True) # Set default bone highlighting
+64 -4
View File
@@ -15,6 +15,26 @@ from ..core.dictionaries import (
) )
from ..core.logging_setup import logger from ..core.logging_setup import logger
def is_pmx_model(armature: Object) -> bool:
"""
Check if the armature is a PMX/MMD model.
PMX models have an mmd_type attribute set to 'ROOT' on the root object.
"""
if not armature:
return False
# Check if armature itself has mmd_type set to ROOT
if hasattr(armature, 'mmd_type') and armature.mmd_type == 'ROOT':
return True
# Check if parent has mmd_type set to ROOT (parent container model)
if hasattr(armature, 'parent') and armature.parent:
parent = armature.parent
if hasattr(parent, 'mmd_type') and parent.mmd_type == 'ROOT':
return True
return False
def validate_armature(armature: Object, detailed_messages: bool = False, override_mode: Optional[str] = None) -> Union[Tuple[bool, List[str], bool], Tuple[bool, List[str], bool, List[str], List[str], List[str]]]: def validate_armature(armature: Object, detailed_messages: bool = False, override_mode: Optional[str] = None) -> Union[Tuple[bool, List[str], bool], Tuple[bool, List[str], bool, List[str], List[str], List[str]]]:
""" """
Validates armature and returns validation results Validates armature and returns validation results
@@ -27,9 +47,8 @@ def validate_armature(armature: Object, detailed_messages: bool = False, overrid
scale_messages: List[str] = [] scale_messages: List[str] = []
# Check if this is a PMX model # Check if this is a PMX model
is_pmx_model = False pmx_model = is_pmx_model(armature)
if armature and hasattr(armature, 'mmd_type') or (hasattr(armature, 'parent') and armature.parent and hasattr(armature.parent, 'mmd_type')): if pmx_model:
is_pmx_model = True
logger.debug("Detected PMX model, using specialized validation") logger.debug("Detected PMX model, using specialized validation")
if validation_mode == 'NONE': if validation_mode == 'NONE':
@@ -157,7 +176,7 @@ def validate_armature(armature: Object, detailed_messages: bool = False, overrid
non_standard_messages.append(t("Armature.validation.standardize_note.line3")) non_standard_messages.append(t("Armature.validation.standardize_note.line3"))
# Special handling for PMX models # Special handling for PMX models
if is_pmx_model: if pmx_model:
logger.info("PMX model detected, applying specialized validation") logger.info("PMX model detected, applying specialized validation")
# For PMX models, we'll be more lenient with validation # For PMX models, we'll be more lenient with validation
# and provide specific guidance for these models # and provide specific guidance for these models
@@ -783,3 +802,44 @@ class AvatarToolkit_OT_ClearBoneHighlighting(Operator):
logger.info("Bone highlighting cleared") logger.info("Bone highlighting cleared")
self.report({'INFO'}, t("Validation.highlighting_cleared")) self.report({'INFO'}, t("Validation.highlighting_cleared"))
return {'FINISHED'} return {'FINISHED'}
class AvatarToolkit_OT_ValidateArmatureManual(Operator):
"""Manually validate armature and show results"""
bl_idname = "avatar_toolkit.validate_armature_manual"
bl_label = t("Validation.validate_now", "Validate Armature Now")
bl_description = t("Validation.validate_now_desc", "Run armature validation and display detailed results")
@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 validation")
self.report({'ERROR'}, t("Validation.no_armature"))
return {'CANCELLED'}
logger.info(f"Running manual validation for armature: {armature.name}")
# Clear the validation cache to force a refresh
from ..ui.quick_access_panel import clear_armature_caches
clear_armature_caches()
# Toggle the show_validation_results flag to display results
props = context.scene.avatar_toolkit
props.show_validation_results = True
# Run validation
is_valid, messages, is_acceptable = validate_armature(armature, detailed_messages=False)
if is_valid:
if is_acceptable:
self.report({'INFO'}, t("Armature.validation.acceptable_standard.success"))
else:
self.report({'INFO'}, t("QuickAccess.valid_armature"))
else:
self.report({'WARNING'}, t("Validation.status.failed"))
logger.info("Manual validation complete")
return {'FINISHED'}
+62 -7
View File
@@ -92,20 +92,75 @@ class ProgressTracker:
def get_active_armature(context: Context) -> Optional[Object]: def get_active_armature(context: Context) -> Optional[Object]:
"""Get the currently selected armature from Avatar Toolkit properties""" """Get the currently selected armature from Avatar Toolkit properties"""
armature_name = str(context.scene.avatar_toolkit.active_armature) try:
if armature_name and armature_name != 'NONE': # Get the safe identifier from the enum property
return bpy.data.objects.get(armature_name) armature_id = context.scene.avatar_toolkit.active_armature
if not armature_id or armature_id == 'NONE':
return None
# The identifier format is "ARM_{pointer_value}"
if armature_id.startswith('ARM_'):
try:
pointer_str = armature_id[4:]
pointer_value = int(pointer_str)
# Find the armature with this pointer value
for obj in context.scene.objects:
if obj.type == 'ARMATURE' and obj.as_pointer() == pointer_value:
return obj
logger.warning(f"Armature with pointer {pointer_value} not found")
except (ValueError, AttributeError) as e:
logger.error(f"Failed to parse armature identifier: {e}")
# Fallback for old-style identifiers (direct name)
# This handles backward compatibility
return bpy.data.objects.get(armature_id)
except (UnicodeDecodeError, UnicodeEncodeError, AttributeError) as e:
# Handle encoding issues as a last resort
logger.warning(f"Encoding issue with active_armature property: {e}")
# Final fallback: return active object if it's an armature, or first armature found
if context.view_layer.objects.active and context.view_layer.objects.active.type == 'ARMATURE':
return context.view_layer.objects.active
for obj in context.scene.objects:
if obj.type == 'ARMATURE':
logger.info(f"Falling back to first armature found: {obj.name}")
return obj
return None return None
def set_active_armature(context: Context, armature: Object) -> None: def set_active_armature(context: Context, armature: Object) -> None:
"""Set the active armature for Avatar Toolkit operations""" """Set the active armature for Avatar Toolkit operations using safe identifier"""
context.scene.avatar_toolkit.active_armature = armature if armature and armature.type == 'ARMATURE':
# Use the same safe identifier format as get_armature_list
safe_id = f"ARM_{armature.as_pointer()}"
context.scene.avatar_toolkit.active_armature = safe_id
else:
context.scene.avatar_toolkit.active_armature = 'NONE'
def get_armature_list(self: Optional[Any] = None, context: Optional[Context] = None) -> List[Tuple[str, str, str]]: def get_armature_list(self: Optional[Any] = None, context: Optional[Context] = None) -> List[Tuple[str, str, str]]:
"""Get list of all armature objects in the scene""" """Get list of all armature objects in the scene
Returns tuples of (identifier, display_name, description) where:
- identifier: ASCII-safe unique ID (uses object's memory address)
- display_name: The actual object name (can contain Japanese characters)
- description: Empty string
"""
if context is None: if context is None:
context = bpy.context context = bpy.context
armatures = [(obj.name, obj.name, "") for obj in context.scene.objects if obj.type == 'ARMATURE']
# Use object's as_pointer() value as a safe ASCII identifier
armatures = []
for obj in context.scene.objects:
if obj.type == 'ARMATURE':
# Create a safe ASCII identifier using the object pointer
safe_id = f"ARM_{obj.as_pointer()}"
armatures.append((safe_id, obj.name, ""))
if not armatures: if not armatures:
return [('NONE', t("Armature.validation.no_armature"), '')] return [('NONE', t("Armature.validation.no_armature"), '')]
return armatures return armatures
+20 -24
View File
@@ -8,7 +8,6 @@ from bpy_extras.io_utils import ImportHelper
from typing import Optional, Callable, Dict, List, Union, Set from typing import Optional, Callable, Dict, List, Union, Set
from ..common import clear_default_objects from ..common import clear_default_objects
from ..translations import t from ..translations import t
from ..mmd.core.pmx.importer import PMXImporter
import traceback import traceback
# Configure logging # Configure logging
@@ -203,34 +202,31 @@ class AvatarToolKit_OT_Import(Operator, ImportHelper):
def import_pmx_file(filepath: str) -> None: def import_pmx_file(filepath: str) -> None:
""" """
Import a PMX file using the MMD Tools PMXImporter Import a PMX file using the MMD Tools import operator
Args: Args:
filepath: Path to the PMX file filepath: Path to the PMX file
""" """
# Default import settings # Use the MMD Tools operator to import PMX files (CATS-compatible)
import_settings = { # Must pass files + directory like CATS does, not just filepath
"filepath": filepath,
"scale": 0.08,
"types": {"MESH", "ARMATURE", "MORPHS", "DISPLAY"},
"clean_model": True,
"remove_doubles": False,
"fix_IK_links": True,
"ik_loop_factor": 3,
"use_mipmap": True,
"sph_blend_factor": 1.0,
"spa_blend_factor": 1.0,
"rename_LR_bones": False,
"use_underscore": False,
"apply_bone_fixed_axis": False,
}
# Create and execute the importer
importer = PMXImporter()
try: try:
importer.execute(**import_settings) directory = os.path.dirname(filepath)
filename = os.path.basename(filepath)
bpy.ops.mmd_tools.import_model('EXEC_DEFAULT',
files=[{'name': filename}],
directory=directory,
scale=0.08,
types={'MESH', 'ARMATURE', 'MORPHS', 'DISPLAY'},
clean_model=False, # Disable cleaning to preserve morph indices
remove_doubles=False,
fix_ik_links=False,
ik_loop_factor=5,
apply_bone_fixed_axis=False,
rename_bones=False,
use_underscore=False)
logger.info(f"Successfully imported PMX file: {filepath}") logger.info(f"Successfully imported PMX file: {filepath}")
except Exception: except (AttributeError, TypeError, ValueError) as e:
logger.error(f"Failed to import PMX file: {traceback.format_exc()}", exc_info=True) logger.error(f"Failed to import PMX file: {e}", exc_info=True)
raise raise
+3 -2
View File
@@ -33,9 +33,10 @@ def configure_logging(enabled: bool = False, level: str = "WARNING") -> None:
logger.addHandler(handler) logger.addHandler(handler)
def error_with_traceback(msg, *args, **kwargs): def error_with_traceback(msg, *args, **kwargs):
if isinstance(kwargs.get('exception', None), Exception): # If exc_info is True, include traceback in the message
if kwargs.get('exc_info', False):
full_msg = f"{msg}\n{traceback.format_exc()}" full_msg = f"{msg}\n{traceback.format_exc()}"
_original_error(full_msg, *args, **{**kwargs, 'exc_info': False}) _original_error(full_msg, *args, **{k: v for k, v in kwargs.items() if k != 'exc_info'})
else: else:
_original_error(msg, *args, **kwargs) _original_error(msg, *args, **kwargs)
+101 -105
View File
@@ -1,18 +1,13 @@
# -*- coding: utf-8 -*- # Copyright 2013 MMD Tools authors
# Copyright 2014 MMD Tools authors # This file is part of MMD Tools.
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import contextlib import contextlib
from typing import Generator, List, Optional, TypeVar, Any, Set, Tuple, Dict, Union import math
from typing import Generator, List, Optional, TypeVar
import bmesh
import bpy import bpy
from bpy.types import Object, Context, ID, Key, ShapeKey, FCurve, LayerCollection, Collection from mathutils import Matrix
from bpy.types import AddonPreferences, Addon, WindowManager, Area, Region, Window
from ..logging_setup import logger
class Props: # For API changes of only name changed properties class Props: # For API changes of only name changed properties
@@ -24,7 +19,7 @@ class Props: # For API changes of only name changed properties
class __EditMode: class __EditMode:
def __init__(self, obj: Object): def __init__(self, obj):
if not isinstance(obj, bpy.types.Object): if not isinstance(obj, bpy.types.Object):
raise ValueError raise ValueError
self.__prevMode = obj.mode self.__prevMode = obj.mode
@@ -34,10 +29,10 @@ class __EditMode:
if obj.mode != "EDIT": if obj.mode != "EDIT":
bpy.ops.object.mode_set(mode="EDIT") bpy.ops.object.mode_set(mode="EDIT")
def __enter__(self) -> Any: def __enter__(self):
return self.__obj.data return self.__obj.data
def __exit__(self, type: Any, value: Any, traceback: Any) -> None: def __exit__(self, exc_type, exc_value, traceback):
if self.__prevMode == "EDIT": if self.__prevMode == "EDIT":
bpy.ops.object.mode_set(mode="OBJECT") # update edited data bpy.ops.object.mode_set(mode="OBJECT") # update edited data
bpy.ops.object.mode_set(mode=self.__prevMode) bpy.ops.object.mode_set(mode=self.__prevMode)
@@ -45,43 +40,46 @@ class __EditMode:
class __SelectObjects: class __SelectObjects:
def __init__(self, active_object: Object, selected_objects: Optional[List[Object]] = None): def __init__(self, active_object: bpy.types.Object, selected_objects: Optional[List[bpy.types.Object]] = None):
if not isinstance(active_object, bpy.types.Object): if not isinstance(active_object, bpy.types.Object):
raise ValueError raise ValueError
try: try:
bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="OBJECT")
except Exception: except Exception:
logger.debug("Failed to set object mode")
pass pass
context = FnContext.ensure_context() contenxt = FnContext.ensure_context()
for i in context.selected_objects: for i in contenxt.selected_objects:
i.select_set(False) i.select_set(False)
self.__active_object = active_object self.__active_object = active_object
self.__selected_objects = tuple(set(selected_objects) | set([active_object])) if selected_objects else (active_object,) self.__selected_objects = tuple(set(selected_objects) | {active_object}) if selected_objects else (active_object,)
self.__hides: List[bool] = [] self.__hides: List[bool] = []
for i in self.__selected_objects: for i in self.__selected_objects:
self.__hides.append(i.hide_get()) self.__hides.append(i.hide_get())
FnContext.select_object(context, i) FnContext.select_object(contenxt, i)
FnContext.set_active_object(context, active_object) FnContext.set_active_object(contenxt, active_object)
def __enter__(self) -> Object: def __enter__(self) -> bpy.types.Object:
return self.__active_object return self.__active_object
def __exit__(self, type: Any, value: Any, traceback: Any) -> None: def __exit__(self, exc_type, exc_value, traceback):
for i, j in zip(self.__selected_objects, self.__hides): for i, j in zip(self.__selected_objects, self.__hides, strict=False):
try:
i.hide_set(j) i.hide_set(j)
except ReferenceError:
# Object may no longer exist, so skip restoring hidden state.
pass
def setParent(obj: Object, parent: Object) -> None: def setParent(obj, parent):
with select_object(parent, objects=[parent, obj]): with select_object(parent, objects=[parent, obj]):
bpy.ops.object.parent_set(type="OBJECT", xmirror=False, keep_transform=False) bpy.ops.object.parent_set(type="OBJECT", xmirror=False, keep_transform=False)
def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None: def setParentToBone(obj, parent, bone_name):
with select_object(parent, objects=[parent, obj]): with select_object(parent, objects=[parent, obj]):
bpy.ops.object.mode_set(mode="POSE") bpy.ops.object.mode_set(mode="POSE")
parent.data.bones.active = parent.data.bones[bone_name] parent.data.bones.active = parent.data.bones[bone_name]
@@ -89,7 +87,7 @@ def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None:
bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="OBJECT")
def edit_object(obj: Object) -> __EditMode: def edit_object(obj):
"""Set the object interaction mode to 'EDIT' """Set the object interaction mode to 'EDIT'
It is recommended to use 'edit_object' with 'with' statement like the following code. It is recommended to use 'edit_object' with 'with' statement like the following code.
@@ -100,7 +98,7 @@ def edit_object(obj: Object) -> __EditMode:
return __EditMode(obj) return __EditMode(obj)
def select_object(obj: Object, objects: Optional[List[Object]] = None) -> __SelectObjects: def select_object(obj: bpy.types.Object, objects: Optional[List[bpy.types.Object]] = None):
"""Select objects. """Select objects.
It is recommended to use 'select_object' with 'with' statement like the following code. It is recommended to use 'select_object' with 'with' statement like the following code.
@@ -109,27 +107,26 @@ def select_object(obj: Object, objects: Optional[List[Object]] = None) -> __Sele
with select_object(obj): with select_object(obj):
some functions... some functions...
""" """
# TODO: Reimplement with bpy.context.temp_override (If it ain't broke, don't fix it.) # TODO: Consider reimplementing with bpy.context.temp_override,
# but note that Blender's new API has stability issues.
# temp_override is prone to crashes, making the current approach safer.
# If it ain't broke, don't fix it.
return __SelectObjects(obj, objects) return __SelectObjects(obj, objects)
def duplicateObject(obj: Object, total_len: int) -> List[Object]: def duplicateObject(obj, total_len):
return FnContext.duplicate_object(FnContext.ensure_context(), obj, total_len) return FnContext.duplicate_object(FnContext.ensure_context(), obj, total_len)
def createObject(name: str = "Object", object_data: Optional[ID] = None, target_scene: Optional[bpy.types.Scene] = None) -> Object: def createObject(name="Object", object_data=None, target_scene=None):
context = FnContext.ensure_context(target_scene) context = FnContext.ensure_context(target_scene)
return FnContext.set_active_object(context, FnContext.new_and_link_object(context, name, object_data)) return FnContext.set_active_object(context, FnContext.new_and_link_object(context, name, object_data))
def makeSphere(segment: int = 8, ring_count: int = 5, radius: float = 1.0, target_object: Optional[Object] = None) -> Object: def makeSphere(segment=8, ring_count=5, radius=1.0, target_object=None):
import bmesh
if target_object is None: if target_object is None:
target_object = createObject(name="Sphere") mesh_data = bpy.data.meshes.new("Sphere")
logger.debug(f"Created new sphere object: {target_object.name}") target_object = createObject(name="Sphere", object_data=mesh_data)
else:
logger.debug(f"Using existing object for sphere: {target_object.name}")
mesh = target_object.data mesh = target_object.data
bm = bmesh.new() bm = bmesh.new()
@@ -146,15 +143,10 @@ def makeSphere(segment: int = 8, ring_count: int = 5, radius: float = 1.0, targe
return target_object return target_object
def makeBox(size: Tuple[float, float, float] = (1, 1, 1), target_object: Optional[Object] = None) -> Object: def makeBox(size=(1, 1, 1), target_object=None):
import bmesh
from mathutils import Matrix
if target_object is None: if target_object is None:
target_object = createObject(name="Box") mesh_data = bpy.data.meshes.new("Box")
logger.debug(f"Created new box object: {target_object.name}") target_object = createObject(name="Box", object_data=mesh_data)
else:
logger.debug(f"Using existing object for box: {target_object.name}")
mesh = target_object.data mesh = target_object.data
bm = bmesh.new() bm = bmesh.new()
@@ -170,16 +162,10 @@ def makeBox(size: Tuple[float, float, float] = (1, 1, 1), target_object: Optiona
return target_object return target_object
def makeCapsule(segment: int = 8, ring_count: int = 2, radius: float = 1.0, height: float = 1.0, target_object: Optional[Object] = None) -> Object: def makeCapsule(segment=8, ring_count=2, radius=1.0, height=1.0, target_object=None):
import math
import bmesh
if target_object is None: if target_object is None:
target_object = createObject(name="Capsule") mesh_data = bpy.data.meshes.new("Capsule")
logger.debug(f"Created new capsule object: {target_object.name}") target_object = createObject(name="Capsule", object_data=mesh_data)
else:
logger.debug(f"Using existing object for capsule: {target_object.name}")
height = max(height, 1e-3) height = max(height, 1e-3)
mesh = target_object.data mesh = target_object.data
@@ -188,8 +174,11 @@ def makeCapsule(segment: int = 8, ring_count: int = 2, radius: float = 1.0, heig
top = (0, 0, height / 2 + radius) top = (0, 0, height / 2 + radius)
verts.new(top) verts.new(top)
# f = lambda i: radius*i/ring_count # def f(i):
f = lambda i: radius * math.sin(0.5 * math.pi * i / ring_count) # return radius * i / ring_count
def f(i):
return radius * math.sin(0.5 * math.pi * i / ring_count)
for i in range(ring_count, 0, -1): for i in range(ring_count, 0, -1):
z = f(i - 1) z = f(i - 1)
t = math.sqrt(radius**2 - z**2) t = math.sqrt(radius**2 - z**2)
@@ -238,10 +227,10 @@ def makeCapsule(segment: int = 8, ring_count: int = 2, radius: float = 1.0, heig
class TransformConstraintOp: class TransformConstraintOp:
__MIN_MAX_MAP: Dict[Union[str, Tuple[str, str]], Union[str, Tuple[str, ...]]] = {"ROTATION": "_rot", "SCALE": "_scale"} __MIN_MAX_MAP = {"ROTATION": "_rot", "SCALE": "_scale"}
@staticmethod @staticmethod
def create(constraints: bpy.types.ObjectConstraints, name: str, map_type: str) -> bpy.types.TransformConstraint: def create(constraints, name, map_type):
c = constraints.get(name, None) c = constraints.get(name, None)
if c and c.type != "TRANSFORM": if c and c.type != "TRANSFORM":
constraints.remove(c) constraints.remove(c)
@@ -259,7 +248,7 @@ class TransformConstraintOp:
return c return c
@classmethod @classmethod
def min_max_attributes(cls, map_type: str, name_id: str = "") -> Tuple[str, ...]: def min_max_attributes(cls, map_type, name_id=""):
key = (map_type, name_id) key = (map_type, name_id)
ret = cls.__MIN_MAX_MAP.get(key, None) ret = cls.__MIN_MAX_MAP.get(key, None)
if ret is None: if ret is None:
@@ -269,7 +258,7 @@ class TransformConstraintOp:
return ret return ret
@classmethod @classmethod
def update_min_max(cls, constraint: bpy.types.TransformConstraint, value: float, influence: Optional[float] = 1) -> None: def update_min_max(cls, constraint, value, influence=1):
c = constraint c = constraint
if not c or c.type != "TRANSFORM": if not c or c.type != "TRANSFORM":
return return
@@ -293,14 +282,14 @@ class FnObject:
raise NotImplementedError("This class is not expected to be instantiated.") raise NotImplementedError("This class is not expected to be instantiated.")
@staticmethod @staticmethod
def mesh_remove_shape_key(mesh_object: Object, shape_key: ShapeKey) -> None: def mesh_remove_shape_key(mesh_object: bpy.types.Object, shape_key: bpy.types.ShapeKey):
assert isinstance(mesh_object.data, bpy.types.Mesh) assert isinstance(mesh_object.data, bpy.types.Mesh)
key: Key = shape_key.id_data key: bpy.types.Key = shape_key.id_data
assert key == mesh_object.data.shape_keys assert key == mesh_object.data.shape_keys
if mesh_object.animation_data is not None: if mesh_object.animation_data is not None:
fc_curve: FCurve fc_curve: bpy.types.FCurve
for fc_curve in mesh_object.animation_data.drivers: for fc_curve in mesh_object.animation_data.drivers:
if not fc_curve.data_path.startswith(shape_key.path_from_id()): if not fc_curve.data_path.startswith(shape_key.path_from_id()):
continue continue
@@ -324,35 +313,43 @@ class FnContext:
raise NotImplementedError("This class is not expected to be instantiated.") raise NotImplementedError("This class is not expected to be instantiated.")
@staticmethod @staticmethod
def ensure_context(context: Optional[Context] = None) -> Context: def ensure_context(context: Optional[bpy.types.Context] = None) -> bpy.types.Context:
return context or bpy.context return context or bpy.context
@staticmethod @staticmethod
def get_active_object(context: Context) -> Optional[Object]: def get_active_object(context: bpy.types.Context) -> Optional[bpy.types.Object]:
# Added defensive programming for get methods
# Related to: https://github.com/MMD-Blender/blender_mmd_tools_local/issues/176
if context is None or not hasattr(context, "active_object"):
return None
return context.active_object return context.active_object
@staticmethod @staticmethod
def set_active_object(context: Context, obj: Object) -> Object: def set_active_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
context.view_layer.objects.active = obj context.view_layer.objects.active = obj
return obj return obj
@staticmethod @staticmethod
def set_active_and_select_single_object(context: Context, obj: Object) -> Object: def set_active_and_select_single_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
return FnContext.set_active_object(context, FnContext.select_single_object(context, obj)) return FnContext.set_active_object(context, FnContext.select_single_object(context, obj))
@staticmethod @staticmethod
def get_scene_objects(context: Context) -> bpy.types.SceneObjects: def get_scene_objects(context: bpy.types.Context) -> bpy.types.SceneObjects:
# Added defensive programming for get methods
# Added for consistency with get_active_object
if context is None or not hasattr(context, "scene") or not hasattr(context.scene, "objects"):
return []
return context.scene.objects return context.scene.objects
@staticmethod @staticmethod
def ensure_selectable(context: Context, obj: Object) -> Object: def ensure_selectable(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
obj.hide_viewport = False obj.hide_viewport = False
obj.hide_select = False obj.hide_select = False
obj.hide_set(False) obj.hide_set(False)
if obj not in context.selectable_objects: if obj not in context.selectable_objects:
def __layer_check(layer_collection: LayerCollection) -> bool: def __layer_check(layer_collection: bpy.types.LayerCollection) -> bool:
for lc in layer_collection.children: for lc in layer_collection.children:
if __layer_check(lc): if __layer_check(lc):
lc.hide_viewport = False lc.hide_viewport = False
@@ -374,44 +371,44 @@ class FnContext:
return obj return obj
@staticmethod @staticmethod
def select_object(context: Context, obj: Object) -> Object: def select_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
FnContext.ensure_selectable(context, obj).select_set(True) FnContext.ensure_selectable(context, obj).select_set(True)
return obj return obj
@staticmethod @staticmethod
def select_objects(context: Context, *objects: Object) -> List[Object]: def select_objects(context: bpy.types.Context, *objects: bpy.types.Object) -> List[bpy.types.Object]:
return [FnContext.select_object(context, obj) for obj in objects] return [FnContext.select_object(context, obj) for obj in objects]
@staticmethod @staticmethod
def select_single_object(context: Context, obj: Object) -> Object: def select_single_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
for i in context.selected_objects: for i in context.selected_objects:
if i != obj: if i != obj:
i.select_set(False) i.select_set(False)
return FnContext.select_object(context, obj) return FnContext.select_object(context, obj)
@staticmethod @staticmethod
def link_object(context: Context, obj: Object) -> Object: def link_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
context.collection.objects.link(obj) context.collection.objects.link(obj)
return obj return obj
@staticmethod @staticmethod
def new_and_link_object(context: Context, name: str, object_data: Optional[ID]) -> Object: def new_and_link_object(context: bpy.types.Context, name: str, object_data: Optional[bpy.types.ID]) -> bpy.types.Object:
return FnContext.link_object(context, bpy.data.objects.new(name=name, object_data=object_data)) return FnContext.link_object(context, bpy.data.objects.new(name=name, object_data=object_data))
@staticmethod @staticmethod
def duplicate_object(context: Context, object_to_duplicate: Object, target_count: int) -> List[Object]: def duplicate_object(context: bpy.types.Context, object_to_duplicate: bpy.types.Object, target_count: int) -> List[bpy.types.Object]:
""" """
Duplicate object. Duplicate object.
This function duplicates the given object and returns a list of duplicated objects. This function duplicates the given object and returns a list of duplicated objects.
Args: Args:
context (Context): The context in which the duplication is performed. context (bpy.types.Context): The context in which the duplication is performed.
object_to_duplicate (Object): The object to be duplicated. object_to_duplicate (bpy.types.Object): The object to be duplicated.
target_count (int): The desired count of duplicated objects. target_count (int): The desired count of duplicated objects.
Returns: Returns:
List[Object]: A list of duplicated objects. List[bpy.types.Object]: A list of duplicated objects.
Raises: Raises:
AssertionError: If the number of selected objects in the context is not equal to 1 or if the selected object is not the same as the object to be duplicated. AssertionError: If the number of selected objects in the context is not equal to 1 or if the selected object is not the same as the object to be duplicated.
@@ -435,28 +432,27 @@ class FnContext:
last_selected_objects[i].select_set(True) last_selected_objects[i].select_set(True)
last_selected_objects = context.selected_objects last_selected_objects = context.selected_objects
assert len(result_objects) == target_count assert len(result_objects) == target_count
logger.debug(f"Duplicated object {object_to_duplicate.name} to create {target_count} objects")
return result_objects return result_objects
@staticmethod @staticmethod
def find_user_layer_collection_by_object(context: Context, target_object: Object) -> Optional[LayerCollection]: def find_user_layer_collection_by_object(context: bpy.types.Context, target_object: bpy.types.Object) -> Optional[bpy.types.LayerCollection]:
""" """
Finds the layer collection that contains the given target_object in the user's collections. Find the layer collection that contains the given target_object in the user's collections.
Args: Args:
context (Context): The Blender context. context (bpy.types.Context): The Blender context.
target_object (Object): The target object to find the layer collection for. target_object (bpy.types.Object): The target object to find the layer collection for.
Returns: Returns:
Optional[LayerCollection]: The layer collection that contains the target_object, or None if not found. Optional[bpy.types.LayerCollection]: The layer collection that contains the target_object, or None if not found.
""" """
scene_layer_collection: LayerCollection = context.view_layer.layer_collection scene_layer_collection: bpy.types.LayerCollection = context.view_layer.layer_collection
def find_layer_collection_by_name(layer_collection: LayerCollection, name: str) -> Optional[LayerCollection]: def find_layer_collection_by_name(layer_collection: bpy.types.LayerCollection, name: str) -> Optional[bpy.types.LayerCollection]:
if layer_collection.name == name: if layer_collection.name == name:
return layer_collection return layer_collection
child_layer_collection: LayerCollection child_layer_collection: bpy.types.LayerCollection
for child_layer_collection in layer_collection.children: for child_layer_collection in layer_collection.children:
found = find_layer_collection_by_name(child_layer_collection, name) found = find_layer_collection_by_name(child_layer_collection, name)
if found is not None: if found is not None:
@@ -464,7 +460,7 @@ class FnContext:
return None return None
user_collection: Collection user_collection: bpy.types.Collection
for user_collection in target_object.users_collection: for user_collection in target_object.users_collection:
found = find_layer_collection_by_name(scene_layer_collection, user_collection.name) found = find_layer_collection_by_name(scene_layer_collection, user_collection.name)
if found is not None: if found is not None:
@@ -474,7 +470,7 @@ class FnContext:
@staticmethod @staticmethod
@contextlib.contextmanager @contextlib.contextmanager
def temp_override_active_layer_collection(context: Context, target_object: Object) -> Generator[Context, None, None]: def temp_override_active_layer_collection(context: bpy.types.Context, target_object: bpy.types.Object) -> Generator[bpy.types.Context, None, None]:
""" """
Context manager to temporarily override the active_layer_collection that contains the target object. Context manager to temporarily override the active_layer_collection that contains the target object.
@@ -482,11 +478,11 @@ class FnContext:
It ensures that the original active_layer_collection is restored after the context is exited. It ensures that the original active_layer_collection is restored after the context is exited.
Args: Args:
context (Context): The context in which the active_layer_collection will be overridden. context (bpy.types.Context): The context in which the active_layer_collection will be overridden.
target_object (Object): The target object whose layer collection will be set as the active_layer_collection. target_object (bpy.types.Object): The target object whose layer collection will be set as the active_layer_collection.
Yields: Yields:
Context: The modified context with the active_layer_collection overridden. bpy.types.Context: The modified context with the active_layer_collection overridden.
Example: Example:
with FnContext.temp_override_active_layer_collection(context, target_object): with FnContext.temp_override_active_layer_collection(context, target_object):
@@ -507,24 +503,24 @@ class FnContext:
context.view_layer.active_layer_collection = original_layer_collection context.view_layer.active_layer_collection = original_layer_collection
@staticmethod @staticmethod
def __get_addon_preferences(context: Context) -> Optional[AddonPreferences]: def __get_addon_preferences(context: bpy.types.Context) -> Optional[bpy.types.AddonPreferences]:
addon: Addon = context.preferences.addons.get(__package__, None) addon: bpy.types.Addon = context.preferences.addons.get(__package__, None)
return addon.preferences if addon else None return addon.preferences if addon else None
@staticmethod @staticmethod
def get_addon_preferences_attribute(context: Context, attribute_name: str, default_value: ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE = None) -> ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE: def get_addon_preferences_attribute(context: bpy.types.Context, attribute_name: str, default_value: ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE = None) -> ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE:
return getattr(FnContext.__get_addon_preferences(context), attribute_name, default_value) return getattr(FnContext.__get_addon_preferences(context), attribute_name, default_value)
@staticmethod @staticmethod
def temp_override_objects( def temp_override_objects(
context: Context, context: bpy.types.Context,
window: Optional[Window] = None, window: Optional[bpy.types.Window] = None,
area: Optional[Area] = None, area: Optional[bpy.types.Area] = None,
region: Optional[Region] = None, region: Optional[bpy.types.Region] = None,
active_object: Optional[Object] = None, active_object: Optional[bpy.types.Object] = None,
selected_objects: Optional[List[Object]] = None, selected_objects: Optional[List[bpy.types.Object]] = None,
**keywords: Any, **keywords,
) -> Generator[Context, None, None]: ) -> Generator[bpy.types.Context, None, None]:
if active_object is not None: if active_object is not None:
keywords["active_object"] = active_object keywords["active_object"] = active_object
keywords["object"] = active_object keywords["object"] = active_object
+95 -185
View File
@@ -1,44 +1,37 @@
# -*- coding: utf-8 -*- # Copyright 2015 MMD Tools authors
# Copyright 2014 MMD Tools authors # This file is part of MMD Tools.
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import math import math
from typing import TYPE_CHECKING, Iterable, Optional, Set, List, Dict, Tuple, Any, Union, cast from typing import TYPE_CHECKING, Iterable, Optional, Set
import bpy import bpy
from mathutils import Vector from mathutils import Vector
from bpy.types import Object, EditBone, PoseBone, Constraint, Armature, BoneCollection
from .. import bpyutils from .. import bpyutils
from ..bpyutils import TransformConstraintOp from ..bpyutils import TransformConstraintOp
from ..utils import ItemOp from ..utils import ItemOp
from ....core.logging_setup import logger
if TYPE_CHECKING: if TYPE_CHECKING:
from ..properties.root import MMDRoot, MMDDisplayItemFrame
from ..properties.pose_bone import MMDBone from ..properties.pose_bone import MMDBone
from ..properties.root import MMDDisplayItemFrame, MMDRoot
def remove_constraint(constraints: Any, name: str) -> bool: def remove_constraint(constraints, name):
"""Remove a constraint by name if it exists"""
c = constraints.get(name, None) c = constraints.get(name, None)
if c: if c:
constraints.remove(c) constraints.remove(c)
return True return True
return False return False
def remove_edit_bones(edit_bones: bpy.types.ArmatureEditBones, bone_names: List[str]) -> None:
"""Remove edit bones by name""" def remove_edit_bones(edit_bones, bone_names):
for name in bone_names: for name in bone_names:
b = edit_bones.get(name, None) b = edit_bones.get(name, None)
if b: if b:
edit_bones.remove(b) edit_bones.remove(b)
BONE_COLLECTION_CUSTOM_PROPERTY_NAME = "mmd_tools" BONE_COLLECTION_CUSTOM_PROPERTY_NAME = "mmd_tools_local"
BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL = "special collection" BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL = "special collection"
BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL = "normal collection" BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL = "normal collection"
BONE_COLLECTION_NAME_SHADOW = "mmd_shadow" BONE_COLLECTION_NAME_SHADOW = "mmd_shadow"
@@ -48,39 +41,33 @@ SPECIAL_BONE_COLLECTION_NAMES = [BONE_COLLECTION_NAME_SHADOW, BONE_COLLECTION_NA
class FnBone: class FnBone:
AUTO_LOCAL_AXIS_ARMS: Tuple[str, ...] = ("左肩", "左腕", "左ひじ", "左手首", "右腕", "右肩", "右ひじ", "右手首") AUTO_LOCAL_AXIS_ARMS = ("左肩", "左腕", "左ひじ", "左手首", "右腕", "右肩", "右ひじ", "右手首")
AUTO_LOCAL_AXIS_FINGERS: Tuple[str, ...] = ("親指", "人指", "中指", "薬指", "小指") AUTO_LOCAL_AXIS_FINGERS = ("親指", "人指", "中指", "薬指", "小指")
AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS: Tuple[str, ...] = ("左腕捩", "左手捩", "左肩P", "左ダミー", "右腕捩", "右手捩", "右肩P", "右ダミー") AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS = ("左腕捩", "左手捩", "左肩P", "左ダミー", "右腕捩", "右手捩", "右肩P", "右ダミー")
def __init__(self) -> None: def __init__(self):
raise NotImplementedError("This class cannot be instantiated.") raise NotImplementedError("This class cannot be instantiated.")
@staticmethod @staticmethod
def find_pose_bone_by_bone_id(armature_object: Object, bone_id: int) -> Optional[PoseBone]: def find_pose_bone_by_bone_id(armature_object: bpy.types.Object, bone_id: int) -> Optional[bpy.types.PoseBone]:
"""Find a pose bone by its bone ID"""
for bone in armature_object.pose.bones: for bone in armature_object.pose.bones:
if bone.mmd_bone.bone_id != bone_id: if bone.mmd_bone.bone_id != bone_id:
continue continue
return bone return bone
logger.debug(f"Bone with ID {bone_id} not found in armature {armature_object.name}")
return None return None
@staticmethod @staticmethod
def __new_bone_id(armature_object: Object) -> int: def __new_bone_id(armature_object: bpy.types.Object) -> int:
"""Generate a new unique bone ID"""
return max(b.mmd_bone.bone_id for b in armature_object.pose.bones) + 1 return max(b.mmd_bone.bone_id for b in armature_object.pose.bones) + 1
@staticmethod @staticmethod
def get_or_assign_bone_id(pose_bone: PoseBone) -> int: def get_or_assign_bone_id(pose_bone: bpy.types.PoseBone) -> int:
"""Get the bone ID or assign a new one if not set"""
if pose_bone.mmd_bone.bone_id < 0: if pose_bone.mmd_bone.bone_id < 0:
pose_bone.mmd_bone.bone_id = FnBone.__new_bone_id(pose_bone.id_data) pose_bone.mmd_bone.bone_id = FnBone.__new_bone_id(pose_bone.id_data)
logger.debug(f"Assigned new bone ID {pose_bone.mmd_bone.bone_id} to bone {pose_bone.name}")
return pose_bone.mmd_bone.bone_id return pose_bone.mmd_bone.bone_id
@staticmethod @staticmethod
def __get_selected_pose_bones(armature_object: Object) -> Iterable[PoseBone]: def __get_selected_pose_bones(armature_object: bpy.types.Object) -> Iterable[bpy.types.PoseBone]:
"""Get selected pose bones from the armature"""
if armature_object.mode == "EDIT": if armature_object.mode == "EDIT":
bpy.ops.object.mode_set(mode="OBJECT") # update selected bones bpy.ops.object.mode_set(mode="OBJECT") # update selected bones
bpy.ops.object.mode_set(mode="EDIT") # back to edit mode bpy.ops.object.mode_set(mode="EDIT") # back to edit mode
@@ -89,11 +76,9 @@ class FnBone:
return (bones[b.name] for b in context_selected_bones if not bones[b.name].is_mmd_shadow_bone) return (bones[b.name] for b in context_selected_bones if not bones[b.name].is_mmd_shadow_bone)
@staticmethod @staticmethod
def load_bone_fixed_axis(armature_object: Object, enable: bool = True) -> None: def load_bone_fixed_axis(armature_object: bpy.types.Object, enable=True):
"""Load fixed axis settings for selected bones"""
logger.debug(f"Loading bone fixed axis (enable={enable}) for {armature_object.name}")
for b in FnBone.__get_selected_pose_bones(armature_object): for b in FnBone.__get_selected_pose_bones(armature_object):
mmd_bone = b.mmd_bone mmd_bone: MMDBone = b.mmd_bone
mmd_bone.enabled_fixed_axis = enable mmd_bone.enabled_fixed_axis = enable
lock_rotation = b.lock_rotation[:] lock_rotation = b.lock_rotation[:]
if enable: if enable:
@@ -108,91 +93,72 @@ class FnBone:
b.lock_location = b.lock_scale = (False, False, False) b.lock_location = b.lock_scale = (False, False, False)
@staticmethod @staticmethod
def setup_special_bone_collections(armature_object: Object) -> Object: def setup_special_bone_collections(armature_object: bpy.types.Object) -> bpy.types.Object:
"""Set up special bone collections for MMD""" armature: bpy.types.Armature = armature_object.data
armature = cast(Armature, armature_object.data)
bone_collections = armature.collections bone_collections = armature.collections
for bone_collection_name in SPECIAL_BONE_COLLECTION_NAMES: for bone_collection_name in SPECIAL_BONE_COLLECTION_NAMES:
if bone_collection_name in bone_collections: if bone_collection_name in bone_collections:
continue continue
bone_collection = bone_collections.new(bone_collection_name) bone_collection = bone_collections.new(bone_collection_name)
FnBone.__set_bone_collection_to_special(bone_collection, is_visible=False) FnBone.__set_bone_collection_to_special(bone_collection, is_visible=False)
logger.debug(f"Created special bone collection: {bone_collection_name}")
return armature_object return armature_object
@staticmethod @staticmethod
def __is_mmd_tools_bone_collection(bone_collection: BoneCollection) -> bool: def __is_mmd_tools_local_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool:
"""Check if a bone collection is an MMD Tools collection"""
return BONE_COLLECTION_CUSTOM_PROPERTY_NAME in bone_collection return BONE_COLLECTION_CUSTOM_PROPERTY_NAME in bone_collection
@staticmethod @staticmethod
def __is_special_bone_collection(bone_collection: BoneCollection) -> bool: def __is_special_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool:
"""Check if a bone collection is a special MMD collection""" return bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME) == BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL
return BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL == bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME)
@staticmethod @staticmethod
def __set_bone_collection_to_special(bone_collection: BoneCollection, is_visible: bool) -> None: def __set_bone_collection_to_special(bone_collection: bpy.types.BoneCollection, is_visible: bool):
"""Mark a bone collection as special"""
bone_collection[BONE_COLLECTION_CUSTOM_PROPERTY_NAME] = BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL bone_collection[BONE_COLLECTION_CUSTOM_PROPERTY_NAME] = BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL
bone_collection.is_visible = is_visible bone_collection.is_visible = is_visible
@staticmethod @staticmethod
def __is_normal_bone_collection(bone_collection: BoneCollection) -> bool: def __is_normal_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool:
"""Check if a bone collection is a normal MMD collection""" return bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME) == BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL
return BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL == bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME)
@staticmethod @staticmethod
def __set_bone_collection_to_normal(bone_collection: BoneCollection) -> None: def __set_bone_collection_to_normal(bone_collection: bpy.types.BoneCollection):
"""Mark a bone collection as normal"""
bone_collection[BONE_COLLECTION_CUSTOM_PROPERTY_NAME] = BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL bone_collection[BONE_COLLECTION_CUSTOM_PROPERTY_NAME] = BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL
@staticmethod @staticmethod
def __set_edit_bone_to_special(edit_bone: EditBone, bone_collection_name: str) -> EditBone: def __set_edit_bone_to_special(edit_bone: bpy.types.EditBone, bone_collection_name: str) -> bpy.types.EditBone:
"""Set an edit bone to a special collection"""
edit_bone.id_data.collections[bone_collection_name].assign(edit_bone) edit_bone.id_data.collections[bone_collection_name].assign(edit_bone)
edit_bone.use_deform = False edit_bone.use_deform = False
return edit_bone return edit_bone
@staticmethod @staticmethod
def set_edit_bone_to_dummy(edit_bone: EditBone) -> EditBone: def set_edit_bone_to_dummy(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone:
"""Set an edit bone as a dummy bone"""
logger.debug(f"Setting bone {edit_bone.name} as dummy bone")
return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_DUMMY) return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_DUMMY)
@staticmethod @staticmethod
def set_edit_bone_to_shadow(edit_bone: EditBone) -> EditBone: def set_edit_bone_to_shadow(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone:
"""Set an edit bone as a shadow bone"""
logger.debug(f"Setting bone {edit_bone.name} as shadow bone")
return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_SHADOW) return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_SHADOW)
@staticmethod @staticmethod
def __unassign_mmd_tools_bone_collections(edit_bone: EditBone) -> EditBone: def __unassign_mmd_tools_local_bone_collections(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone:
"""Unassign an edit bone from all MMD Tools collections"""
for bone_collection in edit_bone.collections: for bone_collection in edit_bone.collections:
if not FnBone.__is_mmd_tools_bone_collection(bone_collection): if not FnBone.__is_mmd_tools_local_bone_collection(bone_collection):
continue continue
bone_collection.unassign(edit_bone) bone_collection.unassign(edit_bone)
return edit_bone return edit_bone
@staticmethod @staticmethod
def sync_bone_collections_from_display_item_frames(armature_object: Object) -> None: def sync_bone_collections_from_display_item_frames(armature_object: bpy.types.Object):
"""Synchronize bone collections from display item frames""" armature: bpy.types.Armature = armature_object.data
logger.info(f"Syncing bone collections from display item frames for {armature_object.name}")
armature = cast(Armature, armature_object.data)
bone_collections = armature.collections bone_collections = armature.collections
from .model import FnModel from .model import FnModel
root_object = FnModel.find_root_object(armature_object) root_object: bpy.types.Object = FnModel.find_root_object(armature_object)
if not root_object: mmd_root: MMDRoot = root_object.mmd_root
logger.error(f"No root object found for armature {armature_object.name}")
return
mmd_root = root_object.mmd_root
bones = armature.bones bones = armature.bones
used_groups: Set[str] = set() used_groups = set()
unassigned_bone_names: Set[str] = {b.name for b in bones} unassigned_bone_names = {b.name for b in bones}
for frame in mmd_root.display_item_frames: for frame in mmd_root.display_item_frames:
for item in frame.data: for item in frame.data:
@@ -204,12 +170,11 @@ class FnBone:
if bone_collection is None: if bone_collection is None:
bone_collection = bone_collections.new(name=group_name) bone_collection = bone_collections.new(name=group_name)
FnBone.__set_bone_collection_to_normal(bone_collection) FnBone.__set_bone_collection_to_normal(bone_collection)
logger.debug(f"Created new bone collection: {group_name}")
bone_collection.assign(bones[item.name]) bone_collection.assign(bones[item.name])
for name in unassigned_bone_names: for name in unassigned_bone_names:
for bc in bones[name].collections: for bc in bones[name].collections:
if not FnBone.__is_mmd_tools_bone_collection(bc): if not FnBone.__is_mmd_tools_local_bone_collection(bc):
continue continue
if not FnBone.__is_normal_bone_collection(bc): if not FnBone.__is_normal_bone_collection(bc):
continue continue
@@ -219,48 +184,40 @@ class FnBone:
for bone_collection in bone_collections.values(): for bone_collection in bone_collections.values():
if bone_collection.name in used_groups: if bone_collection.name in used_groups:
continue continue
if not FnBone.__is_mmd_tools_bone_collection(bone_collection): if not FnBone.__is_mmd_tools_local_bone_collection(bone_collection):
continue continue
if not FnBone.__is_normal_bone_collection(bone_collection): if not FnBone.__is_normal_bone_collection(bone_collection):
continue continue
logger.debug(f"Removing unused bone collection: {bone_collection.name}")
bone_collections.remove(bone_collection) bone_collections.remove(bone_collection)
@staticmethod @staticmethod
def sync_display_item_frames_from_bone_collections(armature_object: Object) -> None: def sync_display_item_frames_from_bone_collections(armature_object: bpy.types.Object):
"""Synchronize display item frames from bone collections""" armature: bpy.types.Armature = armature_object.data
logger.info(f"Syncing display item frames from bone collections for {armature_object.name}") bone_collections: bpy.types.BoneCollections = armature.collections
armature = cast(Armature, armature_object.data)
bone_collections = armature.collections
from .model import FnModel from .model import FnModel
root_object = FnModel.find_root_object(armature_object) root_object: bpy.types.Object = FnModel.find_root_object(armature_object)
if not root_object: mmd_root: MMDRoot = root_object.mmd_root
logger.error(f"No root object found for armature {armature_object.name}")
return
mmd_root = root_object.mmd_root
display_item_frames = mmd_root.display_item_frames display_item_frames = mmd_root.display_item_frames
used_frame_index: Set[int] = set() used_frame_index: Set[int] = set()
bone_collection: BoneCollection bone_collection: bpy.types.BoneCollection
for bone_collection in bone_collections: for bone_collection in bone_collections:
if len(bone_collection.bones) == 0 or FnBone.__is_special_bone_collection(bone_collection): if len(bone_collection.bones) == 0 or FnBone.__is_special_bone_collection(bone_collection):
continue continue
bone_collection_name = bone_collection.name bone_collection_name = bone_collection.name
display_item_frame = display_item_frames.get(bone_collection_name) display_item_frame: Optional[MMDDisplayItemFrame] = display_item_frames.get(bone_collection_name)
if display_item_frame is None: if display_item_frame is None:
display_item_frame = display_item_frames.add() display_item_frame = display_item_frames.add()
display_item_frame.name = bone_collection_name display_item_frame.name = bone_collection_name
display_item_frame.name_e = bone_collection_name display_item_frame.name_e = bone_collection_name
logger.debug(f"Created new display item frame: {bone_collection_name}")
used_frame_index.add(display_item_frames.find(bone_collection_name)) used_frame_index.add(display_item_frames.find(bone_collection_name))
ItemOp.resize(display_item_frame.data, len(bone_collection.bones)) ItemOp.resize(display_item_frame.data, len(bone_collection.bones))
for display_item, bone in zip(display_item_frame.data, bone_collection.bones): for display_item, bone in zip(display_item_frame.data, bone_collection.bones, strict=False):
display_item.type = "BONE" display_item.type = "BONE"
display_item.name = bone.name display_item.name = bone.name
@@ -271,27 +228,23 @@ class FnBone:
if display_item_frame.is_special: if display_item_frame.is_special:
if display_item_frame.name != "表情": if display_item_frame.name != "表情":
display_item_frame.data.clear() display_item_frame.data.clear()
logger.debug(f"Cleared special display item frame: {display_item_frame.name}")
else: else:
logger.debug(f"Removing unused display item frame: {display_item_frames[i].name}")
display_item_frames.remove(i) display_item_frames.remove(i)
mmd_root.active_display_item_frame = 0 mmd_root.active_display_item_frame = 0
@staticmethod @staticmethod
def apply_bone_fixed_axis(armature_object: Object) -> None: def apply_bone_fixed_axis(armature_object: bpy.types.Object):
"""Apply fixed axis to bones""" bone_map = {}
logger.info(f"Applying bone fixed axis for {armature_object.name}")
bone_map: Dict[str, Tuple[Vector, bool, bool]] = {}
for b in armature_object.pose.bones: for b in armature_object.pose.bones:
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_fixed_axis: if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_fixed_axis:
continue continue
mmd_bone = b.mmd_bone mmd_bone: MMDBone = b.mmd_bone
parent_tip = b.parent and not b.parent.is_mmd_shadow_bone and b.parent.mmd_bone.is_tip parent_tip = b.parent and not b.parent.is_mmd_shadow_bone and b.parent.mmd_bone.is_tip
bone_map[b.name] = (mmd_bone.fixed_axis.normalized(), mmd_bone.is_tip, parent_tip) bone_map[b.name] = (mmd_bone.fixed_axis.normalized(), mmd_bone.is_tip, parent_tip)
force_align = True force_align = True
with bpyutils.edit_object(armature_object) as data: with bpyutils.edit_object(armature_object) as data:
bone: EditBone bone: bpy.types.EditBone
for bone in data.edit_bones: for bone in data.edit_bones:
if bone.name not in bone_map: if bone.name not in bone_map:
bone.select = False bone.select = False
@@ -322,7 +275,6 @@ class FnBone:
else: else:
bone_map[bone.name] = (True, True, True) bone_map[bone.name] = (True, True, True)
bone.select = True bone.select = True
logger.debug(f"Applied fixed axis to bone: {bone.name}")
for bone_name, locks in bone_map.items(): for bone_name, locks in bone_map.items():
b = armature_object.pose.bones[bone_name] b = armature_object.pose.bones[bone_name]
@@ -330,11 +282,9 @@ class FnBone:
b.lock_ik_x, b.lock_ik_y, b.lock_ik_z = b.lock_rotation = locks b.lock_ik_x, b.lock_ik_y, b.lock_ik_z = b.lock_rotation = locks
@staticmethod @staticmethod
def load_bone_local_axes(armature_object: Object, enable: bool = True) -> None: def load_bone_local_axes(armature_object: bpy.types.Object, enable=True):
"""Load local axes for selected bones"""
logger.debug(f"Loading bone local axes (enable={enable}) for {armature_object.name}")
for b in FnBone.__get_selected_pose_bones(armature_object): for b in FnBone.__get_selected_pose_bones(armature_object):
mmd_bone = b.mmd_bone mmd_bone: MMDBone = b.mmd_bone
mmd_bone.enabled_local_axes = enable mmd_bone.enabled_local_axes = enable
if enable: if enable:
axes = b.bone.matrix_local.to_3x3().transposed() axes = b.bone.matrix_local.to_3x3().transposed()
@@ -342,18 +292,16 @@ class FnBone:
mmd_bone.local_axis_z = axes[2].xzy mmd_bone.local_axis_z = axes[2].xzy
@staticmethod @staticmethod
def apply_bone_local_axes(armature_object: Object) -> None: def apply_bone_local_axes(armature_object: bpy.types.Object):
"""Apply local axes to bones""" bone_map = {}
logger.info(f"Applying bone local axes for {armature_object.name}")
bone_map: Dict[str, Tuple[Vector, Vector]] = {}
for b in armature_object.pose.bones: for b in armature_object.pose.bones:
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_local_axes: if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_local_axes:
continue continue
mmd_bone = b.mmd_bone mmd_bone: MMDBone = b.mmd_bone
bone_map[b.name] = (mmd_bone.local_axis_x, mmd_bone.local_axis_z) bone_map[b.name] = (mmd_bone.local_axis_x, mmd_bone.local_axis_z)
with bpyutils.edit_object(armature_object) as data: with bpyutils.edit_object(armature_object) as data:
bone: EditBone bone: bpy.types.EditBone
for bone in data.edit_bones: for bone in data.edit_bones:
if bone.name not in bone_map: if bone.name not in bone_map:
bone.select = False bone.select = False
@@ -361,18 +309,15 @@ class FnBone:
local_axis_x, local_axis_z = bone_map[bone.name] local_axis_x, local_axis_z = bone_map[bone.name]
FnBone.update_bone_roll(bone, local_axis_x, local_axis_z) FnBone.update_bone_roll(bone, local_axis_x, local_axis_z)
bone.select = True bone.select = True
logger.debug(f"Applied local axes to bone: {bone.name}")
@staticmethod @staticmethod
def update_bone_roll(edit_bone: EditBone, mmd_local_axis_x: Vector, mmd_local_axis_z: Vector) -> None: def update_bone_roll(edit_bone: bpy.types.EditBone, mmd_local_axis_x, mmd_local_axis_z):
"""Update bone roll based on local axes"""
axes = FnBone.get_axes(mmd_local_axis_x, mmd_local_axis_z) axes = FnBone.get_axes(mmd_local_axis_x, mmd_local_axis_z)
idx, val = max([(i, edit_bone.vector.dot(v)) for i, v in enumerate(axes)], key=lambda x: abs(x[1])) idx, val = max([(i, edit_bone.vector.dot(v)) for i, v in enumerate(axes)], key=lambda x: abs(x[1]))
edit_bone.align_roll(axes[(idx - 1) % 3 if val < 0 else (idx + 1) % 3]) edit_bone.align_roll(axes[(idx - 1) % 3 if val < 0 else (idx + 1) % 3])
@staticmethod @staticmethod
def get_axes(mmd_local_axis_x: Vector, mmd_local_axis_z: Vector) -> Tuple[Vector, Vector, Vector]: def get_axes(mmd_local_axis_x, mmd_local_axis_z):
"""Get axes from local axis vectors"""
x_axis = Vector(mmd_local_axis_x).normalized().xzy x_axis = Vector(mmd_local_axis_x).normalized().xzy
z_axis = Vector(mmd_local_axis_z).normalized().xzy z_axis = Vector(mmd_local_axis_z).normalized().xzy
y_axis = z_axis.cross(x_axis).normalized() y_axis = z_axis.cross(x_axis).normalized()
@@ -380,25 +325,18 @@ class FnBone:
return (x_axis, y_axis, z_axis) return (x_axis, y_axis, z_axis)
@staticmethod @staticmethod
def apply_auto_bone_roll(armature: Object) -> None: def apply_auto_bone_roll(armature):
"""Apply automatic bone roll to appropriate bones""" bone_names = [b.name for b in armature.pose.bones if not b.is_mmd_shadow_bone and not b.mmd_bone.enabled_local_axes and FnBone.has_auto_local_axis(b.mmd_bone.name_j)]
logger.info(f"Applying auto bone roll for {armature.name}")
bone_names: List[str] = []
for b in armature.pose.bones:
if not b.is_mmd_shadow_bone and not b.mmd_bone.enabled_local_axes and FnBone.has_auto_local_axis(b.mmd_bone.name_j):
bone_names.append(b.name)
with bpyutils.edit_object(armature) as data: with bpyutils.edit_object(armature) as data:
bone: EditBone bone: bpy.types.EditBone
for bone in data.edit_bones: for bone in data.edit_bones:
if bone.name not in bone_names: if bone.name not in bone_names:
continue continue
FnBone.update_auto_bone_roll(bone) FnBone.update_auto_bone_roll(bone)
bone.select = True bone.select = True
logger.debug(f"Applied auto bone roll to bone: {bone.name}")
@staticmethod @staticmethod
def update_auto_bone_roll(edit_bone: EditBone) -> None: def update_auto_bone_roll(edit_bone):
"""Update bone roll automatically"""
# make a triangle face (p1,p2,p3) # make a triangle face (p1,p2,p3)
p1 = edit_bone.head.copy() p1 = edit_bone.head.copy()
p2 = edit_bone.tail.copy() p2 = edit_bone.tail.copy()
@@ -419,8 +357,7 @@ class FnBone:
FnBone.update_bone_roll(edit_bone, y.xzy, x.xzy) FnBone.update_bone_roll(edit_bone, y.xzy, x.xzy)
@staticmethod @staticmethod
def has_auto_local_axis(name_j: str) -> bool: def has_auto_local_axis(name_j):
"""Check if a bone should have automatic local axis"""
if name_j: if name_j:
if name_j in FnBone.AUTO_LOCAL_AXIS_ARMS or name_j in FnBone.AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS: if name_j in FnBone.AUTO_LOCAL_AXIS_ARMS or name_j in FnBone.AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS:
return True return True
@@ -430,11 +367,12 @@ class FnBone:
return False return False
@staticmethod @staticmethod
def clean_additional_transformation(armature_object: Object) -> None: def clean_additional_transformation(armature_object: bpy.types.Object):
"""Clean additional transformation constraints and bones""" if armature_object.type != "ARMATURE" or armature_object.pose is None:
logger.info(f"Cleaning additional transformations for {armature_object.name}") return
# clean constraints # clean constraints
p_bone: PoseBone p_bone: bpy.types.PoseBone
for p_bone in armature_object.pose.bones: for p_bone in armature_object.pose.bones:
p_bone.mmd_bone.is_additional_transform_dirty = True p_bone.mmd_bone.is_additional_transform_dirty = True
constraints = p_bone.constraints constraints = p_bone.constraints
@@ -450,21 +388,17 @@ class FnBone:
"ADDITIONAL_TRANSFORM_INVERT", "ADDITIONAL_TRANSFORM_INVERT",
} }
def __is_at_shadow_bone(b: PoseBone) -> bool: def __is_at_shadow_bone(b):
return b.is_mmd_shadow_bone and b.mmd_shadow_bone_type in shadow_bone_types return b.is_mmd_shadow_bone and b.mmd_shadow_bone_type in shadow_bone_types
shadow_bone_names = [b.name for b in armature_object.pose.bones if __is_at_shadow_bone(b)] shadow_bone_names = [b.name for b in armature_object.pose.bones if __is_at_shadow_bone(b)]
if len(shadow_bone_names) > 0: if len(shadow_bone_names) > 0:
logger.debug(f"Removing {len(shadow_bone_names)} shadow bones")
with bpyutils.edit_object(armature_object) as data: with bpyutils.edit_object(armature_object) as data:
remove_edit_bones(data.edit_bones, shadow_bone_names) remove_edit_bones(data.edit_bones, shadow_bone_names)
@staticmethod @staticmethod
def apply_additional_transformation(armature_object: Object) -> None: def apply_additional_transformation(armature_object: bpy.types.Object):
"""Apply additional transformation to bones""" def __is_dirty_bone(b):
logger.info(f"Applying additional transformations for {armature_object.name}")
def __is_dirty_bone(b: PoseBone) -> bool:
if b.is_mmd_shadow_bone: if b.is_mmd_shadow_bone:
return False return False
mmd_bone = b.mmd_bone mmd_bone = b.mmd_bone
@@ -473,10 +407,9 @@ class FnBone:
return mmd_bone.is_additional_transform_dirty return mmd_bone.is_additional_transform_dirty
dirty_bones = [b for b in armature_object.pose.bones if __is_dirty_bone(b)] dirty_bones = [b for b in armature_object.pose.bones if __is_dirty_bone(b)]
logger.debug(f"Found {len(dirty_bones)} dirty bones to process")
# setup constraints # setup constraints
shadow_bone_pool: List[Union[_AT_ShadowBoneRemove, _AT_ShadowBoneCreate]] = [] shadow_bone_pool = []
for p_bone in dirty_bones: for p_bone in dirty_bones:
sb = FnBone.__setup_constraints(p_bone) sb = FnBone.__setup_constraints(p_bone)
if sb: if sb:
@@ -497,8 +430,7 @@ class FnBone:
p_bone.mmd_bone.is_additional_transform_dirty = False p_bone.mmd_bone.is_additional_transform_dirty = False
@staticmethod @staticmethod
def __setup_constraints(p_bone: PoseBone) -> Optional[Union['_AT_ShadowBoneRemove', '_AT_ShadowBoneCreate']]: def __setup_constraints(p_bone):
"""Set up constraints for additional transformation"""
bone_name = p_bone.name bone_name = p_bone.name
mmd_bone = p_bone.mmd_bone mmd_bone = p_bone.mmd_bone
influence = mmd_bone.additional_transform_influence influence = mmd_bone.additional_transform_influence
@@ -511,18 +443,21 @@ class FnBone:
rot = remove_constraint(constraints, "mmd_additional_rotation") rot = remove_constraint(constraints, "mmd_additional_rotation")
loc = remove_constraint(constraints, "mmd_additional_location") loc = remove_constraint(constraints, "mmd_additional_location")
if rot or loc: if rot or loc:
logger.debug(f"Removing additional transform constraints for bone: {bone_name}")
return _AT_ShadowBoneRemove(bone_name) return _AT_ShadowBoneRemove(bone_name)
return None return None
logger.debug(f"Setting up additional transform for bone: {bone_name} targeting {target_bone}")
shadow_bone = _AT_ShadowBoneCreate(bone_name, target_bone) shadow_bone = _AT_ShadowBoneCreate(bone_name, target_bone)
def __config(name: str, mute: bool, map_type: str, value: float) -> None: def __config(name, mute, map_type, value):
if mute: if mute:
remove_constraint(constraints, name) remove_constraint(constraints, name)
return return
c = TransformConstraintOp.create(constraints, name, map_type) c = TransformConstraintOp.create(constraints, name, map_type)
# FIXME: Some bones require specific rotation modes to match MMD behavior.
# Currently using hardcoded bone names as a temporary solution.
# See https://github.com/MMD-Blender/blender_mmd_tools_local/issues/242
if bone_name in {"左肩C", "右肩C", "肩C.L", "肩C.R", "肩C_L", "肩C_R"}:
c.from_rotation_mode = "ZYX" # Best matches MMD behavior for shoulder bones
c.target = p_bone.id_data c.target = p_bone.id_data
shadow_bone.add_constraint(c) shadow_bone.add_constraint(c)
TransformConstraintOp.update_min_max(c, value, influence) TransformConstraintOp.update_min_max(c, value, influence)
@@ -533,81 +468,62 @@ class FnBone:
return shadow_bone return shadow_bone
@staticmethod @staticmethod
def update_additional_transform_influence(pose_bone: PoseBone) -> None: def update_additional_transform_influence(pose_bone: bpy.types.PoseBone):
"""Update the influence of additional transform constraints"""
influence = pose_bone.mmd_bone.additional_transform_influence influence = pose_bone.mmd_bone.additional_transform_influence
constraints = pose_bone.constraints constraints = pose_bone.constraints
c = constraints.get("mmd_additional_rotation", None) c = constraints.get("mmd_additional_rotation", None)
TransformConstraintOp.update_min_max(c, math.pi, influence) TransformConstraintOp.update_min_max(c, math.pi, influence)
c = constraints.get("mmd_additional_location", None) c = constraints.get("mmd_additional_location", None)
TransformConstraintOp.update_min_max(c, 100, influence) TransformConstraintOp.update_min_max(c, 100, influence)
logger.debug(f"Updated additional transform influence for bone: {pose_bone.name} to {influence}")
class MigrationFnBone: class MigrationFnBone:
"""Migration Functions for old MMD models broken by bugs or issues""" """Migration Functions for old MMD models broken by bugs or issues"""
@staticmethod @staticmethod
def fix_mmd_ik_limit_override(armature_object: Object) -> None: def fix_mmd_ik_limit_override(armature_object: bpy.types.Object):
"""Fix IK limit override constraints in old MMD models""" pose_bone: bpy.types.PoseBone
logger.info(f"Fixing MMD IK limit overrides for {armature_object.name}")
pose_bone: PoseBone
for pose_bone in armature_object.pose.bones: for pose_bone in armature_object.pose.bones:
constraint: Constraint constraint: bpy.types.Constraint
for constraint in pose_bone.constraints: for constraint in pose_bone.constraints:
if constraint.type == "LIMIT_ROTATION" and "mmd_ik_limit_override" in constraint.name: if constraint.type == "LIMIT_ROTATION" and "mmd_ik_limit_override" in constraint.name:
constraint.owner_space = "LOCAL" constraint.owner_space = "LOCAL"
logger.debug(f"Fixed IK limit override for bone: {pose_bone.name}")
class _AT_ShadowBoneRemove: class _AT_ShadowBoneRemove:
"""Handler for removing shadow bones""" def __init__(self, bone_name):
def __init__(self, bone_name: str) -> None:
"""Initialize with bone name"""
self.__shadow_bone_names = ("_dummy_" + bone_name, "_shadow_" + bone_name) self.__shadow_bone_names = ("_dummy_" + bone_name, "_shadow_" + bone_name)
def update_edit_bones(self, edit_bones: bpy.types.ArmatureEditBones) -> None: def update_edit_bones(self, edit_bones):
"""Update edit bones by removing shadow bones"""
remove_edit_bones(edit_bones, self.__shadow_bone_names) remove_edit_bones(edit_bones, self.__shadow_bone_names)
logger.debug(f"Removed shadow bones: {self.__shadow_bone_names}")
def update_pose_bones(self, pose_bones: Any) -> None: def update_pose_bones(self, pose_bones):
"""Update pose bones (no-op for removal)"""
pass pass
class _AT_ShadowBoneCreate: class _AT_ShadowBoneCreate:
"""Handler for creating shadow bones""" def __init__(self, bone_name, target_bone_name):
def __init__(self, bone_name: str, target_bone_name: str) -> None:
"""Initialize with bone names"""
self.__dummy_bone_name = "_dummy_" + bone_name self.__dummy_bone_name = "_dummy_" + bone_name
self.__shadow_bone_name = "_shadow_" + bone_name self.__shadow_bone_name = "_shadow_" + bone_name
self.__bone_name = bone_name self.__bone_name = bone_name
self.__target_bone_name = target_bone_name self.__target_bone_name = target_bone_name
self.__constraint_pool: List[Constraint] = [] self.__constraint_pool = []
def __is_well_aligned(self, bone0: EditBone, bone1: EditBone) -> bool: def __is_well_aligned(self, bone0, bone1):
"""Check if two bones are well aligned"""
return bone0.x_axis.dot(bone1.x_axis) > 0.99 and bone0.y_axis.dot(bone1.y_axis) > 0.99 return bone0.x_axis.dot(bone1.x_axis) > 0.99 and bone0.y_axis.dot(bone1.y_axis) > 0.99
def __update_constraints(self, use_shadow: bool = True) -> None: def __update_constraints(self, use_shadow=True):
"""Update constraints to use shadow or target bone"""
subtarget = self.__shadow_bone_name if use_shadow else self.__target_bone_name subtarget = self.__shadow_bone_name if use_shadow else self.__target_bone_name
for c in self.__constraint_pool: for c in self.__constraint_pool:
c.subtarget = subtarget c.subtarget = subtarget
def add_constraint(self, constraint: Constraint) -> None: def add_constraint(self, constraint):
"""Add a constraint to the pool"""
self.__constraint_pool.append(constraint) self.__constraint_pool.append(constraint)
def update_edit_bones(self, edit_bones: bpy.types.ArmatureEditBones) -> None: def update_edit_bones(self, edit_bones):
"""Update edit bones by creating shadow bones"""
bone = edit_bones[self.__bone_name] bone = edit_bones[self.__bone_name]
target_bone = edit_bones[self.__target_bone_name] target_bone = edit_bones[self.__target_bone_name]
if bone != target_bone and self.__is_well_aligned(bone, target_bone): if bone != target_bone and self.__is_well_aligned(bone, target_bone):
logger.debug(f"Bones are well aligned, removing shadow bones for {self.__bone_name}")
_AT_ShadowBoneRemove(self.__bone_name).update_edit_bones(edit_bones) _AT_ShadowBoneRemove(self.__bone_name).update_edit_bones(edit_bones)
return return
@@ -617,7 +533,6 @@ class _AT_ShadowBoneCreate:
dummy.head = target_bone.head dummy.head = target_bone.head
dummy.tail = dummy.head + bone.tail - bone.head dummy.tail = dummy.head + bone.tail - bone.head
dummy.roll = bone.roll dummy.roll = bone.roll
logger.debug(f"Created/updated dummy bone: {dummy_bone_name}")
shadow_bone_name = self.__shadow_bone_name shadow_bone_name = self.__shadow_bone_name
shadow = edit_bones.get(shadow_bone_name, None) or FnBone.set_edit_bone_to_shadow(edit_bones.new(name=shadow_bone_name)) shadow = edit_bones.get(shadow_bone_name, None) or FnBone.set_edit_bone_to_shadow(edit_bones.new(name=shadow_bone_name))
@@ -625,12 +540,9 @@ class _AT_ShadowBoneCreate:
shadow.head = dummy.head shadow.head = dummy.head
shadow.tail = dummy.tail shadow.tail = dummy.tail
shadow.roll = bone.roll shadow.roll = bone.roll
logger.debug(f"Created/updated shadow bone: {shadow_bone_name}")
def update_pose_bones(self, pose_bones: Any) -> None: def update_pose_bones(self, pose_bones):
"""Update pose bones by setting up shadow bone properties"""
if self.__shadow_bone_name not in pose_bones: if self.__shadow_bone_name not in pose_bones:
logger.debug(f"Shadow bone {self.__shadow_bone_name} not found, using target bone directly")
self.__update_constraints(use_shadow=False) self.__update_constraints(use_shadow=False)
return return
@@ -649,7 +561,5 @@ class _AT_ShadowBoneCreate:
c.subtarget = dummy_p_bone.name c.subtarget = dummy_p_bone.name
c.target_space = "POSE" c.target_space = "POSE"
c.owner_space = "POSE" c.owner_space = "POSE"
logger.debug(f"Created copy transforms constraint for shadow bone: {self.__shadow_bone_name}")
self.__update_constraints() self.__update_constraints()
logger.debug(f"Updated constraints for shadow bone: {self.__shadow_bone_name}")
+39 -114
View File
@@ -1,25 +1,18 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors # Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender # This file is part of MMD Tools.
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import math import math
from typing import Optional, List, Tuple, Callable, Any, Union from typing import Optional
import bpy import bpy
from bpy.types import Object, ID, Camera, Context from mathutils import Matrix, Vector
from mathutils import Vector, Matrix, Euler
import traceback
from ..bpyutils import FnContext, Props from ..bpyutils import FnContext, Props
from ....core.logging_setup import logger
class FnCamera: class FnCamera:
@staticmethod @staticmethod
def find_root(obj: Optional[Object]) -> Optional[Object]: def find_root(obj: bpy.types.Object) -> Optional[bpy.types.Object]:
"""Find the root object of an MMD camera setup."""
if obj is None: if obj is None:
return None return None
if FnCamera.is_mmd_camera_root(obj): if FnCamera.is_mmd_camera_root(obj):
@@ -29,22 +22,16 @@ class FnCamera:
return None return None
@staticmethod @staticmethod
def is_mmd_camera(obj: Object) -> bool: def is_mmd_camera(obj: bpy.types.Object) -> bool:
"""Check if an object is an MMD camera."""
return obj.type == "CAMERA" and FnCamera.find_root(obj.parent) is not None return obj.type == "CAMERA" and FnCamera.find_root(obj.parent) is not None
@staticmethod @staticmethod
def is_mmd_camera_root(obj: Object) -> bool: def is_mmd_camera_root(obj: bpy.types.Object) -> bool:
"""Check if an object is an MMD camera root."""
return obj.type == "EMPTY" and obj.mmd_type == "CAMERA" return obj.type == "EMPTY" and obj.mmd_type == "CAMERA"
@staticmethod @staticmethod
def add_drivers(camera_object: Object) -> None: def add_drivers(camera_object: bpy.types.Object):
"""Add drivers to the camera object for MMD camera functionality.""" def __add_driver(id_data: bpy.types.ID, data_path: str, expression: str, index: int = -1):
logger.debug(f"Adding drivers to camera: {camera_object.name}")
def __add_driver(id_data: ID, data_path: str, expression: str, index: int = -1) -> None:
"""Add a driver to the specified ID data."""
d = id_data.driver_add(data_path, index).driver d = id_data.driver_add(data_path, index).driver
d.type = "SCRIPTED" d.type = "SCRIPTED"
if "$empty_distance" in expression: if "$empty_distance" in expression:
@@ -72,46 +59,31 @@ class FnCamera:
v.targets[0].data_path = "mmd_camera.angle" v.targets[0].data_path = "mmd_camera.angle"
expression = expression.replace("$angle", v.name) expression = expression.replace("$angle", v.name)
if "$sensor_height" in expression: if "$sensor_height" in expression:
v = d.variables.new() # Use fixed sensor_height instead of dynamic reference.
v.name = "sensor_height" # When controlled by MMD angle, sensor_height shouldn't change.
v.type = "SINGLE_PROP" # This avoids unnecessary dependency cycles.
v.targets[0].id_type = "CAMERA" # Reference: https://github.com/MMD-Blender/blender_mmd_tools_local/issues/227
v.targets[0].id = camera_object.data current_sensor_height = camera_object.data.sensor_height
v.targets[0].data_path = "sensor_height" expression = expression.replace("$sensor_height", str(current_sensor_height))
expression = expression.replace("$sensor_height", v.name)
d.expression = expression d.expression = expression
try:
__add_driver(camera_object.data, "ortho_scale", "25*abs($empty_distance)/45") __add_driver(camera_object.data, "ortho_scale", "25*abs($empty_distance)/45")
__add_driver(camera_object, "rotation_euler", "pi if $is_perspective == False and $empty_distance > 1e-5 else 0", index=1) __add_driver(camera_object, "rotation_euler", "pi if $is_perspective == False and $empty_distance > 1e-5 else 0", index=1)
__add_driver(camera_object.data, "type", "not $is_perspective") __add_driver(camera_object.data, "type", "not $is_perspective")
__add_driver(camera_object.data, "lens", "$sensor_height/tan($angle/2)/2") __add_driver(camera_object.data, "lens", "$sensor_height/tan($angle/2)/2")
logger.debug(f"Successfully added drivers to camera: {camera_object.name}")
except Exception:
logger.error(f"Failed to add drivers to camera {camera_object.name}: {traceback.format_exc()}")
@staticmethod @staticmethod
def remove_drivers(camera_object: Object) -> None: def remove_drivers(camera_object: bpy.types.Object):
"""Remove drivers from the camera object."""
logger.debug(f"Removing drivers from camera: {camera_object.name}")
try:
camera_object.data.driver_remove("ortho_scale") camera_object.data.driver_remove("ortho_scale")
camera_object.driver_remove("rotation_euler") camera_object.driver_remove("rotation_euler")
camera_object.data.driver_remove("ortho_scale") camera_object.data.driver_remove("type")
camera_object.data.driver_remove("lens") camera_object.data.driver_remove("lens")
logger.debug(f"Successfully removed drivers from camera: {camera_object.name}")
except Exception:
logger.error(f"Failed to remove drivers from camera {camera_object.name}: {traceback.format_exc()}")
class MigrationFnCamera: class MigrationFnCamera:
@staticmethod @staticmethod
def update_mmd_camera() -> None: def update_mmd_camera():
"""Update all MMD cameras in the scene."""
logger.info("Updating all MMD cameras in the scene")
updated_count = 0
for camera_object in bpy.data.objects: for camera_object in bpy.data.objects:
if camera_object.type != "CAMERA": if camera_object.type != "CAMERA":
continue continue
@@ -121,57 +93,39 @@ class MigrationFnCamera:
# It's not a MMD Camera # It's not a MMD Camera
continue continue
try:
FnCamera.remove_drivers(camera_object) FnCamera.remove_drivers(camera_object)
FnCamera.add_drivers(camera_object) FnCamera.add_drivers(camera_object)
updated_count += 1
except Exception:
logger.error(f"Failed to update MMD camera {camera_object.name}: {traceback.format_exc()}")
logger.info(f"Updated {updated_count} MMD cameras")
class MMDCamera: class MMDCamera:
def __init__(self, obj: Object): def __init__(self, obj):
"""Initialize an MMD camera."""
root_object = FnCamera.find_root(obj) root_object = FnCamera.find_root(obj)
if root_object is None: if root_object is None:
logger.error(f"Object {obj.name} is not an MMD camera") raise ValueError(f"{str(obj)} is not MMDCamera")
raise ValueError(f"{obj.name} is not an MMD camera")
self.__emptyObj = getattr(root_object, "original", obj) self.__emptyObj = getattr(root_object, "original", obj)
logger.debug(f"Initialized MMD camera with root: {self.__emptyObj.name}")
@staticmethod @staticmethod
def isMMDCamera(obj: Object) -> bool: def isMMDCamera(obj: bpy.types.Object) -> bool:
"""Check if an object is an MMD camera."""
return FnCamera.find_root(obj) is not None return FnCamera.find_root(obj) is not None
@staticmethod @staticmethod
def addDrivers(cameraObj: Object) -> None: def addDrivers(cameraObj: bpy.types.Object):
"""Add drivers to the camera object."""
FnCamera.add_drivers(cameraObj) FnCamera.add_drivers(cameraObj)
@staticmethod @staticmethod
def removeDrivers(cameraObj: Object) -> None: def removeDrivers(cameraObj: bpy.types.Object):
"""Remove drivers from the camera object. """
if cameraObj.type != "CAMERA": if cameraObj.type != "CAMERA":
return return
FnCamera.remove_drivers(cameraObj) FnCamera.remove_drivers(cameraObj)
@staticmethod @staticmethod
def convertToMMDCamera(cameraObj: Object, scale: float = 1.0) -> 'MMDCamera': def convertToMMDCamera(cameraObj: bpy.types.Object, scale=1.0):
"""Convert a camera to an MMD camera."""
logger.info(f"Converting camera {cameraObj.name} to MMD camera with scale {scale}")
if FnCamera.is_mmd_camera(cameraObj): if FnCamera.is_mmd_camera(cameraObj):
logger.debug(f"Camera {cameraObj.name} is already an MMD camera")
return MMDCamera(cameraObj) return MMDCamera(cameraObj)
try:
empty = bpy.data.objects.new(name="MMD_Camera", object_data=None) empty = bpy.data.objects.new(name="MMD_Camera", object_data=None)
context = FnContext.ensure_context() FnContext.link_object(FnContext.ensure_context(), empty)
FnContext.link_object(context, empty)
cameraObj.parent = empty cameraObj.parent = empty
cameraObj.data.sensor_fit = "VERTICAL" cameraObj.data.sensor_fit = "VERTICAL"
@@ -195,50 +149,34 @@ class MMDCamera:
empty.mmd_type = "CAMERA" empty.mmd_type = "CAMERA"
empty.mmd_camera.angle = math.radians(30) empty.mmd_camera.angle = math.radians(30)
empty.mmd_camera.persp = True empty.mmd_camera.persp = True
logger.info(f"Successfully converted {cameraObj.name} to MMD camera")
return MMDCamera(empty) return MMDCamera(empty)
except Exception:
logger.error(f"Failed to convert camera {cameraObj.name} to MMD camera: {traceback.format_exc()}")
raise
@staticmethod @staticmethod
def newMMDCameraAnimation( def newMMDCameraAnimation(cameraObj, cameraTarget=None, scale=1.0, min_distance=0.1):
cameraObj: Optional[Object],
cameraTarget: Optional[Object] = None,
scale: float = 1.0,
min_distance: float = 0.1
) -> 'MMDCamera':
"""Create a new MMD camera animation."""
logger.info(f"Creating new MMD camera animation with scale {scale}")
try:
scene = bpy.context.scene scene = bpy.context.scene
mmd_cam = bpy.data.objects.new(name="Camera", object_data=bpy.data.cameras.new("Camera")) mmd_cam = bpy.data.objects.new(name="Camera", object_data=bpy.data.cameras.new("Camera"))
FnContext.link_object(FnContext.ensure_context(), mmd_cam) FnContext.link_object(FnContext.ensure_context(), mmd_cam)
MMDCamera.convertToMMDCamera(mmd_cam, scale=scale) MMDCamera.convertToMMDCamera(mmd_cam, scale=scale)
mmd_cam_root = mmd_cam.parent mmd_cam_root = mmd_cam.parent
_camera_override_func: Optional[Callable[[], Object]] = None _camera_override_func = None
if cameraObj is None: if cameraObj is None:
if scene.camera is None: if scene.camera is None:
scene.camera = mmd_cam scene.camera = mmd_cam
logger.debug("Set scene camera to new MMD camera")
return MMDCamera(mmd_cam_root) return MMDCamera(mmd_cam_root)
_camera_override_func = lambda: scene.camera def _camera_override_func():
return scene.camera
_target_override_func: Optional[Callable[[Object], Object]] = None _target_override_func = None
if cameraTarget is None: if cameraTarget is None:
_target_override_func = lambda camObj: camObj.data.dof.focus_object or camObj def _target_override_func(camObj):
return camObj.data.dof.focus_object or camObj
action_name = mmd_cam_root.name action_name = mmd_cam_root.name
parent_action = bpy.data.actions.new(name=action_name) parent_action = bpy.data.actions.new(name=action_name)
distance_action = bpy.data.actions.new(name=action_name + "_dis") distance_action = bpy.data.actions.new(name=action_name + "_dis")
FnCamera.remove_drivers(mmd_cam) FnCamera.remove_drivers(mmd_cam)
from math import atan
from mathutils import Matrix, Vector
render = scene.render render = scene.render
factor = (render.resolution_y * render.pixel_aspect_y) / (render.resolution_x * render.pixel_aspect_x) factor = (render.resolution_y * render.pixel_aspect_y) / (render.resolution_x * render.pixel_aspect_x)
matrix_rotation = Matrix(([1, 0, 0, 0], [0, 0, 1, 0], [0, -1, 0, 0], [0, 0, 0, 1])) matrix_rotation = Matrix(([1, 0, 0, 0], [0, 0, 1, 0], [0, -1, 0, 0], [0, 0, 0, 1]))
@@ -247,19 +185,15 @@ class MMDCamera:
frame_count = frame_end - frame_start frame_count = frame_end - frame_start
frames = range(frame_start, frame_end) frames = range(frame_start, frame_end)
fcurves = [] fcurves = [parent_action.fcurves.new(data_path="location", index=i) for i in range(3)] # x, y, z
for i in range(3): fcurves.extend(parent_action.fcurves.new(data_path="rotation_euler", index=i) for i in range(3)) # rx, ry, rz
fcurves.append(parent_action.fcurves.new(data_path="location", index=i)) # x, y, z
for i in range(3):
fcurves.append(parent_action.fcurves.new(data_path="rotation_euler", index=i)) # rx, ry, rz
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.angle")) # fov fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.angle")) # fov
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.is_perspective")) # persp fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.is_perspective")) # persp
fcurves.append(distance_action.fcurves.new(data_path="location", index=1)) # dis fcurves.append(distance_action.fcurves.new(data_path="location", index=1)) # dis
for c in fcurves: for c in fcurves:
c.keyframe_points.add(frame_count) c.keyframe_points.add(frame_count)
logger.debug(f"Processing {frame_count} frames for camera animation") for f, x, y, z, rx, ry, rz, fov, persp, dis in zip(frames, *(c.keyframe_points for c in fcurves), strict=False):
for f, x, y, z, rx, ry, rz, fov, persp, dis in zip(frames, *(c.keyframe_points for c in fcurves)):
scene.frame_set(f) scene.frame_set(f)
if _camera_override_func: if _camera_override_func:
cameraObj = _camera_override_func() cameraObj = _camera_override_func()
@@ -292,7 +226,7 @@ class MMDCamera:
x.co, y.co, z.co = ((f, i) for i in cam_target_loc) x.co, y.co, z.co = ((f, i) for i in cam_target_loc)
rx.co, ry.co, rz.co = ((f, i) for i in cam_rotation) rx.co, ry.co, rz.co = ((f, i) for i in cam_rotation)
dis.co = (f, cam_dis) dis.co = (f, cam_dis)
fov.co = (f, 2 * atan(tan_val)) fov.co = (f, 2 * math.atan(tan_val))
persp.co = (f, cameraObj.data.type != "ORTHO") persp.co = (f, cameraObj.data.type != "ORTHO")
persp.interpolation = "CONSTANT" persp.interpolation = "CONSTANT"
for kp in (x, y, z, rx, ry, rz, fov, dis): for kp in (x, y, z, rx, ry, rz, fov, dis):
@@ -302,22 +236,13 @@ class MMDCamera:
mmd_cam_root.animation_data_create().action = parent_action mmd_cam_root.animation_data_create().action = parent_action
mmd_cam.animation_data_create().action = distance_action mmd_cam.animation_data_create().action = distance_action
scene.frame_set(frame_current) scene.frame_set(frame_current)
logger.info(f"Successfully created MMD camera animation with {frame_count} frames")
return MMDCamera(mmd_cam_root) return MMDCamera(mmd_cam_root)
except Exception: def object(self):
logger.error(f"Failed to create MMD camera animation: {traceback.format_exc()}")
raise
def object(self) -> Object:
"""Get the root object of the MMD camera."""
return self.__emptyObj return self.__emptyObj
def camera(self) -> Object: def camera(self):
"""Get the camera object of the MMD camera."""
for i in self.__emptyObj.children: for i in self.__emptyObj.children:
if i.type == "CAMERA": if i.type == "CAMERA":
return i return i
logger.error(f"No camera found for MMD camera root {self.__emptyObj.name}") raise KeyError
raise KeyError(f"No camera found for MMD camera root {self.__emptyObj.name}")
+5 -7
View File
@@ -1,14 +1,12 @@
# -*- coding: utf-8 -*- # Copyright 2016 MMD Tools authors
# Copyright 2014 MMD Tools authors # This file is part of MMD Tools.
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools # Module for custom exceptions
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
class MaterialNotFoundError(KeyError): class MaterialNotFoundError(KeyError):
"""Exception raised when a material is not found in the scene""" """Exception raised when a material is not found in the scene"""
def __init__(self, *args: object) -> None: def __init__(self, *args: object) -> None:
"""Constructor for MaterialNotFoundError""" """Initialize MaterialNotFoundError"""
super().__init__(*args) super().__init__(*args)
+14 -35
View File
@@ -1,53 +1,37 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors # Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender # This file is part of MMD Tools.
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import bpy import bpy
from typing import Optional, Union, Any, List, Tuple
from bpy.types import Object, Context
from ..bpyutils import FnContext, Props from ..bpyutils import FnContext, Props
from ....core.logging_setup import logger
class MMDLamp: class MMDLamp:
def __init__(self, obj: Object) -> None: def __init__(self, obj):
if MMDLamp.isLamp(obj): if MMDLamp.isLamp(obj):
obj = obj.parent obj = obj.parent
if obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT": if obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT":
self.__emptyObj: Object = obj self.__emptyObj = obj
else: else:
error_msg = f"{str(obj)} is not MMDLamp" raise ValueError(f"{str(obj)} is not MMDLamp")
logger.error(error_msg)
raise ValueError(error_msg)
@staticmethod @staticmethod
def isLamp(obj: Optional[Object]) -> bool: def isLamp(obj):
"""Check if the object is a lamp/light object""" return obj and obj.type in {"LIGHT", "LAMP"}
return obj is not None and obj.type in {"LIGHT", "LAMP"}
@staticmethod @staticmethod
def isMMDLamp(obj: Optional[Object]) -> bool: def isMMDLamp(obj):
"""Check if the object is an MMD lamp"""
if MMDLamp.isLamp(obj): if MMDLamp.isLamp(obj):
obj = obj.parent obj = obj.parent
return obj is not None and obj.type == "EMPTY" and obj.mmd_type == "LIGHT" return obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT"
@staticmethod @staticmethod
def convertToMMDLamp(lampObj: Object, scale: float = 1.0) -> 'MMDLamp': def convertToMMDLamp(lampObj, scale=1.0):
"""Convert a regular lamp to an MMD lamp"""
if MMDLamp.isMMDLamp(lampObj): if MMDLamp.isMMDLamp(lampObj):
logger.debug(f"Object {lampObj.name} is already an MMD lamp")
return MMDLamp(lampObj) return MMDLamp(lampObj)
logger.info(f"Converting {lampObj.name} to MMD lamp with scale {scale}") empty = bpy.data.objects.new(name="MMD_Light", object_data=None)
FnContext.link_object(FnContext.ensure_context(), empty)
empty: Object = bpy.data.objects.new(name="MMD_Light", object_data=None)
context = FnContext.ensure_context()
FnContext.link_object(context, empty)
empty.rotation_mode = "XYZ" empty.rotation_mode = "XYZ"
empty.lock_rotation = (True, True, True) empty.lock_rotation = (True, True, True)
@@ -69,18 +53,13 @@ class MMDLamp:
constraint.track_axis = "TRACK_NEGATIVE_Z" constraint.track_axis = "TRACK_NEGATIVE_Z"
constraint.up_axis = "UP_Y" constraint.up_axis = "UP_Y"
logger.debug(f"Successfully created MMD lamp from {lampObj.name}")
return MMDLamp(empty) return MMDLamp(empty)
def object(self) -> Object: def object(self):
"""Get the empty object that represents this MMD lamp"""
return self.__emptyObj return self.__emptyObj
def lamp(self) -> Object: def lamp(self):
"""Get the actual lamp/light object"""
for i in self.__emptyObj.children: for i in self.__emptyObj.children:
if MMDLamp.isLamp(i): if MMDLamp.isLamp(i):
return i return i
error_msg = f"No lamp found in MMD lamp {self.__emptyObj.name}" raise KeyError
logger.error(error_msg)
raise KeyError(error_msg)
+89 -154
View File
@@ -1,13 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors # Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender # This file is part of MMD Tools.
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import logging from ....core.logging_setup import logger
import os import os
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, cast, Dict, List, Any, Union, Set from pathlib import Path
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, cast
import bpy import bpy
from mathutils import Vector from mathutils import Vector
@@ -15,7 +12,6 @@ from mathutils import Vector
from ..bpyutils import FnContext from ..bpyutils import FnContext
from .exceptions import MaterialNotFoundError from .exceptions import MaterialNotFoundError
from .shader import _NodeGroupUtils from .shader import _NodeGroupUtils
from ....core.logging_setup import logger
if TYPE_CHECKING: if TYPE_CHECKING:
from ..properties.material import MMDMaterial from ..properties.material import MMDMaterial
@@ -28,55 +24,51 @@ SPHERE_MODE_SUBTEX = 3
class _DummyTexture: class _DummyTexture:
def __init__(self, image: bpy.types.Image): def __init__(self, image):
self.type: str = "IMAGE" self.type = "IMAGE"
self.image: bpy.types.Image = image self.image = image
self.use_mipmap: bool = True self.use_mipmap = True
class _DummyTextureSlot: class _DummyTextureSlot:
def __init__(self, image: bpy.types.Image): def __init__(self, image):
self.diffuse_color_factor: float = 1 self.diffuse_color_factor = 1
self.uv_layer: str = "" self.uv_layer = ""
self.texture: _DummyTexture = _DummyTexture(image) self.texture = _DummyTexture(image)
class FnMaterial: class FnMaterial:
__NODES_ARE_READONLY: bool = False __NODES_ARE_READONLY: bool = False
def __init__(self, material: bpy.types.Material): def __init__(self, material: bpy.types.Material):
self.__material: bpy.types.Material = material self.__material = material
self._nodes_are_readonly: bool = FnMaterial.__NODES_ARE_READONLY self._nodes_are_readonly = FnMaterial.__NODES_ARE_READONLY
@staticmethod @staticmethod
def set_nodes_are_readonly(nodes_are_readonly: bool) -> None: def set_nodes_are_readonly(nodes_are_readonly: bool):
FnMaterial.__NODES_ARE_READONLY = nodes_are_readonly FnMaterial.__NODES_ARE_READONLY = nodes_are_readonly
@classmethod @classmethod
def from_material_id(cls, material_id: str) -> Optional['FnMaterial']: def from_material_id(cls, material_id: int):
for material in bpy.data.materials: for material in bpy.data.materials:
if material.mmd_material.material_id == material_id: if material.mmd_material.material_id == material_id:
return cls(material) return cls(material)
return None return None
@staticmethod @staticmethod
def clean_materials(obj: bpy.types.Object, can_remove: Callable[[bpy.types.Material], bool]) -> None: def clean_materials(obj, can_remove: Callable[[bpy.types.Material], bool]):
materials = obj.data.materials materials = obj.data.materials
materials_pop = materials.pop materials_pop = materials.pop
removed_count = 0
for i in sorted((x for x, m in enumerate(materials) if can_remove(m)), reverse=True): for i in sorted((x for x, m in enumerate(materials) if can_remove(m)), reverse=True):
m = materials_pop(index=i) m = materials_pop(index=i)
removed_count += 1
if m.users < 1: if m.users < 1:
bpy.data.materials.remove(m) bpy.data.materials.remove(m)
if removed_count > 0:
logger.debug(f"Removed {removed_count} materials from {obj.name}")
@staticmethod @staticmethod
def swap_materials(mesh_object: bpy.types.Object, mat1_ref: Union[str, int], mat2_ref: Union[str, int], reverse: bool = False, swap_slots: bool = False) -> Tuple[bpy.types.Material, bpy.types.Material]: def swap_materials(mesh_object: bpy.types.Object, mat1_ref: str | int, mat2_ref: str | int, reverse=False, swap_slots=False) -> Tuple[bpy.types.Material, bpy.types.Material]:
""" """
This method will assign the polygons of mat1 to mat2. Assign the polygons of mat1 to mat2.
If reverse is True it will also swap the polygons assigned to mat2 to mat1. If reverse is True it will also swap the polygons assigned to mat2 to mat1.
The reference to materials can be indexes or names The reference to materials can be indexes or names
Finally it will also swap the material slots if the option is given. Finally it will also swap the material slots if the option is given.
@@ -94,22 +86,18 @@ class FnMaterial:
Raises: Raises:
MaterialNotFoundError: If one of the materials is not found MaterialNotFoundError: If one of the materials is not found
""" """
mesh = cast(bpy.types.Mesh, mesh_object.data) mesh = cast("bpy.types.Mesh", mesh_object.data)
try: try:
# Try to find the materials # Try to find the materials
mat1 = mesh.materials[mat1_ref] mat1 = mesh.materials[mat1_ref]
mat2 = mesh.materials[mat2_ref] mat2 = mesh.materials[mat2_ref]
if None in (mat1, mat2): if None in {mat1, mat2}:
raise MaterialNotFoundError() raise MaterialNotFoundError
except (KeyError, IndexError) as exc: except (KeyError, IndexError) as exc:
# Wrap exceptions within our custom ones # Wrap exceptions within our custom ones
raise MaterialNotFoundError() from exc raise MaterialNotFoundError from exc
mat1_idx = mesh.materials.find(mat1.name) mat1_idx = mesh.materials.find(mat1.name)
mat2_idx = mesh.materials.find(mat2.name) mat2_idx = mesh.materials.find(mat2.name)
logger.debug(f"Swapping materials: {mat1.name} (idx:{mat1_idx}) <-> {mat2.name} (idx:{mat2_idx}) in {mesh_object.name}")
# Swap polygons # Swap polygons
for poly in mesh.polygons: for poly in mesh.polygons:
if poly.material_index == mat1_idx: if poly.material_index == mat1_idx:
@@ -123,37 +111,31 @@ class FnMaterial:
return mat1, mat2 return mat1, mat2
@staticmethod @staticmethod
def fixMaterialOrder(meshObj: bpy.types.Object, material_names: Iterable[str]) -> None: def fixMaterialOrder(meshObj: bpy.types.Object, material_names: Iterable[str]):
""" """Fix the material order which is lost after joining meshes."""
This method will fix the material order. Which is lost after joining meshes. materials = cast("bpy.types.Mesh", meshObj.data).materials
"""
materials = cast(bpy.types.Mesh, meshObj.data).materials
logger.debug(f"Fixing material order for {meshObj.name}")
for new_idx, mat in enumerate(material_names): for new_idx, mat in enumerate(material_names):
# Get the material that is currently on this index # Get the material that is currently on this index
other_mat = materials[new_idx] other_mat = materials[new_idx]
if other_mat.name == mat: if other_mat.name == mat:
continue # This is already in place continue # This is already in place
logger.debug(f"Moving material {mat} to index {new_idx}")
FnMaterial.swap_materials(meshObj, mat, new_idx, reverse=True, swap_slots=True) FnMaterial.swap_materials(meshObj, mat, new_idx, reverse=True, swap_slots=True)
@property @property
def material_id(self) -> int: def material_id(self):
mmd_mat: 'MMDMaterial' = self.__material.mmd_material mmd_mat: MMDMaterial = self.__material.mmd_material
if mmd_mat.material_id < 0: if mmd_mat.material_id < 0:
max_id = -1 max_id = -1
for mat in bpy.data.materials: for mat in bpy.data.materials:
max_id = max(max_id, mat.mmd_material.material_id) max_id = max(max_id, mat.mmd_material.material_id)
mmd_mat.material_id = max_id + 1 mmd_mat.material_id = max_id + 1
logger.debug(f"Assigned new material ID {mmd_mat.material_id} to {self.__material.name}")
return mmd_mat.material_id return mmd_mat.material_id
@property @property
def material(self) -> bpy.types.Material: def material(self):
return self.__material return self.__material
def __same_image_file(self, image: Optional[bpy.types.Image], filepath: str) -> bool: def __same_image_file(self, image, filepath):
if image and image.source == "FILE": if image and image.source == "FILE":
# pylint: disable=assignment-from-no-return # pylint: disable=assignment-from-no-return
img_filepath = bpy.path.abspath(image.filepath) # image.filepath_from_user() img_filepath = bpy.path.abspath(image.filepath) # image.filepath_from_user()
@@ -162,19 +144,18 @@ class FnMaterial:
# pylint: disable=bare-except # pylint: disable=bare-except
try: try:
return os.path.samefile(img_filepath, filepath) return os.path.samefile(img_filepath, filepath)
except: except Exception as e:
pass logger.warning(f"Failed to compare files '{img_filepath}' and '{filepath}': {e}")
return False return False
def _load_image(self, filepath: str) -> bpy.types.Image: def _load_image(self, filepath):
img = next((i for i in bpy.data.images if self.__same_image_file(i, filepath)), None) img = next((i for i in bpy.data.images if self.__same_image_file(i, filepath)), None)
if img is None: if img is None:
# pylint: disable=bare-except # pylint: disable=bare-except
try: try:
logger.debug(f"Loading image: {filepath}")
img = bpy.data.images.load(filepath) img = bpy.data.images.load(filepath)
except: except Exception:
logger.warning(f"Cannot create a texture for {filepath}. No such file.") logger.warning("Cannot create a texture for %s. No such file.", filepath)
img = bpy.data.images.new(os.path.basename(filepath), 1, 1) img = bpy.data.images.new(os.path.basename(filepath), 1, 1)
img.source = "FILE" img.source = "FILE"
img.filepath = filepath img.filepath = filepath
@@ -185,46 +166,43 @@ class FnMaterial:
img.alpha_mode = "NONE" img.alpha_mode = "NONE"
return img return img
def update_toon_texture(self) -> None: def update_toon_texture(self):
if self._nodes_are_readonly: if self._nodes_are_readonly:
return return
mmd_mat: 'MMDMaterial' = self.__material.mmd_material mmd_mat: MMDMaterial = self.__material.mmd_material
if mmd_mat.is_shared_toon_texture: if mmd_mat.is_shared_toon_texture:
shared_toon_folder = FnContext.get_addon_preferences_attribute(FnContext.ensure_context(), "shared_toon_folder", "") shared_toon_folder = FnContext.get_addon_preferences_attribute(FnContext.ensure_context(), "shared_toon_folder", "")
toon_path = os.path.join(shared_toon_folder, "toon%02d.bmp" % (mmd_mat.shared_toon_texture + 1)) toon_path = os.path.join(shared_toon_folder, "toon%02d.bmp" % (mmd_mat.shared_toon_texture + 1))
logger.debug(f"Using shared toon texture: {toon_path}") self.create_toon_texture(str(Path(toon_path).resolve()))
self.create_toon_texture(bpy.path.resolve_ncase(path=toon_path))
elif mmd_mat.toon_texture != "": elif mmd_mat.toon_texture != "":
logger.debug(f"Using custom toon texture: {mmd_mat.toon_texture}")
self.create_toon_texture(mmd_mat.toon_texture) self.create_toon_texture(mmd_mat.toon_texture)
else: else:
logger.debug(f"Removing toon texture from {self.__material.name}")
self.remove_toon_texture() self.remove_toon_texture()
def _mix_diffuse_and_ambient(self, mmd_mat: 'MMDMaterial') -> List[float]: def _mix_diffuse_and_ambient(self, mmd_mat):
r, g, b = mmd_mat.diffuse_color r, g, b = mmd_mat.diffuse_color
ar, ag, ab = mmd_mat.ambient_color ar, ag, ab = mmd_mat.ambient_color
return [min(1.0, 0.5 * r + ar), min(1.0, 0.5 * g + ag), min(1.0, 0.5 * b + ab)] return [min(1.0, 0.5 * r + ar), min(1.0, 0.5 * g + ag), min(1.0, 0.5 * b + ab)]
def update_drop_shadow(self) -> None: def update_drop_shadow(self):
pass pass
def update_enabled_toon_edge(self) -> None: def update_enabled_toon_edge(self):
if self._nodes_are_readonly: if self._nodes_are_readonly:
return return
self.update_edge_color() self.update_edge_color()
def update_edge_color(self) -> None: def update_edge_color(self):
if self._nodes_are_readonly: if self._nodes_are_readonly:
return return
mat = self.__material mat = self.__material
mmd_mat: 'MMDMaterial' = mat.mmd_material mmd_mat: MMDMaterial = mat.mmd_material
color, alpha = mmd_mat.edge_color[:3], mmd_mat.edge_color[3] color, alpha = mmd_mat.edge_color[:3], mmd_mat.edge_color[3]
line_color = color + (min(alpha, int(mmd_mat.enabled_toon_edge)),) line_color = color + (min(alpha, int(mmd_mat.enabled_toon_edge)),)
if hasattr(mat, "line_color"): # freestyle line color if hasattr(mat, "line_color"): # freestyle line color
mat.line_color = line_color mat.line_color = line_color
mat_edge: Optional[bpy.types.Material] = bpy.data.materials.get("mmd_edge." + mat.name, None) mat_edge: bpy.types.Material = bpy.data.materials.get("mmd_edge." + mat.name, None)
if mat_edge: if mat_edge:
mat_edge.mmd_material.edge_color = line_color mat_edge.mmd_material.edge_color = line_color
@@ -236,51 +214,44 @@ class FnMaterial:
if node_shader and "Alpha" in node_shader.inputs: if node_shader and "Alpha" in node_shader.inputs:
node_shader.inputs["Alpha"].default_value = alpha node_shader.inputs["Alpha"].default_value = alpha
logger.debug(f"Updated edge color for {mat.name}") def update_edge_weight(self):
def update_edge_weight(self) -> None:
pass pass
def get_texture(self) -> Optional[_DummyTexture]: def get_texture(self):
return self.__get_texture_node("mmd_base_tex", use_dummy=True) return self.__get_texture_node("mmd_base_tex", use_dummy=True)
def create_texture(self, filepath: str) -> _DummyTextureSlot: def create_texture(self, filepath):
texture = self.__create_texture_node("mmd_base_tex", filepath, (-4, -1)) texture = self.__create_texture_node("mmd_base_tex", filepath, (-4, -1))
logger.debug(f"Created base texture for {self.__material.name}: {filepath}")
return _DummyTextureSlot(texture.image) return _DummyTextureSlot(texture.image)
def remove_texture(self) -> None: def remove_texture(self):
if self._nodes_are_readonly: if self._nodes_are_readonly:
return return
logger.debug(f"Removing base texture from {self.__material.name}")
self.__remove_texture_node("mmd_base_tex") self.__remove_texture_node("mmd_base_tex")
def get_sphere_texture(self) -> Optional[_DummyTexture]: def get_sphere_texture(self):
return self.__get_texture_node("mmd_sphere_tex", use_dummy=True) return self.__get_texture_node("mmd_sphere_tex", use_dummy=True)
def use_sphere_texture(self, use_sphere: bool, obj: Optional[bpy.types.Object] = None) -> None: def use_sphere_texture(self, use_sphere, obj=None):
if self._nodes_are_readonly: if self._nodes_are_readonly:
return return
if use_sphere: if use_sphere:
logger.debug(f"Enabling sphere texture for {self.__material.name}")
self.update_sphere_texture_type(obj) self.update_sphere_texture_type(obj)
else: else:
logger.debug(f"Disabling sphere texture for {self.__material.name}")
self.__update_shader_input("Sphere Tex Fac", 0) self.__update_shader_input("Sphere Tex Fac", 0)
def create_sphere_texture(self, filepath: str, obj: Optional[bpy.types.Object] = None) -> _DummyTextureSlot: def create_sphere_texture(self, filepath, obj=None):
texture = self.__create_texture_node("mmd_sphere_tex", filepath, (-2, -2)) texture = self.__create_texture_node("mmd_sphere_tex", filepath, (-2, -2))
logger.debug(f"Created sphere texture for {self.__material.name}: {filepath}")
self.update_sphere_texture_type(obj) self.update_sphere_texture_type(obj)
return _DummyTextureSlot(texture.image) return _DummyTextureSlot(texture.image)
def update_sphere_texture_type(self, obj: Optional[bpy.types.Object] = None) -> None: def update_sphere_texture_type(self, obj=None):
if self._nodes_are_readonly: if self._nodes_are_readonly:
return return
sphere_texture_type = int(self.material.mmd_material.sphere_texture_type) sphere_texture_type = int(self.material.mmd_material.sphere_texture_type)
is_sph_add = sphere_texture_type == 2 is_sph_add = sphere_texture_type == 2
if sphere_texture_type not in (1, 2, 3): if sphere_texture_type not in {1, 2, 3}:
self.__update_shader_input("Sphere Tex Fac", 0) self.__update_shader_input("Sphere Tex Fac", 0)
else: else:
self.__update_shader_input("Sphere Tex Fac", 1) self.__update_shader_input("Sphere Tex Fac", 1)
@@ -298,62 +269,54 @@ class FnMaterial:
nodes, links = mat.node_tree.nodes, mat.node_tree.links nodes, links = mat.node_tree.nodes, mat.node_tree.links
if sphere_texture_type == 3: if sphere_texture_type == 3:
if obj and obj.type == "MESH" and mat in tuple(obj.data.materials): if obj and obj.type == "MESH" and mat in tuple(obj.data.materials):
uv_layers = (l for l in obj.data.uv_layers if not l.name.startswith("_")) uv_layers = (layer for layer in obj.data.uv_layers if not layer.name.startswith("_"))
next(uv_layers, None) # skip base UV next(uv_layers, None) # skip base UV
subtex_uv = getattr(next(uv_layers, None), "name", "") subtex_uv = getattr(next(uv_layers, None), "name", "")
if subtex_uv != "UV1": if subtex_uv != "UV1":
logger.info(f'Material({mat.name}): object "{obj.name}" use UV "{subtex_uv}" for SubTex') logger.info(' * material(%s): object "%s" use UV "%s" for SubTex', mat.name, obj.name, subtex_uv)
links.new(nodes["mmd_tex_uv"].outputs["SubTex UV"], texture.inputs["Vector"]) links.new(nodes["mmd_tex_uv"].outputs["SubTex UV"], texture.inputs["Vector"])
else: else:
links.new(nodes["mmd_tex_uv"].outputs["Sphere UV"], texture.inputs["Vector"]) links.new(nodes["mmd_tex_uv"].outputs["Sphere UV"], texture.inputs["Vector"])
logger.debug(f"Updated sphere texture type for {self.material.name}: {sphere_texture_type}") def remove_sphere_texture(self):
def remove_sphere_texture(self) -> None:
if self._nodes_are_readonly: if self._nodes_are_readonly:
return return
logger.debug(f"Removing sphere texture from {self.__material.name}")
self.__remove_texture_node("mmd_sphere_tex") self.__remove_texture_node("mmd_sphere_tex")
def get_toon_texture(self) -> Optional[_DummyTexture]: def get_toon_texture(self):
return self.__get_texture_node("mmd_toon_tex", use_dummy=True) return self.__get_texture_node("mmd_toon_tex", use_dummy=True)
def use_toon_texture(self, use_toon: bool) -> None: def use_toon_texture(self, use_toon):
if self._nodes_are_readonly: if self._nodes_are_readonly:
return return
logger.debug(f"{'Enabling' if use_toon else 'Disabling'} toon texture for {self.__material.name}")
self.__update_shader_input("Toon Tex Fac", use_toon) self.__update_shader_input("Toon Tex Fac", use_toon)
def create_toon_texture(self, filepath: str) -> _DummyTextureSlot: def create_toon_texture(self, filepath):
texture = self.__create_texture_node("mmd_toon_tex", filepath, (-3, -1.5)) texture = self.__create_texture_node("mmd_toon_tex", filepath, (-3, -1.5))
logger.debug(f"Created toon texture for {self.__material.name}: {filepath}")
return _DummyTextureSlot(texture.image) return _DummyTextureSlot(texture.image)
def remove_toon_texture(self) -> None: def remove_toon_texture(self):
if self._nodes_are_readonly: if self._nodes_are_readonly:
return return
logger.debug(f"Removing toon texture from {self.__material.name}")
self.__remove_texture_node("mmd_toon_tex") self.__remove_texture_node("mmd_toon_tex")
def __get_texture_node(self, node_name: str, use_dummy: bool = False) -> Optional[Union[bpy.types.ShaderNodeTexImage, _DummyTexture]]: def __get_texture_node(self, node_name, use_dummy=False):
mat = self.material mat = self.material
texture = getattr(mat.node_tree, "nodes", {}).get(node_name, None) texture = getattr(mat.node_tree, "nodes", {}).get(node_name, None)
if isinstance(texture, bpy.types.ShaderNodeTexImage): if isinstance(texture, bpy.types.ShaderNodeTexImage):
return _DummyTexture(texture.image) if use_dummy else texture return _DummyTexture(texture.image) if use_dummy else texture
return None return None
def __remove_texture_node(self, node_name: str) -> None: def __remove_texture_node(self, node_name):
mat = self.material mat = self.material
texture = getattr(mat.node_tree, "nodes", {}).get(node_name, None) texture = getattr(mat.node_tree, "nodes", {}).get(node_name, None)
if isinstance(texture, bpy.types.ShaderNodeTexImage): if isinstance(texture, bpy.types.ShaderNodeTexImage):
mat.node_tree.nodes.remove(texture) mat.node_tree.nodes.remove(texture)
mat.update_tag() mat.update_tag()
def __create_texture_node(self, node_name: str, filepath: str, pos: Tuple[float, float]) -> bpy.types.ShaderNodeTexImage: def __create_texture_node(self, node_name, filepath, pos):
texture = self.__get_texture_node(node_name) texture = self.__get_texture_node(node_name)
if texture is None: if texture is None:
from mathutils import Vector
self.__update_shader_nodes() self.__update_shader_nodes()
nodes = self.material.node_tree.nodes nodes = self.material.node_tree.nodes
texture = nodes.new("ShaderNodeTexImage") texture = nodes.new("ShaderNodeTexImage")
@@ -365,25 +328,23 @@ class FnMaterial:
self.__update_shader_nodes() self.__update_shader_nodes()
return texture return texture
def update_ambient_color(self) -> None: def update_ambient_color(self):
if self._nodes_are_readonly: if self._nodes_are_readonly:
return return
mat = self.material mat = self.material
mmd_mat = mat.mmd_material mmd_mat = mat.mmd_material
mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat) mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat)
self.__update_shader_input("Ambient Color", mmd_mat.ambient_color[:] + (1,)) self.__update_shader_input("Ambient Color", mmd_mat.ambient_color[:] + (1,))
logger.debug(f"Updated ambient color for {mat.name}")
def update_diffuse_color(self) -> None: def update_diffuse_color(self):
if self._nodes_are_readonly: if self._nodes_are_readonly:
return return
mat = self.material mat = self.material
mmd_mat = mat.mmd_material mmd_mat = mat.mmd_material
mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat) mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat)
self.__update_shader_input("Diffuse Color", mmd_mat.diffuse_color[:] + (1,)) self.__update_shader_input("Diffuse Color", mmd_mat.diffuse_color[:] + (1,))
logger.debug(f"Updated diffuse color for {mat.name}")
def update_alpha(self) -> None: def update_alpha(self):
if self._nodes_are_readonly: if self._nodes_are_readonly:
return return
mat = self.material mat = self.material
@@ -401,31 +362,28 @@ class FnMaterial:
mat.diffuse_color[3] = mmd_mat.alpha mat.diffuse_color[3] = mmd_mat.alpha
self.__update_shader_input("Alpha", mmd_mat.alpha) self.__update_shader_input("Alpha", mmd_mat.alpha)
self.update_self_shadow_map() self.update_self_shadow_map()
logger.debug(f"Updated alpha for {mat.name}: {mmd_mat.alpha}")
def update_specular_color(self) -> None: def update_specular_color(self):
if self._nodes_are_readonly: if self._nodes_are_readonly:
return return
mat = self.material mat = self.material
mmd_mat = mat.mmd_material mmd_mat = mat.mmd_material
mat.specular_color = mmd_mat.specular_color mat.specular_color = mmd_mat.specular_color
self.__update_shader_input("Specular Color", mmd_mat.specular_color[:] + (1,)) self.__update_shader_input("Specular Color", mmd_mat.specular_color[:] + (1,))
logger.debug(f"Updated specular color for {mat.name}")
def update_shininess(self) -> None: def update_shininess(self):
if self._nodes_are_readonly: if self._nodes_are_readonly:
return return
mat = self.material mat = self.material
mmd_mat = mat.mmd_material mmd_mat = mat.mmd_material
mat.roughness = 1 / pow(max(mmd_mat.shininess, 1), 0.37) mat.roughness = 1 / pow(max(mmd_mat.shininess, 1), 0.37)
if hasattr(mat, "metallic"): if hasattr(mat, "metallic"):
mat.metallic = pow(1 - mat.roughness, 2.7) mat.metallic = 0.0
if hasattr(mat, "specular_hardness"): if hasattr(mat, "specular_hardness"):
mat.specular_hardness = mmd_mat.shininess mat.specular_hardness = mmd_mat.shininess
self.__update_shader_input("Reflect", mmd_mat.shininess) self.__update_shader_input("Reflect", mmd_mat.shininess)
logger.debug(f"Updated shininess for {mat.name}: {mmd_mat.shininess}")
def update_is_double_sided(self) -> None: def update_is_double_sided(self):
if self._nodes_are_readonly: if self._nodes_are_readonly:
return return
mat = self.material mat = self.material
@@ -435,9 +393,8 @@ class FnMaterial:
elif hasattr(mat, "use_backface_culling"): elif hasattr(mat, "use_backface_culling"):
mat.use_backface_culling = not mmd_mat.is_double_sided mat.use_backface_culling = not mmd_mat.is_double_sided
self.__update_shader_input("Double Sided", mmd_mat.is_double_sided) self.__update_shader_input("Double Sided", mmd_mat.is_double_sided)
logger.debug(f"Updated double-sided setting for {mat.name}: {mmd_mat.is_double_sided}")
def update_self_shadow_map(self) -> None: def update_self_shadow_map(self):
if self._nodes_are_readonly: if self._nodes_are_readonly:
return return
mat = self.material mat = self.material
@@ -445,24 +402,21 @@ class FnMaterial:
cast_shadows = mmd_mat.enabled_self_shadow_map if mmd_mat.alpha > 1e-3 else False cast_shadows = mmd_mat.enabled_self_shadow_map if mmd_mat.alpha > 1e-3 else False
if hasattr(mat, "shadow_method"): if hasattr(mat, "shadow_method"):
mat.shadow_method = "HASHED" if cast_shadows else "NONE" mat.shadow_method = "HASHED" if cast_shadows else "NONE"
logger.debug(f"Updated self shadow map for {mat.name}: {cast_shadows}")
def update_self_shadow(self) -> None: def update_self_shadow(self):
if self._nodes_are_readonly: if self._nodes_are_readonly:
return return
mat = self.material mat = self.material
mmd_mat = mat.mmd_material mmd_mat = mat.mmd_material
self.__update_shader_input("Self Shadow", mmd_mat.enabled_self_shadow) self.__update_shader_input("Self Shadow", mmd_mat.enabled_self_shadow)
logger.debug(f"Updated self shadow for {mat.name}: {mmd_mat.enabled_self_shadow}")
@staticmethod @staticmethod
def convert_to_mmd_material(material: bpy.types.Material, context: bpy.types.Context = bpy.context) -> None: def convert_to_mmd_material(material, context=bpy.context):
m, mmd_material = material, material.mmd_material m, mmd_material = material, material.mmd_material
logger.debug(f"Converting material to MMD material: {material.name}")
if m.use_nodes and next((n for n in m.node_tree.nodes if n.name.startswith("mmd_")), None) is None: if m.use_nodes and next((n for n in m.node_tree.nodes if n.name.startswith("mmd_")), None) is None:
def search_tex_image_node(node: bpy.types.ShaderNode) -> Optional[bpy.types.ShaderNodeTexImage]: def search_tex_image_node(node: bpy.types.ShaderNode):
if node.type == "TEX_IMAGE": if node.type == "TEX_IMAGE":
return node return node
for node_input in node.inputs: for node_input in node.inputs:
@@ -481,7 +435,8 @@ class FnMaterial:
preferred_output_node_target = { preferred_output_node_target = {
"CYCLES": "CYCLES", "CYCLES": "CYCLES",
"BLENDER_EEVEE_NEXT": "EEVEE", "BLENDER_EEVEE": "EEVEE",
"BLENDER_EEVEE_NEXT": "EEVEE", # Keep for backwards compatibility with 4.x
}.get(active_render_engine, "ALL") }.get(active_render_engine, "ALL")
tex_node = None tex_node = None
@@ -499,15 +454,13 @@ class FnMaterial:
if tex_node is None: if tex_node is None:
tex_node = next((n for n in m.node_tree.nodes if n.bl_idname == "ShaderNodeTexImage"), None) tex_node = next((n for n in m.node_tree.nodes if n.bl_idname == "ShaderNodeTexImage"), None)
if tex_node: if tex_node:
logger.debug(f"Found texture node for {material.name}: {tex_node.name}")
tex_node.name = "mmd_base_tex" tex_node.name = "mmd_base_tex"
else: else:
# Take the Base Color from BSDF if there's no texture # Take the Base Color from BSDF if there's no texture
bsdf_node = next((n for n in m.node_tree.nodes if n.type.startswith('BSDF_')), None) bsdf_node = next((n for n in m.node_tree.nodes if n.type.startswith("BSDF_")), None)
if bsdf_node: if bsdf_node:
base_color_input = bsdf_node.inputs.get('Base Color') or bsdf_node.inputs.get('Color') base_color_input = bsdf_node.inputs.get("Base Color") or bsdf_node.inputs.get("Color")
if base_color_input: if base_color_input:
logger.debug(f"Using BSDF base color for {material.name}")
mmd_material.diffuse_color = base_color_input.default_value[:3] mmd_material.diffuse_color = base_color_input.default_value[:3]
# ambient should be half the diffuse # ambient should be half the diffuse
mmd_material.ambient_color = [x * 0.5 for x in mmd_material.diffuse_color] mmd_material.ambient_color = [x * 0.5 for x in mmd_material.diffuse_color]
@@ -538,12 +491,11 @@ class FnMaterial:
# delete bsdf node if it's there # delete bsdf node if it's there
if m.use_nodes: if m.use_nodes:
nodes_to_remove = [n for n in m.node_tree.nodes if n.type == 'BSDF_PRINCIPLED' or n.type.startswith('BSDF_')] nodes_to_remove = [n for n in m.node_tree.nodes if n.type == "BSDF_PRINCIPLED" or n.type.startswith("BSDF_")]
for n in nodes_to_remove: for n in nodes_to_remove:
logger.debug(f"Removing BSDF node from {material.name}: {n.name}")
m.node_tree.nodes.remove(n) m.node_tree.nodes.remove(n)
def __update_shader_input(self, name: str, val: Any) -> None: def __update_shader_input(self, name, val):
mat = self.material mat = self.material
if mat.name.startswith("mmd_"): # skip mmd_edge.* if mat.name.startswith("mmd_"): # skip mmd_edge.*
return return
@@ -555,29 +507,26 @@ class FnMaterial:
val = min(max(val, interface_socket.min_value), interface_socket.max_value) val = min(max(val, interface_socket.min_value), interface_socket.max_value)
shader.inputs[name].default_value = val shader.inputs[name].default_value = val
def __update_shader_nodes(self) -> None: def __update_shader_nodes(self):
mat = self.material mat = self.material
if mat.node_tree is None: if mat.node_tree is None:
logger.debug(f"Creating node tree for {mat.name}")
mat.use_nodes = True mat.use_nodes = True
mat.node_tree.nodes.clear() mat.node_tree.nodes.clear()
nodes, links = mat.node_tree.nodes, mat.node_tree.links nodes, links = mat.node_tree.nodes, mat.node_tree.links
class _Dummy: class _Dummy:
default_value: Any = None default_value, is_linked = None, True
is_linked: bool = True
node_shader = nodes.get("mmd_shader", None) node_shader = nodes.get("mmd_shader", None)
if node_shader is None: if node_shader is None:
logger.debug(f"Creating MMD shader node for {mat.name}")
node_shader: bpy.types.ShaderNodeGroup = nodes.new("ShaderNodeGroup") node_shader: bpy.types.ShaderNodeGroup = nodes.new("ShaderNodeGroup")
node_shader.name = "mmd_shader" node_shader.name = "mmd_shader"
node_shader.location = (0, 1500) node_shader.location = (0, 300)
node_shader.width = 200 node_shader.width = 200
node_shader.node_tree = self.__get_shader() node_shader.node_tree = self.__get_shader()
mmd_mat: 'MMDMaterial' = mat.mmd_material mmd_mat: MMDMaterial = mat.mmd_material
node_shader.inputs.get("Ambient Color", _Dummy).default_value = mmd_mat.ambient_color[:] + (1,) node_shader.inputs.get("Ambient Color", _Dummy).default_value = mmd_mat.ambient_color[:] + (1,)
node_shader.inputs.get("Diffuse Color", _Dummy).default_value = mmd_mat.diffuse_color[:] + (1,) node_shader.inputs.get("Diffuse Color", _Dummy).default_value = mmd_mat.diffuse_color[:] + (1,)
node_shader.inputs.get("Specular Color", _Dummy).default_value = mmd_mat.specular_color[:] + (1,) node_shader.inputs.get("Specular Color", _Dummy).default_value = mmd_mat.specular_color[:] + (1,)
@@ -589,7 +538,6 @@ class FnMaterial:
node_uv = nodes.get("mmd_tex_uv", None) node_uv = nodes.get("mmd_tex_uv", None)
if node_uv is None: if node_uv is None:
logger.debug(f"Creating MMD UV node for {mat.name}")
node_uv: bpy.types.ShaderNodeGroup = nodes.new("ShaderNodeGroup") node_uv: bpy.types.ShaderNodeGroup = nodes.new("ShaderNodeGroup")
node_uv.name = "mmd_tex_uv" node_uv.name = "mmd_tex_uv"
node_uv.location = node_shader.location + Vector((-5 * 210, -2.5 * 220)) node_uv.location = node_shader.location + Vector((-5 * 210, -2.5 * 220))
@@ -604,7 +552,7 @@ class FnMaterial:
links.new(node_shader.outputs["Shader"], node_output.inputs["Surface"]) links.new(node_shader.outputs["Shader"], node_output.inputs["Surface"])
for name_id in ("Base", "Toon", "Sphere"): for name_id in ("Base", "Toon", "Sphere"):
texture = self.__get_texture_node("mmd_%s_tex" % name_id.lower()) texture = self.__get_texture_node(f"mmd_{name_id.lower()}_tex")
if texture: if texture:
name_tex_in, name_alpha_in, name_uv_out = (name_id + x for x in (" Tex", " Alpha", " UV")) name_tex_in, name_alpha_in, name_uv_out = (name_id + x for x in (" Tex", " Alpha", " UV"))
if not node_shader.inputs.get(name_tex_in, _Dummy).is_linked: if not node_shader.inputs.get(name_tex_in, _Dummy).is_linked:
@@ -614,13 +562,12 @@ class FnMaterial:
if not texture.inputs["Vector"].is_linked: if not texture.inputs["Vector"].is_linked:
links.new(node_uv.outputs[name_uv_out], texture.inputs["Vector"]) links.new(node_uv.outputs[name_uv_out], texture.inputs["Vector"])
def __get_shader_uv(self) -> bpy.types.ShaderNodeTree: def __get_shader_uv(self):
group_name = "MMDTexUV" group_name = "MMDTexUV"
shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree") shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
if len(shader.nodes): if len(shader.nodes):
return shader return shader
logger.debug(f"Creating MMD UV shader node group")
ng = _NodeGroupUtils(shader) ng = _NodeGroupUtils(shader)
############################################################################ ############################################################################
@@ -652,13 +599,12 @@ class FnMaterial:
return shader return shader
def __get_shader(self) -> bpy.types.ShaderNodeTree: def __get_shader(self):
group_name = "MMDShaderDev" group_name = "MMDShaderDev"
shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree") shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
if len(shader.nodes): if len(shader.nodes):
return shader return shader
logger.debug(f"Creating MMD shader node group")
ng = _NodeGroupUtils(shader) ng = _NodeGroupUtils(shader)
############################################################################ ############################################################################
@@ -748,18 +694,15 @@ class FnMaterial:
class MigrationFnMaterial: class MigrationFnMaterial:
@staticmethod @staticmethod
def update_mmd_shader() -> None: def update_mmd_shader():
mmd_shader_node_tree: Optional[bpy.types.NodeTree] = bpy.data.node_groups.get("MMDShaderDev") mmd_shader_node_tree: Optional[bpy.types.NodeTree] = bpy.data.node_groups.get("MMDShaderDev")
if mmd_shader_node_tree is None: if mmd_shader_node_tree is None:
logger.debug("No MMD shader node tree found, skipping update")
return return
ng = _NodeGroupUtils(mmd_shader_node_tree) ng = _NodeGroupUtils(mmd_shader_node_tree)
if "Color" in ng.node_output.inputs: if "Color" in ng.node_output.inputs:
logger.debug("MMD shader already has Color output, skipping update")
return return
logger.info("Updating MMD shader node tree")
shader_diffuse: bpy.types.ShaderNodeBsdfDiffuse = [n for n in mmd_shader_node_tree.nodes if n.type == "BSDF_DIFFUSE"][0] shader_diffuse: bpy.types.ShaderNodeBsdfDiffuse = [n for n in mmd_shader_node_tree.nodes if n.type == "BSDF_DIFFUSE"][0]
node_sphere: bpy.types.ShaderNodeMixRGB = shader_diffuse.inputs["Color"].links[0].from_node node_sphere: bpy.types.ShaderNodeMixRGB = shader_diffuse.inputs["Color"].links[0].from_node
node_output: bpy.types.NodeGroupOutput = ng.node_output node_output: bpy.types.NodeGroupOutput = ng.node_output
@@ -768,11 +711,3 @@ class MigrationFnMaterial:
ng.new_output_socket("Color", node_sphere.outputs["Color"]) ng.new_output_socket("Color", node_sphere.outputs["Color"])
ng.new_output_socket("Alpha", node_alpha.outputs["Value"]) ng.new_output_socket("Alpha", node_alpha.outputs["Value"])
logger.info("MMD shader node tree updated successfully")
# Add Self Shadow input if it doesn't exist
if "Self Shadow" not in ng.node_input.outputs:
logger.info("Adding Self Shadow input to MMD shader")
# Find shader_base_mix node to connect Self Shadow
shader_base_mix = shader_alpha_mix.inputs[2].links[0].from_node
ng.new_input_socket("Self Shadow", shader_base_mix.inputs["Fac"], 0, min_max=(0, 1))
+595 -302
View File
File diff suppressed because it is too large Load Diff
+93 -103
View File
@@ -1,39 +1,34 @@
# -*- coding: utf-8 -*- # Copyright 2016 MMD Tools authors
# Copyright 2014 MMD Tools authors # This file is part of MMD Tools.
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from ....core.logging_setup import logger
import math
import re import re
from typing import TYPE_CHECKING, Tuple, cast, List, Dict, Optional, Set, Any, Union, Iterator from typing import TYPE_CHECKING, Tuple, cast
import bpy import bpy
import numpy as np
from bpy.types import Object, ShapeKey, Material, Mesh, Armature, PoseBone, Constraint
from .. import bpyutils, utils from .. import bpyutils, utils
from ..bpyutils import FnContext, FnObject, TransformConstraintOp from ..bpyutils import FnContext, FnObject, TransformConstraintOp
from ....core.logging_setup import logger
if TYPE_CHECKING: if TYPE_CHECKING:
from .model import Model from .model import Model
class FnMorph: class FnMorph:
def __init__(self, morph: Any, model: "Model"): def __init__(self, morph, model: "Model"):
self.__morph = morph self.__morph = morph
self.__rig = model self.__rig = model
@classmethod @classmethod
def storeShapeKeyOrder(cls, obj: Object, shape_key_names: List[str]) -> None: def storeShapeKeyOrder(cls, obj, shape_key_names):
if len(shape_key_names) < 1: if len(shape_key_names) < 1:
return return
assert FnContext.get_active_object(FnContext.ensure_context()) == obj assert FnContext.get_active_object(FnContext.ensure_context()) == obj
if obj.data.shape_keys is None: if obj.data.shape_keys is None:
bpy.ops.object.shape_key_add() bpy.ops.object.shape_key_add()
def __move_to_bottom(key_blocks: bpy.types.bpy_prop_collection, name: str) -> None: def __move_to_bottom(key_blocks, name):
obj.active_shape_key_index = key_blocks.find(name) obj.active_shape_key_index = key_blocks.find(name)
bpy.ops.object.shape_key_move(type="BOTTOM") bpy.ops.object.shape_key_move(type="BOTTOM")
@@ -45,7 +40,7 @@ class FnMorph:
__move_to_bottom(key_blocks, name) __move_to_bottom(key_blocks, name)
@classmethod @classmethod
def fixShapeKeyOrder(cls, obj: Object, shape_key_names: List[str]) -> None: def fixShapeKeyOrder(cls, obj, shape_key_names):
if len(shape_key_names) < 1: if len(shape_key_names) < 1:
return return
assert FnContext.get_active_object(FnContext.ensure_context()) == obj assert FnContext.get_active_object(FnContext.ensure_context()) == obj
@@ -60,11 +55,11 @@ class FnMorph:
bpy.ops.object.shape_key_move(type="BOTTOM") bpy.ops.object.shape_key_move(type="BOTTOM")
@staticmethod @staticmethod
def get_morph_slider(rig: "Model") -> "_MorphSlider": def get_morph_slider(rig):
return _MorphSlider(rig) return _MorphSlider(rig)
@staticmethod @staticmethod
def category_guess(morph: Any) -> None: def category_guess(morph):
name_lower = morph.name.lower() name_lower = morph.name.lower()
if "mouth" in name_lower: if "mouth" in name_lower:
morph.category = "MOUTH" morph.category = "MOUTH"
@@ -75,7 +70,7 @@ class FnMorph:
morph.category = "EYE" morph.category = "EYE"
@classmethod @classmethod
def load_morphs(cls, rig: "Model") -> None: def load_morphs(cls, rig):
mmd_root = rig.rootObject().mmd_root mmd_root = rig.rootObject().mmd_root
vertex_morphs = mmd_root.vertex_morphs vertex_morphs = mmd_root.vertex_morphs
uv_morphs = mmd_root.uv_morphs uv_morphs = mmd_root.uv_morphs
@@ -94,7 +89,7 @@ class FnMorph:
cls.category_guess(item) cls.category_guess(item)
@staticmethod @staticmethod
def remove_shape_key(mesh_object: Object, shape_key_name: str) -> None: def remove_shape_key(mesh_object: bpy.types.Object, shape_key_name: str):
assert isinstance(mesh_object.data, bpy.types.Mesh) assert isinstance(mesh_object.data, bpy.types.Mesh)
shape_keys = mesh_object.data.shape_keys shape_keys = mesh_object.data.shape_keys
@@ -106,7 +101,7 @@ class FnMorph:
FnObject.mesh_remove_shape_key(mesh_object, key_blocks[shape_key_name]) FnObject.mesh_remove_shape_key(mesh_object, key_blocks[shape_key_name])
@staticmethod @staticmethod
def copy_shape_key(mesh_object: Object, src_name: str, dest_name: str) -> None: def copy_shape_key(mesh_object: bpy.types.Object, src_name: str, dest_name: str):
assert isinstance(mesh_object.data, bpy.types.Mesh) assert isinstance(mesh_object.data, bpy.types.Mesh)
shape_keys = mesh_object.data.shape_keys shape_keys = mesh_object.data.shape_keys
@@ -128,13 +123,13 @@ class FnMorph:
mesh_object.active_shape_key_index = key_blocks.find(dest_name) mesh_object.active_shape_key_index = key_blocks.find(dest_name)
@staticmethod @staticmethod
def get_uv_morph_vertex_groups(obj: Object, morph_name: Optional[str] = None, offset_axes: str = "XYZW") -> Iterator[Tuple[bpy.types.VertexGroup, str, str]]: def get_uv_morph_vertex_groups(obj, morph_name=None, offset_axes="XYZW"):
pattern = "UV_%s[+-][%s]$" % (morph_name or ".{1,}", offset_axes or "XYZW") pattern = "UV_%s[+-][%s]$" % (morph_name or ".{1,}", offset_axes or "XYZW")
# yield (vertex_group, morph_name, axis),... # yield (vertex_group, morph_name, axis),...
return ((g, g.name[3:-2], g.name[-2:]) for g in obj.vertex_groups if re.match(pattern, g.name)) return ((g, g.name[3:-2], g.name[-2:]) for g in obj.vertex_groups if re.match(pattern, g.name))
@staticmethod @staticmethod
def copy_uv_morph_vertex_groups(obj: Object, src_name: str, dest_name: str) -> None: def copy_uv_morph_vertex_groups(obj, src_name, dest_name):
for vg, n, x in FnMorph.get_uv_morph_vertex_groups(obj, dest_name): for vg, n, x in FnMorph.get_uv_morph_vertex_groups(obj, dest_name):
obj.vertex_groups.remove(vg) obj.vertex_groups.remove(vg)
@@ -145,12 +140,12 @@ class FnMorph:
obj.vertex_groups.active.name = vg_name.replace(src_name, dest_name) obj.vertex_groups.active.name = vg_name.replace(src_name, dest_name)
@staticmethod @staticmethod
def overwrite_bone_morphs_from_action_pose(armature_object: Object) -> None: def overwrite_bone_morphs_from_action_pose(armature_object):
armature = armature_object.id_data armature = armature_object.id_data
# Use animation_data and action instead of action_pose # Use animation_data and action instead of action_pose
if armature.animation_data is None or armature.animation_data.action is None: if armature.animation_data is None or armature.animation_data.action is None:
logger.warning('Armature "%s" has no animation data or action', armature_object.name) logger.warning('[WARNING] armature "%s" has no animation data or action', armature_object.name)
return return
action = armature.animation_data.action action = armature.animation_data.action
@@ -164,7 +159,7 @@ class FnMorph:
bone_morphs = mmd_root.bone_morphs bone_morphs = mmd_root.bone_morphs
utils.selectAObject(armature_object) utils.selectAObject(armature_object)
original_mode = bpy.context.object.mode original_mode = bpy.context.active_object.mode
bpy.ops.object.mode_set(mode="POSE") bpy.ops.object.mode_set(mode="POSE")
try: try:
for index, pose_marker in enumerate(pose_markers): for index, pose_marker in enumerate(pose_markers):
@@ -189,9 +184,9 @@ class FnMorph:
utils.selectAObject(root) utils.selectAObject(root)
@staticmethod @staticmethod
def clean_uv_morph_vertex_groups(obj: Object) -> None: def clean_uv_morph_vertex_groups(obj):
# remove empty vertex groups of uv morphs # remove empty vertex groups of uv morphs
vg_indices: Set[int] = {g.index for g, n, x in FnMorph.get_uv_morph_vertex_groups(obj)} vg_indices = {g.index for g, n, x in FnMorph.get_uv_morph_vertex_groups(obj)}
vertex_groups = obj.vertex_groups vertex_groups = obj.vertex_groups
for v in obj.data.vertices: for v in obj.data.vertices:
for x in v.groups: for x in v.groups:
@@ -205,8 +200,8 @@ class FnMorph:
vertex_groups.remove(vg) vertex_groups.remove(vg)
@staticmethod @staticmethod
def get_uv_morph_offset_map(obj: Object, morph: Any) -> Dict[int, List[float]]: def get_uv_morph_offset_map(obj, morph):
offset_map: Dict[int, List[float]] = {} # offset_map[vertex_index] = offset_xyzw offset_map = {} # offset_map[vertex_index] = offset_xyzw
if morph.data_type == "VERTEX_GROUP": if morph.data_type == "VERTEX_GROUP":
scale = morph.vertex_group_scale scale = morph.vertex_group_scale
axis_map = {g.index: x for g, n, x in FnMorph.get_uv_morph_vertex_groups(obj, morph.name)} axis_map = {g.index: x for g, n, x in FnMorph.get_uv_morph_vertex_groups(obj, morph.name)}
@@ -221,13 +216,13 @@ class FnMorph:
for val in morph.data: for val in morph.data:
i = val.index i = val.index
if i in offset_map: if i in offset_map:
offset_map[i] = [a + b for a, b in zip(offset_map[i], val.offset)] offset_map[i] = [a + b for a, b in zip(offset_map[i], val.offset, strict=False)]
else: else:
offset_map[i] = val.offset offset_map[i] = val.offset
return offset_map return offset_map
@staticmethod @staticmethod
def store_uv_morph_data(obj: Object, morph: Any, offsets: Optional[List[Any]] = None, offset_axes: str = "XYZW") -> None: def store_uv_morph_data(obj, morph, offsets=None, offset_axes="XYZW"):
vertex_groups = obj.vertex_groups vertex_groups = obj.vertex_groups
morph_name = getattr(morph, "name", None) morph_name = getattr(morph, "name", None)
if offset_axes: if offset_axes:
@@ -246,13 +241,13 @@ class FnMorph:
max_value = max(max(abs(x) for x in v) for v in offset_map.values() or ([0],)) max_value = max(max(abs(x) for x in v) for v in offset_map.values() or ([0],))
scale = morph.vertex_group_scale = max(abs(morph.vertex_group_scale), max_value) scale = morph.vertex_group_scale = max(abs(morph.vertex_group_scale), max_value)
for idx, offset in offset_map.items(): for idx, offset in offset_map.items():
for val, axis in zip(offset, "XYZW"): for val, axis in zip(offset, "XYZW", strict=False):
if abs(val) > 1e-4: if abs(val) > 1e-4:
vg_name = "UV_{0}{1}{2}".format(morph_name, "-" if val < 0 else "+", axis) vg_name = f"UV_{morph_name}{'-' if val < 0 else '+'}{axis}"
vg = vertex_groups.get(vg_name, None) or vertex_groups.new(name=vg_name) vg = vertex_groups.get(vg_name, None) or vertex_groups.new(name=vg_name)
vg.add(index=[idx], weight=abs(val) / scale, type="REPLACE") vg.add(index=[idx], weight=abs(val) / scale, type="REPLACE")
def update_mat_related_mesh(self, new_mesh: Optional[Object] = None) -> None: def update_mat_related_mesh(self, new_mesh=None):
for offset in self.__morph.data: for offset in self.__morph.data:
# Use the new_mesh if provided # Use the new_mesh if provided
meshObj = new_mesh meshObj = new_mesh
@@ -272,28 +267,28 @@ class FnMorph:
offset.related_mesh = meshObj.data.name offset.related_mesh = meshObj.data.name
@staticmethod @staticmethod
def clean_duplicated_material_morphs(mmd_root_object: Object) -> None: def clean_duplicated_material_morphs(mmd_root_object: bpy.types.Object):
"""Clean duplicated material_morphs and data from mmd_root_object.mmd_root.material_morphs[].data[]""" """Clean duplicated material_morphs and data from mmd_root_object.mmd_root.material_morphs[].data[]"""
mmd_root = mmd_root_object.mmd_root mmd_root = mmd_root_object.mmd_root
def morph_data_equals(l: Any, r: Any) -> bool: def morph_data_equals(left, right) -> bool:
return ( return (
l.related_mesh_data == r.related_mesh_data left.related_mesh_data == right.related_mesh_data
and l.offset_type == r.offset_type and left.offset_type == right.offset_type
and l.material == r.material and left.material == right.material
and all(a == b for a, b in zip(l.diffuse_color, r.diffuse_color)) and all(a == b for a, b in zip(left.diffuse_color, right.diffuse_color, strict=False))
and all(a == b for a, b in zip(l.specular_color, r.specular_color)) and all(a == b for a, b in zip(left.specular_color, right.specular_color, strict=False))
and l.shininess == r.shininess and left.shininess == right.shininess
and all(a == b for a, b in zip(l.ambient_color, r.ambient_color)) and all(a == b for a, b in zip(left.ambient_color, right.ambient_color, strict=False))
and all(a == b for a, b in zip(l.edge_color, r.edge_color)) and all(a == b for a, b in zip(left.edge_color, right.edge_color, strict=False))
and l.edge_weight == r.edge_weight and left.edge_weight == right.edge_weight
and all(a == b for a, b in zip(l.texture_factor, r.texture_factor)) and all(a == b for a, b in zip(left.texture_factor, right.texture_factor, strict=False))
and all(a == b for a, b in zip(l.sphere_texture_factor, r.sphere_texture_factor)) and all(a == b for a, b in zip(left.sphere_texture_factor, right.sphere_texture_factor, strict=False))
and all(a == b for a, b in zip(l.toon_texture_factor, r.toon_texture_factor)) and all(a == b for a, b in zip(left.toon_texture_factor, right.toon_texture_factor, strict=False))
) )
def morph_equals(l: Any, r: Any) -> bool: def morph_equals(left, right) -> bool:
return len(l.data) == len(r.data) and all(morph_data_equals(a, b) for a, b in zip(l.data, r.data)) return len(left.data) == len(right.data) and all(morph_data_equals(a, b) for a, b in zip(left.data, right.data, strict=False))
# Remove duplicated mmd_root.material_morphs.data[] # Remove duplicated mmd_root.material_morphs.data[]
for material_morph in mmd_root.material_morphs: for material_morph in mmd_root.material_morphs:
@@ -327,7 +322,7 @@ class _MorphSlider:
def __init__(self, model: "Model"): def __init__(self, model: "Model"):
self.__rig = model self.__rig = model
def placeholder(self, create: bool = False, binded: bool = False) -> Optional[Object]: def placeholder(self, create=False, binded=False):
rig = self.__rig rig = self.__rig
root = rig.rootObject() root = rig.rootObject()
obj = next((x for x in root.children if x.mmd_type == "PLACEHOLDER" and x.type == "MESH"), None) obj = next((x for x in root.children if x.mmd_type == "PLACEHOLDER" and x.type == "MESH"), None)
@@ -345,11 +340,11 @@ class _MorphSlider:
return obj return obj
@property @property
def dummy_armature(self) -> Optional[Object]: def dummy_armature(self):
obj = self.placeholder() obj = self.placeholder()
return self.__dummy_armature(obj) if obj else None return self.__dummy_armature(obj) if obj else None
def __dummy_armature(self, obj: Object, create: bool = False) -> Optional[Object]: def __dummy_armature(self, obj, create=False):
arm = next((x for x in obj.children if x.mmd_type == "PLACEHOLDER" and x.type == "ARMATURE"), None) arm = next((x for x in obj.children if x.mmd_type == "PLACEHOLDER" and x.type == "ARMATURE"), None)
if create and arm is None: if create and arm is None:
arm = bpy.data.objects.new(name=".dummy_armature", object_data=bpy.data.armatures.new(name=".dummy_armature")) arm = bpy.data.objects.new(name=".dummy_armature", object_data=bpy.data.armatures.new(name=".dummy_armature"))
@@ -362,7 +357,7 @@ class _MorphSlider:
FnBone.setup_special_bone_collections(arm) FnBone.setup_special_bone_collections(arm)
return arm return arm
def get(self, morph_name: str) -> Optional[ShapeKey]: def get(self, morph_name):
obj = self.placeholder() obj = self.placeholder()
if obj is None: if obj is None:
return None return None
@@ -371,13 +366,13 @@ class _MorphSlider:
return None return None
return key_blocks.get(morph_name, None) return key_blocks.get(morph_name, None)
def create(self) -> Object: def create(self):
self.__rig.loadMorphs() self.__rig.loadMorphs()
obj = self.placeholder(create=True) obj = self.placeholder(create=True)
self.__load(obj, self.__rig.rootObject().mmd_root) self.__load(obj, self.__rig.rootObject().mmd_root)
return obj return obj
def __load(self, obj: Object, mmd_root: Any) -> None: def __load(self, obj, mmd_root):
attr_list = ("group", "vertex", "bone", "uv", "material") attr_list = ("group", "vertex", "bone", "uv", "material")
morph_sliders = obj.data.shape_keys.key_blocks morph_sliders = obj.data.shape_keys.key_blocks
for m in (x for attr in attr_list for x in getattr(mmd_root, attr + "_morphs", ())): for m in (x for attr in attr_list for x in getattr(mmd_root, attr + "_morphs", ())):
@@ -388,15 +383,15 @@ class _MorphSlider:
obj.shape_key_add(name=name, from_mix=False) obj.shape_key_add(name=name, from_mix=False)
@staticmethod @staticmethod
def __driver_variables(id_data: Any, path: str, index: int = -1) -> Tuple[Any, Any]: def __driver_variables(id_data, path, index=-1):
d = id_data.driver_add(path, index) d = id_data.driver_add(path, index)
variables = d.driver.variables variables = d.driver.variables
for x in variables: for x in reversed(variables):
variables.remove(x) variables.remove(x)
return d.driver, variables return d.driver, variables
@staticmethod @staticmethod
def __add_single_prop(variables: Any, id_obj: Object, data_path: str, prefix: str) -> Any: def __add_single_prop(variables, id_obj, data_path, prefix):
var = variables.new() var = variables.new()
var.name = f"{prefix}{len(variables)}" var.name = f"{prefix}{len(variables)}"
var.type = "SINGLE_PROP" var.type = "SINGLE_PROP"
@@ -407,7 +402,7 @@ class _MorphSlider:
return var return var
@staticmethod @staticmethod
def __shape_key_driver_check(key_block: ShapeKey, resolve_path: bool = False) -> bool: def __shape_key_driver_check(key_block, resolve_path=False):
if resolve_path: if resolve_path:
try: try:
key_block.id_data.path_resolve(key_block.path_from_id()) key_block.id_data.path_resolve(key_block.path_from_id())
@@ -421,22 +416,20 @@ class _MorphSlider:
d = next((i for i in key_block.id_data.animation_data.drivers if i.data_path == data_path), None) d = next((i for i in key_block.id_data.animation_data.drivers if i.data_path == data_path), None)
return not d or d.driver.expression == "".join(("*w", "+g", "v")[-1 if i < 1 else i % 2] + str(i + 1) for i in range(len(d.driver.variables))) return not d or d.driver.expression == "".join(("*w", "+g", "v")[-1 if i < 1 else i % 2] + str(i + 1) for i in range(len(d.driver.variables)))
def __cleanup(self, names_in_use: Optional[Dict[str, Any]] = None) -> None: def __cleanup(self, names_in_use=None):
from math import ceil, floor
names_in_use = names_in_use or {} names_in_use = names_in_use or {}
rig = self.__rig rig = self.__rig
morph_sliders = self.placeholder() morph_sliders = self.placeholder()
morph_sliders = morph_sliders.data.shape_keys.key_blocks if morph_sliders else {} morph_sliders = morph_sliders.data.shape_keys.key_blocks if morph_sliders else {}
for mesh_object in rig.meshes(): for mesh_object in rig.meshes():
for kb in getattr(mesh_object.data.shape_keys, "key_blocks", cast(Tuple[ShapeKey], ())): for kb in getattr(mesh_object.data.shape_keys, "key_blocks", cast("Tuple[bpy.types.ShapeKey]", ())):
if kb.name in names_in_use: if kb.name in names_in_use:
continue continue
if kb.name.startswith("mmd_bind"): if kb.name.startswith("mmd_bind"):
kb.driver_remove("value") kb.driver_remove("value")
ms = morph_sliders[kb.relative_key.name] ms = morph_sliders[kb.relative_key.name]
kb.relative_key.slider_min, kb.relative_key.slider_max = min(ms.slider_min, floor(ms.value)), max(ms.slider_max, ceil(ms.value)) kb.relative_key.slider_min, kb.relative_key.slider_max = min(ms.slider_min, math.floor(ms.value)), max(ms.slider_max, math.ceil(ms.value))
kb.relative_key.value = ms.value kb.relative_key.value = ms.value
kb.relative_key.mute = False kb.relative_key.mute = False
FnObject.mesh_remove_shape_key(mesh_object, kb) FnObject.mesh_remove_shape_key(mesh_object, kb)
@@ -444,9 +437,9 @@ class _MorphSlider:
elif kb.name in morph_sliders and self.__shape_key_driver_check(kb): elif kb.name in morph_sliders and self.__shape_key_driver_check(kb):
ms = morph_sliders[kb.name] ms = morph_sliders[kb.name]
kb.driver_remove("value") kb.driver_remove("value")
kb.slider_min, kb.slider_max = min(ms.slider_min, floor(kb.value)), max(ms.slider_max, ceil(kb.value)) kb.slider_min, kb.slider_max = min(ms.slider_min, math.floor(kb.value)), max(ms.slider_max, math.ceil(kb.value))
for m in mesh_object.modifiers: # uv morph for m in reversed(mesh_object.modifiers): # uv morph
if m.name.startswith("mmd_bind") and m.name not in names_in_use: if m.name.startswith("mmd_bind") and m.name not in names_in_use:
mesh_object.modifiers.remove(m) mesh_object.modifiers.remove(m)
@@ -461,13 +454,13 @@ class _MorphSlider:
attributes = set(TransformConstraintOp.min_max_attributes("LOCATION", "to")) attributes = set(TransformConstraintOp.min_max_attributes("LOCATION", "to"))
attributes |= set(TransformConstraintOp.min_max_attributes("ROTATION", "to")) attributes |= set(TransformConstraintOp.min_max_attributes("ROTATION", "to"))
for b in rig.armature().pose.bones: for b in rig.armature().pose.bones:
for c in b.constraints: for c in reversed(b.constraints):
if c.name.startswith("mmd_bind") and c.name[:-4] not in names_in_use: if c.name.startswith("mmd_bind") and c.name[:-4] not in names_in_use:
for attr in attributes: for attr in attributes:
c.driver_remove(attr) c.driver_remove(attr)
b.constraints.remove(c) b.constraints.remove(c)
def unbind(self) -> None: def unbind(self):
mmd_root = self.__rig.rootObject().mmd_root mmd_root = self.__rig.rootObject().mmd_root
# after unbind, the weird lag problem will disappear. # after unbind, the weird lag problem will disappear.
@@ -490,7 +483,7 @@ class _MorphSlider:
b.driver_remove("rotation_quaternion") b.driver_remove("rotation_quaternion")
self.__cleanup() self.__cleanup()
def bind(self) -> None: def bind(self):
rig = self.__rig rig = self.__rig
root = rig.rootObject() root = rig.rootObject()
armObj = rig.armature() armObj = rig.armature()
@@ -504,10 +497,10 @@ class _MorphSlider:
morph_sliders = obj.data.shape_keys.key_blocks morph_sliders = obj.data.shape_keys.key_blocks
# data gathering # data gathering
group_map: Dict[Tuple[str, str], List[List[Any]]] = {} group_map = {}
shape_key_map: Dict[str, List[Tuple[ShapeKey, str, List[Any]]]] = {} shape_key_map = {}
uv_morph_map: Dict[str, List[Tuple[str, str, str, List[Any]]]] = {} uv_morph_map = {}
for mesh_object in rig.meshes(): for mesh_object in rig.meshes():
mesh_object.show_only_shape_key = False mesh_object.show_only_shape_key = False
key_blocks = getattr(mesh_object.data.shape_keys, "key_blocks", ()) key_blocks = getattr(mesh_object.data.shape_keys, "key_blocks", ())
@@ -528,11 +521,11 @@ class _MorphSlider:
kb_bind.slider_max = 10 kb_bind.slider_max = 10
data_path = 'data.shape_keys.key_blocks["%s"].value' % kb_name.replace('"', '\\"') data_path = 'data.shape_keys.key_blocks["%s"].value' % kb_name.replace('"', '\\"')
groups: List[Any] = [] groups = []
shape_key_map.setdefault(name_bind, []).append((kb_bind, data_path, groups)) shape_key_map.setdefault(name_bind, []).append((kb_bind, data_path, groups))
group_map.setdefault(("vertex_morphs", kb_name), []).append(groups) group_map.setdefault(("vertex_morphs", kb_name), []).append(groups)
uv_layers = [l.name for l in mesh_object.data.uv_layers if not l.name.startswith("_")] uv_layers = [layer.name for layer in mesh_object.data.uv_layers if not layer.name.startswith("_")]
uv_layers += [""] * (5 - len(uv_layers)) uv_layers += [""] * (5 - len(uv_layers))
for vg, morph_name, axis in FnMorph.get_uv_morph_vertex_groups(mesh_object): for vg, morph_name, axis in FnMorph.get_uv_morph_vertex_groups(mesh_object):
morph = mmd_root.uv_morphs.get(morph_name, None) morph = mmd_root.uv_morphs.get(morph_name, None)
@@ -544,7 +537,7 @@ class _MorphSlider:
continue continue
name_bind = "mmd_bind%s" % hash(vg.name) name_bind = "mmd_bind%s" % hash(vg.name)
uv_morph_map.setdefault(name_bind, []) uv_morph_map.setdefault(name_bind, ())
mod = mesh_object.modifiers.get(name_bind, None) or mesh_object.modifiers.new(name=name_bind, type="UV_WARP") mod = mesh_object.modifiers.get(name_bind, None) or mesh_object.modifiers.new(name=name_bind, type="UV_WARP")
mod.show_expanded = False mod.show_expanded = False
mod.vertex_group = vg.name mod.vertex_group = vg.name
@@ -557,13 +550,13 @@ class _MorphSlider:
else: else:
mod.bone_from, mod.bone_to = name_bind, "mmd_bind_ctrl_base" mod.bone_from, mod.bone_to = name_bind, "mmd_bind_ctrl_base"
bone_offset_map: Dict[str, Tuple[str, Any, str, str, List[Any]]] = {} bone_offset_map = {}
with bpyutils.edit_object(arm) as data: with bpyutils.edit_object(arm) as data:
from .bone import FnBone from .bone import FnBone
edit_bones = data.edit_bones edit_bones = data.edit_bones
def __get_bone(name: str, parent: Optional[bpy.types.EditBone]) -> bpy.types.EditBone: def __get_bone(name, parent):
b = edit_bones.get(name, None) or edit_bones.new(name=name) b = edit_bones.get(name, None) or edit_bones.new(name=name)
b.head = (0, 0, 0) b.head = (0, 0, 0)
b.tail = (0, 0, 1) b.tail = (0, 0, 1)
@@ -580,7 +573,7 @@ class _MorphSlider:
continue continue
d.name = name_bind = f"mmd_bind{hash(d)}" d.name = name_bind = f"mmd_bind{hash(d)}"
b = FnBone.set_edit_bone_to_shadow(__get_bone(name_bind, None)) b = FnBone.set_edit_bone_to_shadow(__get_bone(name_bind, None))
groups: List[Any] = [] groups = []
bone_offset_map[name_bind] = (m.name, d, b.name, data_path, groups) bone_offset_map[name_bind] = (m.name, d, b.name, data_path, groups)
group_map.setdefault(("bone_morphs", m.name), []).append(groups) group_map.setdefault(("bone_morphs", m.name), []).append(groups)
@@ -591,21 +584,21 @@ class _MorphSlider:
scale_path = f'mmd_root.uv_morphs["{morph_name}"].vertex_group_scale' scale_path = f'mmd_root.uv_morphs["{morph_name}"].vertex_group_scale'
name_bind = f"mmd_bind{hash(m.name)}" name_bind = f"mmd_bind{hash(m.name)}"
b = FnBone.set_edit_bone_to_dummy(__get_bone(name_bind, ctrl_base)) b = FnBone.set_edit_bone_to_dummy(__get_bone(name_bind, ctrl_base))
groups: List[Any] = [] groups = []
uv_morph_map.setdefault(name_bind, []).append((b.name, data_path, scale_path, groups)) uv_morph_map.setdefault(name_bind, []).append((b.name, data_path, scale_path, groups))
group_map.setdefault(("uv_morphs", m.name), []).append(groups) group_map.setdefault(("uv_morphs", m.name), []).append(groups)
used_bone_names: Set[str] = set(bone_offset_map.keys()) | set(uv_morph_map.keys()) used_bone_names = bone_offset_map.keys() | uv_morph_map.keys()
used_bone_names.add(ctrl_base.name) used_bone_names.add(ctrl_base.name)
for b in edit_bones: # cleanup for b in reversed(edit_bones): # cleanup
if b.name.startswith("mmd_bind") and b.name not in used_bone_names: if b.name.startswith("mmd_bind") and b.name not in used_bone_names:
edit_bones.remove(b) edit_bones.remove(b)
material_offset_map: Dict[str, Any] = {} material_offset_map = {}
for m in mmd_root.material_morphs: for m in mmd_root.material_morphs:
morph_name = m.name.replace('"', '\\"') morph_name = m.name.replace('"', '\\"')
data_path = f'data.shape_keys.key_blocks["{morph_name}"].value' data_path = f'data.shape_keys.key_blocks["{morph_name}"].value'
groups: List[Any] = [] groups = []
group_map.setdefault(("material_morphs", m.name), []).append(groups) group_map.setdefault(("material_morphs", m.name), []).append(groups)
material_offset_map.setdefault("group_dict", {})[m.name] = (data_path, groups) material_offset_map.setdefault("group_dict", {})[m.name] = (data_path, groups)
for d in m.data: for d in m.data:
@@ -616,7 +609,7 @@ class _MorphSlider:
for m in mmd_root.group_morphs: for m in mmd_root.group_morphs:
if len(m.data) != len(set(m.data.keys())): if len(m.data) != len(set(m.data.keys())):
logger.warning('Found duplicated morph data in Group Morph "%s"', m.name) logger.warning(' * Found duplicated morph data in Group Morph "%s"', m.name)
morph_name = m.name.replace('"', '\\"') morph_name = m.name.replace('"', '\\"')
morph_path = f'data.shape_keys.key_blocks["{morph_name}"].value' morph_path = f'data.shape_keys.key_blocks["{morph_name}"].value'
for d in m.data: for d in m.data:
@@ -627,7 +620,7 @@ class _MorphSlider:
self.__cleanup(shape_key_map.keys() | bone_offset_map.keys() | uv_morph_map.keys()) self.__cleanup(shape_key_map.keys() | bone_offset_map.keys() | uv_morph_map.keys())
def __config_groups(variables: Any, expression: str, groups: List[Any]) -> str: def __config_groups(variables, expression, groups):
for g_name, morph_path, factor_path in groups: for g_name, morph_path, factor_path in groups:
var = self.__add_single_prop(variables, obj, morph_path, "g") var = self.__add_single_prop(variables, obj, morph_path, "g")
fvar = self.__add_single_prop(variables, root, factor_path, "w") fvar = self.__add_single_prop(variables, root, factor_path, "w")
@@ -635,7 +628,7 @@ class _MorphSlider:
return expression return expression
# vertex morphs # vertex morphs
for kb_bind, morph_data_path, groups in (i for l in shape_key_map.values() for i in l): for kb_bind, morph_data_path, groups in (i for value_list in shape_key_map.values() for i in value_list):
driver, variables = self.__driver_variables(kb_bind, "value") driver, variables = self.__driver_variables(kb_bind, "value")
var = self.__add_single_prop(variables, obj, morph_data_path, "v") var = self.__add_single_prop(variables, obj, morph_data_path, "v")
if kb_bind.name.startswith("mmd_bind"): if kb_bind.name.startswith("mmd_bind"):
@@ -646,7 +639,7 @@ class _MorphSlider:
kb_bind.mute = False kb_bind.mute = False
# bone morphs # bone morphs
def __config_bone_morph(constraints: bpy.types.ArmatureConstraints, map_type: str, attributes: Set[str], val: float, val_str: str) -> None: def __config_bone_morph(constraints, map_type, attributes, val, val_str):
c_name = f"mmd_bind{hash(data)}.{map_type[:3]}" c_name = f"mmd_bind{hash(data)}.{map_type[:3]}"
c = TransformConstraintOp.create(constraints, c_name, map_type) c = TransformConstraintOp.create(constraints, c_name, map_type)
TransformConstraintOp.update_min_max(c, val, None) TransformConstraintOp.update_min_max(c, val, None)
@@ -660,8 +653,6 @@ class _MorphSlider:
sign = "-" if attr.startswith("to_min") else "" sign = "-" if attr.startswith("to_min") else ""
driver.expression = f"{sign}{val_str}*({expression})" driver.expression = f"{sign}{val_str}*({expression})"
from math import pi
attributes_rot = TransformConstraintOp.min_max_attributes("ROTATION", "to") attributes_rot = TransformConstraintOp.min_max_attributes("ROTATION", "to")
attributes_loc = TransformConstraintOp.min_max_attributes("LOCATION", "to") attributes_loc = TransformConstraintOp.min_max_attributes("LOCATION", "to")
for morph_name, data, bname, morph_data_path, groups in bone_offset_map.values(): for morph_name, data, bname, morph_data_path, groups in bone_offset_map.values():
@@ -671,7 +662,7 @@ class _MorphSlider:
b.is_mmd_shadow_bone = True b.is_mmd_shadow_bone = True
b.mmd_shadow_bone_type = "BIND" b.mmd_shadow_bone_type = "BIND"
pb = armObj.pose.bones[data.bone] pb = armObj.pose.bones[data.bone]
__config_bone_morph(pb.constraints, "ROTATION", attributes_rot, pi, "pi") __config_bone_morph(pb.constraints, "ROTATION", attributes_rot, math.pi, "pi")
__config_bone_morph(pb.constraints, "LOCATION", attributes_loc, 100, "100") __config_bone_morph(pb.constraints, "LOCATION", attributes_loc, 100, "100")
# uv morphs # uv morphs
@@ -680,7 +671,7 @@ class _MorphSlider:
b = arm.pose.bones["mmd_bind_ctrl_base"] b = arm.pose.bones["mmd_bind_ctrl_base"]
b.is_mmd_shadow_bone = True b.is_mmd_shadow_bone = True
b.mmd_shadow_bone_type = "BIND" b.mmd_shadow_bone_type = "BIND"
for bname, data_path, scale_path, groups in (i for l in uv_morph_map.values() for i in l): for bname, data_path, scale_path, groups in (i for value_list in uv_morph_map.values() for i in value_list):
b = arm.pose.bones[bname] b = arm.pose.bones[bname]
b.is_mmd_shadow_bone = True b.is_mmd_shadow_bone = True
b.mmd_shadow_bone_type = "BIND" b.mmd_shadow_bone_type = "BIND"
@@ -694,9 +685,9 @@ class _MorphSlider:
group_dict = material_offset_map.get("group_dict", {}) group_dict = material_offset_map.get("group_dict", {})
def __config_material_morph(mat: Material, morph_list: List[Tuple[str, Any, str]]) -> None: def __config_material_morph(mat, morph_list):
nodes = _MaterialMorph.setup_morph_nodes(mat, tuple(x[1] for x in morph_list)) nodes = _MaterialMorph.setup_morph_nodes(mat, tuple(x[1] for x in morph_list))
for (morph_name, data, name_bind), node in zip(morph_list, nodes): for (morph_name, data, name_bind), node in zip(morph_list, nodes, strict=False):
node.label, node.name = morph_name, name_bind node.label, node.name = morph_name, name_bind
data_path, groups = group_dict[morph_name] data_path, groups = group_dict[morph_name]
driver, variables = self.__driver_variables(mat.node_tree, node.inputs[0].path_from_id("default_value")) driver, variables = self.__driver_variables(mat.node_tree, node.inputs[0].path_from_id("default_value"))
@@ -706,7 +697,7 @@ class _MorphSlider:
for mat in (m for m in rig.materials() if m and m.use_nodes and not m.name.startswith("mmd_")): for mat in (m for m in rig.materials() if m and m.use_nodes and not m.name.startswith("mmd_")):
mul_all, add_all = material_offset_map.get("#", ([], [])) mul_all, add_all = material_offset_map.get("#", ([], []))
if mat.name == "": if mat.name == "":
logger.warning("Oh no. The material name should never be empty.") logger.warning("Oh no. The material name should never empty.")
mul_list, add_list = [], [] mul_list, add_list = [], []
else: else:
mat_name = "#" + mat.name mat_name = "#" + mat.name
@@ -722,7 +713,7 @@ class _MorphSlider:
class MigrationFnMorph: class MigrationFnMorph:
@staticmethod @staticmethod
def update_mmd_morph() -> None: def update_mmd_morph():
from .material import FnMaterial from .material import FnMaterial
for root in bpy.data.objects: for root in bpy.data.objects:
@@ -733,7 +724,7 @@ class MigrationFnMorph:
for morph_data in mat_morph.data: for morph_data in mat_morph.data:
if morph_data.material_data is not None: if morph_data.material_data is not None:
# SUPPORT_UNTIL: 5 LTS # SUPPORT_UNTIL: 5 LTS
# The material_id is also no longer used, but for compatibility with older version mmd_tools, keep it. # The material_id is also no longer used, but for compatibility with older version mmd_tools_local, keep it.
if "material_id" not in morph_data.material_data.mmd_material or "material_id" not in morph_data or morph_data.material_data.mmd_material["material_id"] == morph_data["material_id"]: if "material_id" not in morph_data.material_data.mmd_material or "material_id" not in morph_data or morph_data.material_data.mmd_material["material_id"] == morph_data["material_id"]:
# In the new version, the related_mesh property is no longer used. # In the new version, the related_mesh property is no longer used.
# Explicitly remove this property to avoid misuse. # Explicitly remove this property to avoid misuse.
@@ -741,15 +732,14 @@ class MigrationFnMorph:
del morph_data["related_mesh"] del morph_data["related_mesh"]
continue continue
else: # Compat case. The new version mmd_tools_local saved. And old version mmd_tools_local edit. Then new version mmd_tools_local load again.
# Compat case. The new version mmd_tools saved. And old version mmd_tools edit. Then new version mmd_tools load again.
# Go update path. # Go update path.
pass pass
morph_data.material_data = None morph_data.material_data = None
if "material_id" in morph_data: if "material_id" in morph_data:
mat_id = morph_data["material_id"] mat_id = morph_data["material_id"]
if mat_id != -1: if mat_id >= 0:
fnMat = FnMaterial.from_material_id(mat_id) fnMat = FnMaterial.from_material_id(mat_id)
if fnMat: if fnMat:
morph_data.material_data = fnMat.material morph_data.material_data = fnMat.material
@@ -764,11 +754,11 @@ class MigrationFnMorph:
morph_data.related_mesh_data = bpy.data.meshes[related_mesh] morph_data.related_mesh_data = bpy.data.meshes[related_mesh]
@staticmethod @staticmethod
def ensure_material_id_not_conflict() -> None: def ensure_material_id_not_conflict():
mat_ids_set: Set[int] = set() mat_ids_set = set()
# The reference library properties cannot be modified and bypassed in advance. # The reference library properties cannot be modified and bypassed in advance.
need_update_mat: List[Material] = [] need_update_mat = []
for mat in bpy.data.materials: for mat in bpy.data.materials:
if mat.mmd_material.material_id < 0: if mat.mmd_material.material_id < 0:
continue continue
@@ -783,7 +773,7 @@ class MigrationFnMorph:
mat_ids_set.add(mat.mmd_material.material_id) mat_ids_set.add(mat.mmd_material.material_id)
@staticmethod @staticmethod
def compatible_with_old_version_mmd_tools() -> None: def compatible_with_old_version_mmd_tools_local():
MigrationFnMorph.ensure_material_id_not_conflict() MigrationFnMorph.ensure_material_id_not_conflict()
for root in bpy.data.objects: for root in bpy.data.objects:
+177 -177
View File
@@ -5,7 +5,7 @@
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. # Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import logging from .....core.logging_setup import logger
import os import os
import struct import struct
@@ -40,7 +40,7 @@ class FileStream:
def close(self): def close(self):
if self.__file_obj is not None: if self.__file_obj is not None:
logging.debug('close the file("%s")', self.__path) logger.debug('close the file("%s")', self.__path)
self.__file_obj.close() self.__file_obj.close()
self.__file_obj = None self.__file_obj = None
@@ -260,20 +260,20 @@ class Header:
return 4 return 4
def load(self, fs): def load(self, fs):
logging.info('loading pmx header information...') logger.info('loading pmx header information...')
self.sign = fs.readBytes(4) self.sign = fs.readBytes(4)
logging.debug('File signature is %s', self.sign) logger.debug('File signature is %s', self.sign)
if self.sign[:3] != self.PMX_SIGN[:3]: if self.sign[:3] != self.PMX_SIGN[:3]:
logging.info('File signature is invalid') logger.info('File signature is invalid')
logging.error('This file is unsupported format, or corrupt file.') logger.error('This file is unsupported format, or corrupt file.')
raise InvalidFileError('File signature is invalid.') raise InvalidFileError('File signature is invalid.')
self.version = fs.readFloat() self.version = fs.readFloat()
logging.info('pmx format version: %f', self.version) logger.info('pmx format version: %f', self.version)
if self.version != self.VERSION: if self.version != self.VERSION:
logging.error('PMX version %.1f is unsupported', self.version) logger.error('PMX version %.1f is unsupported', self.version)
raise UnsupportedVersionError('unsupported PMX version: %.1f'%self.version) raise UnsupportedVersionError('unsupported PMX version: %.1f'%self.version)
if fs.readByte() != 8 or self.sign[3] != self.PMX_SIGN[3]: if fs.readByte() != 8 or self.sign[3] != self.PMX_SIGN[3]:
logging.warning(' * This file might be corrupted.') logger.warning(' * This file might be corrupted.')
self.encoding = Encoding(fs.readByte()) self.encoding = Encoding(fs.readByte())
self.additional_uvs = fs.readByte() self.additional_uvs = fs.readByte()
self.vertex_index_size = fs.readByte() self.vertex_index_size = fs.readByte()
@@ -283,19 +283,19 @@ class Header:
self.morph_index_size = fs.readByte() self.morph_index_size = fs.readByte()
self.rigid_index_size = fs.readByte() self.rigid_index_size = fs.readByte()
logging.info('----------------------------') logger.info('----------------------------')
logging.info('pmx header information') logger.info('pmx header information')
logging.info('----------------------------') logger.info('----------------------------')
logging.info('pmx version: %.1f', self.version) logger.info('pmx version: %.1f', self.version)
logging.info('encoding: %s', str(self.encoding)) logger.info('encoding: %s', str(self.encoding))
logging.info('number of uvs: %d', self.additional_uvs) logger.info('number of uvs: %d', self.additional_uvs)
logging.info('vertex index size: %d byte(s)', self.vertex_index_size) logger.info('vertex index size: %d byte(s)', self.vertex_index_size)
logging.info('texture index: %d byte(s)', self.texture_index_size) logger.info('texture index: %d byte(s)', self.texture_index_size)
logging.info('material index: %d byte(s)', self.material_index_size) logger.info('material index: %d byte(s)', self.material_index_size)
logging.info('bone index: %d byte(s)', self.bone_index_size) logger.info('bone index: %d byte(s)', self.bone_index_size)
logging.info('morph index: %d byte(s)', self.morph_index_size) logger.info('morph index: %d byte(s)', self.morph_index_size)
logging.info('rigid index: %d byte(s)', self.rigid_index_size) logger.info('rigid index: %d byte(s)', self.rigid_index_size)
logging.info('----------------------------') logger.info('----------------------------')
def save(self, fs): def save(self, fs):
fs.writeBytes(self.PMX_SIGN) fs.writeBytes(self.PMX_SIGN)
@@ -364,27 +364,27 @@ class Model:
self.comment = fs.readStr() self.comment = fs.readStr()
self.comment_e = fs.readStr() self.comment_e = fs.readStr()
logging.info('Model name: %s', self.name) logger.info('Model name: %s', self.name)
logging.info('Model name(english): %s', self.name_e) logger.info('Model name(english): %s', self.name_e)
logging.info('Comment:%s', self.comment) logger.info('Comment:%s', self.comment)
logging.info('Comment(english):%s', self.comment_e) logger.info('Comment(english):%s', self.comment_e)
logging.info('') logger.info('')
logging.info('------------------------------') logger.info('------------------------------')
logging.info('Load Vertices') logger.info('Load Vertices')
logging.info('------------------------------') logger.info('------------------------------')
num_vertices = fs.readInt() num_vertices = fs.readInt()
self.vertices = [] self.vertices = []
for i in range(num_vertices): for i in range(num_vertices):
v = Vertex() v = Vertex()
v.load(fs) v.load(fs)
self.vertices.append(v) self.vertices.append(v)
logging.info('----- Loaded %d vertices', len(self.vertices)) logger.info('----- Loaded %d vertices', len(self.vertices))
logging.info('') logger.info('')
logging.info('------------------------------') logger.info('------------------------------')
logging.info(' Load Faces') logger.info(' Load Faces')
logging.info('------------------------------') logger.info('------------------------------')
num_faces = fs.readInt() num_faces = fs.readInt()
self.faces = [] self.faces = []
for i in range(int(num_faces/3)): for i in range(int(num_faces/3)):
@@ -392,25 +392,25 @@ class Model:
f2 = fs.readVertexIndex() f2 = fs.readVertexIndex()
f3 = fs.readVertexIndex() f3 = fs.readVertexIndex()
self.faces.append((f3, f2, f1)) self.faces.append((f3, f2, f1))
logging.info(' Load %d faces', len(self.faces)) logger.info(' Load %d faces', len(self.faces))
logging.info('') logger.info('')
logging.info('------------------------------') logger.info('------------------------------')
logging.info(' Load Textures') logger.info(' Load Textures')
logging.info('------------------------------') logger.info('------------------------------')
num_textures = fs.readInt() num_textures = fs.readInt()
self.textures = [] self.textures = []
for i in range(num_textures): for i in range(num_textures):
t = Texture() t = Texture()
t.load(fs) t.load(fs)
self.textures.append(t) self.textures.append(t)
logging.info('Texture %d: %s', i, t.path) logger.info('Texture %d: %s', i, t.path)
logging.info(' ----- Loaded %d textures', len(self.textures)) logger.info(' ----- Loaded %d textures', len(self.textures))
logging.info('') logger.info('')
logging.info('------------------------------') logger.info('------------------------------')
logging.info(' Load Materials') logger.info(' Load Materials')
logging.info('------------------------------') logger.info('------------------------------')
num_materials = fs.readInt() num_materials = fs.readInt()
self.materials = [] self.materials = []
for i in range(num_materials): for i in range(num_materials):
@@ -418,38 +418,38 @@ class Model:
m.load(fs, num_textures) m.load(fs, num_textures)
self.materials.append(m) self.materials.append(m)
logging.info('Material %d: %s', i, m.name) logger.info('Material %d: %s', i, m.name)
logging.debug(' Name(english): %s', m.name_e) logger.debug(' Name(english): %s', m.name_e)
logging.debug(' Comment: %s', m.comment) logger.debug(' Comment: %s', m.comment)
logging.debug(' Vertex Count: %d', m.vertex_count) logger.debug(' Vertex Count: %d', m.vertex_count)
logging.debug(' Diffuse: (%.2f, %.2f, %.2f, %.2f)', *m.diffuse) logger.debug(' Diffuse: (%.2f, %.2f, %.2f, %.2f)', *m.diffuse)
logging.debug(' Specular: (%.2f, %.2f, %.2f)', *m.specular) logger.debug(' Specular: (%.2f, %.2f, %.2f)', *m.specular)
logging.debug(' Shininess: %f', m.shininess) logger.debug(' Shininess: %f', m.shininess)
logging.debug(' Ambient: (%.2f, %.2f, %.2f)', *m.ambient) logger.debug(' Ambient: (%.2f, %.2f, %.2f)', *m.ambient)
logging.debug(' Double Sided: %s', str(m.is_double_sided)) logger.debug(' Double Sided: %s', str(m.is_double_sided))
logging.debug(' Drop Shadow: %s', str(m.enabled_drop_shadow)) logger.debug(' Drop Shadow: %s', str(m.enabled_drop_shadow))
logging.debug(' Self Shadow: %s', str(m.enabled_self_shadow)) logger.debug(' Self Shadow: %s', str(m.enabled_self_shadow))
logging.debug(' Self Shadow Map: %s', str(m.enabled_self_shadow_map)) logger.debug(' Self Shadow Map: %s', str(m.enabled_self_shadow_map))
logging.debug(' Edge: %s', str(m.enabled_toon_edge)) logger.debug(' Edge: %s', str(m.enabled_toon_edge))
logging.debug(' Edge Color: (%.2f, %.2f, %.2f, %.2f)', *m.edge_color) logger.debug(' Edge Color: (%.2f, %.2f, %.2f, %.2f)', *m.edge_color)
logging.debug(' Edge Size: %.2f', m.edge_size) logger.debug(' Edge Size: %.2f', m.edge_size)
if m.texture != -1: if m.texture != -1:
logging.debug(' Texture Index: %d', m.texture) logger.debug(' Texture Index: %d', m.texture)
else: else:
logging.debug(' Texture: None') logger.debug(' Texture: None')
if m.sphere_texture != -1: if m.sphere_texture != -1:
logging.debug(' Sphere Texture Index: %d', m.sphere_texture) logger.debug(' Sphere Texture Index: %d', m.sphere_texture)
logging.debug(' Sphere Texture Mode: %d', m.sphere_texture_mode) logger.debug(' Sphere Texture Mode: %d', m.sphere_texture_mode)
else: else:
logging.debug(' Sphere Texture: None') logger.debug(' Sphere Texture: None')
logging.debug('') logger.debug('')
logging.info('----- Loaded %d materials.', len(self.materials)) logger.info('----- Loaded %d materials.', len(self.materials))
logging.info('') logger.info('')
logging.info('------------------------------') logger.info('------------------------------')
logging.info(' Load Bones') logger.info(' Load Bones')
logging.info('------------------------------') logger.info('------------------------------')
num_bones = fs.readInt() num_bones = fs.readInt()
self.bones = [] self.bones = []
for i in range(num_bones): for i in range(num_bones):
@@ -457,33 +457,33 @@ class Model:
b.load(fs) b.load(fs)
self.bones.append(b) self.bones.append(b)
logging.info('Bone %d: %s', i, b.name) logger.info('Bone %d: %s', i, b.name)
logging.debug(' Name(english): %s', b.name_e) logger.debug(' Name(english): %s', b.name_e)
logging.debug(' Location: (%f, %f, %f)', *b.location) logger.debug(' Location: (%f, %f, %f)', *b.location)
logging.debug(' displayConnection: %s', str(b.displayConnection)) logger.debug(' displayConnection: %s', str(b.displayConnection))
logging.debug(' Parent: %s', str(b.parent)) logger.debug(' Parent: %s', str(b.parent))
logging.debug(' Transform Order: %s', str(b.transform_order)) logger.debug(' Transform Order: %s', str(b.transform_order))
logging.debug(' Rotatable: %s', str(b.isRotatable)) logger.debug(' Rotatable: %s', str(b.isRotatable))
logging.debug(' Movable: %s', str(b.isMovable)) logger.debug(' Movable: %s', str(b.isMovable))
logging.debug(' Visible: %s', str(b.visible)) logger.debug(' Visible: %s', str(b.visible))
logging.debug(' Controllable: %s', str(b.isControllable)) logger.debug(' Controllable: %s', str(b.isControllable))
logging.debug(' Additional Location: %s', str(b.hasAdditionalLocation)) logger.debug(' Additional Location: %s', str(b.hasAdditionalLocation))
logging.debug(' Additional Rotation: %s', str(b.hasAdditionalRotate)) logger.debug(' Additional Rotation: %s', str(b.hasAdditionalRotate))
if b.additionalTransform is not None: if b.additionalTransform is not None:
logging.debug(' Additional Transform: Bone:%d, influence: %f', *b.additionalTransform) logger.debug(' Additional Transform: Bone:%d, influence: %f', *b.additionalTransform)
logging.debug(' IK: %s', str(b.isIK)) logger.debug(' IK: %s', str(b.isIK))
if b.isIK: if b.isIK:
logging.debug(' Unit Angle: %f', b.rotationConstraint) logger.debug(' Unit Angle: %f', b.rotationConstraint)
logging.debug(' Target: %d', b.target) logger.debug(' Target: %d', b.target)
for j, link in enumerate(b.ik_links): for j, link in enumerate(b.ik_links):
logging.debug(' IK Link %d: %d, %s - %s', j, link.target, str(link.minimumAngle), str(link.maximumAngle)) logger.debug(' IK Link %d: %d, %s - %s', j, link.target, str(link.minimumAngle), str(link.maximumAngle))
logging.debug('') logger.debug('')
logging.info('----- Loaded %d bones.', len(self.bones)) logger.info('----- Loaded %d bones.', len(self.bones))
logging.info('') logger.info('')
logging.info('------------------------------') logger.info('------------------------------')
logging.info(' Load Morphs') logger.info(' Load Morphs')
logging.info('------------------------------') logger.info('------------------------------')
num_morph = fs.readInt() num_morph = fs.readInt()
self.morphs = [] self.morphs = []
display_categories = {0: 'System', 1: 'Eyebrow', 2: 'Eye', 3: 'Mouth', 4: 'Other'} display_categories = {0: 'System', 1: 'Eyebrow', 2: 'Eye', 3: 'Mouth', 4: 'Other'}
@@ -491,16 +491,16 @@ class Model:
m = Morph.create(fs) m = Morph.create(fs)
self.morphs.append(m) self.morphs.append(m)
logging.info('%s %d: %s', m.__class__.__name__, i, m.name) logger.info('%s %d: %s', m.__class__.__name__, i, m.name)
logging.debug(' Name(english): %s', m.name_e) logger.debug(' Name(english): %s', m.name_e)
logging.debug(' Category: %s (%d)', display_categories.get(m.category, '#Invalid'), m.category) logger.debug(' Category: %s (%d)', display_categories.get(m.category, '#Invalid'), m.category)
logging.debug('') logger.debug('')
logging.info('----- Loaded %d morphs.', len(self.morphs)) logger.info('----- Loaded %d morphs.', len(self.morphs))
logging.info('') logger.info('')
logging.info('------------------------------') logger.info('------------------------------')
logging.info(' Load Display Items') logger.info(' Load Display Items')
logging.info('------------------------------') logger.info('------------------------------')
num_disp = fs.readInt() num_disp = fs.readInt()
self.display = [] self.display = []
for i in range(num_disp): for i in range(num_disp):
@@ -508,15 +508,15 @@ class Model:
d.load(fs) d.load(fs)
self.display.append(d) self.display.append(d)
logging.info('Display Item %d: %s', i, d.name) logger.info('Display Item %d: %s', i, d.name)
logging.debug(' Name(english): %s', d.name_e) logger.debug(' Name(english): %s', d.name_e)
logging.debug('') logger.debug('')
logging.info('----- Loaded %d display items.', len(self.display)) logger.info('----- Loaded %d display items.', len(self.display))
logging.info('') logger.info('')
logging.info('------------------------------') logger.info('------------------------------')
logging.info(' Load Rigid Bodies') logger.info(' Load Rigid Bodies')
logging.info('------------------------------') logger.info('------------------------------')
num_rigid = fs.readInt() num_rigid = fs.readInt()
self.rigids = [] self.rigids = []
rigid_types = {0: 'Sphere', 1: 'Box', 2: 'Capsule'} rigid_types = {0: 'Sphere', 1: 'Box', 2: 'Capsule'}
@@ -525,27 +525,27 @@ class Model:
r = Rigid() r = Rigid()
r.load(fs) r.load(fs)
self.rigids.append(r) self.rigids.append(r)
logging.info('Rigid Body %d: %s', i, r.name) logger.info('Rigid Body %d: %s', i, r.name)
logging.debug(' Name(english): %s', r.name_e) logger.debug(' Name(english): %s', r.name_e)
logging.debug(' Type: %s', rigid_types[r.type]) logger.debug(' Type: %s', rigid_types[r.type])
logging.debug(' Mode: %s (%d)', rigid_modes.get(r.mode, '#Invalid'), r.mode) logger.debug(' Mode: %s (%d)', rigid_modes.get(r.mode, '#Invalid'), r.mode)
logging.debug(' Related bone: %s', r.bone) logger.debug(' Related bone: %s', r.bone)
logging.debug(' Collision group: %d', r.collision_group_number) logger.debug(' Collision group: %d', r.collision_group_number)
logging.debug(' Collision group mask: 0x%x', r.collision_group_mask) logger.debug(' Collision group mask: 0x%x', r.collision_group_mask)
logging.debug(' Size: (%f, %f, %f)', *r.size) logger.debug(' Size: (%f, %f, %f)', *r.size)
logging.debug(' Location: (%f, %f, %f)', *r.location) logger.debug(' Location: (%f, %f, %f)', *r.location)
logging.debug(' Rotation: (%f, %f, %f)', *r.rotation) logger.debug(' Rotation: (%f, %f, %f)', *r.rotation)
logging.debug(' Mass: %f', r.mass) logger.debug(' Mass: %f', r.mass)
logging.debug(' Bounce: %f', r.bounce) logger.debug(' Bounce: %f', r.bounce)
logging.debug(' Friction: %f', r.friction) logger.debug(' Friction: %f', r.friction)
logging.debug('') logger.debug('')
logging.info('----- Loaded %d rigid bodies.', len(self.rigids)) logger.info('----- Loaded %d rigid bodies.', len(self.rigids))
logging.info('') logger.info('')
logging.info('------------------------------') logger.info('------------------------------')
logging.info(' Load Joints') logger.info(' Load Joints')
logging.info('------------------------------') logger.info('------------------------------')
num_joints = fs.readInt() num_joints = fs.readInt()
self.joints = [] self.joints = []
for i in range(num_joints): for i in range(num_joints):
@@ -553,19 +553,19 @@ class Model:
j.load(fs) j.load(fs)
self.joints.append(j) self.joints.append(j)
logging.info('Joint %d: %s', i, j.name) logger.info('Joint %d: %s', i, j.name)
logging.debug(' Name(english): %s', j.name_e) logger.debug(' Name(english): %s', j.name_e)
logging.debug(' Rigid A: %s', j.src_rigid) logger.debug(' Rigid A: %s', j.src_rigid)
logging.debug(' Rigid B: %s', j.dest_rigid) logger.debug(' Rigid B: %s', j.dest_rigid)
logging.debug(' Location: (%f, %f, %f)', *j.location) logger.debug(' Location: (%f, %f, %f)', *j.location)
logging.debug(' Rotation: (%f, %f, %f)', *j.rotation) logger.debug(' Rotation: (%f, %f, %f)', *j.rotation)
logging.debug(' Location Limit: (%f, %f, %f) - (%f, %f, %f)', *(j.minimum_location + j.maximum_location)) logger.debug(' Location Limit: (%f, %f, %f) - (%f, %f, %f)', *(j.minimum_location + j.maximum_location))
logging.debug(' Rotation Limit: (%f, %f, %f) - (%f, %f, %f)', *(j.minimum_rotation + j.maximum_rotation)) logger.debug(' Rotation Limit: (%f, %f, %f) - (%f, %f, %f)', *(j.minimum_rotation + j.maximum_rotation))
logging.debug(' Spring: (%f, %f, %f)', *j.spring_constant) logger.debug(' Spring: (%f, %f, %f)', *j.spring_constant)
logging.debug(' Spring(rotation): (%f, %f, %f)', *j.spring_rotation_constant) logger.debug(' Spring(rotation): (%f, %f, %f)', *j.spring_rotation_constant)
logging.debug('') logger.debug('')
logging.info('----- Loaded %d joints.', len(self.joints)) logger.info('----- Loaded %d joints.', len(self.joints))
def save(self, fs): def save(self, fs):
fs.writeStr(self.name) fs.writeStr(self.name)
@@ -574,7 +574,7 @@ class Model:
fs.writeStr(self.comment) fs.writeStr(self.comment)
fs.writeStr(self.comment_e) fs.writeStr(self.comment_e)
logging.info('''exportings pmx model data... logger.info('''exportings pmx model data...
name: %s name: %s
name(english): %s name(english): %s
comment: comment:
@@ -583,62 +583,62 @@ comment(english):
%s %s
''', self.name, self.name_e, self.comment, self.comment_e) ''', self.name, self.name_e, self.comment, self.comment_e)
logging.info('exporting vertices... %d', len(self.vertices)) logger.info('exporting vertices... %d', len(self.vertices))
fs.writeInt(len(self.vertices)) fs.writeInt(len(self.vertices))
for i in self.vertices: for i in self.vertices:
i.save(fs) i.save(fs)
logging.info('finished exporting vertices.') logger.info('finished exporting vertices.')
logging.info('exporting faces... %d', len(self.faces)) logger.info('exporting faces... %d', len(self.faces))
fs.writeInt(len(self.faces)*3) fs.writeInt(len(self.faces)*3)
for f3, f2, f1 in self.faces: for f3, f2, f1 in self.faces:
fs.writeVertexIndex(f1) fs.writeVertexIndex(f1)
fs.writeVertexIndex(f2) fs.writeVertexIndex(f2)
fs.writeVertexIndex(f3) fs.writeVertexIndex(f3)
logging.info('finished exporting faces.') logger.info('finished exporting faces.')
logging.info('exporting textures... %d', len(self.textures)) logger.info('exporting textures... %d', len(self.textures))
fs.writeInt(len(self.textures)) fs.writeInt(len(self.textures))
for i in self.textures: for i in self.textures:
i.save(fs) i.save(fs)
logging.info('finished exporting textures.') logger.info('finished exporting textures.')
logging.info('exporting materials... %d', len(self.materials)) logger.info('exporting materials... %d', len(self.materials))
fs.writeInt(len(self.materials)) fs.writeInt(len(self.materials))
for i in self.materials: for i in self.materials:
i.save(fs) i.save(fs)
logging.info('finished exporting materials.') logger.info('finished exporting materials.')
logging.info('exporting bones... %d', len(self.bones)) logger.info('exporting bones... %d', len(self.bones))
fs.writeInt(len(self.bones)) fs.writeInt(len(self.bones))
for i in self.bones: for i in self.bones:
i.save(fs) i.save(fs)
logging.info('finished exporting bones.') logger.info('finished exporting bones.')
logging.info('exporting morphs... %d', len(self.morphs)) logger.info('exporting morphs... %d', len(self.morphs))
fs.writeInt(len(self.morphs)) fs.writeInt(len(self.morphs))
for i in self.morphs: for i in self.morphs:
i.save(fs) i.save(fs)
logging.info('finished exporting morphs.') logger.info('finished exporting morphs.')
logging.info('exporting display items... %d', len(self.display)) logger.info('exporting display items... %d', len(self.display))
fs.writeInt(len(self.display)) fs.writeInt(len(self.display))
for i in self.display: for i in self.display:
i.save(fs) i.save(fs)
logging.info('finished exporting display items.') logger.info('finished exporting display items.')
logging.info('exporting rigid bodies... %d', len(self.rigids)) logger.info('exporting rigid bodies... %d', len(self.rigids))
fs.writeInt(len(self.rigids)) fs.writeInt(len(self.rigids))
for i in self.rigids: for i in self.rigids:
i.save(fs) i.save(fs)
logging.info('finished exporting rigid bodies.') logger.info('finished exporting rigid bodies.')
logging.info('exporting joints... %d', len(self.joints)) logger.info('exporting joints... %d', len(self.joints))
fs.writeInt(len(self.joints)) fs.writeInt(len(self.joints))
for i in self.joints: for i in self.joints:
i.save(fs) i.save(fs)
logging.info('finished exporting joints.') logger.info('finished exporting joints.')
logging.info('finished exporting the model.') logger.info('finished exporting the model.')
def __repr__(self): def __repr__(self):
@@ -803,7 +803,7 @@ class Texture:
except ValueError: except ValueError:
relPath = self.path relPath = self.path
relPath = relPath.replace(os.path.sep, '\\') # always save using windows path conventions relPath = relPath.replace(os.path.sep, '\\') # always save using windows path conventions
logging.info('writing to pmx file the relative texture path: %s', relPath) logger.info('writing to pmx file the relative texture path: %s', relPath)
fs.writeStr(relPath) fs.writeStr(relPath)
class SharedTexture(Texture): class SharedTexture(Texture):
@@ -1170,7 +1170,7 @@ class Morph:
name = fs.readStr() name = fs.readStr()
name_e = fs.readStr() name_e = fs.readStr()
logging.debug('morph: %s', name) logger.debug('morph: %s', name)
category = fs.readSignedByte() category = fs.readSignedByte()
typeIndex = fs.readSignedByte() typeIndex = fs.readSignedByte()
ret = _CLASSES[typeIndex](name, name_e, category, type_index = typeIndex) ret = _CLASSES[typeIndex](name, name_e, category, type_index = typeIndex)
@@ -1399,7 +1399,7 @@ class Display:
else: else:
raise Exception('invalid value.') raise Exception('invalid value.')
self.data.append((disp_type, index)) self.data.append((disp_type, index))
logging.debug('the number of display elements: %d', len(self.data)) logger.debug('the number of display elements: %d', len(self.data))
def save(self, fs): def save(self, fs):
fs.writeStr(self.name) fs.writeStr(self.name)
@@ -1595,12 +1595,12 @@ class Joint:
def load(path): def load(path):
with FileReadStream(path) as fs: with FileReadStream(path) as fs:
logging.info('****************************************') logger.info('****************************************')
logging.info(' mmd_tools.pmx module') logger.info(' mmd_tools.pmx module')
logging.info('----------------------------------------') logger.info('----------------------------------------')
logging.info(' Start to load model data form a pmx file') logger.info(' Start to load model data form a pmx file')
logging.info(' by the mmd_tools.pmx modlue.') logger.info(' by the mmd_tools.pmx modlue.')
logging.info('') logger.info('')
header = Header() header = Header()
header.load(fs) header.load(fs)
fs.setHeader(header) fs.setHeader(header)
@@ -1608,12 +1608,12 @@ def load(path):
try: try:
model.load(fs) model.load(fs)
except struct.error as e: except struct.error as e:
logging.error(' * Corrupted file: %s', e) logger.error(' * Corrupted file: %s', e)
#raise #raise
logging.info(' Finished loading.') logger.info(' Finished loading.')
logging.info('----------------------------------------') logger.info('----------------------------------------')
logging.info(' mmd_tools.pmx module') logger.info(' mmd_tools.pmx module')
logging.info('****************************************') logger.info('****************************************')
return model return model
def save(path, model, add_uv_count=0): def save(path, model, add_uv_count=0):
+68 -31
View File
@@ -6,6 +6,7 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import collections import collections
import math
import os import os
import time import time
from typing import TYPE_CHECKING, List, Optional, Dict, Tuple, Set, Callable, Any, Union, FrozenSet, Iterator from typing import TYPE_CHECKING, List, Optional, Dict, Tuple, Set, Callable, Any, Union, FrozenSet, Iterator
@@ -103,7 +104,7 @@ class PMXImporter:
obj_name = self.__safe_name(bpy.path.display_name(pmxModel.filepath), max_length=54) obj_name = self.__safe_name(bpy.path.display_name(pmxModel.filepath), max_length=54)
logger.info(f"Creating objects for model: {obj_name}") logger.info(f"Creating objects for model: {obj_name}")
self.__rig = Model.create(pmxModel.name, pmxModel.name_e, self.__scale or 1.0, obj_name) self.__rig = Model.create(pmxModel.name, pmxModel.name_e, self.__scale, obj_name)
root = self.__rig.rootObject() root = self.__rig.rootObject()
mmd_root: 'MMDRoot' = root.mmd_root mmd_root: 'MMDRoot' = root.mmd_root
self.__root = root self.__root = root
@@ -192,7 +193,7 @@ class PMXImporter:
mesh: Mesh = self.__meshObj.data mesh: Mesh = self.__meshObj.data
mesh.vertices.add(count=vertex_count) mesh.vertices.add(count=vertex_count)
mesh.vertices.foreach_set("co", tuple(i for pv in pmx_vertices for i in (Vector(pv.co).xzy * (self.__scale or 1.0)))) mesh.vertices.foreach_set("co", tuple(i for pv in pmx_vertices for i in (Vector(pv.co).xzy * self.__scale)))
vertex_group_table = self.__vertexGroupTable vertex_group_table = self.__vertexGroupTable
if not vertex_group_table: if not vertex_group_table:
@@ -249,9 +250,9 @@ class PMXImporter:
for i, pv in self.__sdefVertices.items(): for i, pv in self.__sdefVertices.items():
w = pv.weight.weights w = pv.weight.weights
sdefC.data[i].co = Vector(w.c).xzy * (self.__scale or 1.0) sdefC.data[i].co = Vector(w.c).xzy * self.__scale
sdefR0.data[i].co = Vector(w.r0).xzy * (self.__scale or 1.0) sdefR0.data[i].co = Vector(w.r0).xzy * self.__scale
sdefR1.data[i].co = Vector(w.r1).xzy * (self.__scale or 1.0) sdefR1.data[i].co = Vector(w.r1).xzy * self.__scale
logger.debug(f"Stored {len(self.__sdefVertices)} SDEF vertices in shape keys") logger.debug(f"Stored {len(self.__sdefVertices)} SDEF vertices in shape keys")
@@ -290,13 +291,13 @@ class PMXImporter:
# Create bones # Create bones
for i in pmx_bones: for i in pmx_bones:
bone = data.edit_bones.new(name=i.name) bone = data.edit_bones.new(name=i.name)
loc = _VectorXZY(i.location) * (self.__scale or 1.0) loc = _VectorXZY(i.location) * self.__scale
bone.head = loc bone.head = loc
editBoneTable.append(bone) editBoneTable.append(bone)
nameTable.append(bone.name) nameTable.append(bone.name)
# Set parent relationships # Set parent relationships
for i, (b_bone, m_bone) in enumerate(zip(editBoneTable, pmx_bones)): for i, (b_bone, m_bone) in enumerate(zip(editBoneTable, pmx_bones, strict=False)):
if m_bone.parent != -1: if m_bone.parent != -1:
if i not in dependency_cycle_ik_bones: if i not in dependency_cycle_ik_bones:
b_bone.parent = editBoneTable[m_bone.parent] b_bone.parent = editBoneTable[m_bone.parent]
@@ -304,18 +305,18 @@ class PMXImporter:
b_bone.parent = editBoneTable[m_bone.parent].parent b_bone.parent = editBoneTable[m_bone.parent].parent
# Set tail positions # Set tail positions
for b_bone, m_bone in zip(editBoneTable, pmx_bones): for b_bone, m_bone in zip(editBoneTable, pmx_bones, strict=False):
if isinstance(m_bone.displayConnection, int): if isinstance(m_bone.displayConnection, int):
if m_bone.displayConnection != -1: if m_bone.displayConnection != -1:
b_bone.tail = editBoneTable[m_bone.displayConnection].head b_bone.tail = editBoneTable[m_bone.displayConnection].head
else: else:
b_bone.tail = b_bone.head b_bone.tail = b_bone.head
else: else:
loc = _VectorXZY(m_bone.displayConnection) * (self.__scale or 1.0) loc = _VectorXZY(m_bone.displayConnection) * self.__scale
b_bone.tail = b_bone.head + loc b_bone.tail = b_bone.head + loc
# Check and fix IK links # Check and fix IK links
for b_bone, m_bone in zip(editBoneTable, pmx_bones): for b_bone, m_bone in zip(editBoneTable, pmx_bones, strict=False):
if m_bone.isIK and m_bone.target != -1: if m_bone.isIK and m_bone.target != -1:
logger.debug(f"Checking IK links of {b_bone.name}") logger.debug(f"Checking IK links of {b_bone.name}")
b_target = editBoneTable[m_bone.target] b_target = editBoneTable[m_bone.target]
@@ -333,30 +334,30 @@ class PMXImporter:
b_bone_link.tail = b_bone_link.head + loc b_bone_link.tail = b_bone_link.head + loc
# Fix too short bones # Fix too short bones
for b_bone, m_bone in zip(editBoneTable, pmx_bones): for b_bone, m_bone in zip(editBoneTable, pmx_bones, strict=False):
# Set the length of too short bones to 1 because Blender delete them. # Set the length of too short bones to 1 because Blender delete them.
if b_bone.length < 0.001: if b_bone.length < 0.001:
if not self.__apply_bone_fixed_axis and m_bone.axis is not None: if not self.__apply_bone_fixed_axis and m_bone.axis is not None:
fixed_axis = Vector(m_bone.axis) fixed_axis = Vector(m_bone.axis)
if fixed_axis.length: if fixed_axis.length:
b_bone.tail = b_bone.head + fixed_axis.xzy.normalized() * (self.__scale or 1.0) b_bone.tail = b_bone.head + fixed_axis.xzy.normalized() * self.__scale
else: else:
b_bone.tail = b_bone.head + Vector((0, 0, 1)) * (self.__scale or 1.0) b_bone.tail = b_bone.head + Vector((0, 0, 1)) * self.__scale
else: else:
b_bone.tail = b_bone.head + Vector((0, 0, 1)) * (self.__scale or 1.0) b_bone.tail = b_bone.head + Vector((0, 0, 1)) * self.__scale
if m_bone.displayConnection != -1 and m_bone.displayConnection != [0.0, 0.0, 0.0]: if m_bone.displayConnection != -1 and m_bone.displayConnection != [0.0, 0.0, 0.0]:
logger.debug(f"Special tip bone {b_bone.name}, display {str(m_bone.displayConnection)}") logger.debug(f"Special tip bone {b_bone.name}, display {str(m_bone.displayConnection)}")
specialTipBones.append(b_bone.name) specialTipBones.append(b_bone.name)
# Update bone roll # Update bone roll
for b_bone, m_bone in zip(editBoneTable, pmx_bones): for b_bone, m_bone in zip(editBoneTable, pmx_bones, strict=False):
if m_bone.localCoordinate is not None: if m_bone.localCoordinate is not None:
FnBone.update_bone_roll(b_bone, m_bone.localCoordinate.x_axis, m_bone.localCoordinate.z_axis) FnBone.update_bone_roll(b_bone, m_bone.localCoordinate.x_axis, m_bone.localCoordinate.z_axis)
elif FnBone.has_auto_local_axis(m_bone.name): elif FnBone.has_auto_local_axis(m_bone.name):
FnBone.update_auto_bone_roll(b_bone) FnBone.update_auto_bone_roll(b_bone)
# Set bone connections # Set bone connections
for b_bone, m_bone in zip(editBoneTable, pmx_bones): for b_bone, m_bone in zip(editBoneTable, pmx_bones, strict=False):
if isinstance(m_bone.displayConnection, int) and m_bone.displayConnection >= 0: if isinstance(m_bone.displayConnection, int) and m_bone.displayConnection >= 0:
t = editBoneTable[m_bone.displayConnection] t = editBoneTable[m_bone.displayConnection]
if t.parent is None or t.parent != b_bone: if t.parent is None or t.parent != b_bone:
@@ -534,7 +535,8 @@ class PMXImporter:
elif b_bone.name in specialTipBones: elif b_bone.name in specialTipBones:
mmd_bone.is_tip = True mmd_bone.is_tip = True
b_bone.bone.hide = not pmx_bone.visible # or mmd_bone.is_tip # Blender 5.0: use pose bone hide for Pose/Object mode visibility
b_bone.hide = not pmx_bone.visible # or mmd_bone.is_tip
if not pmx_bone.isRotatable: if not pmx_bone.isRotatable:
b_bone.lock_rotation = [True, True, True] b_bone.lock_rotation = [True, True, True]
@@ -589,7 +591,7 @@ class PMXImporter:
) )
for i, (rigid, rigid_obj) in enumerate(zip(self.__model.rigids, rigid_pool)): for i, (rigid, rigid_obj) in enumerate(zip(self.__model.rigids, rigid_pool)):
loc = Vector(rigid.location).xzy * (self.__scale or 1.0) loc = Vector(rigid.location).xzy * self.__scale
rot = Vector(rigid.rotation).xzy * -1 rot = Vector(rigid.rotation).xzy * -1
size = Vector(rigid.size).xzy if rigid.type == pmx.Rigid.TYPE_BOX else Vector(rigid.size) size = Vector(rigid.size).xzy if rigid.type == pmx.Rigid.TYPE_BOX else Vector(rigid.size)
@@ -598,7 +600,7 @@ class PMXImporter:
shape_type=rigid.type, shape_type=rigid.type,
location=loc, location=loc,
rotation=rot, rotation=rot,
size=size * (self.__scale or 1.0), size=size * self.__scale,
dynamics_type=rigid.mode, dynamics_type=rigid.mode,
name=rigid.name, name=rigid.name,
name_e=rigid.name_e, name_e=rigid.name_e,
@@ -636,7 +638,7 @@ class PMXImporter:
) )
for i, (joint, joint_obj) in enumerate(zip(self.__model.joints, joint_pool)): for i, (joint, joint_obj) in enumerate(zip(self.__model.joints, joint_pool)):
loc = Vector(joint.location).xzy * (self.__scale or 1.0) loc = Vector(joint.location).xzy * self.__scale
rot = Vector(joint.rotation).xzy * -1 rot = Vector(joint.rotation).xzy * -1
obj = FnRigidBody.setup_joint_object( obj = FnRigidBody.setup_joint_object(
@@ -647,8 +649,8 @@ class PMXImporter:
rotation=rot, rotation=rot,
rigid_a=self.__rigidTable.get(joint.src_rigid, None), rigid_a=self.__rigidTable.get(joint.src_rigid, None),
rigid_b=self.__rigidTable.get(joint.dest_rigid, None), rigid_b=self.__rigidTable.get(joint.dest_rigid, None),
maximum_location=Vector(joint.maximum_location).xzy * (self.__scale or 1.0), maximum_location=Vector(joint.maximum_location).xzy * self.__scale,
minimum_location=Vector(joint.minimum_location).xzy * (self.__scale or 1.0), minimum_location=Vector(joint.minimum_location).xzy * self.__scale,
maximum_rotation=Vector(joint.minimum_rotation).xzy * -1, maximum_rotation=Vector(joint.minimum_rotation).xzy * -1,
minimum_rotation=Vector(joint.maximum_rotation).xzy * -1, minimum_rotation=Vector(joint.maximum_rotation).xzy * -1,
spring_linear=Vector(joint.spring_constant).xzy, spring_linear=Vector(joint.spring_constant).xzy,
@@ -752,7 +754,7 @@ class PMXImporter:
uv_layer.data.foreach_set("uv", tuple(v for i in loop_indices_orig for v in uv_table[i])) uv_layer.data.foreach_set("uv", tuple(v for i in loop_indices_orig for v in uv_table[i]))
if hasattr(mesh, "uv_textures"): if hasattr(mesh, "uv_textures"):
for bf, mi in zip(uv_tex.data, material_indices): for bf, mi in zip(uv_tex.data, material_indices, strict=False):
bf.image = self.__imageTable.get(mi, None) bf.image = self.__imageTable.get(mi, None)
if pmxModel.header and pmxModel.header.additional_uvs: if pmxModel.header and pmxModel.header.additional_uvs:
@@ -829,14 +831,18 @@ class PMXImporter:
logger.debug(f"Found {len(vertex_morphs)} vertex morphs") logger.debug(f"Found {len(vertex_morphs)} vertex morphs")
for morph in vertex_morphs: for morph in vertex_morphs:
shapeKey = self.__meshObj.shape_key_add(name=morph.name) shapeKey = self.__meshObj.shape_key_add(name=morph.name, from_mix=False)
shapeKey.value = 0.0 # Set shape key value to 0 (inactive) on import
vtx_morph = mmd_root.vertex_morphs.add() vtx_morph = mmd_root.vertex_morphs.add()
vtx_morph.name = morph.name vtx_morph.name = morph.name
vtx_morph.name_e = morph.name_e vtx_morph.name_e = morph.name_e
vtx_morph.category = categories.get(morph.category, "OTHER") vtx_morph.category = categories.get(morph.category, "OTHER")
for md in morph.offsets: for md in morph.offsets:
if md.index < len(shapeKey.data):
shapeKeyPoint = shapeKey.data[md.index] shapeKeyPoint = shapeKey.data[md.index]
shapeKeyPoint.co += Vector(md.offset).xzy * (self.__scale or 1.0) shapeKeyPoint.co += Vector(md.offset).xzy * self.__scale
else:
logger.warning(f"Morph {morph.name} has out-of-range vertex index: {md.index}")
logger.debug(f"Imported vertex morph: {morph.name} with {len(morph.offsets)} offsets") logger.debug(f"Imported vertex morph: {morph.name} with {len(morph.offsets)} offsets")
def __importMaterialMorphs(self) -> None: def __importMaterialMorphs(self) -> None:
@@ -897,7 +903,7 @@ class PMXImporter:
data = bone_morph.data.add() data = bone_morph.data.add()
bl_bone = self.__boneTable[morph_data.index] bl_bone = self.__boneTable[morph_data.index]
data.bone = bl_bone.name data.bone = bl_bone.name
converter = BoneConverter(bl_bone, self.__scale or 1.0) converter = BoneConverter(bl_bone, self.__scale)
data.location = converter.convert_location(morph_data.location_offset) data.location = converter.convert_location(morph_data.location_offset)
data.rotation = converter.convert_rotation(morph_data.rotation_offset) data.rotation = converter.convert_rotation(morph_data.rotation_offset)
valid_offsets += 1 valid_offsets += 1
@@ -1000,12 +1006,19 @@ class PMXImporter:
armModifier = meshObj.modifiers.new(name="Armature", type="ARMATURE") armModifier = meshObj.modifiers.new(name="Armature", type="ARMATURE")
armModifier.object = armObj armModifier.object = armObj
armModifier.use_vertex_groups = True armModifier.use_vertex_groups = True
armModifier.name = "mmd_bone_order_override" armModifier.name = "mmd_armature"
armModifier.show_render = armModifier.show_viewport = len(meshObj.data.vertices) > 0
logger.debug("Armature modifier added") logger.debug("Armature modifier added")
def __assignCustomNormals(self) -> None: def __assignCustomNormals(self) -> None:
"""Assign custom normals to the mesh""" """Assign custom normals to the mesh"""
# NOTE: This uses the older Blender API instead of the newer mesh.attributes approach
# because it requires "INT16_2D" format for proper functionality.
# Manual calculation of normals in INT16_2D format is overly complex.
# The newer implementation was removed in commit [ad47b9a] due to these issues.
# The current implementation uses normals_split_custom_set() with 179-degree sharp edge
# marking as a workaround. While not ideal, this remains the most practical solution
# for preserving custom normals in most cases.
if not self.__meshObj or not self.__model: if not self.__meshObj or not self.__model:
logger.error("Mesh object or model not created") logger.error("Mesh object or model not created")
return return
@@ -1013,17 +1026,41 @@ class PMXImporter:
mesh: Mesh = self.__meshObj.data mesh: Mesh = self.__meshObj.data
logger.info("Setting custom normals...") logger.info("Setting custom normals...")
# CRITICAL: Mark sharp edges (based on angle) BEFORE setting custom normals
# For mesh.normals_split_custom_set() to work as expected, two conditions must be met:
# 1. The normal vectors must be non-zero (mentioned in Blender documentation)
# 2. Some edges must be marked as sharp (NOT mentioned in Blender documentation)
# An angle of 179 degrees is confirmed to be sufficient to preserve all custom normals.
# 180 degrees does not work because it misses some sharp edges required for normals_split_custom_set to work 100% correctly.
current_mode = bpy.context.active_object.mode if bpy.context.active_object else 'OBJECT'
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.select_all(action="DESELECT")
bpy.context.view_layer.objects.active = self.__meshObj
# Mark sharp edges
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.select_all(action="DESELECT")
bpy.ops.mesh.edges_select_sharp(sharpness=math.radians(179))
bpy.ops.mesh.mark_sharp()
bpy.ops.object.mode_set(mode="OBJECT")
# Logging
total_edges = len(mesh.edges)
sharp_edges = sum(1 for edge in mesh.edges if edge.use_edge_sharp)
percentage = (sharp_edges / total_edges) * 100 if total_edges > 0 else 0
logger.info(f" - Marked {sharp_edges}/{total_edges} ({percentage:.2f}%) sharp edges with angle: 179 degrees")
if self.__vertex_map: if self.__vertex_map:
verts, faces = self.__model.vertices, self.__model.faces verts, faces = self.__model.vertices, self.__model.faces
custom_normals = [(Vector(verts[i].normal).xzy).normalized() for f in faces for i in f] custom_normals = [(Vector(verts[i].normal).xzy).normalized() for f in faces for i in f]
mesh.normals_split_custom_set(custom_normals) mesh.normals_split_custom_set(custom_normals)
logger.debug(f"Set {len(custom_normals)} custom normals using face data")
else: else:
custom_normals = [(Vector(v.normal).xzy).normalized() for v in self.__model.vertices] custom_normals = [(Vector(v.normal).xzy).normalized() for v in self.__model.vertices]
mesh.normals_split_custom_set_from_vertices(custom_normals) mesh.normals_split_custom_set_from_vertices(custom_normals)
logger.debug(f"Set {len(custom_normals)} custom normals from vertices")
logger.info("Custom normals set successfully") bpy.ops.object.mode_set(mode=current_mode)
logger.info(" - Done!!")
# Continue without custom normals - mesh will use auto-calculated normals
def __renameLRBones(self, use_underscore: bool) -> None: def __renameLRBones(self, use_underscore: bool) -> None:
"""Rename bones with left/right naming convention""" """Rename bones with left/right naming convention"""
+19 -44
View File
@@ -1,17 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors # Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender # This file is part of MMD Tools.
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import List, Optional, Tuple, Union, Dict, Any, Set, cast from ....core.logging_setup import logger
from typing import List, Optional
import bpy import bpy
from mathutils import Euler, Vector, Matrix from mathutils import Euler, Vector
from ..bpyutils import FnContext, Props from ..bpyutils import FnContext, Props
from ....core.logging_setup import logger
SHAPE_SPHERE = 0 SHAPE_SPHERE = 0
SHAPE_BOX = 1 SHAPE_BOX = 1
@@ -22,30 +18,25 @@ MODE_DYNAMIC = 1
MODE_DYNAMIC_BONE = 2 MODE_DYNAMIC_BONE = 2
def shapeType(collision_shape: str) -> int: def shapeType(collision_shape):
"""Convert collision shape name to type index"""
return ("SPHERE", "BOX", "CAPSULE").index(collision_shape) return ("SPHERE", "BOX", "CAPSULE").index(collision_shape)
def collisionShape(shape_type: int) -> str: def collisionShape(shape_type):
"""Convert shape type index to collision shape name"""
return ("SPHERE", "BOX", "CAPSULE")[shape_type] return ("SPHERE", "BOX", "CAPSULE")[shape_type]
def setRigidBodyWorldEnabled(enable: bool) -> bool: def setRigidBodyWorldEnabled(enable):
"""Enable or disable the rigid body world and return previous state"""
if bpy.ops.rigidbody.world_add.poll(): if bpy.ops.rigidbody.world_add.poll():
logger.debug("Creating rigid body world")
bpy.ops.rigidbody.world_add() bpy.ops.rigidbody.world_add()
rigidbody_world = bpy.context.scene.rigidbody_world rigidbody_world = bpy.context.scene.rigidbody_world
enabled = rigidbody_world.enabled enabled = rigidbody_world.enabled
rigidbody_world.enabled = enable rigidbody_world.enabled = enable
logger.debug(f"Rigid body world enabled: {enable} (was: {enabled})")
return enabled return enabled
class RigidBodyMaterial: class RigidBodyMaterial:
COLORS: List[int] = [ COLORS = [
0x7FDDD4, 0x7FDDD4,
0xF0E68C, 0xF0E68C,
0xEE82EE, 0xEE82EE,
@@ -65,12 +56,10 @@ class RigidBodyMaterial:
] ]
@classmethod @classmethod
def getMaterial(cls, number: int) -> bpy.types.Material: def getMaterial(cls, number):
"""Get or create a material for rigid bodies with the specified number"""
number = int(number) number = int(number)
material_name = f"mmd_tools_rigid_{number}" material_name = "mmd_tools_rigid_%d" % (number)
if material_name not in bpy.data.materials: if material_name not in bpy.data.materials:
logger.debug(f"Creating rigid body material: {material_name}")
mat = bpy.data.materials.new(material_name) mat = bpy.data.materials.new(material_name)
color = cls.COLORS[number] color = cls.COLORS[number]
mat.diffuse_color[:3] = [((0xFF0000 & color) >> 16) / float(255), ((0x00FF00 & color) >> 8) / float(255), (0x0000FF & color) / float(255)] mat.diffuse_color[:3] = [((0xFF0000 & color) >> 16) / float(255), ((0x00FF00 & color) >> 8) / float(255), (0x0000FF & color) / float(255)]
@@ -97,11 +86,9 @@ class RigidBodyMaterial:
class FnRigidBody: class FnRigidBody:
@staticmethod @staticmethod
def new_rigid_body_objects(context: bpy.types.Context, parent_object: bpy.types.Object, count: int) -> List[bpy.types.Object]: def new_rigid_body_objects(context: bpy.types.Context, parent_object: bpy.types.Object, count: int) -> List[bpy.types.Object]:
"""Create multiple rigid body objects parented to the specified object"""
if count < 1: if count < 1:
return [] return []
logger.debug(f"Creating {count} rigid body objects parented to {parent_object.name}")
obj = FnRigidBody.new_rigid_body_object(context, parent_object) obj = FnRigidBody.new_rigid_body_object(context, parent_object)
if count == 1: if count == 1:
@@ -111,8 +98,6 @@ class FnRigidBody:
@staticmethod @staticmethod
def new_rigid_body_object(context: bpy.types.Context, parent_object: bpy.types.Object) -> bpy.types.Object: def new_rigid_body_object(context: bpy.types.Context, parent_object: bpy.types.Object) -> bpy.types.Object:
"""Create a new rigid body object parented to the specified object"""
logger.debug(f"Creating new rigid body object parented to {parent_object.name}")
obj = FnContext.new_and_link_object(context, name="Rigidbody", object_data=bpy.data.meshes.new(name="Rigidbody")) obj = FnContext.new_and_link_object(context, name="Rigidbody", object_data=bpy.data.meshes.new(name="Rigidbody"))
obj.parent = parent_object obj.parent = parent_object
obj.mmd_type = "RIGID_BODY" obj.mmd_type = "RIGID_BODY"
@@ -130,11 +115,11 @@ class FnRigidBody:
@staticmethod @staticmethod
def setup_rigid_body_object( def setup_rigid_body_object(
obj: bpy.types.Object, obj: bpy.types.Object,
shape_type: int, shape_type: str,
location: Vector, location: Vector,
rotation: Euler, rotation: Euler,
size: Vector, size: Vector,
dynamics_type: int, dynamics_type: str,
collision_group_number: Optional[int] = None, collision_group_number: Optional[int] = None,
collision_group_mask: Optional[List[bool]] = None, collision_group_mask: Optional[List[bool]] = None,
name: Optional[str] = None, name: Optional[str] = None,
@@ -146,8 +131,6 @@ class FnRigidBody:
linear_damping: Optional[float] = None, linear_damping: Optional[float] = None,
bounce: Optional[float] = None, bounce: Optional[float] = None,
) -> bpy.types.Object: ) -> bpy.types.Object:
"""Set up a rigid body object with the specified parameters"""
logger.debug(f"Setting up rigid body object: {obj.name}")
obj.location = location obj.location = location
obj.rotation_euler = rotation obj.rotation_euler = rotation
@@ -189,35 +172,31 @@ class FnRigidBody:
return obj return obj
@staticmethod @staticmethod
def get_rigid_body_size(obj: bpy.types.Object) -> Tuple[float, float, float]: def get_rigid_body_size(obj: bpy.types.Object):
"""Get the size of a rigid body object based on its shape type"""
assert obj.mmd_type == "RIGID_BODY" assert obj.mmd_type == "RIGID_BODY"
x0, y0, z0 = obj.bound_box[0] x0, y0, z0 = obj.bound_box[0]
x1, y1, z1 = obj.bound_box[6] x1, y1, z1 = obj.bound_box[6]
assert x1 >= x0 and y1 >= y0 and z1 >= z0 if not (x1 >= x0 and y1 >= y0 and z1 >= z0):
logger.warning(f"Rigid body '{obj.name}' has invalid bounding box coordinates, using default size")
return (1.0, 1.0, 1.0)
shape = obj.mmd_rigid.shape shape = obj.mmd_rigid.shape
if shape == "SPHERE": if shape == "SPHERE":
radius = (z1 - z0) / 2 radius = (z1 - z0) / 2
return (radius, 0.0, 0.0) return (radius, 0.0, 0.0)
elif shape == "BOX": if shape == "BOX":
x, y, z = (x1 - x0) / 2, (y1 - y0) / 2, (z1 - z0) / 2 x, y, z = (x1 - x0) / 2, (y1 - y0) / 2, (z1 - z0) / 2
return (x, y, z) return (x, y, z)
elif shape == "CAPSULE": if shape == "CAPSULE":
diameter = x1 - x0 diameter = x1 - x0
radius = diameter / 2 radius = diameter / 2
height = abs((z1 - z0) - diameter) height = abs((z1 - z0) - diameter)
return (radius, height, 0.0) return (radius, height, 0.0)
else: raise ValueError(f"Invalid shape type: {shape}")
error_msg = f"Invalid shape type: {shape}"
logger.error(error_msg)
raise ValueError(error_msg)
@staticmethod @staticmethod
def new_joint_object(context: bpy.types.Context, parent_object: bpy.types.Object, empty_display_size: float) -> bpy.types.Object: def new_joint_object(context: bpy.types.Context, parent_object: bpy.types.Object, empty_display_size: float) -> bpy.types.Object:
"""Create a new joint object parented to the specified object"""
logger.debug(f"Creating new joint object parented to {parent_object.name}")
obj = FnContext.new_and_link_object(context, name="Joint", object_data=None) obj = FnContext.new_and_link_object(context, name="Joint", object_data=None)
obj.parent = parent_object obj.parent = parent_object
obj.mmd_type = "JOINT" obj.mmd_type = "JOINT"
@@ -249,11 +228,9 @@ class FnRigidBody:
@staticmethod @staticmethod
def new_joint_objects(context: bpy.types.Context, parent_object: bpy.types.Object, count: int, empty_display_size: float) -> List[bpy.types.Object]: def new_joint_objects(context: bpy.types.Context, parent_object: bpy.types.Object, count: int, empty_display_size: float) -> List[bpy.types.Object]:
"""Create multiple joint objects parented to the specified object"""
if count < 1: if count < 1:
return [] return []
logger.debug(f"Creating {count} joint objects parented to {parent_object.name}")
obj = FnRigidBody.new_joint_object(context, parent_object, empty_display_size) obj = FnRigidBody.new_joint_object(context, parent_object, empty_display_size)
if count == 1: if count == 1:
@@ -277,8 +254,6 @@ class FnRigidBody:
name: str, name: str,
name_e: Optional[str] = None, name_e: Optional[str] = None,
) -> bpy.types.Object: ) -> bpy.types.Object:
"""Set up a joint object with the specified parameters"""
logger.debug(f"Setting up joint object: {obj.name} with name {name}")
obj.name = f"J.{name}" obj.name = f"J.{name}"
obj.location = location obj.location = location
+54 -79
View File
@@ -1,52 +1,42 @@
# -*- coding: utf-8 -*- # Copyright 2018 MMD Tools authors
# Copyright 2014 MMD Tools authors # This file is part of MMD Tools.
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import logging from ....core.logging_setup import logger
import time import time
from typing import Dict, List, Tuple, Set, Optional, Any, Union, cast, TypeVar, Callable
import bpy import bpy
import numpy as np import numpy as np
from mathutils import Matrix, Vector, Quaternion, Euler from mathutils import Matrix, Vector
from bpy.types import Object, PoseBone, Pose, ShapeKey, Modifier, VertexGroup
from ..bpyutils import FnObject from ..bpyutils import FnObject
from ....core.logging_setup import logger
T = TypeVar('T')
def _hash(v: Union[Object, PoseBone, Pose]) -> int: def _hash(v):
if isinstance(v, (bpy.types.Object, bpy.types.PoseBone)): if isinstance(v, (bpy.types.Object, bpy.types.PoseBone)):
return hash(type(v).__name__ + v.name) return hash(type(v).__name__ + v.name)
elif isinstance(v, bpy.types.Pose): if isinstance(v, bpy.types.Pose):
return hash(type(v).__name__ + v.id_data.name) return hash(type(v).__name__ + v.id_data.name)
else:
raise NotImplementedError("hash") raise NotImplementedError("hash")
class FnSDEF: class FnSDEF:
g_verts: Dict[int, Dict[Tuple[int, int], Tuple[PoseBone, PoseBone, List[Tuple[int, float, float, Vector, Vector, Vector]], List[int]]]] = {} # global cache g_verts = {} # global cache
g_shapekey_data: Dict[int, Optional[np.ndarray]] = {} g_shapekey_data = {}
g_bone_check: Dict[int, Dict[Union[Tuple[int, int], str], Union[Tuple[Matrix, Matrix], bool]]] = {} g_bone_check = {}
__g_armature_check: Dict[int, Optional[int]] = {} __g_armature_check = {}
SHAPEKEY_NAME: str = "mmd_sdef_skinning" SHAPEKEY_NAME = "mmd_sdef_skinning"
MASK_NAME: str = "mmd_sdef_mask" MASK_NAME = "mmd_sdef_mask"
def __init__(self) -> None: def __init__(self):
raise NotImplementedError("not allowed") raise NotImplementedError("not allowed")
@classmethod @classmethod
def __init_cache(cls, obj: Object, shapekey: ShapeKey) -> bool: def __init_cache(cls, obj, shapekey):
key = _hash(obj) key = _hash(obj)
obj = getattr(obj, "original", obj) obj = getattr(obj, "original", obj)
mod = obj.modifiers.get("mmd_bone_order_override") mod = obj.modifiers.get("mmd_armature")
key_armature = _hash(mod.object.pose) if mod and mod.type == "ARMATURE" and mod.object else None key_armature = _hash(mod.object.pose) if mod and mod.type == "ARMATURE" and mod.object else None
if key not in cls.g_verts or cls.__g_armature_check.get(key) != key_armature: if key not in cls.g_verts or cls.__g_armature_check.get(key) != key_armature:
logger.debug(f"Initializing SDEF cache for {obj.name}")
cls.g_verts[key] = cls.__find_vertices(obj) cls.g_verts[key] = cls.__find_vertices(obj)
cls.g_bone_check[key] = {} cls.g_bone_check[key] = {}
cls.__g_armature_check[key] = key_armature cls.__g_armature_check[key] = key_armature
@@ -55,7 +45,7 @@ class FnSDEF:
return False return False
@classmethod @classmethod
def __check_bone_update(cls, obj: Object, bone0: PoseBone, bone1: PoseBone) -> bool: def __check_bone_update(cls, obj, bone0, bone1):
check = cls.g_bone_check[_hash(obj)] check = cls.g_bone_check[_hash(obj)]
key = (_hash(bone0), _hash(bone1)) key = (_hash(bone0), _hash(bone1))
if key not in check or (bone0.matrix, bone1.matrix) != check[key]: if key not in check or (bone0.matrix, bone1.matrix) != check[key]:
@@ -64,21 +54,20 @@ class FnSDEF:
return False return False
@classmethod @classmethod
def mute_sdef_set(cls, obj: Object, mute: bool) -> None: def mute_sdef_set(cls, obj, mute):
key_blocks = getattr(obj.data.shape_keys, "key_blocks", ()) key_blocks = getattr(obj.data.shape_keys, "key_blocks", ())
if cls.SHAPEKEY_NAME in key_blocks: if cls.SHAPEKEY_NAME in key_blocks:
shapekey = key_blocks[cls.SHAPEKEY_NAME] shapekey = key_blocks[cls.SHAPEKEY_NAME]
shapekey.mute = mute shapekey.mute = mute
if cls.has_sdef_data(obj): if cls.has_sdef_data(obj):
logger.debug(f"Setting SDEF mute state to {mute} for {obj.name}")
cls.__init_cache(obj, shapekey) cls.__init_cache(obj, shapekey)
cls.__sdef_muted(obj, shapekey) cls.__sdef_muted(obj, shapekey)
@classmethod @classmethod
def __sdef_muted(cls, obj: Object, shapekey: ShapeKey) -> bool: def __sdef_muted(cls, obj, shapekey):
mute = shapekey.mute mute = shapekey.mute
if mute != cls.g_bone_check[_hash(obj)].get("sdef_mute"): if mute != cls.g_bone_check[_hash(obj)].get("sdef_mute"):
mod = obj.modifiers.get("mmd_bone_order_override") mod = obj.modifiers.get("mmd_armature")
if mod and mod.type == "ARMATURE": if mod and mod.type == "ARMATURE":
if not mute and cls.MASK_NAME not in obj.vertex_groups and obj.mode != "EDIT": if not mute and cls.MASK_NAME not in obj.vertex_groups and obj.mode != "EDIT":
mask = tuple(i for v in cls.g_verts[_hash(obj)].values() for i in v[3]) mask = tuple(i for v in cls.g_verts[_hash(obj)].values() for i in v[3])
@@ -87,33 +76,32 @@ class FnSDEF:
mod.invert_vertex_group = True mod.invert_vertex_group = True
shapekey.vertex_group = cls.MASK_NAME shapekey.vertex_group = cls.MASK_NAME
cls.g_bone_check[_hash(obj)]["sdef_mute"] = mute cls.g_bone_check[_hash(obj)]["sdef_mute"] = mute
logger.debug(f"SDEF mute state updated to {mute} for {obj.name}")
return mute return mute
@staticmethod @staticmethod
def has_sdef_data(obj: Object) -> bool: def has_sdef_data(obj):
mod = obj.modifiers.get("mmd_bone_order_override") if obj is None or not hasattr(obj, "modifiers") or not hasattr(obj, "data") or obj.data is None:
return False
mod = obj.modifiers.get("mmd_armature")
if mod and mod.type == "ARMATURE" and mod.object: if mod and mod.type == "ARMATURE" and mod.object:
kb = getattr(obj.data.shape_keys, "key_blocks", None) kb = getattr(obj.data.shape_keys, "key_blocks", None)
return kb and "mmd_sdef_c" in kb and "mmd_sdef_r0" in kb and "mmd_sdef_r1" in kb return kb and "mmd_sdef_c" in kb and "mmd_sdef_r0" in kb and "mmd_sdef_r1" in kb
return False return False
@classmethod @classmethod
def __find_vertices(cls, obj: Object) -> Dict[Tuple[int, int], Tuple[PoseBone, PoseBone, List[Tuple[int, float, float, Vector, Vector, Vector]], List[int]]]: def __find_vertices(cls, obj):
if not cls.has_sdef_data(obj): if not cls.has_sdef_data(obj):
logger.debug(f"SDEF vertex search skipped for '{obj.name}': No SDEF data found")
return {} return {}
vertices: Dict[Tuple[int, int], Tuple[PoseBone, PoseBone, List[Tuple[int, float, float, Vector, Vector, Vector]], List[int]]] = {} vertices = {}
pose_bones = obj.modifiers.get("mmd_bone_order_override").object.pose.bones pose_bones = obj.modifiers.get("mmd_armature").object.pose.bones
bone_map: Dict[int, PoseBone] = {g.index: pose_bones[g.name] for g in obj.vertex_groups if g.name in pose_bones} bone_map = {g.index: pose_bones[g.name] for g in obj.vertex_groups if g.name in pose_bones}
sdef_c = obj.data.shape_keys.key_blocks["mmd_sdef_c"].data sdef_c = obj.data.shape_keys.key_blocks["mmd_sdef_c"].data
sdef_r0 = obj.data.shape_keys.key_blocks["mmd_sdef_r0"].data sdef_r0 = obj.data.shape_keys.key_blocks["mmd_sdef_r0"].data
sdef_r1 = obj.data.shape_keys.key_blocks["mmd_sdef_r1"].data sdef_r1 = obj.data.shape_keys.key_blocks["mmd_sdef_r1"].data
vd = obj.data.vertices vd = obj.data.vertices
logger.debug(f"Finding SDEF vertices for {obj.name}")
vertex_count = 0
for i in range(len(sdef_c)): for i in range(len(sdef_c)):
if vd[i].co != sdef_c[i].co: if vd[i].co != sdef_c[i].co:
bgs = [g for g in vd[i].groups if g.group in bone_map and g.weight] # bone groups bgs = [g for g in vd[i].groups if g.group in bone_map and g.weight] # bone groups
@@ -122,7 +110,7 @@ class FnSDEF:
# preprocessing # preprocessing
w0, w1 = bgs[0].weight, bgs[1].weight w0, w1 = bgs[0].weight, bgs[1].weight
# w0 + w1 == 1 # w0 + w1 == 1
w0 = w0 / (w0 + w1) w0 /= (w0 + w1)
w1 = 1 - w0 w1 = 1 - w0
c, r0, r1 = sdef_c[i].co, sdef_r0[i].co, sdef_r1[i].co c, r0, r1 = sdef_c[i].co, sdef_r0[i].co, sdef_r1[i].co
@@ -136,19 +124,22 @@ class FnSDEF:
vertices[key] = (bone_map[bgs[0].group], bone_map[bgs[1].group], [], []) vertices[key] = (bone_map[bgs[0].group], bone_map[bgs[1].group], [], [])
vertices[key][2].append((i, w0, w1, vd[i].co - c, (c + r0) / 2, (c + r1) / 2)) vertices[key][2].append((i, w0, w1, vd[i].co - c, (c + r0) / 2, (c + r1) / 2))
vertices[key][3].append(i) vertices[key][3].append(i)
vertex_count += 1
logger.debug(f"Found {vertex_count} SDEF vertices in {obj.name}")
return vertices return vertices
@classmethod @classmethod
def driver_function_wrap(cls, obj_name: str, bulk_update: bool, use_skip: bool, use_scale: bool) -> float: def driver_function_wrap(cls, obj_name, bulk_update, use_skip, use_scale):
if obj_name not in bpy.data.objects:
logger.warning(f"SDEF driver wrap: Object '{obj_name}' not found")
return 0.0
obj = bpy.data.objects[obj_name] obj = bpy.data.objects[obj_name]
shapekey = obj.data.shape_keys.key_blocks[cls.SHAPEKEY_NAME] shapekey = obj.data.shape_keys.key_blocks[cls.SHAPEKEY_NAME]
return cls.driver_function(shapekey, obj_name, bulk_update, use_skip, use_scale) return cls.driver_function(shapekey, obj_name, bulk_update, use_skip, use_scale)
@classmethod @classmethod
def driver_function(cls, shapekey: ShapeKey, obj_name: str, bulk_update: bool, use_skip: bool, use_scale: bool) -> float: def driver_function(cls, shapekey, obj_name, bulk_update, use_skip, use_scale):
if obj_name not in bpy.data.objects:
logger.warning(f"SDEF driver: Object '{obj_name}' not found, driver will be inactive")
return 0.0
obj = bpy.data.objects[obj_name] obj = bpy.data.objects[obj_name]
if getattr(shapekey.id_data, "is_evaluated", False): if getattr(shapekey.id_data, "is_evaluated", False):
# For Blender 2.8x, we should use evaluated object, and the only reference is the "obj" variable of SDEF driver # For Blender 2.8x, we should use evaluated object, and the only reference is the "obj" variable of SDEF driver
@@ -159,7 +150,7 @@ class FnSDEF:
if cls.__sdef_muted(obj, shapekey): if cls.__sdef_muted(obj, shapekey):
return 0.0 return 0.0
pose_bones = obj.modifiers.get("mmd_bone_order_override").object.pose.bones pose_bones = obj.modifiers.get("mmd_armature").object.pose.bones
if not bulk_update: if not bulk_update:
shapekey_data = shapekey.data shapekey_data = shapekey.data
if use_scale: if use_scale:
@@ -200,8 +191,6 @@ class FnSDEF:
else: # bulk update else: # bulk update
shapekey_data = cls.g_shapekey_data[_hash(obj)] shapekey_data = cls.g_shapekey_data[_hash(obj)]
if shapekey_data is None: if shapekey_data is None:
import numpy as np
shapekey_data = np.zeros(len(shapekey.data) * 3, dtype=np.float32) shapekey_data = np.zeros(len(shapekey.data) * 3, dtype=np.float32)
shapekey.data.foreach_get("co", shapekey_data) shapekey.data.foreach_get("co", shapekey_data)
shapekey_data = cls.g_shapekey_data[_hash(obj)] = shapekey_data.reshape(len(shapekey.data), 3) shapekey_data = cls.g_shapekey_data[_hash(obj)] = shapekey_data.reshape(len(shapekey.data), 3)
@@ -220,15 +209,15 @@ class FnSDEF:
rot1 = -rot1 rot1 = -rot1
s0, s1 = mat0.to_scale(), mat1.to_scale() s0, s1 = mat0.to_scale(), mat1.to_scale()
def scale(mat_rot: Matrix, w0: float, w1: float) -> Matrix: def scale(mat_rot, w0, w1, s0, s1):
s = s0 * w0 + s1 * w1 s = s0 * w0 + s1 * w1
return mat_rot @ Matrix([(s[0], 0, 0), (0, s[1], 0), (0, 0, s[2])]) return mat_rot @ Matrix([(s[0], 0, 0), (0, s[1], 0), (0, 0, s[2])])
def offset(mat_rot: Matrix, pos_c: Vector, vid: int) -> Vector: def offset(mat_rot, pos_c, vid):
delta = sum(((key.data[vid].co - key.relative_key.data[vid].co) * key.value for key in key_blocks), Vector()) # assuming key.vertex_group = '' delta = sum(((key.data[vid].co - key.relative_key.data[vid].co) * key.value for key in key_blocks), Vector()) # assuming key.vertex_group = ''
return (mat_rot @ (pos_c + delta)) - delta return (mat_rot @ (pos_c + delta)) - delta
shapekey_data[vids] = [offset(scale((rot0 * w0 + rot1 * w1).normalized().to_matrix(), w0, w1), pos_c, vid) + (mat0 @ cr0) * w0 + (mat1 @ cr1) * w1 for vid, w0, w1, pos_c, cr0, cr1 in sdef_data] shapekey_data[vids] = [offset(scale((rot0 * w0 + rot1 * w1).normalized().to_matrix(), w0, w1, s0, s1), pos_c, vid) + (mat0 @ cr0) * w0 + (mat1 @ cr1) * w1 for vid, w0, w1, pos_c, cr0, cr1 in sdef_data]
else: else:
# bulk update # bulk update
for bone0, bone1, sdef_data, vids in cls.g_verts[_hash(obj)].values(): for bone0, bone1, sdef_data, vids in cls.g_verts[_hash(obj)].values():
@@ -247,19 +236,16 @@ class FnSDEF:
return 1.0 # shapkey value return 1.0 # shapkey value
@classmethod @classmethod
def register_driver_function(cls) -> None: def register_driver_function(cls):
"""Register driver functions in Blender's driver namespace."""
if "mmd_sdef_driver" not in bpy.app.driver_namespace: if "mmd_sdef_driver" not in bpy.app.driver_namespace:
logger.debug("Registering SDEF driver function")
bpy.app.driver_namespace["mmd_sdef_driver"] = cls.driver_function bpy.app.driver_namespace["mmd_sdef_driver"] = cls.driver_function
if "mmd_sdef_driver_wrap" not in bpy.app.driver_namespace: if "mmd_sdef_driver_wrap" not in bpy.app.driver_namespace:
logger.debug("Registering SDEF driver wrapper function")
bpy.app.driver_namespace["mmd_sdef_driver_wrap"] = cls.driver_function_wrap bpy.app.driver_namespace["mmd_sdef_driver_wrap"] = cls.driver_function_wrap
BENCH_LOOP: int = 10 BENCH_LOOP = 10
@classmethod @classmethod
def __get_benchmark_result(cls, obj: Object, shapkey: ShapeKey, use_scale: bool, use_skip: bool) -> bool: def __get_benchmark_result(cls, obj, shapkey, use_scale, use_skip):
# warmed up # warmed up
cls.driver_function(shapkey, obj.name, bulk_update=True, use_skip=False, use_scale=use_scale) cls.driver_function(shapkey, obj.name, bulk_update=True, use_skip=False, use_scale=use_scale)
cls.driver_function(shapkey, obj.name, bulk_update=False, use_skip=False, use_scale=use_scale) cls.driver_function(shapkey, obj.name, bulk_update=False, use_skip=False, use_scale=use_scale)
@@ -273,15 +259,15 @@ class FnSDEF:
cls.driver_function(shapkey, obj.name, bulk_update=True, use_skip=False, use_scale=use_scale) cls.driver_function(shapkey, obj.name, bulk_update=True, use_skip=False, use_scale=use_scale)
bulk_time = time.time() - t bulk_time = time.time() - t
result = default_time > bulk_time result = default_time > bulk_time
logger.info(f"SDEF benchmark for {obj.name}: default {default_time:.4f}s vs bulk_update {bulk_time:.4f}s => bulk_update={result}") logger.info("FnSDEF:benchmark: default %.4f vs bulk_update %.4f => bulk_update=%s", default_time, bulk_time, result)
return result return result
@classmethod @classmethod
def bind(cls, obj: Object, bulk_update: Optional[bool] = None, use_skip: bool = True, use_scale: bool = False) -> bool: def bind(cls, obj, bulk_update=None, use_skip=True, use_scale=False):
# Unbind first # Unbind first
cls.unbind(obj) cls.unbind(obj)
if not cls.has_sdef_data(obj): if not cls.has_sdef_data(obj):
logger.debug(f"Object {obj.name} does not have SDEF data") logger.debug(f"SDEF bind skipped for '{obj.name}': No SDEF data found")
return False return False
# Create the shapekey for the driver # Create the shapekey for the driver
shapekey = obj.shape_key_add(name=cls.SHAPEKEY_NAME, from_mix=False) shapekey = obj.shape_key_add(name=cls.SHAPEKEY_NAME, from_mix=False)
@@ -300,50 +286,41 @@ class FnSDEF:
ov.type = "SINGLE_PROP" ov.type = "SINGLE_PROP"
ov.targets[0].id = obj ov.targets[0].id = obj
ov.targets[0].data_path = "name" ov.targets[0].data_path = "name"
if not bulk_update and use_skip: # FIXME: force disable use_skip=True for bulk_update=False on 2.8 mod = obj.modifiers.get("mmd_armature")
use_skip = False
mod = obj.modifiers.get("mmd_bone_order_override")
variables = f.driver.variables variables = f.driver.variables
for name in set(data[i].name for data in cls.g_verts[_hash(obj)].values() for i in range(2)): # add required bones for dependency graph for name in {data[i].name for data in cls.g_verts[_hash(obj)].values() for i in range(2)}: # add required bones for dependency graph
var = variables.new() var = variables.new()
var.type = "TRANSFORMS" var.type = "TRANSFORMS"
var.targets[0].id = mod.object var.targets[0].id = mod.object
var.targets[0].bone_target = name var.targets[0].bone_target = name
f.driver.use_self = True f.driver.use_self = True
param = (bulk_update, use_skip, use_scale) f.driver.expression = f"mmd_sdef_driver(self, obj, bulk_update={bulk_update}, use_skip={use_skip}, use_scale={use_scale})"
f.driver.expression = "mmd_sdef_driver(self, obj, bulk_update={}, use_skip={}, use_scale={})".format(*param)
logger.info(f"Successfully bound SDEF to {obj.name} with bulk_update={bulk_update}, use_skip={use_skip}, use_scale={use_scale}")
return True return True
@classmethod @classmethod
def unbind(cls, obj: Object) -> None: def unbind(cls, obj):
if obj.data.shape_keys: if obj.data.shape_keys:
if cls.SHAPEKEY_NAME in obj.data.shape_keys.key_blocks: if cls.SHAPEKEY_NAME in obj.data.shape_keys.key_blocks:
logger.debug(f"Removing SDEF shape key from {obj.name}")
FnObject.mesh_remove_shape_key(obj, obj.data.shape_keys.key_blocks[cls.SHAPEKEY_NAME]) FnObject.mesh_remove_shape_key(obj, obj.data.shape_keys.key_blocks[cls.SHAPEKEY_NAME])
for mod in obj.modifiers: for mod in obj.modifiers:
if mod.type == "ARMATURE" and mod.vertex_group == cls.MASK_NAME: if mod.type == "ARMATURE" and mod.vertex_group == cls.MASK_NAME:
logger.debug(f"Clearing SDEF vertex group from modifier in {obj.name}")
mod.vertex_group = "" mod.vertex_group = ""
mod.invert_vertex_group = False mod.invert_vertex_group = False
break break
if cls.MASK_NAME in obj.vertex_groups: if cls.MASK_NAME in obj.vertex_groups:
logger.debug(f"Removing SDEF vertex group from {obj.name}")
obj.vertex_groups.remove(obj.vertex_groups[cls.MASK_NAME]) obj.vertex_groups.remove(obj.vertex_groups[cls.MASK_NAME])
cls.clear_cache(obj) cls.clear_cache(obj)
@classmethod @classmethod
def clear_cache(cls, obj: Optional[Object] = None, unused_only: bool = False) -> None: def clear_cache(cls, obj=None, unused_only=False):
if unused_only: if unused_only:
valid_keys = set(_hash(i) for i in bpy.data.objects if i.type == "MESH" and i != obj) valid_keys = {_hash(i) for i in bpy.data.objects if i.type == "MESH" and i != obj}
removed_keys = cls.g_verts.keys() - valid_keys for key in cls.g_verts.keys() - valid_keys:
for key in removed_keys:
del cls.g_verts[key] del cls.g_verts[key]
for key in cls.g_shapekey_data.keys() - cls.g_verts.keys(): for key in cls.g_shapekey_data.keys() - cls.g_verts.keys():
del cls.g_shapekey_data[key] del cls.g_shapekey_data[key]
for key in cls.g_bone_check.keys() - cls.g_verts.keys(): for key in cls.g_bone_check.keys() - cls.g_verts.keys():
del cls.g_bone_check[key] del cls.g_bone_check[key]
logger.debug(f"Cleared {len(removed_keys)} unused SDEF cache entries")
elif obj: elif obj:
key = _hash(obj) key = _hash(obj)
if key in cls.g_verts: if key in cls.g_verts:
@@ -352,9 +329,7 @@ class FnSDEF:
del cls.g_shapekey_data[key] del cls.g_shapekey_data[key]
if key in cls.g_bone_check: if key in cls.g_bone_check:
del cls.g_bone_check[key] del cls.g_bone_check[key]
logger.debug(f"Cleared SDEF cache for {obj.name}")
else: else:
logger.debug("Cleared all SDEF cache")
cls.g_verts = {} cls.g_verts = {}
cls.g_bone_check = {} cls.g_bone_check = {}
cls.g_shapekey_data = {} cls.g_shapekey_data = {}
+43 -66
View File
@@ -1,37 +1,26 @@
# -*- coding: utf-8 -*- # Copyright 2019 MMD Tools authors
# Copyright 2014 MMD Tools authors # This file is part of MMD Tools.
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools from typing import Optional, Tuple, cast
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import Optional, Tuple, cast, List, Dict, Any, Union
import bpy import bpy
from bpy.types import (
ShaderNodeTree,
ShaderNode,
NodeGroupInput,
NodeGroupOutput,
Material
)
from ....core.logging_setup import logger
class _NodeTreeUtils: class _NodeTreeUtils:
def __init__(self, shader: ShaderNodeTree): def __init__(self, shader: bpy.types.ShaderNodeTree):
self.shader = shader self.shader = shader
self.nodes: bpy.types.bpy_prop_collection[ShaderNode] = shader.nodes # type: ignore self.nodes: bpy.types.bpy_prop_collection[bpy.types.ShaderNode] = shader.nodes # type: ignore[assignment]
self.links = shader.links self.links = shader.links
def _find_node(self, node_type: str) -> Optional[ShaderNode]: def _find_node(self, node_type: str) -> Optional[bpy.types.ShaderNode]:
return next((n for n in self.nodes if n.bl_idname == node_type), None) return next((n for n in self.nodes if n.bl_idname == node_type), None)
def new_node(self, idname: str, pos: Tuple[int, int]) -> ShaderNode: def new_node(self, idname: str, pos: Tuple[int, int]) -> bpy.types.ShaderNode:
node: ShaderNode = self.nodes.new(idname) node: bpy.types.ShaderNode = self.nodes.new(idname)
node.location = (pos[0] * 210, pos[1] * 220) node.location = (pos[0] * 210, pos[1] * 220)
return node return node
def new_math_node(self, operation: str, pos: Tuple[int, int], value1: Optional[float] = None, value2: Optional[float] = None) -> ShaderNode: def new_math_node(self, operation, pos, value1=None, value2=None):
node = self.new_node("ShaderNodeMath", pos) node = self.new_node("ShaderNodeMath", pos)
node.operation = operation node.operation = operation
if value1 is not None: if value1 is not None:
@@ -40,7 +29,7 @@ class _NodeTreeUtils:
node.inputs[1].default_value = value2 node.inputs[1].default_value = value2
return node return node
def new_vector_math_node(self, operation: str, pos: Tuple[int, int], vector1: Optional[Tuple[float, float, float, float]] = None, vector2: Optional[Tuple[float, float, float, float]] = None) -> ShaderNode: def new_vector_math_node(self, operation, pos, vector1=None, vector2=None):
node = self.new_node("ShaderNodeVectorMath", pos) node = self.new_node("ShaderNodeVectorMath", pos)
node.operation = operation node.operation = operation
if vector1 is not None: if vector1 is not None:
@@ -49,7 +38,7 @@ class _NodeTreeUtils:
node.inputs[1].default_value = vector2 node.inputs[1].default_value = vector2
return node return node
def new_mix_node(self, blend_type: str, pos: Tuple[int, int], fac: Optional[float] = None, color1: Optional[Tuple[float, float, float, float]] = None, color2: Optional[Tuple[float, float, float, float]] = None) -> ShaderNode: def new_mix_node(self, blend_type, pos, fac=None, color1=None, color2=None):
node = self.new_node("ShaderNodeMixRGB", pos) node = self.new_node("ShaderNodeMixRGB", pos)
node.blend_type = blend_type node.blend_type = blend_type
if fac is not None: if fac is not None:
@@ -61,30 +50,30 @@ class _NodeTreeUtils:
return node return node
SOCKET_TYPE_MAPPING: Dict[str, str] = {"NodeSocketFloatFactor": "NodeSocketFloat"} SOCKET_TYPE_MAPPING = {"NodeSocketFloatFactor": "NodeSocketFloat"}
SOCKET_SUBTYPE_MAPPING: Dict[str, str] = {"NodeSocketFloatFactor": "FACTOR"} SOCKET_SUBTYPE_MAPPING = {"NodeSocketFloatFactor": "FACTOR"}
class _NodeGroupUtils(_NodeTreeUtils): class _NodeGroupUtils(_NodeTreeUtils):
def __init__(self, shader: ShaderNodeTree): def __init__(self, shader: bpy.types.ShaderNodeTree):
super().__init__(shader) super().__init__(shader)
self.__node_input: Optional[NodeGroupInput] = None self.__node_input: Optional[bpy.types.NodeGroupInput] = None
self.__node_output: Optional[NodeGroupOutput] = None self.__node_output: Optional[bpy.types.NodeGroupOutput] = None
@property @property
def node_input(self) -> NodeGroupInput: def node_input(self) -> bpy.types.NodeGroupInput:
if not self.__node_input: if not self.__node_input:
self.__node_input = cast(NodeGroupInput, self._find_node("NodeGroupInput") or self.new_node("NodeGroupInput", (-2, 0))) self.__node_input = cast("bpy.types.NodeGroupInput", self._find_node("NodeGroupInput") or self.new_node("NodeGroupInput", (-2, 0)))
return self.__node_input return self.__node_input
@property @property
def node_output(self) -> NodeGroupOutput: def node_output(self) -> bpy.types.NodeGroupOutput:
if not self.__node_output: if not self.__node_output:
self.__node_output = cast(NodeGroupOutput, self._find_node("NodeGroupOutput") or self.new_node("NodeGroupOutput", (2, 0))) self.__node_output = cast("bpy.types.NodeGroupOutput", self._find_node("NodeGroupOutput") or self.new_node("NodeGroupOutput", (2, 0)))
return self.__node_output return self.__node_output
def hide_nodes(self, hide_sockets: bool = True) -> None: def hide_nodes(self, hide_sockets=True):
skip_nodes = {self.__node_input, self.__node_output} skip_nodes = {self.__node_input, self.__node_output}
for n in (x for x in self.nodes if x not in skip_nodes): for n in (x for x in self.nodes if x not in skip_nodes):
n.hide = True n.hide = True
@@ -95,22 +84,22 @@ class _NodeGroupUtils(_NodeTreeUtils):
for s in n.outputs: for s in n.outputs:
s.hide = not s.is_linked s.hide = not s.is_linked
def new_input_socket(self, io_name: str, socket: Optional[bpy.types.NodeSocket], default_val: Optional[Union[float, Tuple[float, float, float, float]]] = None, min_max: Optional[Tuple[float, float]] = None, socket_type: Optional[str] = None) -> None: def new_input_socket(self, io_name, socket, default_val=None, min_max=None, socket_type=None):
self.__new_io("INPUT", self.node_input.outputs, io_name, socket, default_val, min_max, socket_type) self.__new_io("INPUT", self.node_input.outputs, io_name, socket, default_val, min_max, socket_type)
def new_output_socket(self, io_name: str, socket: Optional[bpy.types.NodeSocket], default_val: Optional[Union[float, Tuple[float, float, float, float]]] = None, min_max: Optional[Tuple[float, float]] = None, socket_type: Optional[str] = None) -> None: def new_output_socket(self, io_name, socket, default_val=None, min_max=None, socket_type=None):
self.__new_io("OUTPUT", self.node_output.inputs, io_name, socket, default_val, min_max, socket_type) self.__new_io("OUTPUT", self.node_output.inputs, io_name, socket, default_val, min_max, socket_type)
def __new_io(self, in_out: str, io_sockets: bpy.types.bpy_prop_collection, io_name: str, socket: Optional[bpy.types.NodeSocket], default_val: Optional[Union[float, Tuple[float, float, float, float]]] = None, min_max: Optional[Tuple[float, float]] = None, socket_type: Optional[str] = None) -> None: def __new_io(self, in_out, io_sockets, io_name, socket, default_val=None, min_max=None, socket_type=None):
if io_name not in io_sockets: if io_name not in io_sockets:
idname = socket_type or (socket.bl_idname if socket else "NodeSocketFloat") idname = socket_type or socket.bl_idname
interface_socket = self.shader.interface.new_socket(name=io_name, in_out=in_out, socket_type=SOCKET_TYPE_MAPPING.get(idname, idname)) interface_socket = self.shader.interface.new_socket(name=io_name, in_out=in_out, socket_type=SOCKET_TYPE_MAPPING.get(idname, idname))
if idname in SOCKET_SUBTYPE_MAPPING: if idname in SOCKET_SUBTYPE_MAPPING:
interface_socket.subtype = SOCKET_SUBTYPE_MAPPING.get(idname, "") interface_socket.subtype = SOCKET_SUBTYPE_MAPPING.get(idname, "")
if not min_max: if not min_max:
if idname.endswith("Factor") or io_name.endswith("Alpha"): if idname.endswith("Factor") or io_name.endswith("Alpha"):
interface_socket.min_value, interface_socket.max_value = 0, 1 interface_socket.min_value, interface_socket.max_value = 0, 1
elif idname.endswith("Float") or idname.endswith("Vector"): elif idname.endswith(("Float", "Vector")):
interface_socket.min_value, interface_socket.max_value = -10, 10 interface_socket.min_value, interface_socket.max_value = -10, 10
if socket is not None: if socket is not None:
self.links.new(io_sockets[io_name], socket) self.links.new(io_sockets[io_name], socket)
@@ -122,18 +111,14 @@ class _NodeGroupUtils(_NodeTreeUtils):
class _MaterialMorph: class _MaterialMorph:
@classmethod @classmethod
def update_morph_inputs(cls, material: Optional[Material], morph: Any) -> None: def update_morph_inputs(cls, material, morph):
"""Update material morph inputs based on morph data"""
if material and material.node_tree and morph.name in material.node_tree.nodes: if material and material.node_tree and morph.name in material.node_tree.nodes:
logger.debug(f"Updating morph inputs for {morph.name} in {material.name}")
cls.__update_node_inputs(material.node_tree.nodes[morph.name], morph) cls.__update_node_inputs(material.node_tree.nodes[morph.name], morph)
cls.update_morph_inputs(bpy.data.materials.get("mmd_edge." + material.name, None), morph) cls.update_morph_inputs(bpy.data.materials.get("mmd_edge." + material.name, None), morph)
@classmethod @classmethod
def setup_morph_nodes(cls, material: Material, morphs: List[Any]) -> List[ShaderNode]: def setup_morph_nodes(cls, material, morphs):
"""Set up morph nodes for a material"""
node, nodes = None, [] node, nodes = None, []
logger.debug(f"Setting up {len(morphs)} morph nodes for {material.name}")
for m in morphs: for m in morphs:
node = cls.__morph_node_add(material, m, node) node = cls.__morph_node_add(material, m, node)
nodes.append(node) nodes.append(node)
@@ -149,25 +134,23 @@ class _MaterialMorph:
return nodes return nodes
@classmethod @classmethod
def reset_morph_links(cls, node: ShaderNode) -> None: def reset_morph_links(cls, node):
"""Reset morph links for a node"""
logger.debug(f"Resetting morph links for {node.name}")
cls.__update_morph_links(node, reset=True) cls.__update_morph_links(node, reset=True)
@classmethod @classmethod
def __update_morph_links(cls, node: ShaderNode, reset: bool = False) -> None: def __update_morph_links(cls, node, reset=False):
nodes, links = node.id_data.nodes, node.id_data.links nodes, links = node.id_data.nodes, node.id_data.links
if reset: if reset:
if any(l.from_node.name.startswith("mmd_bind") for i in node.inputs for l in i.links): if any(link.from_node.name.startswith("mmd_bind") for i in node.inputs for link in i.links):
return return
def __init_link(socket_morph: bpy.types.NodeSocket, socket_shader: Optional[bpy.types.NodeSocket]) -> None: def __init_link(socket_morph, socket_shader):
if socket_shader and socket_morph.is_linked: if socket_shader and socket_morph.is_linked:
links.new(socket_morph.links[0].from_socket, socket_shader) links.new(socket_morph.links[0].from_socket, socket_shader)
else: else:
def __init_link(socket_morph: bpy.types.NodeSocket, socket_shader: Optional[bpy.types.NodeSocket]) -> None: def __init_link(socket_morph, socket_shader):
if socket_shader: if socket_shader:
if socket_shader.is_linked: if socket_shader.is_linked:
links.new(socket_shader.links[0].from_socket, socket_morph) links.new(socket_shader.links[0].from_socket, socket_morph)
@@ -192,8 +175,7 @@ class _MaterialMorph:
__init_link(node.inputs["Edge1 A"], shader.inputs["Alpha"]) __init_link(node.inputs["Edge1 A"], shader.inputs["Alpha"])
@classmethod @classmethod
def __update_node_inputs(cls, node: ShaderNode, morph: Any) -> None: def __update_node_inputs(cls, node, morph):
"""Update node inputs based on morph data"""
node.inputs["Ambient2"].default_value[:3] = morph.ambient_color[:3] node.inputs["Ambient2"].default_value[:3] = morph.ambient_color[:3]
node.inputs["Diffuse2"].default_value[:3] = morph.diffuse_color[:3] node.inputs["Diffuse2"].default_value[:3] = morph.diffuse_color[:3]
node.inputs["Specular2"].default_value[:3] = morph.specular_color[:3] node.inputs["Specular2"].default_value[:3] = morph.specular_color[:3]
@@ -211,8 +193,7 @@ class _MaterialMorph:
node.inputs["Sphere2 A"].default_value = morph.sphere_texture_factor[3] node.inputs["Sphere2 A"].default_value = morph.sphere_texture_factor[3]
@classmethod @classmethod
def __morph_node_add(cls, material: Material, morph: Optional[Any], prev_node: Optional[ShaderNode]) -> Optional[ShaderNode]: def __morph_node_add(cls, material, morph, prev_node):
"""Add a morph node to a material"""
nodes, links = material.node_tree.nodes, material.node_tree.links nodes, links = material.node_tree.nodes, material.node_tree.links
shader = nodes.get("mmd_shader", None) shader = nodes.get("mmd_shader", None)
@@ -237,9 +218,8 @@ class _MaterialMorph:
return node return node
# connect last node to shader # connect last node to shader
if shader: if shader:
logger.debug(f"Connecting last node to shader for {material.name}")
def __soft_link(socket_out: Optional[bpy.types.NodeSocket], socket_in: Optional[bpy.types.NodeSocket]) -> None: def __soft_link(socket_out, socket_in):
if socket_out and socket_in: if socket_out and socket_in:
links.new(socket_out, socket_in) links.new(socket_out, socket_in)
@@ -261,14 +241,12 @@ class _MaterialMorph:
return shader return shader
@classmethod @classmethod
def __get_shader(cls, morph_type: str) -> ShaderNodeTree: def __get_shader(cls, morph_type):
"""Get or create a shader node group for the specified morph type"""
group_name = "MMDMorph" + morph_type group_name = "MMDMorph" + morph_type
shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree") shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
if len(shader.nodes): if len(shader.nodes):
return shader return shader
logger.info(f"Creating new shader node group: {group_name}")
ng = _NodeGroupUtils(shader) ng = _NodeGroupUtils(shader)
links = ng.links links = ng.links
@@ -279,18 +257,18 @@ class _MaterialMorph:
ng.new_input_socket("Fac", None, 0, socket_type="NodeSocketFloat") ng.new_input_socket("Fac", None, 0, socket_type="NodeSocketFloat")
ng.new_node("NodeGroupOutput", (3, 0)) ng.new_node("NodeGroupOutput", (3, 0))
def __blend_color_add(id_name: str, pos: Tuple[int, int], tag: str = "") -> ShaderNode: def __blend_color_add(id_name, pos, tag=""):
# MA_RAMP_MULT: ColorMul = Color1 * (Fac * Color2 + (1 - Fac)) # MA_RAMP_MULT: ColorMul = Color1 * (Fac * Color2 + (1 - Fac))
# MA_RAMP_ADD: ColorAdd = Color1 + Fac * Color2 # MA_RAMP_ADD: ColorAdd = Color1 + Fac * Color2
# https://github.com/blender/blender/blob/594f47ecd2d5367ca936cf6fc6ec8168c2b360d0/source/blender/blenkernel/intern/material.c#L1400 # https://github.com/blender/blender/blob/594f47ecd2d5367ca936cf6fc6ec8168c2b360d0/source/blender/blenkernel/intern/material.c#L1400
node_mix = ng.new_mix_node("MULTIPLY" if use_mul else "ADD", (pos[0] + 1, pos[1])) node_mix = ng.new_mix_node("MULTIPLY" if use_mul else "ADD", (pos[0] + 1, pos[1]))
links.new(node_input.outputs["Fac"], node_mix.inputs["Fac"]) links.new(node_input.outputs["Fac"], node_mix.inputs["Fac"])
ng.new_input_socket("%s1" % id_name + tag, node_mix.inputs["Color1"]) ng.new_input_socket(f"{id_name}1" + tag, node_mix.inputs["Color1"])
ng.new_input_socket("%s2" % id_name + tag, node_mix.inputs["Color2"], socket_type="NodeSocketVector") ng.new_input_socket(f"{id_name}2" + tag, node_mix.inputs["Color2"], socket_type="NodeSocketVector")
ng.new_output_socket(id_name + tag, node_mix.outputs["Color"]) ng.new_output_socket(id_name + tag, node_mix.outputs["Color"])
return node_mix return node_mix
def __blend_tex_color(id_name: str, pos: Tuple[int, int], node_tex_rgb: ShaderNode, node_tex_a_output: bpy.types.NodeSocket) -> None: def __blend_tex_color(id_name, pos, node_tex_rgb, node_tex_a_output):
# Tex Color = tex_rgb * tex_a + (1 - tex_a) # Tex Color = tex_rgb * tex_a + (1 - tex_a)
# : tex_rgb = TexRGB * ColorMul + ColorAdd # : tex_rgb = TexRGB * ColorMul + ColorAdd
# : tex_a = TexA * ValueMul + ValueAdd # : tex_a = TexA * ValueMul + ValueAdd
@@ -313,7 +291,7 @@ class _MaterialMorph:
ng.new_output_socket(id_name + " Tex", node_add.outputs[0], socket_type="NodeSocketColor") ng.new_output_socket(id_name + " Tex", node_add.outputs[0], socket_type="NodeSocketColor")
ng.new_output_socket(id_name + " Tex Add", node_scale.outputs[0], socket_type="NodeSocketColor") ng.new_output_socket(id_name + " Tex Add", node_scale.outputs[0], socket_type="NodeSocketColor")
def __add_sockets(id_name: str, input1: bpy.types.NodeSocket, input2: bpy.types.NodeSocket, output: bpy.types.NodeSocket, tag: str = "") -> None: def __add_sockets(id_name, input1, input2, output, tag=""):
ng.new_input_socket(f"{id_name}1{tag}", input1, use_mul) ng.new_input_socket(f"{id_name}1{tag}", input1, use_mul)
ng.new_input_socket(f"{id_name}2{tag}", input2, use_mul) ng.new_input_socket(f"{id_name}2{tag}", input2, use_mul)
ng.new_output_socket(f"{id_name}{tag}", output) ng.new_output_socket(f"{id_name}{tag}", output)
@@ -362,5 +340,4 @@ class _MaterialMorph:
__blend_tex_color("Sphere", (pos_x + 3, -3.5), sphere_rgb, separate_basea_toona_spherea.outputs[2]) __blend_tex_color("Sphere", (pos_x + 3, -3.5), sphere_rgb, separate_basea_toona_spherea.outputs[2])
ng.hide_nodes() ng.hide_nodes()
logger.debug(f"Shader node group {group_name} created successfully")
return ng.shader return ng.shader
+56 -81
View File
@@ -1,9 +1,5 @@
# -*- coding: utf-8 -*- # Copyright 2021 MMD Tools authors
# Copyright 2014 MMD Tools authors # This file is part of MMD Tools.
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import itertools import itertools
import re import re
@@ -33,11 +29,7 @@ class MMDTranslationElementType(Enum):
class MMDDataHandlerABC(ABC): class MMDDataHandlerABC(ABC):
@classmethod type_name: str
@property
@abstractmethod
def type_name(cls) -> str:
pass
@classmethod @classmethod
@abstractmethod @abstractmethod
@@ -67,7 +59,8 @@ class MMDDataHandlerABC(ABC):
@classmethod @classmethod
@abstractmethod @abstractmethod
def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]: def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]:
"""Returns (name, name_j, name_e)""" """Return (name, name_j, name_e)"""
pass
@classmethod @classmethod
def is_restorable(cls, mmd_translation_element: "MMDTranslationElement") -> bool: def is_restorable(cls, mmd_translation_element: "MMDTranslationElement") -> bool:
@@ -75,7 +68,7 @@ class MMDDataHandlerABC(ABC):
@classmethod @classmethod
def check_data_visible(cls, filter_selected: bool, filter_visible: bool, select: bool, hide: bool) -> bool: def check_data_visible(cls, filter_selected: bool, filter_visible: bool, select: bool, hide: bool) -> bool:
return filter_selected and not select or filter_visible and hide return (filter_selected and not select) or (filter_visible and hide)
@classmethod @classmethod
def prop_restorable(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", prop_name: str, original_value: str, index: int): def prop_restorable(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", prop_name: str, original_value: str, index: int):
@@ -86,7 +79,7 @@ class MMDDataHandlerABC(ABC):
row.label(text="", icon="BLANK1") row.label(text="", icon="BLANK1")
return return
op = row.operator("mmd_tools.restore_mmd_translation_element_name", text="", icon="FILE_REFRESH") op = row.operator("mmd_tools_local.restore_mmd_translation_element_name", text="", icon="FILE_REFRESH")
op.index = index op.index = index
op.prop_name = prop_name op.prop_name = prop_name
op.restore_value = original_value op.restore_value = original_value
@@ -100,10 +93,7 @@ class MMDDataHandlerABC(ABC):
class MMDBoneHandler(MMDDataHandlerABC): class MMDBoneHandler(MMDDataHandlerABC):
@classmethod type_name = MMDTranslationElementType.BONE.name
@property
def type_name(cls) -> str:
return MMDTranslationElementType.BONE.name
@classmethod @classmethod
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int): def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
@@ -114,18 +104,18 @@ class MMDBoneHandler(MMDDataHandlerABC):
cls.prop_restorable(prop_row, mmd_translation_element, "name", pose_bone.name, index) cls.prop_restorable(prop_row, mmd_translation_element, "name", pose_bone.name, index)
cls.prop_restorable(prop_row, mmd_translation_element, "name_j", pose_bone.mmd_bone.name_j, index) cls.prop_restorable(prop_row, mmd_translation_element, "name_j", pose_bone.mmd_bone.name_j, index)
cls.prop_restorable(prop_row, mmd_translation_element, "name_e", pose_bone.mmd_bone.name_e, index) cls.prop_restorable(prop_row, mmd_translation_element, "name_e", pose_bone.mmd_bone.name_e, index)
row.prop(pose_bone.bone, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if pose_bone.bone.select else "RESTRICT_SELECT_ON") row.prop(pose_bone.bone, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if pose_bone.select else "RESTRICT_SELECT_ON")
row.prop(pose_bone.bone, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if pose_bone.bone.hide else "HIDE_OFF") row.prop(pose_bone.bone, "hide", text="", emboss=False, icon_only=True)
@classmethod @classmethod
def collect_data(cls, mmd_translation: "MMDTranslation"): def collect_data(cls, mmd_translation: "MMDTranslation"):
armature_object: bpy.types.Object = FnModel.find_armature_object(mmd_translation.id_data) armature_object: bpy.types.Object = FnModel.find_armature_object(mmd_translation.id_data)
pose_bone: bpy.types.PoseBone pose_bone: bpy.types.PoseBone
for index, pose_bone in enumerate(armature_object.pose.bones): for index, pose_bone in enumerate(armature_object.pose.bones):
if not any(c.is_visible for c in pose_bone.bone.collections): if pose_bone.bone.hide or (pose_bone.bone.collections and not any(c.is_visible for c in pose_bone.bone.collections)):
continue continue
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add() mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements.add()
mmd_translation_element.type = MMDTranslationElementType.BONE.name mmd_translation_element.type = MMDTranslationElementType.BONE.name
mmd_translation_element.object = armature_object mmd_translation_element.object = armature_object
mmd_translation_element.data_path = f"pose.bones[{index}]" mmd_translation_element.data_path = f"pose.bones[{index}]"
@@ -140,14 +130,14 @@ class MMDBoneHandler(MMDDataHandlerABC):
@classmethod @classmethod
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
mmd_translation_element: "MMDTranslationElement" mmd_translation_element: MMDTranslationElement
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements): for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
if mmd_translation_element.type != MMDTranslationElementType.BONE.name: if mmd_translation_element.type != MMDTranslationElementType.BONE.name:
continue continue
pose_bone: bpy.types.PoseBone = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) pose_bone: bpy.types.PoseBone = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
if cls.check_data_visible(filter_selected, filter_visible, pose_bone.bone.select, pose_bone.bone.hide): if cls.check_data_visible(filter_selected, filter_visible, pose_bone.select, pose_bone.bone.hide):
continue continue
if check_blank_name(mmd_translation_element.name_j, mmd_translation_element.name_e): if check_blank_name(mmd_translation_element.name_j, mmd_translation_element.name_e):
@@ -156,7 +146,7 @@ class MMDBoneHandler(MMDDataHandlerABC):
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element): if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
continue continue
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add() mmd_translation_element_index: MMDTranslationElementIndex = mmd_translation.filtered_translation_element_indices.add()
mmd_translation_element_index.value = index mmd_translation_element_index.value = index
@classmethod @classmethod
@@ -176,14 +166,11 @@ class MMDBoneHandler(MMDDataHandlerABC):
class MMDMorphHandler(MMDDataHandlerABC): class MMDMorphHandler(MMDDataHandlerABC):
@classmethod type_name = MMDTranslationElementType.MORPH.name
@property
def type_name(cls) -> str:
return MMDTranslationElementType.MORPH.name
@classmethod @classmethod
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int): def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
morph: "_MorphBase" = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) morph: _MorphBase = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
row = layout.row(align=True) row = layout.row(align=True)
row.label(text="", icon="SHAPEKEY_DATA") row.label(text="", icon="SHAPEKEY_DATA")
prop_row = row.row() prop_row = row.row()
@@ -198,7 +185,7 @@ class MMDMorphHandler(MMDDataHandlerABC):
@classmethod @classmethod
def collect_data(cls, mmd_translation: "MMDTranslation"): def collect_data(cls, mmd_translation: "MMDTranslation"):
root_object: bpy.types.Object = mmd_translation.id_data root_object: bpy.types.Object = mmd_translation.id_data
mmd_root: "MMDRoot" = root_object.mmd_root mmd_root: MMDRoot = root_object.mmd_root
for morphs_name, morphs in { for morphs_name, morphs in {
"material_morphs": mmd_root.material_morphs, "material_morphs": mmd_root.material_morphs,
@@ -207,9 +194,9 @@ class MMDMorphHandler(MMDDataHandlerABC):
"vertex_morphs": mmd_root.vertex_morphs, "vertex_morphs": mmd_root.vertex_morphs,
"group_morphs": mmd_root.group_morphs, "group_morphs": mmd_root.group_morphs,
}.items(): }.items():
morph: "_MorphBase" morph: _MorphBase
for index, morph in enumerate(morphs): for index, morph in enumerate(morphs):
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add() mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements.add()
mmd_translation_element.type = MMDTranslationElementType.MORPH.name mmd_translation_element.type = MMDTranslationElementType.MORPH.name
mmd_translation_element.object = root_object mmd_translation_element.object = root_object
mmd_translation_element.data_path = f"mmd_root.{morphs_name}[{index}]" mmd_translation_element.data_path = f"mmd_root.{morphs_name}[{index}]"
@@ -228,24 +215,24 @@ class MMDMorphHandler(MMDDataHandlerABC):
@classmethod @classmethod
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
mmd_translation_element: "MMDTranslationElement" mmd_translation_element: MMDTranslationElement
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements): for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
if mmd_translation_element.type != MMDTranslationElementType.MORPH.name: if mmd_translation_element.type != MMDTranslationElementType.MORPH.name:
continue continue
morph: "_MorphBase" = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) morph: _MorphBase = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
if check_blank_name(morph.name, morph.name_e): if check_blank_name(morph.name, morph.name_e):
continue continue
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element): if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
continue continue
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add() mmd_translation_element_index: MMDTranslationElementIndex = mmd_translation.filtered_translation_element_indices.add()
mmd_translation_element_index.value = index mmd_translation_element_index.value = index
@classmethod @classmethod
def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]): def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]):
morph: "_MorphBase" = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) morph: _MorphBase = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
if name is not None: if name is not None:
morph.name = name morph.name = name
if name_e is not None: if name_e is not None:
@@ -253,15 +240,12 @@ class MMDMorphHandler(MMDDataHandlerABC):
@classmethod @classmethod
def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]: def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]:
morph: "_MorphBase" = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) morph: _MorphBase = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
return (morph.name, "", morph.name_e) return (morph.name, "", morph.name_e)
class MMDMaterialHandler(MMDDataHandlerABC): class MMDMaterialHandler(MMDDataHandlerABC):
@classmethod type_name = MMDTranslationElementType.MATERIAL.name
@property
def type_name(cls) -> str:
return MMDTranslationElementType.MATERIAL.name
@classmethod @classmethod
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int): def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
@@ -274,7 +258,7 @@ class MMDMaterialHandler(MMDDataHandlerABC):
cls.prop_restorable(prop_row, mmd_translation_element, "name_j", material.mmd_material.name_j, index) cls.prop_restorable(prop_row, mmd_translation_element, "name_j", material.mmd_material.name_j, index)
cls.prop_restorable(prop_row, mmd_translation_element, "name_e", material.mmd_material.name_e, index) cls.prop_restorable(prop_row, mmd_translation_element, "name_e", material.mmd_material.name_e, index)
row.prop(mesh_object, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if mesh_object.select_get() else "RESTRICT_SELECT_ON") row.prop(mesh_object, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if mesh_object.select_get() else "RESTRICT_SELECT_ON")
row.prop(mesh_object, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if mesh_object.hide_get() else "HIDE_OFF") row.prop(mesh_object, "hide", text="", emboss=False, icon_only=True)
MATERIAL_DATA_PATH_EXTRACT = re.compile(r"data\.materials\[(?P<index>\d*)\]") MATERIAL_DATA_PATH_EXTRACT = re.compile(r"data\.materials\[(?P<index>\d*)\]")
@@ -293,7 +277,7 @@ class MMDMaterialHandler(MMDDataHandlerABC):
if not hasattr(material, "mmd_material"): if not hasattr(material, "mmd_material"):
continue continue
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add() mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements.add()
mmd_translation_element.type = MMDTranslationElementType.MATERIAL.name mmd_translation_element.type = MMDTranslationElementType.MATERIAL.name
mmd_translation_element.object = mesh_object mmd_translation_element.object = mesh_object
mmd_translation_element.data_path = f"data.materials[{index}]" mmd_translation_element.data_path = f"data.materials[{index}]"
@@ -314,7 +298,7 @@ class MMDMaterialHandler(MMDDataHandlerABC):
@classmethod @classmethod
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
mmd_translation_element: "MMDTranslationElement" mmd_translation_element: MMDTranslationElement
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements): for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
if mmd_translation_element.type != MMDTranslationElementType.MATERIAL.name: if mmd_translation_element.type != MMDTranslationElementType.MATERIAL.name:
continue continue
@@ -330,7 +314,7 @@ class MMDMaterialHandler(MMDDataHandlerABC):
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element): if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
continue continue
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add() mmd_translation_element_index: MMDTranslationElementIndex = mmd_translation.filtered_translation_element_indices.add()
mmd_translation_element_index.value = index mmd_translation_element_index.value = index
@classmethod @classmethod
@@ -350,10 +334,7 @@ class MMDMaterialHandler(MMDDataHandlerABC):
class MMDDisplayHandler(MMDDataHandlerABC): class MMDDisplayHandler(MMDDataHandlerABC):
@classmethod type_name = MMDTranslationElementType.DISPLAY.name
@property
def type_name(cls) -> str:
return MMDTranslationElementType.DISPLAY.name
@classmethod @classmethod
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int): def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
@@ -366,7 +347,7 @@ class MMDDisplayHandler(MMDDataHandlerABC):
cls.prop_disabled(prop_row, mmd_translation_element, "name") cls.prop_disabled(prop_row, mmd_translation_element, "name")
cls.prop_disabled(prop_row, mmd_translation_element, "name_e") cls.prop_disabled(prop_row, mmd_translation_element, "name_e")
row.prop(mmd_translation_element.object, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if mmd_translation_element.object.select_get() else "RESTRICT_SELECT_ON") row.prop(mmd_translation_element.object, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if mmd_translation_element.object.select_get() else "RESTRICT_SELECT_ON")
row.prop(mmd_translation_element.object, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if mmd_translation_element.object.hide_get() else "HIDE_OFF") row.prop(mmd_translation_element.object, "hide", text="", emboss=False, icon_only=True)
DISPLAY_DATA_PATH_EXTRACT = re.compile(r"data\.collections\[(?P<index>\d*)\]") DISPLAY_DATA_PATH_EXTRACT = re.compile(r"data\.collections\[(?P<index>\d*)\]")
@@ -375,7 +356,7 @@ class MMDDisplayHandler(MMDDataHandlerABC):
armature_object: bpy.types.Object = FnModel.find_armature_object(mmd_translation.id_data) armature_object: bpy.types.Object = FnModel.find_armature_object(mmd_translation.id_data)
bone_collection: bpy.types.BoneCollection bone_collection: bpy.types.BoneCollection
for index, bone_collection in enumerate(armature_object.data.collections): for index, bone_collection in enumerate(armature_object.data.collections):
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add() mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements.add()
mmd_translation_element.type = MMDTranslationElementType.DISPLAY.name mmd_translation_element.type = MMDTranslationElementType.DISPLAY.name
mmd_translation_element.object = armature_object mmd_translation_element.object = armature_object
mmd_translation_element.data_path = f"data.collections[{index}]" mmd_translation_element.data_path = f"data.collections[{index}]"
@@ -396,7 +377,7 @@ class MMDDisplayHandler(MMDDataHandlerABC):
@classmethod @classmethod
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
mmd_translation_element: "MMDTranslationElement" mmd_translation_element: MMDTranslationElement
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements): for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
if mmd_translation_element.type != MMDTranslationElementType.DISPLAY.name: if mmd_translation_element.type != MMDTranslationElementType.DISPLAY.name:
continue continue
@@ -412,7 +393,7 @@ class MMDDisplayHandler(MMDDataHandlerABC):
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element): if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
continue continue
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add() mmd_translation_element_index: MMDTranslationElementIndex = mmd_translation.filtered_translation_element_indices.add()
mmd_translation_element_index.value = index mmd_translation_element_index.value = index
@classmethod @classmethod
@@ -428,10 +409,7 @@ class MMDDisplayHandler(MMDDataHandlerABC):
class MMDPhysicsHandler(MMDDataHandlerABC): class MMDPhysicsHandler(MMDDataHandlerABC):
@classmethod type_name = MMDTranslationElementType.PHYSICS.name
@property
def type_name(cls) -> str:
return MMDTranslationElementType.PHYSICS.name
@classmethod @classmethod
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int): def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
@@ -451,7 +429,7 @@ class MMDPhysicsHandler(MMDDataHandlerABC):
cls.prop_restorable(prop_row, mmd_translation_element, "name_j", mmd_object.name_j, index) cls.prop_restorable(prop_row, mmd_translation_element, "name_j", mmd_object.name_j, index)
cls.prop_restorable(prop_row, mmd_translation_element, "name_e", mmd_object.name_e, index) cls.prop_restorable(prop_row, mmd_translation_element, "name_e", mmd_object.name_e, index)
row.prop(obj, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if obj.select_get() else "RESTRICT_SELECT_ON") row.prop(obj, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if obj.select_get() else "RESTRICT_SELECT_ON")
row.prop(obj, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if obj.hide_get() else "HIDE_OFF") row.prop(obj, "hide", text="", emboss=False, icon_only=True)
@classmethod @classmethod
def collect_data(cls, mmd_translation: "MMDTranslation"): def collect_data(cls, mmd_translation: "MMDTranslation"):
@@ -460,7 +438,7 @@ class MMDPhysicsHandler(MMDDataHandlerABC):
obj: bpy.types.Object obj: bpy.types.Object
for obj in model.rigidBodies(): for obj in model.rigidBodies():
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add() mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements.add()
mmd_translation_element.type = MMDTranslationElementType.PHYSICS.name mmd_translation_element.type = MMDTranslationElementType.PHYSICS.name
mmd_translation_element.object = obj mmd_translation_element.object = obj
mmd_translation_element.data_path = "mmd_rigid" mmd_translation_element.data_path = "mmd_rigid"
@@ -470,7 +448,7 @@ class MMDPhysicsHandler(MMDDataHandlerABC):
obj: bpy.types.Object obj: bpy.types.Object
for obj in model.joints(): for obj in model.joints():
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add() mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements.add()
mmd_translation_element.type = MMDTranslationElementType.PHYSICS.name mmd_translation_element.type = MMDTranslationElementType.PHYSICS.name
mmd_translation_element.object = obj mmd_translation_element.object = obj
mmd_translation_element.data_path = "mmd_joint" mmd_translation_element.data_path = "mmd_joint"
@@ -484,7 +462,7 @@ class MMDPhysicsHandler(MMDDataHandlerABC):
@classmethod @classmethod
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
mmd_translation_element: "MMDTranslationElement" mmd_translation_element: MMDTranslationElement
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements): for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
if mmd_translation_element.type != MMDTranslationElementType.PHYSICS.name: if mmd_translation_element.type != MMDTranslationElementType.PHYSICS.name:
continue continue
@@ -504,7 +482,7 @@ class MMDPhysicsHandler(MMDDataHandlerABC):
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element): if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
continue continue
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add() mmd_translation_element_index: MMDTranslationElementIndex = mmd_translation.filtered_translation_element_indices.add()
mmd_translation_element_index.value = index mmd_translation_element_index.value = index
@classmethod @classmethod
@@ -536,10 +514,7 @@ class MMDPhysicsHandler(MMDDataHandlerABC):
class MMDInfoHandler(MMDDataHandlerABC): class MMDInfoHandler(MMDDataHandlerABC):
@classmethod type_name = MMDTranslationElementType.INFO.name
@property
def type_name(cls) -> str:
return MMDTranslationElementType.INFO.name
TYPE_TO_ICONS = { TYPE_TO_ICONS = {
"EMPTY": "EMPTY_DATA", "EMPTY": "EMPTY_DATA",
@@ -557,7 +532,7 @@ class MMDInfoHandler(MMDDataHandlerABC):
cls.prop_disabled(prop_row, mmd_translation_element, "name") cls.prop_disabled(prop_row, mmd_translation_element, "name")
cls.prop_disabled(prop_row, mmd_translation_element, "name_e") cls.prop_disabled(prop_row, mmd_translation_element, "name_e")
row.prop(info_object, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if info_object.select_get() else "RESTRICT_SELECT_ON") row.prop(info_object, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if info_object.select_get() else "RESTRICT_SELECT_ON")
row.prop(info_object, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if info_object.hide_get() else "HIDE_OFF") row.prop(info_object, "hide", text="", emboss=False, icon_only=True)
@classmethod @classmethod
def collect_data(cls, mmd_translation: "MMDTranslation"): def collect_data(cls, mmd_translation: "MMDTranslation"):
@@ -568,7 +543,7 @@ class MMDInfoHandler(MMDDataHandlerABC):
info_objects.append(armature_object) info_objects.append(armature_object)
for info_object in itertools.chain(info_objects, FnModel.iterate_mesh_objects(root_object)): for info_object in itertools.chain(info_objects, FnModel.iterate_mesh_objects(root_object)):
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add() mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements.add()
mmd_translation_element.type = MMDTranslationElementType.INFO.name mmd_translation_element.type = MMDTranslationElementType.INFO.name
mmd_translation_element.object = info_object mmd_translation_element.object = info_object
mmd_translation_element.data_path = "" mmd_translation_element.data_path = ""
@@ -582,7 +557,7 @@ class MMDInfoHandler(MMDDataHandlerABC):
@classmethod @classmethod
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
mmd_translation_element: "MMDTranslationElement" mmd_translation_element: MMDTranslationElement
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements): for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
if mmd_translation_element.type != MMDTranslationElementType.INFO.name: if mmd_translation_element.type != MMDTranslationElementType.INFO.name:
continue continue
@@ -597,7 +572,7 @@ class MMDInfoHandler(MMDDataHandlerABC):
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element): if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
continue continue
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add() mmd_translation_element_index: MMDTranslationElementIndex = mmd_translation.filtered_translation_element_indices.add()
mmd_translation_element_index.value = index mmd_translation_element_index.value = index
@classmethod @classmethod
@@ -627,10 +602,10 @@ MMD_DATA_TYPE_TO_HANDLERS: Dict[str, MMDDataHandlerABC] = {h.type_name: h for h
class FnTranslations: class FnTranslations:
@staticmethod @staticmethod
def apply_translations(root_object: bpy.types.Object): def apply_translations(root_object: bpy.types.Object):
mmd_translation: "MMDTranslation" = root_object.mmd_root.translation mmd_translation: MMDTranslation = root_object.mmd_root.translation
mmd_translation_element_index: "MMDTranslationElementIndex" mmd_translation_element_index: MMDTranslationElementIndex
for mmd_translation_element_index in mmd_translation.filtered_translation_element_indices: for mmd_translation_element_index in mmd_translation.filtered_translation_element_indices:
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements[mmd_translation_element_index.value] mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements[mmd_translation_element_index.value]
handler: MMDDataHandlerABC = MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type] handler: MMDDataHandlerABC = MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type]
name, name_j, name_e = handler.get_names(mmd_translation_element) name, name_j, name_e = handler.get_names(mmd_translation_element)
handler.set_names( handler.set_names(
@@ -642,7 +617,7 @@ class FnTranslations:
@staticmethod @staticmethod
def execute_translation_batch(root_object: bpy.types.Object) -> Tuple[Dict[str, str], Optional[bpy.types.Text]]: def execute_translation_batch(root_object: bpy.types.Object) -> Tuple[Dict[str, str], Optional[bpy.types.Text]]:
mmd_translation: "MMDTranslation" = root_object.mmd_root.translation mmd_translation: MMDTranslation = root_object.mmd_root.translation
batch_operation_script = mmd_translation.batch_operation_script batch_operation_script = mmd_translation.batch_operation_script
if not batch_operation_script: if not batch_operation_script:
return ({}, None) return ({}, None)
@@ -657,9 +632,9 @@ class FnTranslations:
batch_operation_script_ast = compile(mmd_translation.batch_operation_script, "<string>", "eval") batch_operation_script_ast = compile(mmd_translation.batch_operation_script, "<string>", "eval")
batch_operation_target: str = mmd_translation.batch_operation_target batch_operation_target: str = mmd_translation.batch_operation_target
mmd_translation_element_index: "MMDTranslationElementIndex" mmd_translation_element_index: MMDTranslationElementIndex
for mmd_translation_element_index in mmd_translation.filtered_translation_element_indices: for mmd_translation_element_index in mmd_translation.filtered_translation_element_indices:
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements[mmd_translation_element_index.value] mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements[mmd_translation_element_index.value]
handler: MMDDataHandlerABC = MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type] handler: MMDDataHandlerABC = MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type]
@@ -684,7 +659,7 @@ class FnTranslations:
"org_name_j": org_name_j, "org_name_j": org_name_j,
"org_name_e": org_name_e, "org_name_e": org_name_e,
}, },
) ),
) )
if batch_operation_target == "BLENDER": if batch_operation_target == "BLENDER":
@@ -701,8 +676,8 @@ class FnTranslations:
if mmd_translation.filtered_translation_element_indices_active_index < 0: if mmd_translation.filtered_translation_element_indices_active_index < 0:
return return
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices[mmd_translation.filtered_translation_element_indices_active_index] mmd_translation_element_index: MMDTranslationElementIndex = mmd_translation.filtered_translation_element_indices[mmd_translation.filtered_translation_element_indices_active_index]
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements[mmd_translation_element_index.value] mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements[mmd_translation_element_index.value]
MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type].update_index(mmd_translation_element) MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type].update_index(mmd_translation_element)
@@ -724,7 +699,7 @@ class FnTranslations:
filter_visible: bool = mmd_translation.filter_visible filter_visible: bool = mmd_translation.filter_visible
def check_blank_name(name_j: str, name_e: str) -> bool: def check_blank_name(name_j: str, name_e: str) -> bool:
return filter_japanese_blank and name_j or filter_english_blank and name_e return (filter_japanese_blank and name_j) or (filter_english_blank and name_e)
for handler in MMD_DATA_HANDLERS: for handler in MMD_DATA_HANDLERS:
if handler.type_name in mmd_translation.filter_types: if handler.type_name in mmd_translation.filter_types:
+57 -35
View File
@@ -5,12 +5,13 @@
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. # Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import logging from .....core.logging_setup import logger
import math import math
import os import os
from typing import Union from typing import Union
import bpy import bpy
from bpy_extras import anim_utils
from mathutils import Quaternion, Vector from mathutils import Quaternion, Vector
from ... import utils from ... import utils
@@ -260,7 +261,7 @@ class VMDImporter:
def __init__(self, filepath, scale=1.0, bone_mapper=None, use_pose_mode=False, convert_mmd_camera=True, convert_mmd_lamp=True, frame_margin=5, use_mirror=False, use_NLA=False): def __init__(self, filepath, scale=1.0, bone_mapper=None, use_pose_mode=False, convert_mmd_camera=True, convert_mmd_lamp=True, frame_margin=5, use_mirror=False, use_NLA=False):
self.__vmdFile = vmd.File() self.__vmdFile = vmd.File()
self.__vmdFile.load(filepath=filepath) self.__vmdFile.load(filepath=filepath)
logging.debug(str(self.__vmdFile.header)) logger.debug(str(self.__vmdFile.header))
self.__scale = scale self.__scale = scale
self.__convert_mmd_camera = convert_mmd_camera self.__convert_mmd_camera = convert_mmd_camera
self.__convert_mmd_lamp = convert_mmd_lamp self.__convert_mmd_lamp = convert_mmd_lamp
@@ -300,21 +301,31 @@ class VMDImporter:
kp.handle_right = kp.co + Vector((1, 0)) kp.handle_right = kp.co + Vector((1, 0))
@staticmethod @staticmethod
def __keyframe_insert_inner(fcurves: bpy.types.ActionFCurves, path: str, index: int, frame: float, value: float): def __get_channelbag(action: bpy.types.Action, target_id=None):
fcurve = fcurves.find(path, index=index) """Get or create channelbag for action using Blender 5.0 API."""
if not action.slots:
slot = action.slots.new(for_id=target_id)
else:
slot = action.slots[0]
return anim_utils.action_ensure_channelbag_for_slot(action, slot)
@staticmethod
def __keyframe_insert_inner(action: bpy.types.Action, path: str, index: int, frame: float, value: float, target_id=None, group_name=None):
channelbag = VMDImporter.__get_channelbag(action, target_id)
fcurve = channelbag.fcurves.find(path, index=index)
if fcurve is None: if fcurve is None:
fcurve = fcurves.new(path, index=index) fcurve = channelbag.fcurves.new(path, index=index, group_name=group_name)
fcurve.keyframe_points.insert(frame, value, options={"FAST"}) fcurve.keyframe_points.insert(frame, value, options={"FAST"})
@staticmethod @staticmethod
def __keyframe_insert(fcurves: bpy.types.ActionFCurves, path: str, frame: float, value: Union[int, float, Vector]): def __keyframe_insert(action: bpy.types.Action, path: str, frame: float, value: Union[int, float, Vector], target_id=None, group_name=None):
if isinstance(value, (int, float)): if isinstance(value, (int, float)):
VMDImporter.__keyframe_insert_inner(fcurves, path, 0, frame, value) VMDImporter.__keyframe_insert_inner(action, path, 0, frame, value, target_id, group_name)
elif isinstance(value, Vector): elif isinstance(value, Vector):
VMDImporter.__keyframe_insert_inner(fcurves, path, 0, frame, value[0]) VMDImporter.__keyframe_insert_inner(action, path, 0, frame, value[0], target_id, group_name)
VMDImporter.__keyframe_insert_inner(fcurves, path, 1, frame, value[1]) VMDImporter.__keyframe_insert_inner(action, path, 1, frame, value[1], target_id, group_name)
VMDImporter.__keyframe_insert_inner(fcurves, path, 2, frame, value[2]) VMDImporter.__keyframe_insert_inner(action, path, 2, frame, value[2], target_id, group_name)
else: else:
raise TypeError("Unsupported type: {0}".format(type(value))) raise TypeError("Unsupported type: {0}".format(type(value)))
@@ -370,7 +381,7 @@ class VMDImporter:
def __assignToArmature(self, armObj, action_name=None): def __assignToArmature(self, armObj, action_name=None):
boneAnim = self.__vmdFile.boneAnimation boneAnim = self.__vmdFile.boneAnimation
logging.info("---- bone animations:%5d target: %s", len(boneAnim), armObj.name) logger.info("---- bone animations:%5d target: %s", len(boneAnim), armObj.name)
if len(boneAnim) < 1: if len(boneAnim) < 1:
return return
@@ -401,22 +412,25 @@ class VMDImporter:
continue continue
bone = pose_bones.get(name, None) bone = pose_bones.get(name, None)
if bone is None: if bone is None:
logging.warning("WARNING: not found bone %s (%d frames)", name, len(keyFrames)) logger.warning("WARNING: not found bone %s (%d frames)", name, len(keyFrames))
continue continue
logging.info("(bone) frames:%5d name: %s", len(keyFrames), name) logger.info("(bone) frames:%5d name: %s", len(keyFrames), name)
assert bone_name_table.get(bone.name, name) == name assert bone_name_table.get(bone.name, name) == name
bone_name_table[bone.name] = name bone_name_table[bone.name] = name
# Get channelbag for this action
channelbag = self.__get_channelbag(action, armObj.data)
fcurves = [dummy_keyframe_points] * 7 # x, y, z, r0, r1, r2, (r3) fcurves = [dummy_keyframe_points] * 7 # x, y, z, r0, r1, r2, (r3)
data_path_rot = prop_rot_map.get(bone.rotation_mode, "rotation_euler") data_path_rot = prop_rot_map.get(bone.rotation_mode, "rotation_euler")
bone_rotation = getattr(bone, data_path_rot) bone_rotation = getattr(bone, data_path_rot)
default_values = list(bone.location) + list(bone_rotation) default_values = list(bone.location) + list(bone_rotation)
data_path = 'pose.bones["%s"].location' % bone.name data_path = 'pose.bones["%s"].location' % bone.name
for axis_i in range(3): for axis_i in range(3):
fcurves[axis_i] = action.fcurves.new(data_path=data_path, index=axis_i, action_group=bone.name) fcurves[axis_i] = channelbag.fcurves.new(data_path=data_path, index=axis_i, group_name=bone.name)
data_path = 'pose.bones["%s"].%s' % (bone.name, data_path_rot) data_path = 'pose.bones["%s"].%s' % (bone.name, data_path_rot)
for axis_i in range(len(bone_rotation)): for axis_i in range(len(bone_rotation)):
fcurves[3 + axis_i] = action.fcurves.new(data_path=data_path, index=axis_i, action_group=bone.name) fcurves[3 + axis_i] = channelbag.fcurves.new(data_path=data_path, index=axis_i, group_name=bone.name)
for i in range(len(default_values)): for i in range(len(default_values)):
c = fcurves[i] c = fcurves[i]
@@ -458,15 +472,17 @@ class VMDImporter:
self.__setInterpolation(interp[idx : idx + 16 : 4], prev_kp, kp) self.__setInterpolation(interp[idx : idx + 16 : 4], prev_kp, kp)
prev_kps = curr_kps prev_kps = curr_kps
for c in action.fcurves: # Get channelbag to iterate fcurves
channelbag = self.__get_channelbag(action, armObj.data)
for c in channelbag.fcurves:
self.__fixFcurveHandles(c) self.__fixFcurveHandles(c)
# property animation # property animation
propertyAnim = self.__vmdFile.propertyAnimation propertyAnim = self.__vmdFile.propertyAnimation
if len(propertyAnim) > 0: if len(propertyAnim) > 0:
logging.info("---- IK animations:%5d target: %s", len(propertyAnim), armObj.name) logger.info("---- IK animations:%5d target: %s", len(propertyAnim), armObj.name)
for keyFrame in propertyAnim: for keyFrame in propertyAnim:
logging.debug("(IK) frame:%5d list: %s", keyFrame.frame_number, keyFrame.ik_states) logger.debug("(IK) frame:%5d list: %s", keyFrame.frame_number, keyFrame.ik_states)
frame = keyFrame.frame_number + self.__frame_margin frame = keyFrame.frame_number + self.__frame_margin
for ikName, enable in keyFrame.ik_states: for ikName, enable in keyFrame.ik_states:
bone = pose_bones.get(ikName, None) bone = pose_bones.get(ikName, None)
@@ -500,7 +516,7 @@ class VMDImporter:
def __assignToMesh(self, meshObj, action_name=None): def __assignToMesh(self, meshObj, action_name=None):
shapeKeyAnim = self.__vmdFile.shapeKeyAnimation shapeKeyAnim = self.__vmdFile.shapeKeyAnimation
logging.info("---- morph animations:%5d target: %s", len(shapeKeyAnim), meshObj.name) logger.info("---- morph animations:%5d target: %s", len(shapeKeyAnim), meshObj.name)
if len(shapeKeyAnim) < 1: if len(shapeKeyAnim) < 1:
return return
@@ -514,11 +530,12 @@ class VMDImporter:
for name, keyFrames in shapeKeyAnim.items(): for name, keyFrames in shapeKeyAnim.items():
if name not in shapeKeyDict: if name not in shapeKeyDict:
logging.warning("WARNING: not found shape key %s (%d frames)", name, len(keyFrames)) logger.warning("WARNING: not found shape key %s (%d frames)", name, len(keyFrames))
continue continue
logging.info("(mesh) frames:%5d name: %s", len(keyFrames), name) logger.info("(mesh) frames:%5d name: %s", len(keyFrames), name)
shapeKey = shapeKeyDict[name] shapeKey = shapeKeyDict[name]
fcurve = action.fcurves.new(data_path='key_blocks["%s"].value' % shapeKey.name) channelbag = self.__get_channelbag(action, meshObj.data.shape_keys)
fcurve = channelbag.fcurves.new(data_path='key_blocks["%s"].value' % shapeKey.name)
fcurve.keyframe_points.add(len(keyFrames)) fcurve.keyframe_points.add(len(keyFrames))
keyFrames.sort(key=lambda x: x.frame_number) keyFrames.sort(key=lambda x: x.frame_number)
for k, v in zip(keyFrames, fcurve.keyframe_points): for k, v in zip(keyFrames, fcurve.keyframe_points):
@@ -532,16 +549,16 @@ class VMDImporter:
def __assignToRoot(self, rootObj, action_name=None): def __assignToRoot(self, rootObj, action_name=None):
propertyAnim = self.__vmdFile.propertyAnimation propertyAnim = self.__vmdFile.propertyAnimation
logging.info("---- display animations:%5d target: %s", len(propertyAnim), rootObj.name) logger.info("---- display animations:%5d target: %s", len(propertyAnim), rootObj.name)
if len(propertyAnim) < 1: if len(propertyAnim) < 1:
return return
action_name = action_name or rootObj.name action_name = action_name or rootObj.name
action = bpy.data.actions.new(name=action_name) action = bpy.data.actions.new(name=action_name)
logging.debug("(Display) list(frame, show): %s", [(keyFrame.frame_number, bool(keyFrame.visible)) for keyFrame in propertyAnim]) logger.debug("(Display) list(frame, show): %s", [(keyFrame.frame_number, bool(keyFrame.visible)) for keyFrame in propertyAnim])
for keyFrame in propertyAnim: for keyFrame in propertyAnim:
self.__keyframe_insert(action.fcurves, "mmd_root.show_meshes", keyFrame.frame_number + self.__frame_margin, float(keyFrame.visible)) self.__keyframe_insert(action, "mmd_root.show_meshes", keyFrame.frame_number + self.__frame_margin, float(keyFrame.visible), rootObj)
self.__assign_action(rootObj, action) self.__assign_action(rootObj, action)
@@ -562,7 +579,7 @@ class VMDImporter:
cameraObj = mmdCameraInstance.camera() cameraObj = mmdCameraInstance.camera()
cameraAnim = self.__vmdFile.cameraAnimation cameraAnim = self.__vmdFile.cameraAnimation
logging.info("(camera) frames:%5d name: %s", len(cameraAnim), mmdCamera.name) logger.info("(camera) frames:%5d name: %s", len(cameraAnim), mmdCamera.name)
if len(cameraAnim) < 1: if len(cameraAnim) < 1:
return return
@@ -574,14 +591,18 @@ class VMDImporter:
if self.__mirror: if self.__mirror:
_loc, _rot = _MirrorMapper.get_location, _MirrorMapper.get_rotation3 _loc, _rot = _MirrorMapper.get_location, _MirrorMapper.get_rotation3
# Get channelbags for camera actions
parent_channelbag = self.__get_channelbag(parent_action, mmdCamera.parent)
distance_channelbag = self.__get_channelbag(distance_action, mmdCamera.distance)
fcurves = [] fcurves = []
for i in range(3): for i in range(3):
fcurves.append(parent_action.fcurves.new(data_path="location", index=i)) # x, y, z fcurves.append(parent_channelbag.fcurves.new(data_path="location", index=i)) # x, y, z
for i in range(3): for i in range(3):
fcurves.append(parent_action.fcurves.new(data_path="rotation_euler", index=i)) # rx, ry, rz fcurves.append(parent_channelbag.fcurves.new(data_path="rotation_euler", index=i)) # rx, ry, rz
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.angle")) # fov fcurves.append(parent_channelbag.fcurves.new(data_path="mmd_camera.angle")) # fov
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.is_perspective")) # persp fcurves.append(parent_channelbag.fcurves.new(data_path="mmd_camera.is_perspective")) # persp
fcurves.append(distance_action.fcurves.new(data_path="location", index=1)) # dis fcurves.append(distance_channelbag.fcurves.new(data_path="location", index=1)) # dis
for c in fcurves: for c in fcurves:
c.keyframe_points.add(len(cameraAnim)) c.keyframe_points.add(len(cameraAnim))
@@ -629,7 +650,7 @@ class VMDImporter:
lampObj = mmdLampInstance.lamp() lampObj = mmdLampInstance.lamp()
lampAnim = self.__vmdFile.lampAnimation lampAnim = self.__vmdFile.lampAnimation
logging.info("(lamp) frames:%5d name: %s", len(lampAnim), mmdLamp.name) logger.info("(lamp) frames:%5d name: %s", len(lampAnim), mmdLamp.name)
if len(lampAnim) < 1: if len(lampAnim) < 1:
return return
@@ -640,10 +661,11 @@ class VMDImporter:
_loc = _MirrorMapper.get_location if self.__mirror else lambda i: i _loc = _MirrorMapper.get_location if self.__mirror else lambda i: i
for keyFrame in lampAnim: for keyFrame in lampAnim:
frame = keyFrame.frame_number + self.__frame_margin frame = keyFrame.frame_number + self.__frame_margin
self.__keyframe_insert(color_action.fcurves, "color", frame, Vector(keyFrame.color)) self.__keyframe_insert(color_action, "color", frame, Vector(keyFrame.color), lampObj)
self.__keyframe_insert(location_action.fcurves, "location", frame, Vector(_loc(keyFrame.direction)).xzy * -1) self.__keyframe_insert(location_action, "location", frame, Vector(_loc(keyFrame.direction)).xzy * -1, mmdLamp)
for fcurve in location_action.fcurves: location_channelbag = self.__get_channelbag(location_action, mmdLamp)
for fcurve in location_channelbag.fcurves:
self.detectLampChange(fcurve) self.detectLampChange(fcurve)
self.__assign_action(lampObj.data, color_action) self.__assign_action(lampObj.data, color_action)
+44 -61
View File
@@ -1,48 +1,39 @@
# -*- coding: utf-8 -*- # Copyright 2012 MMD Tools authors
# Copyright 2014 MMD Tools authors # This file is part of MMD Tools.
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import Iterable, Optional, Any, List, Tuple, Union from typing import Iterable, Optional
import bpy import bpy
from bpy.types import Material, NodeTree, Node, NodeSocket, ShaderNodeGroup, ShaderNodeOutputMaterial, NodeLink
from ..logging_setup import logger
from .core.shader import _NodeGroupUtils
from .core.material import FnMaterial from .core.material import FnMaterial
from .core.shader import _NodeGroupUtils
def __switchToCyclesRenderEngine() -> None: def __switchToCyclesRenderEngine():
if bpy.context.scene.render.engine != "CYCLES": if bpy.context.scene.render.engine != "CYCLES":
logger.debug("Switching render engine to Cycles")
bpy.context.scene.render.engine = "CYCLES" bpy.context.scene.render.engine = "CYCLES"
def __exposeNodeTreeInput(in_socket: NodeSocket, name: str, default_value: Any, node_input: Node, shader: NodeTree) -> None: def __exposeNodeTreeInput(in_socket, name, default_value, node_input, shader):
_NodeGroupUtils(shader).new_input_socket(name, in_socket, default_value) _NodeGroupUtils(shader).new_input_socket(name, in_socket, default_value)
def __exposeNodeTreeOutput(out_socket: NodeSocket, name: str, node_output: Node, shader: NodeTree) -> None: def __exposeNodeTreeOutput(out_socket, name, node_output, shader):
_NodeGroupUtils(shader).new_output_socket(name, out_socket) _NodeGroupUtils(shader).new_output_socket(name, out_socket)
def __getMaterialOutput(nodes: bpy.types.Nodes, bl_idname: str) -> Node: def __getMaterialOutput(nodes, bl_idname):
o = next((n for n in nodes if n.bl_idname == bl_idname and n.is_active_output), None) or nodes.new(bl_idname) o = next((n for n in nodes if n.bl_idname == bl_idname and n.is_active_output), None) or nodes.new(bl_idname)
o.is_active_output = True o.is_active_output = True
return o return o
def create_MMDAlphaShader() -> NodeTree: def create_MMDAlphaShader():
__switchToCyclesRenderEngine() __switchToCyclesRenderEngine()
if "MMDAlphaShader" in bpy.data.node_groups: if "MMDAlphaShader" in bpy.data.node_groups:
logger.debug("Using existing MMDAlphaShader node group")
return bpy.data.node_groups["MMDAlphaShader"] return bpy.data.node_groups["MMDAlphaShader"]
logger.info("Creating new MMDAlphaShader node group")
shader = bpy.data.node_groups.new(name="MMDAlphaShader", type="ShaderNodeTree") shader = bpy.data.node_groups.new(name="MMDAlphaShader", type="ShaderNodeTree")
node_input = shader.nodes.new("NodeGroupInput") node_input = shader.nodes.new("NodeGroupInput")
@@ -64,28 +55,26 @@ def create_MMDAlphaShader() -> NodeTree:
return shader return shader
def create_MMDBasicShader() -> NodeTree: def create_MMDBasicShader():
__switchToCyclesRenderEngine() __switchToCyclesRenderEngine()
if "MMDBasicShader" in bpy.data.node_groups: if "MMDBasicShader" in bpy.data.node_groups:
logger.debug("Using existing MMDBasicShader node group")
return bpy.data.node_groups["MMDBasicShader"] return bpy.data.node_groups["MMDBasicShader"]
logger.info("Creating new MMDBasicShader node group") shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.new(name="MMDBasicShader", type="ShaderNodeTree")
shader: NodeTree = bpy.data.node_groups.new(name="MMDBasicShader", type="ShaderNodeTree")
node_input: Node = shader.nodes.new("NodeGroupInput") node_input: bpy.types.NodeGroupInput = shader.nodes.new("NodeGroupInput")
node_output: Node = shader.nodes.new("NodeGroupOutput") node_output: bpy.types.NodeGroupOutput = shader.nodes.new("NodeGroupOutput")
node_output.location.x += 250 node_output.location.x += 250
node_input.location.x -= 500 node_input.location.x -= 500
dif: Node = shader.nodes.new("ShaderNodeBsdfDiffuse") dif: bpy.types.ShaderNodeBsdfDiffuse = shader.nodes.new("ShaderNodeBsdfDiffuse")
dif.location.x -= 250 dif.location.x -= 250
dif.location.y += 150 dif.location.y += 150
glo: Node = shader.nodes.new("ShaderNodeBsdfAnisotropic") glo: bpy.types.ShaderNodeBsdfAnisotropic = shader.nodes.new("ShaderNodeBsdfAnisotropic")
glo.location.x -= 250 glo.location.x -= 250
glo.location.y -= 150 glo.location.y -= 150
mix: Node = shader.nodes.new("ShaderNodeMixShader") mix: bpy.types.ShaderNodeMixShader = shader.nodes.new("ShaderNodeMixShader")
shader.links.new(mix.inputs[1], dif.outputs["BSDF"]) shader.links.new(mix.inputs[1], dif.outputs["BSDF"])
shader.links.new(mix.inputs[2], glo.outputs["BSDF"]) shader.links.new(mix.inputs[2], glo.outputs["BSDF"])
@@ -98,65 +87,62 @@ def create_MMDBasicShader() -> NodeTree:
return shader return shader
def __enum_linked_nodes(node: Node) -> Iterable[Node]: def __enum_linked_nodes(node: bpy.types.Node) -> Iterable[bpy.types.Node]:
yield node yield node
if node.parent: if node.parent:
yield node.parent yield node.parent
for n in set(l.from_node for i in node.inputs for l in i.links): for n in {link.from_node for i in node.inputs for link in i.links}:
yield from __enum_linked_nodes(n) yield from __enum_linked_nodes(n)
def __cleanNodeTree(material: Material) -> None: def __cleanNodeTree(material: bpy.types.Material):
logger.debug(f"Cleaning node tree for material: {material.name}")
nodes = material.node_tree.nodes nodes = material.node_tree.nodes
node_names = set(n.name for n in nodes) node_names = {n.name for n in nodes}
for o in (n for n in nodes if n.bl_idname in {"ShaderNodeOutput", "ShaderNodeOutputMaterial"}): for o in (n for n in nodes if n.bl_idname in {"ShaderNodeOutput", "ShaderNodeOutputMaterial"}):
if any(i.is_linked for i in o.inputs): if any(i.is_linked for i in o.inputs):
node_names -= set(linked.name for linked in __enum_linked_nodes(o)) node_names -= {linked.name for linked in __enum_linked_nodes(o)}
for name in node_names: for name in node_names:
nodes.remove(nodes[name]) nodes.remove(nodes[name])
def convertToCyclesShader(obj: bpy.types.Object, use_principled: bool = False, clean_nodes: bool = False, subsurface: float = 0.001) -> None: def convertToCyclesShader(obj: bpy.types.Object, use_principled=False, clean_nodes=False, subsurface=0.001):
logger.info(f"Converting {obj.name} to Cycles shader (use_principled={use_principled}, clean_nodes={clean_nodes})")
__switchToCyclesRenderEngine() __switchToCyclesRenderEngine()
convertToBlenderShader(obj, use_principled, clean_nodes, subsurface) convertToBlenderShader(obj, use_principled, clean_nodes, subsurface)
def convertToBlenderShader(obj: bpy.types.Object, use_principled: bool = False, clean_nodes: bool = False, subsurface: float = 0.001) -> None: def convertToBlenderShader(obj: bpy.types.Object, use_principled=False, clean_nodes=False, subsurface=0.001):
for i in obj.material_slots: for i in obj.material_slots:
if not i.material: if not i.material:
continue continue
# use_nodes is deprecated in 5.0 but always returns True and setting it is safe
if not i.material.use_nodes: if not i.material.use_nodes:
logger.debug(f"Enabling nodes for material: {i.material.name}")
i.material.use_nodes = True i.material.use_nodes = True
__convertToMMDBasicShader(i.material) __convertToMMDBasicShader(i.material)
if use_principled: if use_principled:
logger.debug(f"Converting material to Principled BSDF: {i.material.name}")
__convertToPrincipledBsdf(i.material, subsurface) __convertToPrincipledBsdf(i.material, subsurface)
if clean_nodes: if clean_nodes:
__cleanNodeTree(i.material) __cleanNodeTree(i.material)
def convertToMMDShader(obj: bpy.types.Object) -> None:
def convertToMMDShader(obj):
"""BSDF -> MMDShaderDev conversion.""" """BSDF -> MMDShaderDev conversion."""
logger.info(f"Converting {obj.name} to MMD shader")
for i in obj.material_slots: for i in obj.material_slots:
if not i.material: if not i.material:
continue continue
# use_nodes is deprecated in 5.0 but always returns True and setting it is safe
if not i.material.use_nodes: if not i.material.use_nodes:
logger.debug(f"Enabling nodes for material: {i.material.name}")
i.material.use_nodes = True i.material.use_nodes = True
FnMaterial.convert_to_mmd_material(i.material) FnMaterial.convert_to_mmd_material(i.material)
def __convertToMMDBasicShader(material: Material) -> None:
logger.debug(f"Converting material to MMD Basic Shader: {material.name}") def __convertToMMDBasicShader(material: bpy.types.Material):
# TODO: test me # TODO: test me
mmd_basic_shader_grp = create_MMDBasicShader() mmd_basic_shader_grp = create_MMDBasicShader()
mmd_alpha_shader_grp = create_MMDAlphaShader() mmd_alpha_shader_grp = create_MMDAlphaShader()
if not any(filter(lambda x: isinstance(x, ShaderNodeGroup) and x.node_tree.name in {"MMDBasicShader", "MMDAlphaShader"}, material.node_tree.nodes)): if not any(filter(lambda x: isinstance(x, bpy.types.ShaderNodeGroup) and x.node_tree.name in {"MMDBasicShader", "MMDAlphaShader"}, material.node_tree.nodes)):
# Add nodes for Cycles Render # Add nodes for Cycles Render
shader: ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup") shader: bpy.types.ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup")
shader.node_tree = mmd_basic_shader_grp shader.node_tree = mmd_basic_shader_grp
shader.inputs[0].default_value[:3] = material.diffuse_color[:3] shader.inputs[0].default_value[:3] = material.diffuse_color[:3]
shader.inputs[1].default_value[:3] = material.specular_color[:3] shader.inputs[1].default_value[:3] = material.specular_color[:3]
@@ -171,8 +157,7 @@ def __convertToMMDBasicShader(material: Material) -> None:
alpha_value = material.diffuse_color[3] alpha_value = material.diffuse_color[3]
if alpha_value < 1.0: if alpha_value < 1.0:
logger.debug(f"Material has alpha: {material.name}, alpha={alpha_value}") alpha_shader: bpy.types.ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup")
alpha_shader: ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup")
alpha_shader.location.x = shader.location.x + 250 alpha_shader.location.x = shader.location.x + 250
alpha_shader.location.y = shader.location.y - 150 alpha_shader.location.y = shader.location.y - 150
alpha_shader.node_tree = mmd_alpha_shader_grp alpha_shader.node_tree = mmd_alpha_shader_grp
@@ -180,22 +165,21 @@ def __convertToMMDBasicShader(material: Material) -> None:
material.node_tree.links.new(alpha_shader.inputs[0], outplug) material.node_tree.links.new(alpha_shader.inputs[0], outplug)
outplug = alpha_shader.outputs[0] outplug = alpha_shader.outputs[0]
material_output: ShaderNodeOutputMaterial = __getMaterialOutput(material.node_tree.nodes, "ShaderNodeOutputMaterial") material_output: bpy.types.ShaderNodeOutputMaterial = __getMaterialOutput(material.node_tree.nodes, "ShaderNodeOutputMaterial")
material.node_tree.links.new(material_output.inputs["Surface"], outplug) material.node_tree.links.new(material_output.inputs["Surface"], outplug)
material_output.location.x = shader.location.x + 500 material_output.location.x = shader.location.x + 500
material_output.location.y = shader.location.y - 150 material_output.location.y = shader.location.y - 150
def __convertToPrincipledBsdf(material: Material, subsurface: float) -> None: def __convertToPrincipledBsdf(material: bpy.types.Material, subsurface: float):
logger.debug(f"Converting material to Principled BSDF: {material.name}")
node_names = set() node_names = set()
for s in (n for n in material.node_tree.nodes if isinstance(n, ShaderNodeGroup)): for s in (n for n in material.node_tree.nodes if isinstance(n, bpy.types.ShaderNodeGroup)):
if s.node_tree.name == "MMDBasicShader": if s.node_tree.name == "MMDBasicShader":
l: NodeLink link: bpy.types.NodeLink
for l in s.outputs[0].links: for link in s.outputs[0].links:
to_node = l.to_node to_node = link.to_node
# assuming there is no bpy.types.NodeReroute between MMDBasicShader and MMDAlphaShader # assuming there is no bpy.types.NodeReroute between MMDBasicShader and MMDAlphaShader
if isinstance(to_node, ShaderNodeGroup) and to_node.node_tree.name == "MMDAlphaShader": if isinstance(to_node, bpy.types.ShaderNodeGroup) and to_node.node_tree.name == "MMDAlphaShader":
__switchToPrincipledBsdf(material.node_tree, s, subsurface, node_alpha=to_node) __switchToPrincipledBsdf(material.node_tree, s, subsurface, node_alpha=to_node)
node_names.add(to_node.name) node_names.add(to_node.name)
else: else:
@@ -210,9 +194,8 @@ def __convertToPrincipledBsdf(material: Material, subsurface: float) -> None:
nodes.remove(nodes[name]) nodes.remove(nodes[name])
def __switchToPrincipledBsdf(node_tree: NodeTree, node_basic: ShaderNodeGroup, subsurface: float, node_alpha: Optional[ShaderNodeGroup] = None) -> None: def __switchToPrincipledBsdf(node_tree: bpy.types.NodeTree, node_basic: bpy.types.ShaderNodeGroup, subsurface: float, node_alpha: Optional[bpy.types.ShaderNodeGroup] = None):
logger.debug(f"Switching to Principled BSDF: {node_basic.name}") shader: bpy.types.ShaderNodeBsdfPrincipled = node_tree.nodes.new("ShaderNodeBsdfPrincipled")
shader: Node = node_tree.nodes.new("ShaderNodeBsdfPrincipled")
shader.parent = node_basic.parent shader.parent = node_basic.parent
shader.location.x = node_basic.location.x shader.location.x = node_basic.location.x
shader.location.y = node_basic.location.y shader.location.y = node_basic.location.y
@@ -240,7 +223,7 @@ def __switchToPrincipledBsdf(node_tree: NodeTree, node_basic: ShaderNodeGroup, s
if alpha_socket_name in node_alpha.inputs: if alpha_socket_name in node_alpha.inputs:
if "Alpha" in shader.inputs: if "Alpha" in shader.inputs:
shader.inputs["Alpha"].default_value = node_alpha.inputs[alpha_socket_name].default_value shader.inputs["Alpha"].default_value = node_alpha.inputs["Alpha"].default_value
if node_alpha.inputs[alpha_socket_name].is_linked: if node_alpha.inputs[alpha_socket_name].is_linked:
node_tree.links.new(node_alpha.inputs[alpha_socket_name].links[0].from_socket, shader.inputs["Alpha"]) node_tree.links.new(node_alpha.inputs[alpha_socket_name].links[0].from_socket, shader.inputs["Alpha"])
else: else:
@@ -256,5 +239,5 @@ def __switchToPrincipledBsdf(node_tree: NodeTree, node_basic: ShaderNodeGroup, s
node_tree.links.new(node_alpha.inputs[alpha_socket_name].links[0].from_socket, node_invert.inputs[1]) node_tree.links.new(node_alpha.inputs[alpha_socket_name].links[0].from_socket, node_invert.inputs[1])
node_tree.links.new(node_invert.outputs[0], shader.inputs["Transmission"]) node_tree.links.new(node_invert.outputs[0], shader.inputs["Transmission"])
for l in output_links: for link in output_links:
node_tree.links.new(shader.outputs[0], l.to_socket) node_tree.links.new(shader.outputs[0], link.to_socket)
File diff suppressed because it is too large Load Diff
+142 -98
View File
@@ -1,22 +1,16 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors # Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender # This file is part of MMD Tools.
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. from collections import defaultdict
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import bpy import bpy
from bpy.props import BoolProperty, StringProperty, FloatProperty from bpy.props import BoolProperty, StringProperty
from bpy.types import Operator, Context, Object, Material from bpy.types import Operator
from typing import Set, Dict, Any, List, Tuple, Optional, Union, cast
from .. import cycles_converter from .. import cycles_converter
from ..core.exceptions import MaterialNotFoundError from ..core.exceptions import MaterialNotFoundError
from ..core.material import FnMaterial from ..core.material import FnMaterial
from ..core.shader import _NodeGroupUtils from ..core.shader import _NodeGroupUtils
from ....core.logging_setup import logger
import traceback
class ConvertMaterialsForCycles(Operator): class ConvertMaterialsForCycles(Operator):
@@ -25,14 +19,14 @@ class ConvertMaterialsForCycles(Operator):
bl_description = "Convert materials of selected objects for Cycles." bl_description = "Convert materials of selected objects for Cycles."
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
use_principled: BoolProperty( use_principled: bpy.props.BoolProperty(
name="Convert to Principled BSDF", name="Convert to Principled BSDF",
description="Convert MMD shader nodes to Principled BSDF as well if enabled", description="Convert MMD shader nodes to Principled BSDF as well if enabled",
default=False, default=False,
options={"SKIP_SAVE"}, options={"SKIP_SAVE"},
) )
clean_nodes: BoolProperty( clean_nodes: bpy.props.BoolProperty(
name="Clean Nodes", name="Clean Nodes",
description="Remove redundant nodes as well if enabled. Disable it to keep node data.", description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
default=False, default=False,
@@ -40,27 +34,22 @@ class ConvertMaterialsForCycles(Operator):
) )
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context):
return next((x for x in context.selected_objects if x.type == "MESH"), None) is not None return any(x.type == "MESH" for x in context.selected_objects)
def draw(self, context: Context) -> None: def draw(self, context):
layout = self.layout layout = self.layout
layout.prop(self, "use_principled") layout.prop(self, "use_principled")
layout.prop(self, "clean_nodes") layout.prop(self, "clean_nodes")
def execute(self, context: Context) -> Set[str]: def execute(self, context):
try: try:
context.scene.render.engine = "CYCLES" context.scene.render.engine = "CYCLES"
except Exception: except Exception:
logger.error(f"Failed to change to Cycles render engine: {traceback.format_exc()}")
self.report({"ERROR"}, " * Failed to change to Cycles render engine.") self.report({"ERROR"}, " * Failed to change to Cycles render engine.")
return {"CANCELLED"} return {"CANCELLED"}
logger.info(f"Converting materials for Cycles with principled={self.use_principled}, clean_nodes={self.clean_nodes}")
for obj in (x for x in context.selected_objects if x.type == "MESH"): for obj in (x for x in context.selected_objects if x.type == "MESH"):
logger.debug(f"Converting materials for object: {obj.name}")
cycles_converter.convertToCyclesShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes) cycles_converter.convertToCyclesShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes)
return {"FINISHED"} return {"FINISHED"}
@@ -70,21 +59,21 @@ class ConvertMaterials(Operator):
bl_description = "Convert materials of selected objects." bl_description = "Convert materials of selected objects."
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
use_principled: BoolProperty( use_principled: bpy.props.BoolProperty(
name="Convert to Principled BSDF", name="Convert to Principled BSDF",
description="Convert MMD shader nodes to Principled BSDF as well if enabled", description="Convert MMD shader nodes to Principled BSDF as well if enabled",
default=True, default=True,
options={"SKIP_SAVE"}, options={"SKIP_SAVE"},
) )
clean_nodes: BoolProperty( clean_nodes: bpy.props.BoolProperty(
name="Clean Nodes", name="Clean Nodes",
description="Remove redundant nodes as well if enabled. Disable it to keep node data.", description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
default=True, default=True,
options={"SKIP_SAVE"}, options={"SKIP_SAVE"},
) )
subsurface: FloatProperty( subsurface: bpy.props.FloatProperty(
name="Subsurface", name="Subsurface",
default=0.001, default=0.001,
soft_min=0.000, soft_min=0.000,
@@ -94,41 +83,130 @@ class ConvertMaterials(Operator):
) )
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context):
return next((x for x in context.selected_objects if x.type == "MESH"), None) is not None return any(x.type == "MESH" for x in context.selected_objects)
def execute(self, context: Context) -> Set[str]: def execute(self, context):
logger.info(f"Converting materials with principled={self.use_principled}, clean_nodes={self.clean_nodes}, subsurface={self.subsurface}")
for obj in context.selected_objects: for obj in context.selected_objects:
if obj.type != "MESH": if obj.type != "MESH":
continue continue
logger.debug(f"Converting materials for object: {obj.name}")
cycles_converter.convertToBlenderShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes, subsurface=self.subsurface) cycles_converter.convertToBlenderShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes, subsurface=self.subsurface)
return {"FINISHED"} return {"FINISHED"}
class ConvertBSDFMaterials(Operator):
bl_idname = 'mmd_tools.convert_bsdf_materials' class MergeMaterials(Operator):
bl_label = 'Convert Blender Materials' bl_idname = "mmd_tools.merge_materials"
bl_description = 'Convert materials of selected objects.' bl_label = "Merge Materials"
bl_options = {'REGISTER', 'UNDO'} bl_description = "Merge materials with the same texture in selected objects. Only merges materials with exactly one texture node. Materials with no texture or with multiple textures are not merged. Please convert to Blender materials first."
bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context):
return next((x for x in context.selected_objects if x.type == 'MESH'), None) is not None return any(x.type == "MESH" for x in context.selected_objects)
def execute(self, context: Context) -> Set[str]: def execute(self, context):
logger.info("Converting BSDF materials to MMD shader") # Process all selected mesh objects
for obj in context.selected_objects: for obj in context.selected_objects:
if obj.type != 'MESH': if obj.type != "MESH":
continue
self.merge_materials_for_object(context, obj)
return {"FINISHED"}
def merge_materials_for_object(self, context, obj):
"""Merge materials with same texture for a single object"""
if not obj.data.materials:
self.report({"INFO"}, f"Object '{obj.name}' has no materials")
return
# Map texture paths to material indices and names
texture_to_materials = defaultdict(list)
# Check each material
for i, material in enumerate(obj.data.materials):
# use_nodes is deprecated in 5.0 but always returns True, so check is safe
if not material or not material.use_nodes:
continue
# 1. Check texture node count (must be exactly 1)
texture_nodes = [node for node in material.node_tree.nodes if node.type == "TEX_IMAGE"]
if len(texture_nodes) != 1:
continue
# 2. Record texture path and material info
texture_node = texture_nodes[0]
if texture_node.image:
texture_path = bpy.path.abspath(texture_node.image.filepath)
texture_to_materials[texture_path].append({"index": i, "name": material.name})
# Find material groups that need merging
materials_to_merge = {path: materials for path, materials in texture_to_materials.items() if len(materials) > 1}
if not materials_to_merge:
self.report({"INFO"}, f"No materials to merge in object '{obj.name}'")
return
# Process each texture group
context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode="EDIT")
merge_details = []
for texture_path, materials in materials_to_merge.items():
# Use first material as target
target_material = materials[0]
target_index = target_material["index"]
target_name = target_material["name"]
source_materials = []
# Reassign faces from other materials to target material
for source_material in materials[1:]:
source_index = source_material["index"]
source_name = source_material["name"]
source_materials.append(source_name)
bpy.ops.mesh.select_all(action="DESELECT")
obj.active_material_index = source_index
bpy.ops.object.material_slot_select()
obj.active_material_index = target_index
bpy.ops.object.material_slot_assign()
# Record merge details
texture_name = bpy.path.basename(texture_path)
merge_details.append({"texture": texture_name, "target": target_name, "sources": source_materials})
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.material_slot_remove_unused()
merged_count = sum(len(details["sources"]) for details in merge_details)
self.report({"INFO"}, f"Object '{obj.name}': Merged {merged_count} materials")
for details in merge_details:
sources_text = ", ".join(details["sources"])
self.report({"INFO"}, f"Same Texture '{details['texture']}': Merged materials [{sources_text}] into '{details['target']}'")
class ConvertBSDFMaterials(Operator):
bl_idname = "mmd_tools.convert_bsdf_materials"
bl_label = "Convert Blender Materials"
bl_description = "Convert materials of selected objects."
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return any(x.type == "MESH" for x in context.selected_objects)
def execute(self, context):
for obj in context.selected_objects:
if obj.type != "MESH":
continue continue
logger.debug(f"Converting BSDF materials for object: {obj.name}")
cycles_converter.convertToMMDShader(obj) cycles_converter.convertToMMDShader(obj)
return {'FINISHED'} return {"FINISHED"}
class _OpenTextureBase: class _OpenTextureBase:
"""Create a texture for mmd model material.""" """Create a texture for mmd model material."""
bl_options: Set[str] = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
filepath: StringProperty( filepath: StringProperty(
name="File Path", name="File Path",
@@ -142,7 +220,7 @@ class _OpenTextureBase:
options={"HIDDEN"}, options={"HIDDEN"},
) )
def invoke(self, context: Context, event: Any) -> Set[str]: def invoke(self, context, event):
context.window_manager.fileselect_add(self) context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"} return {"RUNNING_MODAL"}
@@ -152,13 +230,8 @@ class OpenTexture(Operator, _OpenTextureBase):
bl_label = "Open Texture" bl_label = "Open Texture"
bl_description = "Create main texture of active material" bl_description = "Create main texture of active material"
def execute(self, context: Context) -> Set[str]: def execute(self, context):
mat = context.active_object.active_material mat = context.active_object.active_material
if not mat:
logger.error("No active material found")
return {"CANCELLED"}
logger.info(f"Creating texture for material: {mat.name} from {self.filepath}")
fnMat = FnMaterial(mat) fnMat = FnMaterial(mat)
fnMat.create_texture(self.filepath) fnMat.create_texture(self.filepath)
return {"FINISHED"} return {"FINISHED"}
@@ -172,13 +245,8 @@ class RemoveTexture(Operator):
bl_description = "Remove main texture of active material" bl_description = "Remove main texture of active material"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: Context) -> Set[str]: def execute(self, context):
mat = context.active_object.active_material mat = context.active_object.active_material
if not mat:
logger.error("No active material found")
return {"CANCELLED"}
logger.info(f"Removing texture from material: {mat.name}")
fnMat = FnMaterial(mat) fnMat = FnMaterial(mat)
fnMat.remove_texture() fnMat.remove_texture()
return {"FINISHED"} return {"FINISHED"}
@@ -191,13 +259,8 @@ class OpenSphereTextureSlot(Operator, _OpenTextureBase):
bl_label = "Open Sphere Texture" bl_label = "Open Sphere Texture"
bl_description = "Create sphere texture of active material" bl_description = "Create sphere texture of active material"
def execute(self, context: Context) -> Set[str]: def execute(self, context):
mat = context.active_object.active_material mat = context.active_object.active_material
if not mat:
logger.error("No active material found")
return {"CANCELLED"}
logger.info(f"Creating sphere texture for material: {mat.name} from {self.filepath}")
fnMat = FnMaterial(mat) fnMat = FnMaterial(mat)
fnMat.create_sphere_texture(self.filepath, context.active_object) fnMat.create_sphere_texture(self.filepath, context.active_object)
return {"FINISHED"} return {"FINISHED"}
@@ -211,13 +274,8 @@ class RemoveSphereTexture(Operator):
bl_description = "Remove sphere texture of active material" bl_description = "Remove sphere texture of active material"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: Context) -> Set[str]: def execute(self, context):
mat = context.active_object.active_material mat = context.active_object.active_material
if not mat:
logger.error("No active material found")
return {"CANCELLED"}
logger.info(f"Removing sphere texture from material: {mat.name}")
fnMat = FnMaterial(mat) fnMat = FnMaterial(mat)
fnMat.remove_sphere_texture() fnMat.remove_sphere_texture()
return {"FINISHED"} return {"FINISHED"}
@@ -230,21 +288,17 @@ class MoveMaterialUp(Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context):
obj = context.active_object obj = context.active_object
valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE" return obj is not None and obj.type == "MESH" and obj.mmd_type == "NONE" and obj.active_material_index > 0
return bool(valid_mesh and obj.active_material_index > 0)
def execute(self, context: Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
current_idx = obj.active_material_index current_idx = obj.active_material_index
prev_index = current_idx - 1 prev_index = current_idx - 1
logger.debug(f"Moving material {current_idx} up to position {prev_index} for object {obj.name}")
try: try:
FnMaterial.swap_materials(obj, current_idx, prev_index, reverse=True, swap_slots=True) FnMaterial.swap_materials(obj, current_idx, prev_index, reverse=True, swap_slots=True)
except MaterialNotFoundError: except MaterialNotFoundError:
logger.error(f"Materials not found for indices {current_idx} and {prev_index}")
self.report({"ERROR"}, "Materials not found") self.report({"ERROR"}, "Materials not found")
return {"CANCELLED"} return {"CANCELLED"}
obj.active_material_index = prev_index obj.active_material_index = prev_index
@@ -259,21 +313,17 @@ class MoveMaterialDown(Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context):
obj = context.active_object obj = context.active_object
valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE" return obj is not None and obj.type == "MESH" and obj.mmd_type == "NONE" and obj.active_material_index < len(obj.material_slots) - 1
return bool(valid_mesh and obj.active_material_index < len(obj.material_slots) - 1)
def execute(self, context: Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
current_idx = obj.active_material_index current_idx = obj.active_material_index
next_index = current_idx + 1 next_index = current_idx + 1
logger.debug(f"Moving material {current_idx} down to position {next_index} for object {obj.name}")
try: try:
FnMaterial.swap_materials(obj, current_idx, next_index, reverse=True, swap_slots=True) FnMaterial.swap_materials(obj, current_idx, next_index, reverse=True, swap_slots=True)
except MaterialNotFoundError: except MaterialNotFoundError:
logger.error(f"Materials not found for indices {current_idx} and {next_index}")
self.report({"ERROR"}, "Materials not found") self.report({"ERROR"}, "Materials not found")
return {"CANCELLED"} return {"CANCELLED"}
obj.active_material_index = next_index obj.active_material_index = next_index
@@ -296,31 +346,26 @@ class EdgePreviewSetup(Operator):
default="CREATE", default="CREATE",
) )
def execute(self, context: Context) -> Set[str]: def execute(self, context):
from ..core.model import FnModel from ..core.model import FnModel
root = FnModel.find_root_object(context.active_object) root = FnModel.find_root_object(context.active_object)
if root is None: if root is None:
logger.error("No MMD model root found")
self.report({"ERROR"}, "Select a MMD model") self.report({"ERROR"}, "Select a MMD model")
return {"CANCELLED"} return {"CANCELLED"}
if self.action == "CLEAN": if self.action == "CLEAN":
logger.info(f"Cleaning toon edge for model: {root.name}")
for obj in FnModel.iterate_mesh_objects(root): for obj in FnModel.iterate_mesh_objects(root):
self.__clean_toon_edge(obj) self.__clean_toon_edge(obj)
else: else:
from ..bpyutils import Props from ..bpyutils import Props
logger.info(f"Creating toon edge for model: {root.name}")
scale = 0.2 * getattr(root, Props.empty_display_size) scale = 0.2 * getattr(root, Props.empty_display_size)
counts = sum(self.__create_toon_edge(obj, scale) for obj in FnModel.iterate_mesh_objects(root)) counts = sum(self.__create_toon_edge(obj, scale) for obj in FnModel.iterate_mesh_objects(root))
logger.info(f"Created {counts} toon edge(s)")
self.report({"INFO"}, "Created %d toon edge(s)" % counts) self.report({"INFO"}, "Created %d toon edge(s)" % counts)
return {"FINISHED"} return {"FINISHED"}
def __clean_toon_edge(self, obj: Object) -> None: def __clean_toon_edge(self, obj):
logger.debug(f"Cleaning toon edge for object: {obj.name}")
if "mmd_edge_preview" in obj.modifiers: if "mmd_edge_preview" in obj.modifiers:
obj.modifiers.remove(obj.modifiers["mmd_edge_preview"]) obj.modifiers.remove(obj.modifiers["mmd_edge_preview"])
@@ -329,8 +374,7 @@ class EdgePreviewSetup(Operator):
FnMaterial.clean_materials(obj, can_remove=lambda m: m and m.name.startswith("mmd_edge.")) FnMaterial.clean_materials(obj, can_remove=lambda m: m and m.name.startswith("mmd_edge."))
def __create_toon_edge(self, obj: Object, scale: float = 1.0) -> int: def __create_toon_edge(self, obj, scale=1.0):
logger.debug(f"Creating toon edge for object: {obj.name} with scale {scale}")
self.__clean_toon_edge(obj) self.__clean_toon_edge(obj)
materials = obj.data.materials materials = obj.data.materials
material_offset = len(materials) material_offset = len(materials)
@@ -355,10 +399,10 @@ class EdgePreviewSetup(Operator):
mod.vertex_group = "mmd_edge_preview" mod.vertex_group = "mmd_edge_preview"
return len(materials) - material_offset return len(materials) - material_offset
def __create_edge_preview_group(self, obj: Object) -> None: def __create_edge_preview_group(self, obj):
vertices, materials = obj.data.vertices, obj.data.materials vertices, materials = obj.data.vertices, obj.data.materials
weight_map = {i: m.mmd_material.edge_weight for i, m in enumerate(materials) if m} weight_map = {i: m.mmd_material.edge_weight for i, m in enumerate(materials) if m}
scale_map: Dict[int, float] = {} scale_map = {}
vg_scale_index = obj.vertex_groups.find("mmd_edge_scale") vg_scale_index = obj.vertex_groups.find("mmd_edge_scale")
if vg_scale_index >= 0: if vg_scale_index >= 0:
scale_map = {v.index: g.weight for v in vertices for g in v.groups if g.group == vg_scale_index} scale_map = {v.index: g.weight for v in vertices for g in v.groups if g.group == vg_scale_index}
@@ -367,7 +411,7 @@ class EdgePreviewSetup(Operator):
weight = scale_map.get(i, 1.0) * weight_map.get(mi, 1.0) * 0.02 weight = scale_map.get(i, 1.0) * weight_map.get(mi, 1.0) * 0.02
vg_edge_preview.add(index=[i], weight=weight, type="REPLACE") vg_edge_preview.add(index=[i], weight=weight, type="REPLACE")
def __get_edge_material(self, mat_name: str, edge_color: Tuple[float, float, float, float], materials: List[Material]) -> Material: def __get_edge_material(self, mat_name, edge_color, materials):
if mat_name in materials: if mat_name in materials:
return materials[mat_name] return materials[mat_name]
mat = bpy.data.materials.get(mat_name, None) mat = bpy.data.materials.get(mat_name, None)
@@ -385,7 +429,7 @@ class EdgePreviewSetup(Operator):
self.__make_shader(mat) self.__make_shader(mat)
return mat return mat
def __make_shader(self, m: Material) -> None: def __make_shader(self, m):
m.use_nodes = True m.use_nodes = True
nodes, links = m.node_tree.nodes, m.node_tree.links nodes, links = m.node_tree.nodes, m.node_tree.links
@@ -406,7 +450,7 @@ class EdgePreviewSetup(Operator):
node_shader.inputs["Color"].default_value = m.mmd_material.edge_color node_shader.inputs["Color"].default_value = m.mmd_material.edge_color
node_shader.inputs["Alpha"].default_value = m.mmd_material.edge_color[3] node_shader.inputs["Alpha"].default_value = m.mmd_material.edge_color[3]
def __get_edge_preview_shader(self) -> bpy.types.NodeTree: def __get_edge_preview_shader(self):
group_name = "MMDEdgePreview" group_name = "MMDEdgePreview"
shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree") shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
if len(shader.nodes): if len(shader.nodes):
@@ -414,8 +458,8 @@ class EdgePreviewSetup(Operator):
ng = _NodeGroupUtils(shader) ng = _NodeGroupUtils(shader)
node_input = ng.new_node("NodeGroupInput", (-5, 0)) ng.new_node("NodeGroupInput", (-5, 0))
node_output = ng.new_node("NodeGroupOutput", (3, 0)) ng.new_node("NodeGroupOutput", (3, 0))
############################################################################ ############################################################################
node_color = ng.new_node("ShaderNodeMixRGB", (-1, -1.5)) node_color = ng.new_node("ShaderNodeMixRGB", (-1, -1.5))
+47 -68
View File
@@ -1,22 +1,15 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors # Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender # This file is part of MMD Tools.
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import re import re
from typing import List, Dict, Any, Set, Optional, Tuple, Union, Type
import bpy import bpy
from bpy.types import Context, Object, Operator, ShapeKey
from .. import utils from .. import utils
from ..bpyutils import FnContext, FnObject from ..bpyutils import FnContext, FnObject
from ..core.bone import FnBone from ..core.bone import FnBone
from ..core.model import FnModel, Model from ..core.model import FnModel, Model
from ..core.morph import FnMorph from ..core.morph import FnMorph
from ....core.logging_setup import logger
class SelectObject(bpy.types.Operator): class SelectObject(bpy.types.Operator):
@@ -32,8 +25,7 @@ class SelectObject(bpy.types.Operator):
options={"HIDDEN", "SKIP_SAVE"}, options={"HIDDEN", "SKIP_SAVE"},
) )
def execute(self, context: Context) -> Set[str]: def execute(self, context):
logger.debug(f"Selecting object: {self.name}")
utils.selectAObject(context.scene.objects[self.name]) utils.selectAObject(context.scene.objects[self.name])
return {"FINISHED"} return {"FINISHED"}
@@ -47,43 +39,41 @@ class MoveObject(bpy.types.Operator, utils.ItemMoveOp):
__PREFIX_REGEXP = re.compile(r"(?P<prefix>[0-9A-Z]{3}_)(?P<name>.*)") __PREFIX_REGEXP = re.compile(r"(?P<prefix>[0-9A-Z]{3}_)(?P<name>.*)")
@classmethod @classmethod
def set_index(cls, obj: Object, index: int) -> None: def set_index(cls, obj, index):
m = cls.__PREFIX_REGEXP.match(obj.name) m = cls.__PREFIX_REGEXP.match(obj.name)
name = m.group("name") if m else obj.name name = m.group("name") if m else obj.name
obj.name = "%s_%s" % (utils.int2base(index, 36, 3), name) obj.name = f"{utils.int2base(index, 36, 3)}_{name}"
@classmethod @classmethod
def get_name(cls, obj: Object, prefix: Optional[str] = None) -> str: def get_name(cls, obj, prefix=None):
m = cls.__PREFIX_REGEXP.match(obj.name) m = cls.__PREFIX_REGEXP.match(obj.name)
name = m.group("name") if m else obj.name name = m.group("name") if m else obj.name
return name[len(prefix) :] if prefix and name.startswith(prefix) else name return name[len(prefix) :] if prefix and name.startswith(prefix) else name
@classmethod @classmethod
def normalize_indices(cls, objects: List[Object]) -> None: def normalize_indices(cls, objects):
for i, x in enumerate(objects): for i, x in enumerate(objects):
cls.set_index(x, i) cls.set_index(x, i)
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context):
return context.active_object is not None return context.active_object is not None
def execute(self, context: Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
objects = self.__get_objects(obj) objects = self.__get_objects(obj)
if obj not in objects: if obj not in objects:
logger.error(f'Cannot move object "{obj.name}"')
self.report({"ERROR"}, f'Can not move object "{obj.name}"') self.report({"ERROR"}, f'Can not move object "{obj.name}"')
return {"CANCELLED"} return {"CANCELLED"}
objects.sort(key=lambda x: x.name) objects.sort(key=lambda x: x.name)
logger.debug(f"Moving object {obj.name} {self.type}")
self.move(objects, objects.index(obj), self.type) self.move(objects, objects.index(obj), self.type)
self.normalize_indices(objects) self.normalize_indices(objects)
return {"FINISHED"} return {"FINISHED"}
def __get_objects(self, obj: Object) -> Any: def __get_objects(self, obj):
class __MovableList(list): class __MovableList(list):
def move(self, index_old: int, index_new: int) -> None: def move(self, index_old, index_new):
item = self[index_old] item = self[index_old]
self.remove(item) self.remove(item)
self.insert(index_new, item) self.insert(index_new, item)
@@ -108,43 +98,40 @@ class CleanShapeKeys(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context):
return any(o.type == "MESH" for o in context.selected_objects) return any(o.type == "MESH" for o in context.selected_objects)
@staticmethod @staticmethod
def __can_remove(key_block: ShapeKey) -> bool: def __can_remove(key_block):
if key_block.relative_key == key_block: if key_block.relative_key == key_block:
return False # Basis return False # Basis
for v0, v1 in zip(key_block.relative_key.data, key_block.data): for v0, v1 in zip(key_block.relative_key.data, key_block.data, strict=False):
if v0.co != v1.co: if v0.co != v1.co:
return False return False
return True return True
def __shape_key_clean(self, obj: Object, key_blocks: List[ShapeKey]) -> None: def __shape_key_clean(self, obj, key_blocks):
for kb in key_blocks: for kb in key_blocks:
if self.__can_remove(kb): if self.__can_remove(kb):
logger.debug(f"Removing unused shape key: {kb.name} from {obj.name}")
FnObject.mesh_remove_shape_key(obj, kb) FnObject.mesh_remove_shape_key(obj, kb)
if len(key_blocks) == 1: if len(key_blocks) == 1:
logger.debug(f"Removing single shape key: {key_blocks[0].name} from {obj.name}")
FnObject.mesh_remove_shape_key(obj, key_blocks[0]) FnObject.mesh_remove_shape_key(obj, key_blocks[0])
def execute(self, context: Context) -> Set[str]: def execute(self, context):
logger.info("Cleaning shape keys for selected objects") obj: bpy.types.Object
obj: Object
for obj in context.selected_objects: for obj in context.selected_objects:
if obj.type != "MESH" or obj.data.shape_keys is None: if obj.type != "MESH" or obj.data.shape_keys is None:
continue continue
if not obj.data.shape_keys.use_relative: if not obj.data.shape_keys.use_relative:
continue # not be considered yet continue # not be considered yet
logger.debug(f"Processing shape keys for {obj.name}")
self.__shape_key_clean(obj, obj.data.shape_keys.key_blocks) self.__shape_key_clean(obj, obj.data.shape_keys.key_blocks)
return {"FINISHED"} return {"FINISHED"}
class SeparateByMaterials(bpy.types.Operator): class SeparateByMaterials(bpy.types.Operator):
bl_idname = "mmd_tools.separate_by_materials" bl_idname = "mmd_tools.separate_by_materials"
bl_label = "Separate By Materials" bl_label = "Sep by Mat(High Risk)"
bl_description = "Separate by Materials (High Risk)\nSeparate the mesh into multiple objects based on materials.\nHIGH RISK & BUGGY: This operation is not reversible and may cause various issues. It splits adjacent geometry by material, and merging later will not reconnect shared edges.\nKnown issues include potential mesh corruption, UV mapping problems, and other unpredictable behaviors. Use with extreme caution and backup your work first."
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
clean_shape_keys: bpy.props.BoolProperty( clean_shape_keys: bpy.props.BoolProperty(
@@ -153,26 +140,32 @@ class SeparateByMaterials(bpy.types.Operator):
default=True, default=True,
) )
@classmethod keep_normals: bpy.props.BoolProperty(
def poll(cls, context: Context) -> bool: name="Keep Normals",
obj = context.active_object default=True,
return obj and obj.type == "MESH" )
def __separate_by_materials(self, obj: Object) -> None: @classmethod
logger.info(f"Separating {obj.name} by materials") def poll(cls, context):
utils.separateByMaterials(obj) obj = context.active_object
return obj is not None and obj.type == "MESH"
def __separate_by_materials(self, obj):
utils.separateByMaterials(obj, self.keep_normals)
if self.clean_shape_keys: if self.clean_shape_keys:
logger.debug("Cleaning shape keys after separation")
bpy.ops.mmd_tools.clean_shape_keys() bpy.ops.mmd_tools.clean_shape_keys()
def execute(self, context: Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
# Sep by Mat crashes Blender if used after morph assembly
rig = Model(root)
rig.morph_slider.unbind()
if root is None: if root is None:
logger.debug("No root object found, separating single object")
self.__separate_by_materials(obj) self.__separate_by_materials(obj)
else: else:
logger.debug(f"Root object found: {root.name}, preparing for separation")
bpy.ops.mmd_tools.clear_temp_materials() bpy.ops.mmd_tools.clear_temp_materials()
bpy.ops.mmd_tools.clear_uv_morph_view() bpy.ops.mmd_tools.clear_uv_morph_view()
@@ -185,11 +178,9 @@ class SeparateByMaterials(bpy.types.Operator):
if len(mesh.data.materials) > 0: if len(mesh.data.materials) > 0:
mat = mesh.data.materials[0] mat = mesh.data.materials[0]
idx = mat_names.index(getattr(mat, "name", None)) idx = mat_names.index(getattr(mat, "name", None))
logger.debug(f"Setting index {idx} for mesh {mesh.name}")
MoveObject.set_index(mesh, idx) MoveObject.set_index(mesh, idx)
for morph in root.mmd_root.material_morphs: for morph in root.mmd_root.material_morphs:
logger.debug(f"Updating material morph: {morph.name}")
FnMorph(morph, rig).update_mat_related_mesh() FnMorph(morph, rig).update_mat_related_mesh()
utils.clearUnusedMeshes() utils.clearUnusedMeshes()
return {"FINISHED"} return {"FINISHED"}
@@ -207,15 +198,13 @@ class JoinMeshes(bpy.types.Operator):
default=True, default=True,
) )
def execute(self, context: Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
if root is None: if root is None:
logger.error("No MMD model found")
self.report({"ERROR"}, "Select a MMD model") self.report({"ERROR"}, "Select a MMD model")
return {"CANCELLED"} return {"CANCELLED"}
logger.info(f"Joining meshes for model: {root.name}")
bpy.ops.mmd_tools.clear_temp_materials() bpy.ops.mmd_tools.clear_temp_materials()
bpy.ops.mmd_tools.clear_uv_morph_view() bpy.ops.mmd_tools.clear_uv_morph_view()
@@ -223,11 +212,9 @@ class JoinMeshes(bpy.types.Operator):
rig = Model(root) rig = Model(root)
meshes_list = sorted(rig.meshes(), key=lambda x: x.name) meshes_list = sorted(rig.meshes(), key=lambda x: x.name)
if not meshes_list: if not meshes_list:
logger.error("No meshes found in the model")
self.report({"ERROR"}, "The model does not have any meshes") self.report({"ERROR"}, "The model does not have any meshes")
return {"CANCELLED"} return {"CANCELLED"}
active_mesh = meshes_list[0] active_mesh = meshes_list[0]
logger.debug(f"Found {len(meshes_list)} meshes, using {active_mesh.name} as active")
FnContext.select_objects(context, *meshes_list) FnContext.select_objects(context, *meshes_list)
FnContext.set_active_object(context, active_mesh) FnContext.set_active_object(context, active_mesh)
@@ -236,19 +223,15 @@ class JoinMeshes(bpy.types.Operator):
for m in meshes_list[1:]: for m in meshes_list[1:]:
for mat in m.data.materials: for mat in m.data.materials:
if mat not in active_mesh.data.materials[:]: if mat not in active_mesh.data.materials[:]:
logger.debug(f"Adding material {mat.name} to active mesh")
active_mesh.data.materials.append(mat) active_mesh.data.materials.append(mat)
# Join selected meshes # Join selected meshes
logger.debug("Joining meshes")
bpy.ops.object.join() bpy.ops.object.join()
if self.sort_shape_keys: if self.sort_shape_keys:
logger.debug("Sorting shape keys")
FnMorph.fixShapeKeyOrder(active_mesh, root.mmd_root.vertex_morphs.keys()) FnMorph.fixShapeKeyOrder(active_mesh, root.mmd_root.vertex_morphs.keys())
active_mesh.active_shape_key_index = 0 active_mesh.active_shape_key_index = 0
for morph in root.mmd_root.material_morphs: for morph in root.mmd_root.material_morphs:
logger.debug(f"Updating material morph: {morph.name}")
FnMorph(morph, rig).update_mat_related_mesh(active_mesh) FnMorph(morph, rig).update_mat_related_mesh(active_mesh)
utils.clearUnusedMeshes() utils.clearUnusedMeshes()
return {"FINISHED"} return {"FINISHED"}
@@ -262,20 +245,17 @@ class AttachMeshesToMMD(bpy.types.Operator):
add_armature_modifier: bpy.props.BoolProperty(default=True) add_armature_modifier: bpy.props.BoolProperty(default=True)
def execute(self, context: Context) -> Set[str]: def execute(self, context: bpy.types.Context):
root = FnModel.find_root_object(context.active_object) root = FnModel.find_root_object(context.active_object)
if root is None: if root is None:
logger.error("No MMD model found")
self.report({"ERROR"}, "Select a MMD model") self.report({"ERROR"}, "Select a MMD model")
return {"CANCELLED"} return {"CANCELLED"}
armObj = FnModel.find_armature_object(root) armObj = FnModel.find_armature_object(root)
if armObj is None: if armObj is None:
logger.error("Model armature not found")
self.report({"ERROR"}, "Model Armature not found") self.report({"ERROR"}, "Model Armature not found")
return {"CANCELLED"} return {"CANCELLED"}
logger.info(f"Attaching meshes to model: {root.name}")
FnModel.attach_mesh_objects(root, context.visible_objects, self.add_armature_modifier) FnModel.attach_mesh_objects(root, context.visible_objects, self.add_armature_modifier)
return {"FINISHED"} return {"FINISHED"}
@@ -295,18 +275,18 @@ class ChangeMMDIKLoopFactor(bpy.types.Operator):
) )
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context):
return FnModel.find_root_object(context.active_object) is not None root = FnModel.find_root_object(context.active_object)
return root is not None
def invoke(self, context: Context, event: Any) -> Set[str]: def invoke(self, context, event):
root_object = FnModel.find_root_object(context.active_object) root_object = FnModel.find_root_object(context.active_object)
self.mmd_ik_loop_factor = root_object.mmd_root.ik_loop_factor self.mmd_ik_loop_factor = root_object.mmd_root.ik_loop_factor
vm = context.window_manager vm = context.window_manager
return vm.invoke_props_dialog(self) return vm.invoke_props_dialog(self)
def execute(self, context: Context) -> Set[str]: def execute(self, context):
root_object = FnModel.find_root_object(context.active_object) root_object = FnModel.find_root_object(context.active_object)
logger.info(f"Changing IK loop factor to {self.mmd_ik_loop_factor} for model: {root_object.name}")
FnModel.change_mmd_ik_loop_factor(root_object, self.mmd_ik_loop_factor) FnModel.change_mmd_ik_loop_factor(root_object, self.mmd_ik_loop_factor)
return {"FINISHED"} return {"FINISHED"}
@@ -318,22 +298,21 @@ class RecalculateBoneRoll(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context):
obj = context.active_object obj = context.active_object
return obj and obj.type == "ARMATURE" return obj is not None and obj.type == "ARMATURE"
def invoke(self, context: Context, event: Any) -> Set[str]: def invoke(self, context, event):
vm = context.window_manager vm = context.window_manager
return vm.invoke_props_dialog(self) return vm.invoke_props_dialog(self)
def draw(self, context: Context) -> None: def draw(self, context):
layout = self.layout layout = self.layout
c = layout.column() c = layout.column()
c.label(text="This operation will break existing f-curve/action.", icon="QUESTION") c.label(text="This operation will break existing f-curve/action.", icon="QUESTION")
c.label(text="Click [OK] to run the operation.") c.label(text="Click [OK] to run the operation.")
def execute(self, context: Context) -> Set[str]: def execute(self, context):
arm = context.active_object arm = context.active_object
logger.info(f"Recalculating bone roll for armature: {arm.name}")
FnBone.apply_auto_bone_roll(arm) FnBone.apply_auto_bone_roll(arm)
return {"FINISHED"} return {"FINISHED"}
+232 -131
View File
@@ -1,32 +1,27 @@
# -*- coding: utf-8 -*- # Copyright 2022 MMD Tools authors
# Copyright 2014 MMD Tools authors # This file is part of MMD Tools.
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import itertools import itertools
from operator import itemgetter from operator import itemgetter
from typing import Dict, List, Optional, Set, Tuple, Any from typing import Dict, List, Optional, Set
import bmesh import bmesh
import bpy import bpy
import numpy as np import numpy as np
import numpy.typing as npt from mathutils import Matrix
from bpy.types import Context, Object, Operator, EditBone, Mesh, Armature
from ..bpyutils import FnContext from ..bpyutils import FnContext, select_object
from ..core.model import FnModel, Model from ..core.model import FnModel, Model
from ....core.logging_setup import logger
class MessageException(Exception): class NoModelSelectedError(Exception):
"""Class for error with message.""" """Raised when no MMD model is selected."""
class ModelJoinByBonesOperator(bpy.types.Operator): class ModelJoinByBonesOperator(bpy.types.Operator):
bl_idname = "mmd_tools.model_join_by_bones" bl_idname = "mmd_tools.model_join_by_bones"
bl_label = "Model Join by Bones" bl_label = "Model Join by Bones"
bl_description = "Join multiple MMD models into one.\n\nWARNING: To align models before joining, only adjust the root (cross under the model) transformation. Do not move armatures, meshes, rigid bodies, or joints directly as they will not move together.\n\nIMPORTANT: Don't use any of the 'Assembly' functions before using this function. This function requires the models to be in a clean state."
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
join_type: bpy.props.EnumProperty( join_type: bpy.props.EnumProperty(
@@ -39,8 +34,8 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
) )
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context: bpy.types.Context):
active_object: Optional[Object] = context.active_object active_object: Optional[bpy.types.Object] = context.active_object
if context.mode != "POSE": if context.mode != "POSE":
return False return False
@@ -56,22 +51,19 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
return len(context.selected_pose_bones) > 0 return len(context.selected_pose_bones) > 0
def invoke(self, context: Context, event: Any) -> Set[str]: def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self) return context.window_manager.invoke_props_dialog(self)
def execute(self, context: Context) -> Set[str]: def execute(self, context: bpy.types.Context):
try: try:
logger.info("Starting model join by bones operation")
self.join(context) self.join(context)
logger.info("Model join by bones completed successfully") except NoModelSelectedError as ex:
except MessageException as ex:
logger.error(f"Model join by bones failed: {str(ex)}")
self.report(type={"ERROR"}, message=str(ex)) self.report(type={"ERROR"}, message=str(ex))
return {"CANCELLED"} return {"CANCELLED"}
return {"FINISHED"} return {"FINISHED"}
def join(self, context: Context) -> None: def join(self, context: bpy.types.Context):
bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="OBJECT")
parent_root_object = FnModel.find_root_object(context.active_object) parent_root_object = FnModel.find_root_object(context.active_object)
@@ -79,23 +71,35 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
child_root_objects.remove(parent_root_object) child_root_objects.remove(parent_root_object)
if parent_root_object is None or len(child_root_objects) == 0: if parent_root_object is None or len(child_root_objects) == 0:
raise MessageException("No MMD Models selected") raise NoModelSelectedError("No MMD Models selected")
logger.debug(f"Joining {len(child_root_objects)} models into parent model: {parent_root_object.name}") # Save original active_layer_collection
with FnContext.temp_override_active_layer_collection(context, parent_root_object): orig_active_layer_collection = context.view_layer.active_layer_collection
# Find layer collection containing parent_root_object and set it as active
layer_collection = FnContext.find_user_layer_collection_by_object(context, parent_root_object)
if layer_collection:
context.view_layer.active_layer_collection = layer_collection
# Execute the join operation
FnModel.join_models(parent_root_object, child_root_objects) FnModel.join_models(parent_root_object, child_root_objects)
# Restore original active_layer_collection
context.view_layer.active_layer_collection = orig_active_layer_collection
bpy.ops.object.mode_set(mode="OBJECT")
parent_armature_object = FnModel.find_armature_object(parent_root_object)
FnContext.set_active_and_select_single_object(context, parent_armature_object)
bpy.ops.object.mode_set(mode="EDIT") bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.armature.parent_set(type="OFFSET") bpy.ops.armature.parent_set(type="OFFSET")
# Connect child bones # Connect child bones
if self.join_type == "CONNECTED": if self.join_type == "CONNECTED":
parent_edit_bone: EditBone = context.active_bone parent_edit_bone: bpy.types.EditBone = context.active_bone
child_edit_bones: Set[EditBone] = set(context.selected_bones) child_edit_bones: Set[bpy.types.EditBone] = set(context.selected_bones)
child_edit_bones.remove(parent_edit_bone) child_edit_bones.remove(parent_edit_bone)
logger.debug(f"Connecting {len(child_edit_bones)} child bones to parent bone: {parent_edit_bone.name}") child_edit_bone: bpy.types.EditBone
child_edit_bone: EditBone
for child_edit_bone in child_edit_bones: for child_edit_bone in child_edit_bones:
child_edit_bone.use_connect = True child_edit_bone.use_connect = True
@@ -105,6 +109,7 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
class ModelSeparateByBonesOperator(bpy.types.Operator): class ModelSeparateByBonesOperator(bpy.types.Operator):
bl_idname = "mmd_tools.model_separate_by_bones" bl_idname = "mmd_tools.model_separate_by_bones"
bl_label = "Model Separate by Bones" bl_label = "Model Separate by Bones"
bl_description = "Separate MMD model into multiple models based on selected bones.\n\nWARNING: This operation will split meshes, armatures, rigid bodies and joints. To move models before separating, only adjust the root (cross under the model) transformation. Do not move armatures, meshes, rigid bodies, or joints directly before separating as they will not move together.\n\nIMPORTANT: Don't use any of the 'Assembly' functions before using this function. This function requires the model to be in a clean state."
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
separate_armature: bpy.props.BoolProperty(name="Separate Armature", default=True) separate_armature: bpy.props.BoolProperty(name="Separate Armature", default=True)
@@ -120,8 +125,8 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
) )
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context: bpy.types.Context):
active_object: Optional[Object] = context.active_object active_object: Optional[bpy.types.Object] = context.active_object
if context.mode != "POSE": if context.mode != "POSE":
return False return False
@@ -137,155 +142,183 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
return len(context.selected_pose_bones) > 0 return len(context.selected_pose_bones) > 0
def invoke(self, context: Context, event: Any) -> Set[str]: def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self) return context.window_manager.invoke_props_dialog(self)
def execute(self, context: Context) -> Set[str]: def execute(self, context: bpy.types.Context):
try: try:
logger.info("Starting model separate by bones operation")
self.separate(context) self.separate(context)
logger.info("Model separate by bones completed successfully") except NoModelSelectedError as ex:
except MessageException as ex:
logger.error(f"Model separate by bones failed: {str(ex)}")
self.report(type={"ERROR"}, message=str(ex)) self.report(type={"ERROR"}, message=str(ex))
return {"CANCELLED"} return {"CANCELLED"}
return {"FINISHED"} return {"FINISHED"}
def separate(self, context: Context) -> None: def separate(self, context: bpy.types.Context):
weight_threshold: float = self.weight_threshold weight_threshold: float = self.weight_threshold
mmd_scale = 0.08 mmd_scale = 0.08
target_armature_object: Object = context.active_object target_armature_object: bpy.types.Object = context.active_object
logger.debug(f"Target armature: {target_armature_object.name}")
bpy.ops.object.mode_set(mode="EDIT") bpy.ops.object.mode_set(mode="EDIT")
root_bones: Set[EditBone] = set(context.selected_bones) root_bones: Set[bpy.types.EditBone] = set(context.selected_bones)
logger.debug(f"Selected root bones: {len(root_bones)}")
if self.include_descendant_bones: if self.include_descendant_bones:
logger.debug("Including descendant bones") original_active_bone = context.active_bone
for edit_bone in root_bones: for edit_bone in root_bones:
with context.temp_override(active_bone=edit_bone): context.active_object.data.edit_bones.active = edit_bone
bpy.ops.armature.select_similar(type="CHILDREN", threshold=0.1) bpy.ops.armature.select_similar(type="CHILDREN", threshold=0.1)
self._select_related_ik_bones(target_armature_object)
if original_active_bone:
context.active_object.data.edit_bones.active = original_active_bone
separate_bones: Dict[str, EditBone] = {b.name: b for b in context.selected_bones} separate_bones: Dict[str, bpy.types.EditBone] = {b.name: b for b in context.selected_bones}
deform_bones: Dict[str, EditBone] = {b.name: b for b in target_armature_object.data.edit_bones if b.use_deform} deform_bones: Dict[str, bpy.types.EditBone] = {b.name: b for b in target_armature_object.data.edit_bones if b.use_deform}
logger.debug(f"Total bones to separate: {len(separate_bones)}") mmd_root_object: bpy.types.Object = FnModel.find_root_object(context.active_object)
mmd_root_object: Object = FnModel.find_root_object(context.active_object)
mmd_model = Model(mmd_root_object) mmd_model = Model(mmd_root_object)
mmd_model_mesh_objects: List[Object] = list(mmd_model.meshes()) mmd_model_mesh_objects: List[bpy.types.Object] = list(mmd_model.meshes())
logger.debug(f"Found {len(mmd_model_mesh_objects)} mesh objects in model") mmd_model_mesh_objects = list(self._select_weighted_vertices(mmd_model_mesh_objects, separate_bones, deform_bones, weight_threshold).keys())
mesh_selection_result = self.select_weighted_vertices(mmd_model_mesh_objects, separate_bones, deform_bones, weight_threshold)
mmd_model_mesh_objects = list(mesh_selection_result.keys())
logger.debug(f"Selected {len(mmd_model_mesh_objects)} mesh objects with weighted vertices")
# separate armature bones
separate_armature_object: Optional[Object]
if self.separate_armature:
logger.debug("Separating armature")
target_armature_object.select_set(True)
bpy.ops.armature.separate()
separate_armature_object = next(iter([a for a in context.selected_objects if a != target_armature_object]), None)
if separate_armature_object:
logger.debug(f"Created separate armature: {separate_armature_object.name}")
bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="OBJECT")
# collect separate rigid bodies # Store original transform matrix for root object
separate_rigid_bodies: Set[Object] = {rigid_body_object for rigid_body_object in mmd_model.rigidBodies() if rigid_body_object.mmd_rigid.bone in separate_bones} original_matrix_world = mmd_root_object.matrix_world.copy()
logger.debug(f"Found {len(separate_rigid_bodies)} rigid bodies to separate") mmd_root_object.matrix_world = Matrix.Identity(4)
# Reset object visibility
FnContext.set_active_and_select_single_object(context, mmd_root_object)
bpy.ops.mmd_tools.reset_object_visibility()
# Clean additional transform
FnContext.set_active_and_select_single_object(context, mmd_root_object)
bpy.ops.mmd_tools.clean_additional_transform()
# Create new separate model first
separate_model: Model = Model.create(mmd_root_object.mmd_root.name, mmd_root_object.mmd_root.name_e, mmd_scale, obj_name=mmd_root_object.name, add_root_bone=False)
separate_model.initialDisplayFrames()
separate_root_object = separate_model.rootObject()
separate_root_object.matrix_world = mmd_root_object.matrix_world
separate_model_armature_object = separate_model.armature()
# Now separate armature bones from original model
separate_armature_object: Optional[bpy.types.Object] = None
if self.separate_armature:
FnContext.set_active_and_select_single_object(context, target_armature_object)
bpy.ops.object.mode_set(mode="EDIT")
# Re-select the bones that should be separated (they might have been deselected)
for bone_name in separate_bones.keys():
if bone_name in target_armature_object.data.edit_bones:
target_armature_object.data.edit_bones[bone_name].select = True
bpy.ops.armature.separate()
separate_armature_object = next(iter([a for a in context.selected_objects if a != target_armature_object and a.type == "ARMATURE"]), None)
bpy.ops.object.mode_set(mode="OBJECT")
# Collect separate rigid bodies
separate_rigid_bodies: Set[bpy.types.Object] = {rigid_body_object for rigid_body_object in mmd_model.rigidBodies() if rigid_body_object.mmd_rigid.bone in separate_bones}
boundary_joint_owner_condition = any if self.boundary_joint_owner == "DESTINATION" else all boundary_joint_owner_condition = any if self.boundary_joint_owner == "DESTINATION" else all
# collect separate joints # Collect separate joints
separate_joints: Set[Object] = { separate_joints: Set[bpy.types.Object] = {
joint_object joint_object
for joint_object in mmd_model.joints() for joint_object in mmd_model.joints()
if boundary_joint_owner_condition( if boundary_joint_owner_condition(
[ [
joint_object.rigid_body_constraint.object1 in separate_rigid_bodies, joint_object.rigid_body_constraint.object1 in separate_rigid_bodies,
joint_object.rigid_body_constraint.object2 in separate_rigid_bodies, joint_object.rigid_body_constraint.object2 in separate_rigid_bodies,
] ],
) )
} }
logger.debug(f"Found {len(separate_joints)} joints to separate")
separate_mesh_objects: Set[Object] separate_mesh_objects: List[bpy.types.Object] = []
model2separate_mesh_objects: Dict[Object, Object] model2separate_mesh_objects: Dict[bpy.types.Object, bpy.types.Object] = {}
if len(mmd_model_mesh_objects) == 0: if len(mmd_model_mesh_objects) > 0:
logger.debug("No mesh objects to separate") # Find a single unique attribute name that doesn't conflict with any existing attributes.
separate_mesh_objects = set() all_attribute_names = {attr.name for obj in mmd_model_mesh_objects for attr in obj.data.attributes}
model2separate_mesh_objects = dict() temp_normal_name = "mmd_temp_normal"
i = 0
while temp_normal_name in all_attribute_names:
temp_normal_name = f"mmd_temp_normal.{i:03d}"
i += 1
# Backup custom normals to the unique temporary attribute.
for mesh_obj in mmd_model_mesh_objects:
mesh_data = mesh_obj.data
existing_custom_normal = mesh_data.attributes.get("custom_normal")
if not existing_custom_normal:
continue
if existing_custom_normal.data_type == "INT16_2D":
normals_data = np.empty(len(mesh_data.loops) * 2, dtype=np.int16)
existing_custom_normal.data.foreach_get("value", normals_data)
temp_normal_attr = mesh_data.attributes.new(temp_normal_name, "INT16_2D", "CORNER")
temp_normal_attr.data.foreach_set("value", normals_data)
else: else:
# select meshes raise TypeError(f"Unsupported custom_normal data type: '{existing_custom_normal.data_type}'. Supported types: 'INT16_2D'")
logger.debug("Selecting meshes for separation")
obj: Object # Select meshes
obj: bpy.types.Object
for obj in context.view_layer.objects: for obj in context.view_layer.objects:
obj.select_set(obj in mmd_model_mesh_objects) obj.select_set(obj in mmd_model_mesh_objects)
context.view_layer.objects.active = mmd_model_mesh_objects[0] context.view_layer.objects.active = mmd_model_mesh_objects[0]
# separate mesh by selected vertices # Separate mesh by selected vertices
logger.debug("Separating meshes by selected vertices")
bpy.ops.object.mode_set(mode="EDIT") bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.separate(type="SELECTED") bpy.ops.mesh.separate(type="SELECTED")
separate_mesh_objects: List[Object] = [m for m in context.selected_objects if m.type == "MESH" and m not in mmd_model_mesh_objects] separate_mesh_objects = [m for m in context.selected_objects if m.type == "MESH" and m not in mmd_model_mesh_objects]
bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="OBJECT")
logger.debug(f"Created {len(separate_mesh_objects)} separate mesh objects")
model2separate_mesh_objects = dict(zip(mmd_model_mesh_objects, separate_mesh_objects)) model2separate_mesh_objects = dict(zip(mmd_model_mesh_objects, separate_mesh_objects, strict=False))
logger.debug(f"Creating new model with scale {mmd_scale}") # Restore normal data for all meshes (original and separated)
separate_model: Model = Model.create(mmd_root_object.mmd_root.name, mmd_root_object.mmd_root.name_e, mmd_scale, add_root_bone=False) all_mesh_objects = list(mmd_model_mesh_objects) + list(separate_mesh_objects)
for mesh_obj in all_mesh_objects:
mesh_data = mesh_obj.data
temp_normal_attr = mesh_data.attributes.get(temp_normal_name)
if not temp_normal_attr:
continue
separate_model.initialDisplayFrames() try:
separate_root_object = separate_model.rootObject() if temp_normal_attr.data_type == "INT16_2D":
separate_root_object.matrix_world = mmd_root_object.matrix_world normals_data = np.empty(len(mesh_data.loops) * 2, dtype=np.int16)
separate_model_armature_object = separate_model.armature() temp_normal_attr.data.foreach_get("value", normals_data)
logger.debug(f"Created separate model with root: {separate_root_object.name}") custom_normal_attr = mesh_data.attributes.get("custom_normal")
if not custom_normal_attr:
custom_normal_attr = mesh_data.attributes.new("custom_normal", "INT16_2D", "CORNER")
custom_normal_attr.data.foreach_set("value", normals_data)
else:
raise TypeError(f"Unsupported custom_normal data type: '{temp_normal_attr.data_type}'. Supported types: 'INT16_2D'")
finally:
mesh_data.attributes.remove(temp_normal_attr)
if self.separate_armature: if self.separate_armature and separate_armature_object:
logger.debug("Joining separate armature to new model") separate_armature_data = separate_armature_object.data
with context.temp_override( with select_object(separate_model_armature_object, objects=[separate_model_armature_object, separate_armature_object]):
active_object=separate_model_armature_object,
selected_editable_objects=[separate_model_armature_object, separate_armature_object],
):
bpy.ops.object.join() bpy.ops.object.join()
if separate_armature_data.users == 0:
bpy.data.armatures.remove(separate_armature_data)
# add mesh if separate_mesh_objects:
logger.debug("Parenting separate mesh objects to new model") with select_object(separate_model_armature_object, objects=[separate_model_armature_object] + separate_mesh_objects):
with context.temp_override(
object=separate_model_armature_object,
selected_editable_objects=[separate_model_armature_object, *separate_mesh_objects],
):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
# replace mesh armature modifier.object # Replace mesh armature modifier.object
logger.debug("Updating armature modifiers on separate meshes")
for separate_mesh in separate_mesh_objects: for separate_mesh in separate_mesh_objects:
armature_modifier: Optional[bpy.types.ArmatureModifier] = next(iter([m for m in separate_mesh.modifiers if m.type == "ARMATURE"]), None) armature_modifier: Optional[bpy.types.ArmatureModifier] = next(iter([m for m in separate_mesh.modifiers if m.type == "ARMATURE"]), None)
if armature_modifier is None: if armature_modifier is None:
logger.debug(f"Creating new armature modifier for {separate_mesh.name}") armature_modifier: bpy.types.ArmatureModifier = separate_mesh.modifiers.new("mmd_armature", "ARMATURE")
armature_modifier: bpy.types.ArmatureModifier = separate_mesh.modifiers.new("mmd_bone_order_override", "ARMATURE")
armature_modifier.object = separate_model_armature_object armature_modifier.object = separate_model_armature_object
logger.debug("Parenting rigid bodies to new model") if separate_rigid_bodies:
with context.temp_override( with select_object(separate_model.rigidGroupObject(), objects=[separate_model.rigidGroupObject()] + list(separate_rigid_bodies)):
object=separate_model.rigidGroupObject(),
selected_editable_objects=[separate_model.rigidGroupObject(), *separate_rigid_bodies],
):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
logger.debug("Parenting joints to new model") if separate_joints:
with context.temp_override( with select_object(separate_model.jointGroupObject(), objects=[separate_model.jointGroupObject()] + list(separate_joints)):
object=separate_model.jointGroupObject(),
selected_editable_objects=[separate_model.jointGroupObject(), *separate_joints],
):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
# move separate objects to new collection # Move separate objects to new collection
mmd_layer_collection = FnContext.find_user_layer_collection_by_object(context, mmd_root_object) mmd_layer_collection = FnContext.find_user_layer_collection_by_object(context, mmd_root_object)
assert mmd_layer_collection is not None assert mmd_layer_collection is not None
@@ -293,31 +326,42 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
assert separate_layer_collection is not None assert separate_layer_collection is not None
if mmd_layer_collection.name != separate_layer_collection.name: if mmd_layer_collection.name != separate_layer_collection.name:
logger.debug(f"Moving objects from collection {mmd_layer_collection.name} to {separate_layer_collection.name}")
for separate_object in itertools.chain(separate_mesh_objects, separate_rigid_bodies, separate_joints): for separate_object in itertools.chain(separate_mesh_objects, separate_rigid_bodies, separate_joints):
if separate_object.name not in separate_layer_collection.collection.objects:
separate_layer_collection.collection.objects.link(separate_object) separate_layer_collection.collection.objects.link(separate_object)
if separate_object.name in mmd_layer_collection.collection.objects:
mmd_layer_collection.collection.objects.unlink(separate_object) mmd_layer_collection.collection.objects.unlink(separate_object)
logger.debug("Copying MMD root properties")
FnModel.copy_mmd_root( FnModel.copy_mmd_root(
separate_root_object, separate_root_object,
mmd_root_object, mmd_root_object,
overwrite=True, overwrite=True,
replace_name2values={ replace_name2values={
# replace related_mesh property values # Replace related_mesh property values
"related_mesh": {m.data.name: s.data.name for m, s in model2separate_mesh_objects.items()} "related_mesh": {m.data.name: s.data.name for m, s in model2separate_mesh_objects.items()},
}, },
) )
def select_weighted_vertices(self, mmd_model_mesh_objects: List[Object], separate_bones: Dict[str, EditBone], deform_bones: Dict[str, EditBone], weight_threshold: float) -> Dict[Object, int]: # Apply additional transform
"""Select vertices weighted to the bones to be separated""" FnContext.set_active_and_select_single_object(context, mmd_root_object)
logger.debug(f"Selecting vertices weighted to {len(separate_bones)} bones with threshold {weight_threshold}") bpy.ops.mmd_tools.apply_additional_transform()
mesh2selected_vertex_count: Dict[Object, int] = dict() FnContext.set_active_and_select_single_object(context, separate_root_object)
bpy.ops.mmd_tools.apply_additional_transform()
# Restore original transform matrix for root object
mmd_root_object.matrix_world = original_matrix_world
separate_root_object.matrix_world = original_matrix_world
# End state
FnContext.set_active_and_select_single_object(context, separate_root_object)
def _select_weighted_vertices(self, mmd_model_mesh_objects: List[bpy.types.Object], separate_bones: Dict[str, bpy.types.EditBone], deform_bones: Dict[str, bpy.types.EditBone], weight_threshold: float) -> Dict[bpy.types.Object, int]:
mesh2selected_vertex_count: Dict[bpy.types.Object, int] = {}
target_bmesh: bmesh.types.BMesh = bmesh.new() target_bmesh: bmesh.types.BMesh = bmesh.new()
for mesh_object in mmd_model_mesh_objects: for mesh_object in mmd_model_mesh_objects:
vertex_groups: bpy.types.VertexGroups = mesh_object.vertex_groups vertex_groups: bpy.types.VertexGroups = mesh_object.vertex_groups
mesh: Mesh = mesh_object.data mesh: bpy.types.Mesh = mesh_object.data
target_bmesh.from_mesh(mesh, face_normals=False) target_bmesh.from_mesh(mesh, face_normals=False)
target_bmesh.select_mode |= {"VERT"} target_bmesh.select_mode |= {"VERT"}
deform_layer = target_bmesh.verts.layers.deform.verify() deform_layer = target_bmesh.verts.layers.deform.verify()
@@ -344,7 +388,6 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
vert.select_set(True) vert.select_set(True)
if selected_vertex_count > 0: if selected_vertex_count > 0:
logger.debug(f"Selected {selected_vertex_count} vertices in mesh {mesh_object.name}")
mesh2selected_vertex_count[mesh_object] = selected_vertex_count mesh2selected_vertex_count[mesh_object] = selected_vertex_count
target_bmesh.select_flush_mode() target_bmesh.select_flush_mode()
target_bmesh.to_mesh(mesh) target_bmesh.to_mesh(mesh)
@@ -352,3 +395,61 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
target_bmesh.clear() target_bmesh.clear()
return mesh2selected_vertex_count return mesh2selected_vertex_count
def _select_related_ik_bones(self, armature_object: bpy.types.Object) -> None:
"""
Expand the current selection to include any full IK systems that are
partially selected. An IK system includes the chain bones, the IK
target bone, and the pole target bone.
NOTE: This method operates entirely in EDIT mode and avoids mode switching
to prevent segmentation faults.
"""
edit_bones = armature_object.data.edit_bones
initial_selection_names = {b.name for b in edit_bones if b.select}
# Access pose bones constraints directly without mode switching
pose_bones = armature_object.pose.bones
# Find all complete IK systems
ik_systems = []
for pose_bone in pose_bones:
for constraint in pose_bone.constraints:
if constraint.type == "IK":
# Build the set of bones in this IK system
system_bones = {pose_bone.name}
# Add the main IK Target bone
if constraint.target and constraint.subtarget:
system_bones.add(constraint.subtarget)
# Add the Pole Target bone
if constraint.pole_target and constraint.pole_subtarget:
system_bones.add(constraint.pole_subtarget)
# Add all other bones in the IK chain
current_bone_name = pose_bone.name
chain_count = constraint.chain_count
# Walk up the parent chain
for _ in range(chain_count - 1):
if current_bone_name not in edit_bones:
break
current_bone = edit_bones[current_bone_name]
if not current_bone.parent:
break
current_bone_name = current_bone.parent.name
system_bones.add(current_bone_name)
ik_systems.append(system_bones)
# Expand selection to include any related, full IK systems
final_selection_names = set(initial_selection_names)
for system in ik_systems:
if not system.isdisjoint(initial_selection_names):
final_selection_names.update(system)
# Apply the final selection
for bone in edit_bones:
bone.select = bone.name in final_selection_names
+399 -106
View File
@@ -1,30 +1,26 @@
# -*- coding: utf-8 -*- # Copyright 2015 MMD Tools authors
# Copyright 2014 MMD Tools authors # This file is part of MMD Tools.
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import Optional, cast, List, Dict, Any, Set, Tuple, Union from collections import namedtuple
from typing import Optional, cast
import bpy import bpy
from mathutils import Quaternion, Vector from mathutils import Quaternion, Vector
from ..core.model import FnModel
from .. import bpyutils, utils from .. import bpyutils, utils
from ..core.exceptions import MaterialNotFoundError from ..core.exceptions import MaterialNotFoundError
from ..core.material import FnMaterial from ..core.material import FnMaterial
from ..core.model import FnModel
from ..core.morph import FnMorph from ..core.morph import FnMorph
from ..utils import ItemMoveOp, ItemOp from ..utils import ItemMoveOp, ItemOp
from ....logging_setup import logger
# Util functions # Util functions
def divide_vector_components(vec1: List[float], vec2: List[float]) -> List[float]: def divide_vector_components(vec1, vec2):
if len(vec1) != len(vec2): if len(vec1) != len(vec2):
raise ValueError("Vectors should have the same number of components") raise ValueError("Vectors should have the same number of components")
result = [] result = []
for v1, v2 in zip(vec1, vec2): for v1, v2 in zip(vec1, vec2, strict=False):
if v2 == 0: if v2 == 0:
if v1 == 0: if v1 == 0:
v2 = 1 # If we have a 0/0 case we change the divisor to 1 v2 = 1 # If we have a 0/0 case we change the divisor to 1
@@ -34,17 +30,17 @@ def divide_vector_components(vec1: List[float], vec2: List[float]) -> List[float
return result return result
def multiply_vector_components(vec1: List[float], vec2: List[float]) -> List[float]: def multiply_vector_components(vec1, vec2):
if len(vec1) != len(vec2): if len(vec1) != len(vec2):
raise ValueError("Vectors should have the same number of components") raise ValueError("Vectors should have the same number of components")
result = [] result = []
for v1, v2 in zip(vec1, vec2): for v1, v2 in zip(vec1, vec2, strict=False):
result.append(v1 * v2) result.append(v1 * v2)
return result return result
def special_division(n1: float, n2: float) -> float: def special_division(n1, n2):
"""This function returns 0 in case of 0/0. If non-zero divided by zero case is found, an Exception is raised""" """Return 0 in case of 0/0. If non-zero divided by zero case is found, an Exception is raised"""
if n2 == 0: if n2 == 0:
if n1 == 0: if n1 == 0:
n2 = 1 n2 = 1
@@ -59,7 +55,7 @@ class AddMorph(bpy.types.Operator):
bl_description = "Add a morph item to active morph list" bl_description = "Add a morph item to active morph list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root mmd_root = root.mmd_root
@@ -69,7 +65,6 @@ class AddMorph(bpy.types.Operator):
morph.name = "New Morph" morph.name = "New Morph"
if morph_type.startswith("uv"): if morph_type.startswith("uv"):
morph.data_type = "VERTEX_GROUP" morph.data_type = "VERTEX_GROUP"
logger.debug(f"Added new morph of type {morph_type}")
return {"FINISHED"} return {"FINISHED"}
@@ -86,7 +81,7 @@ class RemoveMorph(bpy.types.Operator):
options={"SKIP_SAVE"}, options={"SKIP_SAVE"},
) )
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root mmd_root = root.mmd_root
@@ -101,21 +96,19 @@ class RemoveMorph(bpy.types.Operator):
if self.all: if self.all:
morphs.clear() morphs.clear()
mmd_root.active_morph = 0 mmd_root.active_morph = 0
logger.debug(f"Removed all morphs of type {morph_type}")
else: else:
morphs.remove(mmd_root.active_morph) morphs.remove(mmd_root.active_morph)
mmd_root.active_morph = max(0, mmd_root.active_morph - 1) mmd_root.active_morph = max(0, mmd_root.active_morph - 1)
logger.debug(f"Removed morph at index {mmd_root.active_morph} of type {morph_type}")
return {"FINISHED"} return {"FINISHED"}
class MoveMorph(bpy.types.Operator, ItemMoveOp): class MoveMorph(bpy.types.Operator, ItemMoveOp):
bl_idname = "mmd_tools.morph_move" bl_idname = "mmd_tools.morph_move"
bl_label = "Move Morph" bl_label = "Move Morph"
bl_description = "Move active morph item up/down in the list" bl_description = "Move active morph item up/down in the list. This will not affect the morph order in exported PMX files (use Display Panel order instead)."
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root mmd_root = root.mmd_root
@@ -124,7 +117,6 @@ class MoveMorph(bpy.types.Operator, ItemMoveOp):
mmd_root.active_morph, mmd_root.active_morph,
self.type, self.type,
) )
logger.debug(f"Moved morph to index {mmd_root.active_morph}")
return {"FINISHED"} return {"FINISHED"}
@@ -134,7 +126,7 @@ class CopyMorph(bpy.types.Operator):
bl_description = "Make a copy of active morph in the list" bl_description = "Make a copy of active morph in the list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
@@ -146,7 +138,7 @@ class CopyMorph(bpy.types.Operator):
if morph is None: if morph is None:
return {"CANCELLED"} return {"CANCELLED"}
name_orig, name_tmp = morph.name, "_tmp%s" % str(morph.as_pointer()) name_orig, name_tmp = morph.name, f"_tmp{str(morph.as_pointer())}"
if morph_type.startswith("vertex"): if morph_type.startswith("vertex"):
for obj in FnModel.iterate_mesh_objects(root): for obj in FnModel.iterate_mesh_objects(root):
@@ -161,7 +153,6 @@ class CopyMorph(bpy.types.Operator):
for k, v in morph.items(): for k, v in morph.items():
morph_new[k] = v if k != "name" else name_tmp morph_new[k] = v if k != "name" else name_tmp
morph_new.name = name_orig + "_copy" # trigger name check morph_new.name = name_orig + "_copy" # trigger name check
logger.debug(f"Copied morph {name_orig} to {morph_new.name}")
return {"FINISHED"} return {"FINISHED"}
@@ -171,17 +162,14 @@ class OverwriteBoneMorphsFromActionPose(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod @classmethod
def poll(cls, context: bpy.types.Context) -> bool: def poll(cls, context):
root = FnModel.find_root_object(context.active_object) root = FnModel.find_root_object(context.active_object)
if root is None: return root is not None and root.mmd_root.active_morph_type == "bone_morphs"
return False
return root.mmd_root.active_morph_type == "bone_morphs" def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
root = FnModel.find_root_object(context.active_object) root = FnModel.find_root_object(context.active_object)
FnMorph.overwrite_bone_morphs_from_action_pose(FnModel.find_armature_object(root)) FnMorph.overwrite_bone_morphs_from_action_pose(FnModel.find_armature_object(root))
logger.info("Overwrote bone morphs from active action pose")
return {"FINISHED"} return {"FINISHED"}
@@ -191,7 +179,7 @@ class AddMorphOffset(bpy.types.Operator):
bl_description = "Add a morph offset item to the list" bl_description = "Add a morph offset item to the list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root mmd_root = root.mmd_root
@@ -216,7 +204,6 @@ class AddMorphOffset(bpy.types.Operator):
item.location = pose_bone.location item.location = pose_bone.location
item.rotation = pose_bone.rotation_quaternion item.rotation = pose_bone.rotation_quaternion
logger.debug(f"Added morph offset to {morph_type}")
return {"FINISHED"} return {"FINISHED"}
@@ -233,7 +220,7 @@ class RemoveMorphOffset(bpy.types.Operator):
options={"SKIP_SAVE"}, options={"SKIP_SAVE"},
) )
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
@@ -250,21 +237,17 @@ class RemoveMorphOffset(bpy.types.Operator):
if morph_type.startswith("vertex"): if morph_type.startswith("vertex"):
for obj in FnModel.iterate_mesh_objects(root): for obj in FnModel.iterate_mesh_objects(root):
FnMorph.remove_shape_key(obj, morph.name) FnMorph.remove_shape_key(obj, morph.name)
logger.debug(f"Removed all vertex morph offsets for {morph.name}")
return {"FINISHED"} return {"FINISHED"}
elif morph_type.startswith("uv"): if morph_type.startswith("uv"):
if morph.data_type == "VERTEX_GROUP": if morph.data_type == "VERTEX_GROUP":
for obj in FnModel.iterate_mesh_objects(root): for obj in FnModel.iterate_mesh_objects(root):
FnMorph.store_uv_morph_data(obj, morph) FnMorph.store_uv_morph_data(obj, morph)
logger.debug(f"Removed all UV morph offsets for {morph.name}")
return {"FINISHED"} return {"FINISHED"}
morph.data.clear() morph.data.clear()
morph.active_data = 0 morph.active_data = 0
logger.debug(f"Cleared all morph offsets for {morph.name}")
else: else:
morph.data.remove(morph.active_data) morph.data.remove(morph.active_data)
morph.active_data = max(0, morph.active_data - 1) morph.active_data = max(0, morph.active_data - 1)
logger.debug(f"Removed morph offset at index {morph.active_data}")
return {"FINISHED"} return {"FINISHED"}
@@ -280,7 +263,7 @@ class InitMaterialOffset(bpy.types.Operator):
default=0, default=0,
) )
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root mmd_root = root.mmd_root
@@ -292,7 +275,6 @@ class InitMaterialOffset(bpy.types.Operator):
mat_data.specular_color = mat_data.ambient_color = (val,) * 3 mat_data.specular_color = mat_data.ambient_color = (val,) * 3
mat_data.shininess = mat_data.edge_weight = val mat_data.shininess = mat_data.edge_weight = val
mat_data.texture_factor = mat_data.toon_texture_factor = mat_data.sphere_texture_factor = (val,) * 4 mat_data.texture_factor = mat_data.toon_texture_factor = mat_data.sphere_texture_factor = (val,) * 4
logger.debug(f"Initialized material offset with value {val}")
return {"FINISHED"} return {"FINISHED"}
@@ -302,7 +284,7 @@ class ApplyMaterialOffset(bpy.types.Operator):
bl_description = "Calculates the offsets and apply them, then the temporary material is removed" bl_description = "Calculates the offsets and apply them, then the temporary material is removed"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root mmd_root = root.mmd_root
@@ -340,7 +322,6 @@ class ApplyMaterialOffset(bpy.types.Operator):
except ZeroDivisionError: except ZeroDivisionError:
mat_data.offset_type = "ADD" # If there is any 0 division we automatically switch it to type ADD mat_data.offset_type = "ADD" # If there is any 0 division we automatically switch it to type ADD
logger.warning("Zero division detected, switching to ADD offset type")
except ValueError: except ValueError:
self.report({"ERROR"}, "An unexpected error happened") self.report({"ERROR"}, "An unexpected error happened")
# We should stop on our tracks and re-raise the exception # We should stop on our tracks and re-raise the exception
@@ -358,7 +339,6 @@ class ApplyMaterialOffset(bpy.types.Operator):
mat_data.edge_weight = work_mmd_mat.edge_weight - base_mmd_mat.edge_weight mat_data.edge_weight = work_mmd_mat.edge_weight - base_mmd_mat.edge_weight
FnMaterial.clean_materials(meshObj, can_remove=lambda m: m == work_mat) FnMaterial.clean_materials(meshObj, can_remove=lambda m: m == work_mat)
logger.info(f"Applied material offset for {mat_data.material}")
return {"FINISHED"} return {"FINISHED"}
@@ -368,7 +348,7 @@ class CreateWorkMaterial(bpy.types.Operator):
bl_description = "Creates a temporary material to edit this offset" bl_description = "Creates a temporary material to edit this offset"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root mmd_root = root.mmd_root
@@ -385,12 +365,12 @@ class CreateWorkMaterial(bpy.types.Operator):
base_mat = meshObj.data.materials.get(mat_data.material, None) base_mat = meshObj.data.materials.get(mat_data.material, None)
if base_mat is None: if base_mat is None:
self.report({"ERROR"}, 'Material "%s" not found' % mat_data.material) self.report({"ERROR"}, f'Material "{mat_data.material}" not found')
return {"CANCELLED"} return {"CANCELLED"}
work_mat_name = base_mat.name + "_temp" work_mat_name = base_mat.name + "_temp"
if work_mat_name in bpy.data.materials: if work_mat_name in bpy.data.materials:
self.report({"ERROR"}, 'Temporary material "%s" is in use' % work_mat_name) self.report({"ERROR"}, f'Temporary material "{work_mat_name}" is in use')
return {"CANCELLED"} return {"CANCELLED"}
work_mat = base_mat.copy() work_mat = base_mat.copy()
@@ -427,7 +407,6 @@ class CreateWorkMaterial(bpy.types.Operator):
work_mmd_mat.edge_color = list(edge_offset) work_mmd_mat.edge_color = list(edge_offset)
work_mmd_mat.edge_weight += mat_data.edge_weight work_mmd_mat.edge_weight += mat_data.edge_weight
logger.info(f"Created work material {work_mat_name}")
return {"FINISHED"} return {"FINISHED"}
@@ -437,24 +416,23 @@ class ClearTempMaterials(bpy.types.Operator):
bl_description = "Clears all the temporary materials" bl_description = "Clears all the temporary materials"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
for meshObj in FnModel.iterate_mesh_objects(root): for meshObj in FnModel.iterate_mesh_objects(root):
def __pre_remove(m: Optional[bpy.types.Material]) -> bool: def __pre_remove(m, meshObj=meshObj):
if m and "_temp" in m.name: if m and "_temp" in m.name:
base_mat_name = m.name.split("_temp")[0] base_mat_name = m.name.split("_temp")[0]
try: try:
FnMaterial.swap_materials(meshObj, m.name, base_mat_name) FnMaterial.swap_materials(meshObj, m.name, base_mat_name)
return True return True
except MaterialNotFoundError: except MaterialNotFoundError:
self.report({"WARNING"}, "Base material for %s was not found" % m.name) self.report({"WARNING"}, f"Base material for {m.name} was not found")
return False return False
FnMaterial.clean_materials(meshObj, can_remove=__pre_remove) FnMaterial.clean_materials(meshObj, can_remove=__pre_remove)
logger.info("Cleared all temporary materials")
return {"FINISHED"} return {"FINISHED"}
@@ -464,7 +442,7 @@ class ViewBoneMorph(bpy.types.Operator):
bl_description = "View the result of active bone morph" bl_description = "View the result of active bone morph"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
@@ -475,11 +453,10 @@ class ViewBoneMorph(bpy.types.Operator):
for morph_data in morph.data: for morph_data in morph.data:
p_bone: Optional[bpy.types.PoseBone] = armature.pose.bones.get(morph_data.bone, None) p_bone: Optional[bpy.types.PoseBone] = armature.pose.bones.get(morph_data.bone, None)
if p_bone: if p_bone:
p_bone.bone.select = True p_bone.select = True
mtx = (p_bone.matrix_basis.to_3x3() @ Quaternion(*morph_data.rotation.to_axis_angle()).to_matrix()).to_4x4() mtx = (p_bone.matrix_basis.to_3x3() @ Quaternion(*morph_data.rotation.to_axis_angle()).to_matrix()).to_4x4()
mtx.translation = p_bone.location + morph_data.location mtx.translation = p_bone.location + morph_data.location
p_bone.matrix_basis = mtx p_bone.matrix_basis = mtx
logger.info(f"Viewing bone morph: {morph.name}")
return {"FINISHED"} return {"FINISHED"}
@@ -489,14 +466,13 @@ class ClearBoneMorphView(bpy.types.Operator):
bl_description = "Reset transforms of all bones to their default values" bl_description = "Reset transforms of all bones to their default values"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
armature = FnModel.find_armature_object(root) armature = FnModel.find_armature_object(root)
for p_bone in armature.pose.bones: for p_bone in armature.pose.bones:
p_bone.matrix_basis.identity() p_bone.matrix_basis.identity()
logger.info("Cleared bone morph view")
return {"FINISHED"} return {"FINISHED"}
@@ -506,7 +482,7 @@ class ApplyBoneMorph(bpy.types.Operator):
bl_description = "Apply current pose to active bone morph" bl_description = "Apply current pose to active bone morph"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
@@ -521,10 +497,9 @@ class ApplyBoneMorph(bpy.types.Operator):
item.bone = p_bone.name item.bone = p_bone.name
item.location = p_bone.location item.location = p_bone.location
item.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion() item.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion()
p_bone.bone.select = True p_bone.select = True
else: else:
p_bone.bone.select = False p_bone.select = False
logger.info(f"Applied current pose to bone morph: {morph.name}")
return {"FINISHED"} return {"FINISHED"}
@@ -534,7 +509,7 @@ class SelectRelatedBone(bpy.types.Operator):
bl_description = "Select the bone assigned to this offset in the armature" bl_description = "Select the bone assigned to this offset in the armature"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
@@ -543,7 +518,6 @@ class SelectRelatedBone(bpy.types.Operator):
morph = mmd_root.bone_morphs[mmd_root.active_morph] morph = mmd_root.bone_morphs[mmd_root.active_morph]
morph_data = morph.data[morph.active_data] morph_data = morph.data[morph.active_data]
utils.selectSingleBone(context, armature, morph_data.bone) utils.selectSingleBone(context, armature, morph_data.bone)
logger.debug(f"Selected bone: {morph_data.bone}")
return {"FINISHED"} return {"FINISHED"}
@@ -553,7 +527,7 @@ class EditBoneOffset(bpy.types.Operator):
bl_description = "Applies the location and rotation of this offset to the bone" bl_description = "Applies the location and rotation of this offset to the bone"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
@@ -566,7 +540,6 @@ class EditBoneOffset(bpy.types.Operator):
mtx.translation = morph_data.location mtx.translation = morph_data.location
p_bone.matrix_basis = mtx p_bone.matrix_basis = mtx
utils.selectSingleBone(context, armature, p_bone.name) utils.selectSingleBone(context, armature, p_bone.name)
logger.debug(f"Edited bone offset for {p_bone.name}")
return {"FINISHED"} return {"FINISHED"}
@@ -576,7 +549,7 @@ class ApplyBoneOffset(bpy.types.Operator):
bl_description = "Stores the current bone location and rotation into this offset" bl_description = "Stores the current bone location and rotation into this offset"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
@@ -588,7 +561,6 @@ class ApplyBoneOffset(bpy.types.Operator):
p_bone = armature.pose.bones[morph_data.bone] p_bone = armature.pose.bones[morph_data.bone]
morph_data.location = p_bone.location morph_data.location = p_bone.location
morph_data.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion() morph_data.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion()
logger.debug(f"Applied bone offset for {p_bone.name}")
return {"FINISHED"} return {"FINISHED"}
@@ -598,7 +570,7 @@ class ViewUVMorph(bpy.types.Operator):
bl_description = "View the result of active UV morph on current mesh object" bl_description = "View the result of active UV morph on current mesh object"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
@@ -616,11 +588,11 @@ class ViewUVMorph(bpy.types.Operator):
selected = meshObj.select_get() selected = meshObj.select_get()
with bpyutils.select_object(meshObj): with bpyutils.select_object(meshObj):
mesh = cast(bpy.types.Mesh, meshObj.data) mesh = cast("bpy.types.Mesh", meshObj.data)
morph = mmd_root.uv_morphs[mmd_root.active_morph] morph = mmd_root.uv_morphs[mmd_root.active_morph]
uv_textures = mesh.uv_layers uv_textures = mesh.uv_layers
base_uv_layers = [l for l in mesh.uv_layers if not l.name.startswith("_")] base_uv_layers = [layer for layer in mesh.uv_layers if not layer.name.startswith("_")]
if morph.uv_index >= len(base_uv_layers): if morph.uv_index >= len(base_uv_layers):
self.report({"ERROR"}, "Invalid uv index: %d" % morph.uv_index) self.report({"ERROR"}, "Invalid uv index: %d" % morph.uv_index)
return {"CANCELLED"} return {"CANCELLED"}
@@ -630,7 +602,7 @@ class ViewUVMorph(bpy.types.Operator):
uv_textures.active = uv_textures[uv_layer_name] uv_textures.active = uv_textures[uv_layer_name]
uv_layer_name = uv_textures.active.name uv_layer_name = uv_textures.active.name
uv_tex = uv_textures.new(name="__uv.%s" % uv_layer_name) uv_tex = uv_textures.new(name=f"__uv.{uv_layer_name}")
if uv_tex is None: if uv_tex is None:
self.report({"ERROR"}, "Failed to create a temporary uv layer") self.report({"ERROR"}, "Failed to create a temporary uv layer")
return {"CANCELLED"} return {"CANCELLED"}
@@ -640,16 +612,15 @@ class ViewUVMorph(bpy.types.Operator):
if len(offsets) > 0: if len(offsets) > 0:
base_uv_data = mesh.uv_layers.active.data base_uv_data = mesh.uv_layers.active.data
temp_uv_data = mesh.uv_layers[uv_tex.name].data temp_uv_data = mesh.uv_layers[uv_tex.name].data
for i, l in enumerate(mesh.loops): for i, loop in enumerate(mesh.loops):
select = temp_uv_data[i].select = l.vertex_index in offsets select = temp_uv_data[i].select = loop.vertex_index in offsets
if select: if select:
temp_uv_data[i].uv = base_uv_data[i].uv + offsets[l.vertex_index] temp_uv_data[i].uv = base_uv_data[i].uv + offsets[loop.vertex_index]
uv_textures.active = uv_tex uv_textures.active = uv_tex
uv_tex.active_render = True uv_tex.active_render = True
meshObj.hide_set(False) meshObj.hide_set(False)
meshObj.select_set(selected) meshObj.select_set(selected)
logger.info(f"Viewing UV morph: {morph.name}")
return {"FINISHED"} return {"FINISHED"}
@@ -659,14 +630,14 @@ class ClearUVMorphView(bpy.types.Operator):
bl_description = "Clear all temporary data of UV morphs" bl_description = "Clear all temporary data of UV morphs"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
for m in FnModel.iterate_mesh_objects(root): for m in FnModel.iterate_mesh_objects(root):
mesh = m.data mesh = m.data
uv_textures = getattr(mesh, "uv_textures", mesh.uv_layers) uv_textures = getattr(mesh, "uv_textures", mesh.uv_layers)
for t in uv_textures: for t in reversed(uv_textures):
if t.name.startswith("__uv."): if t.name.startswith("__uv."):
uv_textures.remove(t) uv_textures.remove(t)
if len(uv_textures) > 0: if len(uv_textures) > 0:
@@ -676,7 +647,7 @@ class ClearUVMorphView(bpy.types.Operator):
animation_data = mesh.animation_data animation_data = mesh.animation_data
if animation_data: if animation_data:
nla_tracks = animation_data.nla_tracks nla_tracks = animation_data.nla_tracks
for t in nla_tracks: for t in reversed(nla_tracks):
if t.name.startswith("__uv."): if t.name.startswith("__uv."):
nla_tracks.remove(t) nla_tracks.remove(t)
if animation_data.action and animation_data.action.name.startswith("__uv."): if animation_data.action and animation_data.action.name.startswith("__uv."):
@@ -684,10 +655,9 @@ class ClearUVMorphView(bpy.types.Operator):
if animation_data.action is None and len(nla_tracks) == 0: if animation_data.action is None and len(nla_tracks) == 0:
mesh.animation_data_clear() mesh.animation_data_clear()
for act in bpy.data.actions: for act in reversed(bpy.data.actions):
if act.name.startswith("__uv.") and act.users < 1: if act.name.startswith("__uv.") and act.users < 1:
bpy.data.actions.remove(act) bpy.data.actions.remove(act)
logger.info("Cleared UV morph view")
return {"FINISHED"} return {"FINISHED"}
@@ -698,20 +668,20 @@ class EditUVMorph(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod @classmethod
def poll(cls, context: bpy.types.Context) -> bool: def poll(cls, context):
obj = context.active_object obj = context.active_object
if obj.type != "MESH": if obj is None or obj.type != "MESH":
return False return False
active_uv_layer = obj.data.uv_layers.active active_uv_layer = obj.data.uv_layers.active
return active_uv_layer and active_uv_layer.name.startswith("__uv.") return active_uv_layer is not None and active_uv_layer.name.startswith("__uv.")
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
meshObj = obj meshObj = obj
selected = meshObj.select_get() selected = meshObj.select_get()
with bpyutils.select_object(meshObj): with bpyutils.select_object(meshObj):
mesh = cast(bpy.types.Mesh, meshObj.data) mesh = cast("bpy.types.Mesh", meshObj.data)
bpy.ops.object.mode_set(mode="EDIT") bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.select_mode(type="VERT", action="ENABLE") bpy.ops.mesh.select_mode(type="VERT", action="ENABLE")
bpy.ops.mesh.reveal() # unhide all vertices bpy.ops.mesh.reveal() # unhide all vertices
@@ -719,16 +689,15 @@ class EditUVMorph(bpy.types.Operator):
bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="OBJECT")
vertices = mesh.vertices vertices = mesh.vertices
for l, d in zip(mesh.loops, mesh.uv_layers.active.data): for loop, d in zip(mesh.loops, mesh.uv_layers.active.data, strict=False):
if d.select: if d.select:
vertices[l.vertex_index].select = True vertices[loop.vertex_index].select = True
polygons = mesh.polygons polygons = mesh.polygons
polygons.active = getattr(next((p for p in polygons if all(vertices[i].select for i in p.vertices)), None), "index", polygons.active) polygons.active = getattr(next((p for p in polygons if all(vertices[i].select for i in p.vertices)), None), "index", polygons.active)
bpy.ops.object.mode_set(mode="EDIT") bpy.ops.object.mode_set(mode="EDIT")
meshObj.select_set(selected) meshObj.select_set(selected)
logger.info("Editing UV morph")
return {"FINISHED"} return {"FINISHED"}
@@ -739,14 +708,14 @@ class ApplyUVMorph(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod @classmethod
def poll(cls, context: bpy.types.Context) -> bool: def poll(cls, context):
obj = context.active_object obj = context.active_object
if obj.type != "MESH": if obj is None or obj.type != "MESH":
return False return False
active_uv_layer = obj.data.uv_layers.active active_uv_layer = obj.data.uv_layers.active
return active_uv_layer and active_uv_layer.name.startswith("__uv.") return active_uv_layer is not None and active_uv_layer.name.startswith("__uv.")
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root mmd_root = root.mmd_root
@@ -754,34 +723,31 @@ class ApplyUVMorph(bpy.types.Operator):
selected = meshObj.select_get() selected = meshObj.select_get()
with bpyutils.select_object(meshObj): with bpyutils.select_object(meshObj):
mesh = cast(bpy.types.Mesh, meshObj.data) mesh = cast("bpy.types.Mesh", meshObj.data)
morph = mmd_root.uv_morphs[mmd_root.active_morph] morph = mmd_root.uv_morphs[mmd_root.active_morph]
base_uv_name = mesh.uv_layers.active.name[5:] base_uv_name = mesh.uv_layers.active.name[5:]
if base_uv_name not in mesh.uv_layers: if base_uv_name not in mesh.uv_layers:
self.report({"ERROR"}, ' * UV map "%s" not found' % base_uv_name) self.report({"ERROR"}, f' * UV map "{base_uv_name}" not found')
return {"CANCELLED"} return {"CANCELLED"}
base_uv_data = mesh.uv_layers[base_uv_name].data base_uv_data = mesh.uv_layers[base_uv_name].data
temp_uv_data = mesh.uv_layers.active.data temp_uv_data = mesh.uv_layers.active.data
axis_type = "ZW" if base_uv_name.startswith("_") else "XY" axis_type = "ZW" if base_uv_name.startswith("_") else "XY"
from collections import namedtuple
__OffsetData = namedtuple("OffsetData", "index, offset") __OffsetData = namedtuple("OffsetData", "index, offset")
offsets = {} offsets = {}
vertices = mesh.vertices vertices = mesh.vertices
for l, i0, i1 in zip(mesh.loops, base_uv_data, temp_uv_data): for loop, i0, i1 in zip(mesh.loops, base_uv_data, temp_uv_data, strict=False):
if vertices[l.vertex_index].select and l.vertex_index not in offsets: if vertices[loop.vertex_index].select and loop.vertex_index not in offsets:
dx, dy = i1.uv - i0.uv dx, dy = i1.uv - i0.uv
if abs(dx) > 0.0001 or abs(dy) > 0.0001: if abs(dx) > 0.0001 or abs(dy) > 0.0001:
offsets[l.vertex_index] = __OffsetData(l.vertex_index, (dx, dy, dx, dy)) offsets[loop.vertex_index] = __OffsetData(loop.vertex_index, (dx, dy, dx, dy))
FnMorph.store_uv_morph_data(meshObj, morph, offsets.values(), axis_type) FnMorph.store_uv_morph_data(meshObj, morph, offsets.values(), axis_type)
morph.data_type = "VERTEX_GROUP" morph.data_type = "VERTEX_GROUP"
meshObj.select_set(selected) meshObj.select_set(selected)
logger.info(f"Applied UV morph: {morph.name}")
return {"FINISHED"} return {"FINISHED"}
@@ -792,12 +758,339 @@ class CleanDuplicatedMaterialMorphs(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context: bpy.types.Context) -> bool: def poll(cls, context):
return FnModel.find_root_object(context.active_object) is not None root = FnModel.find_root_object(context.active_object)
return root is not None
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context: bpy.types.Context):
mmd_root_object = FnModel.find_root_object(context.active_object) mmd_root_object = FnModel.find_root_object(context.active_object)
FnMorph.clean_duplicated_material_morphs(mmd_root_object) FnMorph.clean_duplicated_material_morphs(mmd_root_object)
logger.info("Cleaned duplicated material morphs")
return {"FINISHED"} return {"FINISHED"}
class ConvertBoneMorphToVertexMorph(bpy.types.Operator):
bl_idname = "mmd_tools.convert_bone_morph_to_vertex_morph"
bl_label = "Convert To Vertex Morph"
bl_description = "Convert a bone morph into a single vertex morph by applying the bone transformations.\nIf a corresponding vertex morph already exists, it will be updated."
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod
def poll(cls, context):
root = FnModel.find_root_object(context.active_object)
if root is None:
return False
mmd_root = root.mmd_root
if mmd_root.active_morph_type != "bone_morphs":
return False
morph = ItemOp.get_by_index(mmd_root.bone_morphs, mmd_root.active_morph)
return morph is not None and len(morph.data) > 0
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
# Get the active bone morph
bone_morph = ItemOp.get_by_index(mmd_root.bone_morphs, mmd_root.active_morph)
if bone_morph is None:
self.report({"ERROR"}, "No active bone morph")
return {"CANCELLED"}
original_name = bone_morph.name
target_name = original_name
# Add 'B' suffix if necessary
if not original_name.endswith("B"):
bone_morph.name = original_name + "B"
target_name = original_name
else:
# If already has B suffix, use name without B
target_name = original_name[:-1]
try:
# Step 1: import
from ..core.model import Model
rig = Model(root)
# Ensure morph slider is bound
bpy.ops.mmd_tools.morph_slider_setup(type="BIND")
# Re-obtain placeholder object
placeholder_obj = rig.morph_slider.placeholder()
if placeholder_obj is None or placeholder_obj.data.shape_keys is None:
self.report({"ERROR"}, "Failed to create morph slider system")
return {"CANCELLED"}
shape_keys = placeholder_obj.data.shape_keys
key_blocks = shape_keys.key_blocks
# Step 2: Check if target bone morph exists
current_morph_name = bone_morph.name
if current_morph_name not in key_blocks:
self.report({"ERROR"}, f"Bone morph '{current_morph_name}' not found in morph sliders")
return {"CANCELLED"}
# Step 3: Save all current morph values
original_values = {}
for key_block in key_blocks:
if key_block.name != "--- morph sliders ---":
original_values[key_block.name] = key_block.value
# Step 4: Set all morphs to 0
for key_block in key_blocks:
if key_block.name != "--- morph sliders ---":
key_block.value = 0
# Step 5: Set target bone morph to 1.0
key_blocks[current_morph_name].value = 1.0
# Step 6: Use Armature Modifier's "Apply as Shape Key" functionality
created_shape_keys = []
for mesh_obj in FnModel.iterate_mesh_objects(root):
# Switch to this mesh object
context.view_layer.objects.active = mesh_obj
# Ensure mesh object has shape keys
if mesh_obj.data.shape_keys is None:
mesh_obj.shape_key_add(name="Basis", from_mix=False)
# Delete existing shape key with same name
if target_name in mesh_obj.data.shape_keys.key_blocks:
idx = mesh_obj.data.shape_keys.key_blocks.find(target_name)
if idx >= 0:
mesh_obj.active_shape_key_index = idx
bpy.ops.object.shape_key_remove()
# Find armature modifier
armature_modifier = None
for modifier in mesh_obj.modifiers:
if modifier.type == "ARMATURE":
armature_modifier = modifier
break
if armature_modifier is None:
self.report({"WARNING"}, f"No armature modifier found on mesh '{mesh_obj.name}'")
continue
# Use Apply as Shape Key functionality, keeping the modifier
bpy.ops.object.modifier_apply_as_shapekey(modifier=armature_modifier.name, keep_modifier=True)
# Rename the newly created shape key to target name
shape_key_blocks = mesh_obj.data.shape_keys.key_blocks
new_shape_key = shape_key_blocks[-1] # Latest created shape key
new_shape_key.name = target_name
new_shape_key.value = 0.0 # Set to 0 to avoid double effect
created_shape_keys.append((mesh_obj.name, target_name))
self.report({"INFO"}, f"Created shape key '{target_name}' on mesh '{mesh_obj.name}'")
# Step 7: Restore all original morph values
for key_name, original_value in original_values.items():
if key_name in key_blocks:
key_blocks[key_name].value = original_value
# Step 8: Create or update vertex morph entry
vertex_morph_exists = False
for i, morph in enumerate(mmd_root.vertex_morphs):
if morph.name == target_name:
vertex_morph_exists = True
mmd_root.active_morph_type = "vertex_morphs"
mmd_root.active_morph = i
break
if not vertex_morph_exists:
mmd_root.active_morph_type = "vertex_morphs"
morph, mmd_root.active_morph = ItemOp.add_after(mmd_root.vertex_morphs, mmd_root.active_morph)
morph.name = target_name
# Step 9: Add to facial expression display frame
facial_frame = None
for frame in mmd_root.display_item_frames:
if frame.name == "表情":
facial_frame = frame
break
if facial_frame:
morph_exists_in_frame = False
for item in facial_frame.data:
if item.type == "MORPH" and item.name == target_name and item.morph_type == "vertex_morphs":
morph_exists_in_frame = True
break
if not morph_exists_in_frame:
new_item = facial_frame.data.add()
new_item.type = "MORPH"
new_item.morph_type = "vertex_morphs"
new_item.name = target_name
facial_frame.active_item = len(facial_frame.data) - 1
for i, frame in enumerate(mmd_root.display_item_frames):
if frame.name == "表情":
mmd_root.active_display_item_frame = i
break
# UNBIND
bpy.ops.mmd_tools.morph_slider_setup(type="UNBIND")
# Success message
shape_key_info = ", ".join([f"{mesh}:{key}" for mesh, key in created_shape_keys])
self.report({"INFO"}, f"Successfully converted bone morph '{original_name}' to vertex morph '{target_name}'. Created shape keys: {shape_key_info}")
except Exception as e:
self.report({"ERROR"}, f"Error during conversion: {str(e)}")
return {"CANCELLED"}
return {"FINISHED"}
class ConvertGroupMorphToVertexMorph(bpy.types.Operator):
bl_idname = "mmd_tools.convert_group_morph_to_vertex_morph"
bl_label = "Convert To Vertex Morph"
bl_description = "Convert a group morph into a single vertex morph by merging only the vertex morphs within the group.\nIf a corresponding vertex morph already exists, it will be updated."
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod
def poll(cls, context):
root = FnModel.find_root_object(context.active_object)
if root is None:
return False
mmd_root = root.mmd_root
if mmd_root.active_morph_type != "group_morphs":
return False
morph = ItemOp.get_by_index(mmd_root.group_morphs, mmd_root.active_morph)
return morph is not None and len(morph.data) > 0
def execute(self, context):
bpy.ops.mmd_tools.morph_slider_setup(type="UNBIND")
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
# Get the active group morph
group_morph = ItemOp.get_by_index(mmd_root.group_morphs, mmd_root.active_morph)
if group_morph is None:
self.report({"ERROR"}, "No active group morph")
return {"CANCELLED"}
# Check if the group morph contains any vertex morphs to convert
has_vertex_morphs = False
for offset in group_morph.data:
if offset.morph_type == "vertex_morphs":
has_vertex_morphs = True
break
if not has_vertex_morphs:
self.report({"ERROR"}, "The group morph does not contain any vertex morphs to convert")
return {"CANCELLED"}
original_name = group_morph.name
target_name = original_name
# Add 'G' suffix if necessary
if not original_name.endswith("G"):
group_morph.name = original_name + "G"
target_name = original_name
else:
# If already has G suffix, use name without G
target_name = original_name[:-1]
# First, reset all shape keys to zero
for obj in FnModel.iterate_mesh_objects(root):
if obj.data.shape_keys:
for kb in obj.data.shape_keys.key_blocks:
kb.value = 0
# Apply only the vertex morphs from the group morph
for offset in group_morph.data:
if offset.morph_type == "vertex_morphs":
# Find the vertex morph by name
vertex_morph = getattr(root.mmd_root, offset.morph_type).get(offset.name)
if vertex_morph:
# Apply this morph at the specified factor
for obj in FnModel.iterate_mesh_objects(root):
if obj.data.shape_keys:
kb = obj.data.shape_keys.key_blocks.get(offset.name)
if kb:
kb.value = offset.factor
# Now add a new shape key from mix for each mesh
for obj in FnModel.iterate_mesh_objects(root):
if obj.data.shape_keys:
# Make this the active object
context.view_layer.objects.active = obj
# Remove existing shape key if it exists
if target_name in obj.data.shape_keys.key_blocks:
idx = obj.data.shape_keys.key_blocks.find(target_name)
if idx >= 0:
obj.active_shape_key_index = idx
bpy.ops.object.shape_key_remove()
# Add shape key from mix
bpy.ops.object.shape_key_add(from_mix=True)
# Rename the newly created shape key
new_key = obj.data.shape_keys.key_blocks[-1]
new_key.name = target_name
# Check if a vertex morph with the target name already exists
vertex_morph_exists = False
for i, morph in enumerate(mmd_root.vertex_morphs):
if morph.name == target_name:
vertex_morph_exists = True
mmd_root.active_morph_type = "vertex_morphs"
mmd_root.active_morph = i
break
# If not, create a new vertex morph
if not vertex_morph_exists:
# Switch to vertex morphs panel
mmd_root.active_morph_type = "vertex_morphs"
# Add new vertex morph
morph, mmd_root.active_morph = ItemOp.add_after(mmd_root.vertex_morphs, mmd_root.active_morph)
morph.name = target_name
# Add the new vertex morph to the facial display frame
facial_frame = None
for frame in mmd_root.display_item_frames:
if frame.name == "表情": # This is the facial display frame
facial_frame = frame
break
if facial_frame:
# Check if this morph is already in the facial frame
morph_exists_in_frame = False
for item in facial_frame.data:
if item.type == "MORPH" and item.name == target_name and item.morph_type == "vertex_morphs":
morph_exists_in_frame = True
break
# If not, add it
if not morph_exists_in_frame:
new_item = facial_frame.data.add()
new_item.type = "MORPH"
new_item.morph_type = "vertex_morphs"
new_item.name = target_name
# Make this the active item in the facial frame
facial_frame.active_item = len(facial_frame.data) - 1
# Set the facial frame as active
for i, frame in enumerate(mmd_root.display_item_frames):
if frame.name == "表情":
mmd_root.active_display_item_frame = i
break
# Reset all shape keys
for obj in FnModel.iterate_mesh_objects(root):
if obj.data.shape_keys:
for kb in obj.data.shape_keys.key_blocks:
kb.value = 0
self.report({"INFO"}, f"Successfully converted vertex morphs in group to vertex morph '{target_name}' and added to facial display frame")
return {"FINISHED"}
+31 -49
View File
@@ -1,12 +1,8 @@
# -*- coding: utf-8 -*- # Copyright 2015 MMD Tools authors
# Copyright 2014 MMD Tools authors # This file is part of MMD Tools.
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import math import math
from typing import Dict, Optional, Tuple, cast, Set, List, Any, Union, Generator from typing import Dict, Optional, Tuple, cast
import bpy import bpy
from mathutils import Euler, Vector from mathutils import Euler, Vector
@@ -16,7 +12,6 @@ from ..bpyutils import FnContext, Props
from ..core import rigid_body from ..core import rigid_body
from ..core.model import FnModel, Model from ..core.model import FnModel, Model
from ..core.rigid_body import FnRigidBody from ..core.rigid_body import FnRigidBody
from ...logging_setup import logger
class SelectRigidBody(bpy.types.Operator): class SelectRigidBody(bpy.types.Operator):
@@ -44,15 +39,15 @@ class SelectRigidBody(bpy.types.Operator):
default=False, default=False,
) )
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]: def invoke(self, context, event):
vm = context.window_manager vm = context.window_manager
return vm.invoke_props_dialog(self) return vm.invoke_props_dialog(self)
@classmethod @classmethod
def poll(cls, context: bpy.types.Context) -> bool: def poll(cls, context):
return FnModel.is_rigid_body_object(context.active_object) return FnModel.is_rigid_body_object(context.active_object)
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
if root is None: if root is None:
@@ -174,7 +169,7 @@ class AddRigidBody(bpy.types.Operator):
default=0.1, default=0.1,
) )
def __add_rigid_body(self, context: bpy.types.Context, root_object: bpy.types.Object, pose_bone: Optional[bpy.types.PoseBone] = None) -> bpy.types.Object: def __add_rigid_body(self, context: bpy.types.Context, root_object: bpy.types.Object, pose_bone: Optional[bpy.types.PoseBone] = None):
name_j: str = self.name_j name_j: str = self.name_j
name_e: str = self.name_e name_e: str = self.name_e
size = self.size.copy() size = self.size.copy()
@@ -227,7 +222,7 @@ class AddRigidBody(bpy.types.Operator):
) )
@classmethod @classmethod
def poll(cls, context: bpy.types.Context) -> bool: def poll(cls, context):
root_object = FnModel.find_root_object(context.active_object) root_object = FnModel.find_root_object(context.active_object)
if root_object is None: if root_object is None:
return False return False
@@ -238,11 +233,11 @@ class AddRigidBody(bpy.types.Operator):
return True return True
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
active_object = context.active_object active_object = context.active_object
root_object = cast(bpy.types.Object, FnModel.find_root_object(active_object)) root_object = cast("bpy.types.Object", FnModel.find_root_object(active_object))
armature_object = cast(bpy.types.Object, FnModel.find_armature_object(root_object)) armature_object = cast("bpy.types.Object", FnModel.find_armature_object(root_object))
if active_object != armature_object: if active_object != armature_object:
FnContext.select_single_object(context, root_object).select_set(False) FnContext.select_single_object(context, root_object).select_set(False)
@@ -255,17 +250,15 @@ class AddRigidBody(bpy.types.Operator):
armature_object.select_set(False) armature_object.select_set(False)
if len(selected_pose_bones) > 0: if len(selected_pose_bones) > 0:
logger.info(f"Adding rigid bodies to {len(selected_pose_bones)} selected bones")
for pose_bone in selected_pose_bones: for pose_bone in selected_pose_bones:
rigid = self.__add_rigid_body(context, root_object, pose_bone) rigid = self.__add_rigid_body(context, root_object, pose_bone)
rigid.select_set(True) rigid.select_set(True)
else: else:
logger.info("Adding a single rigid body without bone attachment")
rigid = self.__add_rigid_body(context, root_object) rigid = self.__add_rigid_body(context, root_object)
rigid.select_set(True) rigid.select_set(True)
return {"FINISHED"} return {"FINISHED"}
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]: def invoke(self, context, event):
no_bone = True no_bone = True
if context.selected_bones and len(context.selected_bones) > 0: if context.selected_bones and len(context.selected_bones) > 0:
no_bone = False no_bone = False
@@ -291,13 +284,12 @@ class RemoveRigidBody(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context: bpy.types.Context) -> bool: def poll(cls, context):
return FnModel.is_rigid_body_object(context.active_object) return FnModel.is_rigid_body_object(context.active_object)
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
logger.info(f"Removing rigid body: {obj.name}")
utils.selectAObject(obj) # ensure this is the only one object select utils.selectAObject(obj) # ensure this is the only one object select
bpy.ops.object.delete(use_global=True) bpy.ops.object.delete(use_global=True)
if root: if root:
@@ -310,8 +302,7 @@ class RigidBodyBake(bpy.types.Operator):
bl_label = "Bake" bl_label = "Bake"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context: bpy.types.Context):
logger.info("Baking rigid body simulation")
with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache): with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache):
bpy.ops.ptcache.bake("INVOKE_DEFAULT", bake=True) bpy.ops.ptcache.bake("INVOKE_DEFAULT", bake=True)
@@ -323,8 +314,7 @@ class RigidBodyDeleteBake(bpy.types.Operator):
bl_label = "Delete Bake" bl_label = "Delete Bake"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context: bpy.types.Context):
logger.info("Deleting rigid body simulation bake")
with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache): with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache):
bpy.ops.ptcache.free_bake("INVOKE_DEFAULT") bpy.ops.ptcache.free_bake("INVOKE_DEFAULT")
@@ -387,7 +377,7 @@ class AddJoint(bpy.types.Operator):
min=0, min=0,
) )
def __enumerate_rigid_pair(self, bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]) -> Generator[Tuple[bpy.types.Object, bpy.types.Object], None, None]: def __enumerate_rigid_pair(self, bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]):
obj_seq = tuple(bone_map.keys()) obj_seq = tuple(bone_map.keys())
for rigid_a, bone_a in bone_map.items(): for rigid_a, bone_a in bone_map.items():
for rigid_b, bone_b in bone_map.items(): for rigid_b, bone_b in bone_map.items():
@@ -400,7 +390,7 @@ class AddJoint(bpy.types.Operator):
else: else:
yield obj_seq yield obj_seq
def __add_joint(self, context: bpy.types.Context, root_object: bpy.types.Object, rigid_pair: Tuple[bpy.types.Object, bpy.types.Object], bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]) -> bpy.types.Object: def __add_joint(self, context: bpy.types.Context, root_object: bpy.types.Object, rigid_pair: Tuple[bpy.types.Object, bpy.types.Object], bone_map):
loc: Optional[Vector] = None loc: Optional[Vector] = None
rot = Euler((0.0, 0.0, 0.0)) rot = Euler((0.0, 0.0, 0.0))
rigid_a, rigid_b = rigid_pair rigid_a, rigid_b = rigid_pair
@@ -438,7 +428,7 @@ class AddJoint(bpy.types.Operator):
) )
@classmethod @classmethod
def poll(cls, context: bpy.types.Context) -> bool: def poll(cls, context):
root_object = FnModel.find_root_object(context.active_object) root_object = FnModel.find_root_object(context.active_object)
if root_object is None: if root_object is None:
return False return False
@@ -449,11 +439,11 @@ class AddJoint(bpy.types.Operator):
return True return True
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
active_object = context.active_object active_object = context.active_object
root_object = cast(bpy.types.Object, FnModel.find_root_object(active_object)) root_object = cast("bpy.types.Object", FnModel.find_root_object(active_object))
armature_object = cast(bpy.types.Object, FnModel.find_armature_object(root_object)) armature_object = cast("bpy.types.Object", FnModel.find_armature_object(root_object))
bones = cast(bpy.types.Armature, armature_object.data).bones bones = cast("bpy.types.Armature", armature_object.data).bones
bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]] = {r: bones.get(r.mmd_rigid.bone, None) for r in FnModel.iterate_rigid_body_objects(root_object) if r.select_get()} bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]] = {r: bones.get(r.mmd_rigid.bone, None) for r in FnModel.iterate_rigid_body_objects(root_object) if r.select_get()}
if len(bone_map) < 2: if len(bone_map) < 2:
@@ -462,19 +452,15 @@ class AddJoint(bpy.types.Operator):
FnContext.select_single_object(context, root_object).select_set(False) FnContext.select_single_object(context, root_object).select_set(False)
if context.scene.rigidbody_world is None: if context.scene.rigidbody_world is None:
logger.info("Creating rigid body world")
bpy.ops.rigidbody.world_add() bpy.ops.rigidbody.world_add()
joint_count = 0
for pair in self.__enumerate_rigid_pair(bone_map): for pair in self.__enumerate_rigid_pair(bone_map):
joint = self.__add_joint(context, root_object, pair, bone_map) joint = self.__add_joint(context, root_object, pair, bone_map)
joint.select_set(True) joint.select_set(True)
joint_count += 1
logger.info(f"Added {joint_count} joints between rigid bodies")
return {"FINISHED"} return {"FINISHED"}
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]: def invoke(self, context, event):
vm = context.window_manager vm = context.window_manager
return vm.invoke_props_dialog(self) return vm.invoke_props_dialog(self)
@@ -486,13 +472,12 @@ class RemoveJoint(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context: bpy.types.Context) -> bool: def poll(cls, context):
return FnModel.is_joint_object(context.active_object) return FnModel.is_joint_object(context.active_object)
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
logger.info(f"Removing joint: {obj.name}")
utils.selectAObject(obj) # ensure this is the only one object select utils.selectAObject(obj) # ensure this is the only one object select
bpy.ops.object.delete(use_global=True) bpy.ops.object.delete(use_global=True)
if root: if root:
@@ -507,7 +492,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@staticmethod @staticmethod
def __get_rigid_body_world_objects() -> Tuple[bpy.types.Collection, bpy.types.Collection]: def __get_rigid_body_world_objects():
rigid_body.setRigidBodyWorldEnabled(True) rigid_body.setRigidBodyWorldEnabled(True)
rbw = bpy.context.scene.rigidbody_world rbw = bpy.context.scene.rigidbody_world
if not rbw.collection: if not rbw.collection:
@@ -522,21 +507,21 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
return rbw.collection.objects, rbw.constraints.objects return rbw.collection.objects, rbw.constraints.objects
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context):
scene = context.scene scene = context.scene
scene_objs = set(scene.objects) scene_objs = set(scene.objects)
scene_objs.union(o for x in scene.objects if x.instance_type == "COLLECTION" and x.instance_collection for o in x.instance_collection.objects) scene_objs.union(o for x in scene.objects if x.instance_type == "COLLECTION" and x.instance_collection for o in x.instance_collection.objects)
def _update_group(obj: bpy.types.Object, group: bpy.types.Collection) -> bool: def _update_group(obj, group):
if obj in scene_objs: if obj in scene_objs:
if obj not in group.values(): if obj not in group.values():
group.link(obj) group.link(obj)
return True return True
elif obj in group.values(): if obj in group.values():
group.unlink(obj) group.unlink(obj)
return False return False
def _references(obj: bpy.types.Object) -> Generator[bpy.types.Object, None, None]: def _references(obj):
yield obj yield obj
if getattr(obj, "proxy", None): if getattr(obj, "proxy", None):
yield from _references(obj.proxy) yield from _references(obj.proxy)
@@ -553,7 +538,6 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
# Object.rigid_body are removed, # Object.rigid_body are removed,
# but Object.rigid_body_constraint are retained. # but Object.rigid_body_constraint are retained.
# Therefore, it must be checked with Object.mmd_type. # Therefore, it must be checked with Object.mmd_type.
logger.info("Updating rigid body world objects")
for i in (x for x in objects if x.mmd_type == "RIGID_BODY"): for i in (x for x in objects if x.mmd_type == "RIGID_BODY"):
if not _update_group(i, rb_objs): if not _update_group(i, rb_objs):
continue continue
@@ -568,7 +552,6 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
# TODO Modify mmd_rigid to allow recovery of the remaining rigidbody parameters. # TODO Modify mmd_rigid to allow recovery of the remaining rigidbody parameters.
# mass, friction, restitution, linear_dumping, angular_dumping # mass, friction, restitution, linear_dumping, angular_dumping
logger.info("Updating rigid body constraints")
for i in (x for x in objects if x.rigid_body_constraint): for i in (x for x in objects if x.rigid_body_constraint):
if not _update_group(i, rbc_objs): if not _update_group(i, rbc_objs):
continue continue
@@ -579,7 +562,6 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
rbc.object2 = rb_map.get(rbc.object2, rbc.object2) rbc.object2 = rb_map.get(rbc.object2, rbc.object2)
if need_rebuild_physics: if need_rebuild_physics:
logger.info("Rebuilding physics for models")
for root_object in scene.objects: for root_object in scene.objects:
if root_object.mmd_type != "ROOT": if root_object.mmd_type != "ROOT":
continue continue
+13 -23
View File
@@ -1,23 +1,18 @@
# -*- coding: utf-8 -*- # Copyright 2018 MMD Tools authors
# Copyright 2014 MMD Tools authors # This file is part of MMD Tools.
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import Set, Tuple from typing import Set
import bpy import bpy
from bpy.types import Operator, Context, Object from bpy.types import Operator
from ..core.model import FnModel from ..core.model import FnModel
from ..core.sdef import FnSDEF from ..core.sdef import FnSDEF
from ....core.logging_setup import logger
def _get_target_objects(context: Context) -> Tuple[Set[Object], Set[Object]]: def _get_target_objects(context):
root_objects: Set[Object] = set() root_objects: Set[bpy.types.Object] = set()
selected_objects: Set[Object] = set() selected_objects: Set[bpy.types.Object] = set()
for i in context.selected_objects: for i in context.selected_objects:
if i.type == "MESH": if i.type == "MESH":
selected_objects.add(i) selected_objects.add(i)
@@ -41,13 +36,11 @@ class ResetSDEFCache(Operator):
bl_description = "Reset MMD SDEF cache of selected objects and clean unused cache" bl_description = "Reset MMD SDEF cache of selected objects and clean unused cache"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: Context) -> Set[str]: def execute(self, context):
target_meshes, _ = _get_target_objects(context) target_meshes, _ = _get_target_objects(context)
logger.info(f"Resetting SDEF cache for {len(target_meshes)} objects")
for i in target_meshes: for i in target_meshes:
FnSDEF.clear_cache(i) FnSDEF.clear_cache(i)
FnSDEF.clear_cache(unused_only=True) FnSDEF.clear_cache(unused_only=True)
logger.debug("SDEF cache reset completed")
return {"FINISHED"} return {"FINISHED"}
@@ -78,20 +71,19 @@ class BindSDEF(Operator):
default=False, default=False,
) )
def invoke(self, context: Context, event: bpy.types.Event) -> Set[str]: def invoke(self, context, event):
vm = context.window_manager vm = context.window_manager
return vm.invoke_props_dialog(self) return vm.invoke_props_dialog(self)
def execute(self, context: Context) -> Set[str]: # TODO: Utility Functionalize
def execute(self, context):
target_meshes, root_objects = _get_target_objects(context) target_meshes, root_objects = _get_target_objects(context)
logger.info(f"Binding SDEF for {len(target_meshes)} objects with mode={self.mode}, skip={self.use_skip}, scale={self.use_scale}")
for r in root_objects: for r in root_objects:
r.mmd_root.use_sdef = True r.mmd_root.use_sdef = True
param = ((None, False, True)[int(self.mode)], self.use_skip, self.use_scale) param = ((None, False, True)[int(self.mode)], self.use_skip, self.use_scale)
count = sum(FnSDEF.bind(i, *param) for i in target_meshes) count = sum(FnSDEF.bind(i, *param) for i in target_meshes)
logger.info(f"Successfully bound SDEF for {count} of {len(target_meshes)} meshes")
self.report({"INFO"}, f"Binded {count} of {len(target_meshes)} selected mesh(es)") self.report({"INFO"}, f"Binded {count} of {len(target_meshes)} selected mesh(es)")
return {"FINISHED"} return {"FINISHED"}
@@ -102,15 +94,13 @@ class UnbindSDEF(Operator):
bl_description = "Unbind MMD SDEF data of selected objects" bl_description = "Unbind MMD SDEF data of selected objects"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: Context) -> Set[str]: # TODO: Utility Functionalize
def execute(self, context):
target_meshes, root_objects = _get_target_objects(context) target_meshes, root_objects = _get_target_objects(context)
logger.info(f"Unbinding SDEF for {len(target_meshes)} objects")
for i in target_meshes: for i in target_meshes:
FnSDEF.unbind(i) FnSDEF.unbind(i)
for r in root_objects: for r in root_objects:
r.mmd_root.use_sdef = False r.mmd_root.use_sdef = False
logger.debug("SDEF unbinding completed")
return {"FINISHED"} return {"FINISHED"}
+266 -36
View File
@@ -1,10 +1,8 @@
# -*- coding: utf-8 -*- # Copyright 2021 MMD Tools authors
# Copyright 2014 MMD Tools authors # This file is part of MMD Tools.
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import csv
import os
from typing import TYPE_CHECKING, cast from typing import TYPE_CHECKING, cast
import bpy import bpy
@@ -14,7 +12,11 @@ from ..core.translations import MMD_DATA_TYPE_TO_HANDLERS, FnTranslations
from ..translations import DictionaryEnum from ..translations import DictionaryEnum
if TYPE_CHECKING: if TYPE_CHECKING:
from ..properties.translations import MMDTranslation, MMDTranslationElement, MMDTranslationElementIndex from ..properties.translations import (
MMDTranslation,
MMDTranslationElement,
MMDTranslationElementIndex,
)
class TranslateMMDModel(bpy.types.Operator): class TranslateMMDModel(bpy.types.Operator):
@@ -77,7 +79,8 @@ class TranslateMMDModel(bpy.types.Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
obj = context.active_object obj = context.active_object
return obj in context.selected_objects and FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
return obj is not None and obj in context.selected_objects and root is not None
def invoke(self, context, event): def invoke(self, context, event):
vm = context.window_manager vm = context.window_manager
@@ -87,7 +90,7 @@ class TranslateMMDModel(bpy.types.Operator):
try: try:
self.__translator = DictionaryEnum.get_translator(self.dictionary) self.__translator = DictionaryEnum.get_translator(self.dictionary)
except Exception as e: except Exception as e:
self.report({"ERROR"}, "Failed to load dictionary: %s" % e) self.report({"ERROR"}, f"Failed to load dictionary: {e}")
return {"CANCELLED"} return {"CANCELLED"}
obj = context.active_object obj = context.active_object
@@ -96,7 +99,7 @@ class TranslateMMDModel(bpy.types.Operator):
if "MMD" in self.modes: if "MMD" in self.modes:
for i in self.types: for i in self.types:
getattr(self, "translate_%s" % i.lower())(rig) getattr(self, f"translate_{i.lower()}")(rig)
if "BLENDER" in self.modes: if "BLENDER" in self.modes:
self.translate_blender_names(rig) self.translate_blender_names(rig)
@@ -104,7 +107,11 @@ class TranslateMMDModel(bpy.types.Operator):
translator = self.__translator translator = self.__translator
txt = translator.save_fails() txt = translator.save_fails()
if translator.fails: if translator.fails:
self.report({"WARNING"}, "Failed to translate %d names, see '%s' in text editor" % (len(translator.fails), txt.name)) self.report(
{"WARNING"},
"Failed to translate %d names, see '%s' in text editor"
% (len(translator.fails), txt.name),
)
return {"FINISHED"} return {"FINISHED"}
def translate(self, name_j, name_e): def translate(self, name_j, name_e):
@@ -130,7 +137,7 @@ class TranslateMMDModel(bpy.types.Operator):
if "DISPLAY" in self.types: if "DISPLAY" in self.types:
g: bpy.types.BoneCollection g: bpy.types.BoneCollection
for g in cast(bpy.types.Armature, rig.armature().data).collections: for g in cast("bpy.types.Armature", rig.armature().data).collections:
g.name = self.translate(g.name, g.name) g.name = self.translate(g.name, g.name)
if "PHYSICS" in self.types: if "PHYSICS" in self.types:
@@ -153,7 +160,9 @@ class TranslateMMDModel(bpy.types.Operator):
comment_text = bpy.data.texts.get(mmd_root.comment_text, None) comment_text = bpy.data.texts.get(mmd_root.comment_text, None)
comment_e_text = bpy.data.texts.get(mmd_root.comment_e_text, None) comment_e_text = bpy.data.texts.get(mmd_root.comment_e_text, None)
if comment_text and comment_e_text: if comment_text and comment_e_text:
comment_e = self.translate(comment_text.as_string(), comment_e_text.as_string()) comment_e = self.translate(
comment_text.as_string(), comment_e_text.as_string(),
)
comment_e_text.from_string(comment_e) comment_e_text.from_string(comment_e)
def translate_bone(self, rig): def translate_bone(self, rig):
@@ -167,7 +176,7 @@ class TranslateMMDModel(bpy.types.Operator):
mmd_root = rig.rootObject().mmd_root mmd_root = rig.rootObject().mmd_root
attr_list = ("group", "vertex", "bone", "uv", "material") attr_list = ("group", "vertex", "bone", "uv", "material")
prefix_list = ("G_", "", "B_", "UV_", "M_") prefix_list = ("G_", "", "B_", "UV_", "M_")
for attr, prefix in zip(attr_list, prefix_list): for attr, prefix in zip(attr_list, prefix_list, strict=False):
for m in getattr(mmd_root, attr + "_morphs", []): for m in getattr(mmd_root, attr + "_morphs", []):
m.name_e = self.translate(m.name, m.name_e) m.name_e = self.translate(m.name, m.name_e)
if not prefix: if not prefix:
@@ -182,7 +191,9 @@ class TranslateMMDModel(bpy.types.Operator):
for m in rig.materials(): for m in rig.materials():
if m is None: if m is None:
continue continue
m.mmd_material.name_e = self.translate(m.mmd_material.name_j, m.mmd_material.name_e) m.mmd_material.name_e = self.translate(
m.mmd_material.name_j, m.mmd_material.name_e,
)
def translate_display(self, rig): def translate_display(self, rig):
mmd_root = rig.rootObject().mmd_root mmd_root = rig.rootObject().mmd_root
@@ -200,10 +211,24 @@ class TranslateMMDModel(bpy.types.Operator):
DEFAULT_SHOW_ROW_COUNT = 20 DEFAULT_SHOW_ROW_COUNT = 20
class MMD_TOOLS_UL_MMDTranslationElementIndex(bpy.types.UIList): class MMD_TOOLS_LOCAL_UL_MMDTranslationElementIndex(bpy.types.UIList):
def draw_item(self, context, layout: bpy.types.UILayout, data, mmd_translation_element_index: "MMDTranslationElementIndex", icon, active_data, active_propname, index: int): def draw_item(
mmd_translation_element: "MMDTranslationElement" = data.translation_elements[mmd_translation_element_index.value] self,
MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type].draw_item(layout, mmd_translation_element, index) context,
layout: bpy.types.UILayout,
data,
mmd_translation_element_index: "MMDTranslationElementIndex",
icon,
active_data,
active_propname,
index: int,
):
mmd_translation_element: MMDTranslationElement = data.translation_elements[
mmd_translation_element_index.value
]
MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type].draw_item(
layout, mmd_translation_element, index,
)
class RestoreMMDDataReferenceOperator(bpy.types.Operator): class RestoreMMDDataReferenceOperator(bpy.types.Operator):
@@ -216,9 +241,15 @@ class RestoreMMDDataReferenceOperator(bpy.types.Operator):
restore_value: bpy.props.StringProperty() restore_value: bpy.props.StringProperty()
def execute(self, context: bpy.types.Context): def execute(self, context: bpy.types.Context):
root_object = FnModel.find_root_object(context.object) root_object = FnModel.find_root_object(context.active_object)
mmd_translation_element_index = root_object.mmd_root.translation.filtered_translation_element_indices[self.index].value mmd_translation_element_index = (
mmd_translation_element = root_object.mmd_root.translation.translation_elements[mmd_translation_element_index] root_object.mmd_root.translation.filtered_translation_element_indices[
self.index
].value
)
mmd_translation_element = root_object.mmd_root.translation.translation_elements[
mmd_translation_element_index
]
setattr(mmd_translation_element, self.prop_name, self.restore_value) setattr(mmd_translation_element, self.prop_name, self.restore_value)
return {"FINISHED"} return {"FINISHED"}
@@ -231,7 +262,8 @@ class GlobalTranslationPopup(bpy.types.Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return FnModel.find_root_object(context.object) is not None root = FnModel.find_root_object(context.active_object)
return root is not None
def draw(self, _context): def draw(self, _context):
layout = self.layout layout = self.layout
@@ -244,13 +276,33 @@ class GlobalTranslationPopup(bpy.types.Operator):
group = row.row(align=True, heading="is Blank:") group = row.row(align=True, heading="is Blank:")
group.alignment = "RIGHT" group.alignment = "RIGHT"
group.prop(mmd_translation, "filter_japanese_blank", toggle=True, text="Japanese") group.prop(
mmd_translation, "filter_japanese_blank", toggle=True, text="Japanese",
)
group.prop(mmd_translation, "filter_english_blank", toggle=True, text="English") group.prop(mmd_translation, "filter_english_blank", toggle=True, text="English")
group = row.row(align=True) group = row.row(align=True)
group.prop(mmd_translation, "filter_restorable", toggle=True, icon="FILE_REFRESH", icon_only=True) group.prop(
group.prop(mmd_translation, "filter_selected", toggle=True, icon="RESTRICT_SELECT_OFF", icon_only=True) mmd_translation,
group.prop(mmd_translation, "filter_visible", toggle=True, icon="HIDE_OFF", icon_only=True) "filter_restorable",
toggle=True,
icon="FILE_REFRESH",
icon_only=True,
)
group.prop(
mmd_translation,
"filter_selected",
toggle=True,
icon="RESTRICT_SELECT_OFF",
icon_only=True,
)
group.prop(
mmd_translation,
"filter_visible",
toggle=True,
icon="HIDE_OFF",
icon_only=True,
)
col = layout.column(align=True) col = layout.column(align=True)
box = col.box().column(align=True) box = col.box().column(align=True)
@@ -262,11 +314,14 @@ class GlobalTranslationPopup(bpy.types.Operator):
row.label(text="", icon="RESTRICT_SELECT_OFF") row.label(text="", icon="RESTRICT_SELECT_OFF")
row.label(text="", icon="HIDE_OFF") row.label(text="", icon="HIDE_OFF")
if len(mmd_translation.filtered_translation_element_indices) > DEFAULT_SHOW_ROW_COUNT: if (
len(mmd_translation.filtered_translation_element_indices)
> DEFAULT_SHOW_ROW_COUNT
):
row.label(text="", icon="BLANK1") row.label(text="", icon="BLANK1")
col.template_list( col.template_list(
"MMD_TOOLS_UL_MMDTranslationElementIndex", "mmd_tools_UL_MMDTranslationElementIndex",
"", "",
mmd_translation, mmd_translation,
"filtered_translation_element_indices", "filtered_translation_element_indices",
@@ -281,7 +336,12 @@ class GlobalTranslationPopup(bpy.types.Operator):
box.separator() box.separator()
row = box.row() row = box.row()
row.prop(mmd_translation, "batch_operation_script_preset", text="Preset", icon="CON_TRANSFORM_CACHE") row.prop(
mmd_translation,
"batch_operation_script_preset",
text="Preset",
icon="CON_TRANSFORM_CACHE",
)
row.operator(ExecuteTranslationBatchOperator.bl_idname, text="Execute") row.operator(ExecuteTranslationBatchOperator.bl_idname, text="Execute")
box.separator() box.separator()
@@ -289,18 +349,25 @@ class GlobalTranslationPopup(bpy.types.Operator):
translation_box.label(text="Dictionaries:", icon="HELP") translation_box.label(text="Dictionaries:", icon="HELP")
row = translation_box.row() row = translation_box.row()
row.prop(mmd_translation, "dictionary", text="to_english") row.prop(mmd_translation, "dictionary", text="to_english")
# row.operator(ExecuteTranslationScriptOperator.bl_idname, text='Write to .csv')
translation_box.separator() translation_box.separator()
row = translation_box.row() row = translation_box.row()
row.prop(mmd_translation, "dictionary", text="replace") row.prop(mmd_translation, "dictionary", text="replace")
# CSV import/export
box.separator()
translation_box = box.box().column(align=True)
translation_box.label(text="CSV:", icon="FILE_TEXT")
row = translation_box.row()
row.operator(ImportTranslationCSVOperator.bl_idname, text="Import CSV")
row.operator(ExportTranslationCSVOperator.bl_idname, text="Export CSV")
def invoke(self, context: bpy.types.Context, _event): def invoke(self, context: bpy.types.Context, _event):
root_object = FnModel.find_root_object(context.object) root_object = FnModel.find_root_object(context.active_object)
if root_object is None: if root_object is None:
return {"CANCELLED"} return {"CANCELLED"}
mmd_translation: "MMDTranslation" = root_object.mmd_root.translation mmd_translation: MMDTranslation = root_object.mmd_root.translation
self._mmd_translation = mmd_translation self._mmd_translation = mmd_translation
FnTranslations.clear_data(mmd_translation) FnTranslations.clear_data(mmd_translation)
FnTranslations.collect_data(mmd_translation) FnTranslations.collect_data(mmd_translation)
@@ -309,7 +376,7 @@ class GlobalTranslationPopup(bpy.types.Operator):
return context.window_manager.invoke_props_dialog(self, width=800) return context.window_manager.invoke_props_dialog(self, width=800)
def execute(self, context): def execute(self, context):
root_object = FnModel.find_root_object(context.object) root_object = FnModel.find_root_object(context.active_object)
if root_object is None: if root_object is None:
return {"CANCELLED"} return {"CANCELLED"}
@@ -325,12 +392,175 @@ class ExecuteTranslationBatchOperator(bpy.types.Operator):
bl_options = {"INTERNAL"} bl_options = {"INTERNAL"}
def execute(self, context: bpy.types.Context): def execute(self, context: bpy.types.Context):
root = FnModel.find_root_object(context.object) root = FnModel.find_root_object(context.active_object)
if root is None: if root is None:
return {"CANCELLED"} return {"CANCELLED"}
fails, text = FnTranslations.execute_translation_batch(root) fails, text = FnTranslations.execute_translation_batch(root)
if fails: if fails:
self.report({"WARNING"}, "Failed to translate %d names, see '%s' in text editor" % (len(fails), text.name)) self.report(
{"WARNING"},
"Failed to translate %d names, see '%s' in text editor"
% (len(fails), text.name),
)
return {"FINISHED"} return {"FINISHED"}
class ExportTranslationCSVOperator(bpy.types.Operator):
bl_idname = "mmd_tools.export_translation_csv"
bl_description = "Export CSV for external translation."
bl_label = "Export Translation CSV"
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
filename_ext = ".csv"
filepath: bpy.props.StringProperty(
name="File Path",
description="Path to save the translation CSV",
subtype="FILE_PATH",
default="mmd_translation.csv",
)
def _ensure_csv_extension(self):
"""Ensure the file path ends with a .csv extension (case-insensitive)."""
if not self.filepath.lower().endswith(".csv"):
self.filepath = bpy.path.ensure_ext(self.filepath, ".csv")
def invoke(self, context, event):
self._ensure_csv_extension()
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
self._ensure_csv_extension()
root_object = FnModel.find_root_object(context.active_object)
if root_object is None:
self.report({"ERROR"}, "Root object not found")
return {"CANCELLED"}
mmd_translation = root_object.mmd_root.translation
try:
with open(self.filepath, "w", newline="", encoding="utf-8") as csvfile:
writer = csv.writer(csvfile)
writer.writerow(["type", "blender", "japanese", "english"])
for idx in mmd_translation.filtered_translation_element_indices:
element = mmd_translation.translation_elements[idx.value]
writer.writerow(
[element.type, element.name, element.name_j, element.name_e],
)
except Exception as e:
self.report({"ERROR"}, f"Failed to write CSV: {e}")
return {"CANCELLED"}
self.report({"INFO"}, f"Exported to {os.path.basename(self.filepath)}")
return {"FINISHED"}
class ImportTranslationCSVOperator(bpy.types.Operator):
bl_idname = "mmd_tools.import_translation_csv"
bl_description = "Import translated CSV."
bl_label = "Import Translation CSV"
only_update_english_name: bpy.props.BoolProperty(
name="Only Update English Name",
description="(Enabled by default) Only update English name (name_e). otherwise, update all names when different",
default=True,
)
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
filepath: bpy.props.StringProperty(
name="File Path",
description="Path to import the translation CSV",
subtype="FILE_PATH",
default="*.csv",
)
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
root_object = FnModel.find_root_object(context.active_object)
if root_object is None:
self.report({"ERROR"}, "Root object not found")
return {"CANCELLED"}
mmd_translation = root_object.mmd_root.translation
updated_count = 0
warnings = []
try:
with open(self.filepath, encoding="utf-8") as csvfile:
reader = csv.DictReader(csvfile)
required_headers = {"blender", "japanese", "english"}
if not required_headers.issubset(set(reader.fieldnames or [])):
missing = required_headers - set(reader.fieldnames or [])
self.report(
{"ERROR"},
f"Missing required headers in CSV: {', '.join(missing)}",
)
return {"CANCELLED"}
visible_indices = [
i.value
for i in mmd_translation.filtered_translation_element_indices
]
translation_elements_list = list(mmd_translation.translation_elements)
row_count = 0
for row in reader:
if row_count >= len(visible_indices):
row_count += 1
continue
element = translation_elements_list[visible_indices[row_count]]
b_name = row.get("blender", "").strip()
j_name = row.get("japanese", "").strip()
e_name = row.get("english", "").strip()
updated = False
if self.only_update_english_name:
if element.name_e != e_name:
element.name_e = e_name
updated = True
else:
if element.name != b_name:
element.name = b_name
updated = True
if element.name_j != j_name:
element.name_j = j_name
updated = True
if element.name_e != e_name:
element.name_e = e_name
updated = True
if updated:
updated_count += 1
row_count += 1
# Output warnings
if row_count > len(visible_indices):
warnings.append(
f"{row_count - len(visible_indices)} extra lines in CSV! (ignored)",
)
elif row_count < len(visible_indices):
warnings.append(
f"{len(visible_indices) - row_count} missing lines in CSV! (aborted translation)",
)
except Exception as e:
self.report({"ERROR"}, f"Failed to read CSV: {e}")
return {"CANCELLED"}
FnTranslations.update_query(mmd_translation)
msg = f"Imported {updated_count} entries from CSV"
if warnings:
for w in warnings:
self.report({"WARNING"}, w)
msg += " with warnings"
self.report({"INFO"}, msg)
return {"FINISHED"}
+39 -50
View File
@@ -1,49 +1,43 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors # Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender # This file is part of MMD Tools.
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import re import re
from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable, Union, Type, Iterator
from bpy.types import Operator, Context from bpy.types import Operator
from mathutils import Matrix, Vector, Quaternion from mathutils import Matrix, Quaternion
from ...logging_setup import logger
class _SetShadingBase: class _SetShadingBase:
bl_options: Set[str] = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@staticmethod @staticmethod
def _get_view3d_spaces(context: Context) -> Iterator[Any]: def _get_view3d_spaces(context):
if getattr(context.area, "type", None) == "VIEW_3D": if getattr(context.area, "type", None) == "VIEW_3D":
return (context.area.spaces[0],) return (context.area.spaces[0],)
return (area.spaces[0] for area in getattr(context.screen, "areas", ()) if area.type == "VIEW_3D") return (area.spaces[0] for area in getattr(context.screen, "areas", ()) if area.type == "VIEW_3D")
@staticmethod @staticmethod
def _reset_color_management(context: Context, use_display_device: bool = True) -> None: def _reset_color_management(context, use_display_device=True):
try: try:
context.scene.display_settings.display_device = ("None", "sRGB")[use_display_device] context.scene.display_settings.display_device = ("None", "sRGB")[use_display_device]
except TypeError: except TypeError:
pass pass
@staticmethod @staticmethod
def _reset_material_shading(context: Context, use_shadeless: bool = False) -> None: def _reset_material_shading(context, use_shadeless=False):
for i in (x for x in context.scene.objects if x.type == "MESH" and x.mmd_type == "NONE"): for i in (x for x in context.scene.objects if x.type == "MESH" and x.mmd_type == "NONE"):
for s in i.material_slots: for s in i.material_slots:
if s.material is None: if s.material is None:
continue continue
# use_nodes is deprecated in 5.0 but harmless to set
s.material.use_nodes = False s.material.use_nodes = False
s.material.use_shadeless = use_shadeless s.material.use_shadeless = use_shadeless
def execute(self, context: Context) -> Dict[str, str]: def execute(self, context):
context.scene.render.engine = "BLENDER_EEVEE_NEXT" # Changed from BLENDER_EEVEE_NEXT to BLENDER_EEVEE for Blender 5.0
logger.debug(f"Setting render engine to BLENDER_EEVEE_NEXT") context.scene.render.engine = "BLENDER_EEVEE"
shading_mode: Optional[str] = getattr(self, "_shading_mode", None) shading_mode = getattr(self, "_shading_mode", None)
for space in self._get_view3d_spaces(context): for space in self._get_view3d_spaces(context):
shading = space.shading shading = space.shading
shading.type = "SOLID" shading.type = "SOLID"
@@ -51,40 +45,39 @@ class _SetShadingBase:
shading.color_type = "TEXTURE" if shading_mode else "MATERIAL" shading.color_type = "TEXTURE" if shading_mode else "MATERIAL"
shading.show_object_outline = False shading.show_object_outline = False
shading.show_backface_culling = False shading.show_backface_culling = False
logger.debug(f"Applied shading mode: {shading_mode or 'DEFAULT'}")
return {"FINISHED"} return {"FINISHED"}
class SetGLSLShading(Operator, _SetShadingBase): class SetGLSLShading(Operator, _SetShadingBase):
bl_idname: str = "mmd_tools.set_glsl_shading" bl_idname = "mmd_tools.set_glsl_shading"
bl_label: str = "GLSL View" bl_label = "GLSL View"
bl_description: str = "Use GLSL shading with additional lighting" bl_description = "Use GLSL shading with additional lighting"
_shading_mode: str = "GLSL" _shading_mode = "GLSL"
class SetShadelessGLSLShading(Operator, _SetShadingBase): class SetShadelessGLSLShading(Operator, _SetShadingBase):
bl_idname: str = "mmd_tools.set_shadeless_glsl_shading" bl_idname = "mmd_tools.set_shadeless_glsl_shading"
bl_label: str = "Shadeless GLSL View" bl_label = "Shadeless GLSL View"
bl_description: str = "Use only toon shading" bl_description = "Use only toon shading"
_shading_mode: str = "SHADELESS" _shading_mode = "SHADELESS"
class ResetShading(Operator, _SetShadingBase): class ResetShading(Operator, _SetShadingBase):
bl_idname: str = "mmd_tools.reset_shading" bl_idname = "mmd_tools.reset_shading"
bl_label: str = "Reset View" bl_label = "Reset View"
bl_description: str = "Reset to default Blender shading" bl_description = "Reset to default Blender shading"
class FlipPose(Operator): class FlipPose(Operator):
bl_idname: str = "mmd_tools.flip_pose" bl_idname = "mmd_tools.flip_pose"
bl_label: str = "Flip Pose" bl_label = "Flip Pose"
bl_description: str = "Apply the current pose of selected bones to matching bone on opposite side of X-Axis." bl_description = "Apply the current pose of selected bones to matching bone on opposite side of X-Axis."
bl_options: Set[str] = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
# https://docs.blender.org/manual/en/dev/rigging/armatures/bones/editing/naming.html # https://docs.blender.org/manual/en/dev/rigging/armatures/bones/editing/naming.html
__LR_REGEX: List[Dict[str, Any]] = [ __LR_REGEX = [
{"re": re.compile(r"^(.+)(RIGHT|LEFT)(\.\d+)?$", re.IGNORECASE), "lr": 1}, {"re": re.compile(r"^(.+)(RIGHT|LEFT)(\.\d+)?$", re.IGNORECASE), "lr": 1},
{"re": re.compile(r"^(.+)([\.\- _])(L|R)(\.\d+)?$", re.IGNORECASE), "lr": 2}, {"re": re.compile(r"^(.+)([\.\- _])(L|R)(\.\d+)?$", re.IGNORECASE), "lr": 2},
{"re": re.compile(r"^(LEFT|RIGHT)(.+)$", re.IGNORECASE), "lr": 0}, {"re": re.compile(r"^(LEFT|RIGHT)(.+)$", re.IGNORECASE), "lr": 0},
@@ -92,7 +85,7 @@ class FlipPose(Operator):
{"re": re.compile(r"^(.+)(左|右)(\.\d+)?$"), "lr": 1}, {"re": re.compile(r"^(.+)(左|右)(\.\d+)?$"), "lr": 1},
{"re": re.compile(r"^(左|右)(.+)$"), "lr": 0}, {"re": re.compile(r"^(左|右)(.+)$"), "lr": 0},
] ]
__LR_MAP: Dict[str, str] = { __LR_MAP = {
"RIGHT": "LEFT", "RIGHT": "LEFT",
"Right": "Left", "Right": "Left",
"right": "left", "right": "left",
@@ -108,7 +101,7 @@ class FlipPose(Operator):
} }
@classmethod @classmethod
def flip_name(cls, name: str) -> str: def flip_name(cls, name):
for regex in cls.__LR_REGEX: for regex in cls.__LR_REGEX:
match = regex["re"].match(name) match = regex["re"].match(name)
if match: if match:
@@ -126,15 +119,15 @@ class FlipPose(Operator):
return "" return ""
@staticmethod @staticmethod
def __cmul(vec1: Union[Vector, Quaternion], vec2: Tuple[float, float, float, float]) -> Union[Vector, Quaternion]: def __cmul(vec1, vec2):
return type(vec1)([x * y for x, y in zip(vec1, vec2)]) return type(vec1)([x * y for x, y in zip(vec1, vec2, strict=False)])
@staticmethod @staticmethod
def __matrix_compose(loc: Vector, rot: Quaternion, scale: Vector) -> Matrix: def __matrix_compose(loc, rot, scale):
return (Matrix.Translation(loc) @ rot.to_matrix().to_4x4()) @ Matrix([(scale[0], 0, 0, 0), (0, scale[1], 0, 0), (0, 0, scale[2], 0), (0, 0, 0, 1)]) return (Matrix.Translation(loc) @ rot.to_matrix().to_4x4()) @ Matrix([(scale[0], 0, 0, 0), (0, scale[1], 0, 0), (0, 0, scale[2], 0), (0, 0, 0, 1)])
@classmethod @classmethod
def __flip_pose(cls, matrix_basis: Matrix, bone_src: Any, bone_dest: Any) -> None: def __flip_pose(cls, matrix_basis, bone_src, bone_dest):
m = bone_dest.bone.matrix_local.to_3x3().transposed() m = bone_dest.bone.matrix_local.to_3x3().transposed()
mi = bone_src.bone.matrix_local.to_3x3().transposed().inverted() if bone_src != bone_dest else m.inverted() mi = bone_src.bone.matrix_local.to_3x3().transposed().inverted() if bone_src != bone_dest else m.inverted()
loc, rot, scale = matrix_basis.decompose() loc, rot, scale = matrix_basis.decompose()
@@ -143,16 +136,12 @@ class FlipPose(Operator):
bone_dest.matrix_basis = cls.__matrix_compose(m @ loc, Quaternion(m @ rot.axis, rot.angle).normalized(), scale) bone_dest.matrix_basis = cls.__matrix_compose(m @ loc, Quaternion(m @ rot.axis, rot.angle).normalized(), scale)
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context):
return context.active_object and context.active_object.type == "ARMATURE" and context.active_object.mode == "POSE" obj = context.active_object
return obj is not None and obj.type == "ARMATURE" and obj.mode == "POSE"
def execute(self, context: Context) -> Dict[str, str]: def execute(self, context):
logger.info("Executing flip pose operation")
pose_bones = context.active_object.pose.bones pose_bones = context.active_object.pose.bones
for b, mat in [(x, x.matrix_basis.copy()) for x in context.selected_pose_bones]: for b, mat in [(x, x.matrix_basis.copy()) for x in context.selected_pose_bones]:
flip_name = self.flip_name(b.name) self.__flip_pose(mat, b, pose_bones.get(self.flip_name(b.name), b))
target_bone = pose_bones.get(flip_name, b)
logger.debug(f"Flipping pose from {b.name} to {target_bone.name}")
self.__flip_pose(mat, b, target_bone)
logger.info("Flip pose operation completed")
return {"FINISHED"} return {"FINISHED"}
+20 -30
View File
@@ -1,90 +1,82 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors # Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender # This file is part of MMD Tools.
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import bpy import bpy
from typing import Optional, Set, Dict, Any, List, Tuple, Union, Type
from .. import utils from .. import utils
from ..core import material from ..core import material
from ..core.material import FnMaterial from ..core.material import FnMaterial
from ..core.model import FnModel from ..core.model import FnModel
from . import patch_library_overridable from . import patch_library_overridable
from ....core.logging_setup import logger
def _mmd_material_update_ambient_color(prop: "MMDMaterial", _context: bpy.types.Context) -> None: def _mmd_material_update_ambient_color(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_ambient_color() FnMaterial(prop.id_data).update_ambient_color()
def _mmd_material_update_diffuse_color(prop: "MMDMaterial", _context: bpy.types.Context) -> None: def _mmd_material_update_diffuse_color(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_diffuse_color() FnMaterial(prop.id_data).update_diffuse_color()
def _mmd_material_update_alpha(prop: "MMDMaterial", _context: bpy.types.Context) -> None: def _mmd_material_update_alpha(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_alpha() FnMaterial(prop.id_data).update_alpha()
def _mmd_material_update_specular_color(prop: "MMDMaterial", _context: bpy.types.Context) -> None: def _mmd_material_update_specular_color(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_specular_color() FnMaterial(prop.id_data).update_specular_color()
def _mmd_material_update_shininess(prop: "MMDMaterial", _context: bpy.types.Context) -> None: def _mmd_material_update_shininess(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_shininess() FnMaterial(prop.id_data).update_shininess()
def _mmd_material_update_is_double_sided(prop: "MMDMaterial", _context: bpy.types.Context) -> None: def _mmd_material_update_is_double_sided(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_is_double_sided() FnMaterial(prop.id_data).update_is_double_sided()
def _mmd_material_update_sphere_texture_type(prop: "MMDMaterial", context: bpy.types.Context) -> None: def _mmd_material_update_sphere_texture_type(prop: "MMDMaterial", context):
FnMaterial(prop.id_data).update_sphere_texture_type(context.active_object) FnMaterial(prop.id_data).update_sphere_texture_type(context.active_object)
def _mmd_material_update_toon_texture(prop: "MMDMaterial", _context: bpy.types.Context) -> None: def _mmd_material_update_toon_texture(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_toon_texture() FnMaterial(prop.id_data).update_toon_texture()
def _mmd_material_update_enabled_drop_shadow(prop: "MMDMaterial", _context: bpy.types.Context) -> None: def _mmd_material_update_enabled_drop_shadow(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_drop_shadow() FnMaterial(prop.id_data).update_drop_shadow()
def _mmd_material_update_enabled_self_shadow_map(prop: "MMDMaterial", _context: bpy.types.Context) -> None: def _mmd_material_update_enabled_self_shadow_map(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_self_shadow_map() FnMaterial(prop.id_data).update_self_shadow_map()
def _mmd_material_update_enabled_self_shadow(prop: "MMDMaterial", _context: bpy.types.Context) -> None: def _mmd_material_update_enabled_self_shadow(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_self_shadow() FnMaterial(prop.id_data).update_self_shadow()
def _mmd_material_update_enabled_toon_edge(prop: "MMDMaterial", _context: bpy.types.Context) -> None: def _mmd_material_update_enabled_toon_edge(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_enabled_toon_edge() FnMaterial(prop.id_data).update_enabled_toon_edge()
def _mmd_material_update_edge_color(prop: "MMDMaterial", _context: bpy.types.Context) -> None: def _mmd_material_update_edge_color(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_edge_color() FnMaterial(prop.id_data).update_edge_color()
def _mmd_material_update_edge_weight(prop: "MMDMaterial", _context: bpy.types.Context) -> None: def _mmd_material_update_edge_weight(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_edge_weight() FnMaterial(prop.id_data).update_edge_weight()
def _mmd_material_get_name_j(prop: "MMDMaterial") -> str: def _mmd_material_get_name_j(prop: "MMDMaterial"):
return prop.get("name_j", "") return prop.get("name_j", "")
def _mmd_material_set_name_j(prop: "MMDMaterial", value: str) -> None: def _mmd_material_set_name_j(prop: "MMDMaterial", value: str):
prop_value = value prop_value = value
if prop_value and prop_value != prop.get("name_j"): if prop_value and prop_value != prop.get("name_j"):
root = FnModel.find_root_object(bpy.context.active_object) root = FnModel.find_root_object(bpy.context.active_object)
if root is None: if root is None:
logger.debug(f"No root object found, using unique name for material: {value}")
prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in bpy.data.materials}) prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in bpy.data.materials})
else: else:
logger.debug(f"Root object found, using unique name for material within model: {value}")
prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in FnModel.iterate_materials(root)}) prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in FnModel.iterate_materials(root)})
prop["name_j"] = prop_value prop["name_j"] = prop_value
@@ -279,15 +271,13 @@ class MMDMaterial(bpy.types.PropertyGroup):
description="Comment", description="Comment",
) )
def is_id_unique(self) -> bool: def is_id_unique(self):
return self.material_id < 0 or not next((m for m in bpy.data.materials if m.mmd_material != self and m.mmd_material.material_id == self.material_id), None) return self.material_id < 0 or not next((m for m in bpy.data.materials if m.mmd_material != self and m.mmd_material.material_id == self.material_id), None)
@staticmethod @staticmethod
def register() -> None: def register():
logger.debug("Registering MMD material properties")
bpy.types.Material.mmd_material = patch_library_overridable(bpy.props.PointerProperty(type=MMDMaterial)) bpy.types.Material.mmd_material = patch_library_overridable(bpy.props.PointerProperty(type=MMDMaterial))
@staticmethod @staticmethod
def unregister() -> None: def unregister():
logger.debug("Unregistering MMD material properties")
del bpy.types.Material.mmd_material del bpy.types.Material.mmd_material
+49 -58
View File
@@ -1,38 +1,34 @@
# -*- coding: utf-8 -*- # Copyright 2015 MMD Tools authors
# Copyright 2014 MMD Tools authors # This file is part of MMD Tools.
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import bpy import bpy
from typing import Optional, List, Dict, Any, Set, Tuple, Union, TypeVar, Type
from bpy.types import PropertyGroup, Object, ShapeKey
from .. import utils from .. import utils
from ..core.bone import FnBone from ..core.bone import FnBone
from ..core.material import FnMaterial from ..core.material import FnMaterial
from ..core.model import FnModel, Model from ..core.model import FnModel, Model
from ..core.morph import FnMorph from ..core.morph import FnMorph
from ....core.logging_setup import logger
def _morph_base_get_name(prop: "_MorphBase") -> str: def _morph_base_get_name(prop: "_MorphBase") -> str:
return prop.get("name", "") return prop.get("name", "")
def _morph_base_set_name(prop: "_MorphBase", value: str) -> None: def _morph_base_set_name(prop: "_MorphBase", value: str):
mmd_root = prop.id_data.mmd_root mmd_root = prop.id_data.mmd_root
morph_type = "%s_morphs" % prop.bl_rna.identifier[:-5].lower() # morph_type = mmd_root.active_morph_type
morph_type = f"{prop.bl_rna.identifier[:-5].lower()}_morphs"
# assert(prop.bl_rna.identifier.endswith('Morph'))
# logging.debug('_set_name: %s %s %s', prop, value, morph_type)
prop_name = prop.get("name", None) prop_name = prop.get("name", None)
if prop_name == value: if prop_name == value:
return return
used_names: Set[str] = {x.name for x in getattr(mmd_root, morph_type) if x != prop} used_names = {x.name for x in getattr(mmd_root, morph_type) if x != prop}
value = utils.unique_name(value, used_names) value = utils.unique_name(value, used_names)
if prop_name is not None: if prop_name is not None:
if morph_type == "vertex_morphs": if morph_type == "vertex_morphs":
kb_list: Dict[str, List[ShapeKey]] = {} kb_list = {}
for mesh in FnModel.iterate_mesh_objects(prop.id_data): for mesh in FnModel.iterate_mesh_objects(prop.id_data):
for kb in getattr(mesh.data.shape_keys, "key_blocks", ()): for kb in getattr(mesh.data.shape_keys, "key_blocks", ()):
kb_list.setdefault(kb.name, []).append(kb) kb_list.setdefault(kb.name, []).append(kb)
@@ -43,7 +39,7 @@ def _morph_base_set_name(prop: "_MorphBase", value: str) -> None:
kb.name = value kb.name = value
elif morph_type == "uv_morphs": elif morph_type == "uv_morphs":
vg_list: Dict[str, List[Any]] = {} vg_list = {}
for mesh in FnModel.iterate_mesh_objects(prop.id_data): for mesh in FnModel.iterate_mesh_objects(prop.id_data):
for vg, n, x in FnMorph.get_uv_morph_vertex_groups(mesh): for vg, n, x in FnMorph.get_uv_morph_vertex_groups(mesh):
vg_list.setdefault(n, []).append(vg) vg_list.setdefault(n, []).append(vg)
@@ -72,7 +68,6 @@ def _morph_base_set_name(prop: "_MorphBase", value: str) -> None:
kb.name = value kb.name = value
prop["name"] = value prop["name"] = value
logger.debug(f"Renamed morph from '{prop_name}' to '{value}'")
class _MorphBase: class _MorphBase:
@@ -101,12 +96,16 @@ class _MorphBase:
) )
def _bone_morph_data_update_bone_id(prop: "BoneMorphData", context: bpy.types.Context):
pass # Empty function is sufficient to trigger UI update
def _bone_morph_data_get_bone(prop: "BoneMorphData") -> str: def _bone_morph_data_get_bone(prop: "BoneMorphData") -> str:
bone_id: int = prop.get("bone_id", -1) bone_id = prop.get("bone_id", -1)
if bone_id < 0: if bone_id < 0:
return "" return ""
root_object: Object = prop.id_data root_object = prop.id_data
armature_object: Optional[Object] = FnModel.find_armature_object(root_object) armature_object = FnModel.find_armature_object(root_object)
if armature_object is None: if armature_object is None:
return "" return ""
pose_bone = FnBone.find_pose_bone_by_bone_id(armature_object, bone_id) pose_bone = FnBone.find_pose_bone_by_bone_id(armature_object, bone_id)
@@ -115,9 +114,9 @@ def _bone_morph_data_get_bone(prop: "BoneMorphData") -> str:
return pose_bone.name return pose_bone.name
def _bone_morph_data_set_bone(prop: "BoneMorphData", value: str) -> None: def _bone_morph_data_set_bone(prop: "BoneMorphData", value: str):
root: Object = prop.id_data root = prop.id_data
arm: Optional[Object] = FnModel.find_armature_object(root) arm = FnModel.find_armature_object(root)
# Load the library_override file. This function is triggered when loading, but the arm obj cannot be found. # Load the library_override file. This function is triggered when loading, but the arm obj cannot be found.
# The arm obj is exist, but the relative relationship has not yet been established. # The arm obj is exist, but the relative relationship has not yet been established.
@@ -125,14 +124,13 @@ def _bone_morph_data_set_bone(prop: "BoneMorphData", value: str) -> None:
return return
if value not in arm.pose.bones.keys(): if value not in arm.pose.bones.keys():
prop["bone_id"] = -1 prop.bone_id = -1
return return
pose_bone = arm.pose.bones[value] pose_bone = arm.pose.bones[value]
prop["bone_id"] = FnBone.get_or_assign_bone_id(pose_bone) prop.bone_id = FnBone.get_or_assign_bone_id(pose_bone)
logger.debug(f"Set bone morph data bone to '{value}' with ID {prop['bone_id']}")
def _bone_morph_data_update_location_or_rotation(prop: "BoneMorphData", _context: bpy.types.Context) -> None: def _bone_morph_data_update_location_or_rotation(prop: "BoneMorphData", _context):
if not prop.name.startswith("mmd_bind"): if not prop.name.startswith("mmd_bind"):
return return
arm = FnModel(prop.id_data).morph_slider.dummy_armature arm = FnModel(prop.id_data).morph_slider.dummy_armature
@@ -141,12 +139,9 @@ def _bone_morph_data_update_location_or_rotation(prop: "BoneMorphData", _context
if bone: if bone:
bone.location = prop.location bone.location = prop.location
bone.rotation_quaternion = prop.rotation.__class__(*prop.rotation.to_axis_angle()) # Fix for consistency bone.rotation_quaternion = prop.rotation.__class__(*prop.rotation.to_axis_angle()) # Fix for consistency
logger.debug(f"Updated bone morph data location/rotation for '{prop.name}'")
class BoneMorphData(bpy.types.PropertyGroup): class BoneMorphData(bpy.types.PropertyGroup):
""" """
bone: bpy.props.StringProperty( bone: bpy.props.StringProperty(
name="Bone", name="Bone",
description="Target bone", description="Target bone",
@@ -156,6 +151,7 @@ class BoneMorphData(bpy.types.PropertyGroup):
bone_id: bpy.props.IntProperty( bone_id: bpy.props.IntProperty(
name="Bone ID", name="Bone ID",
update=_bone_morph_data_update_bone_id,
) )
location: bpy.props.FloatVectorProperty( location: bpy.props.FloatVectorProperty(
@@ -191,61 +187,53 @@ class BoneMorph(_MorphBase, bpy.types.PropertyGroup):
) )
def _material_morph_data_get_material(prop: "MaterialMorphData") -> str: def _material_morph_data_get_material(prop: "MaterialMorphData"):
mat_p = prop.get("material_data", None) mat_data = prop.get("material_data", None)
if mat_p is not None: if mat_data is not None:
return mat_p.name return mat_data.name
return "" return ""
def _material_morph_data_set_material(prop: "MaterialMorphData", value: str) -> None: def _material_morph_data_set_material(prop: "MaterialMorphData", value: str):
if value not in bpy.data.materials: if value not in bpy.data.materials:
prop["material_data"] = None prop.material_data = None
prop["material_id"] = -1 prop.material_id = -1
logger.debug(f"Material '{value}' not found, setting material_data to None")
else: else:
mat = bpy.data.materials[value] mat = bpy.data.materials[value]
fnMat = FnMaterial(mat) fnMat = FnMaterial(mat)
prop["material_data"] = mat prop.material_data = mat
prop["material_id"] = fnMat.material_id prop.material_id = fnMat.material_id
logger.debug(f"Set material morph data material to '{value}' with ID {fnMat.material_id}")
def _material_morph_data_set_related_mesh(prop: "MaterialMorphData", value: str) -> None: def _material_morph_data_set_related_mesh(prop: "MaterialMorphData", value: str):
mesh = FnModel.find_mesh_object_by_name(prop.id_data, value) mesh = FnModel.find_mesh_object_by_name(prop.id_data, value)
if mesh is not None: if mesh is not None:
prop["related_mesh_data"] = mesh.data prop.related_mesh_data = mesh.data
logger.debug(f"Set material morph data related mesh to '{value}'")
else: else:
prop["related_mesh_data"] = None prop.related_mesh_data = None
logger.debug(f"Mesh '{value}' not found, setting related_mesh_data to None")
def _material_morph_data_get_related_mesh(prop: "MaterialMorphData") -> str: def _material_morph_data_get_related_mesh(prop):
mesh_p = prop.get("related_mesh_data", None) mesh_data = prop.get("related_mesh_data", None)
if mesh_p is not None: if mesh_data is not None:
return mesh_p.name return mesh_data.name
return "" return ""
def _material_morph_data_update_modifiable_values(prop: "MaterialMorphData", _context: bpy.types.Context) -> None: def _material_morph_data_update_modifiable_values(prop: "MaterialMorphData", _context):
if not prop.name.startswith("mmd_bind"): if not prop.name.startswith("mmd_bind"):
return return
from ..core.shader import _MaterialMorph from ..core.shader import _MaterialMorph
mat = prop["material_data"] mat_data = prop.get("material_data", None)
if mat is not None: if mat_data is not None:
_MaterialMorph.update_morph_inputs(mat, prop) _MaterialMorph.update_morph_inputs(mat_data, prop)
logger.debug(f"Updated material morph modifiable values for '{prop.name}'")
else: else:
for mat in FnModel(prop.id_data).materials(): for mat_data in FnModel(prop.id_data).materials():
_MaterialMorph.update_morph_inputs(mat, prop) _MaterialMorph.update_morph_inputs(mat_data, prop)
logger.debug(f"Updated material morph modifiable values for all materials")
class MaterialMorphData(bpy.types.PropertyGroup): class MaterialMorphData(bpy.types.PropertyGroup):
""" """
related_mesh: bpy.props.StringProperty( related_mesh: bpy.props.StringProperty(
name="Related Mesh", name="Related Mesh",
description="Stores a reference to the mesh where this morph data belongs to", description="Stores a reference to the mesh where this morph data belongs to",
@@ -416,6 +404,9 @@ class UVMorphOffset(bpy.types.PropertyGroup):
name="UV Offset", name="UV Offset",
description="UV offset", description="UV offset",
size=4, size=4,
# min=-1,
# max=1,
# precision=3,
step=0.1, step=0.1,
default=[0, 0, 0, 0], default=[0, 0, 0, 0],
) )
+85 -29
View File
@@ -1,37 +1,31 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors # Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender # This file is part of MMD Tools.
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. from typing import cast
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import cast, Optional, Any, Union
import bpy import bpy
from bpy.types import Context, PropertyGroup, PoseBone, Object, Armature
from ..core.bone import FnBone from ..core.bone import FnBone
from . import patch_library_overridable from . import patch_library_overridable
from ....core.logging_setup import logger
def _mmd_bone_update_additional_transform(prop: "MMDBone", context: Context) -> None: def _mmd_bone_update_additional_transform(prop: "MMDBone", context: bpy.types.Context):
prop["is_additional_transform_dirty"] = True prop.is_additional_transform_dirty = True
# Apply additional transform (Assembly -> Bone button) (Very Slow)
p_bone = context.active_pose_bone p_bone = context.active_pose_bone
if p_bone and p_bone.mmd_bone.as_pointer() == prop.as_pointer(): if p_bone and p_bone.mmd_bone.as_pointer() == prop.as_pointer():
logger.debug(f"Applying additional transformation for {p_bone.name}")
FnBone.apply_additional_transformation(prop.id_data) FnBone.apply_additional_transformation(prop.id_data)
def _mmd_bone_update_additional_transform_influence(prop: "MMDBone", context: Context) -> None: def _mmd_bone_update_additional_transform_influence(prop: "MMDBone", context: bpy.types.Context):
pose_bone = context.active_pose_bone pose_bone = context.active_pose_bone
if pose_bone and pose_bone.mmd_bone.as_pointer() == prop.as_pointer(): if pose_bone and pose_bone.mmd_bone.as_pointer() == prop.as_pointer():
logger.debug(f"Updating additional transform influence for {pose_bone.name}")
FnBone.update_additional_transform_influence(pose_bone) FnBone.update_additional_transform_influence(pose_bone)
else: else:
prop["is_additional_transform_dirty"] = True prop.is_additional_transform_dirty = True
def _mmd_bone_get_additional_transform_bone(prop: "MMDBone") -> str: def _mmd_bone_get_additional_transform_bone(prop: "MMDBone"):
arm = prop.id_data arm = prop.id_data
bone_id = prop.get("additional_transform_bone_id", -1) bone_id = prop.get("additional_transform_bone_id", -1)
if bone_id < 0: if bone_id < 0:
@@ -42,17 +36,57 @@ def _mmd_bone_get_additional_transform_bone(prop: "MMDBone") -> str:
return pose_bone.name return pose_bone.name
def _mmd_bone_set_additional_transform_bone(prop: "MMDBone", value: str) -> None: def _mmd_bone_set_additional_transform_bone(prop: "MMDBone", value: str):
arm = prop.id_data arm = prop.id_data
prop["is_additional_transform_dirty"] = True prop.is_additional_transform_dirty = True
if value not in arm.pose.bones.keys(): if value not in arm.pose.bones.keys():
prop["additional_transform_bone_id"] = -1 prop.additional_transform_bone_id = -1
return return
pose_bone = arm.pose.bones[value] pose_bone = arm.pose.bones[value]
prop["additional_transform_bone_id"] = FnBone.get_or_assign_bone_id(pose_bone) target_bone_id = FnBone.get_or_assign_bone_id(pose_bone)
if prop.bone_id == target_bone_id:
prop.additional_transform_bone_id = -1
return
prop.additional_transform_bone_id = target_bone_id
class MMDBone(PropertyGroup): def _mmd_bone_update_display_connection(prop: "MMDBone", context: bpy.types.Context):
pass # Empty function is sufficient to trigger UI update
def _mmd_bone_get_display_connection_bone(prop: "MMDBone"):
arm = prop.id_data
bone_id = prop.get("display_connection_bone_id", -1)
if bone_id < 0:
return ""
pose_bone = FnBone.find_pose_bone_by_bone_id(arm, bone_id)
if pose_bone is None:
return ""
return pose_bone.name
def _mmd_bone_set_display_connection_bone(prop: "MMDBone", value: str):
arm = prop.id_data
if value not in arm.pose.bones.keys():
prop.display_connection_bone_id = -1
return
pose_bone = arm.pose.bones[value]
target_bone_id = FnBone.get_or_assign_bone_id(pose_bone)
if prop.bone_id == target_bone_id:
prop.display_connection_bone_id = -1
return
prop.display_connection_bone_id = target_bone_id
class MMDBone(bpy.types.PropertyGroup):
name_j: bpy.props.StringProperty( name_j: bpy.props.StringProperty(
name="Name", name="Name",
description="Japanese Name", description="Japanese Name",
@@ -188,12 +222,35 @@ class MMDBone(PropertyGroup):
is_additional_transform_dirty: bpy.props.BoolProperty(name="", default=True) is_additional_transform_dirty: bpy.props.BoolProperty(name="", default=True)
def is_id_unique(self) -> bool: display_connection_bone: bpy.props.StringProperty(
name="Display Connection Bone",
description="Target bone for display connection",
set=_mmd_bone_set_display_connection_bone,
get=_mmd_bone_get_display_connection_bone,
)
display_connection_bone_id: bpy.props.IntProperty(
name="Display Connection Bone ID",
description="Bone ID for display connection (PMX displayConnection)",
default=-1,
update=_mmd_bone_update_display_connection,
)
display_connection_type: bpy.props.EnumProperty(
name="Display Connection Type",
description="Type of display connection",
items=[
("BONE", "Bone", "Connected to a bone"),
("OFFSET", "Offset", "Connected to an offset position"),
],
default="OFFSET",
)
def is_id_unique(self):
return self.bone_id < 0 or not next((b for b in self.id_data.pose.bones if b.mmd_bone != self and b.mmd_bone.bone_id == self.bone_id), None) return self.bone_id < 0 or not next((b for b in self.id_data.pose.bones if b.mmd_bone != self and b.mmd_bone.bone_id == self.bone_id), None)
@staticmethod @staticmethod
def register() -> None: def register():
logger.debug("Registering MMDBone properties")
bpy.types.PoseBone.mmd_bone = patch_library_overridable(bpy.props.PointerProperty(type=MMDBone)) bpy.types.PoseBone.mmd_bone = patch_library_overridable(bpy.props.PointerProperty(type=MMDBone))
bpy.types.PoseBone.is_mmd_shadow_bone = patch_library_overridable(bpy.props.BoolProperty(name="is_mmd_shadow_bone", default=False)) bpy.types.PoseBone.is_mmd_shadow_bone = patch_library_overridable(bpy.props.BoolProperty(name="is_mmd_shadow_bone", default=False))
bpy.types.PoseBone.mmd_shadow_bone_type = patch_library_overridable(bpy.props.StringProperty(name="mmd_shadow_bone_type")) bpy.types.PoseBone.mmd_shadow_bone_type = patch_library_overridable(bpy.props.StringProperty(name="mmd_shadow_bone_type"))
@@ -203,25 +260,24 @@ class MMDBone(PropertyGroup):
description="MMD IK toggle is used to import/export animation of IK on-off", description="MMD IK toggle is used to import/export animation of IK on-off",
update=_pose_bone_update_mmd_ik_toggle, update=_pose_bone_update_mmd_ik_toggle,
default=True, default=True,
) ),
) )
@staticmethod @staticmethod
def unregister() -> None: def unregister():
logger.debug("Unregistering MMDBone properties")
del bpy.types.PoseBone.mmd_ik_toggle del bpy.types.PoseBone.mmd_ik_toggle
del bpy.types.PoseBone.mmd_shadow_bone_type del bpy.types.PoseBone.mmd_shadow_bone_type
del bpy.types.PoseBone.is_mmd_shadow_bone del bpy.types.PoseBone.is_mmd_shadow_bone
del bpy.types.PoseBone.mmd_bone del bpy.types.PoseBone.mmd_bone
def _pose_bone_update_mmd_ik_toggle(prop: PoseBone, _context: Any) -> None: def _pose_bone_update_mmd_ik_toggle(prop: bpy.types.PoseBone, _context):
v = prop.mmd_ik_toggle v = prop.mmd_ik_toggle
armature_object = cast(Object, prop.id_data) armature_object = cast("bpy.types.Object", prop.id_data)
for b in armature_object.pose.bones: for b in armature_object.pose.bones:
for c in b.constraints: for c in b.constraints:
if c.type == "IK" and c.subtarget == prop.name: if c.type == "IK" and c.subtarget == prop.name:
logger.debug(f"Updating IK toggle for {b.name} {c.name}") # logging.debug(' %s %s', b.name, c.name)
c.influence = v c.influence = v
b = b if c.use_tail else b.parent b = b if c.use_tail else b.parent
for b in ([b] + b.parent_recursive)[: c.chain_count]: for b in ([b] + b.parent_recursive)[: c.chain_count]:
+34 -43
View File
@@ -1,42 +1,35 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors # Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender # This file is part of MMD Tools.
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
"""Properties for rigid bodies and joints""" """Properties for rigid bodies and joints"""
import bpy import bpy
from typing import Optional, Any, Set, List, Dict, Tuple, Union
from bpy.types import Context, Object, PropertyGroup, Material
from .. import bpyutils from .. import bpyutils
from ..core import rigid_body from ..core import rigid_body
from ..core.rigid_body import RigidBodyMaterial, FnRigidBody
from ..core.model import FnModel from ..core.model import FnModel
from ..core.rigid_body import FnRigidBody, RigidBodyMaterial
from . import patch_library_overridable from . import patch_library_overridable
from ....core.logging_setup import logger
def _updateCollisionGroup(prop: PropertyGroup, _context: Context) -> None: def _updateCollisionGroup(prop, _context):
obj: Object = prop.id_data obj = prop.id_data
materials: List[Material] = obj.data.materials materials = obj.data.materials
if len(materials) == 0: if len(materials) == 0:
materials.append(RigidBodyMaterial.getMaterial(prop.collision_group_number)) materials.append(RigidBodyMaterial.getMaterial(prop.collision_group_number))
else: else:
obj.material_slots[0].material = RigidBodyMaterial.getMaterial(prop.collision_group_number) obj.material_slots[0].material = RigidBodyMaterial.getMaterial(prop.collision_group_number)
def _updateType(prop: PropertyGroup, _context: Context) -> None: def _updateType(prop, _context):
obj: Object = prop.id_data obj = prop.id_data
rb = obj.rigid_body rb = obj.rigid_body
if rb: if rb:
rb.kinematic = int(prop.type) == rigid_body.MODE_STATIC rb.kinematic = int(prop.type) == rigid_body.MODE_STATIC
def _updateShape(prop: PropertyGroup, _context: Context) -> None: def _updateShape(prop, _context):
obj: Object = prop.id_data obj = prop.id_data
if len(obj.data.vertices) > 0: if len(obj.data.vertices) > 0:
size = prop.size size = prop.size
@@ -47,8 +40,8 @@ def _updateShape(prop: PropertyGroup, _context: Context) -> None:
rb.collision_shape = prop.shape rb.collision_shape = prop.shape
def _get_bone(prop: PropertyGroup) -> str: def _get_bone(prop):
obj: Object = prop.id_data obj = prop.id_data
relation = obj.constraints.get("mmd_tools_rigid_parent", None) relation = obj.constraints.get("mmd_tools_rigid_parent", None)
if relation: if relation:
arm = relation.target arm = relation.target
@@ -58,9 +51,9 @@ def _get_bone(prop: PropertyGroup) -> str:
return prop.get("bone", "") return prop.get("bone", "")
def _set_bone(prop: PropertyGroup, value: str) -> None: def _set_bone(prop, value):
bone_name: str = value bone_name = value
obj: Object = prop.id_data obj = prop.id_data
relation = obj.constraints.get("mmd_tools_rigid_parent", None) relation = obj.constraints.get("mmd_tools_rigid_parent", None)
if relation is None: if relation is None:
relation = obj.constraints.new("CHILD_OF") relation = obj.constraints.new("CHILD_OF")
@@ -81,21 +74,24 @@ def _set_bone(prop: PropertyGroup, value: str) -> None:
prop["bone"] = bone_name prop["bone"] = bone_name
def _get_size(prop: PropertyGroup) -> Tuple[float, float, float]: def _get_size(prop):
if prop.id_data.mmd_type != "RIGID_BODY": if prop.id_data.mmd_type != "RIGID_BODY":
return (0, 0, 0) return (0, 0, 0)
return FnRigidBody.get_rigid_body_size(prop.id_data) return FnRigidBody.get_rigid_body_size(prop.id_data)
def _set_size(prop: PropertyGroup, value: Tuple[float, float, float]) -> None: def _set_size(prop, value):
obj: Object = prop.id_data obj = prop.id_data
assert obj.mode == "OBJECT" # not support other mode yet assert obj.mode == "OBJECT" # not support other mode yet
shape: str = prop.shape shape = prop.shape
mesh = obj.data mesh = obj.data
rb = obj.rigid_body rb = obj.rigid_body
if len(mesh.vertices) == 0 or rb is None or rb.collision_shape != shape: current_size = FnRigidBody.get_rigid_body_size(obj)
is_zero_size = all(abs(s) < 1e-6 for s in current_size)
if len(mesh.vertices) == 0 or rb is None or rb.collision_shape != shape or is_zero_size:
if shape == "SPHERE": if shape == "SPHERE":
bpyutils.makeSphere( bpyutils.makeSphere(
radius=value[0], radius=value[0],
@@ -149,15 +145,15 @@ def _set_size(prop: PropertyGroup, value: Tuple[float, float, float]) -> None:
mesh.update() mesh.update()
def _get_rigid_name(prop: PropertyGroup) -> str: def _get_rigid_name(prop):
return prop.get("name", "") return prop.get("name", "")
def _set_rigid_name(prop: PropertyGroup, value: str) -> None: def _set_rigid_name(prop, value):
prop["name"] = value prop["name"] = value
class MMDRigidBody(PropertyGroup): class MMDRigidBody(bpy.types.PropertyGroup):
name_j: bpy.props.StringProperty( name_j: bpy.props.StringProperty(
name="Name", name="Name",
description="Japanese Name", description="Japanese Name",
@@ -230,18 +226,16 @@ class MMDRigidBody(PropertyGroup):
) )
@staticmethod @staticmethod
def register() -> None: def register():
logger.debug("Registering MMDRigidBody property")
bpy.types.Object.mmd_rigid = patch_library_overridable(bpy.props.PointerProperty(type=MMDRigidBody)) bpy.types.Object.mmd_rigid = patch_library_overridable(bpy.props.PointerProperty(type=MMDRigidBody))
@staticmethod @staticmethod
def unregister() -> None: def unregister():
logger.debug("Unregistering MMDRigidBody property")
del bpy.types.Object.mmd_rigid del bpy.types.Object.mmd_rigid
def _updateSpringLinear(prop: PropertyGroup, context: Context) -> None: def _updateSpringLinear(prop, context):
obj: Object = prop.id_data obj = prop.id_data
rbc = obj.rigid_body_constraint rbc = obj.rigid_body_constraint
if rbc: if rbc:
rbc.spring_stiffness_x = prop.spring_linear[0] rbc.spring_stiffness_x = prop.spring_linear[0]
@@ -249,8 +243,8 @@ def _updateSpringLinear(prop: PropertyGroup, context: Context) -> None:
rbc.spring_stiffness_z = prop.spring_linear[2] rbc.spring_stiffness_z = prop.spring_linear[2]
def _updateSpringAngular(prop: PropertyGroup, context: Context) -> None: def _updateSpringAngular(prop, context):
obj: Object = prop.id_data obj = prop.id_data
rbc = obj.rigid_body_constraint rbc = obj.rigid_body_constraint
if rbc and hasattr(rbc, "use_spring_ang_x"): if rbc and hasattr(rbc, "use_spring_ang_x"):
rbc.spring_stiffness_ang_x = prop.spring_angular[0] rbc.spring_stiffness_ang_x = prop.spring_angular[0]
@@ -258,7 +252,7 @@ def _updateSpringAngular(prop: PropertyGroup, context: Context) -> None:
rbc.spring_stiffness_ang_z = prop.spring_angular[2] rbc.spring_stiffness_ang_z = prop.spring_angular[2]
class MMDJoint(PropertyGroup): class MMDJoint(bpy.types.PropertyGroup):
name_j: bpy.props.StringProperty( name_j: bpy.props.StringProperty(
name="Name", name="Name",
description="Japanese Name", description="Japanese Name",
@@ -292,12 +286,9 @@ class MMDJoint(PropertyGroup):
) )
@staticmethod @staticmethod
def register() -> None: def register():
logger.debug("Registering MMDJoint property")
bpy.types.Object.mmd_joint = patch_library_overridable(bpy.props.PointerProperty(type=MMDJoint)) bpy.types.Object.mmd_joint = patch_library_overridable(bpy.props.PointerProperty(type=MMDJoint))
@staticmethod @staticmethod
def unregister() -> None: def unregister():
logger.debug("Unregistering MMDJoint property")
del bpy.types.Object.mmd_joint del bpy.types.Object.mmd_joint
+86 -55
View File
@@ -1,16 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors # Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender # This file is part of MMD Tools.
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
"""Properties for MMD model root object""" """Properties for MMD model root object"""
import bpy import bpy
from typing import Optional, List, Dict, Any, Set, Tuple, Union, Type, TypeVar, cast
from .. import utils
from ..bpyutils import FnContext from ..bpyutils import FnContext
from ..core.material import FnMaterial from ..core.material import FnMaterial
from ..core.model import FnModel from ..core.model import FnModel
@@ -18,18 +12,19 @@ from ..core.sdef import FnSDEF
from . import patch_library_overridable from . import patch_library_overridable
from .morph import BoneMorph, GroupMorph, MaterialMorph, UVMorph, VertexMorph from .morph import BoneMorph, GroupMorph, MaterialMorph, UVMorph, VertexMorph
from .translations import MMDTranslation from .translations import MMDTranslation
from ....core.logging_setup import logger
IS_BLENDER_50_UP = bpy.app.version >= (5, 0)
def __driver_variables(constraint: bpy.types.Constraint, path: str, index: int = -1) -> Tuple[bpy.types.Driver, Any]: def __driver_variables(constraint: bpy.types.Constraint, path: str, index=-1):
d = constraint.driver_add(path, index) d = constraint.driver_add(path, index)
variables = d.driver.variables variables = d.driver.variables
for x in variables: for x in reversed(variables):
variables.remove(x) variables.remove(x)
return d.driver, variables return d.driver, variables
def __add_single_prop(variables: Any, id_obj: bpy.types.Object, data_path: str, prefix: str) -> Any: def __add_single_prop(variables, id_obj, data_path, prefix):
var = variables.new() var = variables.new()
var.name = prefix + str(len(variables)) var.name = prefix + str(len(variables))
var.type = "SINGLE_PROP" var.type = "SINGLE_PROP"
@@ -40,18 +35,17 @@ def __add_single_prop(variables: Any, id_obj: bpy.types.Object, data_path: str,
return var return var
def _toggleUsePropertyDriver(self: "MMDRoot", _context: bpy.types.Context) -> None: def _toggleUsePropertyDriver(self: "MMDRoot", _context):
root_object: bpy.types.Object = self.id_data root_object: bpy.types.Object = self.id_data
armature_object = FnModel.find_armature_object(root_object) armature_object = FnModel.find_armature_object(root_object)
if armature_object is None: if armature_object is None:
ik_map: Dict[Any, Tuple[Any, Any]] = {} ik_map = {}
else: else:
bones = armature_object.pose.bones bones = armature_object.pose.bones
ik_map = {bones[c.subtarget]: (b, c) for b in bones for c in b.constraints if c.type == "IK" and c.is_valid and c.subtarget in bones} ik_map = {bones[c.subtarget]: (b, c) for b in bones for c in b.constraints if c.type == "IK" and c.is_valid and c.subtarget in bones}
if self.use_property_driver: if self.use_property_driver:
logger.debug("Enabling property drivers for %s", root_object.name)
for ik, (b, c) in ik_map.items(): for ik, (b, c) in ik_map.items():
driver, variables = __driver_variables(c, "influence") driver, variables = __driver_variables(c, "influence")
driver.expression = "%s" % __add_single_prop(variables, ik.id_data, ik.path_from_id("mmd_ik_toggle"), "use_ik").name driver.expression = "%s" % __add_single_prop(variables, ik.id_data, ik.path_from_id("mmd_ik_toggle"), "use_ik").name
@@ -66,7 +60,6 @@ def _toggleUsePropertyDriver(self: "MMDRoot", _context: bpy.types.Context) -> No
driver, variables = __driver_variables(i, prop_hide) driver, variables = __driver_variables(i, prop_hide)
driver.expression = "not %s" % __add_single_prop(variables, root_object, "mmd_root.show_meshes", "show").name driver.expression = "not %s" % __add_single_prop(variables, root_object, "mmd_root.show_meshes", "show").name
else: else:
logger.debug("Disabling property drivers for %s", root_object.name)
for ik, (b, c) in ik_map.items(): for ik, (b, c) in ik_map.items():
c.driver_remove("influence") c.driver_remove("influence")
b = b if c.use_tail else b.parent b = b if c.use_tail else b.parent
@@ -84,35 +77,31 @@ def _toggleUsePropertyDriver(self: "MMDRoot", _context: bpy.types.Context) -> No
# =========================================== # ===========================================
def _toggleUseToonTexture(self: "MMDRoot", _context: bpy.types.Context) -> None: def _toggleUseToonTexture(self: "MMDRoot", _context):
use_toon = self.use_toon_texture use_toon = self.use_toon_texture
logger.debug("Toggling toon texture to %s for %s", use_toon, self.id_data.name)
for i in FnModel.iterate_mesh_objects(self.id_data): for i in FnModel.iterate_mesh_objects(self.id_data):
for m in i.data.materials: for m in i.data.materials:
if m: if m:
FnMaterial(m).use_toon_texture(use_toon) FnMaterial(m).use_toon_texture(use_toon)
def _toggleUseSphereTexture(self: "MMDRoot", _context: bpy.types.Context) -> None: def _toggleUseSphereTexture(self: "MMDRoot", _context):
use_sphere = self.use_sphere_texture use_sphere = self.use_sphere_texture
logger.debug("Toggling sphere texture to %s for %s", use_sphere, self.id_data.name)
for i in FnModel.iterate_mesh_objects(self.id_data): for i in FnModel.iterate_mesh_objects(self.id_data):
for m in i.data.materials: for m in i.data.materials:
if m: if m:
FnMaterial(m).use_sphere_texture(use_sphere, i) FnMaterial(m).use_sphere_texture(use_sphere, i)
def _toggleUseSDEF(self: "MMDRoot", _context: bpy.types.Context) -> None: def _toggleUseSDEF(self: "MMDRoot", _context):
mute_sdef = not self.use_sdef mute_sdef = not self.use_sdef
logger.debug("Toggling SDEF to %s for %s", not mute_sdef, self.id_data.name)
for i in FnModel.iterate_mesh_objects(self.id_data): for i in FnModel.iterate_mesh_objects(self.id_data):
FnSDEF.mute_sdef_set(i, mute_sdef) FnSDEF.mute_sdef_set(i, mute_sdef)
def _toggleVisibilityOfMeshes(self: "MMDRoot", context: bpy.types.Context) -> None: def _toggleVisibilityOfMeshes(self: "MMDRoot", context: bpy.types.Context):
root = self.id_data root = self.id_data
hide = not self.show_meshes hide = not self.show_meshes
logger.debug("Toggling mesh visibility to %s for %s", not hide, root.name)
for i in FnModel.iterate_mesh_objects(self.id_data): for i in FnModel.iterate_mesh_objects(self.id_data):
i.hide_set(hide) i.hide_set(hide)
i.hide_render = hide i.hide_render = hide
@@ -120,30 +109,27 @@ def _toggleVisibilityOfMeshes(self: "MMDRoot", context: bpy.types.Context) -> No
FnContext.set_active_object(context, root) FnContext.set_active_object(context, root)
def _toggleVisibilityOfRigidBodies(self: "MMDRoot", context: bpy.types.Context) -> None: def _toggleVisibilityOfRigidBodies(self: "MMDRoot", context: bpy.types.Context):
root = self.id_data root = self.id_data
hide = not self.show_rigid_bodies hide = not self.show_rigid_bodies
logger.debug("Toggling rigid body visibility to %s for %s", not hide, root.name)
for i in FnModel.iterate_rigid_body_objects(root): for i in FnModel.iterate_rigid_body_objects(root):
i.hide_set(hide) i.hide_set(hide)
if hide and context.active_object is None: if hide and context.active_object is None:
FnContext.set_active_object(context, root) FnContext.set_active_object(context, root)
def _toggleVisibilityOfJoints(self: "MMDRoot", context: bpy.types.Context) -> None: def _toggleVisibilityOfJoints(self: "MMDRoot", context):
root_object = self.id_data root_object = self.id_data
hide = not self.show_joints hide = not self.show_joints
logger.debug("Toggling joint visibility to %s for %s", not hide, root_object.name)
for i in FnModel.iterate_joint_objects(root_object): for i in FnModel.iterate_joint_objects(root_object):
i.hide_set(hide) i.hide_set(hide)
if hide and context.active_object is None: if hide and context.active_object is None:
FnContext.set_active_object(context, root_object) FnContext.set_active_object(context, root_object)
def _toggleVisibilityOfTemporaryObjects(self: "MMDRoot", context: bpy.types.Context) -> None: def _toggleVisibilityOfTemporaryObjects(self: "MMDRoot", context: bpy.types.Context):
root_object: bpy.types.Object = self.id_data root_object: bpy.types.Object = self.id_data
hide = not self.show_temporary_objects hide = not self.show_temporary_objects
logger.debug("Toggling temporary object visibility to %s for %s", not hide, root_object.name)
with FnContext.temp_override_active_layer_collection(context, root_object): with FnContext.temp_override_active_layer_collection(context, root_object):
for i in FnModel.iterate_temporary_objects(root_object): for i in FnModel.iterate_temporary_objects(root_object):
i.hide_set(hide) i.hide_set(hide)
@@ -151,48 +137,45 @@ def _toggleVisibilityOfTemporaryObjects(self: "MMDRoot", context: bpy.types.Cont
FnContext.set_active_object(context, root_object) FnContext.set_active_object(context, root_object)
def _toggleShowNamesOfRigidBodies(self: "MMDRoot", _context: bpy.types.Context) -> None: def _toggleShowNamesOfRigidBodies(self: "MMDRoot", _context):
root = self.id_data root = self.id_data
show_names = root.mmd_root.show_names_of_rigid_bodies show_names = root.mmd_root.show_names_of_rigid_bodies
logger.debug("Toggling rigid body names to %s for %s", show_names, root.name)
for i in FnModel.iterate_rigid_body_objects(root): for i in FnModel.iterate_rigid_body_objects(root):
i.show_name = show_names i.show_name = show_names
def _toggleShowNamesOfJoints(self: "MMDRoot", _context: bpy.types.Context) -> None: def _toggleShowNamesOfJoints(self: "MMDRoot", _context):
root = self.id_data root = self.id_data
show_names = root.mmd_root.show_names_of_joints show_names = root.mmd_root.show_names_of_joints
logger.debug("Toggling joint names to %s for %s", show_names, root.name)
for i in FnModel.iterate_joint_objects(root): for i in FnModel.iterate_joint_objects(root):
i.show_name = show_names i.show_name = show_names
def _setVisibilityOfMMDRigArmature(prop: "MMDRoot", v: bool) -> None: def _setVisibilityOfMMDRigArmature(prop: "MMDRoot", v: bool):
root = prop.id_data root = prop.id_data
arm = FnModel.find_armature_object(root) arm = FnModel.find_armature_object(root)
if arm is None: if arm is None:
return return
if not v and bpy.context.active_object == arm: if not v and bpy.context.active_object == arm:
FnContext.set_active_object(bpy.context, root) FnContext.set_active_object(bpy.context, root)
logger.debug("Setting armature visibility to %s for %s", v, root.name)
arm.hide_set(not v) arm.hide_set(not v)
def _getVisibilityOfMMDRigArmature(prop: "MMDRoot") -> bool: def _getVisibilityOfMMDRigArmature(prop: "MMDRoot"):
if prop.id_data.mmd_type != "ROOT": if prop.id_data.mmd_type != "ROOT":
return False return False
arm = FnModel.find_armature_object(prop.id_data) arm = FnModel.find_armature_object(prop.id_data)
return arm and not arm.hide_get() return arm is not None and not arm.hide_get()
def _setActiveRigidbodyObject(prop: "MMDRoot", v: int) -> None: def _setActiveRigidbodyObject(prop: "MMDRoot", v: int):
obj = FnContext.get_scene_objects(bpy.context)[v] obj = FnContext.get_scene_objects(bpy.context)[v]
if FnModel.is_rigid_body_object(obj): if FnModel.is_rigid_body_object(obj):
FnContext.set_active_and_select_single_object(bpy.context, obj) FnContext.set_active_and_select_single_object(bpy.context, obj)
prop["active_rigidbody_object_index"] = v prop["active_rigidbody_object_index"] = v
def _getActiveRigidbodyObject(prop: "MMDRoot") -> int: def _getActiveRigidbodyObject(prop: "MMDRoot"):
context = bpy.context context = bpy.context
active_obj = FnContext.get_active_object(context) active_obj = FnContext.get_active_object(context)
if FnModel.is_rigid_body_object(active_obj): if FnModel.is_rigid_body_object(active_obj):
@@ -200,14 +183,14 @@ def _getActiveRigidbodyObject(prop: "MMDRoot") -> int:
return prop.get("active_rigidbody_object_index", 0) return prop.get("active_rigidbody_object_index", 0)
def _setActiveJointObject(prop: "MMDRoot", v: int) -> None: def _setActiveJointObject(prop: "MMDRoot", v: int):
obj = FnContext.get_scene_objects(bpy.context)[v] obj = FnContext.get_scene_objects(bpy.context)[v]
if FnModel.is_joint_object(obj): if FnModel.is_joint_object(obj):
FnContext.set_active_and_select_single_object(bpy.context, obj) FnContext.set_active_and_select_single_object(bpy.context, obj)
prop["active_joint_object_index"] = v prop["active_joint_object_index"] = v
def _getActiveJointObject(prop: "MMDRoot") -> int: def _getActiveJointObject(prop: "MMDRoot"):
context = bpy.context context = bpy.context
active_obj = FnContext.get_active_object(context) active_obj = FnContext.get_active_object(context)
if FnModel.is_joint_object(active_obj): if FnModel.is_joint_object(active_obj):
@@ -215,26 +198,26 @@ def _getActiveJointObject(prop: "MMDRoot") -> int:
return prop.get("active_joint_object_index", 0) return prop.get("active_joint_object_index", 0)
def _setActiveMorph(prop: "MMDRoot", v: bool) -> None: def _setActiveMorph(prop: "MMDRoot", v: bool):
if "active_morph_indices" not in prop: if "active_morph_indices" not in prop:
prop["active_morph_indices"] = [0] * 5 prop["active_morph_indices"] = [0] * 5
prop["active_morph_indices"][prop.get("active_morph_type", 3)] = v prop["active_morph_indices"][prop.get("active_morph_type", 3)] = v
def _getActiveMorph(prop: "MMDRoot") -> int: def _getActiveMorph(prop: "MMDRoot"):
if "active_morph_indices" in prop: if "active_morph_indices" in prop:
return prop["active_morph_indices"][prop.get("active_morph_type", 3)] return prop["active_morph_indices"][prop.get("active_morph_type", 3)]
return 0 return 0
def _setActiveMeshObject(prop: "MMDRoot", v: int) -> None: def _setActiveMeshObject(prop: "MMDRoot", v: int):
obj = FnContext.get_scene_objects(bpy.context)[v] obj = FnContext.get_scene_objects(bpy.context)[v]
if FnModel.is_mesh_object(obj): if FnModel.is_mesh_object(obj):
FnContext.set_active_and_select_single_object(bpy.context, obj) FnContext.set_active_and_select_single_object(bpy.context, obj)
prop["active_mesh_index"] = v prop["active_mesh_index"] = v
def _getActiveMeshObject(prop: "MMDRoot") -> int: def _getActiveMeshObject(prop: "MMDRoot"):
context = bpy.context context = bpy.context
active_obj = FnContext.get_active_object(context) active_obj = FnContext.get_active_object(context)
if FnModel.is_mesh_object(active_obj): if FnModel.is_mesh_object(active_obj):
@@ -393,6 +376,18 @@ class MMDRoot(bpy.types.PropertyGroup):
update=_toggleShowNamesOfJoints, update=_toggleShowNamesOfJoints,
) )
show_japanese_name: bpy.props.BoolProperty(
name="Japanese name",
description="Toggle Japanese name display",
default=True,
)
show_english_name: bpy.props.BoolProperty(
name="English name",
description="Toggle English name display",
default=True,
)
use_toon_texture: bpy.props.BoolProperty( use_toon_texture: bpy.props.BoolProperty(
name="Use Toon Texture", name="Use Toon Texture",
description="Use toon texture", description="Use toon texture",
@@ -453,6 +448,15 @@ class MMDRoot(bpy.types.PropertyGroup):
default=0, default=0,
) )
# *************************
# Bone
# *************************
active_bone_index: bpy.props.IntProperty(
name="Active Bone Index",
description="Index of the active bone in the armature",
default=0,
)
# ************************* # *************************
# Morph # Morph
# ************************* # *************************
@@ -513,29 +517,40 @@ class MMDRoot(bpy.types.PropertyGroup):
@staticmethod @staticmethod
def __get_select(prop: bpy.types.Object) -> bool: def __get_select(prop: bpy.types.Object) -> bool:
utils.warn_deprecation("Object.select", "v4.0.0", "Use Object.select_get() method instead") # TODO: Object.select is deprecated since v4.0.0, use Object.select_get() method instead
# utils.warn_deprecation("Object.select", "v4.0.0", "Use Object.select_get() method instead")
return prop.select_get() return prop.select_get()
@staticmethod @staticmethod
def __set_select(prop: bpy.types.Object, value: bool) -> None: def __set_select(prop: bpy.types.Object, value: bool) -> None:
utils.warn_deprecation("Object.select", "v4.0.0", "Use Object.select_set() method instead") # TODO: Object.select is deprecated since v4.0.0, use Object.select_set() method instead
# utils.warn_deprecation("Object.select", "v4.0.0", "Use Object.select_set() method instead")
prop.select_set(value) prop.select_set(value)
@staticmethod @staticmethod
def __get_hide(prop: bpy.types.Object) -> bool: def __get_hide(prop: bpy.types.Object) -> bool:
utils.warn_deprecation("Object.hide", "v4.0.0", "Use Object.hide_get() method instead") # TODO: Object.hide is deprecated since v4.0.0, use Object.hide_get() method instead
# utils.warn_deprecation("Object.hide", "v4.0.0", "Use Object.hide_get() method instead")
return prop.hide_get() return prop.hide_get()
@staticmethod @staticmethod
def __set_hide(prop: bpy.types.Object, value: bool) -> None: def __set_hide(prop: bpy.types.Object, value: bool) -> None:
utils.warn_deprecation("Object.hide", "v4.0.0", "Use Object.hide_set() method instead") # TODO: Object.hide is deprecated since v4.0.0, use Object.hide_set() method instead
# utils.warn_deprecation("Object.hide", "v4.0.0", "Use Object.hide_set() method instead")
prop.hide_set(value) prop.hide_set(value)
if prop.hide_viewport != value: if prop.hide_viewport != value:
prop.hide_viewport = value prop.hide_viewport = value
@staticmethod @staticmethod
def register() -> None: def __get_pose_bone_select(prop: bpy.types.PoseBone) -> bool:
logger.debug("Registering MMDRoot property group") return prop.bone.select
@staticmethod
def __set_pose_bone_select(prop: bpy.types.PoseBone, value: bool) -> None:
prop.bone.select = value
@staticmethod
def register():
bpy.types.Object.mmd_type = patch_library_overridable( bpy.types.Object.mmd_type = patch_library_overridable(
bpy.props.EnumProperty( bpy.props.EnumProperty(
name="Type", name="Type",
@@ -557,7 +572,7 @@ class MMDRoot(bpy.types.PropertyGroup):
("SPRING_CONSTRAINT", "Spring Constraint", "", 53), ("SPRING_CONSTRAINT", "Spring Constraint", "", 53),
("SPRING_GOAL", "Spring Goal", "", 54), ("SPRING_GOAL", "Spring Goal", "", 54),
], ],
) ),
) )
bpy.types.Object.mmd_root = patch_library_overridable(bpy.props.PointerProperty(type=MMDRoot)) bpy.types.Object.mmd_root = patch_library_overridable(bpy.props.PointerProperty(type=MMDRoot))
@@ -570,7 +585,7 @@ class MMDRoot(bpy.types.PropertyGroup):
"ANIMATABLE", "ANIMATABLE",
"LIBRARY_EDITABLE", "LIBRARY_EDITABLE",
}, },
) ),
) )
bpy.types.Object.hide = patch_library_overridable( bpy.types.Object.hide = patch_library_overridable(
bpy.props.BoolProperty( bpy.props.BoolProperty(
@@ -581,13 +596,29 @@ class MMDRoot(bpy.types.PropertyGroup):
"ANIMATABLE", "ANIMATABLE",
"LIBRARY_EDITABLE", "LIBRARY_EDITABLE",
}, },
),
) )
if not IS_BLENDER_50_UP:
bpy.types.PoseBone.select = patch_library_overridable(
bpy.props.BoolProperty(
name="Select",
description="Pose bone selection state (compatibility layer for Blender 4.x, forwards to bone.select)",
get=MMDRoot.__get_pose_bone_select,
set=MMDRoot.__set_pose_bone_select,
options={
"SKIP_SAVE",
"ANIMATABLE",
"LIBRARY_EDITABLE",
},
),
) )
@staticmethod @staticmethod
def unregister() -> None: def unregister():
logger.debug("Unregistering MMDRoot property group")
del bpy.types.Object.hide del bpy.types.Object.hide
del bpy.types.Object.select del bpy.types.Object.select
del bpy.types.Object.mmd_root del bpy.types.Object.mmd_root
del bpy.types.Object.mmd_type del bpy.types.Object.mmd_type
if not IS_BLENDER_50_UP:
del bpy.types.PoseBone.select
+2 -6
View File
@@ -1,9 +1,5 @@
# -*- coding: utf-8 -*- # Copyright 2021 MMD Tools authors
# Copyright 2014 MMD Tools authors # This file is part of MMD Tools.
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple
+42 -103
View File
@@ -1,25 +1,17 @@
# -*- coding: utf-8 -*- # Copyright 2016 MMD Tools authors
# Copyright 2014 MMD Tools authors # This file is part of MMD Tools.
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import csv import csv
from ...core.logging_setup import logger
import os
import time import time
from typing import List, Tuple, Dict, Optional, Any, Generator, Union, TextIO, Iterator, Set from collections import OrderedDict
import bpy import bpy
from bpy.types import Text, Context
from .bpyutils import FnContext from .bpyutils import FnContext
from ..logging_setup import logger
# Type definitions for translation tuples jp_half_to_full_tuples = (
TranslationTuple = Tuple[str, str]
TranslationList = List[TranslationTuple]
jp_half_to_full_tuples: TranslationList = (
("ヴ", ""), ("ヴ", ""),
("ガ", ""), ("ガ", ""),
("ギ", ""), ("ギ", ""),
@@ -109,7 +101,7 @@ jp_half_to_full_tuples: TranslationList = (
("", ""), ("", ""),
) )
jp_to_en_tuples: TranslationList = [ jp_to_en_tuples = [
("全ての親", "ParentNode"), ("全ての親", "ParentNode"),
("操作中心", "ControlNode"), ("操作中心", "ControlNode"),
("センター", "Center"), ("センター", "Center"),
@@ -299,30 +291,22 @@ jp_to_en_tuples: TranslationList = [
] ]
def translateFromJp(name: str) -> str: def translateFromJp(name):
"""Translate a Japanese name to English using the translation tuples.""" for t in jp_to_en_tuples:
logger.debug(f"Translating from Japanese: {name}") if t[0] in name:
for tuple in jp_to_en_tuples: name = name.replace(t[0], t[1])
if tuple[0] in name:
name = name.replace(tuple[0], tuple[1])
logger.debug(f"Translation result: {name}")
return name return name
def getTranslator(csvfile: Union[str, Dict[str, str], Text] = "", keep_order: bool = False) -> 'MMDTranslator': def getTranslator(csvfile="", keep_order=False):
"""Get a translator instance with the specified CSV file."""
translator = MMDTranslator() translator = MMDTranslator()
if isinstance(csvfile, bpy.types.Text): if isinstance(csvfile, bpy.types.Text):
logger.debug(f"Loading translator from Text object: {csvfile.name}")
translator.load_from_stream(csvfile) translator.load_from_stream(csvfile)
elif isinstance(csvfile, dict): elif isinstance(csvfile, dict):
logger.debug(f"Loading translator from dictionary with {len(csvfile)} entries")
translator.csv_tuples.extend(csvfile.items()) translator.csv_tuples.extend(csvfile.items())
elif csvfile in bpy.data.texts.keys(): elif csvfile in bpy.data.texts.keys():
logger.debug(f"Loading translator from text data: {csvfile}")
translator.load_from_stream(bpy.data.texts[csvfile]) translator.load_from_stream(bpy.data.texts[csvfile])
else: else:
logger.debug(f"Loading translator from file: {csvfile}")
translator.load(csvfile) translator.load(csvfile)
if not keep_order: if not keep_order:
@@ -332,20 +316,16 @@ def getTranslator(csvfile: Union[str, Dict[str, str], Text] = "", keep_order: bo
class MMDTranslator: class MMDTranslator:
"""Handles translation of Japanese text to English for MMD models.""" def __init__(self):
self.__csv_tuples = []
def __init__(self) -> None: self.__fails = {}
self.__csv_tuples: List[Tuple[str, str]] = []
self.__fails: Dict[str, str] = {}
@staticmethod @staticmethod
def default_csv_filepath() -> str: def default_csv_filepath():
"""Get the default CSV filepath for translations."""
return __file__[:-3] + ".csv" return __file__[:-3] + ".csv"
@staticmethod @staticmethod
def get_csv_text(text_name: Optional[str] = None) -> Text: def get_csv_text(text_name=None):
"""Get or create a Text object for CSV data."""
text_name = text_name or bpy.path.basename(MMDTranslator.default_csv_filepath()) text_name = text_name or bpy.path.basename(MMDTranslator.default_csv_filepath())
csv_text = bpy.data.texts.get(text_name, None) csv_text = bpy.data.texts.get(text_name, None)
if csv_text is None: if csv_text is None:
@@ -353,88 +333,67 @@ class MMDTranslator:
return csv_text return csv_text
@staticmethod @staticmethod
def replace_from_tuples(name: str, tuples: List[Tuple[str, str]]) -> str: def replace_from_tuples(name, tuples):
"""Replace parts of a string based on translation tuples."""
for pair in tuples: for pair in tuples:
if pair[0] in name: if pair[0] in name:
name = name.replace(pair[0], pair[1]) name = name.replace(pair[0], pair[1])
return name return name
@property @property
def csv_tuples(self) -> List[Tuple[str, str]]: def csv_tuples(self):
"""Get the CSV tuples."""
return self.__csv_tuples return self.__csv_tuples
@property @property
def fails(self) -> Dict[str, str]: def fails(self):
"""Get the failed translations."""
return self.__fails return self.__fails
def sort(self) -> None: def sort(self):
"""Sort the CSV tuples by length (longest first) and then alphabetically."""
logger.debug("Sorting translation tuples")
self.__csv_tuples.sort(key=lambda row: (-len(row[0]), row)) self.__csv_tuples.sort(key=lambda row: (-len(row[0]), row))
def update(self) -> None: def update(self):
"""Update the CSV tuples, removing duplicates."""
from collections import OrderedDict
count_old = len(self.__csv_tuples) count_old = len(self.__csv_tuples)
tuples_dict = OrderedDict((row[0], row) for row in self.__csv_tuples if len(row) >= 2 and row[0]) tuples_dict = OrderedDict((row[0], row) for row in self.__csv_tuples if len(row) >= 2 and row[0])
self.__csv_tuples.clear() self.__csv_tuples.clear()
self.__csv_tuples.extend(tuples_dict.values()) self.__csv_tuples.extend(tuples_dict.values())
logger.info("Translation update - removed items: %d (of %d)", count_old - len(self.__csv_tuples), count_old) logger.info(" - removed items:\t%d\t(of %d)", count_old - len(self.__csv_tuples), count_old)
def half_to_full(self, name: str) -> str: def half_to_full(self, name):
"""Convert half-width Japanese characters to full-width."""
return self.replace_from_tuples(name, jp_half_to_full_tuples) return self.replace_from_tuples(name, jp_half_to_full_tuples)
def is_translated(self, name: str) -> bool: def is_translated(self, name):
"""Check if a string is already translated (contains only ASCII characters)."""
try: try:
name.encode("ascii", errors="strict") name.encode("ascii", errors="strict")
except UnicodeEncodeError: except UnicodeEncodeError:
return False return False
return True return True
def translate(self, name: str, default: Optional[str] = None, from_full_width: bool = True) -> str: def translate(self, name, default=None, from_full_width=True):
"""Translate a string from Japanese to English."""
logger.debug(f"Translating: {name}")
if from_full_width: if from_full_width:
name = self.half_to_full(name) name = self.half_to_full(name)
name_new = self.replace_from_tuples(name, self.__csv_tuples) name_new = self.replace_from_tuples(name, self.__csv_tuples)
if default is not None and not self.is_translated(name_new): if default is not None and not self.is_translated(name_new):
logger.warning(f"Translation failed for: {name}")
self.__fails[name] = name_new self.__fails[name] = name_new
return default return default
return name_new return name_new
def save_fails(self, text_name: Optional[str] = None) -> Text: def save_fails(self, text_name=None):
"""Save failed translations to a Text object."""
text_name = text_name or (__name__ + ".fails") text_name = text_name or (__name__ + ".fails")
txt = self.get_csv_text(text_name) txt = self.get_csv_text(text_name)
fmt = '"%s","%s"' fmt = '"%s","%s"'
items = sorted(self.__fails.items(), key=lambda row: (-len(row[0]), row)) items = sorted(self.__fails.items(), key=lambda row: (-len(row[0]), row))
txt.from_string("\n".join(fmt % (k, v) for k, v in items)) txt.from_string("\n".join(fmt % (k, v) for k, v in items))
logger.info(f"Saved {len(items)} failed translations to {text_name}")
return txt return txt
def load_from_stream(self, csvfile: Union[Text, Iterator[str]] = None) -> None: def load_from_stream(self, csvfile=None):
"""Load translations from a stream."""
csvfile = csvfile or self.get_csv_text() csvfile = csvfile or self.get_csv_text()
if isinstance(csvfile, bpy.types.Text): if isinstance(csvfile, bpy.types.Text):
csvfile = (l.body + "\n" for l in csvfile.lines) csvfile = (line.body + "\n" for line in csvfile.lines)
spamreader = csv.reader(csvfile, delimiter=",", skipinitialspace=True) spamreader = csv.reader(csvfile, delimiter=",", skipinitialspace=True)
csv_tuples = [tuple(row) for row in spamreader if len(row) >= 2] csv_tuples = [tuple(row) for row in spamreader if len(row) >= 2]
self.__csv_tuples = csv_tuples self.__csv_tuples = csv_tuples
logger.info("Loaded %d translation items", len(self.__csv_tuples)) logger.info(" - load items:\t%d", len(self.__csv_tuples))
def save_to_stream(self, csvfile: Union[Text, TextIO] = None) -> None: def save_to_stream(self, csvfile=None):
"""Save translations to a stream.
Args:
csvfile: The CSV file or stream to save to
"""
csvfile = csvfile or self.get_csv_text() csvfile = csvfile or self.get_csv_text()
lineterminator = "\r\n" lineterminator = "\r\n"
if isinstance(csvfile, bpy.types.Text): if isinstance(csvfile, bpy.types.Text):
@@ -442,38 +401,27 @@ class MMDTranslator:
lineterminator = "\n" lineterminator = "\n"
spamwriter = csv.writer(csvfile, delimiter=",", lineterminator=lineterminator, quoting=csv.QUOTE_ALL) spamwriter = csv.writer(csvfile, delimiter=",", lineterminator=lineterminator, quoting=csv.QUOTE_ALL)
spamwriter.writerows(self.__csv_tuples) spamwriter.writerows(self.__csv_tuples)
logger.info("Saved %d translation items", len(self.__csv_tuples)) logger.info(" - save items:\t%d", len(self.__csv_tuples))
def load(self, filepath: Optional[str] = None) -> None: def load(self, filepath=None):
"""Load translations from a file."""
filepath = filepath or self.default_csv_filepath() filepath = filepath or self.default_csv_filepath()
logger.info("Loading CSV file: %s", filepath) logger.info("Loading csv file:\t%s", filepath)
try: with open(filepath, encoding="utf-8", newline="") as csvfile:
with open(filepath, "rt", encoding="utf-8", newline="") as csvfile:
self.load_from_stream(csvfile) self.load_from_stream(csvfile)
except Exception as e:
logger.error(f"Failed to load CSV file: {e}")
def save(self, filepath: Optional[str] = None) -> None: def save(self, filepath=None):
"""Save translations to a file."""
filepath = filepath or self.default_csv_filepath() filepath = filepath or self.default_csv_filepath()
logger.info("Saving CSV file: %s", filepath) logger.info("Saving csv file:\t%s", filepath)
try: with open(filepath, "w", encoding="utf-8", newline="") as csvfile:
with open(filepath, "wt", encoding="utf-8", newline="") as csvfile:
self.save_to_stream(csvfile) self.save_to_stream(csvfile)
except Exception as e:
logger.error(f"Failed to save CSV file: {e}")
class DictionaryEnum: class DictionaryEnum:
"""Handles dictionary enumeration for UI.""" __items_ttl = 0.0
__items_cache = None
__items_ttl: float = 0.0
__items_cache: Optional[List[Tuple[str, str, str, int]]] = None
@staticmethod @staticmethod
def get_dictionary_items(prop: Any, context: Context) -> List[Tuple[str, str, str, Union[int, str], int]]: def get_dictionary_items(prop, context):
"""Get dictionary items for UI enumeration."""
if DictionaryEnum.__items_ttl > time.time(): if DictionaryEnum.__items_ttl > time.time():
return DictionaryEnum.__items_cache return DictionaryEnum.__items_cache
@@ -487,8 +435,6 @@ class DictionaryEnum:
for txt_name in sorted(x.name for x in bpy.data.texts if x.name.lower().endswith(".csv")): for txt_name in sorted(x.name for x in bpy.data.texts if x.name.lower().endswith(".csv")):
items.append((txt_name, txt_name, f"bpy.data.texts['{txt_name}']", "TEXT", len(items))) items.append((txt_name, txt_name, f"bpy.data.texts['{txt_name}']", "TEXT", len(items)))
import os
folder = FnContext.get_addon_preferences_attribute(context, "dictionary_folder", "") folder = FnContext.get_addon_preferences_attribute(context, "dictionary_folder", "")
if os.path.isdir(folder): if os.path.isdir(folder):
for filename in sorted(x for x in os.listdir(folder) if x.lower().endswith(".csv")): for filename in sorted(x for x in os.listdir(folder) if x.lower().endswith(".csv")):
@@ -498,19 +444,12 @@ class DictionaryEnum:
if "dictionary" in prop: if "dictionary" in prop:
prop["dictionary"] = min(prop["dictionary"], len(items) - 1) prop["dictionary"] = min(prop["dictionary"], len(items) - 1)
logger.debug(f"Found {len(items)} dictionary items")
return items return items
@staticmethod @staticmethod
def get_translator(dictionary: str) -> Optional[MMDTranslator]: def get_translator(dictionary):
"""Get a translator for the specified dictionary."""
if dictionary == "DISABLED": if dictionary == "DISABLED":
logger.debug("Translation disabled")
return None return None
if dictionary == "INTERNAL": if dictionary == "INTERNAL":
logger.debug("Using internal dictionary")
return getTranslator(dict(jp_to_en_tuples)) return getTranslator(dict(jp_to_en_tuples))
logger.debug(f"Using dictionary: {dictionary}")
return getTranslator(dictionary) return getTranslator(dictionary)
+89 -61
View File
@@ -1,23 +1,20 @@
# -*- coding: utf-8 -*- # Copyright 2012 MMD Tools authors
# Copyright 2014 MMD Tools authors # This file is part of MMD Tools.
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from ...core.logging_setup import logger
import os import os
import re import re
from typing import Callable, Dict, List, Optional, Set, Tuple, Union, Any import string
from typing import Callable, Optional, Set
import bpy import bpy
from bpy.types import Object, Bone, PoseBone, Mesh, VertexGroup import numpy as np
from ..logging_setup import logger
from .bpyutils import FnContext from .bpyutils import FnContext
## 指定したオブジェクトのみを選択状態かつアクティブにする # 指定したオブジェクトのみを選択状態かつアクティブにする
def selectAObject(obj: Object) -> None: def selectAObject(obj):
try: try:
bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="OBJECT")
except Exception: except Exception:
@@ -27,14 +24,14 @@ def selectAObject(obj: Object) -> None:
FnContext.set_active_object(FnContext.ensure_context(), obj) FnContext.set_active_object(FnContext.ensure_context(), obj)
## 現在のモードを指定したオブジェクトのEdit Modeに変更する # 現在のモードを指定したオブジェクトのEdit Modeに変更する
def enterEditMode(obj: Object) -> None: def enterEditMode(obj):
selectAObject(obj) selectAObject(obj)
if obj.mode != "EDIT": if obj.mode != "EDIT":
bpy.ops.object.mode_set(mode="EDIT") bpy.ops.object.mode_set(mode="EDIT")
def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None: def setParentToBone(obj, parent, bone_name):
selectAObject(obj) selectAObject(obj)
FnContext.set_active_object(FnContext.ensure_context(), parent) FnContext.set_active_object(FnContext.ensure_context(), parent)
bpy.ops.object.mode_set(mode="POSE") bpy.ops.object.mode_set(mode="POSE")
@@ -43,11 +40,11 @@ def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None:
bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="OBJECT")
def selectSingleBone(context: bpy.types.Context, armature: Object, bone_name: str, reset_pose: bool = False) -> None: def selectSingleBone(context, armature, bone_name, reset_pose=False):
try: try:
bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="OBJECT")
except: except Exception as e:
pass logger.warning(f"Failed to set object mode: {e}")
for i in context.selected_objects: for i in context.selected_objects:
i.select_set(False) i.select_set(False)
FnContext.set_active_object(context, armature) FnContext.set_active_object(context, armature)
@@ -55,22 +52,21 @@ def selectSingleBone(context: bpy.types.Context, armature: Object, bone_name: st
if reset_pose: if reset_pose:
for p_bone in armature.pose.bones: for p_bone in armature.pose.bones:
p_bone.matrix_basis.identity() p_bone.matrix_basis.identity()
armature_bones: bpy.types.ArmatureBones = armature.data.bones
i: Bone for p_bone in armature.pose.bones:
for i in armature_bones: is_target = p_bone.name == bone_name
i.select = i.name == bone_name p_bone.select = is_target
i.select_head = i.select_tail = i.select if is_target:
if i.select: armature.data.bones.active = p_bone.bone
armature_bones.active = i p_bone.bone.hide = False
i.hide = False
__CONVERT_NAME_TO_L_REGEXP = re.compile("^(.*)左(.*)$") __CONVERT_NAME_TO_L_REGEXP = re.compile(r"^(.*)左(.*)$")
__CONVERT_NAME_TO_R_REGEXP = re.compile("^(.*)右(.*)$") __CONVERT_NAME_TO_R_REGEXP = re.compile(r"^(.*)右(.*)$")
## 日本語で左右を命名されている名前をblender方式のL(R)に変更する # 日本語で左右を命名されている名前をblender方式のL(R)に変更する
def convertNameToLR(name: str, use_underscore: bool = False) -> str: def convertNameToLR(name, use_underscore=False):
m = __CONVERT_NAME_TO_L_REGEXP.match(name) m = __CONVERT_NAME_TO_L_REGEXP.match(name)
delimiter = "_" if use_underscore else "." delimiter = "_" if use_underscore else "."
if m: if m:
@@ -85,7 +81,7 @@ __CONVERT_L_TO_NAME_REGEXP = re.compile(r"(?P<lr>(?P<separator>[._])[lL])(?P<aft
__CONVERT_R_TO_NAME_REGEXP = re.compile(r"(?P<lr>(?P<separator>[._])[rR])(?P<after>($|(?P=separator)))") __CONVERT_R_TO_NAME_REGEXP = re.compile(r"(?P<lr>(?P<separator>[._])[rR])(?P<after>($|(?P=separator)))")
def convertLRToName(name: str) -> str: def convertLRToName(name):
match = __CONVERT_L_TO_NAME_REGEXP.search(name) match = __CONVERT_L_TO_NAME_REGEXP.search(name)
if match: if match:
return f"{name[0:match.start()]}{match['after']}{name[match.end():]}" return f"{name[0:match.start()]}{match['after']}{name[match.end():]}"
@@ -97,8 +93,8 @@ def convertLRToName(name: str) -> str:
return name return name
## src_vertex_groupのWeightをdest_vertex_groupにaddする # src_vertex_groupのWeightをdest_vertex_groupにaddする
def mergeVertexGroup(meshObj: Object, src_vertex_group_name: str, dest_vertex_group_name: str) -> None: def mergeVertexGroup(meshObj, src_vertex_group_name, dest_vertex_group_name):
mesh = meshObj.data mesh = meshObj.data
src_vertex_group = meshObj.vertex_groups[src_vertex_group_name] src_vertex_group = meshObj.vertex_groups[src_vertex_group_name]
dest_vertex_group = meshObj.vertex_groups[dest_vertex_group_name] dest_vertex_group = meshObj.vertex_groups[dest_vertex_group_name]
@@ -112,43 +108,73 @@ def mergeVertexGroup(meshObj: Object, src_vertex_group_name: str, dest_vertex_gr
pass pass
def separateByMaterials(meshObj: Object) -> None: def separateByMaterials(meshObj: bpy.types.Object, keep_normals: bool = False):
if len(meshObj.data.materials) < 2: meshData = meshObj.data
if len(meshData.materials) < 2:
selectAObject(meshObj) selectAObject(meshObj)
return return
dummy_parent = None
try:
dummy_parent = bpy.data.objects.new(name="tmp", object_data=None)
matrix_parent_inverse = meshObj.matrix_parent_inverse.copy() matrix_parent_inverse = meshObj.matrix_parent_inverse.copy()
prev_parent = meshObj.parent prev_parent = meshObj.parent
dummy_parent = bpy.data.objects.new(name="tmp", object_data=None)
meshObj.parent = dummy_parent meshObj.parent = dummy_parent
meshObj.active_shape_key_index = 0 meshObj.active_shape_key_index = 0
mmd_normal_name = None # To avoid conflict ("mmd_normal.001", etc.)
if keep_normals:
existing_custom_normal = meshData.attributes.get("custom_normal")
if existing_custom_normal:
if existing_custom_normal.data_type == "INT16_2D":
normals_data = np.empty(len(meshData.loops) * 2, dtype=np.int16)
existing_custom_normal.data.foreach_get("value", normals_data)
mmd_normal = meshData.attributes.new("mmd_normal", "INT16_2D", "CORNER")
mmd_normal_name = mmd_normal.name
mmd_normal.data.foreach_set("value", normals_data)
else:
raise TypeError(f"Unsupported custom_normal data type: '{existing_custom_normal.data_type}'. Supported types: 'INT16_2D'")
try: try:
enterEditMode(meshObj) enterEditMode(meshObj)
bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.mesh.separate(type="MATERIAL") bpy.ops.mesh.separate(type="MATERIAL")
finally: finally:
bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="OBJECT")
for i in dummy_parent.children: for i in dummy_parent.children:
materials = i.data.materials materials = i.data.materials
i.name = getattr(materials[0], "name", "None") if len(materials) else "None" i.name = getattr(materials[0], "name", "None") if len(materials) else "None"
i.parent = prev_parent i.parent = prev_parent
i.matrix_parent_inverse = matrix_parent_inverse i.matrix_parent_inverse = matrix_parent_inverse
if keep_normals and mmd_normal_name:
mmd_normal = i.data.attributes.get(mmd_normal_name)
if mmd_normal:
if mmd_normal.data_type == "INT16_2D":
normals_data = np.empty(len(i.data.loops) * 2, dtype=np.int16)
mmd_normal.data.foreach_get("value", normals_data)
custom_normal_attr = i.data.attributes.get("custom_normal")
if not custom_normal_attr:
custom_normal_attr = i.data.attributes.new("custom_normal", "INT16_2D", "CORNER")
custom_normal_attr.data.foreach_set("value", normals_data)
else:
raise TypeError(f"Unsupported custom_normal data type: '{mmd_normal.data_type}'. Supported types: 'INT16_2D'")
i.data.attributes.remove(mmd_normal)
finally:
if dummy_parent and dummy_parent.name in bpy.data.objects:
bpy.data.objects.remove(dummy_parent) bpy.data.objects.remove(dummy_parent)
def clearUnusedMeshes() -> None: def clearUnusedMeshes():
meshes_to_delete = [] meshes_to_delete = [mesh for mesh in bpy.data.meshes if mesh.users == 0]
for mesh in bpy.data.meshes:
if mesh.users == 0:
meshes_to_delete.append(mesh)
for mesh in meshes_to_delete: for mesh in meshes_to_delete:
bpy.data.meshes.remove(mesh) bpy.data.meshes.remove(mesh)
## Boneのカスタムプロパティにname_jが存在する場合、name_jの値を # Boneのカスタムプロパティにname_jが存在する場合、name_jの値を
# それ以外の場合は通常のbone名をキーとしたpose_boneへの辞書を作成 # それ以外の場合は通常のbone名をキーとしたpose_boneへの辞書を作成
def makePmxBoneMap(armObj: Object) -> Dict[str, PoseBone]: def makePmxBoneMap(armObj):
# Maintain backward compatibility with mmd_tools v0.4.x or older. # Maintain backward compatibility with mmd_tools_local v0.4.x or older.
return {(i.mmd_bone.name_j or i.get("mmd_bone_name_j", i.get("name_j", i.name))): i for i in armObj.pose.bones} return {(i.mmd_bone.name_j or i.get("mmd_bone_name_j", i.get("name_j", i.name))): i for i in armObj.pose.bones}
@@ -156,7 +182,7 @@ __REMOVE_PREFIX_DIGITS_REGEXP = re.compile(r"\.\d{1,}$")
def unique_name(name: str, used_names: Set[str]) -> str: def unique_name(name: str, used_names: Set[str]) -> str:
"""Helper function for storing unique names. """Generate a unique name from the given name.
This function is a limited and simplified version of bpy_extras.io_utils.unique_name. This function is a limited and simplified version of bpy_extras.io_utils.unique_name.
Args: Args:
@@ -176,13 +202,11 @@ def unique_name(name: str, used_names: Set[str]) -> str:
return new_name return new_name
def int2base(x: int, base: int, width: int = 0) -> str: def int2base(x, base, width=0):
""" """
Method to convert an int to a base Convert an int to a base
Source: http://stackoverflow.com/questions/2267362 Source: http://stackoverflow.com/questions/2267362
""" """
import string
digs = string.digits + string.ascii_uppercase digs = string.digits + string.ascii_uppercase
assert 2 <= base <= len(digs) assert 2 <= base <= len(digs)
digits, negtive = "", False digits, negtive = "", False
@@ -199,7 +223,7 @@ def int2base(x: int, base: int, width: int = 0) -> str:
return digits return digits
def saferelpath(path: str, start: str, strategy: str = "inside") -> str: def saferelpath(path, start, strategy="inside"):
""" """
On Windows relpath will raise a ValueError On Windows relpath will raise a ValueError
when trying to calculate the relative path to a when trying to calculate the relative path to a
@@ -226,15 +250,16 @@ def saferelpath(path: str, start: str, strategy: str = "inside") -> str:
return os.path.relpath(path, start) return os.path.relpath(path, start)
class ItemOp: class ItemOp:
@staticmethod @staticmethod
def get_by_index(items: bpy.types.bpy_prop_collection, index: int) -> Optional[Any]: def get_by_index(items, index):
if 0 <= index < len(items): if 0 <= index < len(items):
return items[index] return items[index]
return None return None
@staticmethod @staticmethod
def resize(items: bpy.types.bpy_prop_collection, length: int) -> None: def resize(items: bpy.types.bpy_prop_collection, length: int):
count = length - len(items) count = length - len(items)
if count > 0: if count > 0:
for i in range(count): for i in range(count):
@@ -244,7 +269,7 @@ class ItemOp:
items.remove(length) items.remove(length)
@staticmethod @staticmethod
def add_after(items: bpy.types.bpy_prop_collection, index: int) -> Tuple[Any, int]: def add_after(items, index):
index_end = len(items) index_end = len(items)
index = max(0, min(index_end, index + 1)) index = max(0, min(index_end, index + 1))
items.add() items.add()
@@ -266,8 +291,7 @@ class ItemMoveOp:
) )
@staticmethod @staticmethod
def move(items: bpy.types.bpy_prop_collection, index: int, move_type: str, def move(items, index, move_type, index_min=0, index_max=None):
index_min: int = 0, index_max: Optional[int] = None) -> int:
if index_max is None: if index_max is None:
index_max = len(items) - 1 index_max = len(items) - 1
else: else:
@@ -277,7 +301,7 @@ class ItemMoveOp:
if index < index_min: if index < index_min:
items.move(index, index_min) items.move(index, index_min)
return index_min return index_min
elif index > index_max: if index > index_max:
items.move(index, index_max) items.move(index, index_max)
return index_max return index_max
@@ -296,8 +320,8 @@ class ItemMoveOp:
return index_new return index_new
def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = None) -> Callable: def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = None):
"""Decorator to mark a function as deprecated. """Mark a function as deprecated.
Args: Args:
deprecated_in (Optional[str]): Version in which the function was deprecated. deprecated_in (Optional[str]): Version in which the function was deprecated.
details (Optional[str]): Additional details about the deprecation. details (Optional[str]): Additional details about the deprecation.
@@ -305,8 +329,8 @@ def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = Non
Callable: The decorated function. Callable: The decorated function.
""" """
def _function_wrapper(function: Callable) -> Callable: def _function_wrapper(function: Callable):
def _inner_wrapper(*args: Any, **kwargs: Any) -> Any: def _inner_wrapper(*args, **kwargs):
warn_deprecation(function.__name__, deprecated_in, details) warn_deprecation(function.__name__, deprecated_in, details)
return function(*args, **kwargs) return function(*args, **kwargs)
@@ -316,7 +340,7 @@ def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = Non
def warn_deprecation(function_name: str, deprecated_in: Optional[str] = None, details: Optional[str] = None) -> None: def warn_deprecation(function_name: str, deprecated_in: Optional[str] = None, details: Optional[str] = None) -> None:
"""Reports a deprecation warning. """Report a deprecation warning.
Args: Args:
function_name (str): Name of the deprecated function. function_name (str): Name of the deprecated function.
deprecated_in (Optional[str]): Version in which the function was deprecated. deprecated_in (Optional[str]): Version in which the function was deprecated.
@@ -330,3 +354,7 @@ def warn_deprecation(function_name: str, deprecated_in: Optional[str] = None, de
stack_info=True, stack_info=True,
stacklevel=4, stacklevel=4,
) )
# import warnings # pylint: disable=import-outside-toplevel
# warnings.warn(f"""{function_name}is deprecated{f" since {deprecated_in}" if deprecated_in else ""}{f": {details}" if details else ""}""", category=DeprecationWarning, stacklevel=2)
+23 -6
View File
@@ -35,6 +35,11 @@ def update_validation_mode(self: PropertyGroup, context: Context) -> None:
logger.info(f"Updating validation mode to: {self.validation_mode}") logger.info(f"Updating validation mode to: {self.validation_mode}")
save_preference("validation_mode", self.validation_mode) save_preference("validation_mode", self.validation_mode)
# Hide validation results if mode is set to NONE
if self.validation_mode == 'NONE':
self.show_validation_results = False
logger.debug("Validation mode set to NONE, hiding validation results")
def update_logging_state(self: PropertyGroup, context: Context) -> None: def update_logging_state(self: PropertyGroup, context: Context) -> None:
"""Updates logging state and configures logging""" """Updates logging state and configures logging"""
@@ -153,6 +158,12 @@ class AvatarToolkitSceneProperties(PropertyGroup):
default=False default=False
) )
show_validation_results: BoolProperty(
name="Show Validation Results",
default=False,
description="Show the validation results section"
)
material_search_filter: StringProperty( material_search_filter: StringProperty(
name=t("TextureAtlas.search_materials"), name=t("TextureAtlas.search_materials"),
description=t("TextureAtlas.search_materials_desc"), description=t("TextureAtlas.search_materials_desc"),
@@ -283,7 +294,7 @@ class AvatarToolkitSceneProperties(PropertyGroup):
('BASIC', t("Settings.validation_mode.basic"), t("Settings.validation_mode.basic_desc")), ('BASIC', t("Settings.validation_mode.basic"), t("Settings.validation_mode.basic_desc")),
('NONE', t("Settings.validation_mode.none"), t("Settings.validation_mode.none_desc")) ('NONE', t("Settings.validation_mode.none"), t("Settings.validation_mode.none_desc"))
], ],
default=get_preference("validation_mode", "STRICT"), default=get_preference("validation_mode", "NONE"),
update=update_validation_mode update=update_validation_mode
) )
@@ -801,14 +812,18 @@ def update_translation_mode(self: PropertyGroup, context: Context) -> None:
def update_active_armature(self: PropertyGroup, context: Context) -> None: def update_active_armature(self: PropertyGroup, context: Context) -> None:
"""Update the active armature when selection changes""" """Update the active armature when selection changes"""
if self.active_armature: if self.active_armature and self.active_armature != 'NONE':
logger.info(f"Active armature set to: {self.active_armature}") # Get the actual armature object from the identifier
armature = get_active_armature(context)
if armature:
logger.info(f"Active armature set to: {armature.name}")
# Deselect all objects first # Deselect all objects first
bpy.ops.object.select_all(action='DESELECT') bpy.ops.object.select_all(action='DESELECT')
# Select and make active the chosen armature # Select and make active the chosen armature
self.active_armature.select_set(True) armature.select_set(True)
context.view_layer.objects.active = self.active_armature context.view_layer.objects.active = armature
logger.info(f"Selected and activated armature: {self.active_armature.name}") logger.info(f"Selected and activated armature: {armature.name}")
# Clear armature caches when armature changes to ensure fresh validation # Clear armature caches when armature changes to ensure fresh validation
try: try:
@@ -816,6 +831,8 @@ def update_active_armature(self: PropertyGroup, context: Context) -> None:
clear_armature_caches() clear_armature_caches()
except ImportError: except ImportError:
pass # UI module might not be loaded yet pass # UI module might not be loaded yet
else:
logger.warning("Failed to get armature object from identifier")
else: else:
logger.info("No armature selected") logger.info("No armature selected")
+26 -4
View File
@@ -2,6 +2,7 @@ import traceback
from types import FrameType from types import FrameType
import bpy import bpy
import bpy_extras import bpy_extras
from bpy_extras import anim_utils
from numpy import double from numpy import double
from typing import Set, Dict from typing import Set, Dict
import re import re
@@ -116,11 +117,32 @@ class AvatarToolkit_OT_ConvertResonite(Operator):
def makeorexistingfcurve(action: bpy.types.Action, data_path: str, action_group: str, index=0) -> bpy.types.FCurve: def makeorexistingfcurve(action: bpy.types.Action, data_path: str, action_group: str, index=0) -> bpy.types.FCurve:
fcurve = action.fcurves.find(data_path=data_path,index=index) """Get or create an F-Curve using Blender 5.0 channelbag system.
if fcurve == None:
return action.fcurves.new(data_path,action_group=action_group,index=index) Blender 5.0 Breaking Change: The legacy action.fcurves API has been removed.
F-Curves are now accessed through channelbags. Each slot of an Action can have a channelbag.
This function has been migrated to use bpy_extras.anim_utils.action_ensure_channelbag_for_slot().
"""
# Get the action slot (assumes single slot for now - armature actions typically use first slot)
if not action.slots:
slot = action.slots.new(for_id=bpy.context.object.data if bpy.context.object and bpy.context.object.type == 'ARMATURE' else None)
else: else:
print("fcurve with data \""+data_path+"\" already exists") slot = action.slots[0]
# Get or create channelbag for this slot
channelbag = anim_utils.action_ensure_channelbag_for_slot(action, slot)
# Use ensure() to get existing or create new F-Curve
fcurve = channelbag.fcurves.ensure(data_path, index=index, group_name=action_group)
if fcurve:
return fcurve
else:
print(f"fcurve with data \"{data_path}\" creation failed")
# Fallback: try to find or create manually
fcurve = channelbag.fcurves.find(data_path, index=index)
if fcurve is None:
fcurve = channelbag.fcurves.new(data_path, index=index, group_name=action_group)
return fcurve return fcurve
class AvatarToolKit_OT_AnimX_Importer(Operator,bpy_extras.io_utils.ImportHelper): class AvatarToolKit_OT_AnimX_Importer(Operator,bpy_extras.io_utils.ImportHelper):
+61 -4
View File
@@ -67,8 +67,14 @@ class TranslationCache:
"""Load cache from file""" """Load cache from file"""
try: try:
if os.path.exists(self._cache_file): if os.path.exists(self._cache_file):
# Try UTF-8 first, fallback to other encodings
try:
with open(self._cache_file, 'r', encoding='utf-8') as f: with open(self._cache_file, 'r', encoding='utf-8') as f:
self._cache = json.load(f) self._cache = json.load(f)
except UnicodeDecodeError:
# Try with UTF-8 error handling
with open(self._cache_file, 'r', encoding='utf-8', errors='replace') as f:
self._cache = json.load(f)
logger.debug(f"Loaded translation cache with {len(self._cache)} entries") logger.debug(f"Loaded translation cache with {len(self._cache)} entries")
else: else:
self._cache = {} self._cache = {}
@@ -147,6 +153,15 @@ class AvatarToolkitTranslationManager:
def translate_single(self, name: str, category: str = "auto", def translate_single(self, name: str, category: str = "auto",
source_lang: str = "ja", target_lang: str = "en") -> TranslationResult: source_lang: str = "ja", target_lang: str = "en") -> TranslationResult:
"""Translate a single name with comprehensive fallback logic""" """Translate a single name with comprehensive fallback logic"""
# Import safe_decode_text from translation_service
from .translation_service import safe_decode_text
# Ensure name is properly encoded
try:
name = safe_decode_text(name)
except Exception as e:
logger.warning(f"Failed to decode name: {e}")
if not name or not name.strip(): if not name or not name.strip():
return TranslationResult(name, name, "skipped") return TranslationResult(name, name, "skipped")
@@ -300,6 +315,8 @@ class AvatarToolkitTranslationManager:
def _process_category_batch_optimized(self, category_jobs: List[TranslationJob], def _process_category_batch_optimized(self, category_jobs: List[TranslationJob],
completed: int, total_jobs: int, start_time: float) -> Optional[List[TranslationResult]]: completed: int, total_jobs: int, start_time: float) -> Optional[List[TranslationResult]]:
"""Process a batch of jobs from the same category using optimized API batch translation""" """Process a batch of jobs from the same category using optimized API batch translation"""
from .translation_service import safe_decode_text
if not category_jobs: if not category_jobs:
return [] return []
@@ -315,6 +332,14 @@ class AvatarToolkitTranslationManager:
results[i] = TranslationResult(job.name, job.name, "skipped", category=job.category) results[i] = TranslationResult(job.name, job.name, "skipped", category=job.category)
continue continue
# Ensure name is properly encoded
try:
original_name = safe_decode_text(job.name.strip())
except Exception as e:
logger.warning(f"Failed to decode job name: {e}")
original_name = job.name.strip()
continue
original_name = job.name.strip() original_name = job.name.strip()
# Check cache first # Check cache first
@@ -426,13 +451,21 @@ class AvatarToolkitTranslationManager:
def translate_armature_bones(self, armature: Object, apply_results: bool = True) -> List[TranslationResult]: def translate_armature_bones(self, armature: Object, apply_results: bool = True) -> List[TranslationResult]:
"""Translate all bone names in an armature""" """Translate all bone names in an armature"""
from .translation_service import safe_decode_text
if not armature or armature.type != 'ARMATURE': if not armature or armature.type != 'ARMATURE':
return [] return []
jobs = [] jobs = []
for bone in armature.data.bones: for bone in armature.data.bones:
try:
bone_name = safe_decode_text(bone.name)
except Exception as e:
logger.warning(f"Failed to decode bone name, using as-is: {e}")
bone_name = bone.name
jobs.append(TranslationJob( jobs.append(TranslationJob(
name=bone.name, name=bone_name,
category="bones", category="bones",
object_ref=bone, object_ref=bone,
property_name="name" property_name="name"
@@ -442,13 +475,21 @@ class AvatarToolkitTranslationManager:
def translate_object_shapekeys(self, mesh_obj: Object, apply_results: bool = True) -> List[TranslationResult]: def translate_object_shapekeys(self, mesh_obj: Object, apply_results: bool = True) -> List[TranslationResult]:
"""Translate all shape key names in a mesh object""" """Translate all shape key names in a mesh object"""
from .translation_service import safe_decode_text
if not mesh_obj or mesh_obj.type != 'MESH' or not mesh_obj.data.shape_keys: if not mesh_obj or mesh_obj.type != 'MESH' or not mesh_obj.data.shape_keys:
return [] return []
jobs = [] jobs = []
for shape_key in mesh_obj.data.shape_keys.key_blocks: for shape_key in mesh_obj.data.shape_keys.key_blocks:
try:
sk_name = safe_decode_text(shape_key.name)
except Exception as e:
logger.warning(f"Failed to decode shape key name, using as-is: {e}")
sk_name = shape_key.name
jobs.append(TranslationJob( jobs.append(TranslationJob(
name=shape_key.name, name=sk_name,
category="shapekeys", category="shapekeys",
object_ref=shape_key, object_ref=shape_key,
property_name="name" property_name="name"
@@ -458,6 +499,8 @@ class AvatarToolkitTranslationManager:
def translate_scene_materials(self, apply_results: bool = True) -> List[TranslationResult]: def translate_scene_materials(self, apply_results: bool = True) -> List[TranslationResult]:
"""Translate all material names in the scene""" """Translate all material names in the scene"""
from .translation_service import safe_decode_text
jobs = [] jobs = []
processed_materials: Set[str] = set() processed_materials: Set[str] = set()
@@ -465,8 +508,14 @@ class AvatarToolkitTranslationManager:
if obj.type == 'MESH' and obj.data.materials: if obj.type == 'MESH' and obj.data.materials:
for material in obj.data.materials: for material in obj.data.materials:
if material and material.name not in processed_materials: if material and material.name not in processed_materials:
try:
mat_name = safe_decode_text(material.name)
except Exception as e:
logger.warning(f"Failed to decode material name, using as-is: {e}")
mat_name = material.name
jobs.append(TranslationJob( jobs.append(TranslationJob(
name=material.name, name=mat_name,
category="materials", category="materials",
object_ref=material, object_ref=material,
property_name="name" property_name="name"
@@ -478,14 +527,22 @@ class AvatarToolkitTranslationManager:
def translate_scene_objects(self, object_types: Optional[Set[str]] = None, def translate_scene_objects(self, object_types: Optional[Set[str]] = None,
apply_results: bool = True) -> List[TranslationResult]: apply_results: bool = True) -> List[TranslationResult]:
"""Translate all object names in the scene""" """Translate all object names in the scene"""
from .translation_service import safe_decode_text
if object_types is None: if object_types is None:
object_types = {'MESH', 'ARMATURE', 'EMPTY'} object_types = {'MESH', 'ARMATURE', 'EMPTY'}
jobs = [] jobs = []
for obj in bpy.data.objects: for obj in bpy.data.objects:
if obj.type in object_types: if obj.type in object_types:
try:
obj_name = safe_decode_text(obj.name)
except Exception as e:
logger.warning(f"Failed to decode object name, using as-is: {e}")
obj_name = obj.name
jobs.append(TranslationJob( jobs.append(TranslationJob(
name=obj.name, name=obj_name,
category="objects", category="objects",
object_ref=obj, object_ref=obj,
property_name="name" property_name="name"
+51
View File
@@ -14,6 +14,43 @@ from .logging_setup import logger
from .addon_preferences import save_preference, get_preference from .addon_preferences import save_preference, get_preference
def safe_decode_text(text: str) -> str:
"""Safely decode text that might be in various encodings (UTF-8, Shift-JIS, etc.)"""
if not text:
return text
# If it's already a proper string, return it
if isinstance(text, str):
try:
# Test if it's valid UTF-8
text.encode('utf-8')
return text
except (UnicodeDecodeError, UnicodeEncodeError):
pass
# Try common encodings for Japanese text
encodings = ['utf-8', 'shift-jis', 'cp932', 'euc-jp', 'iso-2022-jp']
for encoding in encodings:
try:
if isinstance(text, bytes):
return text.decode(encoding)
else:
# Try to re-encode and decode
return text.encode('latin-1', errors='ignore').decode(encoding, errors='ignore')
except (UnicodeDecodeError, UnicodeEncodeError, AttributeError):
continue
# Fallback: replace problematic characters
try:
if isinstance(text, bytes):
return text.decode('utf-8', errors='replace')
else:
return str(text).encode('utf-8', errors='replace').decode('utf-8')
except:
return str(text)
@dataclass @dataclass
class TranslationRequest: class TranslationRequest:
"""Represents a translation request""" """Represents a translation request"""
@@ -116,6 +153,8 @@ class DeepLService(TranslationService):
def translate_text(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> str: def translate_text(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> str:
"""Translate text using DeepL API""" """Translate text using DeepL API"""
# Ensure text is properly encoded
text = safe_decode_text(text)
logger.info(f"DeepL: Starting translation of '{text}' from {source_lang} to {target_lang}") logger.info(f"DeepL: Starting translation of '{text}' from {source_lang} to {target_lang}")
if not text or not text.strip(): if not text or not text.strip():
@@ -220,6 +259,8 @@ class DeepLService(TranslationService):
if not texts: if not texts:
return [] return []
# Ensure all texts are properly encoded
texts = [safe_decode_text(text) for text in texts]
logger.info(f"DeepL: Starting batch translation of {len(texts)} texts from {source_lang} to {target_lang}") logger.info(f"DeepL: Starting batch translation of {len(texts)} texts from {source_lang} to {target_lang}")
results = [None] * len(texts) results = [None] * len(texts)
@@ -341,6 +382,8 @@ class MyMemoryService(TranslationService):
def translate_text(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> str: def translate_text(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> str:
"""Translate text using MyMemory free API""" """Translate text using MyMemory free API"""
# Ensure text is properly encoded
text = safe_decode_text(text)
logger.info(f"MyMemory: Starting translation of '{text}' from {source_lang} to {target_lang}") logger.info(f"MyMemory: Starting translation of '{text}' from {source_lang} to {target_lang}")
if not text or not text.strip(): if not text or not text.strip():
@@ -430,6 +473,8 @@ class MyMemoryService(TranslationService):
if not texts: if not texts:
return [] return []
# Ensure all texts are properly encoded
texts = [safe_decode_text(text) for text in texts]
logger.info(f"MyMemory: Starting batch translation of {len(texts)} texts from {source_lang} to {target_lang}") logger.info(f"MyMemory: Starting batch translation of {len(texts)} texts from {source_lang} to {target_lang}")
results = [None] * len(texts) results = [None] * len(texts)
@@ -545,6 +590,8 @@ class LibreTranslateService(TranslationService):
def translate_text(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> str: def translate_text(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> str:
"""Translate text using LibreTranslate API""" """Translate text using LibreTranslate API"""
# Ensure text is properly encoded
text = safe_decode_text(text)
logger.info(f"LibreTranslate: Starting translation of '{text}' from {source_lang} to {target_lang}") logger.info(f"LibreTranslate: Starting translation of '{text}' from {source_lang} to {target_lang}")
if not text or not text.strip(): if not text or not text.strip():
@@ -658,6 +705,8 @@ class LibreTranslateService(TranslationService):
if not texts: if not texts:
return [] return []
# Ensure all texts are properly encoded
texts = [safe_decode_text(text) for text in texts]
logger.info(f"LibreTranslate: Starting batch translation of {len(texts)} texts from {source_lang} to {target_lang}") logger.info(f"LibreTranslate: Starting batch translation of {len(texts)} texts from {source_lang} to {target_lang}")
# Check cache and separate cached vs uncached texts # Check cache and separate cached vs uncached texts
@@ -814,6 +863,8 @@ class TranslationServiceManager:
def translate_with_fallback(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> Tuple[str, str]: def translate_with_fallback(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> Tuple[str, str]:
"""Translate text with automatic fallback to other services""" """Translate text with automatic fallback to other services"""
# Ensure text is properly encoded
text = safe_decode_text(text)
if not text or not text.strip(): if not text or not text.strip():
return text, "none" return text, "none"
+1 -1
View File
@@ -20,7 +20,7 @@ GITHUB_REPO = "teamneoneko/Avatar-Toolkit"
# Define which version series this installation can update to # Define which version series this installation can update to
# For example: ["0.1"] means only look for 0.1.x updates # For example: ["0.1"] means only look for 0.1.x updates
# ["0.2", "0.3"] would look for both 0.2.x and 0.3.x # ["0.2", "0.3"] would look for both 0.2.x and 0.3.x
ALLOWED_ = ["0.3, 0.4"] ALLOWED_VERSION_SERIES = ["0.6"]
is_checking_for_update: bool = False is_checking_for_update: bool = False
update_needed: bool = False update_needed: bool = False
+28 -14
View File
@@ -28,7 +28,15 @@ def scale_images_to_largest(images: List[Image]) -> tuple[int, int]:
x: int = 0 x: int = 0
y: int = 0 y: int = 0
valid_images = [img for img in images if img and img.has_data] valid_images = []
for img in images:
if img:
try:
if img.has_data:
valid_images.append(img)
except ReferenceError:
# Image has been removed from Blender's memory
pass
if not valid_images: if not valid_images:
return 0, 0 return 0, 0
@@ -66,50 +74,56 @@ def get_material_images_from_scene(context: Context) -> list[MaterialImageList]:
new_mat_image_item.albedo = bpy.data.images[mat_slot.material.texture_atlas_albedo] new_mat_image_item.albedo = bpy.data.images[mat_slot.material.texture_atlas_albedo]
except Exception: except Exception:
name = mat_slot.material.name + "_albedo_replacement" name = mat_slot.material.name + "_albedo_replacement"
if name in bpy.data.images: if name not in bpy.data.images:
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
new_mat_image_item.albedo = bpy.data.images.new(name=name, width=32, height=32, alpha=True) new_mat_image_item.albedo = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
new_mat_image_item.albedo.pixels[:] = numpy.tile(numpy.array([0.0,0.0,0.0,1.0]), 32*32) new_mat_image_item.albedo.pixels[:] = numpy.tile(numpy.array([0.0,0.0,0.0,1.0]), 32*32)
else:
new_mat_image_item.albedo = bpy.data.images[name]
try: try:
new_mat_image_item.normal = bpy.data.images[mat_slot.material.texture_atlas_normal] new_mat_image_item.normal = bpy.data.images[mat_slot.material.texture_atlas_normal]
except Exception: except Exception:
name = mat_slot.material.name + "_normal_replacement" name = mat_slot.material.name + "_normal_replacement"
if name in bpy.data.images: if name not in bpy.data.images:
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
new_mat_image_item.normal = bpy.data.images.new(name=name, width=32, height=32, alpha=True) new_mat_image_item.normal = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
new_mat_image_item.normal.pixels[:] = numpy.tile(numpy.array([0.5,0.5,1.0,1.0]), 32*32) new_mat_image_item.normal.pixels[:] = numpy.tile(numpy.array([0.5,0.5,1.0,1.0]), 32*32)
else:
new_mat_image_item.normal = bpy.data.images[name]
try: try:
new_mat_image_item.emission = bpy.data.images[mat_slot.material.texture_atlas_emission] new_mat_image_item.emission = bpy.data.images[mat_slot.material.texture_atlas_emission]
except Exception: except Exception:
name = mat_slot.material.name + "_emission_replacement" name = mat_slot.material.name + "_emission_replacement"
if name in bpy.data.images: if name not in bpy.data.images:
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
new_mat_image_item.emission = bpy.data.images.new(name=name, width=32, height=32, alpha=True) new_mat_image_item.emission = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
new_mat_image_item.emission.pixels[:] = numpy.tile(numpy.array([0.0,0.0,0.0,1.0]), 32*32) new_mat_image_item.emission.pixels[:] = numpy.tile(numpy.array([0.0,0.0,0.0,1.0]), 32*32)
else:
new_mat_image_item.emission = bpy.data.images[name]
try: try:
new_mat_image_item.ambient_occlusion = bpy.data.images[mat_slot.material.texture_atlas_ambient_occlusion] new_mat_image_item.ambient_occlusion = bpy.data.images[mat_slot.material.texture_atlas_ambient_occlusion]
except Exception: except Exception:
name = mat_slot.material.name + "_ambient_occlusion_replacement" name = mat_slot.material.name + "_ambient_occlusion_replacement"
if name in bpy.data.images: if name not in bpy.data.images:
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
new_mat_image_item.ambient_occlusion = bpy.data.images.new(name=name, width=32, height=32, alpha=True) new_mat_image_item.ambient_occlusion = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
new_mat_image_item.ambient_occlusion.pixels[:] = numpy.tile(numpy.array([1.0,1.0,1.0,1.0]), 32*32) new_mat_image_item.ambient_occlusion.pixels[:] = numpy.tile(numpy.array([1.0,1.0,1.0,1.0]), 32*32)
else:
new_mat_image_item.ambient_occlusion = bpy.data.images[name]
try: try:
new_mat_image_item.height = bpy.data.images[mat_slot.material.texture_atlas_height] new_mat_image_item.height = bpy.data.images[mat_slot.material.texture_atlas_height]
except Exception: except Exception:
name = mat_slot.material.name + "_height_replacement" name = mat_slot.material.name + "_height_replacement"
if name in bpy.data.images: if name not in bpy.data.images:
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
new_mat_image_item.height = bpy.data.images.new(name=name, width=32, height=32, alpha=True) new_mat_image_item.height = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
new_mat_image_item.height.pixels[:] = numpy.tile(numpy.array([0.5,0.5,0.5,1.0]), 32*32) new_mat_image_item.height.pixels[:] = numpy.tile(numpy.array([0.5,0.5,0.5,1.0]), 32*32)
else:
new_mat_image_item.height = bpy.data.images[name]
try: try:
new_mat_image_item.roughness = bpy.data.images[mat_slot.material.texture_atlas_roughness] new_mat_image_item.roughness = bpy.data.images[mat_slot.material.texture_atlas_roughness]
except Exception: except Exception:
name = mat_slot.material.name + "_roughness_replacement" name = mat_slot.material.name + "_roughness_replacement"
if name in bpy.data.images: if name not in bpy.data.images:
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
new_mat_image_item.roughness = bpy.data.images.new(name=name, width=32, height=32, alpha=True) new_mat_image_item.roughness = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
new_mat_image_item.roughness.pixels[:] = numpy.tile(numpy.array([1.0,1.0,1.0,0.0]), 32*32) new_mat_image_item.roughness.pixels[:] = numpy.tile(numpy.array([1.0,1.0,1.0,0.0]), 32*32)
else:
new_mat_image_item.roughness = bpy.data.images[name]
new_mat_image_item.material = mat_slot.material new_mat_image_item.material = mat_slot.material
new_mat_image_item.parent_mesh = obj new_mat_image_item.parent_mesh = obj
@@ -227,7 +241,7 @@ class AvatarToolKit_OT_AtlasMaterials(Operator):
# Create material nodes # Create material nodes
atlased_mat.material = bpy.data.materials.new( atlased_mat.material = bpy.data.materials.new(
name=f"Atlas_Final_{context.scene.name}_{Path(bpy.data.filepath).stem}") name=f"Atlas_Final_{context.scene.name}_{Path(bpy.data.filepath).stem}")
atlased_mat.material.use_nodes = True # Note: material.use_nodes is deprecated in Blender 5.0 - materials always use nodes
atlased_mat.material.node_tree.nodes.clear() atlased_mat.material.node_tree.nodes.clear()
principled_node = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled") principled_node = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled")
+10 -1
View File
@@ -1,5 +1,7 @@
import traceback import traceback
import bpy import bpy
import bpy_extras
from bpy_extras import anim_utils
import re import re
from bpy.types import Operator, Context, EditBone, Object, Armature, Mesh from bpy.types import Operator, Context, EditBone, Object, Armature, Mesh
from typing import Optional, Dict, Any, List, Tuple from typing import Optional, Dict, Any, List, Tuple
@@ -347,10 +349,17 @@ class AvatarToolKit_OT_FlipCurrentKeyFrames(Operator):
armature_data.bones.foreach_set("select", [False] * len(armature_data.bones)) armature_data.bones.foreach_set("select", [False] * len(armature_data.bones))
# Get channelbag for the action using Blender 5.0 API
action = armature.animation_data.action
if not action.slots:
slot = action.slots.new(for_id=armature.data)
else:
slot = action.slots[0]
channelbag = anim_utils.action_ensure_channelbag_for_slot(action, slot)
#create a set for every frame time where we need to key a keyframe for the flipped pose #create a set for every frame time where we need to key a keyframe for the flipped pose
times: Dict[float,list[bpy.types.FCurve]] = {} times: Dict[float,list[bpy.types.FCurve]] = {}
for curve in armature.animation_data.action.fcurves: for curve in channelbag.fcurves:
if not curve.data_path.startswith("pose"): if not curve.data_path.startswith("pose"):
continue continue
for point in curve.keyframe_points: for point in curve.keyframe_points:
+24 -3
View File
@@ -8,6 +8,26 @@ from ...core.translations import t
from ...core.logging_setup import logger from ...core.logging_setup import logger
import traceback import traceback
def get_uv_vertex_selection(mesh: Mesh) -> List[bool]:
"""
Get UV vertex selection state for Blender 5.0.
UV selection is stored in mesh attributes (.uv_select_vert).
"""
uv_select_attr = mesh.attributes['.uv_select_vert']
selection = [False] * len(mesh.loops)
uv_select_attr.data.foreach_get('value', selection)
return selection
def set_uv_vertex_selection(mesh: Mesh, loop_index: int, value: bool) -> None:
"""
Set UV vertex selection state for Blender 5.0.
UV selection is stored in mesh attributes (.uv_select_vert).
"""
uv_select_attr = mesh.attributes['.uv_select_vert']
uv_select_attr.data[loop_index].value = value
class GenerateLoopTreeResult(TypedDict): class GenerateLoopTreeResult(TypedDict):
tree: Dict[str, Set[str]] tree: Dict[str, Set[str]]
selected_loops: Dict[str, List[int]] selected_loops: Dict[str, List[int]]
@@ -78,8 +98,9 @@ class AvatarToolkit_OT_AlignUVEdgesToTarget(Operator):
# that two vertices share the same face loop, and therefore are connected. # that two vertices share the same face loop, and therefore are connected.
#hmmm real stupid grimlin hours with this one. Using a string as the index of a dictionary of loop corners that end up on the same coordinate #hmmm real stupid grimlin hours with this one. Using a string as the index of a dictionary of loop corners that end up on the same coordinate
for k,i in enumerate(uv_lay.vertex_selection): uv_selection = get_uv_vertex_selection(me)
if (i.value == True) and (bm.verts[me.loops[k].vertex_index].select == True) and (bm.verts[me.loops[k].vertex_index].hide == False): for k, is_selected in enumerate(uv_selection):
if (is_selected == True) and (bm.verts[me.loops[k].vertex_index].select == True) and (bm.verts[me.loops[k].vertex_index].hide == False):
key = np.array(uv_lay.uv[k].vector[:]) key = np.array(uv_lay.uv[k].vector[:])
key = key.round(decimals=5) key = key.round(decimals=5)
@@ -140,7 +161,7 @@ class AvatarToolkit_OT_AlignUVEdgesToTarget(Operator):
uv_lay = me.uv_layers.active uv_lay = me.uv_layers.active
for uvcoordstr in vert_target_loops: for uvcoordstr in vert_target_loops:
for loop in vert_target_loops[uvcoordstr]: for loop in vert_target_loops[uvcoordstr]:
uv_lay.vertex_selection[loop].value = True set_uv_vertex_selection(me, loop, True)
bm.free() bm.free()
me.validate() me.validate()
+11 -2
View File
@@ -1,7 +1,7 @@
{ {
"authors": ["Avatar Toolkit Team"], "authors": ["Avatar Toolkit Team"],
"messages": { "messages": {
"AvatarToolkit.label": "Avatar Toolkit (Alpha 0.4.0)", "AvatarToolkit.label": "Avatar Toolkit (Alpha 0.5.2)",
"AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there", "AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there",
"AvatarToolkit.desc2": "will be issues, if you find any issues,", "AvatarToolkit.desc2": "will be issues, if you find any issues,",
"AvatarToolkit.desc3": "please report it on our Github.", "AvatarToolkit.desc3": "please report it on our Github.",
@@ -117,6 +117,15 @@
"Validation.clear_bone_highlighting": "Clear Bone Highlighting", "Validation.clear_bone_highlighting": "Clear Bone Highlighting",
"Validation.clear_bone_highlighting_desc": "Remove bone highlighting and reset bone colors to default", "Validation.clear_bone_highlighting_desc": "Remove bone highlighting and reset bone colors to default",
"Validation.highlighting_cleared": "Bone highlighting cleared successfully", "Validation.highlighting_cleared": "Bone highlighting cleared successfully",
"Validation.label": "Armature Validation",
"Validation.validate_now": "Validate Armature Now",
"Validation.validate_now_desc": "Run armature validation and display detailed results",
"Validation.results": "Validation Results",
"Validation.tpose.validate_now": "Validate T-Pose Now",
"Armature.validation.acceptable_standard.success": "Armature meets acceptable standards",
"Armature.validation.acceptable_standard.note": "This is a valid armature format that is compatible with most avatar systems",
"Armature.validation.acceptable_standard.option": "You can standardize the armature if desired",
"Mesh.validation.no_data": "No mesh data", "Mesh.validation.no_data": "No mesh data",
"Mesh.validation.no_vertex_groups": "No vertex groups found", "Mesh.validation.no_vertex_groups": "No vertex groups found",
@@ -191,6 +200,7 @@
"Tools.digitigrade_error": "Failed to create digitigrade legs: {error}", "Tools.digitigrade_error": "Failed to create digitigrade legs: {error}",
"Tools.digitigrade_success": "Successfully created digitigrade leg setup", "Tools.digitigrade_success": "Successfully created digitigrade leg setup",
"Tools.processing_leg": "Processing leg bone: {bone}", "Tools.processing_leg": "Processing leg bone: {bone}",
"Tools.weight_title": "Weight Tools",
"Tools.merge_twist_bones": "Keep Twist Bones", "Tools.merge_twist_bones": "Keep Twist Bones",
"Tools.merge_twist_bones_desc": "When checked, twist bones will be kept, even if there are zero-weight", "Tools.merge_twist_bones_desc": "When checked, twist bones will be kept, even if there are zero-weight",
"Tools.clean_weights": "Remove Zero Weight Bones", "Tools.clean_weights": "Remove Zero Weight Bones",
@@ -587,7 +597,6 @@
"VRM.validation.hierarchy_issues": "Conversion completed but hierarchy validation found issues:", "VRM.validation.hierarchy_issues": "Conversion completed but hierarchy validation found issues:",
"VRM.validation.armature_passed": "Armature passes standard validation", "VRM.validation.armature_passed": "Armature passes standard validation",
"VRM.validation.failed": "Conversion completed but validation failed: {error}", "VRM.validation.failed": "Conversion completed but validation failed: {error}",
"VRM.remove_colliders": "Remove Colliders",
"VRM.remove_colliders_desc": "Remove VRM collider bones during conversion", "VRM.remove_colliders_desc": "Remove VRM collider bones during conversion",
"VRM.remove_root": "Remove Root Bone", "VRM.remove_root": "Remove Root Bone",
"VRM.remove_root_desc": "Remove unnecessary VRM root bone and make Hips the root bone", "VRM.remove_root_desc": "Remove unnecessary VRM root bone and make Hips the root bone",
+11 -2
View File
@@ -1,7 +1,7 @@
{ {
"authors": ["Avatar Toolkit Team"], "authors": ["Avatar Toolkit Team"],
"messages": { "messages": {
"AvatarToolkit.label": "アバターツールキット (アルファ 0.4.0)", "AvatarToolkit.label": "アバターツールキット (アルファ 0.5.2)",
"AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、", "AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、",
"AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、", "AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、",
"AvatarToolkit.desc3": "GitHubで報告してください。", "AvatarToolkit.desc3": "GitHubで報告してください。",
@@ -117,6 +117,15 @@
"Validation.clear_bone_highlighting": "ボーンの強調表示をクリア", "Validation.clear_bone_highlighting": "ボーンの強調表示をクリア",
"Validation.clear_bone_highlighting_desc": "ボーンの強調表示を削除し、ボーンの色をデフォルトにリセット", "Validation.clear_bone_highlighting_desc": "ボーンの強調表示を削除し、ボーンの色をデフォルトにリセット",
"Validation.highlighting_cleared": "ボーンの強調表示が正常にクリアされました", "Validation.highlighting_cleared": "ボーンの強調表示が正常にクリアされました",
"Validation.label": "アーマチュア検証",
"Validation.validate_now": "アーマチュアを検証する",
"Validation.validate_now_desc": "アーマチュア検証を実行し、詳細な結果を表示",
"Validation.results": "検証結果",
"Validation.tpose.validate_now": "T-ポーズを検証する",
"Armature.validation.acceptable_standard.success": "アーマチュアが許容可能な標準を満たしています",
"Armature.validation.acceptable_standard.note": "これは、ほとんどのアバターシステムと互換性のある有効なアーマチュア形式です",
"Armature.validation.acceptable_standard.option": "必要に応じてアーマチュアを標準化できます",
"Mesh.validation.no_data": "メッシュデータがありません", "Mesh.validation.no_data": "メッシュデータがありません",
"Mesh.validation.no_vertex_groups": "頂点グループが見つかりません", "Mesh.validation.no_vertex_groups": "頂点グループが見つかりません",
@@ -194,6 +203,7 @@
"Tools.digitigrade_error": "デジティグレード脚の作成に失敗: {error}", "Tools.digitigrade_error": "デジティグレード脚の作成に失敗: {error}",
"Tools.digitigrade_success": "デジティグレード脚の設定が正常に作成されました", "Tools.digitigrade_success": "デジティグレード脚の設定が正常に作成されました",
"Tools.processing_leg": "脚のボーンを処理中: {bone}", "Tools.processing_leg": "脚のボーンを処理中: {bone}",
"Tools.weight_title": "ウェイトツール",
"Tools.merge_twist_bones": "ツイストボーンを保持", "Tools.merge_twist_bones": "ツイストボーンを保持",
"Tools.merge_twist_bones_desc": "チェックすると、ウェイトがゼロでもツイストボーンが保持されます", "Tools.merge_twist_bones_desc": "チェックすると、ウェイトがゼロでもツイストボーンが保持されます",
"Tools.clean_weights": "ゼロウェイトボーンを削除", "Tools.clean_weights": "ゼロウェイトボーンを削除",
@@ -549,7 +559,6 @@
"VRM.armature_name": "アーマチュア: {name}", "VRM.armature_name": "アーマチュア: {name}",
"VRM.armature_detected": "VRMアーマチュアが検出されました", "VRM.armature_detected": "VRMアーマチュアが検出されました",
"VRM.no_vrm_bones_detected": "VRMボーンが検出されませんでした", "VRM.no_vrm_bones_detected": "VRMボーンが検出されませんでした",
"VRM.remove_colliders": "コライダーを削除",
"VRM.remove_root_bone": "ルートボーンを削除", "VRM.remove_root_bone": "ルートボーンを削除",
"VRM.convert_to_unity_format": "Unity形式に変換", "VRM.convert_to_unity_format": "Unity形式に変換",
"VRM.convert_to_unity.label": "VRMをUnityに変換", "VRM.convert_to_unity.label": "VRMをUnityに変換",
+11 -2
View File
@@ -1,7 +1,7 @@
{ {
"authors": ["Avatar Toolkit Team"], "authors": ["Avatar Toolkit Team"],
"messages": { "messages": {
"AvatarToolkit.label": "아바타 툴킷 (알파 0.4.0)", "AvatarToolkit.label": "아바타 툴킷 (알파 0.5.2)",
"AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로", "AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로",
"AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면", "AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면",
"AvatarToolkit.desc3": "Github에 보고해 주세요.", "AvatarToolkit.desc3": "Github에 보고해 주세요.",
@@ -117,6 +117,15 @@
"Validation.clear_bone_highlighting": "본 강조 표시 지우기", "Validation.clear_bone_highlighting": "본 강조 표시 지우기",
"Validation.clear_bone_highlighting_desc": "본 강조 표시를 제거하고 본 색상을 기본값으로 재설정", "Validation.clear_bone_highlighting_desc": "본 강조 표시를 제거하고 본 색상을 기본값으로 재설정",
"Validation.highlighting_cleared": "본 강조 표시 지우기 성공", "Validation.highlighting_cleared": "본 강조 표시 지우기 성공",
"Validation.label": "아마추어 검증",
"Validation.validate_now": "지금 아마추어 검증",
"Validation.validate_now_desc": "아마추어 검증을 실행하고 자세한 결과 표시",
"Validation.results": "검증 결과",
"Validation.tpose.validate_now": "지금 T-포즈 검증",
"Armature.validation.acceptable_standard.success": "아마추어가 허용 가능한 표준을 충족합니다",
"Armature.validation.acceptable_standard.note": "이것은 대부분의 아바타 시스템과 호환되는 유효한 아마추어 형식입니다",
"Armature.validation.acceptable_standard.option": "필요한 경우 아마추어를 표준화할 수 있습니다",
"Mesh.validation.no_data": "메시 데이터 없음", "Mesh.validation.no_data": "메시 데이터 없음",
"Mesh.validation.no_vertex_groups": "버텍스 그룹을 찾을 수 없음", "Mesh.validation.no_vertex_groups": "버텍스 그룹을 찾을 수 없음",
@@ -194,6 +203,7 @@
"Tools.digitigrade_error": "디지티그레이드 다리 생성 실패: {error}", "Tools.digitigrade_error": "디지티그레이드 다리 생성 실패: {error}",
"Tools.digitigrade_success": "디지티그레이드 다리 설정 생성 성공", "Tools.digitigrade_success": "디지티그레이드 다리 설정 생성 성공",
"Tools.processing_leg": "다리 본 처리 중: {bone}", "Tools.processing_leg": "다리 본 처리 중: {bone}",
"Tools.weight_title": "가중치 도구",
"Tools.merge_twist_bones": "트위스트 본 유지", "Tools.merge_twist_bones": "트위스트 본 유지",
"Tools.merge_twist_bones_desc": "체크하면 가중치가 0이더라도 트위스트 본이 유지됩니다", "Tools.merge_twist_bones_desc": "체크하면 가중치가 0이더라도 트위스트 본이 유지됩니다",
"Tools.clean_weights": "가중치 0인 본 제거", "Tools.clean_weights": "가중치 0인 본 제거",
@@ -549,7 +559,6 @@
"VRM.armature_name": "아마추어: {name}", "VRM.armature_name": "아마추어: {name}",
"VRM.armature_detected": "VRM 아마추어 감지됨", "VRM.armature_detected": "VRM 아마추어 감지됨",
"VRM.no_vrm_bones_detected": "VRM 본이 감지되지 않음", "VRM.no_vrm_bones_detected": "VRM 본이 감지되지 않음",
"VRM.remove_colliders": "콜라이더 제거",
"VRM.remove_root_bone": "루트 본 제거", "VRM.remove_root_bone": "루트 본 제거",
"VRM.convert_to_unity_format": "Unity 형식으로 변환", "VRM.convert_to_unity_format": "Unity 형식으로 변환",
"VRM.convert_to_unity.label": "VRM을 Unity로 변환", "VRM.convert_to_unity.label": "VRM을 Unity로 변환",
+3 -1
View File
@@ -2,6 +2,7 @@ from bpy.types import UIList, Panel, UILayout, Object, Context, Material, Operat
import bpy import bpy
from math import sqrt from math import sqrt
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from .panel_layout import get_panel_order, should_open_by_default
from ..core.common import SceneMatClass, MaterialListBool, get_active_armature from ..core.common import SceneMatClass, MaterialListBool, get_active_armature
from ..functions.atlas_materials import AvatarToolKit_OT_AtlasMaterials from ..functions.atlas_materials import AvatarToolKit_OT_AtlasMaterials
from ..core.translations import t from ..core.translations import t
@@ -214,7 +215,8 @@ class AvatarToolKit_PT_TextureAtlasPanel(Panel):
bl_region_type = 'UI' bl_region_type = 'UI'
bl_category = CATEGORY_NAME bl_category = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 7 bl_order = get_panel_order('texture_atlas')
bl_options = set() if not should_open_by_default('TEXTURE_ATLAS') else {'DEFAULT_CLOSED'}
def draw(self, context: Context): def draw(self, context: Context):
layout = self.layout layout = self.layout
+3 -2
View File
@@ -2,6 +2,7 @@ import bpy
from typing import Set, List, Tuple, Any from typing import Set, List, Tuple, Any
from bpy.types import Panel, Context, UILayout, Operator, Event, WindowManager from bpy.types import Panel, Context, UILayout, Operator, Event, WindowManager
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from .panel_layout import get_panel_order, should_open_by_default
from ..functions.custom_tools.mesh_attachment import AvatarToolkit_OT_AttachMesh from ..functions.custom_tools.mesh_attachment import AvatarToolkit_OT_AttachMesh
from ..functions.custom_tools.armature_merging import AvatarToolkit_OT_MergeArmature from ..functions.custom_tools.armature_merging import AvatarToolkit_OT_MergeArmature
from ..core.translations import t from ..core.translations import t
@@ -112,8 +113,8 @@ class AvatarToolKit_PT_CustomPanel(Panel):
bl_region_type: str = 'UI' bl_region_type: str = 'UI'
bl_category: str = CATEGORY_NAME bl_category: str = CATEGORY_NAME
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order: int = 4 bl_order: int = get_panel_order('custom_avatar')
bl_options: Set[str] = {'DEFAULT_CLOSED'} bl_options: Set[str] = set() if not should_open_by_default('CUSTOM_AVATAR') else {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None: def draw(self, context: Context) -> None:
"""Draw the custom avatar panel UI""" """Draw the custom avatar panel UI"""
+43 -72
View File
@@ -2,6 +2,8 @@ import bpy
from typing import Set from typing import Set
from bpy.types import Panel, Context, UILayout, Operator, Event, WindowManager from bpy.types import Panel, Context, UILayout, Operator, Event, WindowManager
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from .ui_utils import UIStyle, draw_section_header, wrap_text_label
from .panel_layout import get_panel_order, should_open_by_default
from ..core.translations import t from ..core.translations import t
from ..core.common import get_active_armature, get_all_meshes from ..core.common import get_active_armature, get_all_meshes
from ..functions.eye_tracking import ( from ..functions.eye_tracking import (
@@ -26,38 +28,37 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
bl_region_type: str = 'UI' bl_region_type: str = 'UI'
bl_category: str = CATEGORY_NAME bl_category: str = CATEGORY_NAME
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order: int = 6 bl_order: int = get_panel_order('eye_tracking')
bl_options: Set[str] = {'DEFAULT_CLOSED'} bl_options: Set[str] = set() if not should_open_by_default('EYE_TRACKING') else {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None: def draw(self, context: Context) -> None:
"""Draw the eye tracking panel interface""" """Draw the eye tracking panel interface"""
layout: UILayout = self.layout layout: UILayout = self.layout
toolkit = context.scene.avatar_toolkit toolkit = context.scene.avatar_toolkit
# SDK Version Selection Box # SDK Version Selection
sdk_box: UILayout = layout.box() col = draw_section_header(layout, t("EyeTracking.sdk_version"), icon='PRESET')
col: UILayout = sdk_box.column(align=True)
col.label(text=t("EyeTracking.sdk_version"), icon='PRESET')
col.separator(factor=0.5)
row: UILayout = col.row(align=True) row: UILayout = col.row(align=True)
row.prop(toolkit, "eye_tracking_type", expand=True) row.prop(toolkit, "eye_tracking_type", expand=True)
if toolkit.eye_tracking_type == 'SDK2': if toolkit.eye_tracking_type == 'SDK2':
# SDK2 Warning Box # SDK2 Warning
warning_box: UILayout = layout.box() warning_box: UILayout = layout.box()
col: UILayout = warning_box.column(align=True) col: UILayout = warning_box.column(align=True)
col.label(text=t("EyeTracking.sdk2_warning"), icon='INFO') col.alert = True
col.separator(factor=0.5) col.label(text=t("EyeTracking.sdk2_warning"), icon='ERROR')
col.label(text=t("EyeTracking.sdk2_warning_detail1")) col.separator(factor=UIStyle.SUBSECTION_SEPARATOR_FACTOR)
col.label(text=t("EyeTracking.sdk2_warning_detail2"))
col.label(text=t("EyeTracking.sdk2_warning_detail3"))
col.label(text=t("EyeTracking.sdk2_warning_detail4"))
# Mode Selection Box warning_text = "\n".join([
mode_box: UILayout = layout.box() t("EyeTracking.sdk2_warning_detail1"),
col: UILayout = mode_box.column(align=True) t("EyeTracking.sdk2_warning_detail2"),
col.label(text=t("EyeTracking.setup"), icon='TOOL_SETTINGS') t("EyeTracking.sdk2_warning_detail3"),
col.separator(factor=0.5) t("EyeTracking.sdk2_warning_detail4")
])
wrap_text_label(col, warning_text, max_length=45)
# Mode Selection
col = draw_section_header(layout, t("EyeTracking.setup"), icon='TOOL_SETTINGS')
col.prop(toolkit, "eye_mode", expand=True) col.prop(toolkit, "eye_mode", expand=True)
if toolkit.eye_mode == 'CREATION': if toolkit.eye_mode == 'CREATION':
@@ -72,11 +73,9 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
"""Draw the AV3 eye tracking setup interface""" """Draw the AV3 eye tracking setup interface"""
toolkit = context.scene.avatar_toolkit toolkit = context.scene.avatar_toolkit
# Bone Setup Box # Bone Setup
bone_box: UILayout = layout.box() col = draw_section_header(layout, t("EyeTracking.bone_setup"), icon='BONE_DATA')
col: UILayout = bone_box.column(align=True)
col.label(text=t("EyeTracking.bone_setup"), icon='BONE_DATA')
col.separator(factor=0.5)
armature = get_active_armature(context) armature = get_active_armature(context)
if armature: if armature:
@@ -86,21 +85,16 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
else: else:
col.label(text=t("EyeTracking.no_armature"), icon='ERROR') col.label(text=t("EyeTracking.no_armature"), icon='ERROR')
# Create Button
row: UILayout = layout.row(align=True) row: UILayout = layout.row(align=True)
row.scale_y = 1.5 row.scale_y = UIStyle.PRIMARY_BUTTON_SCALE
row.operator(CreateEyesAV3Button.bl_idname, icon='PLAY') row.operator(CreateEyesAV3Button.bl_idname, icon='PLAY')
def draw_creation_mode(self, context: Context, layout: UILayout) -> None: def draw_creation_mode(self, context: Context, layout: UILayout) -> None:
"""Draw the eye tracking creation mode interface""" """Draw the eye tracking creation mode interface"""
toolkit = context.scene.avatar_toolkit toolkit = context.scene.avatar_toolkit
# Bone Setup Box # Bone Setup
bone_box: UILayout = layout.box() col = draw_section_header(layout, t("EyeTracking.bone_setup"), icon='BONE_DATA')
col: UILayout = bone_box.column(align=True)
col.label(text=t("EyeTracking.bone_setup"), icon='BONE_DATA')
col.separator(factor=0.5)
armature = get_active_armature(context) armature = get_active_armature(context)
if armature: if armature:
col.prop_search(toolkit, "head", armature.data, "bones", text=t("EyeTracking.head_bone")) col.prop_search(toolkit, "head", armature.data, "bones", text=t("EyeTracking.head_bone"))
@@ -109,19 +103,12 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
else: else:
col.label(text=t("EyeTracking.no_armature"), icon='ERROR') col.label(text=t("EyeTracking.no_armature"), icon='ERROR')
# Mesh Setup Box # Mesh Setup
mesh_box: UILayout = layout.box() col = draw_section_header(layout, t("EyeTracking.mesh_setup"), icon='MESH_DATA')
col: UILayout = mesh_box.column(align=True)
col.label(text=t("EyeTracking.mesh_setup"), icon='MESH_DATA')
col.separator(factor=0.5)
col.prop_search(toolkit, "mesh_name_eye", bpy.data, "objects", text="") col.prop_search(toolkit, "mesh_name_eye", bpy.data, "objects", text="")
# Shape Key Setup Box # Shape Key Setup
shape_box: UILayout = layout.box() col = draw_section_header(layout, t("EyeTracking.shapekey_setup"), icon='SHAPEKEY_DATA')
col: UILayout = shape_box.column(align=True)
col.label(text=t("EyeTracking.shapekey_setup"), icon='SHAPEKEY_DATA')
col.separator(factor=0.5)
mesh = bpy.data.objects.get(toolkit.mesh_name_eye) mesh = bpy.data.objects.get(toolkit.mesh_name_eye)
if mesh and mesh.data.shape_keys: if mesh and mesh.data.shape_keys:
col.prop_search(toolkit, "wink_left", mesh.data.shape_keys, "key_blocks", text=t("EyeTracking.wink_left")) col.prop_search(toolkit, "wink_left", mesh.data.shape_keys, "key_blocks", text=t("EyeTracking.wink_left"))
@@ -131,19 +118,15 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
else: else:
col.label(text=t("EyeTracking.no_shapekeys"), icon='ERROR') col.label(text=t("EyeTracking.no_shapekeys"), icon='ERROR')
# Options Box # Options
options_box: UILayout = layout.box() col = draw_section_header(layout, t("EyeTracking.options"), icon='SETTINGS')
col: UILayout = options_box.column(align=True)
col.label(text=t("EyeTracking.options"), icon='SETTINGS')
col.separator(factor=0.5)
col.prop(toolkit, "disable_eye_blinking") col.prop(toolkit, "disable_eye_blinking")
col.prop(toolkit, "disable_eye_movement") col.prop(toolkit, "disable_eye_movement")
if not toolkit.disable_eye_movement: if not toolkit.disable_eye_movement:
col.prop(toolkit, "eye_distance") col.prop(toolkit, "eye_distance")
# Create Button
row: UILayout = layout.row(align=True) row: UILayout = layout.row(align=True)
row.scale_y = 1.5 row.scale_y = UIStyle.PRIMARY_BUTTON_SCALE
row.operator(CreateEyesSDK2Button.bl_idname, icon='PLAY') row.operator(CreateEyesSDK2Button.bl_idname, icon='PLAY')
def draw_testing_mode(self, context: Context, layout: UILayout) -> None: def draw_testing_mode(self, context: Context, layout: UILayout) -> None:
@@ -151,37 +134,25 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
toolkit = context.scene.avatar_toolkit toolkit = context.scene.avatar_toolkit
if context.mode != 'POSE': if context.mode != 'POSE':
# Testing Start Box # Testing Start
test_box: UILayout = layout.box() col = draw_section_header(layout, t("EyeTracking.testing"), icon='PLAY')
col: UILayout = test_box.column(align=True)
col.label(text=t("EyeTracking.testing"), icon='PLAY')
col.separator(factor=0.5)
row: UILayout = col.row(align=True) row: UILayout = col.row(align=True)
row.scale_y = 1.5 row.scale_y = UIStyle.PRIMARY_BUTTON_SCALE
row.operator(StartTestingButton.bl_idname, icon='PLAY') row.operator(StartTestingButton.bl_idname, icon='PLAY')
else: else:
# Eye Rotation Box # Eye Rotation
rotation_box: UILayout = layout.box() col = draw_section_header(layout, t("EyeTracking.rotation_controls"), icon='DRIVER_ROTATIONAL_DIFFERENCE')
col: UILayout = rotation_box.column(align=True)
col.label(text=t("EyeTracking.rotation_controls"), icon='DRIVER_ROTATIONAL_DIFFERENCE')
col.separator(factor=0.5)
col.prop(toolkit, "eye_rotation_x", text=t("EyeTracking.rotation.x")) col.prop(toolkit, "eye_rotation_x", text=t("EyeTracking.rotation.x"))
col.prop(toolkit, "eye_rotation_y", text=t("EyeTracking.rotation.y")) col.prop(toolkit, "eye_rotation_y", text=t("EyeTracking.rotation.y"))
col.operator(ResetRotationButton.bl_idname, icon='LOOP_BACK') col.operator(ResetRotationButton.bl_idname, icon='LOOP_BACK')
# Eye Adjustment Box # Eye Adjustment
adjust_box: UILayout = layout.box() col = draw_section_header(layout, t("EyeTracking.adjustments"), icon='MODIFIER')
col: UILayout = adjust_box.column(align=True)
col.label(text=t("EyeTracking.adjustments"), icon='MODIFIER')
col.separator(factor=0.5)
col.prop(toolkit, "eye_distance") col.prop(toolkit, "eye_distance")
col.operator(AdjustEyesButton.bl_idname, icon='CON_TRACKTO') col.operator(AdjustEyesButton.bl_idname, icon='CON_TRACKTO')
# Blinking Test Box # Blinking Test
blink_box: UILayout = layout.box() col = draw_section_header(layout, t("EyeTracking.blink_testing"), icon='HIDE_OFF')
col: UILayout = blink_box.column(align=True)
col.label(text=t("EyeTracking.blink_testing"), icon='HIDE_OFF')
col.separator(factor=0.5)
row: UILayout = col.row(align=True) row: UILayout = col.row(align=True)
row.prop(toolkit, "eye_blink_shape") row.prop(toolkit, "eye_blink_shape")
row.operator(TestBlinking.bl_idname, icon='RESTRICT_VIEW_OFF') row.operator(TestBlinking.bl_idname, icon='RESTRICT_VIEW_OFF')
@@ -192,7 +163,7 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
# Stop Testing Button # Stop Testing Button
row: UILayout = layout.row(align=True) row: UILayout = layout.row(align=True)
row.scale_y = 1.5 row.scale_y = UIStyle.PRIMARY_BUTTON_SCALE
row.operator(StopTestingButton.bl_idname, icon='PAUSE') row.operator(StopTestingButton.bl_idname, icon='PAUSE')
# Reset Button # Reset Button
+9 -7
View File
@@ -1,6 +1,7 @@
import bpy import bpy
from typing import Optional, Set from typing import Optional, Set
from bpy.types import Panel, Context, UILayout from bpy.types import Panel, Context, UILayout
from .ui_utils import UIStyle, wrap_text_label
from ..core.translations import t from ..core.translations import t
CATEGORY_NAME: str = "Avatar Toolkit" CATEGORY_NAME: str = "Avatar Toolkit"
@@ -16,13 +17,14 @@ def draw_title(self: Panel) -> None:
row.scale_y: float = 1.2 row.scale_y: float = 1.2
row.label(text=t("AvatarToolkit.label"), icon='ARMATURE_DATA') row.label(text=t("AvatarToolkit.label"), icon='ARMATURE_DATA')
# Description as a flowing paragraph # Description
desc_col: UILayout = col.column() col.separator(factor=UIStyle.SECTION_SEPARATOR_FACTOR)
desc_col.scale_y: float = 0.6 description = " ".join([
desc_col.label(text=t("AvatarToolkit.desc1")) t("AvatarToolkit.desc1"),
desc_col.label(text=t("AvatarToolkit.desc2")) t("AvatarToolkit.desc2"),
desc_col.label(text=t("AvatarToolkit.desc3")) t("AvatarToolkit.desc3")
col.separator() ])
wrap_text_label(col, description, max_length=50)
class AvatarToolKit_PT_AvatarToolkitPanel(Panel): class AvatarToolKit_PT_AvatarToolkitPanel(Panel):
"""Main panel for Avatar Toolkit containing general information and settings""" """Main panel for Avatar Toolkit containing general information and settings"""
+15 -28
View File
@@ -2,6 +2,8 @@ import bpy
from typing import Set from typing import Set
from bpy.types import Panel, Context, UILayout, Operator from bpy.types import Panel, Context, UILayout, Operator
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from .ui_utils import UIStyle, draw_section_header, draw_operator_row
from .panel_layout import get_panel_order, should_open_by_default
from ..core.translations import t from ..core.translations import t
from ..functions.optimization.materials_tools import AvatarToolkit_OT_CombineMaterials from ..functions.optimization.materials_tools import AvatarToolkit_OT_CombineMaterials
from ..functions.optimization.remove_doubles import AvatarToolkit_OT_RemoveDoubles from ..functions.optimization.remove_doubles import AvatarToolkit_OT_RemoveDoubles
@@ -15,39 +17,24 @@ class AvatarToolKit_PT_OptimizationPanel(Panel):
bl_region_type: str = 'UI' bl_region_type: str = 'UI'
bl_category: str = CATEGORY_NAME bl_category: str = CATEGORY_NAME
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order: int = 1 bl_order: int = get_panel_order('optimization')
bl_options = {'DEFAULT_CLOSED'} bl_options = set() if not should_open_by_default('OPTIMIZATION') else {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None: def draw(self, context: Context) -> None:
"""Draws the optimization panel interface with material, mesh cleanup and join mesh tools""" """Draws the optimization panel interface with material, mesh cleanup and join mesh tools"""
layout: UILayout = self.layout layout: UILayout = self.layout
# Materials Box # Materials section
materials_box: UILayout = layout.box() col = draw_section_header(layout, t("Optimization.materials_title"), icon='MATERIAL')
col: UILayout = materials_box.column(align=True)
col.label(text=t("Optimization.materials_title"), icon='MATERIAL')
col.separator(factor=0.5)
# Material Operations
col.operator(AvatarToolkit_OT_CombineMaterials.bl_idname, icon='MATERIAL') col.operator(AvatarToolkit_OT_CombineMaterials.bl_idname, icon='MATERIAL')
# Mesh Cleanup Box # Mesh Cleanup section
cleanup_box: UILayout = layout.box() col = draw_section_header(layout, t("Optimization.cleanup_title"), icon='MESH_DATA')
col: UILayout = cleanup_box.column(align=True) col.operator(AvatarToolkit_OT_RemoveDoubles.bl_idname, icon='MESH_DATA')
col.label(text=t("Optimization.cleanup_title"), icon='MESH_DATA')
col.separator(factor=0.5)
# Remove Doubles Row # Join Meshes section
row: UILayout = col.row(align=True) col = draw_section_header(layout, t("Optimization.join_meshes_title"), icon='OBJECT_DATA')
row.operator(AvatarToolkit_OT_RemoveDoubles.bl_idname, icon='MESH_DATA') draw_operator_row(col, [
(AvatarToolkit_OT_JoinAllMeshes.bl_idname, t("Optimization.join_all_meshes"), 'OBJECT_DATA'),
# Join Meshes Box (AvatarToolkit_OT_JoinSelectedMeshes.bl_idname, t("Optimization.join_selected_meshes"), 'RESTRICT_SELECT_OFF')
join_box: UILayout = layout.box() ])
col: UILayout = join_box.column(align=True)
col.label(text=t("Optimization.join_meshes_title"), icon='OBJECT_DATA')
col.separator(factor=0.5)
# Join Meshes Row
row: UILayout = col.row(align=True)
row.operator(AvatarToolkit_OT_JoinAllMeshes.bl_idname, icon='OBJECT_DATA')
row.operator(AvatarToolkit_OT_JoinSelectedMeshes.bl_idname, icon='RESTRICT_SELECT_OFF')
+53
View File
@@ -0,0 +1,53 @@
"""Panel ordering and organization guide for Avatar Toolkit UI
This module defines the standard panel order and grouping for the Avatar Toolkit.
"""
# Main Panel
MAIN_PANEL_ORDER = -1 # Always first (parent panel)
QUICK_ACCESS_ORDER = 0
OPTIMIZATION_ORDER = 1
TOOLS_ORDER = 2
CUSTOM_TOOLS_ORDER = 3
CUSTOM_AVATAR_ORDER = 4
TRANSLATION_ORDER = 5
VISEMES_ORDER = 6
EYE_TRACKING_ORDER = 7
TEXTURE_ATLAS_ORDER = 8
VRM_UNITY_ORDER = 9
SETTINGS_ORDER = 10
# Panel open/closed by default
PANELS_OPEN_BY_DEFAULT = {
'QUICK_ACCESS': False,
'OPTIMIZATION': True,
'TOOLS': True,
'CUSTOM_TOOLS': True,
'CUSTOM_AVATAR': True,
'VISEMES': True,
'EYE_TRACKING': True,
'TEXTURE_ATLAS': True,
'VRM_UNITY': True,
'SETTINGS': True,
'TRANSLATION': True,
}
def get_panel_order(panel_name: str) -> int:
"""Get the recommended bl_order value for a panel"""
order_map = {
'quick_access': QUICK_ACCESS_ORDER,
'optimization': OPTIMIZATION_ORDER,
'tools': TOOLS_ORDER,
'custom_tools': CUSTOM_TOOLS_ORDER,
'custom_avatar': CUSTOM_AVATAR_ORDER,
'translation': TRANSLATION_ORDER,
'visemes': VISEMES_ORDER,
'eye_tracking': EYE_TRACKING_ORDER,
'texture_atlas': TEXTURE_ATLAS_ORDER,
'vrm_unity': VRM_UNITY_ORDER,
'settings': SETTINGS_ORDER,
}
return order_map.get(panel_name.lower(), 99)
def should_open_by_default(panel_name: str) -> bool:
"""Check if a panel should be open by default"""
return PANELS_OPEN_BY_DEFAULT.get(panel_name.upper(), True)
+107 -149
View File
@@ -10,6 +10,8 @@ from bpy.types import (
Object Object
) )
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from .ui_utils import UIStyle, draw_section_header, draw_operator_row
from .panel_layout import get_panel_order, should_open_by_default
from ..core.translations import t from ..core.translations import t
from ..core.common import ( from ..core.common import (
get_active_armature, get_active_armature,
@@ -34,7 +36,7 @@ from ..functions.pose_mode import (
AvatarToolkit_OT_ApplyPoseAsShapekey, AvatarToolkit_OT_ApplyPoseAsShapekey,
AvatarToolkit_OT_ApplyPoseAsRest AvatarToolkit_OT_ApplyPoseAsRest
) )
from ..core.armature_validation import validate_armature, AvatarToolkit_OT_ValidateTPose from ..core.armature_validation import validate_armature, AvatarToolkit_OT_ValidateTPose, is_pmx_model
from ..core.importers.importer import AvatarToolKit_OT_Import from ..core.importers.importer import AvatarToolKit_OT_Import
from ..core.resonite_utils import AvatarToolKit_OT_ExportResonite from ..core.resonite_utils import AvatarToolKit_OT_ExportResonite
from ..functions.tools.standardize_armature import AvatarToolkit_OT_StandardizeArmature from ..functions.tools.standardize_armature import AvatarToolkit_OT_StandardizeArmature
@@ -79,26 +81,37 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
bl_region_type: str = 'UI' bl_region_type: str = 'UI'
bl_category: str = CATEGORY_NAME bl_category: str = CATEGORY_NAME
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order: int = 0 bl_order: int = get_panel_order('quick_access')
bl_options = {'DEFAULT_CLOSED'} if should_open_by_default('QUICK_ACCESS') else set()
def draw(self, context: Context) -> None: def draw(self, context: Context) -> None:
"""Draw the panel layout""" """Draw the panel layout"""
layout: UILayout = self.layout layout: UILayout = self.layout
props = context.scene.avatar_toolkit props = context.scene.avatar_toolkit
# Armature Selection Box
armature_box: UILayout = layout.box()
col: UILayout = armature_box.column(align=True)
col.label(text=t("QuickAccess.select_armature"), icon='ARMATURE_DATA')
col.separator(factor=0.5)
# Armature Selection # Armature Selection
col = draw_section_header(layout, t("QuickAccess.select_armature"), icon='ARMATURE_DATA')
col.prop(context.scene.avatar_toolkit, "active_armature", text="") col.prop(context.scene.avatar_toolkit, "active_armature", text="")
# Armature Validation (cached to improve performance) # Get active armature
active_armature: Optional[Object] = get_active_armature(context) active_armature: Optional[Object] = get_active_armature(context)
if active_armature: if active_armature:
# Cache validation results to avoid expensive recalculations on every draw # Validation Section
col = draw_section_header(layout, t("Validation.label", "Armature Validation"), icon='CHECKMARK')
# Main validate button with prominent styling
validate_row = col.row(align=True)
validate_row.scale_y = UIStyle.PRIMARY_BUTTON_SCALE
validate_row.operator("avatar_toolkit.validate_armature_manual",
text=t("Validation.validate_now", "Validate Armature Now"),
icon='CHECKMARK')
# Validation mode selector
col.prop(props, "validation_mode", text=t("Settings.validation_mode", "Mode"))
# Show validation results if flag is set
if props.show_validation_results:
# Cache validation results
cache_key = f"validation_{active_armature.name}_{active_armature.data.name}_{len(active_armature.data.bones)}" cache_key = f"validation_{active_armature.name}_{active_armature.data.name}_{len(active_armature.data.bones)}"
if cache_key not in _validation_cache: if cache_key not in _validation_cache:
@@ -107,15 +120,16 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
is_valid, messages, is_acceptable, hierarchy_messages, scale_messages, non_standard_messages = _validation_cache[cache_key] is_valid, messages, is_acceptable, hierarchy_messages, scale_messages, non_standard_messages = _validation_cache[cache_key]
# Check if this is a PMX model # Check if this is a PMX model
is_pmx_model = False pmx_detected = is_pmx_model(active_armature)
if hasattr(active_armature, 'mmd_type') or (hasattr(active_armature, 'parent') and active_armature.parent and hasattr(active_armature.parent, 'mmd_type')):
is_pmx_model = True
info_box = col.box() results_box = col.box()
row = results_box.row()
row.prop(props, "show_validation_results", text=t("Validation.results", "Validation Results"),
icon='TRIA_DOWN' if props.show_validation_results else 'TRIA_RIGHT', emboss=False)
# If it's a PMX model, display a prominent notice # PMX Model Notice
if is_pmx_model: if pmx_detected:
pmx_box = info_box.box() pmx_box = results_box.box()
pmx_box.label(text=t("Armature.validation.pmx_model_detected"), icon='INFO') pmx_box.label(text=t("Armature.validation.pmx_model_detected"), icon='INFO')
validation_mode = context.scene.avatar_toolkit.validation_mode validation_mode = context.scene.avatar_toolkit.validation_mode
@@ -125,38 +139,35 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
else: else:
pmx_box.label(text=t("Armature.validation.pmx_model_basic")) pmx_box.label(text=t("Armature.validation.pmx_model_basic"))
# Validation Results
if not is_valid: if not is_valid:
# Display non-standard bones and hierarchy issues # Display found bones
if messages and len(messages) > 0: if messages and len(messages) > 0:
# Found Bones section bones_section = results_box.box()
validation_box = info_box.box() row = bones_section.row()
row = validation_box.row() row.prop(props, "show_found_bones", text=t("Validation.section.found_bones"),
row.prop(props, "show_found_bones", text=t("Validation.section.found_bones"), icon='TRIA_DOWN' if props.show_found_bones else 'TRIA_RIGHT', emboss=False) icon='TRIA_DOWN' if props.show_found_bones else 'TRIA_RIGHT', emboss=False)
if props.show_found_bones and len(messages) > 0: if props.show_found_bones:
for line in messages[0].split('\n'): for line in messages[0].split('\n'):
validation_box.label(text=line) bones_section.label(text=line)
# Main validation status # Status message
validation_box = info_box.box() status_box = results_box.box()
row = validation_box.row() row = status_box.row()
row.alert = True row.alert = True
row.label(text=t("Validation.status.failed")) row.label(text=t("Validation.status.failed"), icon='ERROR')
# Detailed validation message # Error explanation
validation_box = info_box.box() error_box = results_box.box()
row = validation_box.row() error_box.alert = True
row.alert = True error_box.label(text=t("Validation.message.failed.line1"))
row.label(text=t("Validation.message.failed.line1")) error_box.label(text=t("Validation.message.failed.line2"))
row = validation_box.row() error_box.label(text=t("Validation.message.failed.line3"))
row.alert = True
row.label(text=t("Validation.message.failed.line2"))
row = validation_box.row()
row.alert = True
row.label(text=t("Validation.message.failed.line3"))
# Non-Standard Bones section # Non-Standard Bones section
validation_box = info_box.box() if non_standard_messages or pmx_detected:
row = validation_box.row() ns_section = results_box.box()
row = ns_section.row()
row.alert = True row.alert = True
row.prop(props, "show_non_standard", text=t("Validation.section.non_standard"), row.prop(props, "show_non_standard", text=t("Validation.section.non_standard"),
icon='TRIA_DOWN' if props.show_non_standard else 'TRIA_RIGHT', emboss=False) icon='TRIA_DOWN' if props.show_non_standard else 'TRIA_RIGHT', emboss=False)
@@ -164,161 +175,108 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
if non_standard_messages and len(non_standard_messages) > 0: if non_standard_messages and len(non_standard_messages) > 0:
for message in non_standard_messages: for message in non_standard_messages:
for line in message.split('\n'): for line in message.split('\n'):
sub_row = validation_box.row() sub_row = ns_section.row()
sub_row.alert = True sub_row.alert = True
sub_row.label(text=line) sub_row.label(text=line)
elif pmx_detected:
ns_section.alert = True
ns_section.label(text=t("Armature.validation.pmx_model_basic"))
ns_section.label(text=t("Armature.validation.pmx_model_strict"))
ns_section.label(text=t("Armature.validation.pmx_model_standardize"))
else: else:
# For PMX models, if no non-standard messages but it's a PMX model, ns_section.label(text=t("Validation.no_non_standard_issues"))
# we should still indicate there might be non-standard bones
if is_pmx_model:
sub_row = validation_box.row()
sub_row.alert = True
sub_row.label(text=t("Armature.validation.pmx_model_basic"))
sub_row = validation_box.row()
sub_row.alert = True
sub_row.label(text=t("Armature.validation.pmx_model_strict"))
sub_row = validation_box.row()
sub_row.alert = True
sub_row.label(text=t("Armature.validation.pmx_model_standardize"))
else:
sub_row = validation_box.row()
sub_row.label(text=t("Validation.no_non_standard_issues"))
# Hierarchy Issues section # Hierarchy Issues section
validation_box = info_box.box() if hierarchy_messages:
row = validation_box.row() hier_section = results_box.box()
row = hier_section.row()
row.alert = True row.alert = True
row.prop(props, "show_hierarchy", text=t("Validation.section.hierarchy"), row.prop(props, "show_hierarchy", text=t("Validation.section.hierarchy"),
icon='TRIA_DOWN' if props.show_hierarchy else 'TRIA_RIGHT', emboss=False) icon='TRIA_DOWN' if props.show_hierarchy else 'TRIA_RIGHT', emboss=False)
if props.show_hierarchy: if props.show_hierarchy:
if hierarchy_messages:
for message in hierarchy_messages: for message in hierarchy_messages:
sub_row = validation_box.row() sub_row = hier_section.row()
sub_row.alert = True sub_row.alert = True
sub_row.label(text=message) sub_row.label(text=message)
else:
sub_row = validation_box.row()
sub_row.label(text=t("Validation.no_hierarchy_issues"))
# Scale Issues section # Scale Issues section
validation_box = info_box.box() if scale_messages:
row = validation_box.row() scale_section = results_box.box()
row = scale_section.row()
row.alert = True row.alert = True
row.prop(props, "show_scale_issues", text=t("Validation.section.scale_issues"), row.prop(props, "show_scale_issues", text=t("Validation.section.scale_issues"),
icon='TRIA_DOWN' if props.show_scale_issues else 'TRIA_RIGHT', emboss=False) icon='TRIA_DOWN' if props.show_scale_issues else 'TRIA_RIGHT', emboss=False)
if props.show_scale_issues: if props.show_scale_issues:
if scale_messages:
for scale_msg in scale_messages: for scale_msg in scale_messages:
sub_row = validation_box.row() sub_row = scale_section.row()
sub_row.alert = True sub_row.alert = True
sub_row.label(text=scale_msg) sub_row.label(text=scale_msg)
else:
sub_row = validation_box.row()
sub_row.label(text=t("Validation.no_scale_issues"))
pose_box = layout.box()
col = pose_box.column(align=True)
col.label(text=t("Validation.tpose.label"), icon='ARMATURE_DATA')
col.separator(factor=0.5)
col.operator(AvatarToolkit_OT_ValidateTPose.bl_idname, icon='CHECKMARK')
if props.show_tpose_validation:
validation_box = col.box()
if props.tpose_validation_result:
validation_box.label(text=t("Validation.tpose.valid"), icon='CHECKMARK')
else:
row = validation_box.row()
row.alert = True
row.label(text=t("Validation.tpose.warning"), icon='ERROR')
for msg in props.tpose_validation_messages:
row = validation_box.row()
row.alert = True
row.label(text=msg.name)
else:
# If no specific issues, show acceptable message
if messages and len(messages) > 0:
info_box.label(text=messages[0], icon='INFO')
if len(messages) > 1:
info_box.label(text=messages[1])
if len(messages) > 2:
info_box.label(text=messages[2])
else:
info_box.label(text=t("Validation.no_messages"), icon='INFO')
elif is_valid and not is_acceptable: elif is_valid and not is_acceptable:
row = info_box.row() # Valid armature - show stats
split = row.split(factor=0.6)
split.label(text=t("QuickAccess.valid_armature"), icon='CHECKMARK')
# Cache armature stats to avoid expensive recalculations
stats_cache_key = f"stats_{active_armature.name}_{active_armature.data.name}_{len(active_armature.data.bones)}" stats_cache_key = f"stats_{active_armature.name}_{active_armature.data.name}_{len(active_armature.data.bones)}"
if stats_cache_key not in _stats_cache: if stats_cache_key not in _stats_cache:
_stats_cache[stats_cache_key] = get_armature_stats(active_armature) _stats_cache[stats_cache_key] = get_armature_stats(active_armature)
stats = _stats_cache[stats_cache_key] stats = _stats_cache[stats_cache_key]
status_box = results_box.box()
row = status_box.row()
row.label(text=t("QuickAccess.valid_armature"), icon='CHECKMARK')
split = row.split(factor=0.4)
split.label(text=t("QuickAccess.bones_count", count=stats['bone_count'])) split.label(text=t("QuickAccess.bones_count", count=stats['bone_count']))
if stats['has_pose']: if stats['has_pose']:
info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT') results_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT')
elif is_valid and is_acceptable:
# Show acceptable standard message
if messages and len(messages) > 0:
info_box.label(text=messages[0], icon='INFO')
# Only try to access additional messages if they exist elif is_valid and is_acceptable:
if len(messages) > 1: # Acceptable standard
info_box.label(text=messages[1]) status_box = results_box.box()
if len(messages) > 2: status_box.label(text=t("Armature.validation.acceptable_standard.success"), icon='INFO')
info_box.label(text=messages[2]) status_box.label(text=t("Armature.validation.acceptable_standard.note"))
else: status_box.label(text=t("Armature.validation.acceptable_standard.option"))
info_box.label(text=t("Validation.no_messages"), icon='INFO')
# Add standardize button # Add standardize button
standardize_box = info_box.box() standardize_box = results_box.box()
standardize_box.operator(AvatarToolkit_OT_StandardizeArmature.bl_idname, standardize_box.operator(AvatarToolkit_OT_StandardizeArmature.bl_idname,
text=t("QuickAccess.standardize_armature"), text=t("QuickAccess.standardize_armature"),
icon='MODIFIER') icon='MODIFIER')
# Validation Mode Warnings # T-Pose Validation
validation_mode = context.scene.avatar_toolkit.validation_mode col = draw_section_header(layout, t("Validation.tpose.label"), icon='ARMATURE_DATA')
if validation_mode == 'BASIC': col.operator(AvatarToolkit_OT_ValidateTPose.bl_idname, text=t("Validation.tpose.validate_now"), icon='CHECKMARK')
warning_row = info_box.box()
warning_row.alert = True if props.show_tpose_validation:
warning_row.label(text=t("QuickAccess.validation_basic_warning"), icon='INFO') validation_result_col = col.column(align=True)
warning_row.label(text=t("QuickAccess.validation_basic_details")) if props.tpose_validation_result:
elif validation_mode == 'NONE': validation_result_col.label(text=t("Validation.tpose.valid"), icon='CHECKMARK')
warning_row = info_box.box() else:
warning_row.alert = True validation_result_col.alert = True
warning_row.label(text=t("QuickAccess.validation_none_warning"), icon='ERROR') validation_result_col.label(text=t("Validation.tpose.warning"), icon='ERROR')
warning_row.label(text=t("QuickAccess.validation_none_details"))
for msg in props.tpose_validation_messages:
validation_result_col.label(text=msg.name)
# Pose Mode Controls # Pose Mode Controls
pose_box: UILayout = layout.box() col = draw_section_header(layout, t("QuickAccess.pose_controls"), icon='ARMATURE_DATA')
col = pose_box.column(align=True)
col.label(text=t("QuickAccess.pose_controls"), icon='ARMATURE_DATA')
col.separator(factor=0.5)
if context.mode == "POSE": if context.mode == "POSE":
col.operator(AvatarToolkit_OT_StopPoseMode.bl_idname, icon='POSE_HLT') col.operator(AvatarToolkit_OT_StopPoseMode.bl_idname, icon='POSE_HLT')
col.separator(factor=0.5) col.separator(factor=UIStyle.SUBSECTION_SEPARATOR_FACTOR)
col.operator(AvatarToolkit_OT_ApplyPoseAsRest.bl_idname, icon='MOD_ARMATURE') draw_operator_row(col, [
col.operator(AvatarToolkit_OT_ApplyPoseAsShapekey.bl_idname, icon='MOD_ARMATURE') (AvatarToolkit_OT_ApplyPoseAsRest.bl_idname, t("QuickAccess.apply_pose_as_rest.label"), 'MOD_ARMATURE'),
(AvatarToolkit_OT_ApplyPoseAsShapekey.bl_idname, t("QuickAccess.apply_pose_as_shapekey.label"), 'MOD_ARMATURE')
])
else: else:
col.operator(AvatarToolkit_OT_StartPoseMode.bl_idname, icon='POSE_HLT') col.operator(AvatarToolkit_OT_StartPoseMode.bl_idname, icon='POSE_HLT')
# Import/Export Box # Import/Export Section
import_box: UILayout = layout.box() col = draw_section_header(layout, t("QuickAccess.import_export"), icon='IMPORT')
col = import_box.column(align=True)
col.label(text=t("QuickAccess.import_export"), icon='IMPORT')
col.separator(factor=0.5)
# Import/Export Buttons # Import/Export Buttons
button_row: UILayout = col.row(align=True) draw_operator_row(col, [
button_row.scale_y = 1.5 (AvatarToolKit_OT_Import.bl_idname, t("QuickAccess.import"), 'IMPORT'),
button_row.operator(AvatarToolKit_OT_Import.bl_idname, text=t("QuickAccess.import"), icon='IMPORT') (AvatarToolKit_OT_ExportMenu.bl_idname, t("QuickAccess.export"), 'EXPORT')
button_row.operator(AvatarToolKit_OT_ExportMenu.bl_idname, text=t("QuickAccess.export"), icon='EXPORT') ], scale_y=UIStyle.PRIMARY_BUTTON_SCALE)
+121
View File
@@ -0,0 +1,121 @@
"""Base classes for reusable search operators"""
from typing import Set, Callable, Optional
from bpy.types import Operator, Context, Event, WindowManager
class SearchOperatorBase(Operator):
"""
Reusable base class for search/selection operators.
This is an abstract base class - do not use directly.
Subclass and implement your specific search operator instead.
Subclasses should:
1. Define bl_idname, bl_label, bl_description
2. Define search_property_name (name of EnumProperty)
3. Define target_property_name (name of property to set on scene)
4. Define get_items_func (function to get enum items)
5. Optionally override get_enum_property() to customize the enum
This was created because search in ATK was all over the place and inconsistent, this way we have a standard way to do it.
"""
# Mark this as abstract by setting a non-Blender-compatible idname
bl_idname = "wm.search_operator_base" # Will be overridden in subclasses
bl_label = "Search and Select"
bl_options = {'REGISTER', 'INTERNAL'}
# These should be overridden in subclasses
search_property_name: str = "search_enum"
target_property_name: str = "target_property"
@staticmethod
def get_items_func(scene, context) -> list:
"""Override this to provide enum items. Return list of (id, name, description) tuples"""
return []
def get_enum_property(self) -> None:
"""
Create the enum property dynamically. Override if you need custom behavior.
This is called during class creation.
"""
import bpy
setattr(
type(self),
self.search_property_name,
bpy.props.EnumProperty(
name="Search",
description="Select item",
items=self.get_items_func
)
)
def execute(self, context: Context) -> Set[str]:
"""Set the target property from the search selection"""
search_value = getattr(self, self.search_property_name, None)
if search_value:
setattr(context.scene.avatar_toolkit, self.target_property_name, search_value)
return {'FINISHED'}
def invoke(self, context: Context, event: Event) -> Set[str]:
"""Open search popup"""
wm: WindowManager = context.window_manager
wm.invoke_search_popup(self)
return {'FINISHED'}
class ArmatureSearchOperator(SearchOperatorBase):
"""Specialized search operator for selecting armatures"""
bl_label = "Search Armatures"
search_property_name: str = "search_armature_enum"
@staticmethod
def get_items_func(scene, context) -> list:
"""Get list of all armature objects in scene"""
import bpy
return [
(obj.name, obj.name, "")
for obj in bpy.data.objects
if obj.type == 'ARMATURE'
]
class MeshSearchOperator(SearchOperatorBase):
"""Specialized search operator for selecting meshes"""
bl_label = "Search Meshes"
search_property_name: str = "search_mesh_enum"
@staticmethod
def get_items_func(scene, context) -> list:
"""Get list of all mesh objects without armature modifiers"""
import bpy
return [
(obj.name, obj.name, "")
for obj in bpy.data.objects
if obj.type == 'MESH'
and not any(mod.type == 'ARMATURE' for mod in obj.modifiers)
]
class BoneSearchOperator(SearchOperatorBase):
"""Specialized search operator for selecting bones from active armature"""
bl_label = "Search Bones"
search_property_name: str = "search_bone_enum"
@staticmethod
def get_items_func(scene, context) -> list:
"""Get list of all bones from active armature"""
from ..core.common import get_active_armature
armature = get_active_armature(context)
if not armature:
return []
return [
(bone.name, bone.name, "")
for bone in armature.data.bones
]
+15 -23
View File
@@ -9,6 +9,8 @@ from bpy.types import (
Event Event
) )
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from .ui_utils import UIStyle, draw_section_header, wrap_text_label
from .panel_layout import get_panel_order, should_open_by_default
from ..core.translations import t, get_languages_list from ..core.translations import t, get_languages_list
from ..core.armature_validation import AvatarToolkit_OT_HighlightProblemBones, AvatarToolkit_OT_ClearBoneHighlighting from ..core.armature_validation import AvatarToolkit_OT_HighlightProblemBones, AvatarToolkit_OT_ClearBoneHighlighting
@@ -26,8 +28,10 @@ class AvatarToolkit_OT_TranslationRestartPopup(Operator):
def draw(self, context: Context) -> None: def draw(self, context: Context) -> None:
layout: UILayout = self.layout layout: UILayout = self.layout
layout.label(text=t("Language.changed.success")) col = layout.column(align=True)
layout.label(text=t("Language.changed.restart")) col.label(text=t("Language.changed.success"))
col.separator(factor=UIStyle.SUBSECTION_SEPARATOR_FACTOR)
wrap_text_label(col, t("Language.changed.restart"), max_length=50)
class AvatarToolKit_PT_SettingsPanel(Panel): class AvatarToolKit_PT_SettingsPanel(Panel):
"""Settings panel for Avatar Toolkit containing language preferences""" """Settings panel for Avatar Toolkit containing language preferences"""
@@ -37,8 +41,8 @@ class AvatarToolKit_PT_SettingsPanel(Panel):
bl_region_type: str = 'UI' bl_region_type: str = 'UI'
bl_category: str = CATEGORY_NAME bl_category: str = CATEGORY_NAME
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order: int = 8 bl_order: int = get_panel_order('settings')
bl_options = {'DEFAULT_CLOSED'} bl_options = set() if not should_open_by_default('SETTINGS') else {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None: def draw(self, context: Context) -> None:
"""Draw the settings panel layout with language selection""" """Draw the settings panel layout with language selection"""
@@ -46,30 +50,18 @@ class AvatarToolKit_PT_SettingsPanel(Panel):
props = context.scene.avatar_toolkit props = context.scene.avatar_toolkit
# Language Settings # Language Settings
lang_box: UILayout = layout.box() col = draw_section_header(layout, t("Settings.language"), icon='WORLD')
col: UILayout = lang_box.column(align=True)
row: UILayout = col.row()
row.scale_y = 1.2
row.label(text=t("Settings.language"), icon='WORLD')
col.separator()
col.prop(props, "language", text="") col.prop(props, "language", text="")
# Validation Settings # Validation Settings with help text
val_box: UILayout = layout.box() col = draw_section_header(layout, t("Settings.validation_mode"), icon='CHECKMARK')
col = val_box.column(align=True)
row = col.row()
row.scale_y = 1.2
row.label(text=t("Settings.validation_mode"), icon='CHECKMARK')
col.separator()
col.prop(props, "validation_mode", text="") col.prop(props, "validation_mode", text="")
# Help text for validation mode
col.separator(factor=UIStyle.SUBSECTION_SEPARATOR_FACTOR)
wrap_text_label(col, "Select how strictly to validate armature bone structure and naming conventions.", max_length=40)
# Bone Highlighting Settings # Bone Highlighting Settings
bone_box: UILayout = layout.box() col = draw_section_header(layout, t("Settings.bone_highlighting"), icon='BONE_DATA')
col = bone_box.column(align=True)
row = col.row()
row.scale_y = 1.2
row.label(text=t("Settings.bone_highlighting"), icon='BONE_DATA')
col.separator()
col.prop(props, "highlight_problem_bones") col.prop(props, "highlight_problem_bones")
if props.highlight_problem_bones: if props.highlight_problem_bones:
col.operator(AvatarToolkit_OT_HighlightProblemBones.bl_idname, icon='COLOR') col.operator(AvatarToolkit_OT_HighlightProblemBones.bl_idname, icon='COLOR')
+30 -52
View File
@@ -2,6 +2,8 @@ import bpy
from typing import Set from typing import Set
from bpy.types import Panel, Context, UILayout, Operator, UIList from bpy.types import Panel, Context, UILayout, Operator, UIList
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from .ui_utils import UIStyle, draw_section_header, draw_operator_row
from .panel_layout import get_panel_order, should_open_by_default
from ..core.translations import t from ..core.translations import t
from ..core.resonite_utils import AvatarToolkit_OT_ConvertResonite from ..core.resonite_utils import AvatarToolkit_OT_ConvertResonite
@@ -29,8 +31,8 @@ class AvatarToolKit_PT_ToolsPanel(Panel):
bl_region_type: str = 'UI' bl_region_type: str = 'UI'
bl_category: str = CATEGORY_NAME bl_category: str = CATEGORY_NAME
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order: int = 2 bl_order: int = get_panel_order('tools')
bl_options = {'DEFAULT_CLOSED'} bl_options = set() if not should_open_by_default('TOOLS') else {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None: def draw(self, context: Context) -> None:
"""Draw the tools panel interface""" """Draw the tools panel interface"""
@@ -38,94 +40,70 @@ class AvatarToolKit_PT_ToolsPanel(Panel):
toolkit = context.scene.avatar_toolkit toolkit = context.scene.avatar_toolkit
# General Tools # General Tools
tools_box: UILayout = layout.box() col = draw_section_header(layout, t("Tools.general_title"), icon='TOOL_SETTINGS')
col: UILayout = tools_box.column(align=True)
col.label(text=t("Tools.general_title"), icon='TOOL_SETTINGS')
col.separator(factor=0.5)
col.operator(AvatarToolkit_OT_ConvertResonite.bl_idname, text=t("Tools.convert_resonite"), icon='EXPORT') col.operator(AvatarToolkit_OT_ConvertResonite.bl_idname, text=t("Tools.convert_resonite"), icon='EXPORT')
# Separation Tools # Separation Tools
sep_box: UILayout = layout.box() col = draw_section_header(layout, t("Tools.separate_title"), icon='MOD_EXPLODE')
col = sep_box.column(align=True) draw_operator_row(col, [
col.label(text=t("Tools.separate_title"), icon='MOD_EXPLODE') (AvatarToolKit_OT_SeparateByMaterials.bl_idname, t("Tools.separate_materials"), 'MATERIAL'),
col.separator(factor=0.5) (AvatarToolKit_OT_SeparateByLooseParts.bl_idname, t("Tools.separate_loose"), 'MESH_DATA')
row: UILayout = col.row(align=True) ])
row.operator(AvatarToolKit_OT_SeparateByMaterials.bl_idname, text=t("Tools.separate_materials"), icon='MATERIAL')
row.operator(AvatarToolKit_OT_SeparateByLooseParts.bl_idname, text=t("Tools.separate_loose"), icon='MESH_DATA')
# Bone Tools # Bone Tools
bone_box: UILayout = layout.box() col = draw_section_header(layout, t("Tools.bone_title"), icon='BONE_DATA')
col = bone_box.column(align=True)
col.label(text=t("Tools.bone_title"), icon='BONE_DATA')
col.separator(factor=0.5)
col.operator(AvatarToolKit_OT_CreateDigitigradeLegs.bl_idname, text=t("Tools.create_digitigrade"), icon='BONE_DATA') col.operator(AvatarToolKit_OT_CreateDigitigradeLegs.bl_idname, text=t("Tools.create_digitigrade"), icon='BONE_DATA')
col.operator(AvatarToolKit_OT_FlipCurrentKeyFrames.bl_idname, text=t("Tools.flip_pose_frames"), icon="ACTION") col.operator(AvatarToolKit_OT_FlipCurrentKeyFrames.bl_idname, text=t("Tools.flip_pose_frames"), icon="ACTION")
# Mesh Tools # Mesh Tools
mesh_box: UILayout = layout.box() col = draw_section_header(layout, t("Tools.mesh_title"), icon='MESH_DATA')
col = mesh_box.column(align=True)
col.label(text=t("Tools.mesh_title"), icon='MESH_DATA')
col.separator(factor=0.5)
col.operator(AvatarToolkit_OT_SelectShortestSeamPath.bl_idname, text=t("Tools.find_shortest_seam_path"), icon="MESH_DATA") col.operator(AvatarToolkit_OT_SelectShortestSeamPath.bl_idname, text=t("Tools.find_shortest_seam_path"), icon="MESH_DATA")
col.operator(AvatarToolkit_OT_ApplyModifierForShapkeyObj.bl_idname, text=t("Tools.apply_modifier_on_shapekey_obj"), icon="SHAPEKEY_DATA") col.operator(AvatarToolkit_OT_ApplyModifierForShapkeyObj.bl_idname, text=t("Tools.apply_modifier_on_shapekey_obj"), icon="SHAPEKEY_DATA")
col.operator(AvatarToolkit_OT_ExplodeMesh.bl_idname, text=t("Tools.explode_mesh"), icon="MOD_EXPLODE") col.operator(AvatarToolkit_OT_ExplodeMesh.bl_idname, text=t("Tools.explode_mesh"), icon="MOD_EXPLODE")
# Standardization Tools # Standardization Tools
standardize_box: UILayout = bone_box.box() col = draw_section_header(layout, t("Tools.standardize_title"), icon='OUTLINER_OB_ARMATURE')
col = standardize_box.column(align=True)
col.label(text=t("Tools.standardize_title"), icon='OUTLINER_OB_ARMATURE')
col.separator(factor=0.5)
col.operator(AvatarToolkit_OT_StandardizeArmature.bl_idname, icon='CHECKMARK') col.operator(AvatarToolkit_OT_StandardizeArmature.bl_idname, icon='CHECKMARK')
# Weight Tools # Weight Tools
weight_box: UILayout = bone_box.box() col = draw_section_header(layout, t("Tools.weight_title"), icon='GROUP_BONE')
col = weight_box.column(align=True)
col.prop(toolkit, "merge_twist_bones", text=t("Tools.merge_twist_bones")) col.prop(toolkit, "merge_twist_bones", text=t("Tools.merge_twist_bones"))
col.prop(toolkit, "preserve_parent_bones") col.prop(toolkit, "preserve_parent_bones")
col.prop(toolkit, "target_bone_type") col.prop(toolkit, "target_bone_type")
col.prop(toolkit, "list_only_mode") col.prop(toolkit, "list_only_mode")
if toolkit.list_only_mode and len(toolkit.zero_weight_bones) > 0: if toolkit.list_only_mode and len(toolkit.zero_weight_bones) > 0:
box = weight_box.box() sub_col = col.box()
row = box.row() row = sub_col.row()
row.template_list("AVATAR_TOOLKIT_UL_ZeroWeightBones", "", row.template_list("AVATAR_TOOLKIT_UL_ZeroWeightBones", "",
toolkit, "zero_weight_bones", toolkit, "zero_weight_bones",
toolkit, "zero_weight_bones_index") toolkit, "zero_weight_bones_index")
col = box.column(align=True) sub_col.operator(AvatarToolKit_OT_RemoveSelectedBones.bl_idname,
col.operator(AvatarToolKit_OT_RemoveSelectedBones.bl_idname,
text=t("Tools.remove_selected_bones")) text=t("Tools.remove_selected_bones"))
row = col.row(align=True) # Combine weight
row.operator(AvatarToolKit_OT_RemoveZeroWeightBones.bl_idname, text=t("Tools.clean_weights"), icon='GROUP_BONE') draw_operator_row(col, [
row.operator(AvatarToolKit_OT_DeleteBoneConstraints.bl_idname, text=t("Tools.clean_constraints"), icon='CONSTRAINT_BONE') (AvatarToolKit_OT_RemoveZeroWeightBones.bl_idname, t("Tools.clean_weights"), 'GROUP_BONE'),
row = col.row(align=True) (AvatarToolKit_OT_DeleteBoneConstraints.bl_idname, t("Tools.clean_constraints"), 'CONSTRAINT_BONE')
row.operator(AvatarToolKit_OT_RemoveZeroWeightVertexGroups.bl_idname, text=t("Tools.clean_vertex_groups"), icon='CONSTRAINT_BONE') ])
col.operator(AvatarToolKit_OT_RemoveZeroWeightVertexGroups.bl_idname, text=t("Tools.clean_vertex_groups"), icon='CONSTRAINT_BONE')
# Merge Tools # Merge Tools
merge_box: UILayout = layout.box() col = draw_section_header(layout, t("Tools.merge_title"), icon='AUTOMERGE_ON')
col = merge_box.column(align=True) draw_operator_row(col, [
col.label(text=t("Tools.merge_title"), icon='AUTOMERGE_ON') (AvatarToolkit_OT_MergeToActive.bl_idname, t("Tools.merge_to_active"), 'BONE_DATA'),
col.separator(factor=0.5) (AvatarToolkit_OT_MergeToParent.bl_idname, t("Tools.merge_to_parent"), 'BONE_DATA')
row = col.row(align=True) ])
row.operator(AvatarToolkit_OT_MergeToActive.bl_idname, text=t("Tools.merge_to_active"), icon='BONE_DATA')
row.operator(AvatarToolkit_OT_MergeToParent.bl_idname, text=t("Tools.merge_to_parent"), icon='BONE_DATA')
col.operator(AvatarToolkit_OT_ConnectBones.bl_idname, text=t("Tools.connect_bones"), icon='BONE_DATA') col.operator(AvatarToolkit_OT_ConnectBones.bl_idname, text=t("Tools.connect_bones"), icon='BONE_DATA')
# Additional Tools # Additional Tools
extra_box: UILayout = layout.box() col = draw_section_header(layout, t("Tools.additional_title"), icon='TOOL_SETTINGS')
col = extra_box.column(align=True)
col.label(text=t("Tools.additional_title"), icon='TOOL_SETTINGS')
col.separator(factor=0.5)
col.operator(AvatarToolkit_OT_ApplyTransforms.bl_idname, text=t("Tools.apply_transforms"), icon='OBJECT_DATA') col.operator(AvatarToolkit_OT_ApplyTransforms.bl_idname, text=t("Tools.apply_transforms"), icon='OBJECT_DATA')
col.operator(AvatarToolkit_OT_CleanShapekeys.bl_idname, text=t("Tools.clean_shapekeys"), icon='SHAPEKEY_DATA') col.operator(AvatarToolkit_OT_CleanShapekeys.bl_idname, text=t("Tools.clean_shapekeys"), icon='SHAPEKEY_DATA')
# Rigify Tools # Rigify Tools
rigify_box: UILayout = layout.box() col = draw_section_header(layout, t("Tools.rigify_title"), icon='ARMATURE_DATA')
col = rigify_box.column(align=True)
col.label(text=t("Tools.rigify_title"), icon='ARMATURE_DATA')
col.separator(factor=0.5)
col.operator(AvatarToolkit_OT_ConvertRigifyToUnity.bl_idname, icon='ARMATURE_DATA') col.operator(AvatarToolkit_OT_ConvertRigifyToUnity.bl_idname, icon='ARMATURE_DATA')
col.prop(context.scene.avatar_toolkit, "merge_twist_bones") col.prop(context.scene.avatar_toolkit, "merge_twist_bones")
+3 -2
View File
@@ -12,6 +12,7 @@ from bpy.types import (
Object Object
) )
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from .panel_layout import get_panel_order, should_open_by_default
from ..core.translations import t from ..core.translations import t
from ..core.logging_setup import logger from ..core.logging_setup import logger
from ..core.common import get_active_armature, ProgressTracker from ..core.common import get_active_armature, ProgressTracker
@@ -465,8 +466,8 @@ class AvatarToolKit_PT_TranslationPanel(Panel):
bl_region_type: str = 'UI' bl_region_type: str = 'UI'
bl_category: str = CATEGORY_NAME bl_category: str = CATEGORY_NAME
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order: int = 9 bl_order: int = get_panel_order('translation')
bl_options = {'DEFAULT_CLOSED'} bl_options = set() if not should_open_by_default('TRANSLATION') else {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None: def draw(self, context: Context) -> None:
"""Draw the translation panel layout""" """Draw the translation panel layout"""
+137
View File
@@ -0,0 +1,137 @@
"""UI utilities and styling helpers for consistent Avatar Toolkit panel design"""
from typing import Callable, Optional
from bpy.types import UILayout, Context, Operator
class UIStyle:
"""Centralized UI styling constants for consistent appearance"""
SECTION_SEPARATOR_FACTOR: float = 0.5
SUBSECTION_SEPARATOR_FACTOR: float = 0.3
PRIMARY_BUTTON_SCALE: float = 1.5
STANDARD_BUTTON_SCALE: float = 1.0
COMPACT_BUTTON_SCALE: float = 0.9
DEFAULT_PADDING: float = 1.0
COMPACT_PADDING: float = 0.5
CATEGORY_ICONS = {
'optimization': 'MOD_SMOOTH',
'tools': 'TOOL_SETTINGS',
'custom': 'TOOL_OPTIONS',
'eye_tracking': 'OBJECT_CAMERA',
'settings': 'PREFERENCES',
'import_export': 'EXPORT',
'pose': 'POSE_HLT',
'materials': 'MATERIAL',
'mesh': 'MESH_DATA',
'bones': 'BONE_DATA',
'vfx': 'MOD_DISPLACE'
}
def draw_section_header(layout: UILayout, title: str, icon: str = 'NONE', separator: bool = True) -> UILayout:
"""Draw a consistent section header with optional icon and separator"""
header_box = layout.box()
col = header_box.column(align=True)
row = col.row()
row.scale_y = 1.2
row.label(text=title, icon=icon)
if separator:
col.separator(factor=UIStyle.SECTION_SEPARATOR_FACTOR)
return col
def draw_subsection(layout: UILayout, title: str, icon: str = 'NONE') -> UILayout:
"""Draw a subsection with reduced visual weight (no box)"""
col = layout.column(align=True)
row = col.row()
row.label(text=title, icon=icon)
col.separator(factor=UIStyle.SUBSECTION_SEPARATOR_FACTOR)
return col
def draw_info_text(layout: UILayout, text: str, icon: str = 'INFO') -> None:
"""Draw informational text that can wrap (replaces multiple labels)"""
col = layout.column()
col.alert = False
# Split long text for wrapping
row = col.row()
row.label(text=text, icon=icon)
def draw_warning_text(layout: UILayout, text: str) -> None:
"""Draw warning-styled text"""
col = layout.column()
col.alert = True
row = col.row()
row.label(text=text, icon='ERROR')
def draw_primary_button(layout: UILayout, operator_idname: str, text: str = "",
icon: str = 'NONE', **kwargs) -> None:
"""Draw a primary action button with standard scaling"""
row = layout.row(align=True)
row.scale_y = UIStyle.PRIMARY_BUTTON_SCALE
row.operator(operator_idname, text=text, icon=icon, **kwargs)
def draw_operator_row(layout: UILayout, operators: list[tuple[str, str, str]],
scale_y: float = 1.0, equal_width: bool = True) -> None:
"""Draw multiple operators in a single row with consistent sizing"""
if not operators:
return
row = layout.row(align=equal_width)
row.scale_y = scale_y
for op_id, text, icon in operators:
row.operator(op_id, text=text, icon=icon)
def draw_collapsible_section(layout: UILayout, title: str, icon: str,
draw_func: Callable[[UILayout], None],
context: Context, storage_attr: str) -> None:
"""Draw a collapsible section (using context scene properties for state)"""
col = layout.column(align=True)
row = col.row()
scene = context.scene
attr_name = f"_ui_expand_{storage_attr}"
is_expanded = getattr(scene, attr_name, False)
icon_name = 'DISCLOSURE_TRI_DOWN' if is_expanded else 'DISCLOSURE_TRI_RIGHT'
row.prop(scene, attr_name, text="", icon=icon_name, emboss=False)
row.label(text=title, icon=icon)
if is_expanded:
col.separator(factor=UIStyle.SUBSECTION_SEPARATOR_FACTOR)
draw_func(col)
def apply_operator_disable_feedback(operator: Operator, layout: UILayout,
is_disabled: bool, reason: str = "") -> UILayout:
"""Prepare layout for disabled operator with visual feedback"""
if is_disabled:
layout.enabled = False
return layout
def wrap_text_label(layout: UILayout, text: str, max_length: int = 50) -> None:
"""Draw a label that wraps long text across multiple lines"""
words = text.split()
current_line = ""
col = layout.column()
for word in words:
test_line = (current_line + " " + word).strip()
if len(test_line) > max_length and current_line:
col.label(text=current_line)
current_line = word
else:
current_line = test_line
if current_line:
col.label(text=current_line)
+3 -2
View File
@@ -2,6 +2,7 @@ import bpy
from bpy.types import Panel, Context, UILayout, Object, ShapeKey from bpy.types import Panel, Context, UILayout, Object, ShapeKey
from ..core.translations import t from ..core.translations import t
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from .panel_layout import get_panel_order, should_open_by_default
from ..core.common import get_active_armature from ..core.common import get_active_armature
from ..functions.visemes import AvatarToolkit_OT_PreviewVisemes, AvatarToolkit_OT_CreateVisemes from ..functions.visemes import AvatarToolkit_OT_PreviewVisemes, AvatarToolkit_OT_CreateVisemes
@@ -13,8 +14,8 @@ class AvatarToolKit_PT_VisemesPanel(Panel):
bl_region_type: str = 'UI' bl_region_type: str = 'UI'
bl_category: str = CATEGORY_NAME bl_category: str = CATEGORY_NAME
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order: int = 5 bl_order: int = get_panel_order('visemes')
bl_options: set[str] = {'DEFAULT_CLOSED'} bl_options: set[str] = set() if not should_open_by_default('VISEMES') else {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None: def draw(self, context: Context) -> None:
"""Draw the visemes panel interface with shape key selection and preview controls""" """Draw the visemes panel interface with shape key selection and preview controls"""
+3 -2
View File
@@ -1,6 +1,7 @@
import bpy import bpy
from bpy.types import Panel, Context, UILayout from bpy.types import Panel, Context, UILayout
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from .panel_layout import get_panel_order, should_open_by_default
from ..core.translations import t from ..core.translations import t
from ..core.common import get_active_armature from ..core.common import get_active_armature
from ..core.vrm_unity_converter import detect_vrm_armature from ..core.vrm_unity_converter import detect_vrm_armature
@@ -15,8 +16,8 @@ class AvatarToolKit_PT_VRMUnityPanel(Panel):
bl_region_type = 'UI' bl_region_type = 'UI'
bl_category = CATEGORY_NAME bl_category = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 3 bl_order = get_panel_order('vrm_unity')
bl_options = {'DEFAULT_CLOSED'} bl_options = set() if not should_open_by_default('VRM_UNITY') else {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None: def draw(self, context: Context) -> None:
"""Draw the VRM to Unity conversion panel interface""" """Draw the VRM to Unity conversion panel interface"""
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.