Updated Operations and Properties

- Updated Operations and Properties with tpying and logging.

I have not updated translation files, this is because i want to gut MMD Tools system and replace it with our own, however I want to make MMD Tools more simple and ajust it to our needs only. This is going to take a while and my aim for this is Alpha 4, also the MMD Translation system hurt my head....

- Fixes a couple of bugs as well, with quick access and the PMX importer.
This commit is contained in:
Yusarina
2025-04-23 00:43:38 +01:00
parent 61e4269764
commit cfe760e8df
21 changed files with 689 additions and 347 deletions
+82 -38
View File
@@ -6,13 +6,16 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import bpy
from bpy.props import BoolProperty, StringProperty
from bpy.types import Operator
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 .. import cycles_converter
from ..core.exceptions import MaterialNotFoundError
from ..core.material import FnMaterial
from ..core.shader import _NodeGroupUtils
from ....core.logging_setup import logger
class ConvertMaterialsForCycles(Operator):
@@ -21,14 +24,14 @@ class ConvertMaterialsForCycles(Operator):
bl_description = "Convert materials of selected objects for Cycles."
bl_options = {"REGISTER", "UNDO"}
use_principled: bpy.props.BoolProperty(
use_principled: 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: bpy.props.BoolProperty(
clean_nodes: BoolProperty(
name="Clean Nodes",
description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
default=False,
@@ -36,22 +39,27 @@ class ConvertMaterialsForCycles(Operator):
)
@classmethod
def poll(cls, context):
return next((x for x in context.selected_objects if x.type == "MESH"), None)
def poll(cls, context: Context) -> bool:
return next((x for x in context.selected_objects if x.type == "MESH"), None) is not None
def draw(self, context):
def draw(self, context: Context) -> None:
layout = self.layout
layout.prop(self, "use_principled")
layout.prop(self, "clean_nodes")
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
try:
context.scene.render.engine = "CYCLES"
except:
except Exception as e:
logger.error(f"Failed to change to Cycles render engine: {str(e)}")
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"}
@@ -61,21 +69,21 @@ class ConvertMaterials(Operator):
bl_description = "Convert materials of selected objects."
bl_options = {"REGISTER", "UNDO"}
use_principled: bpy.props.BoolProperty(
use_principled: 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: bpy.props.BoolProperty(
clean_nodes: BoolProperty(
name="Clean Nodes",
description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
default=True,
options={"SKIP_SAVE"},
)
subsurface: bpy.props.FloatProperty(
subsurface: FloatProperty(
name="Subsurface",
default=0.001,
soft_min=0.000,
@@ -85,13 +93,15 @@ class ConvertMaterials(Operator):
)
@classmethod
def poll(cls, context):
return next((x for x in context.selected_objects if x.type == "MESH"), None)
def poll(cls, context: Context) -> bool:
return next((x for x in context.selected_objects if x.type == "MESH"), None) is not None
def execute(self, context):
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}")
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"}
@@ -102,20 +112,22 @@ class ConvertBSDFMaterials(Operator):
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return next((x for x in context.selected_objects if x.type == 'MESH'), None)
def poll(cls, context: Context) -> bool:
return next((x for x in context.selected_objects if x.type == 'MESH'), None) is not None
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
logger.info("Converting BSDF materials to MMD shader")
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'}
class _OpenTextureBase:
"""Create a texture for mmd model material."""
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
bl_options: Set[str] = {"REGISTER", "UNDO", "INTERNAL"}
filepath: StringProperty(
name="File Path",
@@ -129,7 +141,7 @@ class _OpenTextureBase:
options={"HIDDEN"},
)
def invoke(self, context, event):
def invoke(self, context: Context, event: Any) -> Set[str]:
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
@@ -139,8 +151,13 @@ class OpenTexture(Operator, _OpenTextureBase):
bl_label = "Open Texture"
bl_description = "Create main texture of active material"
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
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"}
@@ -154,8 +171,13 @@ class RemoveTexture(Operator):
bl_description = "Remove main texture of active material"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
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"}
@@ -168,8 +190,13 @@ class OpenSphereTextureSlot(Operator, _OpenTextureBase):
bl_label = "Open Sphere Texture"
bl_description = "Create sphere texture of active material"
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
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"}
@@ -183,8 +210,13 @@ class RemoveSphereTexture(Operator):
bl_description = "Remove sphere texture of active material"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
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"}
@@ -197,18 +229,21 @@ class MoveMaterialUp(Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
def poll(cls, context: Context) -> bool:
obj = context.active_object
valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE"
return valid_mesh and obj.active_material_index > 0
return bool(valid_mesh and obj.active_material_index > 0)
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
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
@@ -223,18 +258,21 @@ class MoveMaterialDown(Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
def poll(cls, context: Context) -> bool:
obj = context.active_object
valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE"
return valid_mesh and obj.active_material_index < len(obj.material_slots) - 1
return bool(valid_mesh and obj.active_material_index < len(obj.material_slots) - 1)
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
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
@@ -257,26 +295,31 @@ class EdgePreviewSetup(Operator):
default="CREATE",
)
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
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):
def __clean_toon_edge(self, obj: Object) -> None:
logger.debug(f"Cleaning toon edge for object: {obj.name}")
if "mmd_edge_preview" in obj.modifiers:
obj.modifiers.remove(obj.modifiers["mmd_edge_preview"])
@@ -285,7 +328,8 @@ class EdgePreviewSetup(Operator):
FnMaterial.clean_materials(obj, can_remove=lambda m: m and m.name.startswith("mmd_edge."))
def __create_toon_edge(self, obj, scale=1.0):
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}")
self.__clean_toon_edge(obj)
materials = obj.data.materials
material_offset = len(materials)
@@ -310,10 +354,10 @@ class EdgePreviewSetup(Operator):
mod.vertex_group = "mmd_edge_preview"
return len(materials) - material_offset
def __create_edge_preview_group(self, obj):
def __create_edge_preview_group(self, obj: Object) -> None:
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 = {}
scale_map: Dict[int, float] = {}
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}
@@ -322,7 +366,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, edge_color, materials):
def __get_edge_material(self, mat_name: str, edge_color: Tuple[float, float, float, float], materials: List[Material]) -> Material:
if mat_name in materials:
return materials[mat_name]
mat = bpy.data.materials.get(mat_name, None)
@@ -340,7 +384,7 @@ class EdgePreviewSetup(Operator):
self.__make_shader(mat)
return mat
def __make_shader(self, m):
def __make_shader(self, m: Material) -> None:
m.use_nodes = True
nodes, links = m.node_tree.nodes, m.node_tree.links
@@ -361,7 +405,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):
def __get_edge_preview_shader(self) -> bpy.types.NodeTree:
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):
+56 -27
View File
@@ -6,14 +6,17 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
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):
@@ -29,7 +32,8 @@ class SelectObject(bpy.types.Operator):
options={"HIDDEN", "SKIP_SAVE"},
)
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
logger.debug(f"Selecting object: {self.name}")
utils.selectAObject(context.scene.objects[self.name])
return {"FINISHED"}
@@ -43,41 +47,43 @@ 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, index):
def set_index(cls, obj: Object, index: int) -> None:
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)
@classmethod
def get_name(cls, obj, prefix=None):
def get_name(cls, obj: Object, prefix: Optional[str] = None) -> str:
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):
def normalize_indices(cls, objects: List[Object]) -> None:
for i, x in enumerate(objects):
cls.set_index(x, i)
@classmethod
def poll(cls, context):
return context.active_object
def poll(cls, context: Context) -> bool:
return context.active_object is not None
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
obj = context.active_object
objects = self.__get_objects(obj)
if obj not in objects:
self.report({"ERROR"}, 'Can not move object "%s"' % obj.name)
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):
def __get_objects(self, obj: Object) -> Any:
class __MovableList(list):
def move(self, index_old, index_new):
def move(self, index_old: int, index_new: int) -> None:
item = self[index_old]
self.remove(item)
self.insert(index_new, item)
@@ -102,11 +108,11 @@ class CleanShapeKeys(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
def poll(cls, context: Context) -> bool:
return any(o.type == "MESH" for o in context.selected_objects)
@staticmethod
def __can_remove(key_block):
def __can_remove(key_block: ShapeKey) -> bool:
if key_block.relative_key == key_block:
return False # Basis
for v0, v1 in zip(key_block.relative_key.data, key_block.data):
@@ -114,20 +120,24 @@ class CleanShapeKeys(bpy.types.Operator):
return False
return True
def __shape_key_clean(self, obj, key_blocks):
def __shape_key_clean(self, obj: Object, key_blocks: List[ShapeKey]) -> None:
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):
obj: bpy.types.Object
def execute(self, context: Context) -> Set[str]:
logger.info("Cleaning shape keys for selected objects")
obj: 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"}
@@ -144,21 +154,25 @@ class SeparateByMaterials(bpy.types.Operator):
)
@classmethod
def poll(cls, context):
def poll(cls, context: Context) -> bool:
obj = context.active_object
return obj and obj.type == "MESH"
def __separate_by_materials(self, obj):
def __separate_by_materials(self, obj: Object) -> None:
logger.info(f"Separating {obj.name} by materials")
utils.separateByMaterials(obj)
if self.clean_shape_keys:
logger.debug("Cleaning shape keys after separation")
bpy.ops.mmd_tools.clean_shape_keys()
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
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()
@@ -171,9 +185,11 @@ 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"}
@@ -191,13 +207,15 @@ class JoinMeshes(bpy.types.Operator):
default=True,
)
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
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()
@@ -205,9 +223,11 @@ 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)
@@ -216,15 +236,19 @@ 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"}
@@ -238,17 +262,20 @@ class AttachMeshesToMMD(bpy.types.Operator):
add_armature_modifier: bpy.props.BoolProperty(default=True)
def execute(self, context: bpy.types.Context):
def execute(self, context: Context) -> Set[str]:
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"}
@@ -268,17 +295,18 @@ class ChangeMMDIKLoopFactor(bpy.types.Operator):
)
@classmethod
def poll(cls, context):
def poll(cls, context: Context) -> bool:
return FnModel.find_root_object(context.active_object) is not None
def invoke(self, context, event):
def invoke(self, context: Context, event: Any) -> Set[str]:
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):
def execute(self, context: Context) -> Set[str]:
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"}
@@ -290,21 +318,22 @@ class RecalculateBoneRoll(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
def poll(cls, context: Context) -> bool:
obj = context.active_object
return obj and obj.type == "ARMATURE"
def invoke(self, context, event):
def invoke(self, context: Context, event: Any) -> Set[str]:
vm = context.window_manager
return vm.invoke_props_dialog(self)
def draw(self, context):
def draw(self, context: Context) -> None:
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):
def execute(self, context: Context) -> Set[str]:
arm = context.active_object
logger.info(f"Recalculating bone roll for armature: {arm.name}")
FnBone.apply_auto_bone_roll(arm)
return {"FINISHED"}
+61 -24
View File
@@ -6,10 +6,12 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import bpy
from typing import Optional, Set, Dict, Any, List, Tuple, Union
from ..bpyutils import FnContext
from ..core.bone import FnBone, MigrationFnBone
from ..core.model import FnModel, Model
from ....core.logging_setup import logger
class MorphSliderSetup(bpy.types.Operator):
@@ -29,18 +31,22 @@ class MorphSliderSetup(bpy.types.Operator):
default="CREATE",
)
def execute(self, context: bpy.types.Context):
def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object
root_object = FnModel.find_root_object(active_object)
assert root_object is not None
logger.debug(f"Executing MorphSliderSetup with type: {self.type}")
with FnContext.temp_override_active_layer_collection(context, root_object):
rig = Model(root_object)
if self.type == "BIND":
logger.info(f"Binding morph sliders for {root_object.name}")
rig.morph_slider.bind()
elif self.type == "UNBIND":
logger.info(f"Unbinding morph sliders for {root_object.name}")
rig.morph_slider.unbind()
else:
logger.info(f"Creating morph sliders for {root_object.name}")
rig.morph_slider.create()
FnContext.set_active_object(context, active_object)
@@ -53,10 +59,11 @@ class CleanRiggingObjects(bpy.types.Operator):
bl_description = "Delete temporary physics objects of selected object and revert physics to default MMD state"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
root_object = FnModel.find_root_object(context.active_object)
assert root_object is not None
logger.info(f"Cleaning rig for {root_object.name}")
rig = Model(root_object)
rig.clean()
FnContext.set_active_object(context, root_object)
@@ -86,9 +93,10 @@ class BuildRig(bpy.types.Operator):
default=1e-06,
)
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
root_object = FnModel.find_root_object(context.active_object)
logger.info(f"Building rig for {root_object.name} with non_collision_distance_scale={self.non_collision_distance_scale}, collision_margin={self.collision_margin}")
with FnContext.temp_override_active_layer_collection(context, root_object):
rig = Model(root_object)
rig.build(self.non_collision_distance_scale, self.collision_margin)
@@ -103,11 +111,14 @@ class CleanAdditionalTransformConstraints(bpy.types.Operator):
bl_description = "Delete shadow bones of selected object and revert bones to default MMD state"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object
root_object = FnModel.find_root_object(active_object)
assert root_object is not None
FnBone.clean_additional_transformation(FnModel.find_armature_object(root_object))
logger.info(f"Cleaning additional transform constraints for {root_object.name}")
armature_object = FnModel.find_armature_object(root_object)
FnBone.clean_additional_transformation(armature_object)
FnContext.set_active_object(context, active_object)
return {"FINISHED"}
@@ -118,11 +129,12 @@ class ApplyAdditionalTransformConstraints(bpy.types.Operator):
bl_description = "Translate appended bones of selected object for Blender"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object
root_object = FnModel.find_root_object(active_object)
assert root_object is not None
logger.info(f"Applying additional transform constraints for {root_object.name}")
armature_object = FnModel.find_armature_object(root_object)
assert armature_object is not None
@@ -149,12 +161,14 @@ class SetupBoneFixedAxes(bpy.types.Operator):
default="LOAD",
)
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
armature_object = context.active_object
if not armature_object or armature_object.type != "ARMATURE":
self.report({"ERROR"}, "Active object is not an armature object")
logger.error("Setup Bone Fixed Axis failed: Active object is not an armature object")
return {"CANCELLED"}
logger.info(f"Setting up bone fixed axes with type: {self.type}")
if self.type == "APPLY":
FnBone.apply_bone_fixed_axis(armature_object)
FnBone.apply_additional_transformation(armature_object)
@@ -180,12 +194,14 @@ class SetupBoneLocalAxes(bpy.types.Operator):
default="LOAD",
)
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
armature_object = context.active_object
if not armature_object or armature_object.type != "ARMATURE":
self.report({"ERROR"}, "Active object is not an armature object")
logger.error("Setup Bone Local Axes failed: Active object is not an armature object")
return {"CANCELLED"}
logger.info(f"Setting up bone local axes with type: {self.type}")
if self.type == "APPLY":
FnBone.apply_bone_local_axes(armature_object)
FnBone.apply_additional_transformation(armature_object)
@@ -207,16 +223,18 @@ class AddMissingVertexGroupsFromBones(bpy.types.Operator):
)
@classmethod
def poll(cls, context: bpy.types.Context):
def poll(cls, context: bpy.types.Context) -> bool:
return FnModel.find_root_object(context.active_object) is not None
def execute(self, context: bpy.types.Context):
def execute(self, context: bpy.types.Context) -> Set[str]:
active_object: bpy.types.Object = context.active_object
root_object = FnModel.find_root_object(active_object)
assert root_object is not None
logger.info(f"Adding missing vertex groups from bones for {root_object.name}, search_in_all_meshes={self.search_in_all_meshes}")
bone_order_mesh_object = FnModel.find_bone_order_mesh_object(root_object)
if bone_order_mesh_object is None:
logger.error("Failed to find bone order mesh object")
return {"CANCELLED"}
FnModel.add_missing_vertex_groups_from_bones(root_object, bone_order_mesh_object, self.search_in_all_meshes)
@@ -246,12 +264,13 @@ class CreateMMDModelRoot(bpy.types.Operator):
default=0.08,
)
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
logger.info(f"Creating MMD model root object with name_j={self.name_j}, name_e={self.name_e}, scale={self.scale}")
rig = Model.create(self.name_j, self.name_e, self.scale, add_root_bone=True)
rig.initialDisplayFrames()
return {"FINISHED"}
def invoke(self, context, event):
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
vm = context.window_manager
return vm.invoke_props_dialog(self)
@@ -305,15 +324,16 @@ class ConvertToMMDModel(bpy.types.Operator):
)
@classmethod
def poll(cls, context):
def poll(cls, context: bpy.types.Context) -> bool:
obj = context.active_object
return obj and obj.type == "ARMATURE" and obj.mode != "EDIT"
def invoke(self, context, event):
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
vm = context.window_manager
return vm.invoke_props_dialog(self)
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
logger.info(f"Converting to MMD model with scale={self.scale}, convert_material_nodes={self.convert_material_nodes}")
# TODO convert some basic MMD properties
armature_object = context.active_object
scale = self.scale
@@ -321,29 +341,31 @@ class ConvertToMMDModel(bpy.types.Operator):
root_object = FnModel.find_root_object(armature_object)
if root_object is None or root_object != armature_object.parent:
logger.debug("Creating new MMD model")
Model.create(model_name, model_name, scale, armature_object=armature_object)
self.__attach_meshes_to(armature_object, FnContext.get_scene_objects(context))
self.__configure_rig(context, Model(armature_object.parent))
return {"FINISHED"}
def __attach_meshes_to(self, armature_object: bpy.types.Object, objects: bpy.types.SceneObjects):
def __is_child_of_armature(mesh):
def __attach_meshes_to(self, armature_object: bpy.types.Object, objects: bpy.types.SceneObjects) -> None:
def __is_child_of_armature(mesh: bpy.types.Object) -> bool:
if mesh.parent is None:
return False
return mesh.parent == armature_object or __is_child_of_armature(mesh.parent)
def __is_using_armature(mesh):
def __is_using_armature(mesh: bpy.types.Object) -> bool:
for m in mesh.modifiers:
if m.type == "ARMATURE" and m.object == armature_object:
return True
return False
def __get_root(mesh):
def __get_root(mesh: bpy.types.Object) -> bpy.types.Object:
if mesh.parent is None:
return mesh
return __get_root(mesh.parent)
attached_count = 0
for x in objects:
if __is_using_armature(x) and not __is_child_of_armature(x):
x_root = __get_root(x)
@@ -351,27 +373,35 @@ class ConvertToMMDModel(bpy.types.Operator):
x_root.parent_type = "OBJECT"
x_root.parent = armature_object
x_root.matrix_world = m
attached_count += 1
logger.debug(f"Attached {attached_count} meshes to armature")
def __configure_rig(self, context: bpy.types.Context, mmd_model: Model):
def __configure_rig(self, context: bpy.types.Context, mmd_model: Model) -> None:
root_object = mmd_model.rootObject()
armature_object = mmd_model.armature()
mesh_objects = tuple(mmd_model.meshes())
logger.info(f"Configuring rig for {root_object.name} with {len(mesh_objects)} meshes")
mmd_model.loadMorphs()
if self.middle_joint_bones_lock:
vertex_groups = {g.name for mesh in mesh_objects for g in mesh.vertex_groups}
locked_bones = 0
for pose_bone in armature_object.pose.bones:
if not pose_bone.parent:
continue
if not pose_bone.bone.use_connect and pose_bone.name not in vertex_groups:
continue
pose_bone.lock_location = (True, True, True)
locked_bones += 1
logger.debug(f"Locked {locked_bones} middle joint bones")
from ..core.material import FnMaterial
FnMaterial.set_nodes_are_readonly(not self.convert_material_nodes)
try:
converted_materials = 0
for m in (x for mesh in mesh_objects for x in mesh.data.materials if x):
FnMaterial.convert_to_mmd_material(m, context)
mmd_material = m.mmd_material
@@ -384,6 +414,8 @@ class ConvertToMMDModel(bpy.types.Operator):
line_color = list(m.line_color)
mmd_material.enabled_toon_edge = line_color[3] >= self.edge_threshold
mmd_material.edge_color = line_color[:3] + [max(line_color[3], self.edge_alpha_min)]
converted_materials += 1
logger.debug(f"Converted {converted_materials} materials")
finally:
FnMaterial.set_nodes_are_readonly(False)
from .display_item import DisplayItemQuickSetup
@@ -400,16 +432,17 @@ class ResetObjectVisibility(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod
def poll(cls, context: bpy.types.Context):
def poll(cls, context: bpy.types.Context) -> bool:
active_object: bpy.types.Object = context.active_object
return FnModel.find_root_object(active_object) is not None
def execute(self, context: bpy.types.Context):
def execute(self, context: bpy.types.Context) -> Set[str]:
active_object: bpy.types.Object = context.active_object
mmd_root_object = FnModel.find_root_object(active_object)
assert mmd_root_object is not None
mmd_root = mmd_root_object.mmd_root
logger.info(f"Resetting object visibility for {mmd_root_object.name}")
mmd_root_object.hide_set(False)
rigid_group_object = FnModel.find_rigid_group_object(mmd_root_object)
@@ -440,11 +473,12 @@ class AssembleAll(bpy.types.Operator):
bl_label = "Assemble All"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object
root_object = FnModel.find_root_object(active_object)
assert root_object is not None
logger.info(f"Assembling all components for {root_object.name}")
with FnContext.temp_override_active_layer_collection(context, root_object) as context:
rig = Model(root_object)
MigrationFnBone.fix_mmd_ik_limit_override(rig.armature())
@@ -452,6 +486,7 @@ class AssembleAll(bpy.types.Operator):
rig.build()
rig.morph_slider.bind()
logger.debug("Binding SDEF weights")
with context.temp_override(selected_objects=[active_object]):
bpy.ops.mmd_tools.sdef_bind()
root_object.mmd_root.use_property_driver = True
@@ -466,13 +501,15 @@ class DisassembleAll(bpy.types.Operator):
bl_label = "Disassemble All"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object
root_object = FnModel.find_root_object(active_object)
assert root_object is not None
logger.info(f"Disassembling all components for {root_object.name}")
with FnContext.temp_override_active_layer_collection(context, root_object) as context:
root_object.mmd_root.use_property_driver = False
logger.debug("Unbinding SDEF weights")
with context.temp_override(selected_objects=[active_object]):
bpy.ops.mmd_tools.sdef_unbind()
+72 -31
View File
@@ -7,13 +7,17 @@
import itertools
from operator import itemgetter
from typing import Dict, List, Optional, Set
from typing import Dict, List, Optional, Set, Tuple, Any
import bmesh
import bpy
import numpy as np
import numpy.typing as npt
from bpy.types import Context, Object, Operator, EditBone, Mesh, Armature
from ..bpyutils import FnContext
from ..core.model import FnModel, Model
from ....core.logging_setup import logger
class MessageException(Exception):
@@ -35,8 +39,8 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
)
@classmethod
def poll(cls, context: bpy.types.Context):
active_object: Optional[bpy.types.Object] = context.active_object
def poll(cls, context: Context) -> bool:
active_object: Optional[Object] = context.active_object
if context.mode != "POSE":
return False
@@ -52,19 +56,22 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
return len(context.selected_pose_bones) > 0
def invoke(self, context, event):
def invoke(self, context: Context, event: Any) -> Set[str]:
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: bpy.types.Context):
def execute(self, context: Context) -> Set[str]:
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)}")
self.report(type={"ERROR"}, message=str(ex))
return {"CANCELLED"}
return {"FINISHED"}
def join(self, context: bpy.types.Context):
def join(self, context: Context) -> None:
bpy.ops.object.mode_set(mode="OBJECT")
parent_root_object = FnModel.find_root_object(context.active_object)
@@ -74,6 +81,7 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
if parent_root_object is None or len(child_root_objects) == 0:
raise MessageException("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)
@@ -82,11 +90,12 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
# Connect child bones
if self.join_type == "CONNECTED":
parent_edit_bone: bpy.types.EditBone = context.active_bone
child_edit_bones: Set[bpy.types.EditBone] = set(context.selected_bones)
parent_edit_bone: EditBone = context.active_bone
child_edit_bones: Set[EditBone] = set(context.selected_bones)
child_edit_bones.remove(parent_edit_bone)
child_edit_bone: bpy.types.EditBone
logger.debug(f"Connecting {len(child_edit_bones)} child bones to parent bone: {parent_edit_bone.name}")
child_edit_bone: EditBone
for child_edit_bone in child_edit_bones:
child_edit_bone.use_connect = True
@@ -111,8 +120,8 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
)
@classmethod
def poll(cls, context: bpy.types.Context):
active_object: Optional[bpy.types.Object] = context.active_object
def poll(cls, context: Context) -> bool:
active_object: Optional[Object] = context.active_object
if context.mode != "POSE":
return False
@@ -128,56 +137,70 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
return len(context.selected_pose_bones) > 0
def invoke(self, context, event):
def invoke(self, context: Context, event: Any) -> Set[str]:
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: bpy.types.Context):
def execute(self, context: Context) -> Set[str]:
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)}")
self.report(type={"ERROR"}, message=str(ex))
return {"CANCELLED"}
return {"FINISHED"}
def separate(self, context: bpy.types.Context):
def separate(self, context: Context) -> None:
weight_threshold: float = self.weight_threshold
mmd_scale = 0.08
target_armature_object: bpy.types.Object = context.active_object
target_armature_object: Object = context.active_object
logger.debug(f"Target armature: {target_armature_object.name}")
bpy.ops.object.mode_set(mode="EDIT")
root_bones: Set[bpy.types.EditBone] = set(context.selected_bones)
root_bones: Set[EditBone] = set(context.selected_bones)
logger.debug(f"Selected root bones: {len(root_bones)}")
if self.include_descendant_bones:
logger.debug("Including descendant bones")
for edit_bone in root_bones:
with context.temp_override(active_bone=edit_bone):
bpy.ops.armature.select_similar(type="CHILDREN", threshold=0.1)
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}
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: bpy.types.Object = FnModel.find_root_object(context.active_object)
mmd_root_object: Object = FnModel.find_root_object(context.active_object)
mmd_model = Model(mmd_root_object)
mmd_model_mesh_objects: List[bpy.types.Object] = list(mmd_model.meshes())
mmd_model_mesh_objects: List[Object] = list(mmd_model.meshes())
logger.debug(f"Found {len(mmd_model_mesh_objects)} mesh objects in model")
mmd_model_mesh_objects = list(self.select_weighted_vertices(mmd_model_mesh_objects, separate_bones, deform_bones, weight_threshold).keys())
mesh_selection_result = self.select_weighted_vertices(mmd_model_mesh_objects, separate_bones, deform_bones, weight_threshold)
mmd_model_mesh_objects = list(mesh_selection_result.keys())
logger.debug(f"Selected {len(mmd_model_mesh_objects)} mesh objects with weighted vertices")
# separate armature bones
separate_armature_object: Optional[bpy.types.Object]
separate_armature_object: Optional[Object]
if self.separate_armature:
logger.debug("Separating armature")
target_armature_object.select_set(True)
bpy.ops.armature.separate()
separate_armature_object = next(iter([a for a in context.selected_objects if a != target_armature_object]), None)
if separate_armature_object:
logger.debug(f"Created separate armature: {separate_armature_object.name}")
bpy.ops.object.mode_set(mode="OBJECT")
# 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}
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")
boundary_joint_owner_condition = any if self.boundary_joint_owner == "DESTINATION" else all
# collect separate joints
separate_joints: Set[bpy.types.Object] = {
separate_joints: Set[Object] = {
joint_object
for joint_object in mmd_model.joints()
if boundary_joint_owner_condition(
@@ -187,35 +210,43 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
]
)
}
logger.debug(f"Found {len(separate_joints)} joints to separate")
separate_mesh_objects: Set[bpy.types.Object]
model2separate_mesh_objects: Dict[bpy.types.Object, bpy.types.Object]
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
obj: bpy.types.Object
logger.debug("Selecting meshes for separation")
obj: 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")
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.separate(type="SELECTED")
separate_mesh_objects: List[bpy.types.Object] = [m for m in context.selected_objects if m.type == "MESH" and m not in mmd_model_mesh_objects]
separate_mesh_objects: List[Object] = [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))
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)
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}")
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],
@@ -223,6 +254,7 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
bpy.ops.object.join()
# 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],
@@ -230,19 +262,23 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
# replace mesh armature modifier.object
logger.debug("Updating armature modifiers on separate meshes")
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.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)
logger.debug("Parenting joints to new model")
with context.temp_override(
object=separate_model.jointGroupObject(),
selected_editable_objects=[separate_model.jointGroupObject(), *separate_joints],
@@ -257,10 +293,12 @@ 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)
logger.debug("Copying MMD root properties")
FnModel.copy_mmd_root(
separate_root_object,
mmd_root_object,
@@ -271,13 +309,15 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
},
)
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] = dict()
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()
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: bpy.types.Mesh = mesh_object.data
mesh: 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()
@@ -304,6 +344,7 @@ 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)
+59 -32
View File
@@ -5,7 +5,7 @@
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import Optional, cast
from typing import Optional, cast, List, Dict, Any, Set, Tuple, Union
import bpy
from mathutils import Quaternion, Vector
@@ -16,10 +16,11 @@ from ..core.exceptions import MaterialNotFoundError
from ..core.material import FnMaterial
from ..core.morph import FnMorph
from ..utils import ItemMoveOp, ItemOp
from ....logging_setup import logger
# Util functions
def divide_vector_components(vec1, vec2):
def divide_vector_components(vec1: List[float], vec2: List[float]) -> List[float]:
if len(vec1) != len(vec2):
raise ValueError("Vectors should have the same number of components")
result = []
@@ -33,7 +34,7 @@ def divide_vector_components(vec1, vec2):
return result
def multiply_vector_components(vec1, vec2):
def multiply_vector_components(vec1: List[float], vec2: List[float]) -> List[float]:
if len(vec1) != len(vec2):
raise ValueError("Vectors should have the same number of components")
result = []
@@ -42,7 +43,7 @@ def multiply_vector_components(vec1, vec2):
return result
def special_division(n1, n2):
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"""
if n2 == 0:
if n1 == 0:
@@ -58,7 +59,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):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
@@ -68,6 +69,7 @@ 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"}
@@ -84,7 +86,7 @@ class RemoveMorph(bpy.types.Operator):
options={"SKIP_SAVE"},
)
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
@@ -99,9 +101,11 @@ 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"}
@@ -111,7 +115,7 @@ class MoveMorph(bpy.types.Operator, ItemMoveOp):
bl_description = "Move active morph item up/down in the list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
@@ -120,6 +124,7 @@ 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"}
@@ -129,7 +134,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):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -156,6 +161,7 @@ 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"}
@@ -165,17 +171,17 @@ class OverwriteBoneMorphsFromActionPose(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod
def poll(cls, context):
def poll(cls, context: bpy.types.Context) -> bool:
root = FnModel.find_root_object(context.active_object)
if root is None:
return False
return root.mmd_root.active_morph_type == "bone_morphs"
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
root = FnModel.find_root_object(context.active_object)
FnMorph.overwrite_bone_morphs_from_action_pose(FnModel.find_armature_object(root))
logger.info("Overwrote bone morphs from active action pose")
return {"FINISHED"}
@@ -185,7 +191,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):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
@@ -210,6 +216,7 @@ 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"}
@@ -226,7 +233,7 @@ class RemoveMorphOffset(bpy.types.Operator):
options={"SKIP_SAVE"},
)
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -243,17 +250,21 @@ 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.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"}
@@ -269,7 +280,7 @@ class InitMaterialOffset(bpy.types.Operator):
default=0,
)
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
@@ -281,6 +292,7 @@ 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"}
@@ -290,7 +302,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):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
@@ -328,6 +340,7 @@ 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
@@ -345,6 +358,7 @@ 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"}
@@ -354,7 +368,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):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
@@ -413,6 +427,7 @@ 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"}
@@ -422,13 +437,13 @@ class ClearTempMaterials(bpy.types.Operator):
bl_description = "Clears all the temporary materials"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
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):
def __pre_remove(m: Optional[bpy.types.Material]) -> bool:
if m and "_temp" in m.name:
base_mat_name = m.name.split("_temp")[0]
try:
@@ -439,6 +454,7 @@ class ClearTempMaterials(bpy.types.Operator):
return False
FnMaterial.clean_materials(meshObj, can_remove=__pre_remove)
logger.info("Cleared all temporary materials")
return {"FINISHED"}
@@ -448,7 +464,7 @@ class ViewBoneMorph(bpy.types.Operator):
bl_description = "View the result of active bone morph"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -463,6 +479,7 @@ class ViewBoneMorph(bpy.types.Operator):
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"}
@@ -472,13 +489,14 @@ 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):
def execute(self, context: bpy.types.Context) -> Set[str]:
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"}
@@ -488,7 +506,7 @@ class ApplyBoneMorph(bpy.types.Operator):
bl_description = "Apply current pose to active bone morph"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -506,6 +524,7 @@ class ApplyBoneMorph(bpy.types.Operator):
p_bone.bone.select = True
else:
p_bone.bone.select = False
logger.info(f"Applied current pose to bone morph: {morph.name}")
return {"FINISHED"}
@@ -515,7 +534,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):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -524,6 +543,7 @@ 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"}
@@ -533,7 +553,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):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -546,6 +566,7 @@ 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"}
@@ -555,7 +576,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):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -567,6 +588,7 @@ 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"}
@@ -576,7 +598,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):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -627,6 +649,7 @@ class ViewUVMorph(bpy.types.Operator):
uv_tex.active_render = True
meshObj.hide_set(False)
meshObj.select_set(selected)
logger.info(f"Viewing UV morph: {morph.name}")
return {"FINISHED"}
@@ -636,7 +659,7 @@ class ClearUVMorphView(bpy.types.Operator):
bl_description = "Clear all temporary data of UV morphs"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -664,6 +687,7 @@ class ClearUVMorphView(bpy.types.Operator):
for act in 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"}
@@ -674,14 +698,14 @@ class EditUVMorph(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod
def poll(cls, context):
def poll(cls, context: bpy.types.Context) -> bool:
obj = context.active_object
if obj.type != "MESH":
return False
active_uv_layer = obj.data.uv_layers.active
return active_uv_layer and active_uv_layer.name.startswith("__uv.")
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
meshObj = obj
@@ -704,6 +728,7 @@ class EditUVMorph(bpy.types.Operator):
bpy.ops.object.mode_set(mode="EDIT")
meshObj.select_set(selected)
logger.info("Editing UV morph")
return {"FINISHED"}
@@ -714,14 +739,14 @@ class ApplyUVMorph(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod
def poll(cls, context):
def poll(cls, context: bpy.types.Context) -> bool:
obj = context.active_object
if obj.type != "MESH":
return False
active_uv_layer = obj.data.uv_layers.active
return active_uv_layer and active_uv_layer.name.startswith("__uv.")
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
@@ -756,6 +781,7 @@ class ApplyUVMorph(bpy.types.Operator):
morph.data_type = "VERTEX_GROUP"
meshObj.select_set(selected)
logger.info(f"Applied UV morph: {morph.name}")
return {"FINISHED"}
@@ -766,11 +792,12 @@ class CleanDuplicatedMaterialMorphs(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
def poll(cls, context: bpy.types.Context) -> bool:
return FnModel.find_root_object(context.active_object) is not None
def execute(self, context: bpy.types.Context):
def execute(self, context: bpy.types.Context) -> Set[str]:
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"}
+38 -24
View File
@@ -6,7 +6,7 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import math
from typing import Dict, Optional, Tuple, cast
from typing import Dict, Optional, Tuple, cast, Set, List, Any, Union, Generator
import bpy
from mathutils import Euler, Vector
@@ -16,6 +16,7 @@ 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):
@@ -43,15 +44,15 @@ class SelectRigidBody(bpy.types.Operator):
default=False,
)
def invoke(self, context, event):
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
vm = context.window_manager
return vm.invoke_props_dialog(self)
@classmethod
def poll(cls, context):
def poll(cls, context: bpy.types.Context) -> bool:
return FnModel.is_rigid_body_object(context.active_object)
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
if root is None:
@@ -173,7 +174,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):
def __add_rigid_body(self, context: bpy.types.Context, root_object: bpy.types.Object, pose_bone: Optional[bpy.types.PoseBone] = None) -> bpy.types.Object:
name_j: str = self.name_j
name_e: str = self.name_e
size = self.size.copy()
@@ -226,7 +227,7 @@ class AddRigidBody(bpy.types.Operator):
)
@classmethod
def poll(cls, context):
def poll(cls, context: bpy.types.Context) -> bool:
root_object = FnModel.find_root_object(context.active_object)
if root_object is None:
return False
@@ -237,7 +238,7 @@ class AddRigidBody(bpy.types.Operator):
return True
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object
root_object = cast(bpy.types.Object, FnModel.find_root_object(active_object))
@@ -254,15 +255,17 @@ 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, event):
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
no_bone = True
if context.selected_bones and len(context.selected_bones) > 0:
no_bone = False
@@ -288,12 +291,13 @@ class RemoveRigidBody(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
def poll(cls, context: bpy.types.Context) -> bool:
return FnModel.is_rigid_body_object(context.active_object)
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
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:
@@ -306,7 +310,8 @@ class RigidBodyBake(bpy.types.Operator):
bl_label = "Bake"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context):
def execute(self, context: bpy.types.Context) -> Set[str]:
logger.info("Baking rigid body simulation")
with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache):
bpy.ops.ptcache.bake("INVOKE_DEFAULT", bake=True)
@@ -318,7 +323,8 @@ class RigidBodyDeleteBake(bpy.types.Operator):
bl_label = "Delete Bake"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context):
def execute(self, context: bpy.types.Context) -> Set[str]:
logger.info("Deleting rigid body simulation bake")
with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache):
bpy.ops.ptcache.free_bake("INVOKE_DEFAULT")
@@ -381,7 +387,7 @@ class AddJoint(bpy.types.Operator):
min=0,
)
def __enumerate_rigid_pair(self, bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]):
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]:
obj_seq = tuple(bone_map.keys())
for rigid_a, bone_a in bone_map.items():
for rigid_b, bone_b in bone_map.items():
@@ -394,7 +400,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):
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:
loc: Optional[Vector] = None
rot = Euler((0.0, 0.0, 0.0))
rigid_a, rigid_b = rigid_pair
@@ -432,7 +438,7 @@ class AddJoint(bpy.types.Operator):
)
@classmethod
def poll(cls, context):
def poll(cls, context: bpy.types.Context) -> bool:
root_object = FnModel.find_root_object(context.active_object)
if root_object is None:
return False
@@ -443,7 +449,7 @@ class AddJoint(bpy.types.Operator):
return True
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
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))
@@ -456,15 +462,19 @@ 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, event):
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
vm = context.window_manager
return vm.invoke_props_dialog(self)
@@ -476,12 +486,13 @@ class RemoveJoint(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
def poll(cls, context: bpy.types.Context) -> bool:
return FnModel.is_joint_object(context.active_object)
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
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:
@@ -496,7 +507,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
@staticmethod
def __get_rigid_body_world_objects():
def __get_rigid_body_world_objects() -> Tuple[bpy.types.Collection, bpy.types.Collection]:
rigid_body.setRigidBodyWorldEnabled(True)
rbw = bpy.context.scene.rigidbody_world
if not rbw.collection:
@@ -511,12 +522,12 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
return rbw.collection.objects, rbw.constraints.objects
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
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, group):
def _update_group(obj: bpy.types.Object, group: bpy.types.Collection) -> bool:
if obj in scene_objs:
if obj not in group.values():
group.link(obj)
@@ -525,7 +536,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
group.unlink(obj)
return False
def _references(obj):
def _references(obj: bpy.types.Object) -> Generator[bpy.types.Object, None, None]:
yield obj
if getattr(obj, "proxy", None):
yield from _references(obj.proxy)
@@ -542,6 +553,7 @@ 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
@@ -556,6 +568,7 @@ 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
@@ -566,6 +579,7 @@ 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
+17 -11
View File
@@ -5,18 +5,19 @@
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import Set
from typing import Set, Tuple
import bpy
from bpy.types import Operator
from bpy.types import Operator, Context, Object
from ..core.model import FnModel
from ..core.sdef import FnSDEF
from ....core.logging_setup import logger
def _get_target_objects(context):
root_objects: Set[bpy.types.Object] = set()
selected_objects: Set[bpy.types.Object] = set()
def _get_target_objects(context: Context) -> Tuple[Set[Object], Set[Object]]:
root_objects: Set[Object] = set()
selected_objects: Set[Object] = set()
for i in context.selected_objects:
if i.type == "MESH":
selected_objects.add(i)
@@ -40,11 +41,13 @@ 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):
def execute(self, context: Context) -> Set[str]:
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"}
@@ -75,19 +78,20 @@ class BindSDEF(Operator):
default=False,
)
def invoke(self, context, event):
def invoke(self, context: Context, event: bpy.types.Event) -> Set[str]:
vm = context.window_manager
return vm.invoke_props_dialog(self)
# TODO: Utility Functionalize
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
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"}
@@ -98,13 +102,15 @@ class UnbindSDEF(Operator):
bl_description = "Unbind MMD SDEF data of selected objects"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
# TODO: Utility Functionalize
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
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"}
+42 -34
View File
@@ -6,29 +6,32 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import re
from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable, Union, Type, Iterator
from bpy.types import Operator
from mathutils import Matrix
from bpy.types import Operator, Context
from mathutils import Matrix, Vector, Quaternion
from ...logging_setup import logger
class _SetShadingBase:
bl_options = {"REGISTER", "UNDO"}
bl_options: Set[str] = {"REGISTER", "UNDO"}
@staticmethod
def _get_view3d_spaces(context):
def _get_view3d_spaces(context: Context) -> Iterator[Any]:
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, use_display_device=True):
def _reset_color_management(context: Context, use_display_device: bool = True) -> None:
try:
context.scene.display_settings.display_device = ("None", "sRGB")[use_display_device]
except TypeError:
pass
@staticmethod
def _reset_material_shading(context, use_shadeless=False):
def _reset_material_shading(context: Context, use_shadeless: bool = False) -> None:
for i in (x for x in context.scene.objects if x.type == "MESH" and x.mmd_type == "NONE"):
for s in i.material_slots:
if s.material is None:
@@ -36,10 +39,11 @@ class _SetShadingBase:
s.material.use_nodes = False
s.material.use_shadeless = use_shadeless
def execute(self, context):
def execute(self, context: Context) -> Dict[str, str]:
context.scene.render.engine = "BLENDER_EEVEE_NEXT"
logger.debug(f"Setting render engine to BLENDER_EEVEE_NEXT")
shading_mode = getattr(self, "_shading_mode", None)
shading_mode: Optional[str] = getattr(self, "_shading_mode", None)
for space in self._get_view3d_spaces(context):
shading = space.shading
shading.type = "SOLID"
@@ -47,39 +51,40 @@ 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 = "mmd_tools.set_glsl_shading"
bl_label = "GLSL View"
bl_description = "Use GLSL shading with additional lighting"
bl_idname: str = "mmd_tools.set_glsl_shading"
bl_label: str = "GLSL View"
bl_description: str = "Use GLSL shading with additional lighting"
_shading_mode = "GLSL"
_shading_mode: str = "GLSL"
class SetShadelessGLSLShading(Operator, _SetShadingBase):
bl_idname = "mmd_tools.set_shadeless_glsl_shading"
bl_label = "Shadeless GLSL View"
bl_description = "Use only toon shading"
bl_idname: str = "mmd_tools.set_shadeless_glsl_shading"
bl_label: str = "Shadeless GLSL View"
bl_description: str = "Use only toon shading"
_shading_mode = "SHADELESS"
_shading_mode: str = "SHADELESS"
class ResetShading(Operator, _SetShadingBase):
bl_idname = "mmd_tools.reset_shading"
bl_label = "Reset View"
bl_description = "Reset to default Blender shading"
bl_idname: str = "mmd_tools.reset_shading"
bl_label: str = "Reset View"
bl_description: str = "Reset to default Blender shading"
class FlipPose(Operator):
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"}
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"}
# https://docs.blender.org/manual/en/dev/rigging/armatures/bones/editing/naming.html
__LR_REGEX = [
__LR_REGEX: List[Dict[str, Any]] = [
{"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},
@@ -87,7 +92,7 @@ class FlipPose(Operator):
{"re": re.compile(r"^(.+)(左|右)(\.\d+)?$"), "lr": 1},
{"re": re.compile(r"^(左|右)(.+)$"), "lr": 0},
]
__LR_MAP = {
__LR_MAP: Dict[str, str] = {
"RIGHT": "LEFT",
"Right": "Left",
"right": "left",
@@ -103,7 +108,7 @@ class FlipPose(Operator):
}
@classmethod
def flip_name(cls, name):
def flip_name(cls, name: str) -> str:
for regex in cls.__LR_REGEX:
match = regex["re"].match(name)
if match:
@@ -121,17 +126,15 @@ class FlipPose(Operator):
return ""
@staticmethod
def __cmul(vec1, vec2):
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)])
@staticmethod
def __matrix_compose(loc, rot, scale):
def __matrix_compose(loc: Vector, rot: Quaternion, scale: Vector) -> Matrix:
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, bone_src, bone_dest):
from mathutils import Quaternion
def __flip_pose(cls, matrix_basis: Matrix, bone_src: Any, bone_dest: Any) -> None:
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()
@@ -140,11 +143,16 @@ 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):
def poll(cls, context: Context) -> bool:
return context.active_object and context.active_object.type == "ARMATURE" and context.active_object.mode == "POSE"
def execute(self, context):
def execute(self, context: Context) -> Dict[str, str]:
logger.info("Executing flip pose operation")
pose_bones = context.active_object.pose.bones
for b, mat in [(x, x.matrix_basis.copy()) for x in context.selected_pose_bones]:
self.__flip_pose(mat, b, pose_bones.get(self.flip_name(b.name), b))
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")
return {"FINISHED"}