Holy shit this was a pain

- Truly fixes PMX Import lol, i messed up completely
- Updated MMD Tools to use Cats One
This commit is contained in:
Yusarina
2025-11-19 06:35:06 +00:00
parent f0bda259d3
commit a929f68ad4
38 changed files with 4479 additions and 2709 deletions
+1 -1
View File
@@ -3,4 +3,4 @@
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
File diff suppressed because it is too large Load Diff
+143 -99
View File
@@ -1,22 +1,16 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
# This file is part of MMD Tools.
from collections import defaultdict
import bpy
from bpy.props import BoolProperty, StringProperty, FloatProperty
from bpy.types import Operator, Context, Object, Material
from typing import Set, Dict, Any, List, Tuple, Optional, Union, cast
from bpy.props import BoolProperty, StringProperty
from bpy.types import Operator
from .. import cycles_converter
from ..core.exceptions import MaterialNotFoundError
from ..core.material import FnMaterial
from ..core.shader import _NodeGroupUtils
from ....core.logging_setup import logger
import traceback
class ConvertMaterialsForCycles(Operator):
@@ -25,14 +19,14 @@ class ConvertMaterialsForCycles(Operator):
bl_description = "Convert materials of selected objects for Cycles."
bl_options = {"REGISTER", "UNDO"}
use_principled: BoolProperty(
use_principled: bpy.props.BoolProperty(
name="Convert to Principled BSDF",
description="Convert MMD shader nodes to Principled BSDF as well if enabled",
default=False,
options={"SKIP_SAVE"},
)
clean_nodes: BoolProperty(
clean_nodes: bpy.props.BoolProperty(
name="Clean Nodes",
description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
default=False,
@@ -40,27 +34,22 @@ class ConvertMaterialsForCycles(Operator):
)
@classmethod
def poll(cls, context: Context) -> bool:
return next((x for x in context.selected_objects if x.type == "MESH"), None) is not None
def poll(cls, context):
return any(x.type == "MESH" for x in context.selected_objects)
def draw(self, context: Context) -> None:
def draw(self, context):
layout = self.layout
layout.prop(self, "use_principled")
layout.prop(self, "clean_nodes")
def execute(self, context: Context) -> Set[str]:
def execute(self, context):
try:
context.scene.render.engine = "CYCLES"
except Exception:
logger.error(f"Failed to change to Cycles render engine: {traceback.format_exc()}")
self.report({"ERROR"}, " * Failed to change to Cycles render engine.")
return {"CANCELLED"}
logger.info(f"Converting materials for Cycles with principled={self.use_principled}, clean_nodes={self.clean_nodes}")
for obj in (x for x in context.selected_objects if x.type == "MESH"):
logger.debug(f"Converting materials for object: {obj.name}")
cycles_converter.convertToCyclesShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes)
return {"FINISHED"}
@@ -70,21 +59,21 @@ class ConvertMaterials(Operator):
bl_description = "Convert materials of selected objects."
bl_options = {"REGISTER", "UNDO"}
use_principled: BoolProperty(
use_principled: bpy.props.BoolProperty(
name="Convert to Principled BSDF",
description="Convert MMD shader nodes to Principled BSDF as well if enabled",
default=True,
options={"SKIP_SAVE"},
)
clean_nodes: BoolProperty(
clean_nodes: bpy.props.BoolProperty(
name="Clean Nodes",
description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
default=True,
options={"SKIP_SAVE"},
)
subsurface: FloatProperty(
subsurface: bpy.props.FloatProperty(
name="Subsurface",
default=0.001,
soft_min=0.000,
@@ -94,41 +83,130 @@ class ConvertMaterials(Operator):
)
@classmethod
def poll(cls, context: Context) -> bool:
return next((x for x in context.selected_objects if x.type == "MESH"), None) is not None
def poll(cls, context):
return any(x.type == "MESH" for x in context.selected_objects)
def execute(self, context: Context) -> Set[str]:
logger.info(f"Converting materials with principled={self.use_principled}, clean_nodes={self.clean_nodes}, subsurface={self.subsurface}")
def execute(self, context):
for obj in context.selected_objects:
if obj.type != "MESH":
continue
logger.debug(f"Converting materials for object: {obj.name}")
cycles_converter.convertToBlenderShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes, subsurface=self.subsurface)
return {"FINISHED"}
class ConvertBSDFMaterials(Operator):
bl_idname = 'mmd_tools.convert_bsdf_materials'
bl_label = 'Convert Blender Materials'
bl_description = 'Convert materials of selected objects.'
bl_options = {'REGISTER', 'UNDO'}
class MergeMaterials(Operator):
bl_idname = "mmd_tools.merge_materials"
bl_label = "Merge Materials"
bl_description = "Merge materials with the same texture in selected objects. Only merges materials with exactly one texture node. Materials with no texture or with multiple textures are not merged. Please convert to Blender materials first."
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context: Context) -> bool:
return next((x for x in context.selected_objects if x.type == 'MESH'), None) is not None
def poll(cls, context):
return any(x.type == "MESH" for x in context.selected_objects)
def execute(self, context: Context) -> Set[str]:
logger.info("Converting BSDF materials to MMD shader")
def execute(self, context):
# Process all selected mesh objects
for obj in context.selected_objects:
if obj.type != 'MESH':
if obj.type != "MESH":
continue
self.merge_materials_for_object(context, obj)
return {"FINISHED"}
def merge_materials_for_object(self, context, obj):
"""Merge materials with same texture for a single object"""
if not obj.data.materials:
self.report({"INFO"}, f"Object '{obj.name}' has no materials")
return
# Map texture paths to material indices and names
texture_to_materials = defaultdict(list)
# Check each material
for i, material in enumerate(obj.data.materials):
# use_nodes is deprecated in 5.0 but always returns True, so check is safe
if not material or not material.use_nodes:
continue
# 1. Check texture node count (must be exactly 1)
texture_nodes = [node for node in material.node_tree.nodes if node.type == "TEX_IMAGE"]
if len(texture_nodes) != 1:
continue
# 2. Record texture path and material info
texture_node = texture_nodes[0]
if texture_node.image:
texture_path = bpy.path.abspath(texture_node.image.filepath)
texture_to_materials[texture_path].append({"index": i, "name": material.name})
# Find material groups that need merging
materials_to_merge = {path: materials for path, materials in texture_to_materials.items() if len(materials) > 1}
if not materials_to_merge:
self.report({"INFO"}, f"No materials to merge in object '{obj.name}'")
return
# Process each texture group
context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode="EDIT")
merge_details = []
for texture_path, materials in materials_to_merge.items():
# Use first material as target
target_material = materials[0]
target_index = target_material["index"]
target_name = target_material["name"]
source_materials = []
# Reassign faces from other materials to target material
for source_material in materials[1:]:
source_index = source_material["index"]
source_name = source_material["name"]
source_materials.append(source_name)
bpy.ops.mesh.select_all(action="DESELECT")
obj.active_material_index = source_index
bpy.ops.object.material_slot_select()
obj.active_material_index = target_index
bpy.ops.object.material_slot_assign()
# Record merge details
texture_name = bpy.path.basename(texture_path)
merge_details.append({"texture": texture_name, "target": target_name, "sources": source_materials})
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.material_slot_remove_unused()
merged_count = sum(len(details["sources"]) for details in merge_details)
self.report({"INFO"}, f"Object '{obj.name}': Merged {merged_count} materials")
for details in merge_details:
sources_text = ", ".join(details["sources"])
self.report({"INFO"}, f"Same Texture '{details['texture']}': Merged materials [{sources_text}] into '{details['target']}'")
class ConvertBSDFMaterials(Operator):
bl_idname = "mmd_tools.convert_bsdf_materials"
bl_label = "Convert Blender Materials"
bl_description = "Convert materials of selected objects."
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return any(x.type == "MESH" for x in context.selected_objects)
def execute(self, context):
for obj in context.selected_objects:
if obj.type != "MESH":
continue
logger.debug(f"Converting BSDF materials for object: {obj.name}")
cycles_converter.convertToMMDShader(obj)
return {'FINISHED'}
return {"FINISHED"}
class _OpenTextureBase:
"""Create a texture for mmd model material."""
bl_options: Set[str] = {"REGISTER", "UNDO", "INTERNAL"}
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
filepath: StringProperty(
name="File Path",
@@ -142,7 +220,7 @@ class _OpenTextureBase:
options={"HIDDEN"},
)
def invoke(self, context: Context, event: Any) -> Set[str]:
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
@@ -152,13 +230,8 @@ class OpenTexture(Operator, _OpenTextureBase):
bl_label = "Open Texture"
bl_description = "Create main texture of active material"
def execute(self, context: Context) -> Set[str]:
def execute(self, context):
mat = context.active_object.active_material
if not mat:
logger.error("No active material found")
return {"CANCELLED"}
logger.info(f"Creating texture for material: {mat.name} from {self.filepath}")
fnMat = FnMaterial(mat)
fnMat.create_texture(self.filepath)
return {"FINISHED"}
@@ -172,13 +245,8 @@ class RemoveTexture(Operator):
bl_description = "Remove main texture of active material"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: Context) -> Set[str]:
def execute(self, context):
mat = context.active_object.active_material
if not mat:
logger.error("No active material found")
return {"CANCELLED"}
logger.info(f"Removing texture from material: {mat.name}")
fnMat = FnMaterial(mat)
fnMat.remove_texture()
return {"FINISHED"}
@@ -191,13 +259,8 @@ class OpenSphereTextureSlot(Operator, _OpenTextureBase):
bl_label = "Open Sphere Texture"
bl_description = "Create sphere texture of active material"
def execute(self, context: Context) -> Set[str]:
def execute(self, context):
mat = context.active_object.active_material
if not mat:
logger.error("No active material found")
return {"CANCELLED"}
logger.info(f"Creating sphere texture for material: {mat.name} from {self.filepath}")
fnMat = FnMaterial(mat)
fnMat.create_sphere_texture(self.filepath, context.active_object)
return {"FINISHED"}
@@ -211,13 +274,8 @@ class RemoveSphereTexture(Operator):
bl_description = "Remove sphere texture of active material"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: Context) -> Set[str]:
def execute(self, context):
mat = context.active_object.active_material
if not mat:
logger.error("No active material found")
return {"CANCELLED"}
logger.info(f"Removing sphere texture from material: {mat.name}")
fnMat = FnMaterial(mat)
fnMat.remove_sphere_texture()
return {"FINISHED"}
@@ -230,21 +288,17 @@ class MoveMaterialUp(Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context: Context) -> bool:
def poll(cls, context):
obj = context.active_object
valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE"
return bool(valid_mesh and obj.active_material_index > 0)
return obj is not None and obj.type == "MESH" and obj.mmd_type == "NONE" and obj.active_material_index > 0
def execute(self, context: Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
current_idx = obj.active_material_index
prev_index = current_idx - 1
logger.debug(f"Moving material {current_idx} up to position {prev_index} for object {obj.name}")
try:
FnMaterial.swap_materials(obj, current_idx, prev_index, reverse=True, swap_slots=True)
except MaterialNotFoundError:
logger.error(f"Materials not found for indices {current_idx} and {prev_index}")
self.report({"ERROR"}, "Materials not found")
return {"CANCELLED"}
obj.active_material_index = prev_index
@@ -259,21 +313,17 @@ class MoveMaterialDown(Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context: Context) -> bool:
def poll(cls, context):
obj = context.active_object
valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE"
return bool(valid_mesh and obj.active_material_index < len(obj.material_slots) - 1)
return obj is not None and obj.type == "MESH" and obj.mmd_type == "NONE" and obj.active_material_index < len(obj.material_slots) - 1
def execute(self, context: Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
current_idx = obj.active_material_index
next_index = current_idx + 1
logger.debug(f"Moving material {current_idx} down to position {next_index} for object {obj.name}")
try:
FnMaterial.swap_materials(obj, current_idx, next_index, reverse=True, swap_slots=True)
except MaterialNotFoundError:
logger.error(f"Materials not found for indices {current_idx} and {next_index}")
self.report({"ERROR"}, "Materials not found")
return {"CANCELLED"}
obj.active_material_index = next_index
@@ -296,31 +346,26 @@ class EdgePreviewSetup(Operator):
default="CREATE",
)
def execute(self, context: Context) -> Set[str]:
def execute(self, context):
from ..core.model import FnModel
root = FnModel.find_root_object(context.active_object)
if root is None:
logger.error("No MMD model root found")
self.report({"ERROR"}, "Select a MMD model")
return {"CANCELLED"}
if self.action == "CLEAN":
logger.info(f"Cleaning toon edge for model: {root.name}")
for obj in FnModel.iterate_mesh_objects(root):
self.__clean_toon_edge(obj)
else:
from ..bpyutils import Props
logger.info(f"Creating toon edge for model: {root.name}")
scale = 0.2 * getattr(root, Props.empty_display_size)
counts = sum(self.__create_toon_edge(obj, scale) for obj in FnModel.iterate_mesh_objects(root))
logger.info(f"Created {counts} toon edge(s)")
self.report({"INFO"}, "Created %d toon edge(s)" % counts)
return {"FINISHED"}
def __clean_toon_edge(self, obj: Object) -> None:
logger.debug(f"Cleaning toon edge for object: {obj.name}")
def __clean_toon_edge(self, obj):
if "mmd_edge_preview" in obj.modifiers:
obj.modifiers.remove(obj.modifiers["mmd_edge_preview"])
@@ -329,8 +374,7 @@ class EdgePreviewSetup(Operator):
FnMaterial.clean_materials(obj, can_remove=lambda m: m and m.name.startswith("mmd_edge."))
def __create_toon_edge(self, obj: Object, scale: float = 1.0) -> int:
logger.debug(f"Creating toon edge for object: {obj.name} with scale {scale}")
def __create_toon_edge(self, obj, scale=1.0):
self.__clean_toon_edge(obj)
materials = obj.data.materials
material_offset = len(materials)
@@ -355,10 +399,10 @@ class EdgePreviewSetup(Operator):
mod.vertex_group = "mmd_edge_preview"
return len(materials) - material_offset
def __create_edge_preview_group(self, obj: Object) -> None:
def __create_edge_preview_group(self, obj):
vertices, materials = obj.data.vertices, obj.data.materials
weight_map = {i: m.mmd_material.edge_weight for i, m in enumerate(materials) if m}
scale_map: Dict[int, float] = {}
scale_map = {}
vg_scale_index = obj.vertex_groups.find("mmd_edge_scale")
if vg_scale_index >= 0:
scale_map = {v.index: g.weight for v in vertices for g in v.groups if g.group == vg_scale_index}
@@ -367,7 +411,7 @@ class EdgePreviewSetup(Operator):
weight = scale_map.get(i, 1.0) * weight_map.get(mi, 1.0) * 0.02
vg_edge_preview.add(index=[i], weight=weight, type="REPLACE")
def __get_edge_material(self, mat_name: str, edge_color: Tuple[float, float, float, float], materials: List[Material]) -> Material:
def __get_edge_material(self, mat_name, edge_color, materials):
if mat_name in materials:
return materials[mat_name]
mat = bpy.data.materials.get(mat_name, None)
@@ -385,8 +429,8 @@ class EdgePreviewSetup(Operator):
self.__make_shader(mat)
return mat
def __make_shader(self, m: Material) -> None:
# Note: material.use_nodes is deprecated in Blender 5.0 - materials always use nodes
def __make_shader(self, m):
m.use_nodes = True
nodes, links = m.node_tree.nodes, m.node_tree.links
node_shader = nodes.get("mmd_edge_preview", None)
@@ -406,7 +450,7 @@ class EdgePreviewSetup(Operator):
node_shader.inputs["Color"].default_value = m.mmd_material.edge_color
node_shader.inputs["Alpha"].default_value = m.mmd_material.edge_color[3]
def __get_edge_preview_shader(self) -> bpy.types.NodeTree:
def __get_edge_preview_shader(self):
group_name = "MMDEdgePreview"
shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
if len(shader.nodes):
@@ -414,8 +458,8 @@ class EdgePreviewSetup(Operator):
ng = _NodeGroupUtils(shader)
node_input = ng.new_node("NodeGroupInput", (-5, 0))
node_output = ng.new_node("NodeGroupOutput", (3, 0))
ng.new_node("NodeGroupInput", (-5, 0))
ng.new_node("NodeGroupOutput", (3, 0))
############################################################################
node_color = ng.new_node("ShaderNodeMixRGB", (-1, -1.5))
+47 -68
View File
@@ -1,22 +1,15 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
# This file is part of MMD Tools.
import re
from typing import List, Dict, Any, Set, Optional, Tuple, Union, Type
import bpy
from bpy.types import Context, Object, Operator, ShapeKey
from .. import utils
from ..bpyutils import FnContext, FnObject
from ..core.bone import FnBone
from ..core.model import FnModel, Model
from ..core.morph import FnMorph
from ....core.logging_setup import logger
class SelectObject(bpy.types.Operator):
@@ -32,8 +25,7 @@ class SelectObject(bpy.types.Operator):
options={"HIDDEN", "SKIP_SAVE"},
)
def execute(self, context: Context) -> Set[str]:
logger.debug(f"Selecting object: {self.name}")
def execute(self, context):
utils.selectAObject(context.scene.objects[self.name])
return {"FINISHED"}
@@ -47,43 +39,41 @@ class MoveObject(bpy.types.Operator, utils.ItemMoveOp):
__PREFIX_REGEXP = re.compile(r"(?P<prefix>[0-9A-Z]{3}_)(?P<name>.*)")
@classmethod
def set_index(cls, obj: Object, index: int) -> None:
def set_index(cls, obj, index):
m = cls.__PREFIX_REGEXP.match(obj.name)
name = m.group("name") if m else obj.name
obj.name = "%s_%s" % (utils.int2base(index, 36, 3), name)
obj.name = f"{utils.int2base(index, 36, 3)}_{name}"
@classmethod
def get_name(cls, obj: Object, prefix: Optional[str] = None) -> str:
def get_name(cls, obj, prefix=None):
m = cls.__PREFIX_REGEXP.match(obj.name)
name = m.group("name") if m else obj.name
return name[len(prefix) :] if prefix and name.startswith(prefix) else name
@classmethod
def normalize_indices(cls, objects: List[Object]) -> None:
def normalize_indices(cls, objects):
for i, x in enumerate(objects):
cls.set_index(x, i)
@classmethod
def poll(cls, context: Context) -> bool:
def poll(cls, context):
return context.active_object is not None
def execute(self, context: Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
objects = self.__get_objects(obj)
if obj not in objects:
logger.error(f'Cannot move object "{obj.name}"')
self.report({"ERROR"}, f'Can not move object "{obj.name}"')
return {"CANCELLED"}
objects.sort(key=lambda x: x.name)
logger.debug(f"Moving object {obj.name} {self.type}")
self.move(objects, objects.index(obj), self.type)
self.normalize_indices(objects)
return {"FINISHED"}
def __get_objects(self, obj: Object) -> Any:
def __get_objects(self, obj):
class __MovableList(list):
def move(self, index_old: int, index_new: int) -> None:
def move(self, index_old, index_new):
item = self[index_old]
self.remove(item)
self.insert(index_new, item)
@@ -108,43 +98,40 @@ class CleanShapeKeys(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context: Context) -> bool:
def poll(cls, context):
return any(o.type == "MESH" for o in context.selected_objects)
@staticmethod
def __can_remove(key_block: ShapeKey) -> bool:
def __can_remove(key_block):
if key_block.relative_key == key_block:
return False # Basis
for v0, v1 in zip(key_block.relative_key.data, key_block.data):
for v0, v1 in zip(key_block.relative_key.data, key_block.data, strict=False):
if v0.co != v1.co:
return False
return True
def __shape_key_clean(self, obj: Object, key_blocks: List[ShapeKey]) -> None:
def __shape_key_clean(self, obj, key_blocks):
for kb in key_blocks:
if self.__can_remove(kb):
logger.debug(f"Removing unused shape key: {kb.name} from {obj.name}")
FnObject.mesh_remove_shape_key(obj, kb)
if len(key_blocks) == 1:
logger.debug(f"Removing single shape key: {key_blocks[0].name} from {obj.name}")
FnObject.mesh_remove_shape_key(obj, key_blocks[0])
def execute(self, context: Context) -> Set[str]:
logger.info("Cleaning shape keys for selected objects")
obj: Object
def execute(self, context):
obj: bpy.types.Object
for obj in context.selected_objects:
if obj.type != "MESH" or obj.data.shape_keys is None:
continue
if not obj.data.shape_keys.use_relative:
continue # not be considered yet
logger.debug(f"Processing shape keys for {obj.name}")
self.__shape_key_clean(obj, obj.data.shape_keys.key_blocks)
return {"FINISHED"}
class SeparateByMaterials(bpy.types.Operator):
bl_idname = "mmd_tools.separate_by_materials"
bl_label = "Separate By Materials"
bl_label = "Sep by Mat(High Risk)"
bl_description = "Separate by Materials (High Risk)\nSeparate the mesh into multiple objects based on materials.\nHIGH RISK & BUGGY: This operation is not reversible and may cause various issues. It splits adjacent geometry by material, and merging later will not reconnect shared edges.\nKnown issues include potential mesh corruption, UV mapping problems, and other unpredictable behaviors. Use with extreme caution and backup your work first."
bl_options = {"REGISTER", "UNDO"}
clean_shape_keys: bpy.props.BoolProperty(
@@ -153,26 +140,32 @@ class SeparateByMaterials(bpy.types.Operator):
default=True,
)
@classmethod
def poll(cls, context: Context) -> bool:
obj = context.active_object
return obj and obj.type == "MESH"
keep_normals: bpy.props.BoolProperty(
name="Keep Normals",
default=True,
)
def __separate_by_materials(self, obj: Object) -> None:
logger.info(f"Separating {obj.name} by materials")
utils.separateByMaterials(obj)
@classmethod
def poll(cls, context):
obj = context.active_object
return obj is not None and obj.type == "MESH"
def __separate_by_materials(self, obj):
utils.separateByMaterials(obj, self.keep_normals)
if self.clean_shape_keys:
logger.debug("Cleaning shape keys after separation")
bpy.ops.mmd_tools.clean_shape_keys()
def execute(self, context: Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
# Sep by Mat crashes Blender if used after morph assembly
rig = Model(root)
rig.morph_slider.unbind()
if root is None:
logger.debug("No root object found, separating single object")
self.__separate_by_materials(obj)
else:
logger.debug(f"Root object found: {root.name}, preparing for separation")
bpy.ops.mmd_tools.clear_temp_materials()
bpy.ops.mmd_tools.clear_uv_morph_view()
@@ -185,11 +178,9 @@ class SeparateByMaterials(bpy.types.Operator):
if len(mesh.data.materials) > 0:
mat = mesh.data.materials[0]
idx = mat_names.index(getattr(mat, "name", None))
logger.debug(f"Setting index {idx} for mesh {mesh.name}")
MoveObject.set_index(mesh, idx)
for morph in root.mmd_root.material_morphs:
logger.debug(f"Updating material morph: {morph.name}")
FnMorph(morph, rig).update_mat_related_mesh()
utils.clearUnusedMeshes()
return {"FINISHED"}
@@ -207,15 +198,13 @@ class JoinMeshes(bpy.types.Operator):
default=True,
)
def execute(self, context: Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
if root is None:
logger.error("No MMD model found")
self.report({"ERROR"}, "Select a MMD model")
return {"CANCELLED"}
logger.info(f"Joining meshes for model: {root.name}")
bpy.ops.mmd_tools.clear_temp_materials()
bpy.ops.mmd_tools.clear_uv_morph_view()
@@ -223,11 +212,9 @@ class JoinMeshes(bpy.types.Operator):
rig = Model(root)
meshes_list = sorted(rig.meshes(), key=lambda x: x.name)
if not meshes_list:
logger.error("No meshes found in the model")
self.report({"ERROR"}, "The model does not have any meshes")
return {"CANCELLED"}
active_mesh = meshes_list[0]
logger.debug(f"Found {len(meshes_list)} meshes, using {active_mesh.name} as active")
FnContext.select_objects(context, *meshes_list)
FnContext.set_active_object(context, active_mesh)
@@ -236,19 +223,15 @@ class JoinMeshes(bpy.types.Operator):
for m in meshes_list[1:]:
for mat in m.data.materials:
if mat not in active_mesh.data.materials[:]:
logger.debug(f"Adding material {mat.name} to active mesh")
active_mesh.data.materials.append(mat)
# Join selected meshes
logger.debug("Joining meshes")
bpy.ops.object.join()
if self.sort_shape_keys:
logger.debug("Sorting shape keys")
FnMorph.fixShapeKeyOrder(active_mesh, root.mmd_root.vertex_morphs.keys())
active_mesh.active_shape_key_index = 0
for morph in root.mmd_root.material_morphs:
logger.debug(f"Updating material morph: {morph.name}")
FnMorph(morph, rig).update_mat_related_mesh(active_mesh)
utils.clearUnusedMeshes()
return {"FINISHED"}
@@ -262,20 +245,17 @@ class AttachMeshesToMMD(bpy.types.Operator):
add_armature_modifier: bpy.props.BoolProperty(default=True)
def execute(self, context: Context) -> Set[str]:
def execute(self, context: bpy.types.Context):
root = FnModel.find_root_object(context.active_object)
if root is None:
logger.error("No MMD model found")
self.report({"ERROR"}, "Select a MMD model")
return {"CANCELLED"}
armObj = FnModel.find_armature_object(root)
if armObj is None:
logger.error("Model armature not found")
self.report({"ERROR"}, "Model Armature not found")
return {"CANCELLED"}
logger.info(f"Attaching meshes to model: {root.name}")
FnModel.attach_mesh_objects(root, context.visible_objects, self.add_armature_modifier)
return {"FINISHED"}
@@ -295,18 +275,18 @@ class ChangeMMDIKLoopFactor(bpy.types.Operator):
)
@classmethod
def poll(cls, context: Context) -> bool:
return FnModel.find_root_object(context.active_object) is not None
def poll(cls, context):
root = FnModel.find_root_object(context.active_object)
return root is not None
def invoke(self, context: Context, event: Any) -> Set[str]:
def invoke(self, context, event):
root_object = FnModel.find_root_object(context.active_object)
self.mmd_ik_loop_factor = root_object.mmd_root.ik_loop_factor
vm = context.window_manager
return vm.invoke_props_dialog(self)
def execute(self, context: Context) -> Set[str]:
def execute(self, context):
root_object = FnModel.find_root_object(context.active_object)
logger.info(f"Changing IK loop factor to {self.mmd_ik_loop_factor} for model: {root_object.name}")
FnModel.change_mmd_ik_loop_factor(root_object, self.mmd_ik_loop_factor)
return {"FINISHED"}
@@ -318,22 +298,21 @@ class RecalculateBoneRoll(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context: Context) -> bool:
def poll(cls, context):
obj = context.active_object
return obj and obj.type == "ARMATURE"
return obj is not None and obj.type == "ARMATURE"
def invoke(self, context: Context, event: Any) -> Set[str]:
def invoke(self, context, event):
vm = context.window_manager
return vm.invoke_props_dialog(self)
def draw(self, context: Context) -> None:
def draw(self, context):
layout = self.layout
c = layout.column()
c.label(text="This operation will break existing f-curve/action.", icon="QUESTION")
c.label(text="Click [OK] to run the operation.")
def execute(self, context: Context) -> Set[str]:
def execute(self, context):
arm = context.active_object
logger.info(f"Recalculating bone roll for armature: {arm.name}")
FnBone.apply_auto_bone_roll(arm)
return {"FINISHED"}
+240 -139
View File
@@ -1,32 +1,27 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
# Copyright 2022 MMD Tools authors
# This file is part of MMD Tools.
import itertools
from operator import itemgetter
from typing import Dict, List, Optional, Set, Tuple, Any
from typing import Dict, List, Optional, Set
import bmesh
import bpy
import numpy as np
import numpy.typing as npt
from bpy.types import Context, Object, Operator, EditBone, Mesh, Armature
from mathutils import Matrix
from ..bpyutils import FnContext
from ..bpyutils import FnContext, select_object
from ..core.model import FnModel, Model
from ....core.logging_setup import logger
class MessageException(Exception):
"""Class for error with message."""
class NoModelSelectedError(Exception):
"""Raised when no MMD model is selected."""
class ModelJoinByBonesOperator(bpy.types.Operator):
bl_idname = "mmd_tools.model_join_by_bones"
bl_label = "Model Join by Bones"
bl_description = "Join multiple MMD models into one.\n\nWARNING: To align models before joining, only adjust the root (cross under the model) transformation. Do not move armatures, meshes, rigid bodies, or joints directly as they will not move together.\n\nIMPORTANT: Don't use any of the 'Assembly' functions before using this function. This function requires the models to be in a clean state."
bl_options = {"REGISTER", "UNDO"}
join_type: bpy.props.EnumProperty(
@@ -39,8 +34,8 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
)
@classmethod
def poll(cls, context: Context) -> bool:
active_object: Optional[Object] = context.active_object
def poll(cls, context: bpy.types.Context):
active_object: Optional[bpy.types.Object] = context.active_object
if context.mode != "POSE":
return False
@@ -56,22 +51,19 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
return len(context.selected_pose_bones) > 0
def invoke(self, context: Context, event: Any) -> Set[str]:
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: Context) -> Set[str]:
def execute(self, context: bpy.types.Context):
try:
logger.info("Starting model join by bones operation")
self.join(context)
logger.info("Model join by bones completed successfully")
except MessageException as ex:
logger.error(f"Model join by bones failed: {str(ex)}")
except NoModelSelectedError as ex:
self.report(type={"ERROR"}, message=str(ex))
return {"CANCELLED"}
return {"FINISHED"}
def join(self, context: Context) -> None:
def join(self, context: bpy.types.Context):
bpy.ops.object.mode_set(mode="OBJECT")
parent_root_object = FnModel.find_root_object(context.active_object)
@@ -79,23 +71,35 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
child_root_objects.remove(parent_root_object)
if parent_root_object is None or len(child_root_objects) == 0:
raise MessageException("No MMD Models selected")
raise NoModelSelectedError("No MMD Models selected")
logger.debug(f"Joining {len(child_root_objects)} models into parent model: {parent_root_object.name}")
with FnContext.temp_override_active_layer_collection(context, parent_root_object):
FnModel.join_models(parent_root_object, child_root_objects)
# Save original active_layer_collection
orig_active_layer_collection = context.view_layer.active_layer_collection
# Find layer collection containing parent_root_object and set it as active
layer_collection = FnContext.find_user_layer_collection_by_object(context, parent_root_object)
if layer_collection:
context.view_layer.active_layer_collection = layer_collection
# Execute the join operation
FnModel.join_models(parent_root_object, child_root_objects)
# Restore original active_layer_collection
context.view_layer.active_layer_collection = orig_active_layer_collection
bpy.ops.object.mode_set(mode="OBJECT")
parent_armature_object = FnModel.find_armature_object(parent_root_object)
FnContext.set_active_and_select_single_object(context, parent_armature_object)
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.armature.parent_set(type="OFFSET")
# Connect child bones
if self.join_type == "CONNECTED":
parent_edit_bone: EditBone = context.active_bone
child_edit_bones: Set[EditBone] = set(context.selected_bones)
parent_edit_bone: bpy.types.EditBone = context.active_bone
child_edit_bones: Set[bpy.types.EditBone] = set(context.selected_bones)
child_edit_bones.remove(parent_edit_bone)
logger.debug(f"Connecting {len(child_edit_bones)} child bones to parent bone: {parent_edit_bone.name}")
child_edit_bone: EditBone
child_edit_bone: bpy.types.EditBone
for child_edit_bone in child_edit_bones:
child_edit_bone.use_connect = True
@@ -105,6 +109,7 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
class ModelSeparateByBonesOperator(bpy.types.Operator):
bl_idname = "mmd_tools.model_separate_by_bones"
bl_label = "Model Separate by Bones"
bl_description = "Separate MMD model into multiple models based on selected bones.\n\nWARNING: This operation will split meshes, armatures, rigid bodies and joints. To move models before separating, only adjust the root (cross under the model) transformation. Do not move armatures, meshes, rigid bodies, or joints directly before separating as they will not move together.\n\nIMPORTANT: Don't use any of the 'Assembly' functions before using this function. This function requires the model to be in a clean state."
bl_options = {"REGISTER", "UNDO"}
separate_armature: bpy.props.BoolProperty(name="Separate Armature", default=True)
@@ -120,8 +125,8 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
)
@classmethod
def poll(cls, context: Context) -> bool:
active_object: Optional[Object] = context.active_object
def poll(cls, context: bpy.types.Context):
active_object: Optional[bpy.types.Object] = context.active_object
if context.mode != "POSE":
return False
@@ -137,155 +142,183 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
return len(context.selected_pose_bones) > 0
def invoke(self, context: Context, event: Any) -> Set[str]:
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: Context) -> Set[str]:
def execute(self, context: bpy.types.Context):
try:
logger.info("Starting model separate by bones operation")
self.separate(context)
logger.info("Model separate by bones completed successfully")
except MessageException as ex:
logger.error(f"Model separate by bones failed: {str(ex)}")
except NoModelSelectedError as ex:
self.report(type={"ERROR"}, message=str(ex))
return {"CANCELLED"}
return {"FINISHED"}
def separate(self, context: Context) -> None:
def separate(self, context: bpy.types.Context):
weight_threshold: float = self.weight_threshold
mmd_scale = 0.08
target_armature_object: Object = context.active_object
logger.debug(f"Target armature: {target_armature_object.name}")
target_armature_object: bpy.types.Object = context.active_object
bpy.ops.object.mode_set(mode="EDIT")
root_bones: Set[EditBone] = set(context.selected_bones)
logger.debug(f"Selected root bones: {len(root_bones)}")
root_bones: Set[bpy.types.EditBone] = set(context.selected_bones)
if self.include_descendant_bones:
logger.debug("Including descendant bones")
original_active_bone = context.active_bone
for edit_bone in root_bones:
with context.temp_override(active_bone=edit_bone):
bpy.ops.armature.select_similar(type="CHILDREN", threshold=0.1)
context.active_object.data.edit_bones.active = edit_bone
bpy.ops.armature.select_similar(type="CHILDREN", threshold=0.1)
self._select_related_ik_bones(target_armature_object)
if original_active_bone:
context.active_object.data.edit_bones.active = original_active_bone
separate_bones: Dict[str, EditBone] = {b.name: b for b in context.selected_bones}
deform_bones: Dict[str, EditBone] = {b.name: b for b in target_armature_object.data.edit_bones if b.use_deform}
logger.debug(f"Total bones to separate: {len(separate_bones)}")
mmd_root_object: Object = FnModel.find_root_object(context.active_object)
separate_bones: Dict[str, bpy.types.EditBone] = {b.name: b for b in context.selected_bones}
deform_bones: Dict[str, bpy.types.EditBone] = {b.name: b for b in target_armature_object.data.edit_bones if b.use_deform}
mmd_root_object: bpy.types.Object = FnModel.find_root_object(context.active_object)
mmd_model = Model(mmd_root_object)
mmd_model_mesh_objects: List[Object] = list(mmd_model.meshes())
logger.debug(f"Found {len(mmd_model_mesh_objects)} mesh objects in model")
mesh_selection_result = self.select_weighted_vertices(mmd_model_mesh_objects, separate_bones, deform_bones, weight_threshold)
mmd_model_mesh_objects = list(mesh_selection_result.keys())
logger.debug(f"Selected {len(mmd_model_mesh_objects)} mesh objects with weighted vertices")
# separate armature bones
separate_armature_object: Optional[Object]
if self.separate_armature:
logger.debug("Separating armature")
target_armature_object.select_set(True)
bpy.ops.armature.separate()
separate_armature_object = next(iter([a for a in context.selected_objects if a != target_armature_object]), None)
if separate_armature_object:
logger.debug(f"Created separate armature: {separate_armature_object.name}")
mmd_model_mesh_objects: List[bpy.types.Object] = list(mmd_model.meshes())
mmd_model_mesh_objects = list(self._select_weighted_vertices(mmd_model_mesh_objects, separate_bones, deform_bones, weight_threshold).keys())
bpy.ops.object.mode_set(mode="OBJECT")
# collect separate rigid bodies
separate_rigid_bodies: Set[Object] = {rigid_body_object for rigid_body_object in mmd_model.rigidBodies() if rigid_body_object.mmd_rigid.bone in separate_bones}
logger.debug(f"Found {len(separate_rigid_bodies)} rigid bodies to separate")
# Store original transform matrix for root object
original_matrix_world = mmd_root_object.matrix_world.copy()
mmd_root_object.matrix_world = Matrix.Identity(4)
# Reset object visibility
FnContext.set_active_and_select_single_object(context, mmd_root_object)
bpy.ops.mmd_tools.reset_object_visibility()
# Clean additional transform
FnContext.set_active_and_select_single_object(context, mmd_root_object)
bpy.ops.mmd_tools.clean_additional_transform()
# Create new separate model first
separate_model: Model = Model.create(mmd_root_object.mmd_root.name, mmd_root_object.mmd_root.name_e, mmd_scale, obj_name=mmd_root_object.name, add_root_bone=False)
separate_model.initialDisplayFrames()
separate_root_object = separate_model.rootObject()
separate_root_object.matrix_world = mmd_root_object.matrix_world
separate_model_armature_object = separate_model.armature()
# Now separate armature bones from original model
separate_armature_object: Optional[bpy.types.Object] = None
if self.separate_armature:
FnContext.set_active_and_select_single_object(context, target_armature_object)
bpy.ops.object.mode_set(mode="EDIT")
# Re-select the bones that should be separated (they might have been deselected)
for bone_name in separate_bones.keys():
if bone_name in target_armature_object.data.edit_bones:
target_armature_object.data.edit_bones[bone_name].select = True
bpy.ops.armature.separate()
separate_armature_object = next(iter([a for a in context.selected_objects if a != target_armature_object and a.type == "ARMATURE"]), None)
bpy.ops.object.mode_set(mode="OBJECT")
# Collect separate rigid bodies
separate_rigid_bodies: Set[bpy.types.Object] = {rigid_body_object for rigid_body_object in mmd_model.rigidBodies() if rigid_body_object.mmd_rigid.bone in separate_bones}
boundary_joint_owner_condition = any if self.boundary_joint_owner == "DESTINATION" else all
# collect separate joints
separate_joints: Set[Object] = {
# Collect separate joints
separate_joints: Set[bpy.types.Object] = {
joint_object
for joint_object in mmd_model.joints()
if boundary_joint_owner_condition(
[
joint_object.rigid_body_constraint.object1 in separate_rigid_bodies,
joint_object.rigid_body_constraint.object2 in separate_rigid_bodies,
]
],
)
}
logger.debug(f"Found {len(separate_joints)} joints to separate")
separate_mesh_objects: Set[Object]
model2separate_mesh_objects: Dict[Object, Object]
if len(mmd_model_mesh_objects) == 0:
logger.debug("No mesh objects to separate")
separate_mesh_objects = set()
model2separate_mesh_objects = dict()
else:
# select meshes
logger.debug("Selecting meshes for separation")
obj: Object
separate_mesh_objects: List[bpy.types.Object] = []
model2separate_mesh_objects: Dict[bpy.types.Object, bpy.types.Object] = {}
if len(mmd_model_mesh_objects) > 0:
# Find a single unique attribute name that doesn't conflict with any existing attributes.
all_attribute_names = {attr.name for obj in mmd_model_mesh_objects for attr in obj.data.attributes}
temp_normal_name = "mmd_temp_normal"
i = 0
while temp_normal_name in all_attribute_names:
temp_normal_name = f"mmd_temp_normal.{i:03d}"
i += 1
# Backup custom normals to the unique temporary attribute.
for mesh_obj in mmd_model_mesh_objects:
mesh_data = mesh_obj.data
existing_custom_normal = mesh_data.attributes.get("custom_normal")
if not existing_custom_normal:
continue
if existing_custom_normal.data_type == "INT16_2D":
normals_data = np.empty(len(mesh_data.loops) * 2, dtype=np.int16)
existing_custom_normal.data.foreach_get("value", normals_data)
temp_normal_attr = mesh_data.attributes.new(temp_normal_name, "INT16_2D", "CORNER")
temp_normal_attr.data.foreach_set("value", normals_data)
else:
raise TypeError(f"Unsupported custom_normal data type: '{existing_custom_normal.data_type}'. Supported types: 'INT16_2D'")
# Select meshes
obj: bpy.types.Object
for obj in context.view_layer.objects:
obj.select_set(obj in mmd_model_mesh_objects)
context.view_layer.objects.active = mmd_model_mesh_objects[0]
# separate mesh by selected vertices
logger.debug("Separating meshes by selected vertices")
# Separate mesh by selected vertices
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.separate(type="SELECTED")
separate_mesh_objects: List[Object] = [m for m in context.selected_objects if m.type == "MESH" and m not in mmd_model_mesh_objects]
separate_mesh_objects = [m for m in context.selected_objects if m.type == "MESH" and m not in mmd_model_mesh_objects]
bpy.ops.object.mode_set(mode="OBJECT")
logger.debug(f"Created {len(separate_mesh_objects)} separate mesh objects")
model2separate_mesh_objects = dict(zip(mmd_model_mesh_objects, separate_mesh_objects))
model2separate_mesh_objects = dict(zip(mmd_model_mesh_objects, separate_mesh_objects, strict=False))
logger.debug(f"Creating new model with scale {mmd_scale}")
separate_model: Model = Model.create(mmd_root_object.mmd_root.name, mmd_root_object.mmd_root.name_e, mmd_scale, add_root_bone=False)
# Restore normal data for all meshes (original and separated)
all_mesh_objects = list(mmd_model_mesh_objects) + list(separate_mesh_objects)
for mesh_obj in all_mesh_objects:
mesh_data = mesh_obj.data
temp_normal_attr = mesh_data.attributes.get(temp_normal_name)
if not temp_normal_attr:
continue
separate_model.initialDisplayFrames()
separate_root_object = separate_model.rootObject()
separate_root_object.matrix_world = mmd_root_object.matrix_world
separate_model_armature_object = separate_model.armature()
logger.debug(f"Created separate model with root: {separate_root_object.name}")
try:
if temp_normal_attr.data_type == "INT16_2D":
normals_data = np.empty(len(mesh_data.loops) * 2, dtype=np.int16)
temp_normal_attr.data.foreach_get("value", normals_data)
custom_normal_attr = mesh_data.attributes.get("custom_normal")
if not custom_normal_attr:
custom_normal_attr = mesh_data.attributes.new("custom_normal", "INT16_2D", "CORNER")
custom_normal_attr.data.foreach_set("value", normals_data)
else:
raise TypeError(f"Unsupported custom_normal data type: '{temp_normal_attr.data_type}'. Supported types: 'INT16_2D'")
finally:
mesh_data.attributes.remove(temp_normal_attr)
if self.separate_armature:
logger.debug("Joining separate armature to new model")
with context.temp_override(
active_object=separate_model_armature_object,
selected_editable_objects=[separate_model_armature_object, separate_armature_object],
):
if self.separate_armature and separate_armature_object:
separate_armature_data = separate_armature_object.data
with select_object(separate_model_armature_object, objects=[separate_model_armature_object, separate_armature_object]):
bpy.ops.object.join()
if separate_armature_data.users == 0:
bpy.data.armatures.remove(separate_armature_data)
# add mesh
logger.debug("Parenting separate mesh objects to new model")
with context.temp_override(
object=separate_model_armature_object,
selected_editable_objects=[separate_model_armature_object, *separate_mesh_objects],
):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
if separate_mesh_objects:
with select_object(separate_model_armature_object, objects=[separate_model_armature_object] + separate_mesh_objects):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
# replace mesh armature modifier.object
logger.debug("Updating armature modifiers on separate meshes")
# Replace mesh armature modifier.object
for separate_mesh in separate_mesh_objects:
armature_modifier: Optional[bpy.types.ArmatureModifier] = next(iter([m for m in separate_mesh.modifiers if m.type == "ARMATURE"]), None)
if armature_modifier is None:
logger.debug(f"Creating new armature modifier for {separate_mesh.name}")
armature_modifier: bpy.types.ArmatureModifier = separate_mesh.modifiers.new("mmd_bone_order_override", "ARMATURE")
armature_modifier: bpy.types.ArmatureModifier = separate_mesh.modifiers.new("mmd_armature", "ARMATURE")
armature_modifier.object = separate_model_armature_object
logger.debug("Parenting rigid bodies to new model")
with context.temp_override(
object=separate_model.rigidGroupObject(),
selected_editable_objects=[separate_model.rigidGroupObject(), *separate_rigid_bodies],
):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
if separate_rigid_bodies:
with select_object(separate_model.rigidGroupObject(), objects=[separate_model.rigidGroupObject()] + list(separate_rigid_bodies)):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
logger.debug("Parenting joints to new model")
with context.temp_override(
object=separate_model.jointGroupObject(),
selected_editable_objects=[separate_model.jointGroupObject(), *separate_joints],
):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
if separate_joints:
with select_object(separate_model.jointGroupObject(), objects=[separate_model.jointGroupObject()] + list(separate_joints)):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
# move separate objects to new collection
# Move separate objects to new collection
mmd_layer_collection = FnContext.find_user_layer_collection_by_object(context, mmd_root_object)
assert mmd_layer_collection is not None
@@ -293,31 +326,42 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
assert separate_layer_collection is not None
if mmd_layer_collection.name != separate_layer_collection.name:
logger.debug(f"Moving objects from collection {mmd_layer_collection.name} to {separate_layer_collection.name}")
for separate_object in itertools.chain(separate_mesh_objects, separate_rigid_bodies, separate_joints):
separate_layer_collection.collection.objects.link(separate_object)
mmd_layer_collection.collection.objects.unlink(separate_object)
if separate_object.name not in separate_layer_collection.collection.objects:
separate_layer_collection.collection.objects.link(separate_object)
if separate_object.name in mmd_layer_collection.collection.objects:
mmd_layer_collection.collection.objects.unlink(separate_object)
logger.debug("Copying MMD root properties")
FnModel.copy_mmd_root(
separate_root_object,
mmd_root_object,
overwrite=True,
replace_name2values={
# replace related_mesh property values
"related_mesh": {m.data.name: s.data.name for m, s in model2separate_mesh_objects.items()}
# Replace related_mesh property values
"related_mesh": {m.data.name: s.data.name for m, s in model2separate_mesh_objects.items()},
},
)
def select_weighted_vertices(self, mmd_model_mesh_objects: List[Object], separate_bones: Dict[str, EditBone], deform_bones: Dict[str, EditBone], weight_threshold: float) -> Dict[Object, int]:
"""Select vertices weighted to the bones to be separated"""
logger.debug(f"Selecting vertices weighted to {len(separate_bones)} bones with threshold {weight_threshold}")
mesh2selected_vertex_count: Dict[Object, int] = dict()
# Apply additional transform
FnContext.set_active_and_select_single_object(context, mmd_root_object)
bpy.ops.mmd_tools.apply_additional_transform()
FnContext.set_active_and_select_single_object(context, separate_root_object)
bpy.ops.mmd_tools.apply_additional_transform()
# Restore original transform matrix for root object
mmd_root_object.matrix_world = original_matrix_world
separate_root_object.matrix_world = original_matrix_world
# End state
FnContext.set_active_and_select_single_object(context, separate_root_object)
def _select_weighted_vertices(self, mmd_model_mesh_objects: List[bpy.types.Object], separate_bones: Dict[str, bpy.types.EditBone], deform_bones: Dict[str, bpy.types.EditBone], weight_threshold: float) -> Dict[bpy.types.Object, int]:
mesh2selected_vertex_count: Dict[bpy.types.Object, int] = {}
target_bmesh: bmesh.types.BMesh = bmesh.new()
for mesh_object in mmd_model_mesh_objects:
vertex_groups: bpy.types.VertexGroups = mesh_object.vertex_groups
mesh: Mesh = mesh_object.data
mesh: bpy.types.Mesh = mesh_object.data
target_bmesh.from_mesh(mesh, face_normals=False)
target_bmesh.select_mode |= {"VERT"}
deform_layer = target_bmesh.verts.layers.deform.verify()
@@ -344,7 +388,6 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
vert.select_set(True)
if selected_vertex_count > 0:
logger.debug(f"Selected {selected_vertex_count} vertices in mesh {mesh_object.name}")
mesh2selected_vertex_count[mesh_object] = selected_vertex_count
target_bmesh.select_flush_mode()
target_bmesh.to_mesh(mesh)
@@ -352,3 +395,61 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
target_bmesh.clear()
return mesh2selected_vertex_count
def _select_related_ik_bones(self, armature_object: bpy.types.Object) -> None:
"""
Expand the current selection to include any full IK systems that are
partially selected. An IK system includes the chain bones, the IK
target bone, and the pole target bone.
NOTE: This method operates entirely in EDIT mode and avoids mode switching
to prevent segmentation faults.
"""
edit_bones = armature_object.data.edit_bones
initial_selection_names = {b.name for b in edit_bones if b.select}
# Access pose bones constraints directly without mode switching
pose_bones = armature_object.pose.bones
# Find all complete IK systems
ik_systems = []
for pose_bone in pose_bones:
for constraint in pose_bone.constraints:
if constraint.type == "IK":
# Build the set of bones in this IK system
system_bones = {pose_bone.name}
# Add the main IK Target bone
if constraint.target and constraint.subtarget:
system_bones.add(constraint.subtarget)
# Add the Pole Target bone
if constraint.pole_target and constraint.pole_subtarget:
system_bones.add(constraint.pole_subtarget)
# Add all other bones in the IK chain
current_bone_name = pose_bone.name
chain_count = constraint.chain_count
# Walk up the parent chain
for _ in range(chain_count - 1):
if current_bone_name not in edit_bones:
break
current_bone = edit_bones[current_bone_name]
if not current_bone.parent:
break
current_bone_name = current_bone.parent.name
system_bones.add(current_bone_name)
ik_systems.append(system_bones)
# Expand selection to include any related, full IK systems
final_selection_names = set(initial_selection_names)
for system in ik_systems:
if not system.isdisjoint(initial_selection_names):
final_selection_names.update(system)
# Apply the final selection
for bone in edit_bones:
bone.select = bone.name in final_selection_names
+402 -112
View File
@@ -1,30 +1,26 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
# Copyright 2015 MMD Tools authors
# This file is part of MMD Tools.
from typing import Optional, cast, List, Dict, Any, Set, Tuple, Union
from collections import namedtuple
from typing import Optional, cast
import bpy
from mathutils import Quaternion, Vector
from ..core.model import FnModel
from .. import bpyutils, utils
from ..core.exceptions import MaterialNotFoundError
from ..core.material import FnMaterial
from ..core.model import FnModel
from ..core.morph import FnMorph
from ..utils import ItemMoveOp, ItemOp
from ....logging_setup import logger
# Util functions
def divide_vector_components(vec1: List[float], vec2: List[float]) -> List[float]:
def divide_vector_components(vec1, vec2):
if len(vec1) != len(vec2):
raise ValueError("Vectors should have the same number of components")
result = []
for v1, v2 in zip(vec1, vec2):
for v1, v2 in zip(vec1, vec2, strict=False):
if v2 == 0:
if v1 == 0:
v2 = 1 # If we have a 0/0 case we change the divisor to 1
@@ -34,17 +30,17 @@ def divide_vector_components(vec1: List[float], vec2: List[float]) -> List[float
return result
def multiply_vector_components(vec1: List[float], vec2: List[float]) -> List[float]:
def multiply_vector_components(vec1, vec2):
if len(vec1) != len(vec2):
raise ValueError("Vectors should have the same number of components")
result = []
for v1, v2 in zip(vec1, vec2):
for v1, v2 in zip(vec1, vec2, strict=False):
result.append(v1 * v2)
return result
def special_division(n1: float, n2: float) -> float:
"""This function returns 0 in case of 0/0. If non-zero divided by zero case is found, an Exception is raised"""
def special_division(n1, n2):
"""Return 0 in case of 0/0. If non-zero divided by zero case is found, an Exception is raised"""
if n2 == 0:
if n1 == 0:
n2 = 1
@@ -59,7 +55,7 @@ class AddMorph(bpy.types.Operator):
bl_description = "Add a morph item to active morph list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
@@ -69,7 +65,6 @@ class AddMorph(bpy.types.Operator):
morph.name = "New Morph"
if morph_type.startswith("uv"):
morph.data_type = "VERTEX_GROUP"
logger.debug(f"Added new morph of type {morph_type}")
return {"FINISHED"}
@@ -86,7 +81,7 @@ class RemoveMorph(bpy.types.Operator):
options={"SKIP_SAVE"},
)
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
@@ -101,21 +96,19 @@ class RemoveMorph(bpy.types.Operator):
if self.all:
morphs.clear()
mmd_root.active_morph = 0
logger.debug(f"Removed all morphs of type {morph_type}")
else:
morphs.remove(mmd_root.active_morph)
mmd_root.active_morph = max(0, mmd_root.active_morph - 1)
logger.debug(f"Removed morph at index {mmd_root.active_morph} of type {morph_type}")
return {"FINISHED"}
class MoveMorph(bpy.types.Operator, ItemMoveOp):
bl_idname = "mmd_tools.morph_move"
bl_label = "Move Morph"
bl_description = "Move active morph item up/down in the list"
bl_description = "Move active morph item up/down in the list. This will not affect the morph order in exported PMX files (use Display Panel order instead)."
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
@@ -124,7 +117,6 @@ class MoveMorph(bpy.types.Operator, ItemMoveOp):
mmd_root.active_morph,
self.type,
)
logger.debug(f"Moved morph to index {mmd_root.active_morph}")
return {"FINISHED"}
@@ -134,7 +126,7 @@ class CopyMorph(bpy.types.Operator):
bl_description = "Make a copy of active morph in the list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -146,7 +138,7 @@ class CopyMorph(bpy.types.Operator):
if morph is None:
return {"CANCELLED"}
name_orig, name_tmp = morph.name, "_tmp%s" % str(morph.as_pointer())
name_orig, name_tmp = morph.name, f"_tmp{str(morph.as_pointer())}"
if morph_type.startswith("vertex"):
for obj in FnModel.iterate_mesh_objects(root):
@@ -161,7 +153,6 @@ class CopyMorph(bpy.types.Operator):
for k, v in morph.items():
morph_new[k] = v if k != "name" else name_tmp
morph_new.name = name_orig + "_copy" # trigger name check
logger.debug(f"Copied morph {name_orig} to {morph_new.name}")
return {"FINISHED"}
@@ -171,17 +162,14 @@ class OverwriteBoneMorphsFromActionPose(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
def poll(cls, context):
root = FnModel.find_root_object(context.active_object)
if root is None:
return False
return root is not None and root.mmd_root.active_morph_type == "bone_morphs"
return root.mmd_root.active_morph_type == "bone_morphs"
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
root = FnModel.find_root_object(context.active_object)
FnMorph.overwrite_bone_morphs_from_action_pose(FnModel.find_armature_object(root))
logger.info("Overwrote bone morphs from active action pose")
return {"FINISHED"}
@@ -191,7 +179,7 @@ class AddMorphOffset(bpy.types.Operator):
bl_description = "Add a morph offset item to the list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
@@ -216,7 +204,6 @@ class AddMorphOffset(bpy.types.Operator):
item.location = pose_bone.location
item.rotation = pose_bone.rotation_quaternion
logger.debug(f"Added morph offset to {morph_type}")
return {"FINISHED"}
@@ -233,7 +220,7 @@ class RemoveMorphOffset(bpy.types.Operator):
options={"SKIP_SAVE"},
)
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -250,21 +237,17 @@ class RemoveMorphOffset(bpy.types.Operator):
if morph_type.startswith("vertex"):
for obj in FnModel.iterate_mesh_objects(root):
FnMorph.remove_shape_key(obj, morph.name)
logger.debug(f"Removed all vertex morph offsets for {morph.name}")
return {"FINISHED"}
elif morph_type.startswith("uv"):
if morph_type.startswith("uv"):
if morph.data_type == "VERTEX_GROUP":
for obj in FnModel.iterate_mesh_objects(root):
FnMorph.store_uv_morph_data(obj, morph)
logger.debug(f"Removed all UV morph offsets for {morph.name}")
return {"FINISHED"}
morph.data.clear()
morph.active_data = 0
logger.debug(f"Cleared all morph offsets for {morph.name}")
else:
morph.data.remove(morph.active_data)
morph.active_data = max(0, morph.active_data - 1)
logger.debug(f"Removed morph offset at index {morph.active_data}")
return {"FINISHED"}
@@ -280,7 +263,7 @@ class InitMaterialOffset(bpy.types.Operator):
default=0,
)
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
@@ -292,7 +275,6 @@ class InitMaterialOffset(bpy.types.Operator):
mat_data.specular_color = mat_data.ambient_color = (val,) * 3
mat_data.shininess = mat_data.edge_weight = val
mat_data.texture_factor = mat_data.toon_texture_factor = mat_data.sphere_texture_factor = (val,) * 4
logger.debug(f"Initialized material offset with value {val}")
return {"FINISHED"}
@@ -302,7 +284,7 @@ class ApplyMaterialOffset(bpy.types.Operator):
bl_description = "Calculates the offsets and apply them, then the temporary material is removed"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
@@ -340,7 +322,6 @@ class ApplyMaterialOffset(bpy.types.Operator):
except ZeroDivisionError:
mat_data.offset_type = "ADD" # If there is any 0 division we automatically switch it to type ADD
logger.warning("Zero division detected, switching to ADD offset type")
except ValueError:
self.report({"ERROR"}, "An unexpected error happened")
# We should stop on our tracks and re-raise the exception
@@ -358,7 +339,6 @@ class ApplyMaterialOffset(bpy.types.Operator):
mat_data.edge_weight = work_mmd_mat.edge_weight - base_mmd_mat.edge_weight
FnMaterial.clean_materials(meshObj, can_remove=lambda m: m == work_mat)
logger.info(f"Applied material offset for {mat_data.material}")
return {"FINISHED"}
@@ -368,7 +348,7 @@ class CreateWorkMaterial(bpy.types.Operator):
bl_description = "Creates a temporary material to edit this offset"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
@@ -385,12 +365,12 @@ class CreateWorkMaterial(bpy.types.Operator):
base_mat = meshObj.data.materials.get(mat_data.material, None)
if base_mat is None:
self.report({"ERROR"}, 'Material "%s" not found' % mat_data.material)
self.report({"ERROR"}, f'Material "{mat_data.material}" not found')
return {"CANCELLED"}
work_mat_name = base_mat.name + "_temp"
if work_mat_name in bpy.data.materials:
self.report({"ERROR"}, 'Temporary material "%s" is in use' % work_mat_name)
self.report({"ERROR"}, f'Temporary material "{work_mat_name}" is in use')
return {"CANCELLED"}
work_mat = base_mat.copy()
@@ -427,7 +407,6 @@ class CreateWorkMaterial(bpy.types.Operator):
work_mmd_mat.edge_color = list(edge_offset)
work_mmd_mat.edge_weight += mat_data.edge_weight
logger.info(f"Created work material {work_mat_name}")
return {"FINISHED"}
@@ -437,24 +416,23 @@ class ClearTempMaterials(bpy.types.Operator):
bl_description = "Clears all the temporary materials"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
for meshObj in FnModel.iterate_mesh_objects(root):
def __pre_remove(m: Optional[bpy.types.Material]) -> bool:
def __pre_remove(m, meshObj=meshObj):
if m and "_temp" in m.name:
base_mat_name = m.name.split("_temp")[0]
try:
FnMaterial.swap_materials(meshObj, m.name, base_mat_name)
return True
except MaterialNotFoundError:
self.report({"WARNING"}, "Base material for %s was not found" % m.name)
self.report({"WARNING"}, f"Base material for {m.name} was not found")
return False
FnMaterial.clean_materials(meshObj, can_remove=__pre_remove)
logger.info("Cleared all temporary materials")
return {"FINISHED"}
@@ -464,7 +442,7 @@ class ViewBoneMorph(bpy.types.Operator):
bl_description = "View the result of active bone morph"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -475,12 +453,10 @@ class ViewBoneMorph(bpy.types.Operator):
for morph_data in morph.data:
p_bone: Optional[bpy.types.PoseBone] = armature.pose.bones.get(morph_data.bone, None)
if p_bone:
# Blender 5.0: use pose bone select property directly
p_bone.select = True
mtx = (p_bone.matrix_basis.to_3x3() @ Quaternion(*morph_data.rotation.to_axis_angle()).to_matrix()).to_4x4()
mtx.translation = p_bone.location + morph_data.location
p_bone.matrix_basis = mtx
logger.info(f"Viewing bone morph: {morph.name}")
return {"FINISHED"}
@@ -490,14 +466,13 @@ class ClearBoneMorphView(bpy.types.Operator):
bl_description = "Reset transforms of all bones to their default values"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
armature = FnModel.find_armature_object(root)
for p_bone in armature.pose.bones:
p_bone.matrix_basis.identity()
logger.info("Cleared bone morph view")
return {"FINISHED"}
@@ -507,7 +482,7 @@ class ApplyBoneMorph(bpy.types.Operator):
bl_description = "Apply current pose to active bone morph"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -522,11 +497,9 @@ class ApplyBoneMorph(bpy.types.Operator):
item.bone = p_bone.name
item.location = p_bone.location
item.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion()
# Blender 5.0: use pose bone select property directly
p_bone.select = True
else:
p_bone.select = False
logger.info(f"Applied current pose to bone morph: {morph.name}")
return {"FINISHED"}
@@ -536,7 +509,7 @@ class SelectRelatedBone(bpy.types.Operator):
bl_description = "Select the bone assigned to this offset in the armature"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -545,7 +518,6 @@ class SelectRelatedBone(bpy.types.Operator):
morph = mmd_root.bone_morphs[mmd_root.active_morph]
morph_data = morph.data[morph.active_data]
utils.selectSingleBone(context, armature, morph_data.bone)
logger.debug(f"Selected bone: {morph_data.bone}")
return {"FINISHED"}
@@ -555,7 +527,7 @@ class EditBoneOffset(bpy.types.Operator):
bl_description = "Applies the location and rotation of this offset to the bone"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -568,7 +540,6 @@ class EditBoneOffset(bpy.types.Operator):
mtx.translation = morph_data.location
p_bone.matrix_basis = mtx
utils.selectSingleBone(context, armature, p_bone.name)
logger.debug(f"Edited bone offset for {p_bone.name}")
return {"FINISHED"}
@@ -578,7 +549,7 @@ class ApplyBoneOffset(bpy.types.Operator):
bl_description = "Stores the current bone location and rotation into this offset"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -590,7 +561,6 @@ class ApplyBoneOffset(bpy.types.Operator):
p_bone = armature.pose.bones[morph_data.bone]
morph_data.location = p_bone.location
morph_data.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion()
logger.debug(f"Applied bone offset for {p_bone.name}")
return {"FINISHED"}
@@ -600,7 +570,7 @@ class ViewUVMorph(bpy.types.Operator):
bl_description = "View the result of active UV morph on current mesh object"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -618,11 +588,11 @@ class ViewUVMorph(bpy.types.Operator):
selected = meshObj.select_get()
with bpyutils.select_object(meshObj):
mesh = cast(bpy.types.Mesh, meshObj.data)
mesh = cast("bpy.types.Mesh", meshObj.data)
morph = mmd_root.uv_morphs[mmd_root.active_morph]
uv_textures = mesh.uv_layers
base_uv_layers = [l for l in mesh.uv_layers if not l.name.startswith("_")]
base_uv_layers = [layer for layer in mesh.uv_layers if not layer.name.startswith("_")]
if morph.uv_index >= len(base_uv_layers):
self.report({"ERROR"}, "Invalid uv index: %d" % morph.uv_index)
return {"CANCELLED"}
@@ -632,7 +602,7 @@ class ViewUVMorph(bpy.types.Operator):
uv_textures.active = uv_textures[uv_layer_name]
uv_layer_name = uv_textures.active.name
uv_tex = uv_textures.new(name="__uv.%s" % uv_layer_name)
uv_tex = uv_textures.new(name=f"__uv.{uv_layer_name}")
if uv_tex is None:
self.report({"ERROR"}, "Failed to create a temporary uv layer")
return {"CANCELLED"}
@@ -642,17 +612,15 @@ class ViewUVMorph(bpy.types.Operator):
if len(offsets) > 0:
base_uv_data = mesh.uv_layers.active.data
temp_uv_data = mesh.uv_layers[uv_tex.name].data
for i, l in enumerate(mesh.loops):
# Blender 5.0+: UV selection is now stored in face-corner attributes
# Skipping UV selection assignment as it's not critical for morph preview
select = l.vertex_index in offsets
for i, loop in enumerate(mesh.loops):
select = temp_uv_data[i].select = loop.vertex_index in offsets
if select:
temp_uv_data[i].uv = base_uv_data[i].uv + offsets[l.vertex_index]
temp_uv_data[i].uv = base_uv_data[i].uv + offsets[loop.vertex_index]
uv_textures.active = uv_tex
uv_tex.active_render = True
meshObj.hide_set(False)
meshObj.select_set(selected)
logger.info(f"Viewing UV morph: {morph.name}")
return {"FINISHED"}
@@ -662,24 +630,24 @@ class ClearUVMorphView(bpy.types.Operator):
bl_description = "Clear all temporary data of UV morphs"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
for m in FnModel.iterate_mesh_objects(root):
mesh = m.data
uv_layers = mesh.uv_layers
for t in list(uv_layers): # Create a copy to iterate safely
uv_textures = getattr(mesh, "uv_textures", mesh.uv_layers)
for t in reversed(uv_textures):
if t.name.startswith("__uv."):
uv_layers.remove(t)
if len(uv_layers) > 0:
# Only set active_index
uv_layers.active_index = 0
uv_textures.remove(t)
if len(uv_textures) > 0:
uv_textures[0].active_render = True
uv_textures.active_index = 0
animation_data = mesh.animation_data
if animation_data:
nla_tracks = animation_data.nla_tracks
for t in nla_tracks:
for t in reversed(nla_tracks):
if t.name.startswith("__uv."):
nla_tracks.remove(t)
if animation_data.action and animation_data.action.name.startswith("__uv."):
@@ -687,10 +655,9 @@ class ClearUVMorphView(bpy.types.Operator):
if animation_data.action is None and len(nla_tracks) == 0:
mesh.animation_data_clear()
for act in bpy.data.actions:
for act in reversed(bpy.data.actions):
if act.name.startswith("__uv.") and act.users < 1:
bpy.data.actions.remove(act)
logger.info("Cleared UV morph view")
return {"FINISHED"}
@@ -701,20 +668,20 @@ class EditUVMorph(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
def poll(cls, context):
obj = context.active_object
if obj.type != "MESH":
if obj is None or obj.type != "MESH":
return False
active_uv_layer = obj.data.uv_layers.active
return active_uv_layer and active_uv_layer.name.startswith("__uv.")
return active_uv_layer is not None and active_uv_layer.name.startswith("__uv.")
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
meshObj = obj
selected = meshObj.select_get()
with bpyutils.select_object(meshObj):
mesh = cast(bpy.types.Mesh, meshObj.data)
mesh = cast("bpy.types.Mesh", meshObj.data)
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.select_mode(type="VERT", action="ENABLE")
bpy.ops.mesh.reveal() # unhide all vertices
@@ -722,16 +689,15 @@ class EditUVMorph(bpy.types.Operator):
bpy.ops.object.mode_set(mode="OBJECT")
vertices = mesh.vertices
for l, d in zip(mesh.loops, mesh.uv_layers.active.data):
for loop, d in zip(mesh.loops, mesh.uv_layers.active.data, strict=False):
if d.select:
vertices[l.vertex_index].select = True
vertices[loop.vertex_index].select = True
polygons = mesh.polygons
polygons.active = getattr(next((p for p in polygons if all(vertices[i].select for i in p.vertices)), None), "index", polygons.active)
bpy.ops.object.mode_set(mode="EDIT")
meshObj.select_set(selected)
logger.info("Editing UV morph")
return {"FINISHED"}
@@ -742,14 +708,14 @@ class ApplyUVMorph(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
def poll(cls, context):
obj = context.active_object
if obj.type != "MESH":
if obj is None or obj.type != "MESH":
return False
active_uv_layer = obj.data.uv_layers.active
return active_uv_layer and active_uv_layer.name.startswith("__uv.")
return active_uv_layer is not None and active_uv_layer.name.startswith("__uv.")
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
@@ -757,34 +723,31 @@ class ApplyUVMorph(bpy.types.Operator):
selected = meshObj.select_get()
with bpyutils.select_object(meshObj):
mesh = cast(bpy.types.Mesh, meshObj.data)
mesh = cast("bpy.types.Mesh", meshObj.data)
morph = mmd_root.uv_morphs[mmd_root.active_morph]
base_uv_name = mesh.uv_layers.active.name[5:]
if base_uv_name not in mesh.uv_layers:
self.report({"ERROR"}, ' * UV map "%s" not found' % base_uv_name)
self.report({"ERROR"}, f' * UV map "{base_uv_name}" not found')
return {"CANCELLED"}
base_uv_data = mesh.uv_layers[base_uv_name].data
temp_uv_data = mesh.uv_layers.active.data
axis_type = "ZW" if base_uv_name.startswith("_") else "XY"
from collections import namedtuple
__OffsetData = namedtuple("OffsetData", "index, offset")
offsets = {}
vertices = mesh.vertices
for l, i0, i1 in zip(mesh.loops, base_uv_data, temp_uv_data):
if vertices[l.vertex_index].select and l.vertex_index not in offsets:
for loop, i0, i1 in zip(mesh.loops, base_uv_data, temp_uv_data, strict=False):
if vertices[loop.vertex_index].select and loop.vertex_index not in offsets:
dx, dy = i1.uv - i0.uv
if abs(dx) > 0.0001 or abs(dy) > 0.0001:
offsets[l.vertex_index] = __OffsetData(l.vertex_index, (dx, dy, dx, dy))
offsets[loop.vertex_index] = __OffsetData(loop.vertex_index, (dx, dy, dx, dy))
FnMorph.store_uv_morph_data(meshObj, morph, offsets.values(), axis_type)
morph.data_type = "VERTEX_GROUP"
meshObj.select_set(selected)
logger.info(f"Applied UV morph: {morph.name}")
return {"FINISHED"}
@@ -795,12 +758,339 @@ class CleanDuplicatedMaterialMorphs(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
return FnModel.find_root_object(context.active_object) is not None
def poll(cls, context):
root = FnModel.find_root_object(context.active_object)
return root is not None
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context: bpy.types.Context):
mmd_root_object = FnModel.find_root_object(context.active_object)
FnMorph.clean_duplicated_material_morphs(mmd_root_object)
logger.info("Cleaned duplicated material morphs")
return {"FINISHED"}
class ConvertBoneMorphToVertexMorph(bpy.types.Operator):
bl_idname = "mmd_tools.convert_bone_morph_to_vertex_morph"
bl_label = "Convert To Vertex Morph"
bl_description = "Convert a bone morph into a single vertex morph by applying the bone transformations.\nIf a corresponding vertex morph already exists, it will be updated."
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod
def poll(cls, context):
root = FnModel.find_root_object(context.active_object)
if root is None:
return False
mmd_root = root.mmd_root
if mmd_root.active_morph_type != "bone_morphs":
return False
morph = ItemOp.get_by_index(mmd_root.bone_morphs, mmd_root.active_morph)
return morph is not None and len(morph.data) > 0
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
# Get the active bone morph
bone_morph = ItemOp.get_by_index(mmd_root.bone_morphs, mmd_root.active_morph)
if bone_morph is None:
self.report({"ERROR"}, "No active bone morph")
return {"CANCELLED"}
original_name = bone_morph.name
target_name = original_name
# Add 'B' suffix if necessary
if not original_name.endswith("B"):
bone_morph.name = original_name + "B"
target_name = original_name
else:
# If already has B suffix, use name without B
target_name = original_name[:-1]
try:
# Step 1: import
from ..core.model import Model
rig = Model(root)
# Ensure morph slider is bound
bpy.ops.mmd_tools.morph_slider_setup(type="BIND")
# Re-obtain placeholder object
placeholder_obj = rig.morph_slider.placeholder()
if placeholder_obj is None or placeholder_obj.data.shape_keys is None:
self.report({"ERROR"}, "Failed to create morph slider system")
return {"CANCELLED"}
shape_keys = placeholder_obj.data.shape_keys
key_blocks = shape_keys.key_blocks
# Step 2: Check if target bone morph exists
current_morph_name = bone_morph.name
if current_morph_name not in key_blocks:
self.report({"ERROR"}, f"Bone morph '{current_morph_name}' not found in morph sliders")
return {"CANCELLED"}
# Step 3: Save all current morph values
original_values = {}
for key_block in key_blocks:
if key_block.name != "--- morph sliders ---":
original_values[key_block.name] = key_block.value
# Step 4: Set all morphs to 0
for key_block in key_blocks:
if key_block.name != "--- morph sliders ---":
key_block.value = 0
# Step 5: Set target bone morph to 1.0
key_blocks[current_morph_name].value = 1.0
# Step 6: Use Armature Modifier's "Apply as Shape Key" functionality
created_shape_keys = []
for mesh_obj in FnModel.iterate_mesh_objects(root):
# Switch to this mesh object
context.view_layer.objects.active = mesh_obj
# Ensure mesh object has shape keys
if mesh_obj.data.shape_keys is None:
mesh_obj.shape_key_add(name="Basis", from_mix=False)
# Delete existing shape key with same name
if target_name in mesh_obj.data.shape_keys.key_blocks:
idx = mesh_obj.data.shape_keys.key_blocks.find(target_name)
if idx >= 0:
mesh_obj.active_shape_key_index = idx
bpy.ops.object.shape_key_remove()
# Find armature modifier
armature_modifier = None
for modifier in mesh_obj.modifiers:
if modifier.type == "ARMATURE":
armature_modifier = modifier
break
if armature_modifier is None:
self.report({"WARNING"}, f"No armature modifier found on mesh '{mesh_obj.name}'")
continue
# Use Apply as Shape Key functionality, keeping the modifier
bpy.ops.object.modifier_apply_as_shapekey(modifier=armature_modifier.name, keep_modifier=True)
# Rename the newly created shape key to target name
shape_key_blocks = mesh_obj.data.shape_keys.key_blocks
new_shape_key = shape_key_blocks[-1] # Latest created shape key
new_shape_key.name = target_name
new_shape_key.value = 0.0 # Set to 0 to avoid double effect
created_shape_keys.append((mesh_obj.name, target_name))
self.report({"INFO"}, f"Created shape key '{target_name}' on mesh '{mesh_obj.name}'")
# Step 7: Restore all original morph values
for key_name, original_value in original_values.items():
if key_name in key_blocks:
key_blocks[key_name].value = original_value
# Step 8: Create or update vertex morph entry
vertex_morph_exists = False
for i, morph in enumerate(mmd_root.vertex_morphs):
if morph.name == target_name:
vertex_morph_exists = True
mmd_root.active_morph_type = "vertex_morphs"
mmd_root.active_morph = i
break
if not vertex_morph_exists:
mmd_root.active_morph_type = "vertex_morphs"
morph, mmd_root.active_morph = ItemOp.add_after(mmd_root.vertex_morphs, mmd_root.active_morph)
morph.name = target_name
# Step 9: Add to facial expression display frame
facial_frame = None
for frame in mmd_root.display_item_frames:
if frame.name == "表情":
facial_frame = frame
break
if facial_frame:
morph_exists_in_frame = False
for item in facial_frame.data:
if item.type == "MORPH" and item.name == target_name and item.morph_type == "vertex_morphs":
morph_exists_in_frame = True
break
if not morph_exists_in_frame:
new_item = facial_frame.data.add()
new_item.type = "MORPH"
new_item.morph_type = "vertex_morphs"
new_item.name = target_name
facial_frame.active_item = len(facial_frame.data) - 1
for i, frame in enumerate(mmd_root.display_item_frames):
if frame.name == "表情":
mmd_root.active_display_item_frame = i
break
# UNBIND
bpy.ops.mmd_tools.morph_slider_setup(type="UNBIND")
# Success message
shape_key_info = ", ".join([f"{mesh}:{key}" for mesh, key in created_shape_keys])
self.report({"INFO"}, f"Successfully converted bone morph '{original_name}' to vertex morph '{target_name}'. Created shape keys: {shape_key_info}")
except Exception as e:
self.report({"ERROR"}, f"Error during conversion: {str(e)}")
return {"CANCELLED"}
return {"FINISHED"}
class ConvertGroupMorphToVertexMorph(bpy.types.Operator):
bl_idname = "mmd_tools.convert_group_morph_to_vertex_morph"
bl_label = "Convert To Vertex Morph"
bl_description = "Convert a group morph into a single vertex morph by merging only the vertex morphs within the group.\nIf a corresponding vertex morph already exists, it will be updated."
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod
def poll(cls, context):
root = FnModel.find_root_object(context.active_object)
if root is None:
return False
mmd_root = root.mmd_root
if mmd_root.active_morph_type != "group_morphs":
return False
morph = ItemOp.get_by_index(mmd_root.group_morphs, mmd_root.active_morph)
return morph is not None and len(morph.data) > 0
def execute(self, context):
bpy.ops.mmd_tools.morph_slider_setup(type="UNBIND")
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
# Get the active group morph
group_morph = ItemOp.get_by_index(mmd_root.group_morphs, mmd_root.active_morph)
if group_morph is None:
self.report({"ERROR"}, "No active group morph")
return {"CANCELLED"}
# Check if the group morph contains any vertex morphs to convert
has_vertex_morphs = False
for offset in group_morph.data:
if offset.morph_type == "vertex_morphs":
has_vertex_morphs = True
break
if not has_vertex_morphs:
self.report({"ERROR"}, "The group morph does not contain any vertex morphs to convert")
return {"CANCELLED"}
original_name = group_morph.name
target_name = original_name
# Add 'G' suffix if necessary
if not original_name.endswith("G"):
group_morph.name = original_name + "G"
target_name = original_name
else:
# If already has G suffix, use name without G
target_name = original_name[:-1]
# First, reset all shape keys to zero
for obj in FnModel.iterate_mesh_objects(root):
if obj.data.shape_keys:
for kb in obj.data.shape_keys.key_blocks:
kb.value = 0
# Apply only the vertex morphs from the group morph
for offset in group_morph.data:
if offset.morph_type == "vertex_morphs":
# Find the vertex morph by name
vertex_morph = getattr(root.mmd_root, offset.morph_type).get(offset.name)
if vertex_morph:
# Apply this morph at the specified factor
for obj in FnModel.iterate_mesh_objects(root):
if obj.data.shape_keys:
kb = obj.data.shape_keys.key_blocks.get(offset.name)
if kb:
kb.value = offset.factor
# Now add a new shape key from mix for each mesh
for obj in FnModel.iterate_mesh_objects(root):
if obj.data.shape_keys:
# Make this the active object
context.view_layer.objects.active = obj
# Remove existing shape key if it exists
if target_name in obj.data.shape_keys.key_blocks:
idx = obj.data.shape_keys.key_blocks.find(target_name)
if idx >= 0:
obj.active_shape_key_index = idx
bpy.ops.object.shape_key_remove()
# Add shape key from mix
bpy.ops.object.shape_key_add(from_mix=True)
# Rename the newly created shape key
new_key = obj.data.shape_keys.key_blocks[-1]
new_key.name = target_name
# Check if a vertex morph with the target name already exists
vertex_morph_exists = False
for i, morph in enumerate(mmd_root.vertex_morphs):
if morph.name == target_name:
vertex_morph_exists = True
mmd_root.active_morph_type = "vertex_morphs"
mmd_root.active_morph = i
break
# If not, create a new vertex morph
if not vertex_morph_exists:
# Switch to vertex morphs panel
mmd_root.active_morph_type = "vertex_morphs"
# Add new vertex morph
morph, mmd_root.active_morph = ItemOp.add_after(mmd_root.vertex_morphs, mmd_root.active_morph)
morph.name = target_name
# Add the new vertex morph to the facial display frame
facial_frame = None
for frame in mmd_root.display_item_frames:
if frame.name == "表情": # This is the facial display frame
facial_frame = frame
break
if facial_frame:
# Check if this morph is already in the facial frame
morph_exists_in_frame = False
for item in facial_frame.data:
if item.type == "MORPH" and item.name == target_name and item.morph_type == "vertex_morphs":
morph_exists_in_frame = True
break
# If not, add it
if not morph_exists_in_frame:
new_item = facial_frame.data.add()
new_item.type = "MORPH"
new_item.morph_type = "vertex_morphs"
new_item.name = target_name
# Make this the active item in the facial frame
facial_frame.active_item = len(facial_frame.data) - 1
# Set the facial frame as active
for i, frame in enumerate(mmd_root.display_item_frames):
if frame.name == "表情":
mmd_root.active_display_item_frame = i
break
# Reset all shape keys
for obj in FnModel.iterate_mesh_objects(root):
if obj.data.shape_keys:
for kb in obj.data.shape_keys.key_blocks:
kb.value = 0
self.report({"INFO"}, f"Successfully converted vertex morphs in group to vertex morph '{target_name}' and added to facial display frame")
return {"FINISHED"}
+32 -50
View File
@@ -1,12 +1,8 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
# Copyright 2015 MMD Tools authors
# This file is part of MMD Tools.
import math
from typing import Dict, Optional, Tuple, cast, Set, List, Any, Union, Generator
from typing import Dict, Optional, Tuple, cast
import bpy
from mathutils import Euler, Vector
@@ -16,7 +12,6 @@ from ..bpyutils import FnContext, Props
from ..core import rigid_body
from ..core.model import FnModel, Model
from ..core.rigid_body import FnRigidBody
from ...logging_setup import logger
class SelectRigidBody(bpy.types.Operator):
@@ -44,15 +39,15 @@ class SelectRigidBody(bpy.types.Operator):
default=False,
)
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
def invoke(self, context, event):
vm = context.window_manager
return vm.invoke_props_dialog(self)
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
def poll(cls, context):
return FnModel.is_rigid_body_object(context.active_object)
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
if root is None:
@@ -174,7 +169,7 @@ class AddRigidBody(bpy.types.Operator):
default=0.1,
)
def __add_rigid_body(self, context: bpy.types.Context, root_object: bpy.types.Object, pose_bone: Optional[bpy.types.PoseBone] = None) -> bpy.types.Object:
def __add_rigid_body(self, context: bpy.types.Context, root_object: bpy.types.Object, pose_bone: Optional[bpy.types.PoseBone] = None):
name_j: str = self.name_j
name_e: str = self.name_e
size = self.size.copy()
@@ -227,7 +222,7 @@ class AddRigidBody(bpy.types.Operator):
)
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
def poll(cls, context):
root_object = FnModel.find_root_object(context.active_object)
if root_object is None:
return False
@@ -238,11 +233,11 @@ class AddRigidBody(bpy.types.Operator):
return True
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
active_object = context.active_object
root_object = cast(bpy.types.Object, FnModel.find_root_object(active_object))
armature_object = cast(bpy.types.Object, FnModel.find_armature_object(root_object))
root_object = cast("bpy.types.Object", FnModel.find_root_object(active_object))
armature_object = cast("bpy.types.Object", FnModel.find_armature_object(root_object))
if active_object != armature_object:
FnContext.select_single_object(context, root_object).select_set(False)
@@ -255,17 +250,15 @@ class AddRigidBody(bpy.types.Operator):
armature_object.select_set(False)
if len(selected_pose_bones) > 0:
logger.info(f"Adding rigid bodies to {len(selected_pose_bones)} selected bones")
for pose_bone in selected_pose_bones:
rigid = self.__add_rigid_body(context, root_object, pose_bone)
rigid.select_set(True)
else:
logger.info("Adding a single rigid body without bone attachment")
rigid = self.__add_rigid_body(context, root_object)
rigid.select_set(True)
return {"FINISHED"}
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
def invoke(self, context, event):
no_bone = True
if context.selected_bones and len(context.selected_bones) > 0:
no_bone = False
@@ -291,13 +284,12 @@ class RemoveRigidBody(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
def poll(cls, context):
return FnModel.is_rigid_body_object(context.active_object)
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
logger.info(f"Removing rigid body: {obj.name}")
utils.selectAObject(obj) # ensure this is the only one object select
bpy.ops.object.delete(use_global=True)
if root:
@@ -310,8 +302,7 @@ class RigidBodyBake(bpy.types.Operator):
bl_label = "Bake"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]:
logger.info("Baking rigid body simulation")
def execute(self, context: bpy.types.Context):
with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache):
bpy.ops.ptcache.bake("INVOKE_DEFAULT", bake=True)
@@ -323,8 +314,7 @@ class RigidBodyDeleteBake(bpy.types.Operator):
bl_label = "Delete Bake"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context) -> Set[str]:
logger.info("Deleting rigid body simulation bake")
def execute(self, context: bpy.types.Context):
with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache):
bpy.ops.ptcache.free_bake("INVOKE_DEFAULT")
@@ -387,7 +377,7 @@ class AddJoint(bpy.types.Operator):
min=0,
)
def __enumerate_rigid_pair(self, bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]) -> Generator[Tuple[bpy.types.Object, bpy.types.Object], None, None]:
def __enumerate_rigid_pair(self, bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]):
obj_seq = tuple(bone_map.keys())
for rigid_a, bone_a in bone_map.items():
for rigid_b, bone_b in bone_map.items():
@@ -400,7 +390,7 @@ class AddJoint(bpy.types.Operator):
else:
yield obj_seq
def __add_joint(self, context: bpy.types.Context, root_object: bpy.types.Object, rigid_pair: Tuple[bpy.types.Object, bpy.types.Object], bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]) -> bpy.types.Object:
def __add_joint(self, context: bpy.types.Context, root_object: bpy.types.Object, rigid_pair: Tuple[bpy.types.Object, bpy.types.Object], bone_map):
loc: Optional[Vector] = None
rot = Euler((0.0, 0.0, 0.0))
rigid_a, rigid_b = rigid_pair
@@ -438,7 +428,7 @@ class AddJoint(bpy.types.Operator):
)
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
def poll(cls, context):
root_object = FnModel.find_root_object(context.active_object)
if root_object is None:
return False
@@ -449,11 +439,11 @@ class AddJoint(bpy.types.Operator):
return True
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
active_object = context.active_object
root_object = cast(bpy.types.Object, FnModel.find_root_object(active_object))
armature_object = cast(bpy.types.Object, FnModel.find_armature_object(root_object))
bones = cast(bpy.types.Armature, armature_object.data).bones
root_object = cast("bpy.types.Object", FnModel.find_root_object(active_object))
armature_object = cast("bpy.types.Object", FnModel.find_armature_object(root_object))
bones = cast("bpy.types.Armature", armature_object.data).bones
bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]] = {r: bones.get(r.mmd_rigid.bone, None) for r in FnModel.iterate_rigid_body_objects(root_object) if r.select_get()}
if len(bone_map) < 2:
@@ -462,19 +452,15 @@ class AddJoint(bpy.types.Operator):
FnContext.select_single_object(context, root_object).select_set(False)
if context.scene.rigidbody_world is None:
logger.info("Creating rigid body world")
bpy.ops.rigidbody.world_add()
joint_count = 0
for pair in self.__enumerate_rigid_pair(bone_map):
joint = self.__add_joint(context, root_object, pair, bone_map)
joint.select_set(True)
joint_count += 1
logger.info(f"Added {joint_count} joints between rigid bodies")
return {"FINISHED"}
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
def invoke(self, context, event):
vm = context.window_manager
return vm.invoke_props_dialog(self)
@@ -486,13 +472,12 @@ class RemoveJoint(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
def poll(cls, context):
return FnModel.is_joint_object(context.active_object)
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
logger.info(f"Removing joint: {obj.name}")
utils.selectAObject(obj) # ensure this is the only one object select
bpy.ops.object.delete(use_global=True)
if root:
@@ -507,7 +492,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
@staticmethod
def __get_rigid_body_world_objects() -> Tuple[bpy.types.Collection, bpy.types.Collection]:
def __get_rigid_body_world_objects():
rigid_body.setRigidBodyWorldEnabled(True)
rbw = bpy.context.scene.rigidbody_world
if not rbw.collection:
@@ -522,21 +507,21 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
return rbw.collection.objects, rbw.constraints.objects
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context):
scene = context.scene
scene_objs = set(scene.objects)
scene_objs.union(o for x in scene.objects if x.instance_type == "COLLECTION" and x.instance_collection for o in x.instance_collection.objects)
def _update_group(obj: bpy.types.Object, group: bpy.types.Collection) -> bool:
def _update_group(obj, group):
if obj in scene_objs:
if obj not in group.values():
group.link(obj)
return True
elif obj in group.values():
if obj in group.values():
group.unlink(obj)
return False
def _references(obj: bpy.types.Object) -> Generator[bpy.types.Object, None, None]:
def _references(obj):
yield obj
if getattr(obj, "proxy", None):
yield from _references(obj.proxy)
@@ -553,7 +538,6 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
# Object.rigid_body are removed,
# but Object.rigid_body_constraint are retained.
# Therefore, it must be checked with Object.mmd_type.
logger.info("Updating rigid body world objects")
for i in (x for x in objects if x.mmd_type == "RIGID_BODY"):
if not _update_group(i, rb_objs):
continue
@@ -568,7 +552,6 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
# TODO Modify mmd_rigid to allow recovery of the remaining rigidbody parameters.
# mass, friction, restitution, linear_dumping, angular_dumping
logger.info("Updating rigid body constraints")
for i in (x for x in objects if x.rigid_body_constraint):
if not _update_group(i, rbc_objs):
continue
@@ -579,7 +562,6 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
rbc.object2 = rb_map.get(rbc.object2, rbc.object2)
if need_rebuild_physics:
logger.info("Rebuilding physics for models")
for root_object in scene.objects:
if root_object.mmd_type != "ROOT":
continue
+13 -23
View File
@@ -1,23 +1,18 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
# Copyright 2018 MMD Tools authors
# This file is part of MMD Tools.
from typing import Set, Tuple
from typing import Set
import bpy
from bpy.types import Operator, Context, Object
from bpy.types import Operator
from ..core.model import FnModel
from ..core.sdef import FnSDEF
from ....core.logging_setup import logger
def _get_target_objects(context: Context) -> Tuple[Set[Object], Set[Object]]:
root_objects: Set[Object] = set()
selected_objects: Set[Object] = set()
def _get_target_objects(context):
root_objects: Set[bpy.types.Object] = set()
selected_objects: Set[bpy.types.Object] = set()
for i in context.selected_objects:
if i.type == "MESH":
selected_objects.add(i)
@@ -41,13 +36,11 @@ class ResetSDEFCache(Operator):
bl_description = "Reset MMD SDEF cache of selected objects and clean unused cache"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: Context) -> Set[str]:
def execute(self, context):
target_meshes, _ = _get_target_objects(context)
logger.info(f"Resetting SDEF cache for {len(target_meshes)} objects")
for i in target_meshes:
FnSDEF.clear_cache(i)
FnSDEF.clear_cache(unused_only=True)
logger.debug("SDEF cache reset completed")
return {"FINISHED"}
@@ -78,20 +71,19 @@ class BindSDEF(Operator):
default=False,
)
def invoke(self, context: Context, event: bpy.types.Event) -> Set[str]:
def invoke(self, context, event):
vm = context.window_manager
return vm.invoke_props_dialog(self)
def execute(self, context: Context) -> Set[str]:
# TODO: Utility Functionalize
def execute(self, context):
target_meshes, root_objects = _get_target_objects(context)
logger.info(f"Binding SDEF for {len(target_meshes)} objects with mode={self.mode}, skip={self.use_skip}, scale={self.use_scale}")
for r in root_objects:
r.mmd_root.use_sdef = True
param = ((None, False, True)[int(self.mode)], self.use_skip, self.use_scale)
count = sum(FnSDEF.bind(i, *param) for i in target_meshes)
logger.info(f"Successfully bound SDEF for {count} of {len(target_meshes)} meshes")
self.report({"INFO"}, f"Binded {count} of {len(target_meshes)} selected mesh(es)")
return {"FINISHED"}
@@ -102,15 +94,13 @@ class UnbindSDEF(Operator):
bl_description = "Unbind MMD SDEF data of selected objects"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: Context) -> Set[str]:
# TODO: Utility Functionalize
def execute(self, context):
target_meshes, root_objects = _get_target_objects(context)
logger.info(f"Unbinding SDEF for {len(target_meshes)} objects")
for i in target_meshes:
FnSDEF.unbind(i)
for r in root_objects:
r.mmd_root.use_sdef = False
logger.debug("SDEF unbinding completed")
return {"FINISHED"}
+266 -36
View File
@@ -1,10 +1,8 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
# Copyright 2021 MMD Tools authors
# This file is part of MMD Tools.
import csv
import os
from typing import TYPE_CHECKING, cast
import bpy
@@ -14,7 +12,11 @@ from ..core.translations import MMD_DATA_TYPE_TO_HANDLERS, FnTranslations
from ..translations import DictionaryEnum
if TYPE_CHECKING:
from ..properties.translations import MMDTranslation, MMDTranslationElement, MMDTranslationElementIndex
from ..properties.translations import (
MMDTranslation,
MMDTranslationElement,
MMDTranslationElementIndex,
)
class TranslateMMDModel(bpy.types.Operator):
@@ -77,7 +79,8 @@ class TranslateMMDModel(bpy.types.Operator):
@classmethod
def poll(cls, context):
obj = context.active_object
return obj in context.selected_objects and FnModel.find_root_object(obj)
root = FnModel.find_root_object(obj)
return obj is not None and obj in context.selected_objects and root is not None
def invoke(self, context, event):
vm = context.window_manager
@@ -87,7 +90,7 @@ class TranslateMMDModel(bpy.types.Operator):
try:
self.__translator = DictionaryEnum.get_translator(self.dictionary)
except Exception as e:
self.report({"ERROR"}, "Failed to load dictionary: %s" % e)
self.report({"ERROR"}, f"Failed to load dictionary: {e}")
return {"CANCELLED"}
obj = context.active_object
@@ -96,7 +99,7 @@ class TranslateMMDModel(bpy.types.Operator):
if "MMD" in self.modes:
for i in self.types:
getattr(self, "translate_%s" % i.lower())(rig)
getattr(self, f"translate_{i.lower()}")(rig)
if "BLENDER" in self.modes:
self.translate_blender_names(rig)
@@ -104,7 +107,11 @@ class TranslateMMDModel(bpy.types.Operator):
translator = self.__translator
txt = translator.save_fails()
if translator.fails:
self.report({"WARNING"}, "Failed to translate %d names, see '%s' in text editor" % (len(translator.fails), txt.name))
self.report(
{"WARNING"},
"Failed to translate %d names, see '%s' in text editor"
% (len(translator.fails), txt.name),
)
return {"FINISHED"}
def translate(self, name_j, name_e):
@@ -130,7 +137,7 @@ class TranslateMMDModel(bpy.types.Operator):
if "DISPLAY" in self.types:
g: bpy.types.BoneCollection
for g in cast(bpy.types.Armature, rig.armature().data).collections:
for g in cast("bpy.types.Armature", rig.armature().data).collections:
g.name = self.translate(g.name, g.name)
if "PHYSICS" in self.types:
@@ -153,7 +160,9 @@ class TranslateMMDModel(bpy.types.Operator):
comment_text = bpy.data.texts.get(mmd_root.comment_text, None)
comment_e_text = bpy.data.texts.get(mmd_root.comment_e_text, None)
if comment_text and comment_e_text:
comment_e = self.translate(comment_text.as_string(), comment_e_text.as_string())
comment_e = self.translate(
comment_text.as_string(), comment_e_text.as_string(),
)
comment_e_text.from_string(comment_e)
def translate_bone(self, rig):
@@ -167,7 +176,7 @@ class TranslateMMDModel(bpy.types.Operator):
mmd_root = rig.rootObject().mmd_root
attr_list = ("group", "vertex", "bone", "uv", "material")
prefix_list = ("G_", "", "B_", "UV_", "M_")
for attr, prefix in zip(attr_list, prefix_list):
for attr, prefix in zip(attr_list, prefix_list, strict=False):
for m in getattr(mmd_root, attr + "_morphs", []):
m.name_e = self.translate(m.name, m.name_e)
if not prefix:
@@ -182,7 +191,9 @@ class TranslateMMDModel(bpy.types.Operator):
for m in rig.materials():
if m is None:
continue
m.mmd_material.name_e = self.translate(m.mmd_material.name_j, m.mmd_material.name_e)
m.mmd_material.name_e = self.translate(
m.mmd_material.name_j, m.mmd_material.name_e,
)
def translate_display(self, rig):
mmd_root = rig.rootObject().mmd_root
@@ -200,10 +211,24 @@ class TranslateMMDModel(bpy.types.Operator):
DEFAULT_SHOW_ROW_COUNT = 20
class MMD_TOOLS_UL_MMDTranslationElementIndex(bpy.types.UIList):
def draw_item(self, context, layout: bpy.types.UILayout, data, mmd_translation_element_index: "MMDTranslationElementIndex", icon, active_data, active_propname, index: int):
mmd_translation_element: "MMDTranslationElement" = data.translation_elements[mmd_translation_element_index.value]
MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type].draw_item(layout, mmd_translation_element, index)
class MMD_TOOLS_LOCAL_UL_MMDTranslationElementIndex(bpy.types.UIList):
def draw_item(
self,
context,
layout: bpy.types.UILayout,
data,
mmd_translation_element_index: "MMDTranslationElementIndex",
icon,
active_data,
active_propname,
index: int,
):
mmd_translation_element: MMDTranslationElement = data.translation_elements[
mmd_translation_element_index.value
]
MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type].draw_item(
layout, mmd_translation_element, index,
)
class RestoreMMDDataReferenceOperator(bpy.types.Operator):
@@ -216,9 +241,15 @@ class RestoreMMDDataReferenceOperator(bpy.types.Operator):
restore_value: bpy.props.StringProperty()
def execute(self, context: bpy.types.Context):
root_object = FnModel.find_root_object(context.object)
mmd_translation_element_index = root_object.mmd_root.translation.filtered_translation_element_indices[self.index].value
mmd_translation_element = root_object.mmd_root.translation.translation_elements[mmd_translation_element_index]
root_object = FnModel.find_root_object(context.active_object)
mmd_translation_element_index = (
root_object.mmd_root.translation.filtered_translation_element_indices[
self.index
].value
)
mmd_translation_element = root_object.mmd_root.translation.translation_elements[
mmd_translation_element_index
]
setattr(mmd_translation_element, self.prop_name, self.restore_value)
return {"FINISHED"}
@@ -231,7 +262,8 @@ class GlobalTranslationPopup(bpy.types.Operator):
@classmethod
def poll(cls, context):
return FnModel.find_root_object(context.object) is not None
root = FnModel.find_root_object(context.active_object)
return root is not None
def draw(self, _context):
layout = self.layout
@@ -244,13 +276,33 @@ class GlobalTranslationPopup(bpy.types.Operator):
group = row.row(align=True, heading="is Blank:")
group.alignment = "RIGHT"
group.prop(mmd_translation, "filter_japanese_blank", toggle=True, text="Japanese")
group.prop(
mmd_translation, "filter_japanese_blank", toggle=True, text="Japanese",
)
group.prop(mmd_translation, "filter_english_blank", toggle=True, text="English")
group = row.row(align=True)
group.prop(mmd_translation, "filter_restorable", toggle=True, icon="FILE_REFRESH", icon_only=True)
group.prop(mmd_translation, "filter_selected", toggle=True, icon="RESTRICT_SELECT_OFF", icon_only=True)
group.prop(mmd_translation, "filter_visible", toggle=True, icon="HIDE_OFF", icon_only=True)
group.prop(
mmd_translation,
"filter_restorable",
toggle=True,
icon="FILE_REFRESH",
icon_only=True,
)
group.prop(
mmd_translation,
"filter_selected",
toggle=True,
icon="RESTRICT_SELECT_OFF",
icon_only=True,
)
group.prop(
mmd_translation,
"filter_visible",
toggle=True,
icon="HIDE_OFF",
icon_only=True,
)
col = layout.column(align=True)
box = col.box().column(align=True)
@@ -262,11 +314,14 @@ class GlobalTranslationPopup(bpy.types.Operator):
row.label(text="", icon="RESTRICT_SELECT_OFF")
row.label(text="", icon="HIDE_OFF")
if len(mmd_translation.filtered_translation_element_indices) > DEFAULT_SHOW_ROW_COUNT:
if (
len(mmd_translation.filtered_translation_element_indices)
> DEFAULT_SHOW_ROW_COUNT
):
row.label(text="", icon="BLANK1")
col.template_list(
"MMD_TOOLS_UL_MMDTranslationElementIndex",
"mmd_tools_UL_MMDTranslationElementIndex",
"",
mmd_translation,
"filtered_translation_element_indices",
@@ -281,7 +336,12 @@ class GlobalTranslationPopup(bpy.types.Operator):
box.separator()
row = box.row()
row.prop(mmd_translation, "batch_operation_script_preset", text="Preset", icon="CON_TRANSFORM_CACHE")
row.prop(
mmd_translation,
"batch_operation_script_preset",
text="Preset",
icon="CON_TRANSFORM_CACHE",
)
row.operator(ExecuteTranslationBatchOperator.bl_idname, text="Execute")
box.separator()
@@ -289,18 +349,25 @@ class GlobalTranslationPopup(bpy.types.Operator):
translation_box.label(text="Dictionaries:", icon="HELP")
row = translation_box.row()
row.prop(mmd_translation, "dictionary", text="to_english")
# row.operator(ExecuteTranslationScriptOperator.bl_idname, text='Write to .csv')
translation_box.separator()
row = translation_box.row()
row.prop(mmd_translation, "dictionary", text="replace")
# CSV import/export
box.separator()
translation_box = box.box().column(align=True)
translation_box.label(text="CSV:", icon="FILE_TEXT")
row = translation_box.row()
row.operator(ImportTranslationCSVOperator.bl_idname, text="Import CSV")
row.operator(ExportTranslationCSVOperator.bl_idname, text="Export CSV")
def invoke(self, context: bpy.types.Context, _event):
root_object = FnModel.find_root_object(context.object)
root_object = FnModel.find_root_object(context.active_object)
if root_object is None:
return {"CANCELLED"}
mmd_translation: "MMDTranslation" = root_object.mmd_root.translation
mmd_translation: MMDTranslation = root_object.mmd_root.translation
self._mmd_translation = mmd_translation
FnTranslations.clear_data(mmd_translation)
FnTranslations.collect_data(mmd_translation)
@@ -309,7 +376,7 @@ class GlobalTranslationPopup(bpy.types.Operator):
return context.window_manager.invoke_props_dialog(self, width=800)
def execute(self, context):
root_object = FnModel.find_root_object(context.object)
root_object = FnModel.find_root_object(context.active_object)
if root_object is None:
return {"CANCELLED"}
@@ -325,12 +392,175 @@ class ExecuteTranslationBatchOperator(bpy.types.Operator):
bl_options = {"INTERNAL"}
def execute(self, context: bpy.types.Context):
root = FnModel.find_root_object(context.object)
root = FnModel.find_root_object(context.active_object)
if root is None:
return {"CANCELLED"}
fails, text = FnTranslations.execute_translation_batch(root)
if fails:
self.report({"WARNING"}, "Failed to translate %d names, see '%s' in text editor" % (len(fails), text.name))
self.report(
{"WARNING"},
"Failed to translate %d names, see '%s' in text editor"
% (len(fails), text.name),
)
return {"FINISHED"}
class ExportTranslationCSVOperator(bpy.types.Operator):
bl_idname = "mmd_tools.export_translation_csv"
bl_description = "Export CSV for external translation."
bl_label = "Export Translation CSV"
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
filename_ext = ".csv"
filepath: bpy.props.StringProperty(
name="File Path",
description="Path to save the translation CSV",
subtype="FILE_PATH",
default="mmd_translation.csv",
)
def _ensure_csv_extension(self):
"""Ensure the file path ends with a .csv extension (case-insensitive)."""
if not self.filepath.lower().endswith(".csv"):
self.filepath = bpy.path.ensure_ext(self.filepath, ".csv")
def invoke(self, context, event):
self._ensure_csv_extension()
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
self._ensure_csv_extension()
root_object = FnModel.find_root_object(context.active_object)
if root_object is None:
self.report({"ERROR"}, "Root object not found")
return {"CANCELLED"}
mmd_translation = root_object.mmd_root.translation
try:
with open(self.filepath, "w", newline="", encoding="utf-8") as csvfile:
writer = csv.writer(csvfile)
writer.writerow(["type", "blender", "japanese", "english"])
for idx in mmd_translation.filtered_translation_element_indices:
element = mmd_translation.translation_elements[idx.value]
writer.writerow(
[element.type, element.name, element.name_j, element.name_e],
)
except Exception as e:
self.report({"ERROR"}, f"Failed to write CSV: {e}")
return {"CANCELLED"}
self.report({"INFO"}, f"Exported to {os.path.basename(self.filepath)}")
return {"FINISHED"}
class ImportTranslationCSVOperator(bpy.types.Operator):
bl_idname = "mmd_tools.import_translation_csv"
bl_description = "Import translated CSV."
bl_label = "Import Translation CSV"
only_update_english_name: bpy.props.BoolProperty(
name="Only Update English Name",
description="(Enabled by default) Only update English name (name_e). otherwise, update all names when different",
default=True,
)
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
filepath: bpy.props.StringProperty(
name="File Path",
description="Path to import the translation CSV",
subtype="FILE_PATH",
default="*.csv",
)
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
root_object = FnModel.find_root_object(context.active_object)
if root_object is None:
self.report({"ERROR"}, "Root object not found")
return {"CANCELLED"}
mmd_translation = root_object.mmd_root.translation
updated_count = 0
warnings = []
try:
with open(self.filepath, encoding="utf-8") as csvfile:
reader = csv.DictReader(csvfile)
required_headers = {"blender", "japanese", "english"}
if not required_headers.issubset(set(reader.fieldnames or [])):
missing = required_headers - set(reader.fieldnames or [])
self.report(
{"ERROR"},
f"Missing required headers in CSV: {', '.join(missing)}",
)
return {"CANCELLED"}
visible_indices = [
i.value
for i in mmd_translation.filtered_translation_element_indices
]
translation_elements_list = list(mmd_translation.translation_elements)
row_count = 0
for row in reader:
if row_count >= len(visible_indices):
row_count += 1
continue
element = translation_elements_list[visible_indices[row_count]]
b_name = row.get("blender", "").strip()
j_name = row.get("japanese", "").strip()
e_name = row.get("english", "").strip()
updated = False
if self.only_update_english_name:
if element.name_e != e_name:
element.name_e = e_name
updated = True
else:
if element.name != b_name:
element.name = b_name
updated = True
if element.name_j != j_name:
element.name_j = j_name
updated = True
if element.name_e != e_name:
element.name_e = e_name
updated = True
if updated:
updated_count += 1
row_count += 1
# Output warnings
if row_count > len(visible_indices):
warnings.append(
f"{row_count - len(visible_indices)} extra lines in CSV! (ignored)",
)
elif row_count < len(visible_indices):
warnings.append(
f"{len(visible_indices) - row_count} missing lines in CSV! (aborted translation)",
)
except Exception as e:
self.report({"ERROR"}, f"Failed to read CSV: {e}")
return {"CANCELLED"}
FnTranslations.update_query(mmd_translation)
msg = f"Imported {updated_count} entries from CSV"
if warnings:
for w in warnings:
self.report({"WARNING"}, w)
msg += " with warnings"
self.report({"INFO"}, msg)
return {"FINISHED"}
+44 -53
View File
@@ -1,47 +1,43 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
# This file is part of MMD Tools.
import re
from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable, Union, Type, Iterator
from bpy.types import Operator, Context
from mathutils import Matrix, Vector, Quaternion
from ...logging_setup import logger
from bpy.types import Operator
from mathutils import Matrix, Quaternion
class _SetShadingBase:
bl_options: Set[str] = {"REGISTER", "UNDO"}
bl_options = {"REGISTER", "UNDO"}
@staticmethod
def _get_view3d_spaces(context: Context) -> Iterator[Any]:
def _get_view3d_spaces(context):
if getattr(context.area, "type", None) == "VIEW_3D":
return (context.area.spaces[0],)
return (area.spaces[0] for area in getattr(context.screen, "areas", ()) if area.type == "VIEW_3D")
@staticmethod
def _reset_color_management(context: Context, use_display_device: bool = True) -> None:
def _reset_color_management(context, use_display_device=True):
try:
context.scene.display_settings.display_device = ("None", "sRGB")[use_display_device]
except TypeError:
pass
@staticmethod
def _reset_material_shading(context: Context, use_shadeless: bool = False) -> None:
# Note: material.use_nodes and material.use_shadeless are deprecated in Blender 5.0
# Materials always use nodes now, and shadeless is handled differently
# This method is kept for compatibility but no longer modifies materials
pass
def _reset_material_shading(context, use_shadeless=False):
for i in (x for x in context.scene.objects if x.type == "MESH" and x.mmd_type == "NONE"):
for s in i.material_slots:
if s.material is None:
continue
# use_nodes is deprecated in 5.0 but harmless to set
s.material.use_nodes = False
s.material.use_shadeless = use_shadeless
def execute(self, context: Context) -> Dict[str, str]:
def execute(self, context):
# Changed from BLENDER_EEVEE_NEXT to BLENDER_EEVEE for Blender 5.0
context.scene.render.engine = "BLENDER_EEVEE"
logger.debug(f"Setting render engine to BLENDER_EEVEE")
shading_mode: Optional[str] = getattr(self, "_shading_mode", None)
shading_mode = getattr(self, "_shading_mode", None)
for space in self._get_view3d_spaces(context):
shading = space.shading
shading.type = "SOLID"
@@ -49,40 +45,39 @@ class _SetShadingBase:
shading.color_type = "TEXTURE" if shading_mode else "MATERIAL"
shading.show_object_outline = False
shading.show_backface_culling = False
logger.debug(f"Applied shading mode: {shading_mode or 'DEFAULT'}")
return {"FINISHED"}
class SetGLSLShading(Operator, _SetShadingBase):
bl_idname: str = "mmd_tools.set_glsl_shading"
bl_label: str = "GLSL View"
bl_description: str = "Use GLSL shading with additional lighting"
bl_idname = "mmd_tools.set_glsl_shading"
bl_label = "GLSL View"
bl_description = "Use GLSL shading with additional lighting"
_shading_mode: str = "GLSL"
_shading_mode = "GLSL"
class SetShadelessGLSLShading(Operator, _SetShadingBase):
bl_idname: str = "mmd_tools.set_shadeless_glsl_shading"
bl_label: str = "Shadeless GLSL View"
bl_description: str = "Use only toon shading"
bl_idname = "mmd_tools.set_shadeless_glsl_shading"
bl_label = "Shadeless GLSL View"
bl_description = "Use only toon shading"
_shading_mode: str = "SHADELESS"
_shading_mode = "SHADELESS"
class ResetShading(Operator, _SetShadingBase):
bl_idname: str = "mmd_tools.reset_shading"
bl_label: str = "Reset View"
bl_description: str = "Reset to default Blender shading"
bl_idname = "mmd_tools.reset_shading"
bl_label = "Reset View"
bl_description = "Reset to default Blender shading"
class FlipPose(Operator):
bl_idname: str = "mmd_tools.flip_pose"
bl_label: str = "Flip Pose"
bl_description: str = "Apply the current pose of selected bones to matching bone on opposite side of X-Axis."
bl_options: Set[str] = {"REGISTER", "UNDO"}
bl_idname = "mmd_tools.flip_pose"
bl_label = "Flip Pose"
bl_description = "Apply the current pose of selected bones to matching bone on opposite side of X-Axis."
bl_options = {"REGISTER", "UNDO"}
# https://docs.blender.org/manual/en/dev/rigging/armatures/bones/editing/naming.html
__LR_REGEX: List[Dict[str, Any]] = [
__LR_REGEX = [
{"re": re.compile(r"^(.+)(RIGHT|LEFT)(\.\d+)?$", re.IGNORECASE), "lr": 1},
{"re": re.compile(r"^(.+)([\.\- _])(L|R)(\.\d+)?$", re.IGNORECASE), "lr": 2},
{"re": re.compile(r"^(LEFT|RIGHT)(.+)$", re.IGNORECASE), "lr": 0},
@@ -90,7 +85,7 @@ class FlipPose(Operator):
{"re": re.compile(r"^(.+)(左|右)(\.\d+)?$"), "lr": 1},
{"re": re.compile(r"^(左|右)(.+)$"), "lr": 0},
]
__LR_MAP: Dict[str, str] = {
__LR_MAP = {
"RIGHT": "LEFT",
"Right": "Left",
"right": "left",
@@ -106,7 +101,7 @@ class FlipPose(Operator):
}
@classmethod
def flip_name(cls, name: str) -> str:
def flip_name(cls, name):
for regex in cls.__LR_REGEX:
match = regex["re"].match(name)
if match:
@@ -124,15 +119,15 @@ class FlipPose(Operator):
return ""
@staticmethod
def __cmul(vec1: Union[Vector, Quaternion], vec2: Tuple[float, float, float, float]) -> Union[Vector, Quaternion]:
return type(vec1)([x * y for x, y in zip(vec1, vec2)])
def __cmul(vec1, vec2):
return type(vec1)([x * y for x, y in zip(vec1, vec2, strict=False)])
@staticmethod
def __matrix_compose(loc: Vector, rot: Quaternion, scale: Vector) -> Matrix:
def __matrix_compose(loc, rot, scale):
return (Matrix.Translation(loc) @ rot.to_matrix().to_4x4()) @ Matrix([(scale[0], 0, 0, 0), (0, scale[1], 0, 0), (0, 0, scale[2], 0), (0, 0, 0, 1)])
@classmethod
def __flip_pose(cls, matrix_basis: Matrix, bone_src: Any, bone_dest: Any) -> None:
def __flip_pose(cls, matrix_basis, bone_src, bone_dest):
m = bone_dest.bone.matrix_local.to_3x3().transposed()
mi = bone_src.bone.matrix_local.to_3x3().transposed().inverted() if bone_src != bone_dest else m.inverted()
loc, rot, scale = matrix_basis.decompose()
@@ -141,16 +136,12 @@ class FlipPose(Operator):
bone_dest.matrix_basis = cls.__matrix_compose(m @ loc, Quaternion(m @ rot.axis, rot.angle).normalized(), scale)
@classmethod
def poll(cls, context: Context) -> bool:
return context.active_object and context.active_object.type == "ARMATURE" and context.active_object.mode == "POSE"
def poll(cls, context):
obj = context.active_object
return obj is not None and obj.type == "ARMATURE" and obj.mode == "POSE"
def execute(self, context: Context) -> Dict[str, str]:
logger.info("Executing flip pose operation")
def execute(self, context):
pose_bones = context.active_object.pose.bones
for b, mat in [(x, x.matrix_basis.copy()) for x in context.selected_pose_bones]:
flip_name = self.flip_name(b.name)
target_bone = pose_bones.get(flip_name, b)
logger.debug(f"Flipping pose from {b.name} to {target_bone.name}")
self.__flip_pose(mat, b, target_bone)
logger.info("Flip pose operation completed")
self.__flip_pose(mat, b, pose_bones.get(self.flip_name(b.name), b))
return {"FINISHED"}