Start of the Major Overhaul
I decided to go through each function and UI section one by one, improving and overhauling things. Each function and section is going to be fully tested and not rushed out. This is the best way to catch things, but also include the code base as much as possible.
This commit is contained in:
+40
-27
@@ -7,6 +7,7 @@ import pkgutil
|
|||||||
import tomllib
|
import tomllib
|
||||||
import importlib
|
import importlib
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import List, Dict, Set, Optional, Any, Type, Tuple, Generator, TypeVar
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"init",
|
"init",
|
||||||
@@ -14,10 +15,12 @@ __all__ = (
|
|||||||
"unregister",
|
"unregister",
|
||||||
)
|
)
|
||||||
|
|
||||||
modules = None
|
T = TypeVar('T')
|
||||||
ordered_classes = None
|
modules: Optional[List[Any]] = None
|
||||||
|
ordered_classes: Optional[List[Type]] = None
|
||||||
|
|
||||||
def init():
|
def init() -> None:
|
||||||
|
"""Initialize the auto-loader by discovering modules and classes"""
|
||||||
global modules
|
global modules
|
||||||
global ordered_classes
|
global ordered_classes
|
||||||
print("Auto-load init starting")
|
print("Auto-load init starting")
|
||||||
@@ -26,7 +29,8 @@ def init():
|
|||||||
print(f"Found modules: {modules}")
|
print(f"Found modules: {modules}")
|
||||||
print(f"Found classes: {ordered_classes}")
|
print(f"Found classes: {ordered_classes}")
|
||||||
|
|
||||||
def register():
|
def register() -> None:
|
||||||
|
"""Register all discovered classes and modules"""
|
||||||
print("Registering classes")
|
print("Registering classes")
|
||||||
for cls in ordered_classes:
|
for cls in ordered_classes:
|
||||||
print(f"Registering: {cls}")
|
print(f"Registering: {cls}")
|
||||||
@@ -41,7 +45,8 @@ def register():
|
|||||||
if hasattr(module, "register"):
|
if hasattr(module, "register"):
|
||||||
module.register()
|
module.register()
|
||||||
|
|
||||||
def unregister():
|
def unregister() -> None:
|
||||||
|
"""Unregister all classes and modules in reverse order"""
|
||||||
for cls in reversed(ordered_classes):
|
for cls in reversed(ordered_classes):
|
||||||
bpy.utils.unregister_class(cls)
|
bpy.utils.unregister_class(cls)
|
||||||
|
|
||||||
@@ -51,13 +56,15 @@ def unregister():
|
|||||||
if hasattr(module, "unregister"):
|
if hasattr(module, "unregister"):
|
||||||
module.unregister()
|
module.unregister()
|
||||||
|
|
||||||
def get_manifest_id():
|
def get_manifest_id() -> str:
|
||||||
|
"""Get the addon ID from the manifest file"""
|
||||||
manifest_path = Path(__file__).parent.parent / "blender_manifest.toml"
|
manifest_path = Path(__file__).parent.parent / "blender_manifest.toml"
|
||||||
with open(manifest_path, "rb") as f:
|
with open(manifest_path, "rb") as f:
|
||||||
manifest = tomllib.load(f)
|
manifest = tomllib.load(f)
|
||||||
return manifest["id"]
|
return manifest["id"]
|
||||||
|
|
||||||
def get_all_submodules(directory):
|
def get_all_submodules(directory: Path) -> List[Any]:
|
||||||
|
"""Discover and import all submodules in the given directory"""
|
||||||
modules = []
|
modules = []
|
||||||
addon_id = get_manifest_id()
|
addon_id = get_manifest_id()
|
||||||
for root, dirs, files in os.walk(directory):
|
for root, dirs, files in os.walk(directory):
|
||||||
@@ -73,66 +80,75 @@ def get_all_submodules(directory):
|
|||||||
modules.append(importlib.import_module(f".{name}", package_name))
|
modules.append(importlib.import_module(f".{name}", package_name))
|
||||||
return modules
|
return modules
|
||||||
|
|
||||||
def iter_submodules(path, package_name):
|
def iter_submodules(path: Path, package_name: str) -> Generator[Any, None, None]:
|
||||||
|
"""Iterate through submodules in a package"""
|
||||||
for name in sorted(iter_module_names(path)):
|
for name in sorted(iter_module_names(path)):
|
||||||
yield importlib.import_module("." + name, package_name)
|
yield importlib.import_module("." + name, package_name)
|
||||||
|
|
||||||
def iter_module_names(path):
|
def iter_module_names(path: Path) -> Generator[str, None, None]:
|
||||||
print(f"Scanning path: {path}") # Debug path
|
"""Iterate through module names in a directory"""
|
||||||
|
print(f"Scanning path: {path}")
|
||||||
modules_list = list(pkgutil.iter_modules([str(path)]))
|
modules_list = list(pkgutil.iter_modules([str(path)]))
|
||||||
print(f"Found these modules: {modules_list}") # Debug modules
|
print(f"Found these modules: {modules_list}")
|
||||||
for _, module_name, is_pkg in modules_list:
|
for _, module_name, is_pkg in modules_list:
|
||||||
if not is_pkg:
|
if not is_pkg:
|
||||||
print(f"Found module: {module_name}")
|
print(f"Found module: {module_name}")
|
||||||
yield module_name
|
yield module_name
|
||||||
|
|
||||||
|
def get_ordered_classes_to_register(modules: List[Any]) -> List[Type]:
|
||||||
|
"""Get a topologically sorted list of classes to register"""
|
||||||
def get_ordered_classes_to_register(modules):
|
|
||||||
return toposort(get_register_deps_dict(modules))
|
return toposort(get_register_deps_dict(modules))
|
||||||
|
|
||||||
def get_register_deps_dict(modules):
|
def get_register_deps_dict(modules: List[Any]) -> Dict[Type, Set[Type]]:
|
||||||
|
"""Get dependencies dictionary for class registration"""
|
||||||
deps_dict = {}
|
deps_dict = {}
|
||||||
classes_to_register = set(iter_classes_to_register(modules))
|
classes_to_register = set(iter_classes_to_register(modules))
|
||||||
for cls in classes_to_register:
|
for cls in classes_to_register:
|
||||||
deps_dict[cls] = set(iter_own_register_deps(cls, classes_to_register))
|
deps_dict[cls] = set(iter_own_register_deps(cls, classes_to_register))
|
||||||
return deps_dict
|
return deps_dict
|
||||||
|
|
||||||
def iter_own_register_deps(cls, classes_to_register):
|
def iter_own_register_deps(cls: Type, classes_to_register: Set[Type]) -> Generator[Type, None, None]:
|
||||||
|
"""Iterate through a class's own registration dependencies"""
|
||||||
yield from (dep for dep in iter_register_deps(cls) if dep in classes_to_register)
|
yield from (dep for dep in iter_register_deps(cls) if dep in classes_to_register)
|
||||||
|
|
||||||
def iter_register_deps(cls):
|
def iter_register_deps(cls: Type) -> Generator[Type, None, None]:
|
||||||
|
"""Iterate through all registration dependencies of a class"""
|
||||||
for value in typing.get_type_hints(cls, {}, {}).values():
|
for value in typing.get_type_hints(cls, {}, {}).values():
|
||||||
dependency = get_dependency_from_annotation(value)
|
dependency = get_dependency_from_annotation(value)
|
||||||
if dependency is not None:
|
if dependency is not None:
|
||||||
yield dependency
|
yield dependency
|
||||||
|
|
||||||
def get_dependency_from_annotation(value):
|
def get_dependency_from_annotation(value: Any) -> Optional[Type]:
|
||||||
|
"""Get dependency type from a type annotation"""
|
||||||
if isinstance(value, tuple) and len(value) == 2:
|
if isinstance(value, tuple) and len(value) == 2:
|
||||||
if value[0] in (bpy.props.PointerProperty, bpy.props.CollectionProperty):
|
if value[0] in (bpy.props.PointerProperty, bpy.props.CollectionProperty):
|
||||||
return value[1]["type"]
|
return value[1]["type"]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def iter_classes_to_register(modules):
|
def iter_classes_to_register(modules: List[Any]) -> Generator[Type, None, None]:
|
||||||
|
"""Iterate through classes that need to be registered"""
|
||||||
base_types = get_register_base_types()
|
base_types = get_register_base_types()
|
||||||
for cls in get_classes_in_modules(modules):
|
for cls in get_classes_in_modules(modules):
|
||||||
if any(base in base_types for base in cls.__bases__):
|
if any(base in base_types for base in cls.__bases__):
|
||||||
if not getattr(cls, "_is_registered", False):
|
if not getattr(cls, "_is_registered", False):
|
||||||
yield cls
|
yield cls
|
||||||
|
|
||||||
def get_classes_in_modules(modules):
|
def get_classes_in_modules(modules: List[Any]) -> Set[Type]:
|
||||||
|
"""Get all classes defined in the modules"""
|
||||||
classes = set()
|
classes = set()
|
||||||
for module in modules:
|
for module in modules:
|
||||||
for cls in iter_classes_in_module(module):
|
for cls in iter_classes_in_module(module):
|
||||||
classes.add(cls)
|
classes.add(cls)
|
||||||
return classes
|
return classes
|
||||||
|
|
||||||
def iter_classes_in_module(module):
|
def iter_classes_in_module(module: Any) -> Generator[Type, None, None]:
|
||||||
|
"""Iterate through classes defined in a module"""
|
||||||
for value in module.__dict__.values():
|
for value in module.__dict__.values():
|
||||||
if inspect.isclass(value):
|
if inspect.isclass(value):
|
||||||
yield value
|
yield value
|
||||||
|
|
||||||
def get_register_base_types():
|
def get_register_base_types() -> Set[Type]:
|
||||||
|
"""Get set of base types that need registration"""
|
||||||
return set(getattr(bpy.types, name) for name in [
|
return set(getattr(bpy.types, name) for name in [
|
||||||
"Panel", "Operator", "PropertyGroup",
|
"Panel", "Operator", "PropertyGroup",
|
||||||
"AddonPreferences", "Header", "Menu",
|
"AddonPreferences", "Header", "Menu",
|
||||||
@@ -140,24 +156,22 @@ def get_register_base_types():
|
|||||||
"UIList", "RenderEngine"
|
"UIList", "RenderEngine"
|
||||||
])
|
])
|
||||||
|
|
||||||
def toposort(deps_dict):
|
def toposort(deps_dict: Dict[Type, Set[Type]]) -> List[Type]:
|
||||||
|
"""Topologically sort classes based on their dependencies"""
|
||||||
sorted_list = []
|
sorted_list = []
|
||||||
sorted_values = set()
|
sorted_values = set()
|
||||||
|
|
||||||
# First pass: Register panels without parents
|
|
||||||
panels_to_sort = [(value, deps) for value, deps in deps_dict.items()
|
panels_to_sort = [(value, deps) for value, deps in deps_dict.items()
|
||||||
if hasattr(value, 'bl_parent_id')]
|
if hasattr(value, 'bl_parent_id')]
|
||||||
|
|
||||||
base_panels = [(value, deps) for value, deps in deps_dict.items()
|
base_panels = [(value, deps) for value, deps in deps_dict.items()
|
||||||
if not hasattr(value, 'bl_parent_id')]
|
if not hasattr(value, 'bl_parent_id')]
|
||||||
|
|
||||||
# Add base panels first
|
|
||||||
for value, deps in base_panels:
|
for value, deps in base_panels:
|
||||||
if len(deps) == 0:
|
if len(deps) == 0:
|
||||||
sorted_list.append(value)
|
sorted_list.append(value)
|
||||||
sorted_values.add(value)
|
sorted_values.add(value)
|
||||||
|
|
||||||
# Then add child panels
|
|
||||||
while len(deps_dict) > len(sorted_values):
|
while len(deps_dict) > len(sorted_values):
|
||||||
unsorted = []
|
unsorted = []
|
||||||
for value, deps in deps_dict.items():
|
for value, deps in deps_dict.items():
|
||||||
@@ -169,4 +183,3 @@ def toposort(deps_dict):
|
|||||||
unsorted.append(value)
|
unsorted.append(value)
|
||||||
|
|
||||||
return sorted_list
|
return sorted_list
|
||||||
|
|
||||||
|
|||||||
+68
-409
@@ -1,268 +1,83 @@
|
|||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from .dictionaries import bone_names
|
from bpy.types import Context, Object
|
||||||
import threading
|
from typing import Optional, Tuple, List, Set
|
||||||
import time
|
from ..core.translations import t
|
||||||
import webbrowser
|
from ..core.dictionaries import bone_names
|
||||||
import typing
|
|
||||||
|
|
||||||
from typing import List, Optional, Tuple
|
def get_active_armature(context: bpy.types.Context) -> Optional[bpy.types.Object]:
|
||||||
from bpy.types import Object, ShapeKey, Mesh, Context, Material, PropertyGroup
|
"""Get the currently selected armature from Avatar Toolkit properties"""
|
||||||
from functools import lru_cache
|
armature_name = context.scene.avatar_toolkit.active_armature
|
||||||
from bpy.props import PointerProperty, IntProperty, StringProperty
|
if armature_name and armature_name != 'NONE':
|
||||||
from bpy.utils import register_class
|
return bpy.data.objects.get(armature_name)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class SceneMatClass(PropertyGroup):
|
|
||||||
mat: PointerProperty(type=Material)
|
|
||||||
|
|
||||||
register_class(SceneMatClass)
|
|
||||||
|
|
||||||
class MaterialListBool:
|
|
||||||
#For the love that is holy do not ever touch these. If this was java I would make these private
|
|
||||||
#They should only be accessed via context.scene.texture_atlas_Has_Mat_List_Shown
|
|
||||||
#This is so we know if the materials are up to date. messing with these variables directly will make the thing blow up.
|
|
||||||
|
|
||||||
#The only exception to this is the ExpandSection_Materials operator which populates this with new data once the materials have changed and need reloading.
|
|
||||||
old_list: dict[str,list[Material]] = {}
|
|
||||||
bool_material_list_expand: dict[str,bool] = {}
|
|
||||||
|
|
||||||
def set_bool(self, value: bool) -> None:
|
|
||||||
MaterialListBool.bool_material_list_expand[bpy.context.scene.name] = value
|
|
||||||
if value == False:
|
|
||||||
MaterialListBool.old_list[bpy.context.scene.name] = []
|
|
||||||
|
|
||||||
def get_bool(self) -> bool:
|
|
||||||
newlist: list[Material] = []
|
|
||||||
for obj in bpy.context.scene.objects:
|
|
||||||
if len(obj.material_slots)>0:
|
|
||||||
for mat_slot in obj.material_slots:
|
|
||||||
if mat_slot.material:
|
|
||||||
if mat_slot.material not in newlist:
|
|
||||||
newlist.append(mat_slot.material)
|
|
||||||
|
|
||||||
still_the_same: bool = True
|
|
||||||
if bpy.context.scene.name in MaterialListBool.old_list:
|
|
||||||
for item in newlist:
|
|
||||||
if item not in MaterialListBool.old_list[bpy.context.scene.name]:
|
|
||||||
still_the_same = False
|
|
||||||
break
|
|
||||||
for item in MaterialListBool.old_list[bpy.context.scene.name]:
|
|
||||||
if item not in newlist:
|
|
||||||
still_the_same = False
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
still_the_same = False
|
|
||||||
MaterialListBool.bool_material_list_expand[bpy.context.scene.name] = still_the_same
|
|
||||||
|
|
||||||
return MaterialListBool.bool_material_list_expand[bpy.context.scene.name]
|
|
||||||
|
|
||||||
|
|
||||||
### Clean up material names in the given mesh by removing the '.001' suffix.
|
|
||||||
def clean_material_names(mesh: Mesh) -> None:
|
|
||||||
for j, mat in enumerate(mesh.material_slots):
|
|
||||||
if mat.name.endswith(('.0+', ' 0+')):
|
|
||||||
mesh.active_material_index = j
|
|
||||||
mesh.active_material.name = mat.name[:-len(mat.name.rstrip('0')) - 1]
|
|
||||||
|
|
||||||
# This will fix faulty uv coordinates, cats did this a other way which can have unintended consequences,
|
|
||||||
# this is the best way i could of think of doing this for the time being, however may need improvements.
|
|
||||||
|
|
||||||
def fix_uv_coordinates(context: Context) -> None:
|
|
||||||
obj = context.object
|
|
||||||
|
|
||||||
# Store current mode and selection
|
|
||||||
current_mode = context.mode
|
|
||||||
current_active = context.view_layer.objects.active
|
|
||||||
current_selected = context.selected_objects.copy()
|
|
||||||
|
|
||||||
# Ensure we're in object mode and select the object
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
obj.select_set(True)
|
|
||||||
context.view_layer.objects.active = obj
|
|
||||||
|
|
||||||
# Check if the object has any mesh data
|
|
||||||
if obj.type == 'MESH' and obj.data:
|
|
||||||
|
|
||||||
# Switch to Edit Mode
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
|
|
||||||
# Select all UVs
|
|
||||||
bpy.ops.mesh.select_all(action='SELECT')
|
|
||||||
|
|
||||||
# Try to find UV Editor area, fall back to 3D View if not found
|
|
||||||
area = next((area for area in context.screen.areas if area.type == 'UV_EDITOR'), None)
|
|
||||||
if not area:
|
|
||||||
area = next((area for area in context.screen.areas if area.type == 'VIEW_3D'), None)
|
|
||||||
|
|
||||||
# Get the region and space data
|
|
||||||
region = next((region for region in area.regions if region.type == 'WINDOW'), None)
|
|
||||||
space_data = area.spaces.active
|
|
||||||
|
|
||||||
# Create a context override
|
|
||||||
override = {
|
|
||||||
'area': area,
|
|
||||||
'region': region,
|
|
||||||
'space_data': space_data,
|
|
||||||
'edit_object': obj,
|
|
||||||
'active_object': obj,
|
|
||||||
'selected_objects': [obj],
|
|
||||||
'mode': 'EDIT_MESH',
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Ensure UVs are selected
|
|
||||||
bpy.ops.uv.select_all(override, action='SELECT')
|
|
||||||
# Average UV island scales
|
|
||||||
bpy.ops.uv.average_islands_scale(override)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"UV Fix - Error during UV scaling: {str(e)}")
|
|
||||||
|
|
||||||
# Switch back to Object Mode
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
print("UV Fix - Switched back to Object Mode")
|
|
||||||
|
|
||||||
# Restore previous selection and active object
|
|
||||||
for sel_obj in current_selected:
|
|
||||||
sel_obj.select_set(True)
|
|
||||||
context.view_layer.objects.active = current_active
|
|
||||||
else:
|
|
||||||
print("UV Fix - Object is not a valid mesh with UV data")
|
|
||||||
|
|
||||||
def has_shapekeys(mesh_obj: Object) -> bool:
|
|
||||||
return mesh_obj.data.shape_keys is not None
|
|
||||||
|
|
||||||
@lru_cache(maxsize=None)
|
|
||||||
def _get_shape_key_co(shape_key: ShapeKey) -> np.ndarray:
|
|
||||||
return np.array([v.co for v in shape_key.data])
|
|
||||||
|
|
||||||
def simplify_bonename(n: str) -> str:
|
|
||||||
return n.lower().translate(dict.fromkeys(map(ord, u" _.")))
|
|
||||||
|
|
||||||
def get_armature(context: Context, armature_name: Optional[str] = None) -> Optional[Object]:
|
|
||||||
if armature_name:
|
|
||||||
obj = bpy.data.objects[armature_name]
|
|
||||||
if obj.type == "ARMATURE":
|
|
||||||
return obj
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
if context.view_layer.objects.active:
|
|
||||||
obj = context.view_layer.objects.active
|
|
||||||
if obj.type == "ARMATURE":
|
|
||||||
return obj
|
|
||||||
return next((obj for obj in context.view_layer.objects if obj.type == 'ARMATURE'), None)
|
|
||||||
|
|
||||||
def get_armatures(self, context: Context) -> List[Tuple[str, str, str]]:
|
|
||||||
armatures = [(obj.name, obj.name, "") for obj in bpy.data.objects if obj.type == 'ARMATURE']
|
|
||||||
if not armatures:
|
|
||||||
return [('NONE', 'No Armature', '')]
|
|
||||||
return armatures
|
|
||||||
|
|
||||||
def get_armatures_that_are_not_selected(self, context: Context) -> List[Tuple[str, str, str]]:
|
|
||||||
armatures = [(obj.name, obj.name, "") for obj in bpy.data.objects if ((obj.type == 'ARMATURE') and (obj.name != context.scene.avatar_toolkit.selected_armature))]
|
|
||||||
if not armatures:
|
|
||||||
return [('NONE', 'No Other Armature', '')]
|
|
||||||
return armatures
|
|
||||||
|
|
||||||
def get_selected_armature(context: Context) -> Optional[Object]:
|
|
||||||
try:
|
|
||||||
if hasattr(context.scene, 'avatar_toolkit'):
|
|
||||||
armature_name = context.scene.avatar_toolkit.selected_armature
|
|
||||||
if isinstance(armature_name, bytes):
|
|
||||||
try:
|
|
||||||
armature_name = armature_name.decode('utf-8')
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
try:
|
|
||||||
armature_name = armature_name.decode('gbk') # For Chinese characters
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
try:
|
|
||||||
armature_name = armature_name.decode('shift-jis')
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
armature_name = armature_name.decode('latin1')
|
|
||||||
|
|
||||||
if armature_name:
|
|
||||||
armature = bpy.data.objects.get(str(armature_name))
|
|
||||||
if is_valid_armature(armature):
|
|
||||||
return armature
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_merge_armature_source(context: Context) -> Optional[Object]:
|
def set_active_armature(context: bpy.types.Context, armature: bpy.types.Object) -> None:
|
||||||
try:
|
"""Set the active armature for Avatar Toolkit operations"""
|
||||||
if hasattr(context.scene, 'merge_armature_source'):
|
context.scene.avatar_toolkit.active_armature = armature
|
||||||
source_name = context.scene.merge_armature_source
|
|
||||||
if isinstance(source_name, bytes):
|
|
||||||
try:
|
|
||||||
source_name = source_name.decode('utf-8')
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
try:
|
|
||||||
source_name = source_name.decode('shift-jis')
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
source_name = source_name.decode('latin1', errors='ignore')
|
|
||||||
|
|
||||||
if source_name:
|
def get_armature_list(self=None, context: bpy.types.Context = None) -> List[Tuple[str, str, str]]:
|
||||||
return bpy.data.objects.get(str(source_name))
|
"""Get list of all armature objects in the scene"""
|
||||||
except Exception:
|
if context is None:
|
||||||
pass
|
context = bpy.context
|
||||||
return None
|
armatures = [(obj.name, obj.name, "") for obj in context.scene.objects if obj.type == 'ARMATURE']
|
||||||
|
if not armatures:
|
||||||
|
return [('NONE', t("Armature.validation.no_armature"), '')]
|
||||||
|
return armatures
|
||||||
|
|
||||||
def set_selected_armature(context: Context, armature: Optional[Object]) -> None:
|
def validate_armature(armature: bpy.types.Object) -> Tuple[bool, str]:
|
||||||
context.scene.avatar_toolkit.selected_armature = armature.name if armature else ""
|
"""
|
||||||
|
Validate if the selected object is a proper armature and has required bones
|
||||||
|
Returns tuple of (is_valid, message)
|
||||||
|
"""
|
||||||
|
if not armature:
|
||||||
|
return False, t("Armature.validation.no_armature")
|
||||||
|
if armature.type != 'ARMATURE':
|
||||||
|
return False, t("Armature.validation.not_armature")
|
||||||
|
if not armature.data.bones:
|
||||||
|
return False, t("Armature.validation.no_bones")
|
||||||
|
|
||||||
def is_valid_armature(armature: Object) -> bool:
|
essential_bones: Set[str] = {'hips', 'spine', 'chest', 'neck', 'head'}
|
||||||
if not armature or armature.type != 'ARMATURE':
|
found_bones: Set[str] = {bone.name.lower() for bone in armature.data.bones}
|
||||||
return False
|
|
||||||
if not armature.data or not armature.data.bones:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def select_current_armature(context: Context) -> bool:
|
for bone in essential_bones:
|
||||||
armature = get_selected_armature(context)
|
if not any(alt_name in found_bones for alt_name in bone_names[bone]):
|
||||||
|
return False, t("Armature.validation.missing_bone", bone=bone)
|
||||||
|
|
||||||
|
return True, t("QuickAccess.valid_armature")
|
||||||
|
|
||||||
|
def auto_select_single_armature(context: bpy.types.Context) -> None:
|
||||||
|
"""Automatically select armature if only one exists in scene"""
|
||||||
|
armatures = get_armature_list(context)
|
||||||
|
if len(armatures) == 1:
|
||||||
|
set_active_armature(context, armatures[0])
|
||||||
|
|
||||||
|
def clear_default_objects() -> None:
|
||||||
|
"""Removes default Blender objects (cube, light, camera)"""
|
||||||
|
default_names: Set[str] = {'Cube', 'Light', 'Camera'}
|
||||||
|
for obj in bpy.data.objects:
|
||||||
|
if obj.name.split('.')[0] in default_names:
|
||||||
|
bpy.data.objects.remove(obj, do_unlink=True)
|
||||||
|
|
||||||
|
def get_armature_stats(armature: bpy.types.Object) -> dict:
|
||||||
|
"""Get statistics about the armature"""
|
||||||
|
return {
|
||||||
|
'bone_count': len(armature.data.bones),
|
||||||
|
'has_pose': bool(armature.pose),
|
||||||
|
'visible': not armature.hide_viewport,
|
||||||
|
'name': armature.name
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_all_meshes(context: Context) -> List[Object]:
|
||||||
|
armature = get_active_armature(context)
|
||||||
if armature:
|
if armature:
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
return [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == armature]
|
||||||
armature.select_set(True)
|
return []
|
||||||
context.view_layer.objects.active = armature
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def apply_shapekey_to_basis(context: bpy.types.Context, obj: bpy.types.Object, shape_key_name: str, delete_old: bool = False) -> bool:
|
def apply_pose_as_rest(context: Context, armature_obj: Object, meshes: List[Object]) -> bool:
|
||||||
if shape_key_name not in obj.data.shape_keys.key_blocks:
|
|
||||||
return False
|
|
||||||
shapekeynum = obj.data.shape_keys.key_blocks.find(shape_key_name)
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode="EDIT")
|
|
||||||
|
|
||||||
bpy.ops.mesh.select_all(action='SELECT')
|
|
||||||
|
|
||||||
|
|
||||||
obj.active_shape_key_index = 0
|
|
||||||
bpy.ops.mesh.blend_from_shape(shape = shape_key_name, add=True, blend=1)
|
|
||||||
obj.active_shape_key_index = shapekeynum
|
|
||||||
bpy.ops.mesh.select_all(action='SELECT')
|
|
||||||
bpy.ops.mesh.blend_from_shape(shape = shape_key_name, add=True, blend=-2)
|
|
||||||
|
|
||||||
|
|
||||||
bpy.ops.mesh.select_all(action='DESELECT')
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode="OBJECT")
|
|
||||||
print("blended!")
|
|
||||||
|
|
||||||
if delete_old:
|
|
||||||
obj.active_shape_key_index = shapekeynum
|
|
||||||
bpy.ops.object.shape_key_remove(all=False)
|
|
||||||
else:
|
|
||||||
mesh: bpy.types.Mesh = obj.data
|
|
||||||
mesh.shape_keys.key_blocks[shape_key_name].name = shape_key_name + "_reversed"
|
|
||||||
return True
|
|
||||||
|
|
||||||
def apply_pose_as_rest(context: Context, armature_obj: Object, meshes: list[Object]) -> bool:
|
|
||||||
for mesh_obj in meshes:
|
for mesh_obj in meshes:
|
||||||
if not mesh_obj.data:
|
if not mesh_obj.data:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if mesh_obj.data.shape_keys and mesh_obj.data.shape_keys.key_blocks:
|
if mesh_obj.data.shape_keys and mesh_obj.data.shape_keys.key_blocks:
|
||||||
if len(mesh_obj.data.shape_keys.key_blocks) == 1:
|
if len(mesh_obj.data.shape_keys.key_blocks) == 1:
|
||||||
basis = mesh_obj.data.shape_keys.key_blocks[0]
|
basis = mesh_obj.data.shape_keys.key_blocks[0]
|
||||||
@@ -278,7 +93,6 @@ def apply_pose_as_rest(context: Context, armature_obj: Object, meshes: list[Obje
|
|||||||
bpy.ops.object.mode_set(mode='POSE')
|
bpy.ops.object.mode_set(mode='POSE')
|
||||||
bpy.ops.pose.armature_apply(selected=False)
|
bpy.ops.pose.armature_apply(selected=False)
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def apply_armature_to_mesh(armature_obj: Object, mesh_obj: Object) -> None:
|
def apply_armature_to_mesh(armature_obj: Object, mesh_obj: Object) -> None:
|
||||||
@@ -302,6 +116,7 @@ def apply_armature_to_mesh_with_shapekeys(armature_obj: Object, mesh_obj: Object
|
|||||||
shape_keys = mesh_obj.data.shape_keys.key_blocks
|
shape_keys = mesh_obj.data.shape_keys.key_blocks
|
||||||
vertex_groups = []
|
vertex_groups = []
|
||||||
mutes = []
|
mutes = []
|
||||||
|
|
||||||
for sk in shape_keys:
|
for sk in shape_keys:
|
||||||
vertex_groups.append(sk.vertex_group)
|
vertex_groups.append(sk.vertex_group)
|
||||||
sk.vertex_group = ''
|
sk.vertex_group = ''
|
||||||
@@ -333,6 +148,7 @@ def apply_armature_to_mesh_with_shapekeys(armature_obj: Object, mesh_obj: Object
|
|||||||
|
|
||||||
for mod in disabled_mods:
|
for mod in disabled_mods:
|
||||||
mod.show_viewport = True
|
mod.show_viewport = True
|
||||||
|
|
||||||
mesh_obj.modifiers.remove(arm_mod)
|
mesh_obj.modifiers.remove(arm_mod)
|
||||||
|
|
||||||
for sk, vg, mute in zip(shape_keys, vertex_groups, mutes):
|
for sk, vg, mute in zip(shape_keys, vertex_groups, mutes):
|
||||||
@@ -341,160 +157,3 @@ def apply_armature_to_mesh_with_shapekeys(armature_obj: Object, mesh_obj: Object
|
|||||||
|
|
||||||
mesh_obj.active_shape_key_index = old_active_index
|
mesh_obj.active_shape_key_index = old_active_index
|
||||||
mesh_obj.show_only_shape_key = old_show_only
|
mesh_obj.show_only_shape_key = old_show_only
|
||||||
|
|
||||||
def get_all_meshes(context: Context) -> List[Object]:
|
|
||||||
armature = get_selected_armature(context)
|
|
||||||
if armature and is_valid_armature(armature):
|
|
||||||
return [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == armature]
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_mesh_items(self, context):
|
|
||||||
return [(obj.name, obj.name, "") for obj in get_all_meshes(context)]
|
|
||||||
|
|
||||||
def open_web_after_delay_multi_threaded(delay: typing.Optional[float] = 1.0, url: typing.Union[str, typing.Any] = ""):
|
|
||||||
thread = threading.Thread(target=open_web_after_delay,args=[delay,url],name="open_browser_thread")
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
def open_web_after_delay(delay, url):
|
|
||||||
print("opening browser in "+str(delay)+" seconds.")
|
|
||||||
time.sleep(delay)
|
|
||||||
|
|
||||||
webbrowser.open_new_tab(url)
|
|
||||||
|
|
||||||
def duplicatebone(b: bpy.types.EditBone) -> bpy.types.EditBone:
|
|
||||||
arm = bpy.context.object.data
|
|
||||||
cb = arm.edit_bones.new(b.name)
|
|
||||||
|
|
||||||
cb.head = b.head
|
|
||||||
cb.tail = b.tail
|
|
||||||
cb.matrix = b.matrix
|
|
||||||
cb.parent = b.parent
|
|
||||||
return cb
|
|
||||||
|
|
||||||
def has_shapekeys(mesh_obj: Object) -> bool:
|
|
||||||
return mesh_obj.data.shape_keys is not None
|
|
||||||
|
|
||||||
def sort_shape_keys(mesh: Object) -> None:
|
|
||||||
print("Starting shape key sorting...")
|
|
||||||
if not has_shapekeys(mesh):
|
|
||||||
print("No shape keys found. Exiting sort function.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Set the mesh as the active object
|
|
||||||
bpy.context.view_layer.objects.active = mesh
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
order = [
|
|
||||||
'Basis',
|
|
||||||
'vrc.blink_left',
|
|
||||||
'vrc.blink_right',
|
|
||||||
'vrc.lowerlid_left',
|
|
||||||
'vrc.lowerlid_right',
|
|
||||||
'vrc.v_aa',
|
|
||||||
'vrc.v_ch',
|
|
||||||
'vrc.v_dd',
|
|
||||||
'vrc.v_e',
|
|
||||||
'vrc.v_ff',
|
|
||||||
'vrc.v_ih',
|
|
||||||
'vrc.v_kk',
|
|
||||||
'vrc.v_nn',
|
|
||||||
'vrc.v_oh',
|
|
||||||
'vrc.v_ou',
|
|
||||||
'vrc.v_pp',
|
|
||||||
'vrc.v_rr',
|
|
||||||
'vrc.v_sil',
|
|
||||||
'vrc.v_ss',
|
|
||||||
'vrc.v_th',
|
|
||||||
]
|
|
||||||
|
|
||||||
shape_keys = mesh.data.shape_keys.key_blocks
|
|
||||||
print(f"Total shape keys: {len(shape_keys)}")
|
|
||||||
|
|
||||||
# Create a list of shape key names in their current order
|
|
||||||
current_order = [key.name for key in shape_keys]
|
|
||||||
|
|
||||||
# Create a new order list
|
|
||||||
new_order = []
|
|
||||||
|
|
||||||
# First, add all the keys that are in the predefined order
|
|
||||||
for name in order:
|
|
||||||
if name in current_order:
|
|
||||||
new_order.append(name)
|
|
||||||
current_order.remove(name)
|
|
||||||
|
|
||||||
# Then add any remaining keys that weren't in the predefined order
|
|
||||||
new_order.extend(current_order)
|
|
||||||
|
|
||||||
print("New order:", new_order)
|
|
||||||
|
|
||||||
# Now, rearrange the shape keys based on the new order
|
|
||||||
for i, name in enumerate(new_order):
|
|
||||||
index = shape_keys.find(name)
|
|
||||||
if index != i:
|
|
||||||
print(f"Moving {name} from index {index} to {i}")
|
|
||||||
mesh.active_shape_key_index = index
|
|
||||||
while mesh.active_shape_key_index > i:
|
|
||||||
bpy.ops.object.shape_key_move(type='UP')
|
|
||||||
|
|
||||||
print("Shape key sorting completed.")
|
|
||||||
|
|
||||||
def get_shapekeys(mesh: Object, prefix: str = '') -> List[tuple]:
|
|
||||||
if not has_shapekeys(mesh):
|
|
||||||
return []
|
|
||||||
return [(key.name, key.name, key.name) for key in mesh.data.shape_keys.key_blocks if key.name != 'Basis' and key.name.startswith(prefix)]
|
|
||||||
|
|
||||||
def remove_default_objects():
|
|
||||||
for obj in bpy.data.objects:
|
|
||||||
if obj.name in ["Camera", "Light", "Cube"]:
|
|
||||||
bpy.data.objects.remove(obj, do_unlink=True)
|
|
||||||
|
|
||||||
def init_progress(context, steps):
|
|
||||||
context.window_manager.progress_begin(0, 100)
|
|
||||||
context.scene.avatar_toolkit.progress_steps = steps
|
|
||||||
context.scene.avatar_toolkit.progress_current = 0
|
|
||||||
|
|
||||||
def update_progress(self, context, message):
|
|
||||||
context.scene.avatar_toolkit.progress_current += 1
|
|
||||||
progress = (context.scene.avatar_toolkit.progress_current / context.scene.avatar_toolkit.progress_steps) * 100
|
|
||||||
context.window_manager.progress_update(progress)
|
|
||||||
context.area.header_text_set(message)
|
|
||||||
self.report({'INFO'}, message)
|
|
||||||
|
|
||||||
def finish_progress(context):
|
|
||||||
context.window_manager.progress_end()
|
|
||||||
context.area.header_text_set(None)
|
|
||||||
|
|
||||||
def transfer_vertex_weights(context: Context, obj: bpy.types.Object, source_group: str, target_group: str, delete_source_group: bool = True) -> bool:
|
|
||||||
# Create and configure the Vertex Weight Mix modifier
|
|
||||||
modifier = obj.modifiers.new(name="merge_weights", type="VERTEX_WEIGHT_MIX")
|
|
||||||
modifier.show_viewport = True
|
|
||||||
modifier.show_render = True
|
|
||||||
modifier.mix_set = 'B'
|
|
||||||
modifier.vertex_group_a = target_group
|
|
||||||
modifier.vertex_group_b = source_group
|
|
||||||
modifier.mask_constant = 1.0
|
|
||||||
|
|
||||||
# Ensure we're in Object Mode
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
# Deselect all objects and select only our target object
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
obj.select_set(True)
|
|
||||||
context.view_layer.objects.active = obj
|
|
||||||
|
|
||||||
# Move modifier to the top of the stack
|
|
||||||
if len(obj.modifiers) > 1:
|
|
||||||
obj.modifiers.move(obj.modifiers.find(modifier.name), 0)
|
|
||||||
|
|
||||||
# Apply modifier with correct syntax
|
|
||||||
with context.temp_override(active_object=obj):
|
|
||||||
bpy.ops.object.modifier_apply(modifier=modifier.name)
|
|
||||||
|
|
||||||
# Clean up
|
|
||||||
if delete_source_group and source_group in obj.vertex_groups:
|
|
||||||
obj.vertex_groups.remove(obj.vertex_groups[source_group])
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import bpy
|
import bpy
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from ...core.common import get_armature
|
from ...core.common import get_active_armature
|
||||||
from bpy.types import Object, ShapeKey, Mesh, Context, Operator
|
from bpy.types import Object, ShapeKey, Mesh, Context, Operator
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from ...core.translations import t
|
from ...core.translations import t
|
||||||
@@ -12,10 +12,9 @@ class AvatarToolKit_OT_ExportResonite(Operator):
|
|||||||
bl_options = {'REGISTER', 'UNDO'}
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
filepath: bpy.props.StringProperty()
|
filepath: bpy.props.StringProperty()
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context):
|
def poll(cls, context: Context):
|
||||||
if get_armature(context) is None:
|
if get_active_armature(context) is None:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
+119
-44
@@ -1,54 +1,129 @@
|
|||||||
import bpy
|
import bpy
|
||||||
|
import logging
|
||||||
# Importers which don't need much code should be added here, however if a importer needs alot of code
|
|
||||||
# Like the PMX and PMD importers, they should be added to their own files and referenced in the import_types str->lambda dictionary.
|
|
||||||
|
|
||||||
#See below comments on how the system works. - @989onan
|
|
||||||
|
|
||||||
import importlib.util
|
|
||||||
import os
|
import os
|
||||||
import typing
|
import typing
|
||||||
|
from typing import Optional, Callable, Dict, List, Union, Set
|
||||||
|
from ..common import clear_default_objects
|
||||||
from .import_pmx import import_pmx
|
from .import_pmx import import_pmx
|
||||||
from .import_pmd import import_pmd
|
from .import_pmd import import_pmd
|
||||||
|
|
||||||
if importlib.util.find_spec("io_scene_valvesource") is not None:
|
# Configure logging
|
||||||
#from .....scripts.addons.io_scene_valvesource.import_smd import SmdImporter #<- use this to check if your IDE is working properly. idfk
|
logging.basicConfig(level=logging.INFO)
|
||||||
from io_scene_valvesource.import_smd import SmdImporter #ignore IDE bitching this is fine, trust me, also above comment should be okay to an IDE usually if set up right. ^_^ - @989onan
|
logger: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def import_multi_files(method = None, directory: typing.Optional[str] = None, files: list[dict[str,str]] = None, filepath: typing.Optional[str] = ""):
|
import importlib.util
|
||||||
if not files:
|
|
||||||
method(directory, filepath)
|
if importlib.util.find_spec("io_scene_valvesource") is not None:
|
||||||
else:
|
from io_scene_valvesource.import_smd import SmdImporter
|
||||||
for file in files:
|
|
||||||
fullpath = os.path.join(directory,os.path.basename(file["name"]))
|
class ImportProgress:
|
||||||
print("run method!")
|
"""Tracks and logs the progress of multi-file imports"""
|
||||||
method(directory, fullpath)
|
def __init__(self, total_files: int):
|
||||||
#each import should map to a type. even in the case that multiple methods should import together, or have the same import method. Make sure the lambdas match so they get grouped together
|
self.total: int = total_files
|
||||||
#In the case of a file importer that takes only one file argument and each one needs individual import, use above method. (example of it in use is ".dae" format)
|
self.current: int = 0
|
||||||
import_types: dict[str, typing.Callable[[str, list[dict[str,str]], str], None]] = {
|
|
||||||
"fbx": (lambda directory, files, filepath : bpy.ops.import_scene.fbx(files=files, directory=directory, filepath=filepath,automatic_bone_orientation=False,use_prepost_rot=False,use_anim=False)),
|
def update(self, filename: str) -> None:
|
||||||
"smd": (lambda directory, files, filepath : eval("bpy."+SmdImporter.bl_idname+".(files=files, directory=directory, filepath=filepath)")),
|
"""Update import progress and log current file"""
|
||||||
"dmx": (lambda directory, files, filepath: eval("bpy."+SmdImporter.bl_idname+".(files=files, directory=directory, filepath=filepath)")),
|
self.current += 1
|
||||||
"gltf": (lambda directory, files, filepath : bpy.ops.import_scene.gltf(files=files, filepath=filepath)),
|
logger.info(f"Importing {filename} ({self.current}/{self.total})")
|
||||||
"glb": (lambda directory, files, filepath : bpy.ops.import_scene.gltf(files=files, filepath=filepath)),
|
|
||||||
"qc": (lambda directory, files, filepath : eval("bpy."+SmdImporter.bl_idname+".(files=files, directory=directory, filepath=filepath)")),
|
def validate_file(filepath: str) -> bool:
|
||||||
"obj": (lambda directory, files, filepath : bpy.ops.wm.obj_import(files=files, directory=directory, filepath=filepath)),
|
"""
|
||||||
"dae": (lambda directory, files, filepath : import_multi_files(directory=directory, files=files, filepath=filepath, method = (lambda directory, filepath: bpy.ops.wm.collada_import(filepath=filepath, auto_connect = True, find_chains = True, fix_orientation = True)))),
|
Validate if a file exists and is accessible
|
||||||
"3ds": (lambda directory, files, filepath : bpy.ops.import_scene.max3ds(files=files, directory=directory, filepath=filepath)),
|
Returns: True if file is valid, False otherwise
|
||||||
"stl": (lambda directory, files, filepath : bpy.ops.import_mesh.stl(files=files, directory=directory, filepath=filepath)),
|
"""
|
||||||
"mtl": (lambda directory, files, filepath : bpy.ops.wm.obj_import(files=files, directory=directory, filepath=filepath)),
|
if not os.path.exists(filepath):
|
||||||
"x3d": (lambda directory, files, filepath : bpy.ops.import_scene.x3d(files=files, directory=directory, filepath=filepath)),
|
logger.error(f"File not found: {filepath}")
|
||||||
"wrl": (lambda directory, files, filepath : bpy.ops.import_scene.x3d(files=files, directory=directory, filepath=filepath)),
|
return False
|
||||||
"vmd": (lambda directory, files, filepath : import_multi_files(directory=directory, files=files, filepath=filepath, method = (lambda directory, filepath: bpy.ops.tuxedo.import_mmd_animation(directory=directory, filepath=filepath)))),
|
if not os.path.isfile(filepath):
|
||||||
"vrm": (lambda directory, files, filepath: bpy.ops.import_scene.vrm(filepath=filepath)),
|
logger.error(f"Not a file: {filepath}")
|
||||||
"pmx": (lambda directory, files, filepath : import_pmx(filepath)),
|
return False
|
||||||
"pmd": (lambda directory, files, filepath : import_pmd(filepath)),
|
return True
|
||||||
|
|
||||||
|
def import_multi_files(
|
||||||
|
method: Optional[Callable] = None,
|
||||||
|
directory: Optional[str] = None,
|
||||||
|
files: Optional[List[Dict[str, str]]] = None,
|
||||||
|
filepath: str = "",
|
||||||
|
progress_callback: Optional[Callable[[str], None]] = None
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Import multiple files using the specified import method
|
||||||
|
|
||||||
|
Args:
|
||||||
|
method: Import method to use
|
||||||
|
directory: Directory containing files
|
||||||
|
files: List of files to import
|
||||||
|
filepath: Single file path to import
|
||||||
|
progress_callback: Callback for progress updates
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not method:
|
||||||
|
raise ValueError("Import method not specified")
|
||||||
|
|
||||||
|
if not files:
|
||||||
|
if not validate_file(filepath):
|
||||||
|
return
|
||||||
|
method(directory, filepath)
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(filepath)
|
||||||
|
else:
|
||||||
|
progress = ImportProgress(len(files))
|
||||||
|
for file in files:
|
||||||
|
fullpath: str = os.path.join(directory, os.path.basename(file["name"]))
|
||||||
|
if not validate_file(fullpath):
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.info(f"Importing file: {fullpath}")
|
||||||
|
method(directory, fullpath)
|
||||||
|
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(fullpath)
|
||||||
|
progress.update(file["name"])
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Import failed: {str(e)}", exc_info=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
ImportMethod = Callable[[str, List[Dict[str, str]], str], None]
|
||||||
|
|
||||||
|
import_types: Dict[str, ImportMethod] = {
|
||||||
|
"fbx": lambda directory, files, filepath: bpy.ops.import_scene.fbx(
|
||||||
|
files=files, directory=directory, filepath=filepath,
|
||||||
|
automatic_bone_orientation=False, use_prepost_rot=False, use_anim=False
|
||||||
|
),
|
||||||
|
"smd": lambda directory, files, filepath: eval("bpy."+SmdImporter.bl_idname+".(files=files, directory=directory, filepath=filepath)"),
|
||||||
|
"dmx": lambda directory, files, filepath: eval("bpy."+SmdImporter.bl_idname+".(files=files, directory=directory, filepath=filepath)"),
|
||||||
|
"gltf": lambda directory, files, filepath: bpy.ops.import_scene.gltf(files=files, filepath=filepath),
|
||||||
|
"glb": lambda directory, files, filepath: bpy.ops.import_scene.gltf(files=files, filepath=filepath),
|
||||||
|
"qc": lambda directory, files, filepath: eval("bpy."+SmdImporter.bl_idname+".(files=files, directory=directory, filepath=filepath)"),
|
||||||
|
"obj": lambda directory, files, filepath: bpy.ops.wm.obj_import(files=files, directory=directory, filepath=filepath),
|
||||||
|
"dae": lambda directory, files, filepath: import_multi_files(
|
||||||
|
directory=directory,
|
||||||
|
files=files,
|
||||||
|
filepath=filepath,
|
||||||
|
method=lambda directory, filepath: bpy.ops.wm.collada_import(
|
||||||
|
filepath=filepath, auto_connect=True, find_chains=True, fix_orientation=True
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"3ds": lambda directory, files, filepath: bpy.ops.import_scene.max3ds(files=files, directory=directory, filepath=filepath),
|
||||||
|
"stl": lambda directory, files, filepath: bpy.ops.import_mesh.stl(files=files, directory=directory, filepath=filepath),
|
||||||
|
"mtl": lambda directory, files, filepath: bpy.ops.wm.obj_import(files=files, directory=directory, filepath=filepath),
|
||||||
|
"x3d": lambda directory, files, filepath: bpy.ops.import_scene.x3d(files=files, directory=directory, filepath=filepath),
|
||||||
|
"wrl": lambda directory, files, filepath: bpy.ops.import_scene.x3d(files=files, directory=directory, filepath=filepath),
|
||||||
|
"vmd": lambda directory, files, filepath: import_multi_files(
|
||||||
|
directory=directory,
|
||||||
|
files=files,
|
||||||
|
filepath=filepath,
|
||||||
|
method=lambda directory, filepath: bpy.ops.tuxedo.import_mmd_animation(directory=directory, filepath=filepath)
|
||||||
|
),
|
||||||
|
"vrm": lambda directory, files, filepath: bpy.ops.import_scene.vrm(filepath=filepath),
|
||||||
|
"pmx": lambda directory, files, filepath: import_pmx(filepath),
|
||||||
|
"pmd": lambda directory, files, filepath: import_pmd(filepath),
|
||||||
}
|
}
|
||||||
|
|
||||||
def concat_imports_filter(imports):
|
def concat_imports_filter(imports: Dict[str, ImportMethod]) -> str:
|
||||||
names = ""
|
"""Create a file filter string from import types"""
|
||||||
for importer in imports.keys():
|
return "".join(f"*.{importer};" for importer in imports.keys())
|
||||||
names = names+"*."+importer+";"
|
|
||||||
return names
|
|
||||||
|
|
||||||
imports = concat_imports_filter(import_types)
|
imports: str = concat_imports_filter(import_types)
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
# thank you https://stackoverflow.com/a/71432759
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
from bpy.types import Image, Material
|
|
||||||
|
|
||||||
|
|
||||||
# Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016 Jake Gordon and contributors
|
|
||||||
#
|
|
||||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
# of this software and associated documentation files (the "Software"), to deal
|
|
||||||
# in the Software without restriction, including without limitation the rights
|
|
||||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
# copies of the Software, and to permit persons to whom the Software is
|
|
||||||
# furnished to do so, subject to the following conditions:
|
|
||||||
#
|
|
||||||
# The above copyright notice and this permission notice shall be included in all
|
|
||||||
# copies or substantial portions of the Software.
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
# SOFTWARE.
|
|
||||||
|
|
||||||
class Rectangle_Obj:
|
|
||||||
x: int = 0
|
|
||||||
y: int = 0
|
|
||||||
w: int = 0
|
|
||||||
h: int = 0
|
|
||||||
down: Rectangle_Obj = None
|
|
||||||
used: bool = False
|
|
||||||
right: Rectangle_Obj = None
|
|
||||||
|
|
||||||
def __init__(self, x:int, y:int, w:int, h:int, down=None, used =False, right=None):
|
|
||||||
self.x = x
|
|
||||||
self.y = y
|
|
||||||
self.w = w
|
|
||||||
self.h = h
|
|
||||||
self.down = down
|
|
||||||
self.used = used
|
|
||||||
self.right = right
|
|
||||||
|
|
||||||
def split(self, w, h) -> Rectangle_Obj:
|
|
||||||
self.used = True
|
|
||||||
self.down = Rectangle_Obj(x=self.x, y=self.y + h, w=self.w, h=self.h - h)
|
|
||||||
self.right = Rectangle_Obj(x=self.x + w, y=self.y, w=self.w - w, h=h)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def find(self, w, h) -> Optional[Rectangle_Obj]:
|
|
||||||
if self.used:
|
|
||||||
return self.right.find(w, h) or self.down.find(w, h)
|
|
||||||
elif (w <= self.w) and (h <= self.h):
|
|
||||||
return self
|
|
||||||
return None
|
|
||||||
|
|
||||||
class MaterialImageList:
|
|
||||||
albedo: Image
|
|
||||||
normal: Image
|
|
||||||
emission: Image
|
|
||||||
ambient_occlusion: Image
|
|
||||||
height: Image
|
|
||||||
roughness: Image
|
|
||||||
fit: Rectangle_Obj
|
|
||||||
material: Material
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
x: int = 0
|
|
||||||
y: int = 0
|
|
||||||
w: int = 0
|
|
||||||
h: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class BinPacker(object):
|
|
||||||
root: Rectangle_Obj
|
|
||||||
bin: list[MaterialImageList] = []
|
|
||||||
def __init__(self, structure: list[MaterialImageList]):
|
|
||||||
self.root = None
|
|
||||||
self.bin = structure
|
|
||||||
|
|
||||||
def fit(self):
|
|
||||||
structure = self.bin
|
|
||||||
structure_len = len(self.bin)
|
|
||||||
w: int = 0
|
|
||||||
h: int = 0
|
|
||||||
if structure_len > 0:
|
|
||||||
w = structure[0].w
|
|
||||||
h = structure[0].h
|
|
||||||
self.root = Rectangle_Obj(x=0, y=0, w=w, h=h)
|
|
||||||
for img in structure:
|
|
||||||
w = img.w
|
|
||||||
h = img.h
|
|
||||||
node = self.root.find(w, h)
|
|
||||||
if node:
|
|
||||||
img.fit = node.split(w, h)
|
|
||||||
else:
|
|
||||||
img.fit = self.grow_node(w, h)
|
|
||||||
return structure
|
|
||||||
|
|
||||||
def grow_node(self, w, h) -> Optional[Rectangle_Obj]:
|
|
||||||
can_grow_right = (h <= self.root.h)
|
|
||||||
can_grow_down = (w <= self.root.w)
|
|
||||||
|
|
||||||
should_grow_right = can_grow_right and (self.root.h >= (self.root.w + w))
|
|
||||||
should_grow_down = can_grow_down and (self.root.w >= (self.root.h + h))
|
|
||||||
|
|
||||||
if should_grow_right:
|
|
||||||
return self.grow_right(w, h)
|
|
||||||
elif should_grow_down:
|
|
||||||
return self.grow_down(w, h)
|
|
||||||
elif can_grow_right:
|
|
||||||
return self.grow_right(w, h)
|
|
||||||
elif can_grow_down:
|
|
||||||
return self.grow_down(w, h)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def grow_right(self, w, h) -> Optional[Rectangle_Obj]:
|
|
||||||
self.root = Rectangle_Obj(
|
|
||||||
used=True,
|
|
||||||
x=0,
|
|
||||||
y=0,
|
|
||||||
w=self.root.w + w,
|
|
||||||
h=self.root.h,
|
|
||||||
down=self.root,
|
|
||||||
right=Rectangle_Obj(x=self.root.w, y=0, w=w, h=self.root.h))
|
|
||||||
node = self.root.find(w, h)
|
|
||||||
if node:
|
|
||||||
return node.split(w, h)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def grow_down(self, w, h) -> Optional[Rectangle_Obj]:
|
|
||||||
self.root = Rectangle_Obj(
|
|
||||||
used=True,
|
|
||||||
x=0,
|
|
||||||
y=0,
|
|
||||||
w=self.root.w,
|
|
||||||
h=self.root.h + h,
|
|
||||||
down=Rectangle_Obj(x=0, y=self.root.h, w=self.root.w, h=h),
|
|
||||||
right=self.root
|
|
||||||
)
|
|
||||||
node = self.root.find(w, h)
|
|
||||||
if node:
|
|
||||||
return node.split(w, h)
|
|
||||||
return None
|
|
||||||
+23
-173
@@ -1,189 +1,39 @@
|
|||||||
import bpy
|
import bpy
|
||||||
from .translations import t, get_languages_list, update_language
|
from typing import List, Tuple, Optional
|
||||||
from bpy.types import PropertyGroup, Material, Scene, Object, Context
|
from bpy.types import PropertyGroup, Material, Scene, Object, Context
|
||||||
from bpy.props import (StringProperty, BoolProperty, EnumProperty,
|
from bpy.props import (
|
||||||
IntProperty, FloatProperty, CollectionProperty,
|
StringProperty,
|
||||||
PointerProperty)
|
BoolProperty,
|
||||||
|
EnumProperty,
|
||||||
|
IntProperty,
|
||||||
|
FloatProperty,
|
||||||
|
CollectionProperty,
|
||||||
|
PointerProperty
|
||||||
|
)
|
||||||
|
from .translations import t, get_languages_list, update_language
|
||||||
from .addon_preferences import get_preference
|
from .addon_preferences import get_preference
|
||||||
from .common import SceneMatClass, MaterialListBool, get_armatures, get_mesh_items, get_armatures_that_are_not_selected
|
|
||||||
from .updater import get_version_list
|
from .updater import get_version_list
|
||||||
|
from .common import get_armature_list
|
||||||
|
|
||||||
class AvatarToolkitSceneProperties(PropertyGroup):
|
class AvatarToolkitSceneProperties(PropertyGroup):
|
||||||
language: EnumProperty(
|
"""Property group containing Avatar Toolkit scene-level settings and properties"""
|
||||||
name="Language",
|
|
||||||
description="Select the language for the addon",
|
|
||||||
items=get_languages_list,
|
|
||||||
update=update_language
|
|
||||||
)
|
|
||||||
|
|
||||||
selected_mesh: EnumProperty(
|
|
||||||
items=get_mesh_items,
|
|
||||||
name="Selected Mesh",
|
|
||||||
description="Select mesh to modify"
|
|
||||||
)
|
|
||||||
|
|
||||||
material_search_filter: StringProperty(
|
|
||||||
name="Search Materials",
|
|
||||||
description="Filter materials by name",
|
|
||||||
default=""
|
|
||||||
)
|
|
||||||
|
|
||||||
merge_armature_apply_transforms: BoolProperty(
|
|
||||||
default=False,
|
|
||||||
name="Apply Transforms",
|
|
||||||
description="Apply transforms when merging armatures"
|
|
||||||
)
|
|
||||||
|
|
||||||
merge_armature_align_bones: BoolProperty(
|
|
||||||
default=False,
|
|
||||||
name="Align Bones",
|
|
||||||
description="Align bones when merging armatures"
|
|
||||||
)
|
|
||||||
|
|
||||||
progress_steps: IntProperty(default=0)
|
|
||||||
progress_current: IntProperty(default=0)
|
|
||||||
language_changed: BoolProperty(default=False)
|
|
||||||
|
|
||||||
mouth_a: StringProperty(
|
|
||||||
name="Mouth A",
|
|
||||||
description="Shape key for A sound"
|
|
||||||
)
|
|
||||||
|
|
||||||
mouth_o: StringProperty(
|
|
||||||
name="Mouth O",
|
|
||||||
description="Shape key for O sound"
|
|
||||||
)
|
|
||||||
|
|
||||||
mouth_ch: StringProperty(
|
|
||||||
name="Mouth CH",
|
|
||||||
description="Shape key for CH sound"
|
|
||||||
)
|
|
||||||
|
|
||||||
shape_intensity: FloatProperty(
|
|
||||||
name="Shape Intensity",
|
|
||||||
description="Intensity of shape key modifications",
|
|
||||||
default=1.0,
|
|
||||||
min=0.0,
|
|
||||||
max=2.0
|
|
||||||
)
|
|
||||||
|
|
||||||
merge_twist_bones: BoolProperty(
|
|
||||||
name="Merge Twist Bones",
|
|
||||||
description="Merge twist bones during processing",
|
|
||||||
default=True
|
|
||||||
)
|
|
||||||
|
|
||||||
selected_armature: EnumProperty(
|
|
||||||
items=get_armatures,
|
|
||||||
name="Selected Armature",
|
|
||||||
description="Select the armature to work with"
|
|
||||||
)
|
|
||||||
|
|
||||||
merge_armature_source: EnumProperty(
|
|
||||||
items=get_armatures_that_are_not_selected,
|
|
||||||
name="Source Armature",
|
|
||||||
description="Select the source armature for merging"
|
|
||||||
)
|
|
||||||
|
|
||||||
texture_atlas_material_index: IntProperty(
|
|
||||||
default=-1,
|
|
||||||
get=lambda self: -1,
|
|
||||||
set=lambda self, context: None
|
|
||||||
)
|
|
||||||
|
|
||||||
materials: CollectionProperty(type=SceneMatClass)
|
|
||||||
|
|
||||||
texture_atlas_Has_Mat_List_Shown: BoolProperty(
|
|
||||||
default=False,
|
|
||||||
get=MaterialListBool.get_bool,
|
|
||||||
set=MaterialListBool.set_bool
|
|
||||||
)
|
|
||||||
|
|
||||||
avatar_toolkit_updater_version_list: EnumProperty(
|
avatar_toolkit_updater_version_list: EnumProperty(
|
||||||
items=get_version_list,
|
items=get_version_list,
|
||||||
name="Version List",
|
name=t("Scene.avatar_toolkit_updater_version_list.name"),
|
||||||
description="List of available versions"
|
description=t("Scene.avatar_toolkit_updater_version_list.description")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
active_armature: EnumProperty(
|
||||||
class AvatarToolkitMaterialProperties(PropertyGroup):
|
items=get_armature_list,
|
||||||
material_expanded: BoolProperty(
|
name=t("QuickAccess.select_armature"),
|
||||||
name="Expand Material",
|
description=t("QuickAccess.select_armature")
|
||||||
description="Show/hide material properties",
|
|
||||||
default=False
|
|
||||||
)
|
)
|
||||||
|
|
||||||
include_in_atlas: BoolProperty(
|
def register() -> None:
|
||||||
name="Include in Atlas",
|
"""Register the Avatar Toolkit property group"""
|
||||||
description="Include this material in texture atlas",
|
|
||||||
default=True
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_texture_node_list(self, context):
|
|
||||||
# Access the material through the property group's id_data
|
|
||||||
material = self.id_data
|
|
||||||
if material and material.use_nodes:
|
|
||||||
nodes = [(i.image.name if i.image else i.name+"_image",
|
|
||||||
i.image.name if i.image else "node with no image...",
|
|
||||||
i.image.name if i.image else i.name, index+1)
|
|
||||||
for index, i in enumerate(material.node_tree.nodes)
|
|
||||||
if i.bl_idname == "ShaderNodeTexImage"]
|
|
||||||
if not nodes:
|
|
||||||
nodes = [("Error", "No images found", "Error", 0)]
|
|
||||||
else:
|
|
||||||
nodes = [("Error", "No node tree found", "Error", 0)]
|
|
||||||
nodes.append(("None", "None", "None", 0))
|
|
||||||
return nodes
|
|
||||||
|
|
||||||
texture_atlas_albedo: EnumProperty(
|
|
||||||
name="Albedo",
|
|
||||||
description="Albedo texture for atlas",
|
|
||||||
items=get_texture_node_list
|
|
||||||
)
|
|
||||||
|
|
||||||
texture_atlas_normal: EnumProperty(
|
|
||||||
name="Normal",
|
|
||||||
description="Normal map for atlas",
|
|
||||||
items=get_texture_node_list
|
|
||||||
)
|
|
||||||
|
|
||||||
texture_atlas_emission: EnumProperty(
|
|
||||||
name="Emission",
|
|
||||||
description="Emission texture for atlas",
|
|
||||||
items=get_texture_node_list
|
|
||||||
)
|
|
||||||
|
|
||||||
texture_atlas_ambient_occlusion: EnumProperty(
|
|
||||||
name="Ambient Occlusion",
|
|
||||||
description="AO texture for atlas",
|
|
||||||
items=get_texture_node_list
|
|
||||||
)
|
|
||||||
|
|
||||||
texture_atlas_height: EnumProperty(
|
|
||||||
name="Height",
|
|
||||||
description="Height map for atlas",
|
|
||||||
items=get_texture_node_list
|
|
||||||
)
|
|
||||||
|
|
||||||
texture_atlas_roughness: EnumProperty(
|
|
||||||
name="Roughness",
|
|
||||||
description="Roughness map for atlas",
|
|
||||||
items=get_texture_node_list
|
|
||||||
)
|
|
||||||
|
|
||||||
class AvatarToolkitObjectProperties(PropertyGroup):
|
|
||||||
material_group_expanded: BoolProperty(
|
|
||||||
name="Expand Material Group",
|
|
||||||
description="Show/hide materials for this mesh",
|
|
||||||
default=False
|
|
||||||
)
|
|
||||||
|
|
||||||
def register():
|
|
||||||
bpy.types.Scene.avatar_toolkit = PointerProperty(type=AvatarToolkitSceneProperties)
|
bpy.types.Scene.avatar_toolkit = PointerProperty(type=AvatarToolkitSceneProperties)
|
||||||
bpy.types.Material.avatar_toolkit = PointerProperty(type=AvatarToolkitMaterialProperties)
|
|
||||||
bpy.types.Object.avatar_toolkit = PointerProperty(type=AvatarToolkitObjectProperties)
|
|
||||||
|
|
||||||
def unregister():
|
def unregister() -> None:
|
||||||
|
"""Unregister the Avatar Toolkit property group"""
|
||||||
del bpy.types.Scene.avatar_toolkit
|
del bpy.types.Scene.avatar_toolkit
|
||||||
del bpy.types.Material.avatar_toolkit
|
|
||||||
del bpy.types.Object.avatar_toolkit
|
|
||||||
|
|||||||
+33
-7
@@ -276,22 +276,48 @@ def get_version_list(self, context: bpy.types.Context) -> List[Tuple[str, str, s
|
|||||||
return [(v, v, '') for v in version_list.keys()] if version_list else []
|
return [(v, v, '') for v in version_list.keys()] if version_list else []
|
||||||
|
|
||||||
def draw_updater_panel(context: bpy.types.Context, layout: bpy.types.UILayout) -> None:
|
def draw_updater_panel(context: bpy.types.Context, layout: bpy.types.UILayout) -> None:
|
||||||
col = layout.column(align=True)
|
box = layout.box()
|
||||||
|
col = box.column(align=True)
|
||||||
|
|
||||||
|
# Header
|
||||||
|
row = col.row()
|
||||||
|
row.scale_y = 1.2
|
||||||
|
row.label(text=t('Updater.label'), icon='DOWNARROW_HLT')
|
||||||
|
|
||||||
|
col.separator()
|
||||||
|
|
||||||
|
# Update check/status section
|
||||||
if is_checking_for_update:
|
if is_checking_for_update:
|
||||||
col.operator(AvatarToolkit_OT_CheckForUpdate.bl_idname, text=t('Updater.CheckForUpdateButton.label'))
|
col.operator(AvatarToolkit_OT_CheckForUpdate.bl_idname,
|
||||||
|
text=t('Updater.CheckForUpdateButton.label'),
|
||||||
|
icon='SORTTIME')
|
||||||
elif update_needed:
|
elif update_needed:
|
||||||
col.operator(AvatarToolkit_OT_UpdateToLatest.bl_idname, text=t('Updater.UpdateToLatestButton.label', name=latest_version_str))
|
update_row = col.row(align=True)
|
||||||
|
update_row.scale_y = 1.5
|
||||||
|
update_row.alert = True
|
||||||
|
update_row.operator(AvatarToolkit_OT_UpdateToLatest.bl_idname,
|
||||||
|
text=t('Updater.UpdateToLatestButton.label', name=latest_version_str),
|
||||||
|
icon='IMPORT')
|
||||||
else:
|
else:
|
||||||
col.operator(AvatarToolkit_OT_CheckForUpdate.bl_idname, text=t('Updater.CheckForUpdateButton.label_alt'))
|
col.operator(AvatarToolkit_OT_CheckForUpdate.bl_idname,
|
||||||
|
text=t('Updater.CheckForUpdateButton.label_alt'),
|
||||||
|
icon='FILE_REFRESH')
|
||||||
|
|
||||||
|
# Version selection section
|
||||||
col.separator()
|
col.separator()
|
||||||
row = col.row(align=True)
|
box_inner = col.box()
|
||||||
|
box_inner.label(text=t('Updater.selectVersion'), icon='SETTINGS')
|
||||||
|
row = box_inner.row(align=True)
|
||||||
row.prop(context.scene.avatar_toolkit, 'avatar_toolkit_updater_version_list', text='')
|
row.prop(context.scene.avatar_toolkit, 'avatar_toolkit_updater_version_list', text='')
|
||||||
row.operator(AvatarToolkit_OT_UpdateToLatest.bl_idname, text=t('Updater.UpdateToSelectedButton.label'))
|
row.operator(AvatarToolkit_OT_UpdateToLatest.bl_idname,
|
||||||
|
text=t('Updater.UpdateToSelectedButton.label'),
|
||||||
|
icon='IMPORT')
|
||||||
|
|
||||||
|
# Current version info
|
||||||
col.separator()
|
col.separator()
|
||||||
col.label(text=t('Updater.currentVersion').format(name=get_current_version()))
|
curr_ver_row = col.row()
|
||||||
|
curr_ver_row.label(text=t('Updater.currentVersion').format(name=get_current_version()),
|
||||||
|
icon='CHECKMARK')
|
||||||
|
|
||||||
def ui_refresh() -> None:
|
def ui_refresh() -> None:
|
||||||
for windowManager in bpy.data.window_managers:
|
for windowManager in bpy.data.window_managers:
|
||||||
|
|||||||
@@ -1,161 +0,0 @@
|
|||||||
import bpy
|
|
||||||
import math
|
|
||||||
from bpy.types import Context, Operator
|
|
||||||
from ..core.common import get_selected_armature, is_valid_armature, get_all_meshes
|
|
||||||
from ..core.translations import t
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_ApplyTransforms(Operator):
|
|
||||||
bl_idname = "avatar_toolkit.apply_transforms"
|
|
||||||
bl_label = t("Tools.apply_transforms.label")
|
|
||||||
bl_description = t("Tools.apply_transforms.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context: Context) -> bool:
|
|
||||||
return get_selected_armature(context) is not None
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
|
||||||
armature = get_selected_armature(context)
|
|
||||||
if not is_valid_armature(armature):
|
|
||||||
self.report({'ERROR'}, t("Tools.apply_transforms.invalid_armature"))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
|
|
||||||
armature.select_set(True)
|
|
||||||
context.view_layer.objects.active = armature
|
|
||||||
|
|
||||||
meshes = get_all_meshes(context)
|
|
||||||
for mesh in meshes:
|
|
||||||
mesh.select_set(True)
|
|
||||||
|
|
||||||
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
|
|
||||||
|
|
||||||
self.report({'INFO'}, t("Tools.apply_transforms.success"))
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_ConnectBones(Operator):
|
|
||||||
bl_idname = "avatar_toolkit.connect_bones"
|
|
||||||
bl_label = t("Tools.connect_bones.label")
|
|
||||||
bl_description = t("Tools.connect_bones.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
min_distance: bpy.props.FloatProperty(
|
|
||||||
name=t("Tools.connect_bones.min_distance.label"),
|
|
||||||
description=t("Tools.connect_bones.min_distance.desc"),
|
|
||||||
default=0.005,
|
|
||||||
min=0.001,
|
|
||||||
max=0.1
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context: Context) -> bool:
|
|
||||||
return get_selected_armature(context) is not None
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
|
||||||
armature = get_selected_armature(context)
|
|
||||||
if not is_valid_armature(armature):
|
|
||||||
self.report({'ERROR'}, t("Tools.connect_bones.invalid_armature"))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
|
|
||||||
edit_bones = armature.data.edit_bones
|
|
||||||
bones_connected = 0
|
|
||||||
|
|
||||||
for bone in edit_bones:
|
|
||||||
if len(bone.children) == 1 and bone.name not in ['LeftEye', 'RightEye', 'Head', 'Hips']:
|
|
||||||
child = bone.children[0]
|
|
||||||
distance = math.dist(bone.head, child.head)
|
|
||||||
|
|
||||||
if distance > self.min_distance:
|
|
||||||
bone.tail = child.head
|
|
||||||
if bone.parent and len(bone.parent.children) == 1:
|
|
||||||
bone.use_connect = True
|
|
||||||
bones_connected += 1
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
self.report({'INFO'}, t("Tools.connect_bones.success").format(bones_connected=bones_connected))
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
return context.window_manager.invoke_props_dialog(self)
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
layout.prop(self, "min_distance")
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_DeleteBoneConstraints(Operator):
|
|
||||||
bl_idname = "avatar_toolkit.delete_bone_constraints"
|
|
||||||
bl_label = t("Tools.delete_bone_constraints.label")
|
|
||||||
bl_description = t("Tools.delete_bone_constraints.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context: Context) -> bool:
|
|
||||||
return get_selected_armature(context) is not None
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
|
||||||
armature = get_selected_armature(context)
|
|
||||||
if not is_valid_armature(armature):
|
|
||||||
self.report({'ERROR'}, t("Tools.delete_bone_constraints.invalid_armature"))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='POSE')
|
|
||||||
|
|
||||||
constraints_removed = 0
|
|
||||||
for bone in armature.pose.bones:
|
|
||||||
while bone.constraints:
|
|
||||||
bone.constraints.remove(bone.constraints[0])
|
|
||||||
constraints_removed += 1
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
self.report({'INFO'}, t("Tools.delete_bone_constraints.success").format(constraints_removed=constraints_removed))
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_SeparateByMaterials(Operator):
|
|
||||||
bl_idname = "avatar_toolkit.separate_by_materials"
|
|
||||||
bl_label = t("Tools.separate_by_materials.label")
|
|
||||||
bl_description = t("Tools.separate_by_materials.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context: Context) -> bool:
|
|
||||||
return context.active_object and context.active_object.type == 'MESH'
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
|
||||||
obj = context.active_object
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
bpy.ops.mesh.select_all(action='SELECT')
|
|
||||||
bpy.ops.mesh.separate(type='MATERIAL')
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
self.report({'INFO'}, t("Tools.separate_by_materials.success"))
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_SeparateByLooseParts(Operator):
|
|
||||||
bl_idname = "avatar_toolkit.separate_by_loose_parts"
|
|
||||||
bl_label = t("Tools.separate_by_loose_parts.label")
|
|
||||||
bl_description = t("Tools.separate_by_loose_parts.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context: Context) -> bool:
|
|
||||||
return context.active_object and context.active_object.type == 'MESH'
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
|
||||||
obj = context.active_object
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
bpy.ops.mesh.select_all(action='SELECT')
|
|
||||||
bpy.ops.mesh.separate(type='LOOSE')
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
self.report({'INFO'}, t("Tools.separate_by_loose_parts.success"))
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
@@ -1,461 +0,0 @@
|
|||||||
import bpy
|
|
||||||
from bpy.types import Context, Mesh, Panel, Operator, Armature, EditBone
|
|
||||||
from ..core.translations import t
|
|
||||||
from ..core.common import get_selected_armature, get_all_meshes
|
|
||||||
from ..core import common
|
|
||||||
from ..core.dictionaries import bone_names
|
|
||||||
from mathutils import Matrix
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolkit_OT_StartPoseMode(Operator):
|
|
||||||
bl_idname = 'avatar_toolkit.start_pose_mode'
|
|
||||||
bl_label = t("Quick_Access.start_pose_mode.label")
|
|
||||||
bl_description = t("Quick_Access.start_pose_mode.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
return get_selected_armature(context) != None and context.mode != "POSE"
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
|
||||||
|
|
||||||
#give an active object so the next line doesn't throw an error.
|
|
||||||
context.view_layer.objects.active = get_selected_armature(context)
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
#deselect everything and select just our armature, then go into pose on just our selected armature. - @989onan
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
context.view_layer.objects.active = get_selected_armature(context)
|
|
||||||
context.view_layer.objects.active.select_set(True)
|
|
||||||
bpy.ops.object.mode_set(mode='POSE')
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolkit_OT_StopPoseMode(Operator):
|
|
||||||
bl_idname = 'avatar_toolkit.stop_pose_mode'
|
|
||||||
bl_label = t("Quick_Access.stop_pose_mode.label")
|
|
||||||
bl_description = t("Quick_Access.stop_pose_mode.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
return get_selected_armature(context) != None and context.mode == "POSE"
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
|
||||||
#this is done so that transforms are cleared but user selection is respected. - @989onan
|
|
||||||
bpy.ops.pose.transforms_clear()
|
|
||||||
bpy.ops.pose.select_all(action="INVERT")
|
|
||||||
bpy.ops.pose.transforms_clear()
|
|
||||||
bpy.ops.pose.select_all(action="INVERT")
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolkit_OT_ApplyPoseAsShapekey(Operator):
|
|
||||||
bl_idname = 'avatar_toolkit.apply_pose_as_shapekey'
|
|
||||||
bl_label = t("Quick_Access.apply_pose_as_shapekey.label")
|
|
||||||
bl_description = t("Quick_Access.apply_pose_as_shapekey.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
armature = common.get_selected_armature(context)
|
|
||||||
return armature and context.mode == 'POSE'
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
armature_obj = common.get_selected_armature(context)
|
|
||||||
mesh_objects = common.get_all_meshes(context)
|
|
||||||
|
|
||||||
for mesh_obj in mesh_objects:
|
|
||||||
if not mesh_obj.data:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Ensure basis exists
|
|
||||||
if not mesh_obj.data.shape_keys:
|
|
||||||
mesh_obj.shape_key_add(name='Basis')
|
|
||||||
|
|
||||||
# Store current pose as new shapekey
|
|
||||||
new_shape = mesh_obj.shape_key_add(name='Pose_Shapekey', from_mix=False)
|
|
||||||
|
|
||||||
# Evaluate mesh in current pose
|
|
||||||
depsgraph = context.evaluated_depsgraph_get()
|
|
||||||
eval_mesh = mesh_obj.evaluated_get(depsgraph)
|
|
||||||
|
|
||||||
# Apply evaluated vertices to new shapekey
|
|
||||||
for i, v in enumerate(eval_mesh.data.vertices):
|
|
||||||
new_shape.data[i].co = v.co.copy()
|
|
||||||
|
|
||||||
# Reset pose
|
|
||||||
bpy.ops.pose.select_all(action='SELECT')
|
|
||||||
bpy.ops.pose.transforms_clear()
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
self.report({'INFO'}, t('Tools.apply_pose_as_rest.success'))
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolkit_OT_ApplyPoseAsRest(Operator):
|
|
||||||
bl_idname = 'avatar_toolkit.apply_pose_as_rest'
|
|
||||||
bl_label = t("Quick_Access.apply_pose_as_rest.label")
|
|
||||||
bl_description = t("Quick_Access.apply_pose_as_rest.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
return get_selected_armature(context) != None and context.mode == "POSE"
|
|
||||||
|
|
||||||
def execute(self, context: Context):
|
|
||||||
if not common.apply_pose_as_rest(armature_obj=get_selected_armature(context),
|
|
||||||
meshes=get_all_meshes(context),
|
|
||||||
context=context):
|
|
||||||
self.report({'ERROR'}, t("Quick_Access.apply_armature_failed"))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolkit_OT_RemoveZeroWeightBones(Operator):
|
|
||||||
bl_idname = "avatar_toolkit.remove_zero_weight_bones"
|
|
||||||
bl_label = t("Tools.remove_zero_weight_bones.label")
|
|
||||||
bl_description = t("Tools.remove_zero_weight_bones.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
threshold: bpy.props.FloatProperty(
|
|
||||||
default=0.01,
|
|
||||||
name=t("Tools.remove_zero_weight_bones.threshold.label"),
|
|
||||||
description=t("Tools.remove_zero_weight_bones.threshold.desc"),
|
|
||||||
min=0.0000001,
|
|
||||||
max=0.9999999)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context: Context) -> bool:
|
|
||||||
return common.get_selected_armature(context) is not None
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
|
||||||
armature = common.get_selected_armature(context)
|
|
||||||
if not common.is_valid_armature(armature):
|
|
||||||
self.report({'ERROR'}, t("Tools.apply_transforms.invalid_armature"))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
weighted_bones: list[str] = []
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
|
|
||||||
# Modify the initial transforms collection section to include all bones:
|
|
||||||
initial_transforms = {}
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
for bone in armature.data.edit_bones:
|
|
||||||
initial_transforms[bone.name] = {
|
|
||||||
'head': bone.head.copy(),
|
|
||||||
'tail': bone.tail.copy(),
|
|
||||||
'roll': bone.roll,
|
|
||||||
'matrix': bone.matrix.copy(),
|
|
||||||
'parent': bone.parent.name if bone.parent else None
|
|
||||||
}
|
|
||||||
# Handle any child bones including _end bones
|
|
||||||
for child in bone.children:
|
|
||||||
initial_transforms[child.name] = {
|
|
||||||
'head': child.head.copy(),
|
|
||||||
'tail': child.tail.copy(),
|
|
||||||
'roll': child.roll,
|
|
||||||
'matrix': child.matrix.copy(),
|
|
||||||
'parent': child.parent.name if child.parent else None
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get weighted bones
|
|
||||||
armature.select_set(True)
|
|
||||||
context.view_layer.objects.active = armature
|
|
||||||
|
|
||||||
meshes = common.get_all_meshes(context)
|
|
||||||
for mesh in meshes:
|
|
||||||
mesh_data: Mesh = mesh.data
|
|
||||||
for vertex in mesh_data.vertices:
|
|
||||||
for group in vertex.groups:
|
|
||||||
if group.weight > self.threshold:
|
|
||||||
weighted_bones.append(mesh.vertex_groups[group.group].name)
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
amature_data: Armature = armature.data
|
|
||||||
unweighted_bones: list[str] = []
|
|
||||||
|
|
||||||
# Identify unweighted bones
|
|
||||||
for bone in amature_data.edit_bones:
|
|
||||||
if bone.name not in weighted_bones:
|
|
||||||
unweighted_bones.append(bone.name)
|
|
||||||
|
|
||||||
# Process bone removal while preserving positions
|
|
||||||
for bone_name in unweighted_bones:
|
|
||||||
bone = amature_data.edit_bones[bone_name]
|
|
||||||
|
|
||||||
# Store children data
|
|
||||||
children = bone.children
|
|
||||||
children_data = {}
|
|
||||||
for child in children:
|
|
||||||
children_data[child.name] = initial_transforms[child.name]
|
|
||||||
|
|
||||||
# Reparent children
|
|
||||||
for child in children:
|
|
||||||
child.use_connect = False
|
|
||||||
if bone.parent:
|
|
||||||
child.parent = bone.parent
|
|
||||||
|
|
||||||
# Remove bone
|
|
||||||
amature_data.edit_bones.remove(bone)
|
|
||||||
|
|
||||||
# Restore children positions
|
|
||||||
for child_name, data in children_data.items():
|
|
||||||
if child_name in amature_data.edit_bones:
|
|
||||||
child = amature_data.edit_bones[child_name]
|
|
||||||
child.head = data['head']
|
|
||||||
child.tail = data['tail']
|
|
||||||
child.roll = data['roll']
|
|
||||||
child.matrix = data['matrix']
|
|
||||||
|
|
||||||
# Final position verification
|
|
||||||
for bone_name, transform in initial_transforms.items():
|
|
||||||
if bone_name in amature_data.edit_bones:
|
|
||||||
bone = amature_data.edit_bones[bone_name]
|
|
||||||
bone.matrix = transform['matrix']
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
self.report({'INFO'}, t("Tools.remove_zero_weight_bones.success"))
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolkit_OT_MergeBonesToActive(Operator):
|
|
||||||
bl_idname = "avatar_toolkit.merge_bones_to_active"
|
|
||||||
bl_label = t("Tools.merge_bones_to_active.label")
|
|
||||||
bl_description = t("Tools.merge_bones_to_active.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
delete_old: bpy.props.BoolProperty(name=t("Tools.merge_bones_to_active.delete_old.label"), description=t("Tools.merge_bones_to_active.delete_old.desc"), default=False)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context: Context) -> bool:
|
|
||||||
if common.get_selected_armature(context) is not None:
|
|
||||||
if common.get_selected_armature(context) == context.view_layer.objects.active:
|
|
||||||
if context.mode == "POSE":
|
|
||||||
return len(context.selected_pose_bones) > 1
|
|
||||||
elif context.mode == "EDIT_ARMATURE":
|
|
||||||
return len(context.selected_bones) > 1
|
|
||||||
return False
|
|
||||||
|
|
||||||
def execute(cls, context: Context) -> set[str]:
|
|
||||||
|
|
||||||
prev_mode: str = "EDIT"
|
|
||||||
if context.mode == "POSE":
|
|
||||||
prev_mode = "POSE"
|
|
||||||
|
|
||||||
#get active bone and a list of all other selected bones
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
target_bone: str = context.active_bone.name
|
|
||||||
|
|
||||||
armature_data: Armature = context.view_layer.objects.active.data
|
|
||||||
|
|
||||||
|
|
||||||
bones: list[str] = [i.name for i in context.selected_bones]
|
|
||||||
bones.remove(target_bone)
|
|
||||||
|
|
||||||
for obj in common.get_all_meshes(context):
|
|
||||||
for bone in bones:
|
|
||||||
bone_name: str = armature_data.edit_bones[bone].name
|
|
||||||
common.transfer_vertex_weights(context=context,obj=obj,source_group=bone_name,target_group=armature_data.edit_bones[target_bone].name)
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
for bone in bones:
|
|
||||||
if cls.delete_old:
|
|
||||||
for bone_child in armature_data.edit_bones[bone].children:
|
|
||||||
bone_child.parent = armature_data.edit_bones[bone].parent
|
|
||||||
armature_data.edit_bones.remove(armature_data.edit_bones[bone])
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode=prev_mode)
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolkit_OT_MergeBonesToParents(Operator):
|
|
||||||
bl_idname = "avatar_toolkit.merge_bones_to_parents"
|
|
||||||
bl_label = t("Tools.merge_bones_to_parents.label")
|
|
||||||
bl_description = t("Tools.merge_bones_to_parents.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
delete_old: bpy.props.BoolProperty(
|
|
||||||
name=t("Tools.merge_bones_to_parents.delete_old.label"),
|
|
||||||
description=t("Tools.merge_bones_to_parents.delete_old.desc"),
|
|
||||||
default=False
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context: Context) -> bool:
|
|
||||||
armature = common.get_selected_armature(context)
|
|
||||||
if armature and armature == context.view_layer.objects.active:
|
|
||||||
if context.mode == "POSE":
|
|
||||||
return len(context.selected_pose_bones) > 0
|
|
||||||
elif context.mode == "EDIT_ARMATURE":
|
|
||||||
return len(context.selected_editable_bones) > 0
|
|
||||||
return False
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
|
||||||
prev_mode = context.mode
|
|
||||||
armature = common.get_selected_armature(context)
|
|
||||||
|
|
||||||
# Map 'EDIT_ARMATURE' to 'EDIT' for bpy.ops.object.mode_set
|
|
||||||
if prev_mode == 'EDIT_ARMATURE':
|
|
||||||
prev_mode = 'EDIT'
|
|
||||||
|
|
||||||
# Set active object and mode
|
|
||||||
context.view_layer.objects.active = armature
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
armature.select_set(True)
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
|
|
||||||
armature_data: Armature = armature.data
|
|
||||||
|
|
||||||
# Get selected bones in Edit Mode
|
|
||||||
selected_bones = context.selected_editable_bones
|
|
||||||
selected_bone_names = [bone.name for bone in selected_bones]
|
|
||||||
|
|
||||||
if not selected_bone_names:
|
|
||||||
self.report({'ERROR'}, t("No bones selected"))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
for obj in common.get_all_meshes(context):
|
|
||||||
for bone_name in selected_bone_names:
|
|
||||||
bone = armature_data.edit_bones.get(bone_name)
|
|
||||||
if bone and bone.parent:
|
|
||||||
# Transfer weights from bone to its parent
|
|
||||||
context.view_layer.objects.active = obj
|
|
||||||
common.transfer_vertex_weights(
|
|
||||||
context=context,
|
|
||||||
obj=obj,
|
|
||||||
source_group=bone_name,
|
|
||||||
target_group=bone.parent.name
|
|
||||||
)
|
|
||||||
# Return to armature edit mode
|
|
||||||
context.view_layer.objects.active = armature
|
|
||||||
armature.select_set(True)
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
else:
|
|
||||||
self.report({'WARNING'}, f"Bone '{bone_name}' has no parent or not found; skipping")
|
|
||||||
|
|
||||||
# Optionally delete old bones
|
|
||||||
if self.delete_old:
|
|
||||||
for bone_name in selected_bone_names:
|
|
||||||
bone = armature_data.edit_bones.get(bone_name)
|
|
||||||
if bone:
|
|
||||||
# Reassign children to the parent of the bone being deleted
|
|
||||||
for child in bone.children:
|
|
||||||
child.parent = bone.parent
|
|
||||||
# Remove the bone
|
|
||||||
armature_data.edit_bones.remove(bone)
|
|
||||||
else:
|
|
||||||
self.report({'WARNING'}, f"Bone '{bone_name}' not found in armature; cannot delete")
|
|
||||||
|
|
||||||
# Return to previous mode
|
|
||||||
context.view_layer.objects.active = armature
|
|
||||||
armature.select_set(True)
|
|
||||||
bpy.ops.object.mode_set(mode=prev_mode)
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
class AvatarToolkit_OT_MergeArmatures(Operator):
|
|
||||||
bl_idname = "avatar_toolkit.merge_armatures"
|
|
||||||
bl_label = t("MergeArmature.merge_armatures.label")
|
|
||||||
bl_description = t("MergeArmature.merge_armatures.desc").format(selected_armature_label=t("MergeArmatures.selected_armature.label"))
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
return (common.get_selected_armature(context) is not None) and (common.get_merge_armature_source(context) is not None)
|
|
||||||
|
|
||||||
def make_active(self, obj: bpy.types.Object, context: Context):
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
context.view_layer.objects.active = obj
|
|
||||||
obj.select_set(True)
|
|
||||||
|
|
||||||
def execute(cls, context: Context) -> set[str]:
|
|
||||||
source_armature: bpy.types.Object = bpy.data.objects[context.scene.merge_armature_source]
|
|
||||||
source_armature_data: Armature = source_armature.data
|
|
||||||
target_armature: bpy.types.Object = common.get_selected_armature(context)
|
|
||||||
target_armature_data: Armature = target_armature.data
|
|
||||||
parent_dictionary: dict[str, list[str]] = {}
|
|
||||||
|
|
||||||
cls.make_active(obj=source_armature, context=context)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if context.scene.merge_armature_apply_transforms:
|
|
||||||
target_armature.select_set(True)
|
|
||||||
for obj in target_armature.children:
|
|
||||||
obj.select_set(True)
|
|
||||||
for obj in source_armature.children:
|
|
||||||
obj.select_set(True)
|
|
||||||
bpy.ops.object.transform_apply()
|
|
||||||
|
|
||||||
|
|
||||||
if context.scene.merge_armature_align_bones:
|
|
||||||
if not context.scene.merge_armature_apply_transforms:
|
|
||||||
source_armature.matrix_world = target_armature.matrix_world
|
|
||||||
|
|
||||||
def children_bone_recursive(parent_bone) -> list[bpy.types.PoseBone]:
|
|
||||||
child_bones = []
|
|
||||||
child_bones.append(parent_bone)
|
|
||||||
for child in parent_bone.children:
|
|
||||||
child_bones.extend(children_bone_recursive(child))
|
|
||||||
return child_bones
|
|
||||||
bpy.ops.object.mode_set(mode='POSE')
|
|
||||||
source_armature_bone_names = [j.name for j in children_bone_recursive(
|
|
||||||
source_armature.pose.bones[
|
|
||||||
next(bone.name for bone in source_armature.pose.bones if common.simplify_bonename(bone.name) in bone_names['hips']) #Find bone that matches dictionary for hips before continuing.
|
|
||||||
]
|
|
||||||
)] #bones are default in order of parent child.
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
context.view_layer.objects.active = target_armature
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
for source_bone_name in source_armature_bone_names:
|
|
||||||
|
|
||||||
if source_bone_name in target_armature_data.edit_bones:
|
|
||||||
obj = source_armature
|
|
||||||
editbone = target_armature_data.edit_bones[source_bone_name]
|
|
||||||
bone = obj.pose.bones[source_bone_name]
|
|
||||||
bone.matrix = editbone.matrix
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
if not common.apply_pose_as_rest(armature_obj=source_armature,meshes=[i for i in source_armature.children if i.type == 'MESH'], context=context):
|
|
||||||
cls.report({'ERROR'}, t("Quick_Access.apply_armature_failed"))
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
cls.make_active(obj=source_armature, context=context)
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
source_armature_data: Armature = source_armature.data
|
|
||||||
for bone_name in [i.name for i in source_armature_data.edit_bones]:
|
|
||||||
if bone_name in target_armature_data.bones:
|
|
||||||
parent_dictionary[bone_name] = [i.name for i in source_armature_data.edit_bones[bone_name].children]
|
|
||||||
source_armature_data.edit_bones.remove(source_armature_data.edit_bones[bone_name])
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
cls.make_active(obj=target_armature, context=context)
|
|
||||||
source_armature.select_set(True)
|
|
||||||
|
|
||||||
bpy.ops.object.join()
|
|
||||||
target_armature: bpy.types.Object = common.get_selected_armature(context)
|
|
||||||
cls.make_active(obj=target_armature, context=context)
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
for bone_name, bone_name_list in parent_dictionary.items():
|
|
||||||
if bone_name in target_armature_data.edit_bones:
|
|
||||||
for bone_child in bone_name_list:
|
|
||||||
target_armature_data.edit_bones[bone_child].parent = target_armature_data.edit_bones[bone_name]
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
@@ -1,321 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import numpy
|
|
||||||
import bpy
|
|
||||||
import os
|
|
||||||
from typing import List, Tuple, Optional
|
|
||||||
from bpy.types import Material, Operator, Context, Object, Image, Mesh, MeshUVLoopLayer, Float2AttributeValue, ShaderNodeTexImage, ShaderNodeBsdfPrincipled, ShaderNodeNormalMap
|
|
||||||
from ..core.common import SceneMatClass, MaterialListBool
|
|
||||||
from ..core.packer.rectangle_packer import MaterialImageList, BinPacker
|
|
||||||
from ..core.translations import t
|
|
||||||
|
|
||||||
class MaterialImageList:
|
|
||||||
def __init__(self):
|
|
||||||
self.albedo: Image = None
|
|
||||||
self.normal: Image = None
|
|
||||||
self.emission: Image = None
|
|
||||||
self.ambient_occlusion: Image = None
|
|
||||||
self.height: Image = None
|
|
||||||
self.roughness: Image = None
|
|
||||||
self.material: Material = None
|
|
||||||
self.parent_mesh: Object = None
|
|
||||||
self.w: int = 0
|
|
||||||
self.h: int = 0
|
|
||||||
self.fit = None
|
|
||||||
|
|
||||||
def scale_images_to_largest(images: list[Image]) -> tuple[int, int]:
|
|
||||||
try:
|
|
||||||
valid_images = []
|
|
||||||
for img in images:
|
|
||||||
if img and hasattr(img, 'name'):
|
|
||||||
image_data = bpy.data.images.get(img.name)
|
|
||||||
if image_data and image_data.has_data:
|
|
||||||
valid_images.append(image_data)
|
|
||||||
|
|
||||||
if not valid_images:
|
|
||||||
return 1, 1
|
|
||||||
|
|
||||||
max_width = max(img.size[0] for img in valid_images)
|
|
||||||
max_height = max(img.size[1] for img in valid_images)
|
|
||||||
|
|
||||||
return max_width, max_height
|
|
||||||
except:
|
|
||||||
return 1, 1
|
|
||||||
|
|
||||||
def MaterialImageList_to_Image_list(classitem: MaterialImageList) -> list[Image]:
|
|
||||||
list_of_images: list[Image] = []
|
|
||||||
|
|
||||||
list_of_images.append(classitem.albedo)
|
|
||||||
list_of_images.append(classitem.normal)
|
|
||||||
list_of_images.append(classitem.emission)
|
|
||||||
list_of_images.append(classitem.ambient_occlusion)
|
|
||||||
list_of_images.append(classitem.height)
|
|
||||||
list_of_images.append(classitem.roughness)
|
|
||||||
|
|
||||||
return list_of_images
|
|
||||||
|
|
||||||
|
|
||||||
def get_material_images_from_scene(context: Context) -> list[MaterialImageList]:
|
|
||||||
material_image_list: list[MaterialImageList] = []
|
|
||||||
|
|
||||||
for obj in context.scene.objects:
|
|
||||||
if obj.type == 'MESH':
|
|
||||||
for mat_slot in obj.material_slots:
|
|
||||||
# Only process materials that are selected for atlas
|
|
||||||
if mat_slot.material and mat_slot.material.avatar_toolkit.include_in_atlas:
|
|
||||||
new_mat_image_item = MaterialImageList()
|
|
||||||
|
|
||||||
def get_or_create_image(image_name, replacement_name, default_color):
|
|
||||||
if image_name and image_name in bpy.data.images:
|
|
||||||
image = bpy.data.images[image_name]
|
|
||||||
else:
|
|
||||||
# Create a new image with the replacement name if it doesn't exist
|
|
||||||
if replacement_name in bpy.data.images:
|
|
||||||
image = bpy.data.images[replacement_name]
|
|
||||||
else:
|
|
||||||
image = bpy.data.images.new(
|
|
||||||
name=replacement_name, width=32, height=32, alpha=True
|
|
||||||
)
|
|
||||||
# Set the pixel data to the default color
|
|
||||||
num_pixels = 32 * 32
|
|
||||||
pixel_data = numpy.tile(numpy.array(default_color), num_pixels)
|
|
||||||
image.pixels[:] = pixel_data
|
|
||||||
# Set use_fake_user to True to prevent Blender from removing the image
|
|
||||||
image.use_fake_user = True
|
|
||||||
return image
|
|
||||||
|
|
||||||
# Albedo
|
|
||||||
albedo_name = getattr(mat_slot.material, 'texture_atlas_albedo', '')
|
|
||||||
new_mat_image_item.albedo = get_or_create_image(
|
|
||||||
albedo_name,
|
|
||||||
mat_slot.material.name + "_albedo_replacement",
|
|
||||||
[0.0, 0.0, 0.0, 1.0]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Normal
|
|
||||||
normal_name = getattr(mat_slot.material, 'texture_atlas_normal', '')
|
|
||||||
new_mat_image_item.normal = get_or_create_image(
|
|
||||||
normal_name,
|
|
||||||
mat_slot.material.name + "_normal_replacement",
|
|
||||||
[0.5, 0.5, 1.0, 1.0]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Emission
|
|
||||||
emission_name = getattr(mat_slot.material, 'texture_atlas_emission', '')
|
|
||||||
new_mat_image_item.emission = get_or_create_image(
|
|
||||||
emission_name,
|
|
||||||
mat_slot.material.name + "_emission_replacement",
|
|
||||||
[0.0, 0.0, 0.0, 1.0]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ambient Occlusion
|
|
||||||
ao_name = getattr(mat_slot.material, 'texture_atlas_ambient_occlusion', '')
|
|
||||||
new_mat_image_item.ambient_occlusion = get_or_create_image(
|
|
||||||
ao_name,
|
|
||||||
mat_slot.material.name + "_ambient_occlusion_replacement",
|
|
||||||
[1.0, 1.0, 1.0, 1.0]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Height
|
|
||||||
height_name = getattr(mat_slot.material, 'texture_atlas_height', '')
|
|
||||||
new_mat_image_item.height = get_or_create_image(
|
|
||||||
height_name,
|
|
||||||
mat_slot.material.name + "_height_replacement",
|
|
||||||
[0.5, 0.5, 0.5, 1.0]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Roughness
|
|
||||||
roughness_name = getattr(mat_slot.material, 'texture_atlas_roughness', '')
|
|
||||||
new_mat_image_item.roughness = get_or_create_image(
|
|
||||||
roughness_name,
|
|
||||||
mat_slot.material.name + "_roughness_replacement",
|
|
||||||
[1.0, 1.0, 1.0, 0.0]
|
|
||||||
)
|
|
||||||
|
|
||||||
new_mat_image_item.material = mat_slot.material
|
|
||||||
new_mat_image_item.parent_mesh = obj
|
|
||||||
material_image_list.append(new_mat_image_item)
|
|
||||||
|
|
||||||
return material_image_list
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def prep_images_in_scene(context: Context) -> list[MaterialImageList]:
|
|
||||||
preped_images: list[MaterialImageList] = get_material_images_from_scene(context)
|
|
||||||
for MaterialImageClass in preped_images:
|
|
||||||
ImageList: list[Image] = MaterialImageList_to_Image_list(MaterialImageClass)
|
|
||||||
|
|
||||||
MaterialImageClass.w, MaterialImageClass.h = scale_images_to_largest(ImageList)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return preped_images
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_AtlasMaterials(Operator):
|
|
||||||
bl_idname = "avatar_toolkit.atlas_materials"
|
|
||||||
bl_label = t("TextureAtlas.atlas_materials")
|
|
||||||
bl_description = t("TextureAtlas.atlas_materials_desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context: Context) -> bool:
|
|
||||||
return context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> set:
|
|
||||||
try:
|
|
||||||
# Get only materials that are explicitly marked for inclusion
|
|
||||||
selected_materials = [m for m in prep_images_in_scene(context)
|
|
||||||
if m.material and m.material.avatar_toolkit.include_in_atlas is True]
|
|
||||||
|
|
||||||
if not selected_materials:
|
|
||||||
self.report({'WARNING'}, t("TextureAtlas.no_materials_selected"))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
packer: BinPacker = BinPacker(selected_materials)
|
|
||||||
mat_images = packer.fit()
|
|
||||||
|
|
||||||
size: list[int] = [
|
|
||||||
max([
|
|
||||||
matimg.fit.w + matimg.albedo.size[0]
|
|
||||||
for matimg in mat_images
|
|
||||||
if matimg.albedo and matimg.albedo.has_data
|
|
||||||
] or [1]),
|
|
||||||
max([
|
|
||||||
matimg.fit.h + matimg.albedo.size[1]
|
|
||||||
for matimg in mat_images
|
|
||||||
if matimg.albedo and matimg.albedo.has_data
|
|
||||||
] or [1])
|
|
||||||
]
|
|
||||||
print([matimg.fit.w + matimg.albedo.size[0] for matimg in mat_images if matimg.albedo and matimg.albedo.has_data])
|
|
||||||
|
|
||||||
atlased_mat: MaterialImageList = MaterialImageList()
|
|
||||||
|
|
||||||
for mat in mat_images:
|
|
||||||
if mat.albedo and mat.albedo.has_data:
|
|
||||||
x: int = int(mat.fit.x)
|
|
||||||
y: int = int(mat.fit.y)
|
|
||||||
w: int = int(mat.albedo.size[0])
|
|
||||||
h: int = int(mat.albedo.size[1])
|
|
||||||
|
|
||||||
for obj in bpy.data.objects:
|
|
||||||
if obj.type == 'MESH':
|
|
||||||
mesh: Mesh = obj.data
|
|
||||||
for layer in mesh.polygons:
|
|
||||||
if obj.material_slots[layer.material_index].material:
|
|
||||||
if obj.material_slots[layer.material_index].material == mat.material:
|
|
||||||
for loop_idx in layer.loop_indices:
|
|
||||||
layer_loops: MeshUVLoopLayer
|
|
||||||
for layer_loops in mesh.uv_layers:
|
|
||||||
uv_item: Float2AttributeValue = layer_loops.uv[loop_idx]
|
|
||||||
uv_item.vector.x = (uv_item.vector.x * (w / size[0])) + (x / size[0])
|
|
||||||
uv_item.vector.y = (uv_item.vector.y * (h / size[1])) + (y / size[1])
|
|
||||||
|
|
||||||
for texture_type in ["albedo", "normal", "emission", "ambient_occlusion", "height", "roughness"]:
|
|
||||||
new_image_name: str = f"Atlas_{texture_type}_{context.scene.name}_{Path(bpy.data.filepath).stem}"
|
|
||||||
|
|
||||||
print(f"Processing {texture_type} atlas image")
|
|
||||||
|
|
||||||
if new_image_name in bpy.data.images:
|
|
||||||
bpy.data.images.remove(bpy.data.images[new_image_name])
|
|
||||||
|
|
||||||
canvas: Image = bpy.data.images.new(name=new_image_name, width=int(size[0]), height=int(size[1]), alpha=True)
|
|
||||||
c_w = canvas.size[0]
|
|
||||||
canvas_pixels: list[float] = list(canvas.pixels[:])
|
|
||||||
for mat in mat_images:
|
|
||||||
image_var: Image = getattr(mat, texture_type, None)
|
|
||||||
if image_var and image_var.has_data:
|
|
||||||
x: int = int(mat.fit.x)
|
|
||||||
y: int = int(mat.fit.y)
|
|
||||||
w: int = int(image_var.size[0])
|
|
||||||
h: int = int(image_var.size[1])
|
|
||||||
|
|
||||||
image_pixels: list[float] = list(image_var.pixels[:])
|
|
||||||
|
|
||||||
print(f"Writing image \"{image_var.name}\" to canvas.")
|
|
||||||
print(f"x: \"{x}\" y: \"{y}\" w: \"{w}\" h: \"{h}\"")
|
|
||||||
for k in range(0, h):
|
|
||||||
for i in range(0, w):
|
|
||||||
for channel in range(0, 4):
|
|
||||||
canvas_index = (((k + y) * c_w) + (i + x)) * 4 + channel
|
|
||||||
image_index = ((k * w) + i) * 4 + channel
|
|
||||||
canvas_pixels[int(canvas_index)] = image_pixels[int(image_index)]
|
|
||||||
|
|
||||||
canvas.pixels[:] = canvas_pixels[:]
|
|
||||||
canvas.save(filepath=os.path.join(os.path.dirname(bpy.data.filepath), new_image_name + ".png"))
|
|
||||||
setattr(atlased_mat, texture_type, canvas)
|
|
||||||
|
|
||||||
#I am sorry for the amount of nodes I'm instanciating here and their values.
|
|
||||||
#This is so that the nodes look pretty in the UI, which I think looks kinda nice. - @989onan
|
|
||||||
atlased_mat.material = bpy.data.materials.new(name="Atlas_Final_"+bpy.context.scene.name+"_"+Path(bpy.data.filepath).stem)
|
|
||||||
atlased_mat.material.use_nodes = True
|
|
||||||
atlased_mat.material.node_tree.nodes.clear()
|
|
||||||
|
|
||||||
principled_node: ShaderNodeBsdfPrincipled = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled")
|
|
||||||
principled_node.location.x = 7.29706335067749
|
|
||||||
principled_node.location.y = 298.918212890625
|
|
||||||
|
|
||||||
output_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeOutputMaterial")
|
|
||||||
output_node.location.x = 297.29705810546875
|
|
||||||
output_node.location.y = 298.918212890625
|
|
||||||
|
|
||||||
albedo_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
|
|
||||||
albedo_node.location.x = -588.6177978515625
|
|
||||||
albedo_node.location.y = 414.1948547363281
|
|
||||||
albedo_node.image = atlased_mat.albedo
|
|
||||||
|
|
||||||
emission_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
|
|
||||||
emission_node.location.x = -588.6177978515625
|
|
||||||
emission_node.location.y = -173.9259033203125
|
|
||||||
emission_node.image = atlased_mat.emission
|
|
||||||
|
|
||||||
normal_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
|
|
||||||
normal_node.location.x = -941.4189453125
|
|
||||||
normal_node.location.y = -20.8391780853271
|
|
||||||
normal_node.image = atlased_mat.normal
|
|
||||||
|
|
||||||
normal_map_node: ShaderNodeNormalMap = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeNormalMap")
|
|
||||||
normal_map_node.location.x = -545.550537109375
|
|
||||||
normal_map_node.location.y = -0.7543716430664062
|
|
||||||
|
|
||||||
roughness_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
|
|
||||||
roughness_node.location.x = -592.1703491210938
|
|
||||||
roughness_node.location.y = 206.74075317382812
|
|
||||||
roughness_node.image = atlased_mat.roughness
|
|
||||||
|
|
||||||
ambient_occlusion_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
|
|
||||||
ambient_occlusion_node.location.x = -906.4371337890625
|
|
||||||
ambient_occlusion_node.location.y = -389.9602355957031
|
|
||||||
ambient_occlusion_node.image = atlased_mat.ambient_occlusion
|
|
||||||
|
|
||||||
height_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
|
|
||||||
height_node.location.x = -1222.383056640625
|
|
||||||
height_node.location.y = -375.48406982421875
|
|
||||||
height_node.image = atlased_mat.height
|
|
||||||
|
|
||||||
atlased_mat.material.node_tree.links.new(principled_node.inputs["Base Color"], albedo_node.outputs["Color"])
|
|
||||||
atlased_mat.material.node_tree.links.new(principled_node.inputs["Metallic"], roughness_node.outputs["Alpha"])
|
|
||||||
atlased_mat.material.node_tree.links.new(principled_node.inputs["Roughness"], roughness_node.outputs["Color"])
|
|
||||||
atlased_mat.material.node_tree.links.new(principled_node.inputs["Alpha"], albedo_node.outputs["Alpha"])
|
|
||||||
atlased_mat.material.node_tree.links.new(principled_node.inputs["Normal"], normal_map_node.outputs["Normal"])
|
|
||||||
atlased_mat.material.node_tree.links.new(principled_node.inputs["Emission Color"], emission_node.outputs["Color"])
|
|
||||||
atlased_mat.material.node_tree.links.new(output_node.inputs["Surface"], principled_node.outputs["BSDF"])
|
|
||||||
atlased_mat.material.node_tree.links.new(normal_map_node.inputs["Color"], normal_node.outputs["Color"])
|
|
||||||
|
|
||||||
# Only update selected materials for meshes
|
|
||||||
for obj in context.scene.objects:
|
|
||||||
if obj.type == 'MESH':
|
|
||||||
mesh: Mesh = obj.data
|
|
||||||
for i, mat_slot in enumerate(obj.material_slots):
|
|
||||||
if mat_slot.material and mat_slot.material.avatar_toolkit.include_in_atlas is True:
|
|
||||||
mesh.materials[i] = atlased_mat.material
|
|
||||||
|
|
||||||
self.report({'INFO'}, t("TextureAtlas.atlas_completed"))
|
|
||||||
return {"FINISHED"}
|
|
||||||
except Exception as e:
|
|
||||||
self.report({'ERROR'}, t("TextureAtlas.atlas_error"))
|
|
||||||
raise e
|
|
||||||
return {"FINISHED"}
|
|
||||||
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
import bpy
|
|
||||||
import re
|
|
||||||
from typing import List, Tuple, Optional, Set, Dict
|
|
||||||
from bpy.types import Material, Operator, Context, Object, NodeTree
|
|
||||||
from ..core.common import clean_material_names, get_selected_armature, is_valid_armature, get_all_meshes, init_progress, update_progress, finish_progress
|
|
||||||
from ..core.translations import t
|
|
||||||
|
|
||||||
def textures_match(tex1: bpy.types.ImageTexture, tex2: bpy.types.ImageTexture) -> bool:
|
|
||||||
return tex1.image == tex2.image and tex1.extension == tex2.extension
|
|
||||||
|
|
||||||
def consolidate_nodes(node1: bpy.types.ShaderNodeTexImage, node2: bpy.types.ShaderNodeTexImage) -> None:
|
|
||||||
node2.color_space = node1.color_space
|
|
||||||
node2.coordinates = node1.coordinates
|
|
||||||
|
|
||||||
def copy_tex_nodes(mat1: Material, mat2: Material) -> None:
|
|
||||||
for node1 in mat1.node_tree.nodes:
|
|
||||||
if node1.type == 'TEX_IMAGE':
|
|
||||||
node2 = mat2.node_tree.nodes.get(node1.name)
|
|
||||||
if node2:
|
|
||||||
node2.mapping = node1.mapping
|
|
||||||
node2.projection = node1.projection
|
|
||||||
|
|
||||||
def consolidate_textures(node_tree1: NodeTree, node_tree2: NodeTree) -> None:
|
|
||||||
for node1 in node_tree1.nodes:
|
|
||||||
if node1.type == 'TEX_IMAGE':
|
|
||||||
for node2 in node_tree2.nodes:
|
|
||||||
if (node2.type == 'TEX_IMAGE' and
|
|
||||||
node1.image == node2.image):
|
|
||||||
consolidate_nodes(node1, node2)
|
|
||||||
node2.image = node1.image
|
|
||||||
elif node1.type == 'GROUP':
|
|
||||||
if node1.node_tree and node2.node_tree:
|
|
||||||
consolidate_textures(node1.node_tree, node2.node_tree)
|
|
||||||
|
|
||||||
def color_match(col1: Tuple[float, float, float, float], col2: Tuple[float, float, float, float], tolerance: float = 0.01) -> bool:
|
|
||||||
return all(abs(c1 - c2) < tolerance for c1, c2 in zip(col1, col2))
|
|
||||||
|
|
||||||
def materials_match(mat1: Material, mat2: Material, tolerance: float = 0.01) -> bool:
|
|
||||||
if not color_match(mat1.diffuse_color, mat2.diffuse_color, tolerance):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if abs(mat1.roughness - mat2.roughness) > tolerance:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if mat1.node_tree and mat2.node_tree:
|
|
||||||
consolidate_textures(mat1.node_tree, mat2.node_tree)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_base_name(name: str) -> str:
|
|
||||||
mat_match = re.match(r"^(.*)\.\d{3}$", name)
|
|
||||||
return mat_match.group(1) if mat_match else name
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_CombineMaterials(Operator):
|
|
||||||
bl_idname = "avatar_toolkit.combine_materials"
|
|
||||||
bl_label = t("Optimization.combine_materials.label")
|
|
||||||
bl_description = t("Optimization.combine_materials.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context: Context) -> bool:
|
|
||||||
armature = get_selected_armature(context)
|
|
||||||
return armature is not None and is_valid_armature(armature)
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
|
||||||
armature = get_selected_armature(context)
|
|
||||||
if not armature:
|
|
||||||
self.report({'WARNING'}, t("Optimization.no_armature_selected"))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
context.view_layer.objects.active = armature
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
meshes = get_all_meshes(context)
|
|
||||||
if not meshes:
|
|
||||||
self.report({'WARNING'}, t("Optimization.no_meshes_found"))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
init_progress(context, 5) # 5 steps in total
|
|
||||||
|
|
||||||
update_progress(self, context, t("Optimization.consolidating_materials"))
|
|
||||||
num_combined = self.consolidate_materials(meshes)
|
|
||||||
|
|
||||||
update_progress(self, context, t("Optimization.cleaning_material_slots"))
|
|
||||||
cleaned_slots = self.clean_material_slots(meshes)
|
|
||||||
|
|
||||||
update_progress(self, context, t("Optimization.cleaning_material_names"))
|
|
||||||
cleaned_names = self.clean_material_names()
|
|
||||||
|
|
||||||
update_progress(self, context, t("Optimization.clearing_unused_data"))
|
|
||||||
removed_data_blocks = self.clear_unused_data_blocks()
|
|
||||||
|
|
||||||
update_progress(self, context, t("Optimization.finalizing"))
|
|
||||||
finish_progress(context)
|
|
||||||
|
|
||||||
self.report({'INFO'}, t("Optimization.materials_optimization_report").format(
|
|
||||||
num_combined=num_combined,
|
|
||||||
num_cleaned_slots=cleaned_slots,
|
|
||||||
num_cleaned_names=cleaned_names,
|
|
||||||
num_removed_data_blocks=removed_data_blocks
|
|
||||||
))
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def consolidate_materials(self, meshes: List[Object]) -> int:
|
|
||||||
mat_mapping: Dict[str, Material] = {}
|
|
||||||
num_combined: int = 0
|
|
||||||
for mesh in meshes:
|
|
||||||
for slot in mesh.material_slots:
|
|
||||||
mat: Optional[Material] = slot.material
|
|
||||||
if mat:
|
|
||||||
base_name: str = get_base_name(mat.name)
|
|
||||||
|
|
||||||
if base_name in mat_mapping:
|
|
||||||
base_mat: Material = mat_mapping[base_name]
|
|
||||||
try:
|
|
||||||
if materials_match(base_mat, mat):
|
|
||||||
consolidate_textures(base_mat.node_tree, mat.node_tree)
|
|
||||||
num_combined += 1
|
|
||||||
slot.material = base_mat
|
|
||||||
except AttributeError:
|
|
||||||
self.report({'WARNING'}, t("Optimization.material_attribute_mismatch").format(material_name=mat.name))
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
mat_mapping[base_name] = mat
|
|
||||||
return num_combined
|
|
||||||
|
|
||||||
def clean_material_slots(self, meshes: List[Object]) -> int:
|
|
||||||
cleaned_slots = 0
|
|
||||||
for obj in meshes:
|
|
||||||
obj.select_set(True)
|
|
||||||
bpy.context.view_layer.objects.active = obj
|
|
||||||
initial_slots = len(obj.material_slots)
|
|
||||||
bpy.ops.object.material_slot_remove_unused()
|
|
||||||
cleaned_slots += initial_slots - len(obj.material_slots)
|
|
||||||
obj.select_set(False)
|
|
||||||
return cleaned_slots
|
|
||||||
|
|
||||||
def clean_material_names(self) -> int:
|
|
||||||
cleaned_names = 0
|
|
||||||
for obj in bpy.data.objects:
|
|
||||||
if obj.type == 'MESH':
|
|
||||||
result = clean_material_names(obj)
|
|
||||||
if result is not None:
|
|
||||||
cleaned_names += result
|
|
||||||
return cleaned_names
|
|
||||||
|
|
||||||
def clear_unused_data_blocks(self) -> int:
|
|
||||||
initial_count = sum(len(getattr(bpy.data, attr)) for attr in dir(bpy.data) if isinstance(getattr(bpy.data, attr), bpy.types.bpy_prop_collection))
|
|
||||||
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
|
|
||||||
final_count = sum(len(getattr(bpy.data, attr)) for attr in dir(bpy.data) if isinstance(getattr(bpy.data, attr), bpy.types.bpy_prop_collection))
|
|
||||||
return initial_count - final_count
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
import bpy
|
|
||||||
from ..core import common
|
|
||||||
from ..core.translations import t
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_CreateDigitigradeLegs(bpy.types.Operator):
|
|
||||||
bl_idname = "avatar_toolkit.create_digitigrade_legs"
|
|
||||||
bl_label = t('Tools.create_digitigrade_legs.label')
|
|
||||||
bl_description = t('Tools.create_digitigrade_legs.desc')
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
if(context.active_object is None):
|
|
||||||
return False
|
|
||||||
if(context.selected_editable_bones is not None):
|
|
||||||
if(len(context.selected_editable_bones) == 2):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
|
|
||||||
for digi0 in context.selected_editable_bones:
|
|
||||||
digi1: bpy.types.EditBone = None
|
|
||||||
digi2: bpy.types.EditBone = None
|
|
||||||
digi3: bpy.types.EditBone = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
digi1 = digi0.children[0]
|
|
||||||
digi2 = digi1.children[0]
|
|
||||||
digi3 = digi2.children[0]
|
|
||||||
except:
|
|
||||||
self.report({'ERROR'}, t('Tools.digitigrade_legs.error.bone_format'))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
digi4 = None
|
|
||||||
try:
|
|
||||||
digi4 = digi3.children[0]
|
|
||||||
|
|
||||||
except:
|
|
||||||
print("no toe bone. Continuing.")
|
|
||||||
digi0.select = True
|
|
||||||
digi1.select = True
|
|
||||||
digi2.select = True
|
|
||||||
digi3.select = True
|
|
||||||
if(digi4):
|
|
||||||
digi4.select = True
|
|
||||||
bpy.ops.armature.roll_clear()
|
|
||||||
bpy.ops.armature.select_all(action='DESELECT')
|
|
||||||
|
|
||||||
#creating transform for upper leg
|
|
||||||
digi0.select = True
|
|
||||||
bpy.ops.transform.create_orientation(name="Toolkit_digi0", overwrite=True)
|
|
||||||
bpy.ops.armature.select_all(action='DESELECT')
|
|
||||||
|
|
||||||
|
|
||||||
#duplicate digi0 and assign it to thigh
|
|
||||||
thigh = common.duplicatebone(digi0)
|
|
||||||
bpy.ops.armature.select_all(action='DESELECT')
|
|
||||||
|
|
||||||
#make digi2 parrallel to digi1
|
|
||||||
digi2.align_orientation(digi0)
|
|
||||||
|
|
||||||
#extrude thigh
|
|
||||||
thigh.select_tail = True
|
|
||||||
bpy.ops.armature.extrude_move(ARMATURE_OT_extrude={"forked":False},TRANSFORM_OT_translate=None)
|
|
||||||
#set new bone to calf varible
|
|
||||||
bpy.ops.armature.select_more()
|
|
||||||
calf = context.selected_bones[0]
|
|
||||||
bpy.ops.armature.select_all(action='DESELECT')
|
|
||||||
|
|
||||||
#set calf end to digi2 end
|
|
||||||
calf.tail = digi2.tail
|
|
||||||
|
|
||||||
#make copy of calf, flip it, and then align bone so that it's head is moved to match in align phase
|
|
||||||
flipedcalf = common.duplicatebone(calf)
|
|
||||||
bpy.ops.armature.select_all(action='DESELECT')
|
|
||||||
flipedcalf.select = True
|
|
||||||
bpy.ops.armature.switch_direction()
|
|
||||||
bpy.ops.armature.select_all(action='DESELECT')
|
|
||||||
flippeddigi1 = common.duplicatebone(digi1)
|
|
||||||
bpy.ops.armature.select_all(action='DESELECT')
|
|
||||||
flippeddigi1.select = True
|
|
||||||
bpy.ops.armature.switch_direction()
|
|
||||||
bpy.ops.armature.select_all(action='DESELECT')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#align flipped calf to flipped middle leg to move the head
|
|
||||||
flipedcalf.align_orientation(flippeddigi1)
|
|
||||||
|
|
||||||
flipedcalf.length = flippeddigi1.length
|
|
||||||
|
|
||||||
#assign calf tail to flipped calf head so it moves calf's tail to be out at the perfect parallelagram
|
|
||||||
calf.head = flipedcalf.tail
|
|
||||||
|
|
||||||
#delete helper bones
|
|
||||||
bpy.ops.armature.select_all(action='DESELECT')
|
|
||||||
flippeddigi1.select = True
|
|
||||||
bpy.ops.armature.delete()
|
|
||||||
bpy.ops.armature.select_all(action='DESELECT')
|
|
||||||
flipedcalf.select = True
|
|
||||||
bpy.ops.armature.delete()
|
|
||||||
bpy.ops.armature.select_all(action='DESELECT')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#reparent the foot to the new calf so it will be part of the new foot IK chain
|
|
||||||
digi3.parent = calf
|
|
||||||
#Tada! It's done! now to rename the old 3 segments that make up the old part to noik so resonite doesn't try to select them
|
|
||||||
|
|
||||||
digi0.name = re.compile(re.escape("<noik>"), re.IGNORECASE).sub("",digi0.name)+"<noik>"
|
|
||||||
digi1.name = re.compile(re.escape("<noik>"), re.IGNORECASE).sub("",digi1.name)+"<noik>"
|
|
||||||
digi2.name = re.compile(re.escape("<noik>"), re.IGNORECASE).sub("",digi2.name)+"<noik>"
|
|
||||||
#finally fully done!
|
|
||||||
|
|
||||||
self.report({'INFO'}, t('Tools.digitigrade_legs.success'))
|
|
||||||
return {'FINISHED'}
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
import bpy
|
|
||||||
from bpy.types import Operator
|
|
||||||
from bpy_extras.io_utils import ImportHelper
|
|
||||||
from ..core.importers.importer import imports, import_types
|
|
||||||
from ..core.common import remove_default_objects
|
|
||||||
from ..core.translations import t
|
|
||||||
import pathlib
|
|
||||||
import os
|
|
||||||
|
|
||||||
VRM_IMPORTER_URL = "https://github.com/saturday06/VRM_Addon_for_Blender"
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_ImportAnyModel(Operator, ImportHelper):
|
|
||||||
bl_idname = 'avatar_toolkit.import_any_model'
|
|
||||||
bl_label = t('Tools.import_any_model.label')
|
|
||||||
bl_description = t('Tools.import_any_model.desc')
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'})
|
|
||||||
|
|
||||||
filter_glob: bpy.props.StringProperty(default=imports, options={'HIDDEN', 'SKIP_SAVE'})
|
|
||||||
directory: bpy.props.StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN', 'SKIP_SAVE'})
|
|
||||||
|
|
||||||
# since I wrote this myself, a bit more efficient than cats. mostly - @989onan
|
|
||||||
def execute(self, context: bpy.types.Context):
|
|
||||||
file_grouping_dict: dict[str, list[dict[str, str]]] = dict() # group our files so our importers can import them together. in the case of OBJ+MTL and others that need grouped files, this is extremely important.
|
|
||||||
remove_default_objects()
|
|
||||||
# check if we are importing multiple files
|
|
||||||
is_multi = len(self.files) > 0
|
|
||||||
|
|
||||||
if is_multi:
|
|
||||||
for file in self.files:
|
|
||||||
fullpath = os.path.join(self.directory, os.path.basename(file.name))
|
|
||||||
name = pathlib.Path(fullpath).suffix.replace(".", "")
|
|
||||||
# this makes sure our imports that should be grouped stay together.
|
|
||||||
# basically the method checks for if the first value has a lambda with the same bytecode as another lambda, then it will use that value's key (ex:"obj"<->"mtl" or "fbx"), keeping same importers together
|
|
||||||
if name not in file_grouping_dict:
|
|
||||||
file_grouping_dict[name] = []
|
|
||||||
file_grouping_dict[name].append({"name": os.path.basename(file.name)}) # emulate passing a list of files.
|
|
||||||
else:
|
|
||||||
fullpath: str = os.path.join(os.path.dirname(self.filepath), os.path.basename(self.filepath))
|
|
||||||
name = pathlib.Path(fullpath).suffix.replace(".", "")
|
|
||||||
if name not in file_grouping_dict:
|
|
||||||
file_grouping_dict[name] = []
|
|
||||||
file_grouping_dict[name].append({"name": fullpath}) # emulate passing a list of files.
|
|
||||||
|
|
||||||
# import the files together to make sure things like obj import together. This is important
|
|
||||||
for file_group_name, files in file_grouping_dict.items():
|
|
||||||
try:
|
|
||||||
# Check for VRM importer availability
|
|
||||||
if file_group_name == "vrm" and not hasattr(bpy.ops.import_scene, "vrm"):
|
|
||||||
bpy.ops.wm.vrm_importer_popup('INVOKE_DEFAULT')
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
if self.directory:
|
|
||||||
import_types[file_group_name](self.directory, files, self.filepath)
|
|
||||||
else:
|
|
||||||
import_types[file_group_name]("", files, self.filepath) # give an empty directory, works just fine for 90%
|
|
||||||
except AttributeError as e:
|
|
||||||
if file_group_name == "vrm":
|
|
||||||
bpy.ops.wm.vrm_importer_popup('INVOKE_DEFAULT')
|
|
||||||
else:
|
|
||||||
self.report({'ERROR'}, t('Importing.need_importer').format(extension=file_group_name))
|
|
||||||
print("Importer error:", e)
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
self.report({'INFO'}, t('Quick_Access.import_success'))
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class VRMImporterPopup(Operator):
|
|
||||||
bl_idname = "wm.vrm_importer_popup"
|
|
||||||
bl_label = "VRM Importer Not Installed"
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
return context.window_manager.invoke_props_dialog(self, width=300)
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
layout.label(text="VRM importer plugin is not installed.")
|
|
||||||
layout.label(text="Please install it to import VRM files.")
|
|
||||||
layout.operator("wm.url_open", text="Get VRM Importer").url = VRM_IMPORTER_URL
|
|
||||||
|
|
||||||
#TODO: This needs to be done with our own MMD importer.
|
|
||||||
"""
|
|
||||||
#stolen from cats. Oh wait I made this code riiiiiiight - @989onan
|
|
||||||
|
|
||||||
class ImportMMDAnimation(bpy.types.Operator, ImportHelper):
|
|
||||||
bl_idname = 'avatar_toolkit.import_mmd_animation'
|
|
||||||
bl_label = t('Importer.mmd_anim_importer.label')
|
|
||||||
bl_description = t('Importer.mmd_anim_importer.desc')
|
|
||||||
bl_options = {'INTERNAL', 'UNDO'}
|
|
||||||
|
|
||||||
filter_glob: bpy.props.StringProperty(
|
|
||||||
default="*.vmd",
|
|
||||||
options={'HIDDEN'}
|
|
||||||
)
|
|
||||||
files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'})
|
|
||||||
directory: bpy.props.StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN', 'SKIP_SAVE'})
|
|
||||||
filepath: bpy.props.StringProperty()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
if common.get_armature(context) is None:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
|
|
||||||
# Make sure that the first layer is visible
|
|
||||||
if hasattr(context.scene, 'layers'):
|
|
||||||
context.scene.layers[0] = True
|
|
||||||
|
|
||||||
filename, extension = os.path.splitext(self.filepath)
|
|
||||||
|
|
||||||
if(extension == ".vmd"):
|
|
||||||
|
|
||||||
#A dictionary to change the current model to MMD importer compatable temporarily
|
|
||||||
bonedict = {
|
|
||||||
"chest":"UpperBody",
|
|
||||||
"neck":"Neck",
|
|
||||||
"head":"Head",
|
|
||||||
"hips":"Center",
|
|
||||||
"spine":"LowerBody",
|
|
||||||
|
|
||||||
"right_wrist":"Wrist_R",
|
|
||||||
"right_elbow":"Elbow_R",
|
|
||||||
"right_arm":"Arm_R",
|
|
||||||
"right_shoulder":"Shoulder_R",
|
|
||||||
"right_leg":"Leg_R",
|
|
||||||
"right_knee":"Knee_R",
|
|
||||||
"right_ankle":"Ankle_R",
|
|
||||||
"right_toe":"Toe_R",
|
|
||||||
|
|
||||||
|
|
||||||
"left_wrist":"Wrist_L",
|
|
||||||
"left_elbow":"Elbow_L",
|
|
||||||
"left_arm":"Arm_L",
|
|
||||||
"left_shoulder":"Shoulder_L",
|
|
||||||
"left_leg":"Leg_L",
|
|
||||||
"left_knee":"Knee_L",
|
|
||||||
"left_ankle":"Ankle_L",
|
|
||||||
"left_toe":"Toe_L"
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
armature = common.get_armature(context)
|
|
||||||
common.unselect_all()
|
|
||||||
common.Set_Mode(context, 'OBJECT')
|
|
||||||
common.unselect_all()
|
|
||||||
common.set_active(armature)
|
|
||||||
|
|
||||||
orig_names = dict()
|
|
||||||
reverse_bone_lookup = dict()
|
|
||||||
for (preferred_name, name_list) in bone_names.items():
|
|
||||||
for name in name_list:
|
|
||||||
reverse_bone_lookup[name] = preferred_name
|
|
||||||
|
|
||||||
|
|
||||||
for bone in armature.data.bones:
|
|
||||||
if common.simplify_bonename(bone.name) in reverse_bone_lookup and reverse_bone_lookup[common.simplify_bonename(bone.name)] in bonedict:
|
|
||||||
orig_names[bonedict[reverse_bone_lookup[common.simplify_bonename(bone.name)]]] = bone.name
|
|
||||||
bone.name = bonedict[reverse_bone_lookup[common.simplify_bonename(bone.name)]]
|
|
||||||
try:
|
|
||||||
bpy.ops.mmd_tools.import_vmd(filepath=self.filepath,bone_mapper='RENAMED_BONES',use_underscore=True, dictionary='INTERNAL')
|
|
||||||
except AttributeError as e:
|
|
||||||
print("importer error was:")
|
|
||||||
print(e)
|
|
||||||
print(t('Importing.importer_search_term'))
|
|
||||||
common.open_web_after_delay_multi_threaded(delay=12, url=t('Importing.importer_search_term').format(extension = "MMD"))
|
|
||||||
self.report({'ERROR'},t('Importing.need_importer').format(extension = "MMD"))
|
|
||||||
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
#iterate through bones and put them back, therefore blender API will change the animation to be correct.
|
|
||||||
#this is because renaming bones fixes the animation targets in the data model.
|
|
||||||
for bone in armature.data.bones:
|
|
||||||
if common.simplify_bonename(bone.name) in orig_names:
|
|
||||||
bone.name = orig_names[common.simplify_bonename(bone.name)]
|
|
||||||
|
|
||||||
common.unselect_all()
|
|
||||||
common.Set_Mode(context, 'OBJECT')
|
|
||||||
common.unselect_all()
|
|
||||||
common.set_active(armature)
|
|
||||||
|
|
||||||
return {'FINISHED'} """
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
import numpy as np
|
|
||||||
import bpy
|
|
||||||
from typing import List, Optional, Set
|
|
||||||
from bpy.types import Operator, Context, Object
|
|
||||||
from ..core.common import fix_uv_coordinates, get_selected_armature, get_all_meshes, is_valid_armature, apply_shapekey_to_basis, has_shapekeys, select_current_armature, init_progress, update_progress, finish_progress
|
|
||||||
from ..core.translations import t
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolkit_OT_RemoveUnusedShapekeys(bpy.types.Operator):
|
|
||||||
tolerance: bpy.props.FloatProperty(name=t("Tools.remove_unused_shapekeys.tolerance.label"), default=0.001, description=t("Tools.remove_unused_shapekeys.tolerance.desc"))
|
|
||||||
bl_idname = "avatar_toolkit.remove_unused_shapekeys"
|
|
||||||
bl_label = t("Tools.remove_unused_shapekeys.label")
|
|
||||||
bl_description = t("Tools.remove_unused_shapekeys.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context: Context) -> bool:
|
|
||||||
armature = get_selected_armature(context)
|
|
||||||
return armature is not None and is_valid_armature(armature) and (len(get_all_meshes(context)) > 0) and (context.mode == "OBJECT")
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
|
||||||
#Shamefully taken from: https://blender.stackexchange.com/a/237611
|
|
||||||
#at least I am crediting them - @989onan
|
|
||||||
for ob in get_all_meshes(context):
|
|
||||||
if not ob.data.shape_keys: continue
|
|
||||||
if not ob.data.shape_keys.use_relative: continue
|
|
||||||
|
|
||||||
kbs = ob.data.shape_keys.key_blocks
|
|
||||||
nverts = len(ob.data.vertices)
|
|
||||||
to_delete = []
|
|
||||||
|
|
||||||
# Cache locs for rel keys since many keys have the same rel key
|
|
||||||
cache = {}
|
|
||||||
|
|
||||||
locs = np.empty(3*nverts, dtype=np.float32)
|
|
||||||
|
|
||||||
for kb in kbs:
|
|
||||||
if kb == kb.relative_key: continue
|
|
||||||
|
|
||||||
kb.data.foreach_get("co", locs)
|
|
||||||
|
|
||||||
if kb.relative_key.name not in cache:
|
|
||||||
rel_locs = np.empty(3*nverts, dtype=np.float32)
|
|
||||||
kb.relative_key.data.foreach_get("co", rel_locs)
|
|
||||||
cache[kb.relative_key.name] = rel_locs
|
|
||||||
rel_locs = cache[kb.relative_key.name]
|
|
||||||
|
|
||||||
locs -= rel_locs
|
|
||||||
if (np.abs(locs) < self.tolerance).all():
|
|
||||||
to_delete.append(kb.name)
|
|
||||||
|
|
||||||
for kb_name in to_delete:
|
|
||||||
if ("-" in kb_name) or ("=" in kb_name) or ("~" in kb_name):
|
|
||||||
continue
|
|
||||||
ob.shape_key_remove(ob.data.shape_keys.key_blocks[kb_name])
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolkit_OT_ApplyShapeKey(bpy.types.Operator):
|
|
||||||
bl_idname = "avatar_toolkit.apply_shape_key"
|
|
||||||
bl_label = t("Tools.apply_shape_key.label")
|
|
||||||
bl_description = t("Tools.apply_shape_key.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context: Context) -> bool:
|
|
||||||
armature = get_selected_armature(context)
|
|
||||||
return armature is not None and is_valid_armature(armature) and (len(get_all_meshes(context)) > 0) and (context.mode == "OBJECT") and context.view_layer.objects.active is not None and has_shapekeys(context.view_layer.objects.active)
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
|
||||||
obj: bpy.types.Object = context.view_layer.objects.active
|
|
||||||
|
|
||||||
|
|
||||||
if (apply_shapekey_to_basis(context,obj,obj.active_shape_key.name,False)):
|
|
||||||
return {'FINISHED'}
|
|
||||||
else:
|
|
||||||
self.report({'ERROR'}, t("Tools.apply_shape_key.error"))
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_JoinAllMeshes(Operator):
|
|
||||||
bl_idname = "avatar_toolkit.join_all_meshes"
|
|
||||||
bl_label = t("Optimization.join_all_meshes.label")
|
|
||||||
bl_description = t("Optimization.join_all_meshes.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context: Context) -> bool:
|
|
||||||
armature = get_selected_armature(context)
|
|
||||||
return armature is not None and is_valid_armature(armature)
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
|
||||||
try:
|
|
||||||
self.join_all_meshes(context)
|
|
||||||
return {'FINISHED'}
|
|
||||||
except Exception as e:
|
|
||||||
self.report({'ERROR'}, f"{t('Optimization.join_error')}: {str(e)}")
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
def join_all_meshes(self, context: Context) -> None:
|
|
||||||
if not select_current_armature(context):
|
|
||||||
raise ValueError(t("Optimization.no_armature_selected"))
|
|
||||||
|
|
||||||
armature = get_selected_armature(context)
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
|
|
||||||
meshes: List[Object] = get_all_meshes(context)
|
|
||||||
|
|
||||||
if not meshes:
|
|
||||||
raise ValueError(t("Optimization.no_meshes_found"))
|
|
||||||
|
|
||||||
init_progress(context, 5) # 5 steps in total
|
|
||||||
|
|
||||||
update_progress(self, context, t("Optimization.selecting_meshes"))
|
|
||||||
for mesh in meshes:
|
|
||||||
mesh.select_set(True)
|
|
||||||
|
|
||||||
if bpy.context.selected_objects:
|
|
||||||
bpy.context.view_layer.objects.active = bpy.context.selected_objects[0]
|
|
||||||
|
|
||||||
update_progress(self, context, t("Optimization.joining_meshes"))
|
|
||||||
try:
|
|
||||||
bpy.ops.object.join()
|
|
||||||
except RuntimeError as e:
|
|
||||||
raise RuntimeError(f"{t('Optimization.join_operation_failed')}: {str(e)}")
|
|
||||||
|
|
||||||
update_progress(self, context, t("Optimization.applying_transforms"))
|
|
||||||
try:
|
|
||||||
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
|
|
||||||
except RuntimeError as e:
|
|
||||||
raise RuntimeError(f"{t('Optimization.transform_apply_failed')}: {str(e)}")
|
|
||||||
|
|
||||||
update_progress(self, context, t("Optimization.fixing_uv_coordinates"))
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
fix_uv_coordinates(context)
|
|
||||||
|
|
||||||
update_progress(self, context, t("Optimization.finalizing"))
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
self.report({'INFO'}, t("Optimization.meshes_joined"))
|
|
||||||
else:
|
|
||||||
raise ValueError(t("Optimization.no_mesh_selected"))
|
|
||||||
|
|
||||||
context.view_layer.objects.active = armature
|
|
||||||
finish_progress(context)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_JoinSelectedMeshes(Operator):
|
|
||||||
bl_idname = "avatar_toolkit.join_selected_meshes"
|
|
||||||
bl_label = t("Optimization.join_selected_meshes.label")
|
|
||||||
bl_description = t("Optimization.join_selected_meshes.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context: Context) -> bool:
|
|
||||||
return context.mode == 'OBJECT' and len([obj for obj in context.selected_objects if obj.type == 'MESH']) > 1
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
|
||||||
try:
|
|
||||||
self.join_selected_meshes(context)
|
|
||||||
return {'FINISHED'}
|
|
||||||
except Exception as e:
|
|
||||||
self.report({'ERROR'}, f"{t('Optimization.join_error')}: {str(e)}")
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
def join_selected_meshes(self, context: Context) -> None:
|
|
||||||
selected_objects: List[Object] = [obj for obj in bpy.context.selected_objects if obj.type == 'MESH']
|
|
||||||
|
|
||||||
if len(selected_objects) < 2:
|
|
||||||
raise ValueError(t("Optimization.select_at_least_two_meshes"))
|
|
||||||
|
|
||||||
init_progress(context, 5) # 5 steps in total
|
|
||||||
|
|
||||||
update_progress(self, context, t("Optimization.preparing_meshes"))
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
|
|
||||||
update_progress(self, context, t("Optimization.selecting_meshes"))
|
|
||||||
for obj in selected_objects:
|
|
||||||
obj.select_set(True)
|
|
||||||
|
|
||||||
if bpy.context.selected_objects:
|
|
||||||
bpy.context.view_layer.objects.active = bpy.context.selected_objects[0]
|
|
||||||
|
|
||||||
update_progress(self, context, t("Optimization.joining_meshes"))
|
|
||||||
try:
|
|
||||||
bpy.ops.object.join()
|
|
||||||
except RuntimeError as e:
|
|
||||||
raise RuntimeError(f"{t('Optimization.join_operation_failed')}: {str(e)}")
|
|
||||||
|
|
||||||
update_progress(self, context, t("Optimization.applying_transforms"))
|
|
||||||
try:
|
|
||||||
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
|
|
||||||
except RuntimeError as e:
|
|
||||||
raise RuntimeError(f"{t('Optimization.transform_apply_failed')}: {str(e)}")
|
|
||||||
|
|
||||||
update_progress(self, context, t("Optimization.fixing_uv_coordinates"))
|
|
||||||
fix_uv_coordinates(context)
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
self.report({'INFO'}, t("Optimization.selected_meshes_joined"))
|
|
||||||
else:
|
|
||||||
raise ValueError(t("Optimization.no_mesh_selected"))
|
|
||||||
|
|
||||||
finish_progress(context)
|
|
||||||
@@ -1,396 +0,0 @@
|
|||||||
import bpy
|
|
||||||
import numpy as np
|
|
||||||
import re
|
|
||||||
from bpy.types import Operator, Context, Material, ShaderNodeTexImage, ShaderNodeGroup, Object
|
|
||||||
from ..core.translations import t
|
|
||||||
from ..core.common import get_selected_armature, is_valid_armature, get_all_meshes, init_progress, update_progress, finish_progress
|
|
||||||
from ..functions.additional_tools import AvatarToolKit_OT_ConnectBones, AvatarToolKit_OT_DeleteBoneConstraints
|
|
||||||
from ..functions.armature_modifying import AvatarToolkit_OT_RemoveZeroWeightBones, AvatarToolkit_OT_MergeBonesToParents
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_CleanupMesh(Operator):
|
|
||||||
bl_idname = "avatar_toolkit.cleanup_mesh"
|
|
||||||
bl_label = t("MMDOptions.cleanup_mesh.label")
|
|
||||||
bl_description = t("MMDOptions.cleanup_mesh.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
|
||||||
init_progress(context, 4)
|
|
||||||
|
|
||||||
update_progress(self, context, t("MMDOptions.removing_empty_objects"))
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
for obj in context.scene.objects:
|
|
||||||
if obj.type == 'EMPTY':
|
|
||||||
obj.select_set(True)
|
|
||||||
bpy.ops.object.delete()
|
|
||||||
|
|
||||||
update_progress(self, context, t("MMDOptions.removing_unused_vertex_groups"))
|
|
||||||
for obj in get_all_meshes(context):
|
|
||||||
self.remove_unused_vertex_groups(obj)
|
|
||||||
|
|
||||||
update_progress(self, context, t("MMDOptions.removing_unused_vertices"))
|
|
||||||
for obj in get_all_meshes(context):
|
|
||||||
self.remove_unused_vertices(obj)
|
|
||||||
|
|
||||||
update_progress(self, context, t("MMDOptions.removing_empty_shape_keys"))
|
|
||||||
for obj in get_all_meshes(context):
|
|
||||||
self.remove_empty_shape_keys(obj)
|
|
||||||
|
|
||||||
finish_progress(context)
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def remove_unused_vertex_groups(self, obj):
|
|
||||||
vgroups = obj.vertex_groups
|
|
||||||
for vgroup in vgroups:
|
|
||||||
if not any(vgroup.index in [g.group for g in v.groups] for v in obj.data.vertices):
|
|
||||||
vgroups.remove(vgroup)
|
|
||||||
|
|
||||||
def remove_unused_vertices(self, obj):
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
obj.select_set(True)
|
|
||||||
bpy.context.view_layer.objects.active = obj
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
bpy.ops.mesh.select_all(action='SELECT')
|
|
||||||
bpy.ops.mesh.delete_loose()
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
def remove_empty_shape_keys(self, obj):
|
|
||||||
if obj.data.shape_keys:
|
|
||||||
for key in obj.data.shape_keys.key_blocks:
|
|
||||||
if key.name != 'Basis' and all(abs(key.data[i].co[j] - obj.data.shape_keys.reference_key.data[i].co[j]) < 0.0001 for i in range(len(key.data)) for j in range(3)):
|
|
||||||
obj.shape_key_remove(key)
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_OptimizeWeights(Operator):
|
|
||||||
bl_idname = "avatar_toolkit.optimize_weights"
|
|
||||||
bl_label = t("MMDOptions.optimize_weights.label")
|
|
||||||
bl_description = t("MMDOptions.optimize_weights.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
max_weights: bpy.props.IntProperty(
|
|
||||||
name=t("MMDOptions.max_weights.label"),
|
|
||||||
description=t("MMDOptions.max_weights.desc"),
|
|
||||||
default=4,
|
|
||||||
min=1,
|
|
||||||
max=8
|
|
||||||
)
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
|
||||||
armature = get_selected_armature(context)
|
|
||||||
if not armature:
|
|
||||||
self.report({'ERROR'}, t("MMDOptions.no_armature_selected"))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
init_progress(context, 4)
|
|
||||||
|
|
||||||
update_progress(self, context, t("MMDOptions.merging_weights"))
|
|
||||||
for obj in get_all_meshes(context):
|
|
||||||
for modifier in obj.modifiers:
|
|
||||||
if modifier.type == 'ARMATURE' and modifier.object != armature:
|
|
||||||
bpy.ops.object.modifier_apply(modifier=modifier.name)
|
|
||||||
|
|
||||||
update_progress(self, context, t("MMDOptions.removing_zero_weight_bones"))
|
|
||||||
bpy.ops.avatar_toolkit.remove_zero_weight_bones('EXEC_DEFAULT')
|
|
||||||
|
|
||||||
update_progress(self, context, t("MMDOptions.limiting_vertex_weights"))
|
|
||||||
for obj in get_all_meshes(context):
|
|
||||||
self.limit_vertex_weights(obj)
|
|
||||||
|
|
||||||
update_progress(self, context, t("MMDOptions.weight_optimization_complete"))
|
|
||||||
finish_progress(context)
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def limit_vertex_weights(self, obj):
|
|
||||||
for v in obj.data.vertices:
|
|
||||||
if len(v.groups) > self.max_weights:
|
|
||||||
sorted_groups = sorted(v.groups, key=lambda g: g.weight, reverse=True)
|
|
||||||
for g in sorted_groups[self.max_weights:]:
|
|
||||||
obj.vertex_groups[g.group].remove([v.index])
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_OptimizeArmature(Operator):
|
|
||||||
bl_idname = "avatar_toolkit.optimize_armature"
|
|
||||||
bl_label = t("MMDOptions.optimize_armature.label")
|
|
||||||
bl_description = t("MMDOptions.optimize_armature.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
|
||||||
armature = get_selected_armature(context)
|
|
||||||
if not armature:
|
|
||||||
self.report({'ERROR'}, t("MMDOptions.no_armature_selected"))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
init_progress(context, 9)
|
|
||||||
|
|
||||||
# Ensure proper object selection and mode
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
armature.select_set(True)
|
|
||||||
context.view_layer.objects.active = armature
|
|
||||||
|
|
||||||
# Store initial transforms
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
initial_transforms = {}
|
|
||||||
for bone in armature.data.edit_bones:
|
|
||||||
initial_transforms[bone.name] = {
|
|
||||||
'head': bone.head.copy(),
|
|
||||||
'tail': bone.tail.copy(),
|
|
||||||
'roll': bone.roll,
|
|
||||||
'matrix': bone.matrix.copy(),
|
|
||||||
'parent': bone.parent.name if bone.parent else None
|
|
||||||
}
|
|
||||||
|
|
||||||
update_progress(self, context, t("MMDOptions.deleting_bone_constraints"))
|
|
||||||
bpy.ops.avatar_toolkit.delete_bone_constraints('EXEC_DEFAULT')
|
|
||||||
|
|
||||||
update_progress(self, context, t("MMDOptions.merging_bones_to_parents"))
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
armature.select_set(True)
|
|
||||||
context.view_layer.objects.active = armature
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
try:
|
|
||||||
bpy.ops.avatar_toolkit.merge_bones_to_parents('EXEC_DEFAULT')
|
|
||||||
except RuntimeError as e:
|
|
||||||
self.report({'WARNING'}, f"Failed to merge bones to parents: {str(e)}")
|
|
||||||
|
|
||||||
update_progress(self, context, t("MMDOptions.reordering_bones"))
|
|
||||||
self.reorder_bones(context, armature)
|
|
||||||
|
|
||||||
update_progress(self, context, t("MMDOptions.fixing_armature_names"))
|
|
||||||
self.fix_armature_names(armature)
|
|
||||||
|
|
||||||
update_progress(self, context, t("MMDOptions.renaming_bones"))
|
|
||||||
self.rename_bones(armature)
|
|
||||||
|
|
||||||
# Restore original bone transforms
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
for bone_name, transform in initial_transforms.items():
|
|
||||||
if bone_name in armature.data.edit_bones:
|
|
||||||
bone = armature.data.edit_bones[bone_name]
|
|
||||||
bone.head = transform['head']
|
|
||||||
bone.tail = transform['tail']
|
|
||||||
bone.roll = transform['roll']
|
|
||||||
bone.matrix = transform['matrix']
|
|
||||||
|
|
||||||
update_progress(self, context, t("MMDOptions.armature_optimization_complete"))
|
|
||||||
|
|
||||||
# Ensure we end in object mode with proper selection
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
armature.select_set(True)
|
|
||||||
context.view_layer.objects.active = armature
|
|
||||||
|
|
||||||
finish_progress(context)
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def reorder_bones(self, context: Context, armature: bpy.types.Object):
|
|
||||||
def sort_bones(bone):
|
|
||||||
children = sorted(bone.children, key=lambda b: b.name)
|
|
||||||
for child in children:
|
|
||||||
sort_bones(child)
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
root_bones = [bone for bone in armature.data.edit_bones if not bone.parent]
|
|
||||||
for root_bone in sorted(root_bones, key=lambda b: b.name):
|
|
||||||
sort_bones(root_bone)
|
|
||||||
|
|
||||||
def fix_armature_names(self, armature):
|
|
||||||
for bone in armature.data.bones:
|
|
||||||
fixed_name = self.get_fixed_bone_name(bone.name)
|
|
||||||
if fixed_name != bone.name:
|
|
||||||
bone.name = fixed_name
|
|
||||||
|
|
||||||
def get_fixed_bone_name(self, name):
|
|
||||||
name = name.replace(' ', '_')
|
|
||||||
name = re.sub(r'[^\w]', '', name)
|
|
||||||
return name
|
|
||||||
|
|
||||||
def rename_bones(self, armature):
|
|
||||||
for bone in armature.data.bones:
|
|
||||||
new_name = self.get_standardized_bone_name(bone.name)
|
|
||||||
if new_name != bone.name:
|
|
||||||
bone.name = new_name
|
|
||||||
|
|
||||||
def get_standardized_bone_name(self, name):
|
|
||||||
if 'left' in name.lower():
|
|
||||||
return f"Left_{name}"
|
|
||||||
elif 'right' in name.lower():
|
|
||||||
return f"Right_{name}"
|
|
||||||
return name
|
|
||||||
|
|
||||||
def bake_mmd_colors(node_base_tex: ShaderNodeTexImage, node_mmd_shader: ShaderNodeGroup):
|
|
||||||
ambient_color_input = node_mmd_shader.inputs.get("Ambient Color")
|
|
||||||
diffuse_color_input = node_mmd_shader.inputs.get("Diffuse Color")
|
|
||||||
|
|
||||||
if not ambient_color_input or not diffuse_color_input:
|
|
||||||
return node_base_tex, None
|
|
||||||
|
|
||||||
ambient_color = np.array(ambient_color_input.default_value[:3])
|
|
||||||
diffuse_color = np.array(diffuse_color_input.default_value[:3])
|
|
||||||
mmd_color = np.clip(ambient_color + diffuse_color * 0.6, 0, 1)
|
|
||||||
|
|
||||||
if not node_base_tex or not node_base_tex.image:
|
|
||||||
principled_base_color = np.append(mmd_color, 1)
|
|
||||||
return None, principled_base_color
|
|
||||||
|
|
||||||
base_tex_image = node_base_tex.image
|
|
||||||
if not base_tex_image.pixels:
|
|
||||||
return node_base_tex, None
|
|
||||||
|
|
||||||
if base_tex_image.colorspace_settings.name == 'sRGB':
|
|
||||||
is_small_mask = mmd_color < 0.0031308
|
|
||||||
mmd_color[is_small_mask] = np.where(mmd_color[is_small_mask] < 0.0, 0, mmd_color[is_small_mask] * 12.92)
|
|
||||||
is_large_mask = np.invert(is_small_mask)
|
|
||||||
mmd_color[is_large_mask] = (mmd_color[is_large_mask] ** (1.0 / 2.4)) * 1.055 - 0.055
|
|
||||||
|
|
||||||
pixels = np.array(base_tex_image.pixels).reshape((-1, 4))
|
|
||||||
pixels[:, :3] *= mmd_color
|
|
||||||
|
|
||||||
baked_image = bpy.data.images.new(base_tex_image.name + "MMDCatsBaked",
|
|
||||||
width=base_tex_image.size[0],
|
|
||||||
height=base_tex_image.size[1],
|
|
||||||
alpha=True)
|
|
||||||
baked_image.filepath = bpy.path.abspath("//" + base_tex_image.name + ".png")
|
|
||||||
baked_image.file_format = 'PNG'
|
|
||||||
baked_image.colorspace_settings.name = base_tex_image.colorspace_settings.name
|
|
||||||
|
|
||||||
baked_image.pixels = pixels.flatten()
|
|
||||||
node_base_tex.image = baked_image
|
|
||||||
|
|
||||||
if bpy.data.is_saved:
|
|
||||||
baked_image.save()
|
|
||||||
|
|
||||||
return node_base_tex, None
|
|
||||||
|
|
||||||
def add_principled_shader(material: Material, bake_mmd=True):
|
|
||||||
node_tree = material.node_tree
|
|
||||||
nodes = node_tree.nodes
|
|
||||||
links = node_tree.links
|
|
||||||
|
|
||||||
principled_shader = nodes.new(type="ShaderNodeBsdfPrincipled")
|
|
||||||
principled_shader.label = "Cats Export Shader"
|
|
||||||
principled_shader.location = (501, -500)
|
|
||||||
|
|
||||||
output_shader = nodes.new(type="ShaderNodeOutputMaterial")
|
|
||||||
output_shader.label = "Cats Export"
|
|
||||||
output_shader.location = (801, -500)
|
|
||||||
|
|
||||||
links.new(principled_shader.outputs["BSDF"], output_shader.inputs["Surface"])
|
|
||||||
|
|
||||||
node_base_tex = nodes.get("mmd_base_tex") or next((n for n in nodes if n.type == 'TEX_IMAGE'), None)
|
|
||||||
node_mmd_shader = nodes.get("mmd_shader")
|
|
||||||
|
|
||||||
if node_mmd_shader and bake_mmd:
|
|
||||||
node_base_tex, principled_base_color = bake_mmd_colors(node_base_tex, node_mmd_shader)
|
|
||||||
else:
|
|
||||||
principled_base_color = None
|
|
||||||
|
|
||||||
if node_base_tex and node_base_tex.image:
|
|
||||||
links.new(node_base_tex.outputs["Color"], principled_shader.inputs["Base Color"])
|
|
||||||
links.new(node_base_tex.outputs["Alpha"], principled_shader.inputs["Alpha"])
|
|
||||||
elif principled_base_color is not None:
|
|
||||||
principled_shader.inputs["Base Color"].default_value = principled_base_color
|
|
||||||
|
|
||||||
principled_shader.inputs["Specular IOR Level"].default_value = 0
|
|
||||||
principled_shader.inputs["Roughness"].default_value = 0.9
|
|
||||||
principled_shader.inputs["Sheen Tint"].default_value = (1.0, 1.0, 1.0, 1.0)
|
|
||||||
principled_shader.inputs["Coat Roughness"].default_value = 0
|
|
||||||
principled_shader.inputs["IOR"].default_value = 1.45
|
|
||||||
|
|
||||||
# Handle transparency
|
|
||||||
if material.blend_method != 'OPAQUE':
|
|
||||||
principled_shader.inputs["Alpha"].default_value = material.alpha_threshold
|
|
||||||
material.blend_method = 'CLIP'
|
|
||||||
|
|
||||||
def fix_mmd_shader(material: Material):
|
|
||||||
mmd_shader_node = material.node_tree.nodes.get("mmd_shader")
|
|
||||||
if mmd_shader_node:
|
|
||||||
reflect_input = mmd_shader_node.inputs.get("Reflect")
|
|
||||||
if reflect_input:
|
|
||||||
reflect_input.default_value = 1
|
|
||||||
|
|
||||||
def fix_vrm_shader(material: Material):
|
|
||||||
nodes = material.node_tree.nodes
|
|
||||||
is_vrm_mat = False
|
|
||||||
for node in nodes:
|
|
||||||
if hasattr(node, 'node_tree') and 'MToon_unversioned' in node.node_tree.name:
|
|
||||||
node.location[0] = 200
|
|
||||||
node.inputs['ReceiveShadow_Texture_alpha'].default_value = -10000
|
|
||||||
node.inputs['ShadeTexture'].default_value = (1.0, 1.0, 1.0, 1.0)
|
|
||||||
node.inputs['Emission_Texture'].default_value = (0.0, 0.0, 0.0, 0.0)
|
|
||||||
node.inputs['SphereAddTexture'].default_value = (0.0, 0.0, 0.0, 0.0)
|
|
||||||
node_input = node.inputs.get('NomalmapTexture') or node.inputs.get('NormalmapTexture')
|
|
||||||
node_input.default_value = (1.0, 1.0, 1.0, 1.0)
|
|
||||||
is_vrm_mat = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if is_vrm_mat:
|
|
||||||
nodes_to_keep = ['DiffuseColor', 'MainTexture', 'Emission_Texture']
|
|
||||||
if 'HAIR' in material.name:
|
|
||||||
nodes_to_keep.append('SphereAddTexture')
|
|
||||||
|
|
||||||
for node in nodes:
|
|
||||||
if ('RGB' in node.name or 'Value' in node.name or 'Image Texture' in node.name or
|
|
||||||
'UV Map' in node.name or 'Mapping' in node.name):
|
|
||||||
if node.label not in nodes_to_keep:
|
|
||||||
material.node_tree.links = [link for link in material.node_tree.links
|
|
||||||
if not (link.from_node == node or link.to_node == node)]
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_ConvertMaterials(Operator):
|
|
||||||
bl_idname = "avatar_toolkit.convert_materials"
|
|
||||||
bl_label = t("MMDOptions.convert_materials.label")
|
|
||||||
bl_description = t("MMDOptions.convert_materials.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
|
||||||
meshes = get_all_meshes(context)
|
|
||||||
init_progress(context, len(meshes))
|
|
||||||
|
|
||||||
for obj in meshes:
|
|
||||||
update_progress(self, context, t("MMDOptions.converting_materials").format(name=obj.name))
|
|
||||||
self.convert_materials_for_mesh(obj)
|
|
||||||
|
|
||||||
finish_progress(context)
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def convert_materials_for_mesh(self, mesh: Object):
|
|
||||||
for mat_slot in mesh.material_slots:
|
|
||||||
if mat_slot.material:
|
|
||||||
mat = mat_slot.material
|
|
||||||
mat.use_nodes = True
|
|
||||||
|
|
||||||
# Add Principled BSDF shader
|
|
||||||
add_principled_shader(mat)
|
|
||||||
|
|
||||||
# Fix MMD shader if present
|
|
||||||
fix_mmd_shader(mat)
|
|
||||||
|
|
||||||
# Fix VRM shader if present
|
|
||||||
fix_vrm_shader(mat)
|
|
||||||
|
|
||||||
# Clean up unused nodes
|
|
||||||
self.clean_unused_nodes(mat)
|
|
||||||
|
|
||||||
def clean_unused_nodes(self, material: Material):
|
|
||||||
nodes = material.node_tree.nodes
|
|
||||||
links = material.node_tree.links
|
|
||||||
|
|
||||||
used_nodes = set()
|
|
||||||
output_node = next((n for n in nodes if n.type == 'OUTPUT_MATERIAL'), None)
|
|
||||||
|
|
||||||
if output_node:
|
|
||||||
self.traverse_node_tree(output_node, used_nodes)
|
|
||||||
|
|
||||||
for node in nodes:
|
|
||||||
if node not in used_nodes:
|
|
||||||
nodes.remove(node)
|
|
||||||
|
|
||||||
def traverse_node_tree(self, node, used_nodes):
|
|
||||||
used_nodes.add(node)
|
|
||||||
for input in node.inputs:
|
|
||||||
for link in input.links:
|
|
||||||
if link.from_node not in used_nodes:
|
|
||||||
self.traverse_node_tree(link.from_node, used_nodes)
|
|
||||||
|
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import bpy
|
||||||
|
import numpy as np
|
||||||
|
from bpy.types import Operator, Context, Object
|
||||||
|
from typing import List
|
||||||
|
from ..core.translations import t
|
||||||
|
from ..core.common import (
|
||||||
|
get_active_armature,
|
||||||
|
get_all_meshes,
|
||||||
|
apply_pose_as_rest,
|
||||||
|
apply_armature_to_mesh,
|
||||||
|
apply_armature_to_mesh_with_shapekeys,
|
||||||
|
validate_armature
|
||||||
|
)
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_StartPoseMode(Operator):
|
||||||
|
bl_idname = 'avatar_toolkit.start_pose_mode'
|
||||||
|
bl_label = t("Quick_Access.start_pose_mode.label")
|
||||||
|
bl_description = t("Quick_Access.start_pose_mode.desc")
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
if not armature or context.mode == "POSE":
|
||||||
|
return False
|
||||||
|
is_valid, _ = validate_armature(armature)
|
||||||
|
return is_valid
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> set[str]:
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
context.view_layer.objects.active = armature
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
armature.select_set(True)
|
||||||
|
bpy.ops.object.mode_set(mode='POSE')
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_StopPoseMode(Operator):
|
||||||
|
bl_idname = 'avatar_toolkit.stop_pose_mode'
|
||||||
|
bl_label = t("Quick_Access.stop_pose_mode.label")
|
||||||
|
bl_description = t("Quick_Access.stop_pose_mode.desc")
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return get_active_armature(context) and context.mode == "POSE"
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> set[str]:
|
||||||
|
bpy.ops.pose.transforms_clear()
|
||||||
|
bpy.ops.pose.select_all(action="INVERT")
|
||||||
|
bpy.ops.pose.transforms_clear()
|
||||||
|
bpy.ops.pose.select_all(action="INVERT")
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_ApplyPoseAsShapekey(Operator):
|
||||||
|
bl_idname = 'avatar_toolkit.apply_pose_as_shapekey'
|
||||||
|
bl_label = t("Quick_Access.apply_pose_as_shapekey.label")
|
||||||
|
bl_description = t("Quick_Access.apply_pose_as_shapekey.desc")
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
if not armature or context.mode != 'POSE':
|
||||||
|
return False
|
||||||
|
is_valid, _ = validate_armature(armature)
|
||||||
|
return is_valid
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
armature_obj = get_active_armature(context)
|
||||||
|
mesh_objects = get_all_meshes(context)
|
||||||
|
|
||||||
|
for mesh_obj in mesh_objects:
|
||||||
|
if not mesh_obj.data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not mesh_obj.data.shape_keys:
|
||||||
|
mesh_obj.shape_key_add(name='Basis')
|
||||||
|
|
||||||
|
new_shape = mesh_obj.shape_key_add(name='Pose_Shapekey', from_mix=False)
|
||||||
|
|
||||||
|
depsgraph = context.evaluated_depsgraph_get()
|
||||||
|
eval_mesh = mesh_obj.evaluated_get(depsgraph)
|
||||||
|
|
||||||
|
for i, v in enumerate(eval_mesh.data.vertices):
|
||||||
|
new_shape.data[i].co = v.co.copy()
|
||||||
|
|
||||||
|
bpy.ops.pose.select_all(action='SELECT')
|
||||||
|
bpy.ops.pose.transforms_clear()
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
|
self.report({'INFO'}, t('Tools.apply_pose_as_rest.success'))
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_ApplyPoseAsRest(Operator):
|
||||||
|
bl_idname = 'avatar_toolkit.apply_pose_as_rest'
|
||||||
|
bl_label = t("Quick_Access.apply_pose_as_rest.label")
|
||||||
|
bl_description = t("Quick_Access.apply_pose_as_rest.desc")
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
if not armature or context.mode != "POSE":
|
||||||
|
return False
|
||||||
|
is_valid, _ = validate_armature(armature)
|
||||||
|
return is_valid
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
if not apply_pose_as_rest(
|
||||||
|
context=context,
|
||||||
|
armature_obj=get_active_armature(context),
|
||||||
|
meshes=get_all_meshes(context)
|
||||||
|
):
|
||||||
|
self.report({'ERROR'}, t("Quick_Access.apply_armature_failed"))
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
self.report({'INFO'}, t("Tools.apply_pose_as_rest.success"))
|
||||||
|
return {'FINISHED'}
|
||||||
@@ -1,308 +0,0 @@
|
|||||||
import bpy
|
|
||||||
from typing import List, TypedDict, Any
|
|
||||||
from bpy.types import Operator, Context, Object
|
|
||||||
from ..core.common import get_selected_armature, is_valid_armature, select_current_armature, get_all_meshes
|
|
||||||
from ..core.translations import t
|
|
||||||
|
|
||||||
class meshEntry(TypedDict):
|
|
||||||
mesh: Object
|
|
||||||
shapekeys: list[str]
|
|
||||||
vertices: int
|
|
||||||
cur_vertex_pass: int
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_RemoveDoublesSafelyAdvanced(Operator):
|
|
||||||
bl_idname = "avatar_toolkit.remove_doubles_safely_advanced"
|
|
||||||
bl_label = t("Optimization.remove_doubles_safely_advanced.label")
|
|
||||||
bl_description = t("Optimization.remove_doubles_safely_advanced.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
merge_distance: bpy.props.FloatProperty(default=0.0001)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context: Context) -> bool:
|
|
||||||
armature = get_selected_armature(context)
|
|
||||||
return armature is not None and is_valid_armature(armature)
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
layout.label(text="This process may take a long time.")
|
|
||||||
layout.label(text="Blender may seem unresponsive during this operation.")
|
|
||||||
layout.label(text="Please be patient and wait for it to complete.")
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
return context.window_manager.invoke_props_dialog(self)
|
|
||||||
|
|
||||||
def execute(self, context: Context):
|
|
||||||
bpy.ops.avatar_toolkit.remove_doubles_safely('INVOKE_DEFAULT', advanced=True, merge_distance=self.merge_distance)
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_RemoveDoublesSafely(Operator):
|
|
||||||
bl_idname = "avatar_toolkit.remove_doubles_safely"
|
|
||||||
bl_label = t("Optimization.remove_doubles_safely.label")
|
|
||||||
bl_description = t("Optimization.remove_doubles_safely.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
objects_to_do: list[meshEntry] = []
|
|
||||||
merge_distance: bpy.props.FloatProperty(default=0.0001)
|
|
||||||
advanced: bpy.props.BoolProperty(default=False)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context: Context) -> bool:
|
|
||||||
armature = get_selected_armature(context)
|
|
||||||
return armature is not None and is_valid_armature(armature)
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> set:
|
|
||||||
if not select_current_armature(context):
|
|
||||||
self.report({'WARNING'}, t("Optimization.no_armature_selected"))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
armature = get_selected_armature(context)
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
objects: List[Object] = get_all_meshes(context)
|
|
||||||
self.objects_to_do = []
|
|
||||||
|
|
||||||
for mesh in objects:
|
|
||||||
if mesh.data.name not in [stored_object["mesh"].data.name for stored_object in self.objects_to_do]:
|
|
||||||
print("setting up data for object" + mesh.name)
|
|
||||||
mesh_shapekeys = {"mesh":mesh,"shapekeys":[],"vertices":0,"cur_vertex_pass":0}
|
|
||||||
mesh_data: bpy.types.Mesh = mesh.data
|
|
||||||
shape: bpy.types.ShapeKey = None
|
|
||||||
mesh_shapekeys["vertices"] = len(mesh_data.vertices)
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
mesh_data.vertices.foreach_set("select",[False]*len(mesh_data.vertices))
|
|
||||||
|
|
||||||
if mesh_data.shape_keys:
|
|
||||||
for shape in mesh_data.shape_keys.key_blocks:
|
|
||||||
mesh_shapekeys["shapekeys"].append(shape.name)
|
|
||||||
if self.advanced:
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
context.view_layer.objects.active = mesh
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
print("queued data for "+mesh.name+" is: ")
|
|
||||||
print(mesh_shapekeys)
|
|
||||||
self.objects_to_do.append(mesh_shapekeys)
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def invoke(self, context: Context, event: bpy.types.Event) -> set:
|
|
||||||
print("==================")
|
|
||||||
print("==================")
|
|
||||||
print("==================")
|
|
||||||
print("==================")
|
|
||||||
print("starting modal execution of merge doubles safely.")
|
|
||||||
print("==================")
|
|
||||||
print("==================")
|
|
||||||
print("==================")
|
|
||||||
print("==================")
|
|
||||||
self.execute(context)
|
|
||||||
context.window_manager.modal_handler_add(self)
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
|
|
||||||
def modify_mesh(self, context: Context, mesh: meshEntry):
|
|
||||||
mesh["mesh"].select_set(True)
|
|
||||||
context.view_layer.objects.active = mesh["mesh"]
|
|
||||||
mesh_data: bpy.types.Mesh = mesh["mesh"].data
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
for index, point in enumerate(mesh["mesh"].active_shape_key.points):
|
|
||||||
if point.co.xyz != mesh_data.shape_keys.key_blocks[0].points[index].co.xyz:
|
|
||||||
mesh_data.vertices[index].select = True
|
|
||||||
print("shapekey has a moved vertex at index \""+str(index)+"\", excluding from simple double merging!")
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
mesh["mesh"].select_set(False)
|
|
||||||
print("finished shapekey basic.")
|
|
||||||
|
|
||||||
def modify_mesh_advanced(self, context: Context, mesh_entry: meshEntry):
|
|
||||||
|
|
||||||
final_merged_vertex_group: list[int] = []
|
|
||||||
initialized_final: bool = False
|
|
||||||
|
|
||||||
for shapekey_name in mesh_entry["shapekeys"]:
|
|
||||||
mesh = mesh_entry["mesh"]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#make a copy to do double merge testing on for the current vertex
|
|
||||||
context.view_layer.objects.active = mesh
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
context.view_layer.objects.active = mesh
|
|
||||||
mesh_data: bpy.types.Mesh = mesh.data
|
|
||||||
vertices_original: dict[int,Any] = {}
|
|
||||||
original_count: int = len(mesh_data.vertices)
|
|
||||||
mesh.select_set(True)
|
|
||||||
mesh.active_shape_key_index = mesh_data.shape_keys.key_blocks.find(shapekey_name)
|
|
||||||
bpy.ops.object.duplicate()
|
|
||||||
bpy.ops.object.shape_key_move(type='TOP')
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
bpy.ops.object.shape_key_remove(all=True, apply_mix=False)
|
|
||||||
|
|
||||||
mesh = context.view_layer.objects.active
|
|
||||||
mesh.name = shapekey_name+"_object_is_"+mesh.name
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
|
|
||||||
mesh.select_set(True)
|
|
||||||
context.view_layer.objects.active = mesh
|
|
||||||
mesh_data: bpy.types.Mesh = mesh.data
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
for index, merged_point in enumerate(mesh_data.vertices):
|
|
||||||
vertices_original[index] = merged_point.co.xyz
|
|
||||||
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
mesh_data.vertices.foreach_set("select",[False]*len(mesh_data.vertices))
|
|
||||||
|
|
||||||
select_target_vertex = [False]*len(mesh_data.vertices)
|
|
||||||
try:
|
|
||||||
select_target_vertex[mesh_entry["cur_vertex_pass"]] = True
|
|
||||||
except:
|
|
||||||
bpy.ops.object.delete() #remove our double merge testing object for this shapekey, since we merged doubles on it, it will be useless.
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
mesh_data.vertices.foreach_set("select",select_target_vertex)
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
for i in range(0,20): #for some reason, if using merge to unselected on a vertex, the vertex will only merge to 1 other vertex. so we gotta spam it to fix it.
|
|
||||||
bpy.ops.mesh.remove_doubles(threshold=self.merge_distance, use_unselected=True, use_sharp_edge_from_normals=False)
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
merged_vertices: list[int] = []
|
|
||||||
mesh_data_vertices: dict[int,Any] = {}
|
|
||||||
for idx,vertex in enumerate(mesh_data.vertices):
|
|
||||||
mesh_data_vertices[idx] = vertex.co.xyz
|
|
||||||
|
|
||||||
#I'm loosing my mind with indices because I cannot keep so many numbers in my head. I will have to use 2 pointers
|
|
||||||
# yes this can be simplified more, but the mountains of errors with using a normal for statement are making me
|
|
||||||
# loose my mind. This is hard. - @989onan
|
|
||||||
#Below is the magic that determines whether or not vertices were merged and then puts the vertices
|
|
||||||
#that were merged into a list. - @989onan
|
|
||||||
|
|
||||||
i = 0
|
|
||||||
j = 0
|
|
||||||
while(i<len(vertices_original)):
|
|
||||||
if j+1 > len(mesh_data.vertices):
|
|
||||||
merged_vertices.append(i)
|
|
||||||
j = j-1
|
|
||||||
elif mesh_data.vertices[j].co.xyz != vertices_original[i]:
|
|
||||||
merged_vertices.append(i)
|
|
||||||
j = j-1
|
|
||||||
elif vertices_original[i] == vertices_original[mesh_entry["cur_vertex_pass"]]:
|
|
||||||
merged_vertices.append(i)
|
|
||||||
|
|
||||||
i = i+1
|
|
||||||
j = j+1
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#give our final set of points some inital data. we're looking for points that are merged on every shape key (and therefore appear in every version of merged_vertices).
|
|
||||||
# If we initialize the array with points from the first version of merged_vertices, then we can remove the vertices from final that don't get merged from
|
|
||||||
#every future version of merged_vertices with the "if merged_point not in merged_vertices:" code.
|
|
||||||
if initialized_final == False:
|
|
||||||
for point in merged_vertices:
|
|
||||||
final_merged_vertex_group.append(point)
|
|
||||||
initialized_final = True
|
|
||||||
#iterate through a copy of final vertex groups to prevent crash. If a vertex was merged before, but didn't merge in this vertex,
|
|
||||||
# then the vertex shouldn't be merged because it moves away from the vertex we are double merging now (ex: bottom of mouth moving away from top when opening on a shapekey) - @989onan
|
|
||||||
for merged_point in final_merged_vertex_group[:]:
|
|
||||||
if merged_point not in merged_vertices:
|
|
||||||
final_merged_vertex_group.remove(merged_point)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
mesh_data.vertices.foreach_set("select",[False]*len(mesh_data.vertices))
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
bpy.ops.object.delete() #remove our double merge testing object for this shapekey, since we merged doubles on it, it will be useless.
|
|
||||||
context.view_layer.objects.active = mesh_entry["mesh"]
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
context.view_layer.objects.active = mesh_entry["mesh"]
|
|
||||||
mesh_entry["mesh"].select_set(True)
|
|
||||||
|
|
||||||
original_mesh_data: bpy.types.Mesh = mesh_entry["mesh"].data
|
|
||||||
select_target_group = [False]*len(original_mesh_data.vertices)
|
|
||||||
|
|
||||||
|
|
||||||
for vertex_index in final_merged_vertex_group:
|
|
||||||
select_target_group[vertex_index] = True
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
original_mesh_data.vertices.foreach_set("select",select_target_group)
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
bpy.ops.mesh.remove_doubles(threshold=self.merge_distance, use_unselected=False, use_sharp_edge_from_normals=False)
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
original_mesh_data.vertices.foreach_set("select",[False]*len(original_mesh_data.vertices))
|
|
||||||
print("finished advanced merge doubles for single vertex at index: "+str(mesh_entry["cur_vertex_pass"]))
|
|
||||||
return not (len(final_merged_vertex_group) > 1)
|
|
||||||
|
|
||||||
def modal(self, context: Context, event: bpy.types.Event) -> set:
|
|
||||||
if len(self.objects_to_do) > 0:
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
mesh: meshEntry = self.objects_to_do[0]
|
|
||||||
mesh_data: bpy.types.Mesh = mesh["mesh"].data
|
|
||||||
if (len(mesh['shapekeys']) > 0) and (not self.advanced):
|
|
||||||
shapekeyname: str = mesh['shapekeys'].pop(0)
|
|
||||||
|
|
||||||
target_shapekey: int = mesh_data.shape_keys.key_blocks.find(shapekeyname)
|
|
||||||
mesh["mesh"].active_shape_key_index = target_shapekey
|
|
||||||
print("doing shapekey \""+shapekeyname+"\" on mesh \""+mesh['mesh'].name+"\".")
|
|
||||||
self.modify_mesh(context, mesh)
|
|
||||||
elif not (mesh_data.shape_keys):
|
|
||||||
print("doing mesh with no shapekeys named \""+mesh['mesh'].name+"\".")
|
|
||||||
mesh["mesh"].select_set(True)
|
|
||||||
context.view_layer.objects.active = mesh["mesh"]
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
mesh_data.vertices.foreach_set("select",[False]*len(mesh_data.vertices))
|
|
||||||
|
|
||||||
bpy.ops.mesh.select_all(action="INVERT")
|
|
||||||
bpy.ops.mesh.remove_doubles(threshold=self.merge_distance,use_unselected=False)
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
mesh["mesh"].select_set(False)
|
|
||||||
self.objects_to_do.pop(0)
|
|
||||||
elif (not (mesh["cur_vertex_pass"] > mesh["vertices"])) and self.advanced:
|
|
||||||
|
|
||||||
print("doing a merge by single vertex index at index "+str(mesh["cur_vertex_pass"]))
|
|
||||||
|
|
||||||
if self.modify_mesh_advanced(context, mesh):
|
|
||||||
mesh["cur_vertex_pass"] = mesh["cur_vertex_pass"]+1
|
|
||||||
else:
|
|
||||||
print("finishing double merge object.")
|
|
||||||
if not self.advanced:
|
|
||||||
mesh["mesh"].select_set(True)
|
|
||||||
context.view_layer.objects.active = mesh["mesh"]
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
|
|
||||||
bpy.ops.mesh.select_all(action="INVERT")
|
|
||||||
bpy.ops.mesh.remove_doubles(threshold=self.merge_distance,use_unselected=False)
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
mesh["mesh"].select_set(False)
|
|
||||||
|
|
||||||
self.objects_to_do.pop(0)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.report({'INFO'}, t("Optimization.remove_doubles_completed"))
|
|
||||||
print("finishing modal execution of merge doubles safely.")
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
import bpy
|
|
||||||
from typing import List, Optional
|
|
||||||
import re
|
|
||||||
from bpy.types import Operator, Context, Object
|
|
||||||
from ..core.dictionaries import bone_names
|
|
||||||
from ..core.common import get_selected_armature, simplify_bonename, is_valid_armature
|
|
||||||
from ..core.translations import t
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_ConvertToResonite(Operator):
|
|
||||||
bl_idname = 'avatar_toolkit.convert_to_resonite'
|
|
||||||
bl_label = t('Tools.convert_to_resonite.label')
|
|
||||||
bl_description = t('Tools.convert_to_resonite.desc')
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context: Context) -> bool:
|
|
||||||
armature = get_selected_armature(context)
|
|
||||||
return armature is not None and is_valid_armature(armature)
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> set:
|
|
||||||
armature = get_selected_armature(context)
|
|
||||||
if not armature:
|
|
||||||
self.report({'WARNING'}, t("Tools.no_armature_selected"))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
translate_bone_fails = 0
|
|
||||||
untranslated_bones = set()
|
|
||||||
|
|
||||||
reverse_bone_lookup = dict()
|
|
||||||
for (preferred_name, name_list) in bone_names.items():
|
|
||||||
for name in name_list:
|
|
||||||
reverse_bone_lookup[name] = preferred_name
|
|
||||||
|
|
||||||
resonite_translations = {
|
|
||||||
'hips': "Hips",
|
|
||||||
'spine': "Spine",
|
|
||||||
'chest': "Chest",
|
|
||||||
'neck': "Neck",
|
|
||||||
'head': "Head",
|
|
||||||
'left_eye': "Eye.L",
|
|
||||||
'right_eye': "Eye.R",
|
|
||||||
'right_leg': "UpperLeg.R",
|
|
||||||
'right_knee': "Calf.R",
|
|
||||||
'right_ankle': "Foot.R",
|
|
||||||
'right_toe': 'Toes.R',
|
|
||||||
'right_shoulder': "Shoulder.R",
|
|
||||||
'right_arm': "UpperArm.R",
|
|
||||||
'right_elbow': "ForeArm.R",
|
|
||||||
'right_wrist': "Hand.R",
|
|
||||||
'left_leg': "UpperLeg.L",
|
|
||||||
'left_knee': "Calf.L",
|
|
||||||
'left_ankle': "Foot.L",
|
|
||||||
'left_toe': "Toes.L",
|
|
||||||
'left_shoulder': "Shoulder.L",
|
|
||||||
'left_arm': "UpperArm.L",
|
|
||||||
'left_elbow': "ForeArm.L",
|
|
||||||
'left_wrist': "Hand.R",
|
|
||||||
|
|
||||||
'pinkie_1_l': "pinkie1.L",
|
|
||||||
'pinkie_2_l': "pinkie2.L",
|
|
||||||
'pinkie_3_l': "pinkie3.L",
|
|
||||||
'ring_1_l': "ring1.L",
|
|
||||||
'ring_2_l': "ring2.L",
|
|
||||||
'ring_3_l': "ring3.L",
|
|
||||||
'middle_1_l': "middle1.L",
|
|
||||||
'middle_2_l': "middle2.L",
|
|
||||||
'middle_3_l': "middle3.L",
|
|
||||||
'index_1_l': "index1.L",
|
|
||||||
'index_2_l': "index2.L",
|
|
||||||
'index_3_l': "index3.L",
|
|
||||||
'thumb_1_l': "thumb1.L",
|
|
||||||
'thumb_2_l': "thumb2.L",
|
|
||||||
'thumb_3_l': "thumb3.L",
|
|
||||||
|
|
||||||
'pinkie_1_r': "pinkie1.R",
|
|
||||||
'pinkie_2_r': "pinkie2.R",
|
|
||||||
'pinkie_3_r': "pinkie3.R",
|
|
||||||
'ring_1_r': "ring1.R",
|
|
||||||
'ring_2_r': "ring2.R",
|
|
||||||
'ring_3_r': "ring3.R",
|
|
||||||
'middle_1_r': "middle1.R",
|
|
||||||
'middle_2_r': "middle2.R",
|
|
||||||
'middle_3_r': "middle3.R",
|
|
||||||
'index_1_r': "index1.R",
|
|
||||||
'index_2_r': "index2.R",
|
|
||||||
'index_3_r': "index3.R",
|
|
||||||
'thumb_1_r': "thumb1.R",
|
|
||||||
'thumb_2_r': "thumb2.R",
|
|
||||||
'thumb_3_r': "thumb3.R"
|
|
||||||
}
|
|
||||||
|
|
||||||
context.view_layer.objects.active = armature
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
bone.name = re.compile(re.escape("<noik>"), re.IGNORECASE).sub("",bone.name) #remove "NOIK" from bones before translating again, in case an update was done that fixes a translation.
|
|
||||||
for bone in armature.data.bones:
|
|
||||||
if simplify_bonename(bone.name) in reverse_bone_lookup and reverse_bone_lookup[simplify_bonename(bone.name)] in resonite_translations:
|
|
||||||
bone.name = resonite_translations[reverse_bone_lookup[simplify_bonename(bone.name)]]
|
|
||||||
else:
|
|
||||||
untranslated_bones.add(bone.name)
|
|
||||||
|
|
||||||
bone.name = bone.name+"<noik>"
|
|
||||||
translate_bone_fails += 1
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
if translate_bone_fails > 0:
|
|
||||||
self.report({'INFO'}, t("Tools.bones_translated_with_fails").format(translate_bone_fails=translate_bone_fails))
|
|
||||||
else:
|
|
||||||
self.report({'INFO'}, t("Tools.bones_translated_success"))
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
# This code is heavily based on the Rigify-Move-DEF by NyankoNyan (https://github.com/NyankoNyan/Rigify-Move-DEF), which is licensed under the MIT License. We just heavily improve the code and add some new features.
|
|
||||||
import bpy
|
|
||||||
from ..core.common import get_selected_armature, is_valid_armature
|
|
||||||
from ..core.translations import t
|
|
||||||
from bpy.types import Operator, Context
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_ConvertRigifyToUnity(Operator):
|
|
||||||
bl_idname = "avatar_toolkit.convert_rigify_to_unity"
|
|
||||||
bl_label = t("Tools.convert_rigify_to_unity.label")
|
|
||||||
bl_description = t("Tools.convert_rigify_to_unity.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context: Context) -> bool:
|
|
||||||
armature = get_selected_armature(context)
|
|
||||||
return armature is not None and is_valid_armature(armature) and "DEF-spine" in armature.data.bones
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
|
||||||
armature = get_selected_armature(context)
|
|
||||||
if not armature:
|
|
||||||
self.report({'ERROR'}, t("Tools.no_armature_selected"))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
self.move_def_bones(armature)
|
|
||||||
self.rename_bones_for_unity(armature)
|
|
||||||
if context.scene.merge_twist_bones:
|
|
||||||
self.handle_twist_bones(armature)
|
|
||||||
self.report({'INFO'}, t("Tools.convert_rigify_to_unity.success"))
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def move_def_bones(self, armature):
|
|
||||||
remap = self.get_org_remap(armature)
|
|
||||||
remap.update(self.get_special_remap())
|
|
||||||
|
|
||||||
remove_bones_in_chain = [
|
|
||||||
'DEF-upper_arm.L.001', 'DEF-forearm.L.001',
|
|
||||||
'DEF-upper_arm.R.001', 'DEF-forearm.R.001',
|
|
||||||
'DEF-thigh.L.001', 'DEF-shin.L.001',
|
|
||||||
'DEF-thigh.R.001', 'DEF-shin.R.001'
|
|
||||||
]
|
|
||||||
|
|
||||||
transform_copies = self.get_transform_copies(armature)
|
|
||||||
|
|
||||||
# Add missing constraints
|
|
||||||
bpy.ops.object.mode_set(mode='POSE')
|
|
||||||
for bone_name in transform_copies:
|
|
||||||
bone = armature.pose.bones[bone_name]
|
|
||||||
org_name = 'ORG-' + self.get_proto_name(bone_name)
|
|
||||||
if org_name in armature.pose.bones:
|
|
||||||
constraint = bone.constraints.new('COPY_TRANSFORMS')
|
|
||||||
constraint.target = armature
|
|
||||||
constraint.subtarget = org_name
|
|
||||||
constr_count = len(bone.constraints)
|
|
||||||
if constr_count > 1:
|
|
||||||
bone.constraints.move(constr_count-1, 0)
|
|
||||||
|
|
||||||
# Apply new parents
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
for remap_key in remap:
|
|
||||||
if remap_key in armature.data.edit_bones and remap[remap_key] in armature.data.edit_bones:
|
|
||||||
armature.data.edit_bones[remap_key].parent = armature.data.edit_bones[remap[remap_key]]
|
|
||||||
|
|
||||||
# Remove extra bones in chains
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
for bone_name in remove_bones_in_chain:
|
|
||||||
if bone_name in armature.data.bones:
|
|
||||||
armature.data.bones[bone_name].use_deform = False
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
for bone_name in remove_bones_in_chain:
|
|
||||||
if bone_name in armature.data.bones:
|
|
||||||
remove_bone = armature.data.edit_bones[bone_name]
|
|
||||||
parent_bone = remove_bone.parent
|
|
||||||
parent_bone.tail = remove_bone.tail
|
|
||||||
retarget_bones = list(remove_bone.children)
|
|
||||||
for bone in retarget_bones:
|
|
||||||
bone.parent = parent_bone
|
|
||||||
armature.data.edit_bones.remove(remove_bone)
|
|
||||||
|
|
||||||
def rename_bones_for_unity(self, armature):
|
|
||||||
unity_bone_names = {
|
|
||||||
"DEF-spine": "Hips",
|
|
||||||
"DEF-spine.001": "Spine",
|
|
||||||
"DEF-spine.002": "Chest",
|
|
||||||
"DEF-spine.003": "UpperChest",
|
|
||||||
"DEF-neck": "Neck",
|
|
||||||
"DEF-head": "Head",
|
|
||||||
"DEF-shoulder.L": "LeftShoulder",
|
|
||||||
"DEF-upper_arm.L": "LeftUpperArm",
|
|
||||||
"DEF-forearm.L": "LeftLowerArm",
|
|
||||||
"DEF-hand.L": "LeftHand",
|
|
||||||
"DEF-shoulder.R": "RightShoulder",
|
|
||||||
"DEF-upper_arm.R": "RightUpperArm",
|
|
||||||
"DEF-forearm.R": "RightLowerArm",
|
|
||||||
"DEF-hand.R": "RightHand",
|
|
||||||
"DEF-thigh.L": "LeftUpperLeg",
|
|
||||||
"DEF-shin.L": "LeftLowerLeg",
|
|
||||||
"DEF-foot.L": "LeftFoot",
|
|
||||||
"DEF-toe.L": "LeftToes",
|
|
||||||
"DEF-thigh.R": "RightUpperLeg",
|
|
||||||
"DEF-shin.R": "RightLowerLeg",
|
|
||||||
"DEF-foot.R": "RightFoot",
|
|
||||||
"DEF-toe.R": "RightToes"
|
|
||||||
}
|
|
||||||
|
|
||||||
for old_name, new_name in unity_bone_names.items():
|
|
||||||
bone = armature.pose.bones.get(old_name)
|
|
||||||
if bone:
|
|
||||||
bone.name = new_name
|
|
||||||
|
|
||||||
def handle_twist_bones(self, armature):
|
|
||||||
twist_bones = [
|
|
||||||
("DEF-upper_arm_twist.L", "DEF-upper_arm.L"),
|
|
||||||
("DEF-upper_arm_twist.R", "DEF-upper_arm.R"),
|
|
||||||
("DEF-forearm_twist.L", "DEF-forearm.L"),
|
|
||||||
("DEF-forearm_twist.R", "DEF-forearm.R"),
|
|
||||||
("DEF-thigh_twist.L", "DEF-thigh.L"),
|
|
||||||
("DEF-thigh_twist.R", "DEF-thigh.R")
|
|
||||||
]
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
for twist_bone, parent_bone in twist_bones:
|
|
||||||
if twist_bone in armature.data.edit_bones and parent_bone in armature.data.edit_bones:
|
|
||||||
twist = armature.data.edit_bones[twist_bone]
|
|
||||||
parent = armature.data.edit_bones[parent_bone]
|
|
||||||
parent.tail = twist.tail
|
|
||||||
for child in twist.children:
|
|
||||||
child.parent = parent
|
|
||||||
armature.data.edit_bones.remove(twist)
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
def get_org_remap(self, armature):
|
|
||||||
remap = {}
|
|
||||||
for bone in armature.data.bones:
|
|
||||||
if self.is_def_bone(bone.name):
|
|
||||||
name = self.get_proto_name(bone.name)
|
|
||||||
parent = bone.parent
|
|
||||||
while parent:
|
|
||||||
parent_name = self.get_proto_name(parent.name)
|
|
||||||
if parent_name != name:
|
|
||||||
if ('DEF-' + parent_name) in armature.data.bones:
|
|
||||||
remap[bone.name] = 'DEF-' + parent_name
|
|
||||||
break
|
|
||||||
parent = parent.parent
|
|
||||||
return remap
|
|
||||||
|
|
||||||
def get_special_remap(self):
|
|
||||||
return {
|
|
||||||
'DEF-thigh.L': 'DEF-pelvis.L',
|
|
||||||
'DEF-thigh.R': 'DEF-pelvis.R',
|
|
||||||
'DEF-upper_arm.L': 'DEF-shoulder.L',
|
|
||||||
'DEF-upper_arm.R': 'DEF-shoulder.R',
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_transform_copies(self, armature):
|
|
||||||
result = []
|
|
||||||
for bone in armature.pose.bones:
|
|
||||||
if self.is_def_bone(bone.name) and not self.has_transform_copies(bone):
|
|
||||||
result.append(bone.name)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def has_transform_copies(self, bone):
|
|
||||||
return any(constraint.type == 'COPY_TRANSFORMS' for constraint in bone.constraints)
|
|
||||||
|
|
||||||
def is_def_bone(self, bone_name):
|
|
||||||
return bone_name.startswith('DEF-')
|
|
||||||
|
|
||||||
def is_org_bone(self, bone_name):
|
|
||||||
return bone_name.startswith('ORG-')
|
|
||||||
|
|
||||||
def get_proto_name(self, bone_name):
|
|
||||||
if self.is_def_bone(bone_name) or self.is_org_bone(bone_name):
|
|
||||||
return bone_name[4:]
|
|
||||||
return bone_name
|
|
||||||
@@ -1,295 +0,0 @@
|
|||||||
from typing import TypedDict
|
|
||||||
import bpy
|
|
||||||
from bpy.types import Operator, Object, Context, Mesh, MeshUVLoopLayer
|
|
||||||
import bmesh
|
|
||||||
import numpy as np
|
|
||||||
import math
|
|
||||||
from ..core.translations import t
|
|
||||||
|
|
||||||
class GenerateLoopTreeResult(TypedDict):
|
|
||||||
tree: dict[str, set[str]]
|
|
||||||
selected_loops: dict[str,list[int]]
|
|
||||||
selected_verts: dict[str,int]
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolkit_OT_AlignUVEdgesToTarget(Operator):
|
|
||||||
bl_idname = "avatar_toolkit.align_uv_edges_to_target"
|
|
||||||
bl_label = t("avatar_toolkit.align_uv_edges_to_target.label")
|
|
||||||
bl_description = t("avatar_toolkit.align_uv_edges_to_target.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#all selected objects need to be meshes for this to work - @989onan
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context: Context):
|
|
||||||
if not ((context.view_layer.objects.active is not None) and (len(context.view_layer.objects.selected) > 0)):
|
|
||||||
return False
|
|
||||||
if context.mode != "EDIT_MESH":
|
|
||||||
return False
|
|
||||||
for obj in context.view_layer.objects.selected:
|
|
||||||
if obj.type != "MESH":
|
|
||||||
return False
|
|
||||||
if not context.space_data:
|
|
||||||
return False
|
|
||||||
if not context.space_data.show_uvedit:
|
|
||||||
return False
|
|
||||||
if context.scene.tool_settings.use_uv_select_sync:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def execute(self, context: Context):
|
|
||||||
|
|
||||||
|
|
||||||
target: str = context.view_layer.objects.active.name #The object which we want to align every other selected object's selected UV vertex line to
|
|
||||||
|
|
||||||
sources: list[str] = [i.name for i in context.view_layer.objects.selected] #The objects which we want to align their selected UV lines to the target's UV line
|
|
||||||
|
|
||||||
prev_mode: str = bpy.context.object.mode
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
|
|
||||||
def generate_loop_tree(obj_name: str) -> GenerateLoopTreeResult:
|
|
||||||
print("Finding selected line for: \""+obj_name+"\"!")
|
|
||||||
|
|
||||||
|
|
||||||
vert_target_loops: dict[str,list[int]] = {}
|
|
||||||
vert_target_verts: dict[str,int] = {}
|
|
||||||
|
|
||||||
me: Mesh = bpy.data.objects[obj_name].data
|
|
||||||
uv_lay: MeshUVLoopLayer = me.uv_layers.active
|
|
||||||
bm: bmesh.types.BMesh = bmesh.new()
|
|
||||||
bm.from_mesh(me)
|
|
||||||
bm.verts.ensure_lookup_table()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# To explain:
|
|
||||||
# So loops in UV maps are X polygons that make up a face (So a MeshLoop represent a face and each vertex on that face is in order)
|
|
||||||
#
|
|
||||||
# For some preknowledge:
|
|
||||||
# When a mesh is UV unwrapped, if a vertice is shared by two different faces on the model in the viewport and the vertice of both faces are in
|
|
||||||
# the same position on the UV map, then it considers it one point and the user can move it
|
|
||||||
# (is why the uv map doesn't split apart when you try to move a vertex because that would be annoying)
|
|
||||||
#
|
|
||||||
# The problem:
|
|
||||||
# The problem is that the data for whether the uv corners of two faces that share a vertex physically being connected and selected as one vertex on the uv map does not exist
|
|
||||||
# Though thankfully, blender forcibly (whether you like it or not) merges vertices of a uv map if the vertex of two different faces are actually shared in the UI,
|
|
||||||
# allowing for the moving of vertices of 4 faces connected by a single vertex. Behavior every normal blender user is familiar with.
|
|
||||||
#
|
|
||||||
# The solution
|
|
||||||
# We can use this to our advantage, by finding vertices on the uv map that share the same coridinate as another vertex that is also selected.
|
|
||||||
# that way we can group each pair shared in a line as the same vertex, and identify the line using these pairs and using the data that says for certain
|
|
||||||
# that two vertices share the same face loop, and therefore are connected.
|
|
||||||
|
|
||||||
#hmmm real stupid grimlin hours with this one. Using a string as the index of a dictionary of loop corners that end up on the same coordinate
|
|
||||||
|
|
||||||
for k,i in enumerate(uv_lay.vertex_selection): #go through the selected vertices on object.
|
|
||||||
if (i.value == True) and (bm.verts[me.loops[k].vertex_index].select == True) and (bm.verts[me.loops[k].vertex_index].hide == False): #filter out vertices that are hidden from UV port
|
|
||||||
key = np.array(uv_lay.uv[k].vector[:])
|
|
||||||
key = key.round(decimals=5) #make a key that is the position of a selected vertex
|
|
||||||
|
|
||||||
if str(key) not in vert_target_loops:
|
|
||||||
vert_target_loops[str(key)] = [] #if the vertex's position is not a list yet, add it.
|
|
||||||
vert_target_loops[str(key)].append(k) #Basically, group vertices based on their position on a UV map as a list.
|
|
||||||
vert_target_verts[str(key)] = me.loops[k].vertex_index #associate the index of the physical vertex in real space with the coordinate of the uv vertices that share a position (Basically associate UV vert with real vert)
|
|
||||||
if len(vert_target_loops) > 4000: #This usually indicates that the user has a bunch of crap selected.
|
|
||||||
self.report({'WARNING'}, t("UVTools.align_uv_to_target.warning.too_much"))
|
|
||||||
return
|
|
||||||
print("Finding connections on line for \""+obj_name+"\"!")
|
|
||||||
me.validate()
|
|
||||||
|
|
||||||
bm = bmesh.new()
|
|
||||||
bm.from_mesh(me)
|
|
||||||
|
|
||||||
|
|
||||||
#print(vert_target_loops)
|
|
||||||
#print(vert_target_verts)
|
|
||||||
tree: dict[str, set[str]] = {}
|
|
||||||
selected_verts = np.hstack(list(vert_target_loops.values()))
|
|
||||||
#print(selected_verts)
|
|
||||||
bm.verts.ensure_lookup_table()
|
|
||||||
for uvcoordsstr in vert_target_loops:
|
|
||||||
|
|
||||||
uv_lay = me.uv_layers.active
|
|
||||||
|
|
||||||
|
|
||||||
#before this section, each vert_target_loops is just groupings of vertices that share coordinates.
|
|
||||||
# Using the data that determines UV face corners (uvloops) that are associated with the real vertex,
|
|
||||||
# and the uv face corners (loops) that are on the same faces as the vertices that share coordinates in
|
|
||||||
# vert_target_loops, we can now identify them
|
|
||||||
#TL;DR: pairs of vertices that share cooridinates (chain links) find their buddies (make chain connected)
|
|
||||||
|
|
||||||
# Someone explain this better than me if you can please - @989onan
|
|
||||||
extension_loops = []
|
|
||||||
loops = bm.verts[vert_target_verts[uvcoordsstr]].link_loops
|
|
||||||
loops_indexes = [i.index for i in loops]
|
|
||||||
for loop in vert_target_loops[uvcoordsstr]:
|
|
||||||
if loop in loops_indexes:
|
|
||||||
loop_obj = loops[loops_indexes.index(loop)]
|
|
||||||
extension_loops.append(loop_obj.link_loop_next.index)
|
|
||||||
extension_loops.append(loop_obj.link_loop_prev.index)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#make a tree out of the vertices we identified as sharing faces with the vertices in vert_target_loops, and then link them together in a dictionary.
|
|
||||||
#the order of this dictionary is unknown.
|
|
||||||
# Someone explain this better than me if you can please - @989onan
|
|
||||||
tree[uvcoordsstr] = set()
|
|
||||||
|
|
||||||
for i in extension_loops:
|
|
||||||
if i in selected_verts:
|
|
||||||
key = np.array(uv_lay.uv[i].vector[:])
|
|
||||||
key = key.round(decimals=5)
|
|
||||||
tree[uvcoordsstr].add(str(key))
|
|
||||||
|
|
||||||
if uvcoordsstr in tree:
|
|
||||||
if len(tree[uvcoordsstr]) > 2:
|
|
||||||
self.report({'WARNING'}, t("UVTools.align_uv_to_target.warning.need_a_line").format(obj=obj_name))
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
uv_lay = me.uv_layers.active
|
|
||||||
for uvcoordstr in vert_target_loops:
|
|
||||||
for loop in vert_target_loops[uvcoordstr]:
|
|
||||||
uv_lay.vertex_selection[loop].value = True
|
|
||||||
|
|
||||||
|
|
||||||
bm.free()
|
|
||||||
me.validate()
|
|
||||||
print("found UV line connections for \""+obj_name+"\":")
|
|
||||||
#print(tree)
|
|
||||||
|
|
||||||
return {"tree":tree,"selected_loops":vert_target_loops,"selected_verts":vert_target_verts}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#This function uses the previous point to find the next point based on connected loops and faces.
|
|
||||||
def sort_uv_tree(originaltree: dict[str, set[str]], obj_name: str):
|
|
||||||
sortedtree: dict[str, set[str]] = originaltree.copy()
|
|
||||||
startpoints: list[str] = []
|
|
||||||
for i in sortedtree:
|
|
||||||
if len(sortedtree[i]) < 2:
|
|
||||||
startpoints.append(i)
|
|
||||||
|
|
||||||
if len(startpoints) != 2:
|
|
||||||
self.report({'WARNING'}, t("UVTools.align_uv_to_target.warning.need_a_line").format(obj=obj_name))
|
|
||||||
return
|
|
||||||
|
|
||||||
a_list1 = startpoints[0].replace(", "," ").replace("[","").replace("]","").split()
|
|
||||||
map_object1 = map(float, a_list1)
|
|
||||||
uvcoords1 = list(map_object1)
|
|
||||||
a_list2 = startpoints[1].replace(", "," ").replace("[","").replace("]","").split()
|
|
||||||
map_object2 = map(float, a_list2)
|
|
||||||
uvcoords2 = list(map_object2)
|
|
||||||
|
|
||||||
cursor = context.space_data.cursor_location
|
|
||||||
|
|
||||||
startpoint = None
|
|
||||||
if math.sqrt( (((uvcoords1[0]) - (cursor[0])) **2) + (((uvcoords1[1]) - (cursor[1])) **2) ) > math.sqrt( (((uvcoords2[0]) - (cursor[0])) **2) + (((uvcoords2[1]) - (cursor[1])) **2) ):
|
|
||||||
startpoint = startpoints[0]
|
|
||||||
else:
|
|
||||||
startpoint = startpoints[1]
|
|
||||||
|
|
||||||
#Wew my first actual recursive sort! - @989onan
|
|
||||||
def recursive_sort_uv_tree(point: str, sortedfinal: list[str]):
|
|
||||||
#print("appending "+point)
|
|
||||||
sortedfinal.append(point)
|
|
||||||
|
|
||||||
new_point: str = ""
|
|
||||||
for i in sortedtree:
|
|
||||||
if point in sortedtree[i]:
|
|
||||||
new_point = i
|
|
||||||
removed_value = sortedtree.pop(i)
|
|
||||||
#print(removed_value)
|
|
||||||
break
|
|
||||||
|
|
||||||
if new_point == "":
|
|
||||||
print("BROKE OUT OF SORTING, FINAL TREE (Should be empty, if not you errored here!):")
|
|
||||||
print(sortedtree)
|
|
||||||
|
|
||||||
return sortedfinal
|
|
||||||
|
|
||||||
return recursive_sort_uv_tree(new_point, sortedfinal)
|
|
||||||
|
|
||||||
array = []
|
|
||||||
|
|
||||||
sortedtree.pop(startpoint)
|
|
||||||
return recursive_sort_uv_tree(startpoint, array)
|
|
||||||
|
|
||||||
def lerp(v0, v1, t):
|
|
||||||
return v0 + t * (v1 - v0)
|
|
||||||
|
|
||||||
|
|
||||||
target_data: GenerateLoopTreeResult = generate_loop_tree(target)
|
|
||||||
sorted_target_tree = sort_uv_tree(target_data["tree"], target)
|
|
||||||
print("sorted target.")
|
|
||||||
#print(sorted_target_tree)
|
|
||||||
|
|
||||||
for source in sources:
|
|
||||||
if source == target:
|
|
||||||
continue
|
|
||||||
|
|
||||||
#create our list of points that is a chain. then sort the chain into the correct order based on connections of vertices and the faces that the vertices make up in the UV map.
|
|
||||||
try:
|
|
||||||
source_data = generate_loop_tree(source)
|
|
||||||
sorted_source_tree = sort_uv_tree(source_data["tree"], source)
|
|
||||||
print("Sorted source "+source)
|
|
||||||
print(sorted_source_tree)
|
|
||||||
|
|
||||||
vertex_factor = float(len(sorted_target_tree)-1) / (float(len(sorted_source_tree)-1))
|
|
||||||
|
|
||||||
print(str(vertex_factor)+" = "+str(float(len(sorted_target_tree)-1)) + " / " + str((float(len(sorted_source_tree)-1)))+")")
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
for k,i in enumerate(sorted_source_tree):
|
|
||||||
|
|
||||||
try:
|
|
||||||
#find where we are on the target edges, to interpolate the current point we're placing along the target point's line.
|
|
||||||
progress_along_edge = (float(k)*vertex_factor)
|
|
||||||
previous_vertex_index = math.floor(progress_along_edge)
|
|
||||||
next_vertex_index = math.ceil(progress_along_edge)
|
|
||||||
|
|
||||||
|
|
||||||
#find the uv coordinates of the previous and next points on the target uv line.
|
|
||||||
a_list1 = sorted_target_tree[previous_vertex_index].replace(", "," ").replace("[","").replace("]","").split()
|
|
||||||
map_object1 = map(float, a_list1)
|
|
||||||
previous_point = list(map_object1)
|
|
||||||
a_list2 = sorted_target_tree[next_vertex_index].replace(", "," ").replace("[","").replace("]","").split()
|
|
||||||
map_object2 = map(float, a_list2)
|
|
||||||
next_point = list(map_object2)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#create a point between these two values that represents a decimal 0-1 going where we are to where we are going between the two current points on the edge we are targeting this whole shebang with.
|
|
||||||
progress_between_points = progress_along_edge - int(progress_along_edge)
|
|
||||||
lerped_point = [lerp(previous_point[0],next_point[0],progress_between_points),lerp(previous_point[1],next_point[1],progress_between_points)]
|
|
||||||
|
|
||||||
#grab our uv face corners for each uv coord that we saved.
|
|
||||||
#Since each face is considered separate internally, we have to treat each connected face to a vertex in a uv map as separate entities/vertexes.
|
|
||||||
#basically pretend they are split apart.
|
|
||||||
uv_face_corners = source_data["selected_loops"][i]
|
|
||||||
#print("doing from vertex "+str(previous_vertex_index)+" to "+str(next_vertex_index)+" total progress: "+str(progress_along_edge))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
me: Mesh = bpy.data.objects[source].data
|
|
||||||
me.validate()
|
|
||||||
bm: bmesh.types.BMesh = bmesh.new()
|
|
||||||
bm.from_mesh(me)
|
|
||||||
uv_lay: MeshUVLoopLayer = me.uv_layers.active
|
|
||||||
bm.verts.ensure_lookup_table()
|
|
||||||
for corner in uv_face_corners:
|
|
||||||
uv_lay.uv[corner].vector = lerped_point #put the vertcies at the point we calculated.
|
|
||||||
except:
|
|
||||||
print("This is probably fine? - @989onan") #TODO: What happened here? The magic of making code so complex you forget if this is even an issue. - @989onan
|
|
||||||
|
|
||||||
print("Finished mesh \""+source+"\" for UV's")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode=prev_mode)
|
|
||||||
return {'FINISHED'}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import bpy
|
|
||||||
from ..core import common
|
|
||||||
from ..core.translations import t
|
|
||||||
from typing import List, Tuple
|
|
||||||
from ..core.common import get_selected_armature, is_valid_armature, get_all_meshes, init_progress, update_progress, finish_progress
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_AutoVisemeButton(bpy.types.Operator):
|
|
||||||
bl_idname = 'avatar_toolkit.create_visemes'
|
|
||||||
bl_label = t('AutoVisemeButton.label')
|
|
||||||
bl_description = t('AutoVisemeButton.desc')
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context: bpy.types.Context) -> bool:
|
|
||||||
armature = get_selected_armature(context)
|
|
||||||
return armature is not None and is_valid_armature(armature) and get_all_meshes(context)
|
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> set:
|
|
||||||
try:
|
|
||||||
self.create_visemes(context)
|
|
||||||
return {'FINISHED'}
|
|
||||||
except Exception as e:
|
|
||||||
self.report({'ERROR'}, str(e))
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
def create_visemes(self, context: bpy.types.Context) -> None:
|
|
||||||
init_progress(context, 5) # 5 main steps
|
|
||||||
|
|
||||||
update_progress(self, context, t("VisemePanel.start_viseme_creation"))
|
|
||||||
mesh = bpy.data.objects.get(context.scene.avatar_toolkit.selected_mesh)
|
|
||||||
if not mesh or not common.has_shapekeys(mesh):
|
|
||||||
raise ValueError(t('AutoVisemeButton.error.noShapekeys'))
|
|
||||||
|
|
||||||
update_progress(self, context, t("VisemePanel.removing_existing_visemes"))
|
|
||||||
self.remove_existing_vrc_shapekeys(mesh)
|
|
||||||
|
|
||||||
shape_a = context.scene.avatar_toolkit.mouth_a
|
|
||||||
shape_o = context.scene.avatar_toolkit.mouth_o
|
|
||||||
shape_ch = context.scene.avatar_toolkit.mouth_ch
|
|
||||||
|
|
||||||
if shape_a == "Basis" or shape_o == "Basis" or shape_ch == "Basis":
|
|
||||||
raise ValueError(t('AutoVisemeButton.error.selectShapekeys'))
|
|
||||||
|
|
||||||
update_progress(self, context, t("VisemePanel.creating_visemes"))
|
|
||||||
visemes: List[Tuple[str, List[Tuple[str, float]]]] = [
|
|
||||||
('vrc.v_aa', [(shape_a, 0.9998)]),
|
|
||||||
('vrc.v_ch', [(shape_ch, 0.9996)]),
|
|
||||||
('vrc.v_dd', [(shape_a, 0.3), (shape_ch, 0.7)]),
|
|
||||||
('vrc.v_e', [(shape_a, 0.5), (shape_ch, 0.2)]),
|
|
||||||
('vrc.v_ff', [(shape_a, 0.2), (shape_ch, 0.4)]),
|
|
||||||
('vrc.v_ih', [(shape_ch, 0.7), (shape_o, 0.3)]),
|
|
||||||
('vrc.v_kk', [(shape_a, 0.7), (shape_ch, 0.4)]),
|
|
||||||
('vrc.v_nn', [(shape_a, 0.2), (shape_ch, 0.7)]),
|
|
||||||
('vrc.v_oh', [(shape_a, 0.2), (shape_o, 0.8)]),
|
|
||||||
('vrc.v_ou', [(shape_o, 0.9994)]),
|
|
||||||
('vrc.v_pp', [(shape_a, 0.0004), (shape_o, 0.0004)]),
|
|
||||||
('vrc.v_rr', [(shape_ch, 0.5), (shape_o, 0.3)]),
|
|
||||||
('vrc.v_sil', [(shape_a, 0.0002), (shape_ch, 0.0002)]),
|
|
||||||
('vrc.v_ss', [(shape_ch, 0.8)]),
|
|
||||||
('vrc.v_th', [(shape_a, 0.4), (shape_o, 0.15)])
|
|
||||||
]
|
|
||||||
|
|
||||||
for viseme_name, shape_mix in visemes:
|
|
||||||
self.create_viseme(mesh, viseme_name, shape_mix, context.scene.avatar_toolkit.shape_intensity)
|
|
||||||
|
|
||||||
update_progress(self, context, t("VisemePanel.sorting_shapekeys"))
|
|
||||||
common.sort_shape_keys(mesh)
|
|
||||||
|
|
||||||
update_progress(self, context, t("VisemePanel.viseme_creation_completed"))
|
|
||||||
finish_progress(context)
|
|
||||||
|
|
||||||
def create_viseme(self, mesh: bpy.types.Object, viseme_name: str, shape_mix: List[Tuple[str, float]], intensity: float) -> None:
|
|
||||||
shape_keys = mesh.data.shape_keys.key_blocks
|
|
||||||
|
|
||||||
if viseme_name in shape_keys:
|
|
||||||
mesh.shape_key_remove(shape_keys[viseme_name])
|
|
||||||
|
|
||||||
new_key = mesh.shape_key_add(name=viseme_name, from_mix=False)
|
|
||||||
new_key.value = 0.0
|
|
||||||
|
|
||||||
for shape_name, value in shape_mix:
|
|
||||||
if shape_name in shape_keys:
|
|
||||||
source_shape = shape_keys[shape_name]
|
|
||||||
for i, vert in enumerate(new_key.data):
|
|
||||||
vert.co += (source_shape.data[i].co - shape_keys['Basis'].data[i].co) * value * intensity
|
|
||||||
|
|
||||||
def remove_existing_vrc_shapekeys(self, mesh: bpy.types.Object) -> None:
|
|
||||||
vrc_prefixes = ['vrc.v_', 'vrc.blink_', 'vrc.lowerlid_']
|
|
||||||
shape_keys = mesh.data.shape_keys.key_blocks
|
|
||||||
for key in reversed(shape_keys):
|
|
||||||
if any(key.name.startswith(prefix) for prefix in vrc_prefixes):
|
|
||||||
mesh.shape_key_remove(key)
|
|
||||||
@@ -1,263 +1,11 @@
|
|||||||
{
|
{
|
||||||
"authors": ["Avatar Toolkit Team"],
|
"authors": ["Avatar Toolkit Team"],
|
||||||
"messages": {
|
"messages": {
|
||||||
"AutoVisemeButton.desc": "Create visemes automatically, based on shape keys",
|
"AvatarToolkit.label": "Avatar Toolkit (Alpha 0.1.0)",
|
||||||
"AutoVisemeButton.error.noShapekeys": "No shape keys found",
|
"AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there",
|
||||||
"AutoVisemeButton.error.selectShapekeys": "Please Select shape keys",
|
"AvatarToolkit.desc2": "will be issues, if you find any issues,",
|
||||||
"AutoVisemeButton.label": "Create Visemes",
|
"AvatarToolkit.desc3": "please report it on our Github.",
|
||||||
"AutoVisemeButton.success": "Visemes created successfully",
|
|
||||||
"AvatarToolkit.label": "Avatar Toolkit (Alpha)",
|
|
||||||
"AvatarToolkit.desc1": "Avatar Toolkit is in Early Access",
|
|
||||||
"AvatarToolkit.desc2": "There will be issues, if you find",
|
|
||||||
"AvatarToolkit.desc3": "an issue, please report it on Github.",
|
|
||||||
"Export.resonite.desc": "Export a GLB with all animations and materials. For animation data see:",
|
|
||||||
"Export.resonite.label": "Export to Resonite",
|
|
||||||
"Importer.export_resonite.desc": "Export to Resonite as a GLTF. Make sure your model is to scale in blender, and import as meters in Resonite.",
|
|
||||||
"Importer.export_resonite.label": "Export to Resonite",
|
|
||||||
"Importer.export_vrchat.desc": "Export to VRChat, may also work for ChilloutVR. Is similar to Cats export.",
|
|
||||||
"Importer.export_vrchat.label": "Export to VRChat",
|
|
||||||
"Importer.mmd_anim_importer.desc": "Import a MMD Animation (.vmd)",
|
|
||||||
"Importer.mmd_anim_importer.label": "MMD Animation",
|
|
||||||
"Importing.importer_search_term": "https://search.brave.com/search?q=blender+{extension}+importer+addon&source=web",
|
|
||||||
"Importing.need_importer": "You do not have the required importer for the {extension} type! Opening web browser for importer search term...",
|
|
||||||
"Language.auto": "Automatic",
|
|
||||||
"Language.en_US": "English",
|
|
||||||
"Language.ja_JP": "日本語",
|
|
||||||
"Optimization.applying_transforms": "Applying transforms...",
|
|
||||||
"Optimization.cleaning_material_names": "Cleaning material names...",
|
|
||||||
"Optimization.cleaning_material_slots": "Cleaning material slots...",
|
|
||||||
"Optimization.clearing_unused_data": "Clearing unused data...",
|
|
||||||
"Optimization.materials_optimization_report": "Materials optimization completed: Combined {num_combined} materials, cleaned {num_cleaned_slots} material slots, cleaned {num_cleaned_names} material names, and removed {num_removed_data_blocks} unused data blocks",
|
|
||||||
"Optimization.combine_materials.desc": "Combine similar materials to reduce draw calls and improve performance",
|
|
||||||
"Optimization.combine_materials.label": "Combine Materials",
|
|
||||||
"Optimization.consolidating_materials": "Consolidating materials...",
|
|
||||||
"Optimization.finalizing": "Finalizing...",
|
|
||||||
"Optimization.fixing_uv_coordinates": "Fixing UV coordinates...",
|
|
||||||
"Optimization.join_all_meshes.desc": "Merge all meshes into a single object to reduce draw calls",
|
|
||||||
"Optimization.join_all_meshes.label": "Join All Meshes",
|
|
||||||
"Optimization.join_error": "Error during mesh joining",
|
|
||||||
"Optimization.join_operation_failed": "Join operation failed",
|
|
||||||
"Optimization.join_selected_meshes.desc": "Merge only the selected meshes into a single object",
|
|
||||||
"Optimization.join_selected_meshes.label": "Join Selected Meshes",
|
|
||||||
"Optimization.joinmeshes.label": "Join Meshes:",
|
|
||||||
"Optimization.joining_meshes": "Joining meshes...",
|
|
||||||
"Optimization.label": "Optimization",
|
|
||||||
"Optimization.material_attribute_mismatch": "Attribute mismatch in material {material_name}, skipping",
|
|
||||||
"Optimization.materials_combined": "Combined {num_combined} materials",
|
|
||||||
"Optimization.meshes_joined": "Meshes joined successfully",
|
|
||||||
"Optimization.no_armature_selected": "No armature selected",
|
|
||||||
"Optimization.no_mesh_selected": "No mesh objects selected",
|
|
||||||
"Optimization.no_meshes_found": "No meshes found for the selected armature",
|
|
||||||
"Optimization.options.label": "Optimization:",
|
|
||||||
"Optimization.preparing_meshes": "Preparing meshes...",
|
|
||||||
"Optimization.processing_mesh_no_shapekeys": "Processing mesh with no shapekeys named \"{mesh_name}\"",
|
|
||||||
"Optimization.processing_shapekey": "Processing shapekey \"{shapekeyname}\" on mesh \"{mesh_name}\"",
|
|
||||||
"Optimization.remove_doubles_completed": "Remove doubles operation completed",
|
|
||||||
"Optimization.remove_doubles_safely.desc": "Remove duplicate vertices while preserving important features like mouth shapes.\nIs a quick solution but does not merge vertices that move at all.",
|
|
||||||
"Optimization.remove_doubles_safely.label": "Remove Doubles Safely",
|
|
||||||
"Optimization.remove_doubles_safely_advanced.label": "Advanced Remove Doubles Safely",
|
|
||||||
"Optimization.remove_doubles_safely_advanced.desc": "Remove duplicate vertices while preserving important features like mouth shapes.\nUnlike basic, Advanced will merge vertices together that move, but still preserve shapekeys.\nEx: It will not seal the lips of the mouth closed, but will fix split polygons that make up the lips.",
|
|
||||||
"Optimization.select_armature": "Please select an armature",
|
|
||||||
"Optimization.select_at_least_two_meshes": "Please select at least two mesh objects",
|
|
||||||
"Optimization.selected_meshes_joined": "Selected meshes joined successfully",
|
|
||||||
"Optimization.selecting_meshes": "Selecting meshes...",
|
|
||||||
"Optimization.transform_apply_failed": "Transform apply failed",
|
|
||||||
"Optimization.vertex_excluded": "Shapekey has a moved vertex at index \"{index}\", excluding from double merging!",
|
|
||||||
"Quick_Access.selected_armature.label": "Selected Armature",
|
|
||||||
"Quick_Access.selected_armature.desc": "The currently \"targeted\" armature for Avatar Toolkit operations",
|
|
||||||
"Quick_Access.export": "Export",
|
|
||||||
"Quick_Access.export_fbx.desc": "Export the model as FBX",
|
|
||||||
"Quick_Access.export_fbx.label": "Export FBX",
|
|
||||||
"Quick_Access.export_menu.desc": "Export to a supported format",
|
|
||||||
"Quick_Access.export_menu.label": "Export Menu",
|
|
||||||
"Quick_Access.import": "Import",
|
|
||||||
"Quick_Access.import_export.label": "Import/Export:",
|
|
||||||
"Quick_Access.import_menu.desc": "Import a Model",
|
|
||||||
"Quick_Access.import_menu.label": "Import Menu",
|
|
||||||
"Quick_Access.import_pmd": "Import PMD",
|
|
||||||
"Quick_Access.import_pmd.desc": "Import MMD PMD Model",
|
|
||||||
"Quick_Access.import_pmx": "Import PMX",
|
|
||||||
"Quick_Access.import_pmx.desc": "Import MMD PMX Model",
|
|
||||||
"Quick_Access.import_success": "Model imported successfully",
|
|
||||||
"Quick_Access.label": "Quick Access",
|
|
||||||
"Quick_Access.options": "Quick Access:",
|
|
||||||
"Quick_Access.select_armature": "Select Armature:",
|
|
||||||
"Quick_Access.apply_armature_failed": "Applying armature as pose failed at the joining shapekeys back together stage!",
|
|
||||||
"Quick_Access.apply_pose_as_rest.desc": "Makes current pose the default rest pose.",
|
|
||||||
"Quick_Access.stop_pose_mode.desc": "Exits pose mode and clears all posing on all visible bones in pose mode.",
|
|
||||||
"Quick_Access.apply_pose_as_rest.label": "Apply Pose as Rest Pose",
|
|
||||||
"Quick_Access.apply_pose_as_shapekey.desc": "Makes the current pose a shapekey that can be activated later.\nThis is good for applying a jaw open position as a shapekey for facial movements.",
|
|
||||||
"Quick_Access.apply_pose_as_shapekey.label": "Apply Pose as Shapekey",
|
|
||||||
"Quick_Access.stop_pose_mode.label": "Exit Pose Mode",
|
|
||||||
"Quick_Access.start_pose_mode.desc": "Starts pose mode for the armature targeted by Avatar Toolkit.",
|
|
||||||
"Quick_Access.start_pose_mode.label": "Start Pose Mode",
|
|
||||||
"Quick_Access.select_export.label": "Select Export Method",
|
|
||||||
"Quick_Access.select_export_resonite.label": "Resonite",
|
|
||||||
"Settings.label": "Settings",
|
|
||||||
"Settings.language.desc": "Select the language for the addon's UI",
|
|
||||||
"Settings.language.label": "Language:",
|
|
||||||
"Settings.translation_restart_popup.description": "Information about translation updates",
|
|
||||||
"Settings.translation_restart_popup.label": "Translation Update",
|
|
||||||
"Settings.translation_restart_popup.message1": "Some translations may not apply",
|
|
||||||
"Settings.translation_restart_popup.message2": "until you restart Blender.",
|
|
||||||
"TextureAtlas.atlas_completed": "Texture atlas creation completed",
|
|
||||||
"TextureAtlas.atlas_error": "An error occurred during texture atlas creation",
|
|
||||||
"TextureAtlas.atlas_materials": "Atlas Materials",
|
|
||||||
"TextureAtlas.atlas_materials_desc": "Atlas materials to optimize the model",
|
|
||||||
"TextureAtlas.label": "Texture Atlasing",
|
|
||||||
"TextureAtlas.loaded_list": "Loaded Texture Atlas Material List",
|
|
||||||
"TextureAtlas.material_list_label": "Texture Atlas Material List Material",
|
|
||||||
"TextureAtlas.reload_list": "Reload Texture Atlas Material List",
|
|
||||||
"TextureAtlas.error.label": "ERROR",
|
|
||||||
"TextureAtlas.none.label": "None",
|
|
||||||
"TextureAtlas.no_nodes_error.desc": "THIS MATERIAL DOES NOT USE NODES!",
|
|
||||||
"TextureAtlas.no_images_error.desc": "THIS MATERIAL HAS NO IMAGES!",
|
|
||||||
"TextureAtlas.texture_use_atlas.desc": "The texture that will be used for the {name} map atlas",
|
|
||||||
"TextureAtlas.albedo": "Albedo",
|
|
||||||
"TextureAtlas.normal": "Normal",
|
|
||||||
"TextureAtlas.emission": "Emission",
|
|
||||||
"TextureAtlas.ambient_occlusion": "Ambient Occlusion",
|
|
||||||
"TextureAtlas.height": "Height",
|
|
||||||
"TextureAtlas.roughness": "Roughness",
|
|
||||||
"Tools.bones_translated_success": "Successfully translated all bones to humanoid names",
|
|
||||||
"Tools.bones_translated_with_fails": "Failed to translate {translate_bone_fails} bones to humanoid names. Adding \"<noik>\" to their names.",
|
|
||||||
"Tools.convert_to_resonite.desc": "Converts bone names on a model to names compatible with Resonite",
|
|
||||||
"Tools.convert_to_resonite.label": "Convert to Resonite",
|
|
||||||
"Tools.create_digitigrade_legs.desc": "Create digitigrade legs from a selected bone chain",
|
|
||||||
"Tools.create_digitigrade_legs.label": "Create Digitigrade Legs",
|
|
||||||
"Tools.digitigrade_legs.error.bone_format": "Bone format incorrect! Please select a chain of 4 continuous bones!",
|
|
||||||
"Tools.digitigrade_legs.success": "Digitigrade legs created successfully",
|
|
||||||
"Tools.import_any_model.desc": "Import any supported model, FBX, SMD, DMX, GLTF, PMD, PMX and more.",
|
|
||||||
"Tools.import_any_model.label": "Import Model",
|
|
||||||
"UVTools.align_uv_to_target.warning.too_much": "Error! You have way to much stuff selected. Are you sure you're selecting two edges?",
|
|
||||||
"UVTools.align_uv_to_target.warning.need_a_line": "You need one line of selected uv points per selected object. Object \"{obj}\" does not meet this requirement!",
|
|
||||||
"avatar_toolkit.align_uv_edges_to_target.label":"Align UV Edges to Target",
|
|
||||||
"avatar_toolkit.align_uv_edges_to_target.desc":"Aligns a selected line of UV points on each selected mesh\nto the line of selected uv points on the active mesh.\nUseful for kitbashing textures of one model onto another.\nUses distance from the 2D cursor to identify the start of the line of uv points on each mesh.",
|
|
||||||
"Tools.label": "Tools",
|
|
||||||
"Tools.no_armature_selected": "No armature selected",
|
|
||||||
"Tools.select_armature": "Please select an armature",
|
|
||||||
"Tools.tools_title.label": "Tools:",
|
|
||||||
"Tools.separate_by.label": "Separate By:",
|
|
||||||
"Tools.separate_by_materials.label": "Separate by Materials",
|
|
||||||
"Tools.separate_by_materials.desc": "Separate the selected mesh by materials",
|
|
||||||
"Tools.separate_by_materials.success": "Mesh separated by materials successfully",
|
|
||||||
"Tools.separate_by_loose_parts.label": "Separate by Loose Parts",
|
|
||||||
"Tools.separate_by_loose_parts.desc": "Separate the selected mesh by loose parts",
|
|
||||||
"Tools.separate_by_loose_parts.success": "Mesh separated by loose parts successfully",
|
|
||||||
"Tools.apply_transforms.label": "Apply Transforms",
|
|
||||||
"Tools.apply_transforms.desc": "Apply position, rotation, and scale to the armature and its meshes",
|
|
||||||
"Tools.apply_transforms.invalid_armature": "Invalid armature selected",
|
|
||||||
"Tools.apply_transforms.success": "Transforms applied successfully to armature and meshes",
|
|
||||||
"Tools.remove_unused_shapekeys.label": "Remove Unused Shapekeys",
|
|
||||||
"Tools.remove_unused_shapekeys.tolerance.desc": "Min movement for position on any coordinate\n for any vertex for a shapekey to be kept.",
|
|
||||||
"Tools.remove_unused_shapekeys.desc": "Remove shapekeys that don't move anything.\nDoesn't get rid of category shapekeys.\n(ex: has \"~\", \"-\", or \"=\" in the name.)",
|
|
||||||
"Tools.remove_unused_shapekeys.tolerance.label": "Position Tolerance",
|
|
||||||
"Tools.apply_shape_key.label": "Apply Shapekey to Basis",
|
|
||||||
"Tools.apply_shape_key.desc": "Apply the selected shapekey to the basis, making it default on.",
|
|
||||||
"Tools.apply_shape_key.error": "The shape keys were not merged for some reason!",
|
|
||||||
"Tools.remove_zero_weight_bones.success": "Zero weight bones removed successfully",
|
|
||||||
"Tools.remove_zero_weight_bones.label": "Remove Zero Weight Bones",
|
|
||||||
"Tools.remove_zero_weight_bones.desc": "Remove bones from the armature that have weights less than threshold.",
|
|
||||||
"Tools.merge_bones_to_active.delete_old.desc": "Remove old bones when merging.",
|
|
||||||
"Tools.merge_bones_to_active.delete_old.label": "Remove Old Bones",
|
|
||||||
"Tools.merge_bones_to_active.desc": "Merge selected bones to active bone (selected in bright blue or orange).",
|
|
||||||
"Tools.merge_bones_to_active.label": "Merge Bones to Active",
|
|
||||||
"Tools.merge_bones_to_parents.delete_old.desc": "Remove old bones when merging.",
|
|
||||||
"Tools.merge_bones_to_parents.delete_old.label": "Remove Old Bones",
|
|
||||||
"Tools.merge_bones_to_parents.desc": "Merges every bone in the selection to each of their parents.",
|
|
||||||
"Tools.merge_bones_to_parents.label": "Merge Bones to Individual Parents",
|
|
||||||
"Tools.remove_zero_weight_bones.threshold.label": "Weight Threshold",
|
|
||||||
"Tools.remove_zero_weight_bones.threshold.desc": "If a bone is not weighted to any part of any mesh under the armature with a threshold greater than this, it is removed",
|
|
||||||
"Tools.connect_bones.label": "Connect Bones",
|
|
||||||
"Tools.bone_tools.label": "Bone Tools",
|
|
||||||
"Tools.additional_tools.label": "Additional Tools",
|
|
||||||
"Tools.merge_twist_bones.label": "Merge Twist Bones",
|
|
||||||
"Tools.merge_twist_bones.desc": "Merge twist bones into their parent bones",
|
|
||||||
"Tools.connect_bones.desc": "Connect bones with their respective children",
|
|
||||||
"Tools.connect_bones.invalid_armature": "Invalid armature selected",
|
|
||||||
"Tools.connect_bones.min_distance.label": "Minimum Distance",
|
|
||||||
"Tools.connect_bones.min_distance.desc": "Minimum distance between bones to connect them",
|
|
||||||
"Tools.connect_bones.success": "Connected {bones_connected} bones successfully",
|
|
||||||
"Tools.delete_bone_constraints.label": "Delete Bone Constraints",
|
|
||||||
"Tools.delete_bone_constraints.desc": "Remove all constraints from bones in the armature",
|
|
||||||
"Tools.delete_bone_constraints.invalid_armature": "Invalid armature selected",
|
|
||||||
"Tools.delete_bone_constraints.success": "Removed {constraints_removed} constraints from bones",
|
|
||||||
"Tools.convert_rigify_to_unity.label": "Convert Rigify to Unity",
|
|
||||||
"Tools.convert_rigify_to_unity.desc": "Prepare Rigify armature for use in Unity",
|
|
||||||
"Tools.convert_rigify_to_unity.success": "Rigify armature successfully converted for Unity",
|
|
||||||
"MergeArmatures.select_armature": "Please select an armature",
|
|
||||||
"MergeArmatures.title.label": "Merge Armatures:",
|
|
||||||
"MergeArmatures.label": "Merge Armatures",
|
|
||||||
"MergeArmatures.selected_armature.label": "Armature to Merge From",
|
|
||||||
"MergeArmatures.selected_armature.desc": "The armature that should be merged into the targeted armature for Avatar Toolkit.",
|
|
||||||
"MergeArmatures.target_armature.label": "Armature to Merge To",
|
|
||||||
"MergeArmatures.target_armature.desc": "The armature that should be the target for merging armatures.",
|
|
||||||
"MergeArmature.merge_armatures.label": "Merge Armatures Together",
|
|
||||||
"MergeArmature.merge_armatures.desc": "Merge {selected_armature_label} to the targeted armature for Avatar Toolkit.",
|
|
||||||
"MergeArmature.merge_armatures.align_bones.label": "Align Bones",
|
|
||||||
"MergeArmature.merge_armatures.align_bones.desc": "Align bones from source armature to target armature,\nstretching bones to match before merging.",
|
|
||||||
"MergeArmature.merge_armatures.apply_transforms.label": "Apply Transforms",
|
|
||||||
"MergeArmature.merge_armatures.apply_transforms.desc": "Apply transforms on armature and it's meshes before merging.",
|
|
||||||
"VisemePanel.create_visemes": "Create Visemes",
|
|
||||||
"VisemePanel.creating_viseme": "Creating viseme: {viseme_name}",
|
|
||||||
"VisemePanel.creating_viseme_detail": "Creating viseme: {viseme_name}",
|
|
||||||
"VisemePanel.creating_visemes": "Creating visemes...",
|
|
||||||
"VisemePanel.error.noArmature": "No armature selected",
|
|
||||||
"VisemePanel.error.noMesh": "No mesh selected",
|
|
||||||
"VisemePanel.error.noShapekeys": "Selected mesh has no shape keys",
|
|
||||||
"VisemePanel.error.selectMesh": "Select a mesh to create visemes",
|
|
||||||
"VisemePanel.info.selectMesh": "Select a mesh to create visemes",
|
|
||||||
"VisemePanel.label": "Visemes",
|
|
||||||
"VisemePanel.mixing_shape": "Mixing shape: {shape_name} with value: {value}",
|
|
||||||
"VisemePanel.mouth_a.desc": "The shapekey for the 'A' mouth shape",
|
|
||||||
"VisemePanel.mouth_a.label": "Mouth A",
|
|
||||||
"VisemePanel.mouth_ch.desc": "The shapekey for the 'CH' mouth shape",
|
|
||||||
"VisemePanel.mouth_ch.label": "Mouth CH",
|
|
||||||
"VisemePanel.mouth_o.desc": "The shapekey for the 'O' mouth shape",
|
|
||||||
"VisemePanel.mouth_o.label": "Mouth O",
|
|
||||||
"VisemePanel.removing_existing_viseme": "Removing existing viseme: {viseme_name}",
|
|
||||||
"VisemePanel.removing_existing_visemes": "Removing existing visemes...",
|
|
||||||
"VisemePanel.select_mesh": "Select Mesh",
|
|
||||||
"VisemePanel.selected_mesh.label": "Selected Mesh",
|
|
||||||
"VisemePanel.selected_mesh.desc": "The currently selected mesh for viseme operations",
|
|
||||||
"VisemePanel.selected_shapes": "Selected shapes: A={shape_a}, O={shape_o}, CH={shape_ch}",
|
|
||||||
"VisemePanel.shape_intensity": "Shape Intensity",
|
|
||||||
"VisemePanel.shape_intensity_desc": "The intensity of the viseme shapekeys",
|
|
||||||
"VisemePanel.sorting_shapekeys": "Sorting shape keys...",
|
|
||||||
"VisemePanel.start_viseme_creation": "Starting viseme creation...",
|
|
||||||
"VisemePanel.viseme_created_successfully": "Viseme {viseme_name} created successfully",
|
|
||||||
"VisemePanel.viseme_creation_completed": "Viseme creation completed.",
|
|
||||||
"MMDOptions.title": "MMD Options",
|
|
||||||
"MMDOptions.no_armature_selected": "No armature selected",
|
|
||||||
"MMDOptions.label": "MMD Options",
|
|
||||||
"MMDOptions.cleanup_mesh.label": "Cleanup Mesh",
|
|
||||||
"MMDOptions.cleanup_mesh.desc": "Clean up the mesh by removing empty objects, unused vertex groups, unused vertices, and empty shape keys",
|
|
||||||
"MMDOptions.removing_empty_objects": "Removing empty objects",
|
|
||||||
"MMDOptions.removing_unused_vertex_groups": "Removing unused vertex groups",
|
|
||||||
"MMDOptions.removing_unused_vertices": "Removing unused vertices",
|
|
||||||
"MMDOptions.removing_empty_shape_keys": "Removing empty shape keys",
|
|
||||||
"MMDOptions.optimize_weights.label": "Optimize Weights",
|
|
||||||
"MMDOptions.optimize_weights.desc": "Optimize vertex weights by limiting the number of weights per vertex",
|
|
||||||
"MMDOptions.max_weights.label": "Max Weights",
|
|
||||||
"MMDOptions.max_weights.desc": "Maximum number of weights per vertex",
|
|
||||||
"MMDOptions.merging_weights": "Merging weights",
|
|
||||||
"MMDOptions.removing_zero_weight_bones": "Removing zero weight bones",
|
|
||||||
"MMDOptions.limiting_vertex_weights": "Limiting vertex weights",
|
|
||||||
"MMDOptions.weight_optimization_complete": "Weight optimization complete",
|
|
||||||
"MMDOptions.optimize_armature.label": "Optimize Armature",
|
|
||||||
"MMDOptions.optimize_armature.desc": "Optimize the armature by fixing bone rolls, aligning bones, connecting bones, and more",
|
|
||||||
"MMDOptions.fixing_bone_rolls": "Fixing bone rolls",
|
|
||||||
"MMDOptions.aligning_bones": "Aligning bones",
|
|
||||||
"MMDOptions.connecting_bones": "Connecting bones",
|
|
||||||
"MMDOptions.deleting_bone_constraints": "Deleting bone constraints",
|
|
||||||
"MMDOptions.merging_bones_to_parents": "Merging bones to parents",
|
|
||||||
"MMDOptions.reordering_bones": "Reordering bones",
|
|
||||||
"MMDOptions.fixing_armature_names": "Fixing armature names",
|
|
||||||
"MMDOptions.renaming_bones": "Renaming bones",
|
|
||||||
"MMDOptions.armature_optimization_complete": "Armature optimization complete",
|
|
||||||
"MMDOptions.convert_materials.label": "Convert Materials",
|
|
||||||
"MMDOptions.convert_materials.desc": "Convert materials to use Principled BSDF shader and fix MMD and VRM shaders",
|
|
||||||
"MMDOptions.converting_materials": "Converting materials for {name}",
|
|
||||||
"Updater.label": "Updater",
|
"Updater.label": "Updater",
|
||||||
"Updater.CheckForUpdateButton.label": "Check for Updates",
|
"Updater.CheckForUpdateButton.label": "Check for Updates",
|
||||||
"Updater.CheckForUpdateButton.label_alt": "No Updates Available",
|
"Updater.CheckForUpdateButton.label_alt": "No Updates Available",
|
||||||
@@ -276,25 +24,37 @@
|
|||||||
"download_file.cantConnect": "Cannot connect to update server",
|
"download_file.cantConnect": "Cannot connect to update server",
|
||||||
"download_file.cantFindZip": "Update file not found",
|
"download_file.cantFindZip": "Update file not found",
|
||||||
"download_file.cantFindAvatarToolkit": "Avatar Toolkit files not found in update package",
|
"download_file.cantFindAvatarToolkit": "Avatar Toolkit files not found in update package",
|
||||||
"CreditsSupport.label": "Credits & Support",
|
|
||||||
"CreditsSupport.credits_title": "Credits",
|
"QuickAccess.label": "Quick Access",
|
||||||
"CreditsSupport.credits_text1": "Avatar Toolkit has been created by the Neoneko team:",
|
"QuickAccess.select_armature": "Select Armature",
|
||||||
"CreditsSupport.credits_text2": "Yusarina and 989Onan",
|
"QuickAccess.valid_armature": "Valid Armature",
|
||||||
"CreditsSupport.credits_text3": "Some code has been inspired by Cats Blender Plugin,",
|
"QuickAccess.bones_count": "Bones: {count}",
|
||||||
"CreditsSupport.credits_text4": "thanks to the original contributors to that plugin.",
|
"QuickAccess.pose_bones_available": "Pose bones: Available",
|
||||||
"CreditsSupport.support_text1": "If you like what we do, you can donate/ tip to us",
|
"QuickAccess.pose_controls": "Pose Controls",
|
||||||
"CreditsSupport.support_text2": "through our pally.gg page.",
|
"QuickAccess.import_export": "Import/Export",
|
||||||
"CreditsSupport.support_title": "Support Us",
|
"QuickAccess.import": "Import",
|
||||||
"CreditsSupport.support_button": "Support Us",
|
"QuickAccess.export": "Export",
|
||||||
"CreditsSupport.help_title": "Need Help?",
|
"QuickAccess.export_fbx": "Export FBX",
|
||||||
"CreditsSupport.help_text1": "Check out our wiki first, we HIGHLY encourage",
|
"QuickAccess.export_resonite": "Export to Resonite",
|
||||||
"CreditsSupport.help_text2": "that you read it before seeking further support.",
|
|
||||||
"CreditsSupport.wiki_button": "Wiki",
|
"Quick_Access.start_pose_mode.label": "Start Pose Mode",
|
||||||
"CreditsSupport.discord_button": "Join Discord",
|
"Quick_Access.start_pose_mode.desc": "Enter pose mode for the selected armature",
|
||||||
"TextureAtlas.include_in_atlas": "Include in Atlas",
|
"Quick_Access.stop_pose_mode.label": "Stop Pose Mode",
|
||||||
"TextureAtlas.include_in_atlas_desc": "Include this material in the texture atlas",
|
"Quick_Access.stop_pose_mode.desc": "Exit pose mode and clear transforms",
|
||||||
|
"Quick_Access.apply_pose_as_shapekey.label": "Apply Pose as Shape Key",
|
||||||
|
"Quick_Access.apply_pose_as_shapekey.desc": "Create a new shape key from current pose",
|
||||||
|
"Quick_Access.apply_pose_as_rest.label": "Apply Pose as Rest",
|
||||||
|
"Quick_Access.apply_pose_as_rest.desc": "Apply current pose as rest pose",
|
||||||
|
"Quick_Access.apply_armature_failed": "Failed to apply armature modifications",
|
||||||
|
|
||||||
|
"Tools.apply_pose_as_rest.success": "Successfully applied pose as rest position",
|
||||||
|
|
||||||
|
"Armature.validation.no_armature": "No armature selected",
|
||||||
|
"Armature.validation.not_armature": "Selected object is not an armature",
|
||||||
|
"Armature.validation.no_bones": "Armature has no bones",
|
||||||
|
"Armature.validation.missing_bone": "Missing essential bone: {bone}",
|
||||||
|
|
||||||
"Scene.avatar_toolkit_updater_version_list.name": "Version List",
|
"Scene.avatar_toolkit_updater_version_list.name": "Version List",
|
||||||
"Scene.avatar_toolkit_updater_version_list.description": "List of available versions to update to",
|
"Scene.avatar_toolkit_updater_version_list.description": "List of available versions"
|
||||||
"TextureAtlas.no_materials_selected": "No materials selected for atlas"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,182 +0,0 @@
|
|||||||
from bpy.types import UIList, Panel, UILayout, Object, Context, Material, Operator
|
|
||||||
import bpy
|
|
||||||
from math import sqrt
|
|
||||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
|
||||||
from ..core.common import SceneMatClass, MaterialListBool, get_selected_armature
|
|
||||||
from ..functions.atlas_materials import AvatarToolKit_OT_AtlasMaterials
|
|
||||||
from ..core.translations import t
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_SelectAllMaterials(Operator):
|
|
||||||
bl_idname = 'avatar_toolkit.select_all_materials'
|
|
||||||
bl_label = "Select All"
|
|
||||||
bl_description = "Select all materials for atlas"
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
for item in context.scene.avatar_toolkit.materials:
|
|
||||||
item.mat.avatar_toolkit.include_in_atlas = True
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_SelectNoneMaterials(Operator):
|
|
||||||
bl_idname = 'avatar_toolkit.select_none_materials'
|
|
||||||
bl_label = "Select None"
|
|
||||||
bl_description = "Deselect all materials"
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
for item in context.scene.avatar_toolkit.materials:
|
|
||||||
item.mat.avatar_toolkit.include_in_atlas = False
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_ExpandAllMaterials(Operator):
|
|
||||||
bl_idname = 'avatar_toolkit.expand_all_materials'
|
|
||||||
bl_label = "Expand All"
|
|
||||||
bl_description = "Expand all material settings"
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
for item in context.scene.avatar_toolkit.materials:
|
|
||||||
item.mat.avatar_toolkit.material_expanded = True
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_CollapseAllMaterials(Operator):
|
|
||||||
bl_idname = 'avatar_toolkit.collapse_all_materials'
|
|
||||||
bl_label = "Collapse All"
|
|
||||||
bl_description = "Collapse all material settings"
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
for item in context.scene.avatar_toolkit.materials:
|
|
||||||
item.mat.avatar_toolkit.material_expanded = False
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_ExpandSectionMaterials(Operator):
|
|
||||||
bl_idname = 'avatar_toolkit.expand_section_materials'
|
|
||||||
bl_label = ""
|
|
||||||
bl_description = ""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context: Context) -> bool:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> set:
|
|
||||||
if not context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown:
|
|
||||||
context.scene.avatar_toolkit.materials.clear()
|
|
||||||
newlist: list[Material] = []
|
|
||||||
for obj in context.scene.objects:
|
|
||||||
if len(obj.material_slots)>0:
|
|
||||||
for mat_slot in obj.material_slots:
|
|
||||||
if mat_slot.material:
|
|
||||||
if mat_slot.material not in newlist:
|
|
||||||
newlist.append(mat_slot.material)
|
|
||||||
newitem: SceneMatClass = context.scene.avatar_toolkit.materials.add()
|
|
||||||
newitem.mat = mat_slot.material
|
|
||||||
MaterialListBool.old_list[context.scene.name] = newlist
|
|
||||||
else:
|
|
||||||
context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown = False
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
class AvatarToolKit_UL_MaterialTextureAtlasProperties(UIList):
|
|
||||||
bl_label = t("TextureAtlas.material_list_label")
|
|
||||||
bl_idname = "Material_UL_avatar_toolkit_texture_atlas_mat_list_mat"
|
|
||||||
bl_space_type = 'VIEW_3D'
|
|
||||||
bl_region_type = 'UI'
|
|
||||||
|
|
||||||
def draw_header(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
row = layout.row(align=True)
|
|
||||||
|
|
||||||
row.operator("avatar_toolkit.select_all_materials", text="", icon='CHECKBOX_HLT')
|
|
||||||
row.operator("avatar_toolkit.select_none_materials", text="", icon='CHECKBOX_DEHLT')
|
|
||||||
row.operator("avatar_toolkit.expand_all_materials", text="", icon='DISCLOSURE_TRI_DOWN')
|
|
||||||
row.operator("avatar_toolkit.collapse_all_materials", text="", icon='DISCLOSURE_TRI_RIGHT')
|
|
||||||
row.prop(context.scene.avatar_toolkit, "material_search_filter", text="", icon='VIEWZOOM')
|
|
||||||
|
|
||||||
box = layout.box()
|
|
||||||
row = box.row()
|
|
||||||
row.label(text=f"Estimated Atlas Size: {self.calculate_atlas_size(context)}px")
|
|
||||||
|
|
||||||
def draw_item(self, context: Context, layout: UILayout, data: Object, item: SceneMatClass, icon, active_data, active_propname, index):
|
|
||||||
if context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown:
|
|
||||||
if context.scene.avatar_toolkit.material_search_filter and context.scene.avatar_toolkit.material_search_filter.lower() not in item.mat.name.lower():
|
|
||||||
return
|
|
||||||
|
|
||||||
row = layout.row()
|
|
||||||
|
|
||||||
row.prop(item.mat.avatar_toolkit, "include_in_atlas", text="", icon='CHECKBOX_HLT' if item.mat.avatar_toolkit.include_in_atlas else 'CHECKBOX_DEHLT')
|
|
||||||
|
|
||||||
row.prop(item.mat.avatar_toolkit, "material_expanded",
|
|
||||||
text=item.mat.name,
|
|
||||||
icon='DOWNARROW_HLT' if item.mat.avatar_toolkit.material_expanded else 'RIGHTARROW',
|
|
||||||
emboss=False)
|
|
||||||
|
|
||||||
if item.mat.avatar_toolkit.material_expanded and item.mat.avatar_toolkit.include_in_atlas:
|
|
||||||
box = layout.box()
|
|
||||||
col = box.column(align=True)
|
|
||||||
self.draw_texture_row(col, item.mat.avatar_toolkit, "texture_atlas_albedo", "IMAGE_RGB")
|
|
||||||
self.draw_texture_row(col, item.mat.avatar_toolkit, "texture_atlas_normal", "NORMALS_FACE")
|
|
||||||
self.draw_texture_row(col, item.mat.avatar_toolkit, "texture_atlas_emission", "LIGHT")
|
|
||||||
self.draw_texture_row(col, item.mat.avatar_toolkit, "texture_atlas_ambient_occlusion", "SHADING_SOLID")
|
|
||||||
self.draw_texture_row(col, item.mat.avatar_toolkit, "texture_atlas_height", "IMAGE_ZDEPTH")
|
|
||||||
self.draw_texture_row(col, item.mat.avatar_toolkit, "texture_atlas_roughness", "MATERIAL")
|
|
||||||
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
|
|
||||||
def draw_texture_row(self, layout, material, prop_name, icon):
|
|
||||||
row = layout.row()
|
|
||||||
row.prop(material, prop_name, icon=icon)
|
|
||||||
if getattr(material, prop_name):
|
|
||||||
row.label(text="", icon='CHECKMARK')
|
|
||||||
else:
|
|
||||||
row.label(text="", icon='X')
|
|
||||||
|
|
||||||
def calculate_atlas_size(self, context):
|
|
||||||
total_size = 0
|
|
||||||
for mat in context.scene.avatar_toolkit.materials:
|
|
||||||
if mat.mat.avatar_toolkit.include_in_atlas:
|
|
||||||
if mat.mat.avatar_toolkit.texture_atlas_albedo:
|
|
||||||
img = bpy.data.images[mat.mat.avatar_toolkit.texture_atlas_albedo]
|
|
||||||
total_size += img.size[0] * img.size[1]
|
|
||||||
return f"{int(sqrt(total_size))}x{int(sqrt(total_size))}"
|
|
||||||
|
|
||||||
class AvatarToolKit_PT_TextureAtlasPanel(Panel):
|
|
||||||
bl_label = t("TextureAtlas.label")
|
|
||||||
bl_idname = "OBJECT_PT_avatar_toolkit_texture_atlas"
|
|
||||||
bl_space_type = 'VIEW_3D'
|
|
||||||
bl_region_type = 'UI'
|
|
||||||
bl_category = CATEGORY_NAME
|
|
||||||
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
|
||||||
bl_order = 6
|
|
||||||
|
|
||||||
def draw(self, context: Context):
|
|
||||||
layout = self.layout
|
|
||||||
armature = get_selected_armature(context)
|
|
||||||
|
|
||||||
if armature:
|
|
||||||
layout.label(text=t("TextureAtlas.label"), icon='TEXTURE')
|
|
||||||
layout.separator(factor=0.5)
|
|
||||||
|
|
||||||
box = layout.box()
|
|
||||||
row = box.row()
|
|
||||||
direction_icon = 'RIGHTARROW' if not context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown else 'DOWNARROW_HLT'
|
|
||||||
row.operator(AvatarToolKit_OT_ExpandSectionMaterials.bl_idname,
|
|
||||||
text=(t("TextureAtlas.reload_list") if not context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown else t("TextureAtlas.loaded_list")),
|
|
||||||
icon=direction_icon)
|
|
||||||
|
|
||||||
if context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown:
|
|
||||||
row = box.row()
|
|
||||||
row.template_list(AvatarToolKit_UL_MaterialTextureAtlasProperties.bl_idname,
|
|
||||||
'material_list',
|
|
||||||
context.scene.avatar_toolkit,
|
|
||||||
'materials',
|
|
||||||
context.scene.avatar_toolkit,
|
|
||||||
'texture_atlas_material_index',
|
|
||||||
rows=12,
|
|
||||||
type='DEFAULT')
|
|
||||||
|
|
||||||
layout.separator(factor=1.0)
|
|
||||||
|
|
||||||
row = layout.row()
|
|
||||||
row.scale_y = 1.5
|
|
||||||
row.operator(AvatarToolKit_OT_AtlasMaterials.bl_idname,
|
|
||||||
text=t("TextureAtlas.atlas_materials"),
|
|
||||||
icon='NODE_TEXTURE')
|
|
||||||
else:
|
|
||||||
layout.label(text=t("Tools.select_armature"), icon='ERROR')
|
|
||||||
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import bpy
|
|
||||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
|
||||||
from ..core.translations import t
|
|
||||||
from ..core.common import open_web_after_delay_multi_threaded
|
|
||||||
|
|
||||||
class AvatarToolkit_PT_CreditsSupport(bpy.types.Panel):
|
|
||||||
bl_label = t("CreditsSupport.label")
|
|
||||||
bl_idname = "OBJECT_PT_avatar_toolkit_credits_support"
|
|
||||||
bl_space_type = 'VIEW_3D'
|
|
||||||
bl_region_type = 'UI'
|
|
||||||
bl_category = CATEGORY_NAME
|
|
||||||
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
|
||||||
bl_order = 10
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
|
|
||||||
layout.label(text=t("CreditsSupport.credits_title"))
|
|
||||||
box = layout.box()
|
|
||||||
column = box.column(align=True)
|
|
||||||
column.scale_y = 0.7
|
|
||||||
column.label(text=t("CreditsSupport.credits_text1"))
|
|
||||||
column.label(text=t("CreditsSupport.credits_text2"))
|
|
||||||
column.label(text=t("CreditsSupport.credits_text3"))
|
|
||||||
column.label(text=t("CreditsSupport.credits_text4"))
|
|
||||||
|
|
||||||
layout.separator()
|
|
||||||
|
|
||||||
layout.label(text=t("CreditsSupport.support_title"))
|
|
||||||
box = layout.box()
|
|
||||||
column = box.column(align=True)
|
|
||||||
column.scale_y = 0.7
|
|
||||||
column.label(text=t("CreditsSupport.support_text1"))
|
|
||||||
column.label(text=t("CreditsSupport.support_text2"))
|
|
||||||
row = column.row()
|
|
||||||
row.scale_y = 1.5
|
|
||||||
row.operator("wm.url_open", text=t("CreditsSupport.support_button")).url = "https://neoneko.xyz/supportus.html"
|
|
||||||
|
|
||||||
layout.separator()
|
|
||||||
|
|
||||||
layout.label(text=t("CreditsSupport.help_title"))
|
|
||||||
box = layout.box()
|
|
||||||
column = box.column(align=True)
|
|
||||||
column.scale_y = 0.7
|
|
||||||
column.label(text=t("CreditsSupport.help_text1"))
|
|
||||||
column.label(text=t("CreditsSupport.help_text2"))
|
|
||||||
row = column.row()
|
|
||||||
row.scale_y = 1.5
|
|
||||||
row.operator("wm.url_open", text=t("CreditsSupport.wiki_button")).url = "https://github.com/teamneoneko/Avatar-Toolkit"
|
|
||||||
row = column.row()
|
|
||||||
row.scale_y = 1.5
|
|
||||||
row.operator("wm.url_open", text=t("CreditsSupport.discord_button")).url = "https://discord.catsblenderplugin.xyz"
|
|
||||||
|
|
||||||
+26
-9
@@ -1,21 +1,38 @@
|
|||||||
import bpy
|
import bpy
|
||||||
|
from typing import Optional
|
||||||
|
from bpy.types import Panel, Context, UILayout
|
||||||
from ..core.translations import t
|
from ..core.translations import t
|
||||||
|
|
||||||
CATEGORY_NAME = "Avatar Toolkit"
|
CATEGORY_NAME: str = "Avatar Toolkit"
|
||||||
|
|
||||||
def draw_title(self: bpy.types.Panel):
|
def draw_title(self: Panel) -> None:
|
||||||
layout = self.layout
|
"""Draw the main panel title and description"""
|
||||||
layout.label(text=t("AvatarToolkit.desc1"))
|
layout: UILayout = self.layout
|
||||||
layout.label(text=t("AvatarToolkit.desc2"))
|
box: UILayout = layout.box()
|
||||||
layout.label(text=t("AvatarToolkit.desc3"))
|
col: UILayout = box.column(align=True)
|
||||||
|
|
||||||
class AvatarToolKit_PT_AvatarToolkitPanel(bpy.types.Panel):
|
# Add a nice header
|
||||||
|
row: UILayout = col.row()
|
||||||
|
row.scale_y = 1.2
|
||||||
|
row.label(text=t("AvatarToolkit.label"), icon='ARMATURE_DATA')
|
||||||
|
|
||||||
|
# Description as a flowing paragraph
|
||||||
|
desc_col: UILayout = col.column()
|
||||||
|
desc_col.scale_y = 0.6
|
||||||
|
desc_col.label(text=t("AvatarToolkit.desc1"))
|
||||||
|
desc_col.label(text=t("AvatarToolkit.desc2"))
|
||||||
|
desc_col.label(text=t("AvatarToolkit.desc3"))
|
||||||
|
col.separator()
|
||||||
|
|
||||||
|
class AvatarToolKit_PT_AvatarToolkitPanel(Panel):
|
||||||
|
"""Main panel for Avatar Toolkit containing general information and settings"""
|
||||||
bl_label = t("AvatarToolkit.label")
|
bl_label = t("AvatarToolkit.label")
|
||||||
bl_idname = "OBJECT_PT_avatar_toolkit"
|
bl_idname = "OBJECT_PT_avatar_toolkit"
|
||||||
bl_space_type = 'VIEW_3D'
|
bl_space_type = 'VIEW_3D'
|
||||||
bl_region_type = 'UI'
|
bl_region_type = 'UI'
|
||||||
bl_category = CATEGORY_NAME
|
bl_category = CATEGORY_NAME
|
||||||
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
def draw(self: bpy.types.Panel, context: bpy.types.Context):
|
def draw(self, context: Context) -> None:
|
||||||
|
"""Draw the main panel layout"""
|
||||||
draw_title(self)
|
draw_title(self)
|
||||||
|
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import bpy
|
|
||||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
|
||||||
from bpy.types import Panel, Context
|
|
||||||
from ..core.common import get_selected_armature
|
|
||||||
from ..core.translations import t
|
|
||||||
from ..functions.armature_modifying import AvatarToolkit_OT_MergeArmatures
|
|
||||||
|
|
||||||
class AvatarToolkit_PT_MergeArmaturesPanel(Panel):
|
|
||||||
bl_label = t("MergeArmatures.label")
|
|
||||||
bl_idname = "OBJECT_PT_avatar_toolkit_merge_armatures"
|
|
||||||
bl_space_type = 'VIEW_3D'
|
|
||||||
bl_region_type = 'UI'
|
|
||||||
bl_category = CATEGORY_NAME
|
|
||||||
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
|
||||||
bl_order = 5
|
|
||||||
|
|
||||||
def draw(self, context: Context):
|
|
||||||
layout = self.layout
|
|
||||||
armature = get_selected_armature(context)
|
|
||||||
|
|
||||||
if armature:
|
|
||||||
layout.label(text=t("MergeArmatures.title.label"), icon='ARMATURE_DATA')
|
|
||||||
|
|
||||||
layout.separator(factor=0.5)
|
|
||||||
|
|
||||||
box = layout.box()
|
|
||||||
col = box.column(align=True)
|
|
||||||
|
|
||||||
col.prop(context.scene.avatar_toolkit, "selected_armature", text=t("MergeArmatures.target_armature.label"), icon="ARMATURE_DATA")
|
|
||||||
col.prop(context.scene.avatar_toolkit, "merge_armature_source", icon="OUTLINER_OB_ARMATURE")
|
|
||||||
|
|
||||||
layout.separator(factor=0.5)
|
|
||||||
|
|
||||||
col = layout.column(align=True)
|
|
||||||
col.prop(context.scene.avatar_toolkit, "merge_armature_align_bones", icon="BONE_DATA")
|
|
||||||
col.prop(context.scene.avatar_toolkit, "merge_armature_apply_transforms", icon="OBJECT_ORIGIN")
|
|
||||||
|
|
||||||
layout.separator(factor=1.0)
|
|
||||||
|
|
||||||
row = layout.row()
|
|
||||||
row.scale_y = 1.5
|
|
||||||
row.operator(operator=AvatarToolkit_OT_MergeArmatures.bl_idname, icon="ARMATURE_DATA")
|
|
||||||
|
|
||||||
else:
|
|
||||||
layout.label(text=t("MergeArmatures.select_armature"), icon='ERROR')
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import bpy
|
|
||||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
|
||||||
from ..core.translations import t
|
|
||||||
from ..functions.mmd_functions import *
|
|
||||||
from ..functions.mesh_tools import AvatarToolKit_OT_JoinAllMeshes
|
|
||||||
from ..functions.combine_materials import AvatarToolKit_OT_CombineMaterials
|
|
||||||
from ..functions.additional_tools import AvatarToolKit_OT_ApplyTransforms
|
|
||||||
|
|
||||||
class AvatarToolkit_PT_MMDOptionsPanel(bpy.types.Panel):
|
|
||||||
bl_label = t("MMDOptions.label")
|
|
||||||
bl_idname = "OBJECT_PT_avatar_toolkit_mmd_options"
|
|
||||||
bl_space_type = 'VIEW_3D'
|
|
||||||
bl_region_type = 'UI'
|
|
||||||
bl_category = CATEGORY_NAME
|
|
||||||
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
|
||||||
bl_order = 4
|
|
||||||
|
|
||||||
def draw(self, context: bpy.types.Context) -> None:
|
|
||||||
layout = self.layout
|
|
||||||
|
|
||||||
layout.label(text=t("MMDOptions.title"), icon='OUTLINER_OB_ARMATURE')
|
|
||||||
|
|
||||||
layout.separator(factor=0.5)
|
|
||||||
|
|
||||||
col = layout.column(align=True)
|
|
||||||
col.scale_y = 1.2
|
|
||||||
col.operator(AvatarToolKit_OT_CleanupMesh.bl_idname, icon='BRUSH_DATA')
|
|
||||||
col.operator(AvatarToolKit_OT_JoinAllMeshes.bl_idname, icon='OBJECT_DATAMODE')
|
|
||||||
|
|
||||||
layout.separator(factor=0.5)
|
|
||||||
|
|
||||||
col = layout.column(align=True)
|
|
||||||
col.scale_y = 1.2
|
|
||||||
col.operator(AvatarToolKit_OT_OptimizeWeights.bl_idname, icon='MOD_VERTEX_WEIGHT')
|
|
||||||
col.operator(AvatarToolKit_OT_OptimizeArmature.bl_idname, icon='ARMATURE_DATA')
|
|
||||||
|
|
||||||
layout.separator(factor=0.5)
|
|
||||||
|
|
||||||
row = layout.row(align=True)
|
|
||||||
row.scale_y = 1.2
|
|
||||||
row.operator(AvatarToolKit_OT_ApplyTransforms.bl_idname, icon='OBJECT_ORIGIN')
|
|
||||||
row.operator(AvatarToolKit_OT_CombineMaterials.bl_idname, icon='MATERIAL')
|
|
||||||
|
|
||||||
layout.separator(factor=0.5)
|
|
||||||
|
|
||||||
row = layout.row()
|
|
||||||
row.scale_y = 1.2
|
|
||||||
row.operator(AvatarToolKit_OT_ConvertMaterials.bl_idname, icon='SHADING_TEXTURE')
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import bpy
|
|
||||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
|
||||||
from ..core.translations import t
|
|
||||||
from ..functions.remove_doubles_safely import AvatarToolKit_OT_RemoveDoublesSafely, AvatarToolKit_OT_RemoveDoublesSafelyAdvanced
|
|
||||||
from ..core.common import get_selected_armature
|
|
||||||
from ..functions.mesh_tools import AvatarToolKit_OT_JoinAllMeshes, AvatarToolKit_OT_JoinSelectedMeshes
|
|
||||||
from ..functions.combine_materials import AvatarToolKit_OT_CombineMaterials
|
|
||||||
|
|
||||||
class AvatarToolkit_PT_OptimizationPanel(bpy.types.Panel):
|
|
||||||
bl_label = t("Optimization.label")
|
|
||||||
bl_idname = "OBJECT_PT_avatar_toolkit_optimization"
|
|
||||||
bl_space_type = 'VIEW_3D'
|
|
||||||
bl_region_type = 'UI'
|
|
||||||
bl_category = CATEGORY_NAME
|
|
||||||
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
|
||||||
bl_order = 2
|
|
||||||
|
|
||||||
def draw(self, context: bpy.types.Context):
|
|
||||||
layout = self.layout
|
|
||||||
armature = get_selected_armature(context)
|
|
||||||
|
|
||||||
if armature:
|
|
||||||
layout.label(text=t("Optimization.options.label"), icon='SETTINGS')
|
|
||||||
|
|
||||||
layout.separator(factor=0.5)
|
|
||||||
|
|
||||||
row = layout.row(align=True)
|
|
||||||
row.scale_y = 1.2
|
|
||||||
row.operator(AvatarToolKit_OT_CombineMaterials.bl_idname, text=t("Optimization.combine_materials.label"), icon='MATERIAL')
|
|
||||||
|
|
||||||
layout.separator(factor=0.5)
|
|
||||||
|
|
||||||
row = layout.row(align=True)
|
|
||||||
row.scale_y = 1.2
|
|
||||||
row.operator(AvatarToolKit_OT_RemoveDoublesSafely.bl_idname, text=t("Optimization.remove_doubles_safely.label"), icon='SNAP_VERTEX')
|
|
||||||
row.operator(AvatarToolKit_OT_RemoveDoublesSafelyAdvanced.bl_idname, text=t("Optimization.remove_doubles_safely_advanced.label"), icon="ACTION")
|
|
||||||
layout.separator(factor=1.0)
|
|
||||||
|
|
||||||
layout.label(text=t("Optimization.joinmeshes.label"), icon='OBJECT_DATA')
|
|
||||||
row = layout.row(align=True)
|
|
||||||
row.scale_y = 1.2
|
|
||||||
row.operator(AvatarToolKit_OT_JoinAllMeshes.bl_idname, text=t("Optimization.join_all_meshes.label"), icon='OUTLINER_OB_MESH')
|
|
||||||
row.operator(AvatarToolKit_OT_JoinSelectedMeshes.bl_idname, text=t("Optimization.join_selected_meshes.label"), icon='STICKY_UVS_LOC')
|
|
||||||
|
|
||||||
else:
|
|
||||||
layout.label(text=t("Optimization.select_armature"), icon='ERROR')
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import bpy
|
|
||||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
|
||||||
from ..core.exporters.export_resonite import AvatarToolKit_OT_ExportResonite
|
|
||||||
from bpy.types import Context, Mesh, Panel, Operator
|
|
||||||
from ..core.translations import t
|
|
||||||
from ..core.common import get_selected_armature
|
|
||||||
from ..functions.import_anything import AvatarToolKit_OT_ImportAnyModel
|
|
||||||
from ..functions.armature_modifying import (AvatarToolkit_OT_StartPoseMode,
|
|
||||||
AvatarToolkit_OT_StopPoseMode,
|
|
||||||
AvatarToolkit_OT_ApplyPoseAsRest,
|
|
||||||
AvatarToolkit_OT_ApplyPoseAsShapekey)
|
|
||||||
|
|
||||||
class AvatarToolkitQuickAccessPanel(Panel):
|
|
||||||
bl_label = t("Quick_Access.label")
|
|
||||||
bl_idname = "OBJECT_PT_avatar_toolkit_quick_access"
|
|
||||||
bl_space_type = 'VIEW_3D'
|
|
||||||
bl_region_type = 'UI'
|
|
||||||
bl_category = CATEGORY_NAME
|
|
||||||
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
|
||||||
bl_order = 1
|
|
||||||
|
|
||||||
def draw(self, context: Context):
|
|
||||||
layout = self.layout
|
|
||||||
layout.label(text=t("Quick_Access.options"), icon='TOOL_SETTINGS')
|
|
||||||
|
|
||||||
layout.separator(factor=1.0)
|
|
||||||
|
|
||||||
layout.label(text=t("Quick_Access.select_armature"), icon='ARMATURE_DATA')
|
|
||||||
layout.prop(context.scene.avatar_toolkit, "selected_armature", text="")
|
|
||||||
|
|
||||||
layout.separator(factor=1.0)
|
|
||||||
|
|
||||||
layout.label(text=t("Quick_Access.import_export.label"), icon='IMPORT')
|
|
||||||
|
|
||||||
row = layout.row(align=True)
|
|
||||||
row.scale_y = 1.5
|
|
||||||
row.operator(AvatarToolKit_OT_ImportAnyModel.bl_idname, text=t("Quick_Access.import"), icon='IMPORT')
|
|
||||||
row.operator(AVATAR_TOOLKIT_OT_ExportMenu.bl_idname, text=t("Quick_Access.export"), icon='EXPORT')
|
|
||||||
|
|
||||||
layout.separator(factor=1.0)
|
|
||||||
|
|
||||||
if get_selected_armature(context) != None:
|
|
||||||
if(context.mode == "POSE"):
|
|
||||||
col = layout.column(align=True)
|
|
||||||
col.scale_y = 1.2
|
|
||||||
col.operator(AvatarToolkit_OT_StopPoseMode.bl_idname, text=t("Quick_Access.stop_pose_mode.label"), icon='POSE_HLT')
|
|
||||||
|
|
||||||
layout.separator(factor=0.5)
|
|
||||||
|
|
||||||
col = layout.column(align=True)
|
|
||||||
col.scale_y = 1.2
|
|
||||||
col.operator(AvatarToolkit_OT_ApplyPoseAsRest.bl_idname, text=t("Quick_Access.apply_pose_as_rest.label"), icon='MOD_ARMATURE')
|
|
||||||
col.operator(AvatarToolkit_OT_ApplyPoseAsShapekey.bl_idname, text=t("Quick_Access.apply_pose_as_shapekey.label"), icon='MOD_ARMATURE')
|
|
||||||
else:
|
|
||||||
row = layout.row()
|
|
||||||
row.scale_y = 1.2
|
|
||||||
row.operator(AvatarToolkit_OT_StartPoseMode.bl_idname, text=t("Quick_Access.start_pose_mode.label"), icon='POSE_HLT')
|
|
||||||
|
|
||||||
class AVATAR_TOOLKIT_OT_ExportMenu(bpy.types.Operator):
|
|
||||||
bl_idname = "avatar_toolkit.export_menu"
|
|
||||||
bl_label = t("Quick_Access.export_menu.label")
|
|
||||||
bl_description = t("Quick_Access.export_menu.desc")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
return any(obj.type == 'MESH' for obj in context.scene.objects)
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def invoke(self, context: Context, event):
|
|
||||||
wm = context.window_manager
|
|
||||||
return wm.invoke_popup(self, width=200)
|
|
||||||
|
|
||||||
def draw(self, context: Context):
|
|
||||||
layout = self.layout
|
|
||||||
layout.label(text=t("Quick_Access.select_export.label"), icon='EXPORT')
|
|
||||||
layout.operator(AvatarToolKit_OT_ExportResonite.bl_idname, text=t("Quick_Access.select_export_resonite.label"), icon='SCENE_DATA')
|
|
||||||
layout.operator(AVATAR_TOOLKIT_OT_ExportFbx.bl_idname, text=t("Quick_Access.export_fbx.label"), icon='OBJECT_DATA')
|
|
||||||
|
|
||||||
class AVATAR_TOOLKIT_OT_ExportFbx(bpy.types.Operator):
|
|
||||||
bl_idname = 'avatar_toolkit.export_fbx'
|
|
||||||
bl_label = t("Quick_Access.export_fbx.label")
|
|
||||||
bl_description = t("Quick_Access.export_fbx.desc")
|
|
||||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
|
||||||
|
|
||||||
def execute(self, context) -> set[str]:
|
|
||||||
bpy.ops.export_scene.fbx('INVOKE_DEFAULT')
|
|
||||||
return {'FINISHED'}
|
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import bpy
|
||||||
|
from typing import Set, Optional, List, Tuple
|
||||||
|
from bpy.types import Operator, Panel, Menu, Context, UILayout
|
||||||
|
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
||||||
|
from ..core.translations import t
|
||||||
|
from ..core.common import (
|
||||||
|
get_active_armature,
|
||||||
|
clear_default_objects,
|
||||||
|
validate_armature,
|
||||||
|
get_armature_list,
|
||||||
|
get_armature_stats
|
||||||
|
)
|
||||||
|
from ..core.importers.importer import import_types, imports
|
||||||
|
from ..functions.pose_mode import (
|
||||||
|
AvatarToolkit_OT_StartPoseMode,
|
||||||
|
AvatarToolkit_OT_StopPoseMode,
|
||||||
|
AvatarToolkit_OT_ApplyPoseAsShapekey,
|
||||||
|
AvatarToolkit_OT_ApplyPoseAsRest
|
||||||
|
)
|
||||||
|
|
||||||
|
class AvatarToolKit_OT_Import(Operator):
|
||||||
|
"""Import FBX files into Blender with Avatar Toolkit settings"""
|
||||||
|
bl_idname = "avatar_toolkit.import"
|
||||||
|
bl_label = t("QuickAccess.import")
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
clear_default_objects()
|
||||||
|
bpy.ops.import_scene.fbx('INVOKE_DEFAULT', filter_glob=imports)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class AvatarToolKit_OT_ExportFBX(Operator):
|
||||||
|
"""Export selected objects as FBX"""
|
||||||
|
bl_idname = "avatar_toolkit.export_fbx"
|
||||||
|
bl_label = t("QuickAccess.export_fbx")
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
bpy.ops.export_scene.fbx('INVOKE_DEFAULT')
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class AvatarToolKit_MT_ExportMenu(Menu):
|
||||||
|
"""Export menu containing various export options"""
|
||||||
|
bl_idname = "AVATAR_TOOLKIT_MT_export_menu"
|
||||||
|
bl_label = t("QuickAccess.export")
|
||||||
|
|
||||||
|
def draw(self, context: Context) -> None:
|
||||||
|
layout: UILayout = self.layout
|
||||||
|
layout.operator("avatar_toolkit.export_fbx", text=t("QuickAccess.export_fbx"))
|
||||||
|
layout.operator("avatar_toolkit.export_resonite", text=t("QuickAccess.export_resonite"))
|
||||||
|
|
||||||
|
class AvatarToolKit_OT_ExportMenu(Operator):
|
||||||
|
"""Open the export menu"""
|
||||||
|
bl_idname = "avatar_toolkit.export"
|
||||||
|
bl_label = t("QuickAccess.export")
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
bpy.ops.wm.call_menu(name=AvatarToolKit_MT_ExportMenu.bl_idname)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class AvatarToolKit_PT_QuickAccessPanel(Panel):
|
||||||
|
"""Quick access panel for common Avatar Toolkit operations"""
|
||||||
|
bl_label = t("QuickAccess.label")
|
||||||
|
bl_idname = "OBJECT_PT_avatar_toolkit_quick_access"
|
||||||
|
bl_space_type = 'VIEW_3D'
|
||||||
|
bl_region_type = 'UI'
|
||||||
|
bl_category = CATEGORY_NAME
|
||||||
|
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
||||||
|
bl_order = 0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context: Context) -> bool:
|
||||||
|
"""Only show panel in Object or Pose mode"""
|
||||||
|
return context.mode in {'OBJECT', 'POSE'}
|
||||||
|
|
||||||
|
def draw(self, context: Context) -> None:
|
||||||
|
"""Draw the panel layout"""
|
||||||
|
layout: UILayout = self.layout
|
||||||
|
|
||||||
|
# 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
|
||||||
|
col.prop(context.scene.avatar_toolkit, "active_armature", text="")
|
||||||
|
|
||||||
|
# Armature Validation
|
||||||
|
active_armature = get_active_armature(context)
|
||||||
|
if active_armature:
|
||||||
|
is_valid: bool
|
||||||
|
message: str
|
||||||
|
is_valid, message = validate_armature(active_armature)
|
||||||
|
|
||||||
|
if is_valid:
|
||||||
|
info_box: UILayout = col.box()
|
||||||
|
row: UILayout = info_box.row()
|
||||||
|
split: UILayout = row.split(factor=0.6)
|
||||||
|
split.label(text=t("QuickAccess.valid_armature"), icon='CHECKMARK')
|
||||||
|
stats: dict = get_armature_stats(active_armature)
|
||||||
|
split.label(text=t("QuickAccess.bones_count", count=stats['bone_count']))
|
||||||
|
|
||||||
|
if stats['has_pose']:
|
||||||
|
info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT')
|
||||||
|
else:
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
col.label(text=message, icon='ERROR')
|
||||||
|
|
||||||
|
# Pose Mode Controls
|
||||||
|
pose_box: UILayout = layout.box()
|
||||||
|
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":
|
||||||
|
col.operator(AvatarToolkit_OT_StopPoseMode.bl_idname, icon='POSE_HLT')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
col.operator(AvatarToolkit_OT_ApplyPoseAsRest.bl_idname, icon='MOD_ARMATURE')
|
||||||
|
col.operator(AvatarToolkit_OT_ApplyPoseAsShapekey.bl_idname, icon='MOD_ARMATURE')
|
||||||
|
else:
|
||||||
|
col.operator(AvatarToolkit_OT_StartPoseMode.bl_idname, icon='POSE_HLT')
|
||||||
|
|
||||||
|
# Import/Export Box
|
||||||
|
import_box: UILayout = layout.box()
|
||||||
|
col = import_box.column(align=True)
|
||||||
|
col.label(text=t("QuickAccess.import_export"), icon='IMPORT')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
|
||||||
|
# Import/Export Buttons
|
||||||
|
button_row: UILayout = col.row(align=True)
|
||||||
|
button_row.scale_y = 1.5
|
||||||
|
button_row.operator("avatar_toolkit.import", text=t("QuickAccess.import"), icon='IMPORT')
|
||||||
|
button_row.operator("avatar_toolkit.export", text=t("QuickAccess.export"), icon='EXPORT')
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import bpy
|
|
||||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
|
||||||
from ..core.translations import t
|
|
||||||
|
|
||||||
class AvatarToolkitSettingsPanel(bpy.types.Panel):
|
|
||||||
bl_label = t("Settings.label")
|
|
||||||
bl_idname = "OBJECT_PT_avatar_toolkit_settings"
|
|
||||||
bl_space_type = 'VIEW_3D'
|
|
||||||
bl_region_type = 'UI'
|
|
||||||
bl_category = CATEGORY_NAME
|
|
||||||
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
|
||||||
bl_order = 8
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
|
|
||||||
layout.label(text=t("Settings.language.label"))
|
|
||||||
layout.prop(context.scene.avatar_toolkit, "language", text="", icon='WORLD')
|
|
||||||
|
|
||||||
class AVATAR_TOOLKIT_OT_translation_restart_popup(bpy.types.Operator):
|
|
||||||
bl_idname = "avatar_toolkit.translation_restart_popup"
|
|
||||||
bl_label = t("Settings.translation_restart_popup.label")
|
|
||||||
bl_description = t("Settings.translation_restart_popup.description")
|
|
||||||
bl_options = {'INTERNAL'}
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
if context.scene.avatar_toolkit.language_changed:
|
|
||||||
bpy.ops.script.reload()
|
|
||||||
context.scene.avatar_toolkit.language_changed = False
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
return context.window_manager.invoke_props_dialog(self, width=300)
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
layout.label(text=t("Settings.translation_restart_popup.message1"), icon='INFO')
|
|
||||||
layout.label(text=t("Settings.translation_restart_popup.message2"), icon='FILE_REFRESH')
|
|
||||||
-76
@@ -1,76 +0,0 @@
|
|||||||
import bpy
|
|
||||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
|
||||||
from bpy.types import Context
|
|
||||||
from ..functions.digitigrade_legs import AvatarToolKit_OT_CreateDigitigradeLegs
|
|
||||||
from ..functions.resonite_functions import AvatarToolKit_OT_ConvertToResonite
|
|
||||||
from ..core.translations import t
|
|
||||||
from ..core.common import get_selected_armature
|
|
||||||
from ..functions.mesh_tools import AvatarToolkit_OT_RemoveUnusedShapekeys
|
|
||||||
from ..functions.additional_tools import (AvatarToolKit_OT_ApplyTransforms,
|
|
||||||
AvatarToolKit_OT_ConnectBones,
|
|
||||||
AvatarToolKit_OT_DeleteBoneConstraints,
|
|
||||||
AvatarToolKit_OT_SeparateByMaterials,
|
|
||||||
AvatarToolKit_OT_SeparateByLooseParts)
|
|
||||||
from ..functions.armature_modifying import (AvatarToolkit_OT_RemoveZeroWeightBones,
|
|
||||||
AvatarToolkit_OT_MergeBonesToActive,
|
|
||||||
AvatarToolkit_OT_MergeBonesToParents)
|
|
||||||
|
|
||||||
class AvatarToolkit_PT_ToolsPanel(bpy.types.Panel):
|
|
||||||
bl_label = t("Tools.label")
|
|
||||||
bl_idname = "OBJECT_PT_avatar_toolkit_tools"
|
|
||||||
bl_space_type = 'VIEW_3D'
|
|
||||||
bl_region_type = 'UI'
|
|
||||||
bl_category = CATEGORY_NAME
|
|
||||||
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
|
||||||
bl_order = 3
|
|
||||||
|
|
||||||
def draw(self, context: Context):
|
|
||||||
layout = self.layout
|
|
||||||
armature = get_selected_armature(context)
|
|
||||||
|
|
||||||
if armature:
|
|
||||||
layout.label(text=t("Tools.tools_title.label"), icon='TOOL_SETTINGS')
|
|
||||||
layout.separator(factor=0.5)
|
|
||||||
|
|
||||||
row = layout.row(align=True)
|
|
||||||
row.scale_y = 1.5
|
|
||||||
row.operator(AvatarToolKit_OT_ConvertToResonite.bl_idname, text=t("Tools.convert_to_resonite.label"), icon='SCENE_DATA')
|
|
||||||
|
|
||||||
layout.separator(factor=1.0)
|
|
||||||
|
|
||||||
layout.label(text=t("Tools.separate_by.label"), icon='MESH_DATA')
|
|
||||||
row = layout.row(align=True)
|
|
||||||
row.operator(AvatarToolKit_OT_SeparateByMaterials.bl_idname, text=t("Tools.separate_by_materials.label"), icon='MATERIAL')
|
|
||||||
row.operator(AvatarToolKit_OT_SeparateByLooseParts.bl_idname, text=t("Tools.separate_by_loose_parts.label"), icon='OUTLINER_OB_MESH')
|
|
||||||
|
|
||||||
layout.separator(factor=1.0)
|
|
||||||
|
|
||||||
layout.label(text=t("Tools.bone_tools.label"), icon='BONE_DATA')
|
|
||||||
row = layout.row(align=True)
|
|
||||||
row.operator(AvatarToolKit_OT_CreateDigitigradeLegs.bl_idname, text=t("Tools.create_digitigrade_legs.label"), icon='BONE_DATA')
|
|
||||||
|
|
||||||
row = layout.row(align=True)
|
|
||||||
row.operator(AvatarToolkit_OT_RemoveZeroWeightBones.bl_idname, text=t("Tools.remove_zero_weight_bones.label"), icon='BONE_DATA')
|
|
||||||
|
|
||||||
row = layout.row(align=True)
|
|
||||||
row.operator(AvatarToolkit_OT_MergeBonesToActive.bl_idname, text=t("Tools.merge_bones_to_active.label"), icon='BONE_DATA')
|
|
||||||
row.operator(AvatarToolkit_OT_MergeBonesToParents.bl_idname, text=t("Tools.merge_bones_to_parents.label"), icon='BONE_DATA')
|
|
||||||
|
|
||||||
row = layout.row(align=True)
|
|
||||||
row.operator(AvatarToolKit_OT_ConnectBones.bl_idname, text=t("Tools.connect_bones.label"), icon='BONE_DATA')
|
|
||||||
row.operator(AvatarToolKit_OT_DeleteBoneConstraints.bl_idname, text=t("Tools.delete_bone_constraints.label"), icon='CONSTRAINT_BONE')
|
|
||||||
|
|
||||||
row = layout.row()
|
|
||||||
row.prop(context.scene.avatar_toolkit, "merge_twist_bones")
|
|
||||||
|
|
||||||
layout.separator(factor=1.0)
|
|
||||||
|
|
||||||
layout.label(text=t("Tools.additional_tools.label"), icon='TOOL_SETTINGS')
|
|
||||||
row = layout.row(align=True)
|
|
||||||
row.operator(AvatarToolKit_OT_ApplyTransforms.bl_idname, text=t("Tools.apply_transforms.label"), icon='OBJECT_ORIGIN')
|
|
||||||
row.operator(AvatarToolkit_OT_RemoveUnusedShapekeys.bl_idname, text=t("Tools.remove_unused_shapekeys.label"), icon='SHAPEKEY_DATA')
|
|
||||||
|
|
||||||
layout.separator(factor=1.0)
|
|
||||||
else:
|
|
||||||
layout.label(text=t("Tools.select_armature"), icon='ERROR')
|
|
||||||
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import bpy
|
|
||||||
from ..core.translations import t
|
|
||||||
from .main_panel import draw_title
|
|
||||||
|
|
||||||
class UVTools_PT_MainPanel(bpy.types.Panel):
|
|
||||||
bl_label = t("AvatarToolkit.label")
|
|
||||||
bl_idname = "OBJECT_PT_avatar_toolkit_uv"
|
|
||||||
bl_space_type = 'IMAGE_EDITOR'
|
|
||||||
bl_region_type = 'UI'
|
|
||||||
bl_category = "Avatar Toolkit"
|
|
||||||
|
|
||||||
def draw(self: bpy.types.Panel, context: bpy.types.Context):
|
|
||||||
layout = self.layout
|
|
||||||
draw_title(self)
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import bpy
|
|
||||||
from ..core.translations import t
|
|
||||||
from ..functions.uv_tools import AvatarToolkit_OT_AlignUVEdgesToTarget
|
|
||||||
from .main_panel import draw_title
|
|
||||||
from .uv_panel import UVTools_PT_MainPanel
|
|
||||||
|
|
||||||
class UVTools_PT_Tools(bpy.types.Panel):
|
|
||||||
bl_label = t("Tools.label")
|
|
||||||
bl_idname = "OBJECT_PT_avatar_toolkit_uv_tools"
|
|
||||||
bl_space_type = 'IMAGE_EDITOR'
|
|
||||||
bl_region_type = 'UI'
|
|
||||||
bl_category = "Avatar Toolkit"
|
|
||||||
bl_parent_id = UVTools_PT_MainPanel.bl_idname
|
|
||||||
bl_order = 3
|
|
||||||
|
|
||||||
def draw(self, context: bpy.types.Context):
|
|
||||||
layout = self.layout
|
|
||||||
row = layout.row(align=True)
|
|
||||||
row.operator(AvatarToolkit_OT_AlignUVEdgesToTarget.bl_idname, text=t("avatar_toolkit.align_uv_edges_to_target.label"), icon='GP_MULTIFRAME_EDITING')
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import bpy
|
|
||||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
|
||||||
from ..functions.viseme import AvatarToolKit_OT_AutoVisemeButton
|
|
||||||
from ..core.translations import t
|
|
||||||
from ..core.common import get_selected_armature
|
|
||||||
|
|
||||||
class AvatarToolkitVisemePanel(bpy.types.Panel):
|
|
||||||
bl_label = t("VisemePanel.label")
|
|
||||||
bl_idname = "OBJECT_PT_avatar_toolkit_viseme"
|
|
||||||
bl_space_type = 'VIEW_3D'
|
|
||||||
bl_region_type = 'UI'
|
|
||||||
bl_category = CATEGORY_NAME
|
|
||||||
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
|
||||||
bl_order = 7
|
|
||||||
|
|
||||||
def draw(self, context: bpy.types.Context) -> None:
|
|
||||||
layout = self.layout
|
|
||||||
|
|
||||||
armature = get_selected_armature(context)
|
|
||||||
if armature:
|
|
||||||
layout.label(text=t("VisemePanel.label"), icon='SOUND')
|
|
||||||
|
|
||||||
layout.separator(factor=0.5)
|
|
||||||
|
|
||||||
layout.prop(context.scene.avatar_toolkit, "selected_mesh", text=t("VisemePanel.select_mesh"), icon='OUTLINER_OB_MESH')
|
|
||||||
|
|
||||||
mesh = bpy.data.objects.get(context.scene.avatar_toolkit.selected_mesh)
|
|
||||||
if mesh and mesh.type == 'MESH':
|
|
||||||
if mesh.data.shape_keys:
|
|
||||||
box = layout.box()
|
|
||||||
col = box.column(align=True)
|
|
||||||
col.prop_search(context.scene.avatar_toolkit, "mouth_a", mesh.data.shape_keys, "key_blocks", text=t('VisemePanel.mouth_a.label'), icon='SHAPEKEY_DATA')
|
|
||||||
col.prop_search(context.scene.avatar_toolkit, "mouth_o", mesh.data.shape_keys, "key_blocks", text=t('VisemePanel.mouth_o.label'), icon='SHAPEKEY_DATA')
|
|
||||||
col.prop_search(context.scene.avatar_toolkit, "mouth_ch", mesh.data.shape_keys, "key_blocks", text=t('VisemePanel.mouth_ch.label'), icon='SHAPEKEY_DATA')
|
|
||||||
|
|
||||||
layout.separator(factor=0.5)
|
|
||||||
|
|
||||||
layout.prop(context.scene.avatar_toolkit, 'shape_intensity', text=t('VisemePanel.shape_intensity'), icon='FORCE_LENNARDJONES')
|
|
||||||
|
|
||||||
layout.separator(factor=1.0)
|
|
||||||
|
|
||||||
row = layout.row()
|
|
||||||
row.scale_y = 1.5
|
|
||||||
row.operator(AvatarToolKit_OT_AutoVisemeButton.bl_idname, text=t('VisemePanel.create_visemes'), icon='TRIA_RIGHT')
|
|
||||||
else:
|
|
||||||
layout.label(text=t('VisemePanel.error.noShapekeys'), icon='ERROR')
|
|
||||||
else:
|
|
||||||
layout.label(text=t('VisemePanel.error.selectMesh'), icon='INFO')
|
|
||||||
else:
|
|
||||||
layout.label(text=t('VisemePanel.error.noArmature'), icon='ERROR')
|
|
||||||
|
|
||||||
layout.separator(factor=1.0)
|
|
||||||
layout.label(text=t('VisemePanel.info.selectMesh'), icon='HELP')
|
|
||||||
Reference in New Issue
Block a user