Update Files and Fixes

This commit is contained in:
Yusarina
2025-04-22 00:28:47 +01:00
parent bf92ca905b
commit 61e4269764
9 changed files with 524 additions and 307 deletions
+4 -5
View File
@@ -15,14 +15,14 @@ from bpy.types import Object, EditBone, PoseBone, Constraint, Armature, BoneColl
from .. import bpyutils
from ..bpyutils import TransformConstraintOp
from ..utils import ItemOp
from ....logging_setup import logger
from ....core.logging_setup import logger
if TYPE_CHECKING:
from ..properties.root import MMDRoot, MMDDisplayItemFrame
from ..properties.pose_bone import MMDBone
def remove_constraint(constraints: bpy.types.ConstraintSequence, name: str) -> bool:
def remove_constraint(constraints: Any, name: str) -> bool:
"""Remove a constraint by name if it exists"""
c = constraints.get(name, None)
if c:
@@ -30,7 +30,6 @@ def remove_constraint(constraints: bpy.types.ConstraintSequence, name: str) -> b
return True
return False
def remove_edit_bones(edit_bones: bpy.types.ArmatureEditBones, bone_names: List[str]) -> None:
"""Remove edit bones by name"""
for name in bone_names:
@@ -573,7 +572,7 @@ class _AT_ShadowBoneRemove:
remove_edit_bones(edit_bones, self.__shadow_bone_names)
logger.debug(f"Removed shadow bones: {self.__shadow_bone_names}")
def update_pose_bones(self, pose_bones: bpy.types.ArmaturePoseBones) -> None:
def update_pose_bones(self, pose_bones: Any) -> None:
"""Update pose bones (no-op for removal)"""
pass
@@ -628,7 +627,7 @@ class _AT_ShadowBoneCreate:
shadow.roll = bone.roll
logger.debug(f"Created/updated shadow bone: {shadow_bone_name}")
def update_pose_bones(self, pose_bones: bpy.types.ArmaturePoseBones) -> None:
def update_pose_bones(self, pose_bones: Any) -> None:
"""Update pose bones by setting up shadow bone properties"""
if self.__shadow_bone_name not in pose_bones:
logger.debug(f"Shadow bone {self.__shadow_bone_name} not found, using target bone directly")
+1 -1
View File
@@ -13,7 +13,7 @@ from bpy.types import Object, ID, Camera, Context
from mathutils import Vector, Matrix, Euler
from ..bpyutils import FnContext, Props
from core.logging_setup import logger
from ....core.logging_setup import logger
class FnCamera:
@staticmethod
+30 -13
View File
@@ -6,36 +6,48 @@
# 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, Union, Any, List, Tuple
from bpy.types import Object, Context
from ..bpyutils import FnContext, Props
from ....core.logging_setup import logger
class MMDLamp:
def __init__(self, obj):
def __init__(self, obj: Object) -> None:
if MMDLamp.isLamp(obj):
obj = obj.parent
if obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT":
self.__emptyObj = obj
self.__emptyObj: Object = obj
else:
raise ValueError("%s is not MMDLamp" % str(obj))
error_msg = f"{str(obj)} is not MMDLamp"
logger.error(error_msg)
raise ValueError(error_msg)
@staticmethod
def isLamp(obj):
return obj and obj.type in {"LIGHT", "LAMP"}
def isLamp(obj: Optional[Object]) -> bool:
"""Check if the object is a lamp/light object"""
return obj is not None and obj.type in {"LIGHT", "LAMP"}
@staticmethod
def isMMDLamp(obj):
def isMMDLamp(obj: Optional[Object]) -> bool:
"""Check if the object is an MMD lamp"""
if MMDLamp.isLamp(obj):
obj = obj.parent
return obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT"
return obj is not None and obj.type == "EMPTY" and obj.mmd_type == "LIGHT"
@staticmethod
def convertToMMDLamp(lampObj, scale=1.0):
def convertToMMDLamp(lampObj: Object, scale: float = 1.0) -> 'MMDLamp':
"""Convert a regular lamp to an MMD lamp"""
if MMDLamp.isMMDLamp(lampObj):
logger.debug(f"Object {lampObj.name} is already an MMD lamp")
return MMDLamp(lampObj)
empty = bpy.data.objects.new(name="MMD_Light", object_data=None)
FnContext.link_object(FnContext.ensure_context(), empty)
logger.info(f"Converting {lampObj.name} to MMD lamp with scale {scale}")
empty: Object = bpy.data.objects.new(name="MMD_Light", object_data=None)
context = FnContext.ensure_context()
FnContext.link_object(context, empty)
empty.rotation_mode = "XYZ"
empty.lock_rotation = (True, True, True)
@@ -57,13 +69,18 @@ class MMDLamp:
constraint.track_axis = "TRACK_NEGATIVE_Z"
constraint.up_axis = "UP_Y"
logger.debug(f"Successfully created MMD lamp from {lampObj.name}")
return MMDLamp(empty)
def object(self):
def object(self) -> Object:
"""Get the empty object that represents this MMD lamp"""
return self.__emptyObj
def lamp(self):
def lamp(self) -> Object:
"""Get the actual lamp/light object"""
for i in self.__emptyObj.children:
if MMDLamp.isLamp(i):
return i
raise KeyError
error_msg = f"No lamp found in MMD lamp {self.__emptyObj.name}"
logger.error(error_msg)
raise KeyError(error_msg)
+124 -64
View File
@@ -7,7 +7,7 @@
import logging
import os
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, cast
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, cast, Dict, List, Any, Union, Set
import bpy
from mathutils import Vector
@@ -15,6 +15,7 @@ from mathutils import Vector
from ..bpyutils import FnContext
from .exceptions import MaterialNotFoundError
from .shader import _NodeGroupUtils
from ....core.logging_setup import logger
if TYPE_CHECKING:
from ..properties.material import MMDMaterial
@@ -27,48 +28,53 @@ SPHERE_MODE_SUBTEX = 3
class _DummyTexture:
def __init__(self, image):
self.type = "IMAGE"
self.image = image
self.use_mipmap = True
def __init__(self, image: bpy.types.Image):
self.type: str = "IMAGE"
self.image: bpy.types.Image = image
self.use_mipmap: bool = True
class _DummyTextureSlot:
def __init__(self, image):
self.diffuse_color_factor = 1
self.uv_layer = ""
self.texture = _DummyTexture(image)
def __init__(self, image: bpy.types.Image):
self.diffuse_color_factor: float = 1
self.uv_layer: str = ""
self.texture: _DummyTexture = _DummyTexture(image)
class FnMaterial:
__NODES_ARE_READONLY: bool = False
def __init__(self, material: bpy.types.Material):
self.__material = material
self._nodes_are_readonly = FnMaterial.__NODES_ARE_READONLY
self.__material: bpy.types.Material = material
self._nodes_are_readonly: bool = FnMaterial.__NODES_ARE_READONLY
@staticmethod
def set_nodes_are_readonly(nodes_are_readonly: bool):
def set_nodes_are_readonly(nodes_are_readonly: bool) -> None:
FnMaterial.__NODES_ARE_READONLY = nodes_are_readonly
@classmethod
def from_material_id(cls, material_id: str):
def from_material_id(cls, material_id: str) -> Optional['FnMaterial']:
for material in bpy.data.materials:
if material.mmd_material.material_id == material_id:
return cls(material)
return None
@staticmethod
def clean_materials(obj, can_remove: Callable[[bpy.types.Material], bool]):
def clean_materials(obj: bpy.types.Object, can_remove: Callable[[bpy.types.Material], bool]) -> None:
materials = obj.data.materials
materials_pop = materials.pop
removed_count = 0
for i in sorted((x for x, m in enumerate(materials) if can_remove(m)), reverse=True):
m = materials_pop(index=i)
removed_count += 1
if m.users < 1:
bpy.data.materials.remove(m)
if removed_count > 0:
logger.debug(f"Removed {removed_count} materials from {obj.name}")
@staticmethod
def swap_materials(mesh_object: bpy.types.Object, mat1_ref: str | int, mat2_ref: str | int, reverse=False, swap_slots=False) -> Tuple[bpy.types.Material, bpy.types.Material]:
def swap_materials(mesh_object: bpy.types.Object, mat1_ref: Union[str, int], mat2_ref: Union[str, int], reverse: bool = False, swap_slots: bool = False) -> Tuple[bpy.types.Material, bpy.types.Material]:
"""
This method will assign the polygons of mat1 to mat2.
If reverse is True it will also swap the polygons assigned to mat2 to mat1.
@@ -98,8 +104,12 @@ class FnMaterial:
except (KeyError, IndexError) as exc:
# Wrap exceptions within our custom ones
raise MaterialNotFoundError() from exc
mat1_idx = mesh.materials.find(mat1.name)
mat2_idx = mesh.materials.find(mat2.name)
logger.debug(f"Swapping materials: {mat1.name} (idx:{mat1_idx}) <-> {mat2.name} (idx:{mat2_idx}) in {mesh_object.name}")
# Swap polygons
for poly in mesh.polygons:
if poly.material_index == mat1_idx:
@@ -113,33 +123,37 @@ class FnMaterial:
return mat1, mat2
@staticmethod
def fixMaterialOrder(meshObj: bpy.types.Object, material_names: Iterable[str]):
def fixMaterialOrder(meshObj: bpy.types.Object, material_names: Iterable[str]) -> None:
"""
This method will fix the material order. Which is lost after joining meshes.
"""
materials = cast(bpy.types.Mesh, meshObj.data).materials
logger.debug(f"Fixing material order for {meshObj.name}")
for new_idx, mat in enumerate(material_names):
# Get the material that is currently on this index
other_mat = materials[new_idx]
if other_mat.name == mat:
continue # This is already in place
logger.debug(f"Moving material {mat} to index {new_idx}")
FnMaterial.swap_materials(meshObj, mat, new_idx, reverse=True, swap_slots=True)
@property
def material_id(self):
mmd_mat: MMDMaterial = self.__material.mmd_material
def material_id(self) -> int:
mmd_mat: 'MMDMaterial' = self.__material.mmd_material
if mmd_mat.material_id < 0:
max_id = -1
for mat in bpy.data.materials:
max_id = max(max_id, mat.mmd_material.material_id)
mmd_mat.material_id = max_id + 1
logger.debug(f"Assigned new material ID {mmd_mat.material_id} to {self.__material.name}")
return mmd_mat.material_id
@property
def material(self):
def material(self) -> bpy.types.Material:
return self.__material
def __same_image_file(self, image, filepath):
def __same_image_file(self, image: Optional[bpy.types.Image], filepath: str) -> bool:
if image and image.source == "FILE":
# pylint: disable=assignment-from-no-return
img_filepath = bpy.path.abspath(image.filepath) # image.filepath_from_user()
@@ -152,14 +166,15 @@ class FnMaterial:
pass
return False
def _load_image(self, filepath):
def _load_image(self, filepath: str) -> bpy.types.Image:
img = next((i for i in bpy.data.images if self.__same_image_file(i, filepath)), None)
if img is None:
# pylint: disable=bare-except
try:
logger.debug(f"Loading image: {filepath}")
img = bpy.data.images.load(filepath)
except:
logging.warning("Cannot create a texture for %s. No such file.", filepath)
logger.warning(f"Cannot create a texture for {filepath}. No such file.")
img = bpy.data.images.new(os.path.basename(filepath), 1, 1)
img.source = "FILE"
img.filepath = filepath
@@ -170,43 +185,46 @@ class FnMaterial:
img.alpha_mode = "NONE"
return img
def update_toon_texture(self):
def update_toon_texture(self) -> None:
if self._nodes_are_readonly:
return
mmd_mat: MMDMaterial = self.__material.mmd_material
mmd_mat: 'MMDMaterial' = self.__material.mmd_material
if mmd_mat.is_shared_toon_texture:
shared_toon_folder = FnContext.get_addon_preferences_attribute(FnContext.ensure_context(), "shared_toon_folder", "")
toon_path = os.path.join(shared_toon_folder, "toon%02d.bmp" % (mmd_mat.shared_toon_texture + 1))
logger.debug(f"Using shared toon texture: {toon_path}")
self.create_toon_texture(bpy.path.resolve_ncase(path=toon_path))
elif mmd_mat.toon_texture != "":
logger.debug(f"Using custom toon texture: {mmd_mat.toon_texture}")
self.create_toon_texture(mmd_mat.toon_texture)
else:
logger.debug(f"Removing toon texture from {self.__material.name}")
self.remove_toon_texture()
def _mix_diffuse_and_ambient(self, mmd_mat):
def _mix_diffuse_and_ambient(self, mmd_mat: 'MMDMaterial') -> List[float]:
r, g, b = mmd_mat.diffuse_color
ar, ag, ab = mmd_mat.ambient_color
return [min(1.0, 0.5 * r + ar), min(1.0, 0.5 * g + ag), min(1.0, 0.5 * b + ab)]
def update_drop_shadow(self):
def update_drop_shadow(self) -> None:
pass
def update_enabled_toon_edge(self):
def update_enabled_toon_edge(self) -> None:
if self._nodes_are_readonly:
return
self.update_edge_color()
def update_edge_color(self):
def update_edge_color(self) -> None:
if self._nodes_are_readonly:
return
mat = self.__material
mmd_mat: MMDMaterial = mat.mmd_material
mmd_mat: 'MMDMaterial' = mat.mmd_material
color, alpha = mmd_mat.edge_color[:3], mmd_mat.edge_color[3]
line_color = color + (min(alpha, int(mmd_mat.enabled_toon_edge)),)
if hasattr(mat, "line_color"): # freestyle line color
mat.line_color = line_color
mat_edge: bpy.types.Material = bpy.data.materials.get("mmd_edge." + mat.name, None)
mat_edge: Optional[bpy.types.Material] = bpy.data.materials.get("mmd_edge." + mat.name, None)
if mat_edge:
mat_edge.mmd_material.edge_color = line_color
@@ -218,38 +236,45 @@ class FnMaterial:
if node_shader and "Alpha" in node_shader.inputs:
node_shader.inputs["Alpha"].default_value = alpha
def update_edge_weight(self):
logger.debug(f"Updated edge color for {mat.name}")
def update_edge_weight(self) -> None:
pass
def get_texture(self):
def get_texture(self) -> Optional[_DummyTexture]:
return self.__get_texture_node("mmd_base_tex", use_dummy=True)
def create_texture(self, filepath):
def create_texture(self, filepath: str) -> _DummyTextureSlot:
texture = self.__create_texture_node("mmd_base_tex", filepath, (-4, -1))
logger.debug(f"Created base texture for {self.__material.name}: {filepath}")
return _DummyTextureSlot(texture.image)
def remove_texture(self):
def remove_texture(self) -> None:
if self._nodes_are_readonly:
return
logger.debug(f"Removing base texture from {self.__material.name}")
self.__remove_texture_node("mmd_base_tex")
def get_sphere_texture(self):
def get_sphere_texture(self) -> Optional[_DummyTexture]:
return self.__get_texture_node("mmd_sphere_tex", use_dummy=True)
def use_sphere_texture(self, use_sphere, obj=None):
def use_sphere_texture(self, use_sphere: bool, obj: Optional[bpy.types.Object] = None) -> None:
if self._nodes_are_readonly:
return
if use_sphere:
logger.debug(f"Enabling sphere texture for {self.__material.name}")
self.update_sphere_texture_type(obj)
else:
logger.debug(f"Disabling sphere texture for {self.__material.name}")
self.__update_shader_input("Sphere Tex Fac", 0)
def create_sphere_texture(self, filepath, obj=None):
def create_sphere_texture(self, filepath: str, obj: Optional[bpy.types.Object] = None) -> _DummyTextureSlot:
texture = self.__create_texture_node("mmd_sphere_tex", filepath, (-2, -2))
logger.debug(f"Created sphere texture for {self.__material.name}: {filepath}")
self.update_sphere_texture_type(obj)
return _DummyTextureSlot(texture.image)
def update_sphere_texture_type(self, obj=None):
def update_sphere_texture_type(self, obj: Optional[bpy.types.Object] = None) -> None:
if self._nodes_are_readonly:
return
sphere_texture_type = int(self.material.mmd_material.sphere_texture_type)
@@ -277,48 +302,54 @@ class FnMaterial:
next(uv_layers, None) # skip base UV
subtex_uv = getattr(next(uv_layers, None), "name", "")
if subtex_uv != "UV1":
logging.info(' * material(%s): object "%s" use UV "%s" for SubTex', mat.name, obj.name, subtex_uv)
logger.info(f'Material({mat.name}): object "{obj.name}" use UV "{subtex_uv}" for SubTex')
links.new(nodes["mmd_tex_uv"].outputs["SubTex UV"], texture.inputs["Vector"])
else:
links.new(nodes["mmd_tex_uv"].outputs["Sphere UV"], texture.inputs["Vector"])
def remove_sphere_texture(self):
logger.debug(f"Updated sphere texture type for {self.material.name}: {sphere_texture_type}")
def remove_sphere_texture(self) -> None:
if self._nodes_are_readonly:
return
logger.debug(f"Removing sphere texture from {self.__material.name}")
self.__remove_texture_node("mmd_sphere_tex")
def get_toon_texture(self):
def get_toon_texture(self) -> Optional[_DummyTexture]:
return self.__get_texture_node("mmd_toon_tex", use_dummy=True)
def use_toon_texture(self, use_toon):
def use_toon_texture(self, use_toon: bool) -> None:
if self._nodes_are_readonly:
return
logger.debug(f"{'Enabling' if use_toon else 'Disabling'} toon texture for {self.__material.name}")
self.__update_shader_input("Toon Tex Fac", use_toon)
def create_toon_texture(self, filepath):
def create_toon_texture(self, filepath: str) -> _DummyTextureSlot:
texture = self.__create_texture_node("mmd_toon_tex", filepath, (-3, -1.5))
logger.debug(f"Created toon texture for {self.__material.name}: {filepath}")
return _DummyTextureSlot(texture.image)
def remove_toon_texture(self):
def remove_toon_texture(self) -> None:
if self._nodes_are_readonly:
return
logger.debug(f"Removing toon texture from {self.__material.name}")
self.__remove_texture_node("mmd_toon_tex")
def __get_texture_node(self, node_name, use_dummy=False):
def __get_texture_node(self, node_name: str, use_dummy: bool = False) -> Optional[Union[bpy.types.ShaderNodeTexImage, _DummyTexture]]:
mat = self.material
texture = getattr(mat.node_tree, "nodes", {}).get(node_name, None)
if isinstance(texture, bpy.types.ShaderNodeTexImage):
return _DummyTexture(texture.image) if use_dummy else texture
return None
def __remove_texture_node(self, node_name):
def __remove_texture_node(self, node_name: str) -> None:
mat = self.material
texture = getattr(mat.node_tree, "nodes", {}).get(node_name, None)
if isinstance(texture, bpy.types.ShaderNodeTexImage):
mat.node_tree.nodes.remove(texture)
mat.update_tag()
def __create_texture_node(self, node_name, filepath, pos):
def __create_texture_node(self, node_name: str, filepath: str, pos: Tuple[float, float]) -> bpy.types.ShaderNodeTexImage:
texture = self.__get_texture_node(node_name)
if texture is None:
from mathutils import Vector
@@ -334,23 +365,25 @@ class FnMaterial:
self.__update_shader_nodes()
return texture
def update_ambient_color(self):
def update_ambient_color(self) -> None:
if self._nodes_are_readonly:
return
mat = self.material
mmd_mat = mat.mmd_material
mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat)
self.__update_shader_input("Ambient Color", mmd_mat.ambient_color[:] + (1,))
logger.debug(f"Updated ambient color for {mat.name}")
def update_diffuse_color(self):
def update_diffuse_color(self) -> None:
if self._nodes_are_readonly:
return
mat = self.material
mmd_mat = mat.mmd_material
mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat)
self.__update_shader_input("Diffuse Color", mmd_mat.diffuse_color[:] + (1,))
logger.debug(f"Updated diffuse color for {mat.name}")
def update_alpha(self):
def update_alpha(self) -> None:
if self._nodes_are_readonly:
return
mat = self.material
@@ -368,16 +401,18 @@ class FnMaterial:
mat.diffuse_color[3] = mmd_mat.alpha
self.__update_shader_input("Alpha", mmd_mat.alpha)
self.update_self_shadow_map()
logger.debug(f"Updated alpha for {mat.name}: {mmd_mat.alpha}")
def update_specular_color(self):
def update_specular_color(self) -> None:
if self._nodes_are_readonly:
return
mat = self.material
mmd_mat = mat.mmd_material
mat.specular_color = mmd_mat.specular_color
self.__update_shader_input("Specular Color", mmd_mat.specular_color[:] + (1,))
logger.debug(f"Updated specular color for {mat.name}")
def update_shininess(self):
def update_shininess(self) -> None:
if self._nodes_are_readonly:
return
mat = self.material
@@ -388,8 +423,9 @@ class FnMaterial:
if hasattr(mat, "specular_hardness"):
mat.specular_hardness = mmd_mat.shininess
self.__update_shader_input("Reflect", mmd_mat.shininess)
logger.debug(f"Updated shininess for {mat.name}: {mmd_mat.shininess}")
def update_is_double_sided(self):
def update_is_double_sided(self) -> None:
if self._nodes_are_readonly:
return
mat = self.material
@@ -399,8 +435,9 @@ class FnMaterial:
elif hasattr(mat, "use_backface_culling"):
mat.use_backface_culling = not mmd_mat.is_double_sided
self.__update_shader_input("Double Sided", mmd_mat.is_double_sided)
logger.debug(f"Updated double-sided setting for {mat.name}: {mmd_mat.is_double_sided}")
def update_self_shadow_map(self):
def update_self_shadow_map(self) -> None:
if self._nodes_are_readonly:
return
mat = self.material
@@ -408,21 +445,24 @@ class FnMaterial:
cast_shadows = mmd_mat.enabled_self_shadow_map if mmd_mat.alpha > 1e-3 else False
if hasattr(mat, "shadow_method"):
mat.shadow_method = "HASHED" if cast_shadows else "NONE"
logger.debug(f"Updated self shadow map for {mat.name}: {cast_shadows}")
def update_self_shadow(self):
def update_self_shadow(self) -> None:
if self._nodes_are_readonly:
return
mat = self.material
mmd_mat = mat.mmd_material
self.__update_shader_input("Self Shadow", mmd_mat.enabled_self_shadow)
logger.debug(f"Updated self shadow for {mat.name}: {mmd_mat.enabled_self_shadow}")
@staticmethod
def convert_to_mmd_material(material, context=bpy.context):
def convert_to_mmd_material(material: bpy.types.Material, context: bpy.types.Context = bpy.context) -> None:
m, mmd_material = material, material.mmd_material
logger.debug(f"Converting material to MMD material: {material.name}")
if m.use_nodes and next((n for n in m.node_tree.nodes if n.name.startswith("mmd_")), None) is None:
def search_tex_image_node(node: bpy.types.ShaderNode):
def search_tex_image_node(node: bpy.types.ShaderNode) -> Optional[bpy.types.ShaderNodeTexImage]:
if node.type == "TEX_IMAGE":
return node
for node_input in node.inputs:
@@ -459,6 +499,7 @@ class FnMaterial:
if tex_node is None:
tex_node = next((n for n in m.node_tree.nodes if n.bl_idname == "ShaderNodeTexImage"), None)
if tex_node:
logger.debug(f"Found texture node for {material.name}: {tex_node.name}")
tex_node.name = "mmd_base_tex"
else:
# Take the Base Color from BSDF if there's no texture
@@ -466,6 +507,7 @@ class FnMaterial:
if bsdf_node:
base_color_input = bsdf_node.inputs.get('Base Color') or bsdf_node.inputs.get('Color')
if base_color_input:
logger.debug(f"Using BSDF base color for {material.name}")
mmd_material.diffuse_color = base_color_input.default_value[:3]
# ambient should be half the diffuse
mmd_material.ambient_color = [x * 0.5 for x in mmd_material.diffuse_color]
@@ -498,9 +540,10 @@ class FnMaterial:
if m.use_nodes:
nodes_to_remove = [n for n in m.node_tree.nodes if n.type == 'BSDF_PRINCIPLED' or n.type.startswith('BSDF_')]
for n in nodes_to_remove:
logger.debug(f"Removing BSDF node from {material.name}: {n.name}")
m.node_tree.nodes.remove(n)
def __update_shader_input(self, name, val):
def __update_shader_input(self, name: str, val: Any) -> None:
mat = self.material
if mat.name.startswith("mmd_"): # skip mmd_edge.*
return
@@ -512,26 +555,29 @@ class FnMaterial:
val = min(max(val, interface_socket.min_value), interface_socket.max_value)
shader.inputs[name].default_value = val
def __update_shader_nodes(self):
def __update_shader_nodes(self) -> None:
mat = self.material
if mat.node_tree is None:
logger.debug(f"Creating node tree for {mat.name}")
mat.use_nodes = True
mat.node_tree.nodes.clear()
nodes, links = mat.node_tree.nodes, mat.node_tree.links
class _Dummy:
default_value, is_linked = None, True
default_value: Any = None
is_linked: bool = True
node_shader = nodes.get("mmd_shader", None)
if node_shader is None:
logger.debug(f"Creating MMD shader node for {mat.name}")
node_shader: bpy.types.ShaderNodeGroup = nodes.new("ShaderNodeGroup")
node_shader.name = "mmd_shader"
node_shader.location = (0, 1500)
node_shader.width = 200
node_shader.node_tree = self.__get_shader()
mmd_mat: MMDMaterial = mat.mmd_material
mmd_mat: 'MMDMaterial' = mat.mmd_material
node_shader.inputs.get("Ambient Color", _Dummy).default_value = mmd_mat.ambient_color[:] + (1,)
node_shader.inputs.get("Diffuse Color", _Dummy).default_value = mmd_mat.diffuse_color[:] + (1,)
node_shader.inputs.get("Specular Color", _Dummy).default_value = mmd_mat.specular_color[:] + (1,)
@@ -543,6 +589,7 @@ class FnMaterial:
node_uv = nodes.get("mmd_tex_uv", None)
if node_uv is None:
logger.debug(f"Creating MMD UV node for {mat.name}")
node_uv: bpy.types.ShaderNodeGroup = nodes.new("ShaderNodeGroup")
node_uv.name = "mmd_tex_uv"
node_uv.location = node_shader.location + Vector((-5 * 210, -2.5 * 220))
@@ -567,12 +614,13 @@ class FnMaterial:
if not texture.inputs["Vector"].is_linked:
links.new(node_uv.outputs[name_uv_out], texture.inputs["Vector"])
def __get_shader_uv(self):
def __get_shader_uv(self) -> bpy.types.ShaderNodeTree:
group_name = "MMDTexUV"
shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
if len(shader.nodes):
return shader
logger.debug(f"Creating MMD UV shader node group")
ng = _NodeGroupUtils(shader)
############################################################################
@@ -604,12 +652,13 @@ class FnMaterial:
return shader
def __get_shader(self):
def __get_shader(self) -> bpy.types.ShaderNodeTree:
group_name = "MMDShaderDev"
shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
if len(shader.nodes):
return shader
logger.debug(f"Creating MMD shader node group")
ng = _NodeGroupUtils(shader)
############################################################################
@@ -699,15 +748,18 @@ class FnMaterial:
class MigrationFnMaterial:
@staticmethod
def update_mmd_shader():
def update_mmd_shader() -> None:
mmd_shader_node_tree: Optional[bpy.types.NodeTree] = bpy.data.node_groups.get("MMDShaderDev")
if mmd_shader_node_tree is None:
logger.debug("No MMD shader node tree found, skipping update")
return
ng = _NodeGroupUtils(mmd_shader_node_tree)
if "Color" in ng.node_output.inputs:
logger.debug("MMD shader already has Color output, skipping update")
return
logger.info("Updating MMD shader node tree")
shader_diffuse: bpy.types.ShaderNodeBsdfDiffuse = [n for n in mmd_shader_node_tree.nodes if n.type == "BSDF_DIFFUSE"][0]
node_sphere: bpy.types.ShaderNodeMixRGB = shader_diffuse.inputs["Color"].links[0].from_node
node_output: bpy.types.NodeGroupOutput = ng.node_output
@@ -716,3 +768,11 @@ class MigrationFnMaterial:
ng.new_output_socket("Color", node_sphere.outputs["Color"])
ng.new_output_socket("Alpha", node_alpha.outputs["Value"])
logger.info("MMD shader node tree updated successfully")
# Add Self Shadow input if it doesn't exist
if "Self Shadow" not in ng.node_input.outputs:
logger.info("Adding Self Shadow input to MMD shader")
# Find shader_base_mix node to connect Self Shadow
shader_base_mix = shader_alpha_mix.inputs[2].links[0].from_node
ng.new_input_socket("Self Shadow", shader_base_mix.inputs["Fac"], 0, min_max=(0, 1))
+156 -86
View File
@@ -6,9 +6,8 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import itertools
import logging
import time
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator, Optional, Set, TypeGuard, Union, cast
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator, Optional, Set, TypeGuard, Union, cast, List, Tuple
import bpy
import idprop
@@ -20,15 +19,17 @@ from ..bpyutils import FnContext, Props
from . import rigid_body
from .morph import FnMorph
from .rigid_body import MODE_DYNAMIC, MODE_DYNAMIC_BONE, MODE_STATIC
from ....core.logging_setup import logger
if TYPE_CHECKING:
from ..properties.morph import MaterialMorphData
from ..properties.rigid_body import MMDRigidBody
from bpy.types import Context, Object, PropertyGroup, Material, Mesh, Armature, EditBone, PoseBone, KinematicConstraint
class FnModel:
@staticmethod
def copy_mmd_root(destination_root_object: bpy.types.Object, source_root_object: bpy.types.Object, overwrite: bool = True, replace_name2values: Dict[str, Dict[Any, Any]] = None):
def copy_mmd_root(destination_root_object: bpy.types.Object, source_root_object: bpy.types.Object, overwrite: bool = True, replace_name2values: Optional[Dict[str, Dict[Any, Any]]] = None) -> None:
FnModel.__copy_property(destination_root_object.mmd_root, source_root_object.mmd_root, overwrite=overwrite, replace_name2values=replace_name2values or {})
@staticmethod
@@ -213,7 +214,8 @@ class FnModel:
return obj is not None and obj.type == "MESH" and obj.mmd_type == "NONE"
@staticmethod
def join_models(parent_root_object: bpy.types.Object, child_root_objects: Iterable[bpy.types.Object]):
def join_models(parent_root_object: bpy.types.Object, child_root_objects: Iterable[bpy.types.Object]) -> None:
logger.info(f"Joining models to parent root: {parent_root_object.name}")
parent_armature_object = FnModel.find_armature_object(parent_root_object)
with bpy.context.temp_override(
active_object=parent_armature_object,
@@ -221,7 +223,7 @@ class FnModel:
):
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
def _change_bone_id(bone: bpy.types.PoseBone, new_bone_id: int, bone_morphs, pose_bones):
def _change_bone_id(bone: bpy.types.PoseBone, new_bone_id: int, bone_morphs: List[Any], pose_bones: List[bpy.types.PoseBone]) -> None:
"""This function will also update the references of bone morphs and rotate+/move+."""
bone_id = bone.mmd_bone.bone_id
@@ -259,6 +261,7 @@ class FnModel:
child_root_object: bpy.types.Object
for child_root_object in child_root_objects:
logger.info(f"Processing child root: {child_root_object.name}")
child_armature_object = FnModel.find_armature_object(child_root_object)
child_pose_bones = child_armature_object.pose.bones
child_bone_morphs = child_root_object.mmd_root.bone_morphs
@@ -279,7 +282,7 @@ class FnModel:
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
# Disconnect mesh dependencies because transform_apply fails when mesh data are multiple used.
related_meshes: Dict[MaterialMorphData, bpy.types.Mesh] = {}
related_meshes: Dict['MaterialMorphData', bpy.types.Mesh] = {}
for material_morph in child_root_object.mmd_root.material_morphs:
for material_morph_data in material_morph.data:
if material_morph_data.related_mesh_data is not None:
@@ -353,6 +356,8 @@ class FnModel:
if len(child_root_object.children) == 0:
bpy.data.objects.remove(child_root_object)
logger.info("Model joining completed successfully")
@staticmethod
def _add_armature_modifier(mesh_object: bpy.types.Object, armature_object: bpy.types.Object) -> bpy.types.ArmatureModifier:
for m in mesh_object.modifiers:
@@ -369,10 +374,13 @@ class FnModel:
return modifier
@staticmethod
def attach_mesh_objects(parent_root_object: bpy.types.Object, mesh_objects: Iterable[bpy.types.Object], add_armature_modifier: bool):
def attach_mesh_objects(parent_root_object: bpy.types.Object, mesh_objects: Iterable[bpy.types.Object], add_armature_modifier: bool) -> None:
logger.info(f"Attaching mesh objects to {parent_root_object.name}")
armature_object = FnModel.find_armature_object(parent_root_object)
if armature_object is None:
raise ValueError(f"Armature object not found in {parent_root_object}")
error_msg = f"Armature object not found in {parent_root_object.name}"
logger.error(error_msg)
raise ValueError(error_msg)
def __get_root_object(obj: bpy.types.Object) -> bpy.types.Object:
if obj.parent is None:
@@ -381,9 +389,11 @@ class FnModel:
for mesh_object in mesh_objects:
if not FnModel.is_mesh_object(mesh_object):
logger.debug(f"Skipping non-mesh object: {mesh_object.name}")
continue
if FnModel.find_root_object(mesh_object) is not None:
logger.debug(f"Skipping mesh with existing root: {mesh_object.name}")
continue
mesh_root_object = __get_root_object(mesh_object)
@@ -391,15 +401,20 @@ class FnModel:
mesh_root_object.parent_type = "OBJECT"
mesh_root_object.parent = armature_object
mesh_root_object.matrix_world = original_matrix_world
logger.debug(f"Attached mesh: {mesh_object.name}")
if add_armature_modifier:
FnModel._add_armature_modifier(mesh_object, armature_object)
logger.debug(f"Added armature modifier to: {mesh_object.name}")
@staticmethod
def add_missing_vertex_groups_from_bones(root_object: bpy.types.Object, mesh_object: bpy.types.Object, search_in_all_meshes: bool):
def add_missing_vertex_groups_from_bones(root_object: bpy.types.Object, mesh_object: bpy.types.Object, search_in_all_meshes: bool) -> None:
logger.info(f"Adding missing vertex groups from bones to {mesh_object.name}")
armature_object = FnModel.find_armature_object(root_object)
if armature_object is None:
raise ValueError(f"Armature object not found in {root_object}")
error_msg = f"Armature object not found in {root_object.name}"
logger.error(error_msg)
raise ValueError(error_msg)
vertex_group_names: Set[str] = set()
@@ -408,6 +423,7 @@ class FnModel:
for search_mesh in search_meshes:
vertex_group_names.update(search_mesh.vertex_groups.keys())
added_count = 0
pose_bone: bpy.types.PoseBone
for pose_bone in armature_object.pose.bones:
pose_bone_name = pose_bone.name
@@ -419,28 +435,34 @@ class FnModel:
continue
mesh_object.vertex_groups.new(name=pose_bone_name)
added_count += 1
logger.debug(f"Added {added_count} missing vertex groups to {mesh_object.name}")
@staticmethod
def change_mmd_ik_loop_factor(root_object: bpy.types.Object, new_ik_loop_factor: int):
def change_mmd_ik_loop_factor(root_object: bpy.types.Object, new_ik_loop_factor: int) -> None:
logger.info(f"Changing IK loop factor to {new_ik_loop_factor}")
mmd_root = root_object.mmd_root
old_ik_loop_factor = mmd_root.ik_loop_factor
if new_ik_loop_factor == old_ik_loop_factor:
logger.debug("IK loop factor already set to the requested value")
return
armature_object = FnModel.find_armature_object(root_object)
updated_count = 0
for pose_bone in armature_object.pose.bones:
for constraint in (cast(bpy.types.KinematicConstraint, c) for c in pose_bone.constraints if c.type == "IK"):
iterations = int(constraint.iterations * new_ik_loop_factor / old_ik_loop_factor)
logging.info("Update %s of %s: %d -> %d", constraint.name, pose_bone.name, constraint.iterations, iterations)
logger.debug(f"Update {constraint.name} of {pose_bone.name}: {constraint.iterations} -> {iterations}")
constraint.iterations = iterations
updated_count += 1
mmd_root.ik_loop_factor = new_ik_loop_factor
return
logger.info(f"Updated {updated_count} IK constraints")
@staticmethod
def __copy_property_group(destination: bpy.types.PropertyGroup, source: bpy.types.PropertyGroup, overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]):
def __copy_property_group(destination: bpy.types.PropertyGroup, source: bpy.types.PropertyGroup, overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]) -> None:
destination_rna_properties = destination.bl_rna.properties
for name in source.keys():
is_attr = hasattr(source, name)
@@ -466,7 +488,7 @@ class FnModel:
destination[name] = value
@staticmethod
def __copy_collection_property(destination: bpy.types.bpy_prop_collection, source: bpy.types.bpy_prop_collection, overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]):
def __copy_collection_property(destination: bpy.types.bpy_prop_collection, source: bpy.types.bpy_prop_collection, overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]) -> None:
if overwrite:
destination.clear()
@@ -499,16 +521,19 @@ class FnModel:
FnModel.__copy_property(destination[index], source[index], overwrite=True, replace_name2values=replace_name2values)
@staticmethod
def __copy_property(destination: Union[bpy.types.PropertyGroup, bpy.types.bpy_prop_collection], source: Union[bpy.types.PropertyGroup, bpy.types.bpy_prop_collection], overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]):
def __copy_property(destination: Union[bpy.types.PropertyGroup, bpy.types.bpy_prop_collection], source: Union[bpy.types.PropertyGroup, bpy.types.bpy_prop_collection], overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]) -> None:
if isinstance(destination, bpy.types.PropertyGroup):
FnModel.__copy_property_group(destination, source, overwrite=overwrite, replace_name2values=replace_name2values)
elif isinstance(destination, bpy.types.bpy_prop_collection):
FnModel.__copy_collection_property(destination, source, overwrite=overwrite, replace_name2values=replace_name2values)
else:
raise ValueError(f"Unsupported destination: {destination}")
error_msg = f"Unsupported destination: {destination}"
logger.error(error_msg)
raise ValueError(error_msg)
@staticmethod
def initalize_display_item_frames(root_object: bpy.types.Object, reset: bool = True):
def initalize_display_item_frames(root_object: bpy.types.Object, reset: bool = True) -> None:
logger.info(f"Initializing display item frames for {root_object.name}")
frames = root_object.mmd_root.display_item_frames
if reset and len(frames) > 0:
root_object.mmd_root.active_display_item_frame = 0
@@ -532,6 +557,8 @@ class FnModel:
frames.move(frames.find("Root"), 0)
frames.move(frames.find("表情"), 1)
logger.debug(f"Display item frames initialized with {len(frames)} frames")
@staticmethod
def get_empty_display_size(root_object: bpy.types.Object) -> float:
return getattr(root_object, Props.empty_display_size)
@@ -541,19 +568,28 @@ class MigrationFnModel:
"""Migration Functions for old MMD models broken by bugs or issues"""
@classmethod
def update_mmd_ik_loop_factor(cls):
def update_mmd_ik_loop_factor(cls) -> None:
logger.info("Updating MMD IK loop factor for all armatures")
updated_count = 0
for armature_object in bpy.data.objects:
if armature_object.type != "ARMATURE":
continue
if "mmd_ik_loop_factor" not in armature_object:
return
continue
FnModel.find_root_object(armature_object).mmd_root.ik_loop_factor = max(armature_object["mmd_ik_loop_factor"], 1)
root_object = FnModel.find_root_object(armature_object)
if root_object:
root_object.mmd_root.ik_loop_factor = max(armature_object["mmd_ik_loop_factor"], 1)
del armature_object["mmd_ik_loop_factor"]
updated_count += 1
logger.info(f"Updated IK loop factor for {updated_count} armatures")
@staticmethod
def update_avatar_toolkit_version():
def update_avatar_toolkit_version() -> None:
logger.info("Updating Avatar Toolkit version for all MMD root objects")
updated_count = 0
for root_object in bpy.data.objects:
if root_object.type != "EMPTY":
continue
@@ -565,10 +601,13 @@ class MigrationFnModel:
continue
root_object["avatar_toolkit_version"] = "0.2.1"
updated_count += 1
logger.info(f"Updated Avatar Toolkit version for {updated_count} root objects")
class Model:
def __init__(self, root_obj):
def __init__(self, root_obj: bpy.types.Object) -> None:
if root_obj is None:
raise ValueError("must be MMD ROOT type object")
if root_obj.mmd_type != "ROOT":
@@ -578,13 +617,15 @@ class Model:
self.__rigid_grp: Optional[bpy.types.Object] = None
self.__joint_grp: Optional[bpy.types.Object] = None
self.__temporary_grp: Optional[bpy.types.Object] = None
logger.debug(f"Model initialized with root object: {self.__root.name}")
@staticmethod
def create(name: str, name_e: str = "", scale: float = 1, obj_name: Optional[str] = None, armature_object: Optional[bpy.types.Object] = None, add_root_bone: bool = False):
def create(name: str, name_e: str = "", scale: float = 1, obj_name: Optional[str] = None, armature_object: Optional[bpy.types.Object] = None, add_root_bone: bool = False) -> 'Model':
if obj_name is None:
obj_name = name
context = FnContext.ensure_context()
logger.info(f"Creating new MMD model: {name}")
root: bpy.types.Object = bpy.data.objects.new(name=obj_name, object_data=None)
root.mmd_type = "ROOT"
@@ -595,6 +636,7 @@ class Model:
FnContext.link_object(context, root)
if armature_object:
logger.debug(f"Using existing armature: {armature_object.name}")
m = armature_object.matrix_world
armature_object.parent_type = "OBJECT"
armature_object.parent = root
@@ -602,6 +644,7 @@ class Model:
root.matrix_world = m
armature_object.matrix_local.identity()
else:
logger.debug("Creating new armature")
armature_object = bpy.data.objects.new(name=obj_name + "_arm", object_data=bpy.data.armatures.new(name=obj_name))
armature_object.parent = root
FnContext.link_object(context, armature_object)
@@ -614,6 +657,7 @@ class Model:
FnBone.setup_special_bone_collections(armature_object)
if add_root_bone:
logger.debug("Adding root bone")
bone_name = "全ての親"
bone_name_english = "Root"
@@ -637,34 +681,37 @@ class Model:
bone_collection.assign(data_bone)
FnContext.set_active_and_select_single_object(context, root)
logger.info(f"Model created successfully: {name}")
return Model(root)
@staticmethod
def findRoot(obj: bpy.types.Object) -> Optional[bpy.types.Object]:
return FnModel.find_root_object(obj)
def initialDisplayFrames(self, reset=True):
def initialDisplayFrames(self, reset: bool = True) -> None:
FnModel.initalize_display_item_frames(self.__root, reset=reset)
@property
def morph_slider(self):
def morph_slider(self) -> Any:
return FnMorph.get_morph_slider(self)
def loadMorphs(self):
def loadMorphs(self) -> None:
logger.info(f"Loading morphs for model: {self.__root.name}")
FnMorph.load_morphs(self)
def create_ik_constraint(self, bone, ik_target):
def create_ik_constraint(self, bone: bpy.types.PoseBone, ik_target: bpy.types.PoseBone) -> bpy.types.KinematicConstraint:
"""create IK constraint
Args:
bone: A pose bone to add a IK constraint
id_target: A pose bone for IK target
ik_target: A pose bone for IK target
Returns:
The bpy.types.KinematicConstraint object created. It is set target
and subtarget options.
"""
logger.debug(f"Creating IK constraint on {bone.name} targeting {ik_target.name}")
ik_target_name = ik_target.name
ik_const = bone.constraints.new("IK")
ik_const.target = self.__arm
@@ -693,6 +740,7 @@ class Model:
if self.__rigid_grp is None:
self.__rigid_grp = FnModel.find_rigid_group_object(self.__root)
if self.__rigid_grp is None:
logger.debug(f"Creating rigid group object for {self.__root.name}")
rigids = bpy.data.objects.new(name="rigidbodies", object_data=None)
FnContext.link_object(FnContext.ensure_context(), rigids)
rigids.mmd_type = "RIGID_GRP_OBJ"
@@ -710,6 +758,7 @@ class Model:
if self.__joint_grp is None:
self.__joint_grp = FnModel.find_joint_group_object(self.__root)
if self.__joint_grp is None:
logger.debug(f"Creating joint group object for {self.__root.name}")
joints = bpy.data.objects.new(name="joints", object_data=None)
FnContext.link_object(FnContext.ensure_context(), joints)
joints.mmd_type = "JOINT_GRP_OBJ"
@@ -727,6 +776,7 @@ class Model:
if self.__temporary_grp is None:
self.__temporary_grp = FnModel.find_temporary_group_object(self.__root)
if self.__temporary_grp is None:
logger.debug(f"Creating temporary group object for {self.__root.name}")
temporarys = bpy.data.objects.new(name="temporary", object_data=None)
FnContext.link_object(FnContext.ensure_context(), temporarys)
temporarys.mmd_type = "TEMPORARY_GRP_OBJ"
@@ -740,7 +790,7 @@ class Model:
def meshes(self) -> Iterator[bpy.types.Object]:
return FnModel.iterate_mesh_objects(self.__root)
def attachMeshes(self, meshes: Iterator[bpy.types.Object], add_armature_modifier: bool = True):
def attachMeshes(self, meshes: Iterator[bpy.types.Object], add_armature_modifier: bool = True) -> None:
FnModel.attach_mesh_objects(self.rootObject(), meshes, add_armature_modifier)
def firstMesh(self) -> Optional[bpy.types.Object]:
@@ -748,7 +798,7 @@ class Model:
return i
return None
def findMesh(self, mesh_name) -> Optional[bpy.types.Object]:
def findMesh(self, mesh_name: str) -> Optional[bpy.types.Object]:
"""
Helper method to find a mesh by name
"""
@@ -787,25 +837,26 @@ class Model:
def joints(self) -> Iterator[bpy.types.Object]:
return FnModel.iterate_joint_objects(self.__root)
def temporaryObjects(self, rigid_track_only=False) -> Iterator[bpy.types.Object]:
def temporaryObjects(self, rigid_track_only: bool = False) -> Iterator[bpy.types.Object]:
return FnModel.iterate_temporary_objects(self.__root, rigid_track_only)
def materials(self) -> Iterator[bpy.types.Material]:
"""
Helper method to list all materials in all meshes
"""
materials = {} # Use dict instead of set to guarantee preserve order
materials: Dict[bpy.types.Material, int] = {} # Use dict instead of set to guarantee preserve order
for mesh in self.meshes():
materials.update((slot.material, 0) for slot in mesh.material_slots if slot.material is not None)
return iter(materials.keys())
def renameBone(self, old_bone_name, new_bone_name):
def renameBone(self, old_bone_name: str, new_bone_name: str) -> None:
if old_bone_name == new_bone_name:
return
logger.info(f"Renaming bone: {old_bone_name} -> {new_bone_name}")
armature = self.armature()
bone = armature.pose.bones[old_bone_name]
bone.name = new_bone_name
new_bone_name = bone.name
new_bone_name = bone.name # Get the actual name (might be adjusted by Blender)
mmd_root = self.rootObject().mmd_root
for frame in mmd_root.display_item_frames:
@@ -816,28 +867,31 @@ class Model:
if old_bone_name in mesh.vertex_groups:
mesh.vertex_groups[old_bone_name].name = new_bone_name
def build(self, non_collision_distance_scale=1.5, collision_margin=1e-06):
def build(self, non_collision_distance_scale: float = 1.5, collision_margin: float = 1e-06) -> None:
logger.info(f"Building physics rig for {self.__root.name}")
rigidbody_world_enabled = rigid_body.setRigidBodyWorldEnabled(False)
if self.__root.mmd_root.is_built:
logger.info("Model is already built, cleaning first")
self.clean()
self.__root.mmd_root.is_built = True
logging.info("****************************************")
logging.info(" Build rig")
logging.info("****************************************")
logger.info("****************************************")
logger.info(" Build rig")
logger.info("****************************************")
start_time = time.time()
self.__preBuild()
self.disconnectPhysicsBones()
self.buildRigids(non_collision_distance_scale, collision_margin)
self.buildJoints()
self.__postBuild()
logging.info(" Finished building in %f seconds.", time.time() - start_time)
logger.info(" Finished building in %f seconds.", time.time() - start_time)
rigid_body.setRigidBodyWorldEnabled(rigidbody_world_enabled)
def clean(self):
def clean(self) -> None:
logger.info(f"Cleaning physics rig for {self.__root.name}")
rigidbody_world_enabled = rigid_body.setRigidBodyWorldEnabled(False)
logging.info("****************************************")
logging.info(" Clean rig")
logging.info("****************************************")
logger.info("****************************************")
logger.info(" Clean rig")
logger.info("****************************************")
start_time = time.time()
pose_bones = []
@@ -848,13 +902,14 @@ class Model:
if "mmd_tools_rigid_track" in i.constraints:
const = i.constraints["mmd_tools_rigid_track"]
i.constraints.remove(const)
logger.debug(f"Removed rigid track constraint from {i.name}")
rigid_track_counts = 0
for i in self.rigidBodies():
rigid_type = int(i.mmd_rigid.type)
if "mmd_tools_rigid_parent" not in i.constraints:
rigid_track_counts += 1
logging.info('%3d# Create a "CHILD_OF" constraint for %s', rigid_track_counts, i.name)
logger.info('%3d# Create a "CHILD_OF" constraint for %s', rigid_track_counts, i.name)
i.mmd_rigid.bone = i.mmd_rigid.bone
relation = i.constraints["mmd_tools_rigid_parent"]
relation.mute = True
@@ -884,35 +939,39 @@ class Model:
mmd_root = self.rootObject().mmd_root
if mmd_root.show_temporary_objects:
mmd_root.show_temporary_objects = False
logging.info(" Finished cleaning in %f seconds.", time.time() - start_time)
logger.info(" Finished cleaning in %f seconds.", time.time() - start_time)
mmd_root.is_built = False
rigid_body.setRigidBodyWorldEnabled(rigidbody_world_enabled)
def __removeTemporaryObjects(self):
def __removeTemporaryObjects(self) -> None:
logger.debug("Removing temporary objects")
with bpy.context.temp_override(selected_objects=tuple(self.temporaryObjects()), active_object=self.rootObject()):
bpy.ops.object.delete()
def __restoreTransforms(self, obj):
def __restoreTransforms(self, obj: bpy.types.Object) -> None:
for attr in ("location", "rotation_euler"):
attr_name = "__backup_%s__" % attr
val = obj.get(attr_name, None)
if val is not None:
setattr(obj, attr, val)
del obj[attr_name]
logger.debug(f"Restored {attr} for {obj.name}")
def __backupTransforms(self, obj):
def __backupTransforms(self, obj: bpy.types.Object) -> None:
for attr in ("location", "rotation_euler"):
attr_name = "__backup_%s__" % attr
if attr_name in obj: # should not happen in normal build/clean cycle
continue
obj[attr_name] = getattr(obj, attr, None)
logger.debug(f"Backed up {attr} for {obj.name}")
def __preBuild(self):
self.__fake_parent_map = {}
self.__rigid_body_matrix_map = {}
self.__empty_parent_map = {}
def __preBuild(self) -> None:
logger.debug("Pre-build preparation")
self.__fake_parent_map: Dict[bpy.types.Object, List[bpy.types.Object]] = {}
self.__rigid_body_matrix_map: Dict[bpy.types.Object, Any] = {}
self.__empty_parent_map: Dict[bpy.types.Object, bpy.types.Object] = {}
no_parents = []
no_parents: List[bpy.types.Object] = []
for i in self.rigidBodies():
self.__backupTransforms(i)
# mute relation
@@ -932,7 +991,7 @@ class Model:
# update changes of armature constraints
bpy.context.scene.frame_set(bpy.context.scene.frame_current)
parented = []
parented: List[bpy.types.Object] = []
for i in self.joints():
self.__backupTransforms(i)
rbc = i.rigid_body_constraint
@@ -950,7 +1009,8 @@ class Model:
# assert(len(no_parents) == len(parented))
def __postBuild(self):
def __postBuild(self) -> None:
logger.debug("Post-build finalization")
self.__fake_parent_map = None
self.__rigid_body_matrix_map = None
@@ -962,6 +1022,7 @@ class Model:
matrix_world = empty.matrix_world
empty.parent = rigid_obj
empty.matrix_world = matrix_world
logger.debug(f"Parented empty {empty.name} to rigid object {rigid_obj.name}")
self.__empty_parent_map = None
arm = self.armature()
@@ -970,11 +1031,13 @@ class Model:
c = p_bone.constraints.get("mmd_tools_rigid_track", None)
if c:
c.mute = False
logger.debug(f"Enabled rigid track constraint for {p_bone.name}")
def updateRigid(self, rigid_obj: bpy.types.Object, collision_margin: float):
def updateRigid(self, rigid_obj: bpy.types.Object, collision_margin: float) -> None:
assert rigid_obj.mmd_type == "RIGID_BODY"
rb = rigid_obj.rigid_body
if rb is None:
logger.warning(f"No rigid body for {rigid_obj.name}")
return
rigid = rigid_obj.mmd_rigid
@@ -1018,7 +1081,7 @@ class Model:
fake_children = self.__fake_parent_map.get(rigid_obj, None)
if fake_children:
for fake_child in fake_children:
logging.debug(" - fake_child: %s", fake_child.name)
logger.debug(" - fake_child: %s", fake_child.name)
t, r, s = (m @ fake_child.matrix_local).decompose()
fake_child.location = t
fake_child.rotation_euler = r.to_euler(fake_child.rotation_mode)
@@ -1032,7 +1095,7 @@ class Model:
fake_children = self.__fake_parent_map.get(rigid_obj, None)
if fake_children:
for fake_child in fake_children:
logging.debug(" - fake_child: %s", fake_child.name)
logger.debug(" - fake_child: %s", fake_child.name)
t, r, s = (m @ fake_child.matrix_local).decompose()
fake_child.location = t
fake_child.rotation_euler = r.to_euler(fake_child.rotation_mode)
@@ -1062,7 +1125,7 @@ class Model:
ori_rigid_obj = self.__empty_parent_map[empty]
ori_rb = ori_rigid_obj.rigid_body
if ori_rb and rb.mass > ori_rb.mass:
logging.debug(" * Bone (%s): change target from [%s] to [%s]", target_bone.name, ori_rigid_obj.name, rigid_obj.name)
logger.debug(" * Bone (%s): change target from [%s] to [%s]", target_bone.name, ori_rigid_obj.name, rigid_obj.name)
# re-parenting
rigid_obj.mmd_rigid.bone = bone_name
rigid_obj.constraints.remove(relation)
@@ -1070,21 +1133,22 @@ class Model:
# revert change
ori_rigid_obj.mmd_rigid.bone = bone_name
else:
logging.debug(" * Bone (%s): track target [%s]", target_bone.name, ori_rigid_obj.name)
logger.debug(" * Bone (%s): track target [%s]", target_bone.name, ori_rigid_obj.name)
rb.collision_shape = rigid.shape
logger.debug(f"Updated rigid body {rigid_obj.name} with type {rigid_type}")
def __getRigidRange(self, obj):
def __getRigidRange(self, obj: bpy.types.Object) -> float:
return (Vector(obj.bound_box[0]) - Vector(obj.bound_box[6])).length
def __createNonCollisionConstraint(self, nonCollisionJointTable):
def __createNonCollisionConstraint(self, nonCollisionJointTable: List[Tuple[bpy.types.Object, bpy.types.Object]]) -> None:
total_len = len(nonCollisionJointTable)
if total_len < 1:
return
start_time = time.time()
logging.debug("-" * 60)
logging.debug(" creating ncc, counts: %d", total_len)
logger.debug("-" * 60)
logger.debug(" creating ncc, counts: %d", total_len)
ncc_obj = bpyutils.createObject(name="ncc", object_data=None)
ncc_obj.location = [0, 0, 0]
@@ -1099,26 +1163,26 @@ class Model:
rb.disable_collisions = True
ncc_objs = bpyutils.duplicateObject(ncc_obj, total_len)
logging.debug(" created %d ncc.", len(ncc_objs))
logger.debug(" created %d ncc.", len(ncc_objs))
for ncc_obj, pair in zip(ncc_objs, nonCollisionJointTable):
rbc = ncc_obj.rigid_body_constraint
rbc.object1, rbc.object2 = pair
ncc_obj.hide_set(True)
ncc_obj.hide_select = True
logging.debug(" finish in %f seconds.", time.time() - start_time)
logging.debug("-" * 60)
logger.debug(" finish in %f seconds.", time.time() - start_time)
logger.debug("-" * 60)
def buildRigids(self, non_collision_distance_scale, collision_margin):
logging.debug("--------------------------------")
logging.debug(" Build riggings of rigid bodies")
logging.debug("--------------------------------")
def buildRigids(self, non_collision_distance_scale: float, collision_margin: float) -> List[bpy.types.Object]:
logger.debug("--------------------------------")
logger.debug(" Build riggings of rigid bodies")
logger.debug("--------------------------------")
rigid_objects = list(self.rigidBodies())
rigid_object_groups = [[] for i in range(16)]
rigid_object_groups: List[List[bpy.types.Object]] = [[] for i in range(16)]
for i in rigid_objects:
rigid_object_groups[i.mmd_rigid.collision_group_number].append(i)
jointMap = {}
jointMap: Dict[frozenset, bpy.types.Object] = {}
for joint in self.joints():
rbc = joint.rigid_body_constraint
if rbc is None:
@@ -1126,10 +1190,10 @@ class Model:
rbc.disable_collisions = False
jointMap[frozenset((rbc.object1, rbc.object2))] = joint
logging.info("Creating non collision constraints")
logger.info("Creating non collision constraints")
# create non collision constraints
nonCollisionJointTable = []
non_collision_pairs = set()
nonCollisionJointTable: List[Tuple[bpy.types.Object, bpy.types.Object]] = []
non_collision_pairs: Set[frozenset] = set()
rigid_object_cnt = len(rigid_objects)
for obj_a in rigid_objects:
for n, ignore in enumerate(obj_a.mmd_rigid.collision_group_mask):
@@ -1150,12 +1214,13 @@ class Model:
nonCollisionJointTable.append((obj_a, obj_b))
non_collision_pairs.add(pair)
for cnt, i in enumerate(rigid_objects):
logging.info("%3d/%3d: Updating rigid body %s", cnt + 1, rigid_object_cnt, i.name)
logger.info("%3d/%3d: Updating rigid body %s", cnt + 1, rigid_object_cnt, i.name)
self.updateRigid(i, collision_margin)
self.__createNonCollisionConstraint(nonCollisionJointTable)
return rigid_objects
def buildJoints(self):
def buildJoints(self) -> None:
logger.info("Building joints")
for i in self.joints():
rbc = i.rigid_body_constraint
if rbc is None:
@@ -1168,8 +1233,9 @@ class Model:
t, r, s = (m @ i.matrix_local).decompose()
i.location = t
i.rotation_euler = r.to_euler(i.rotation_mode)
logger.debug(f"Built joint: {i.name}")
def __editPhysicsBones(self, editor: Callable[[bpy.types.EditBone], None], target_modes: Set[str]):
def __editPhysicsBones(self, editor: Callable[[bpy.types.EditBone], None], target_modes: Set[str]) -> None:
armature_object = self.armature()
armature: bpy.types.Armature
@@ -1177,7 +1243,7 @@ class Model:
edit_bones = armature.edit_bones
rigid_body_object: bpy.types.Object
for rigid_body_object in self.rigidBodies():
mmd_rigid: MMDRigidBody = rigid_body_object.mmd_rigid
mmd_rigid: 'MMDRigidBody' = rigid_body_object.mmd_rigid
if mmd_rigid.type not in target_modes:
continue
@@ -1188,21 +1254,25 @@ class Model:
editor(edit_bone)
def disconnectPhysicsBones(self):
def editor(edit_bone: bpy.types.EditBone):
def disconnectPhysicsBones(self) -> None:
logger.info("Disconnecting physics bones")
def editor(edit_bone: bpy.types.EditBone) -> None:
rna_prop_ui.rna_idprop_ui_create(edit_bone, "mmd_bone_use_connect", default=edit_bone.use_connect)
edit_bone.use_connect = False
logger.debug(f"Disconnected bone: {edit_bone.name}")
self.__editPhysicsBones(editor, {str(MODE_DYNAMIC)})
def connectPhysicsBones(self):
def editor(edit_bone: bpy.types.EditBone):
def connectPhysicsBones(self) -> None:
logger.info("Connecting physics bones")
def editor(edit_bone: bpy.types.EditBone) -> None:
mmd_bone_use_connect_str: Optional[str] = edit_bone.get("mmd_bone_use_connect")
if mmd_bone_use_connect_str is None:
return
if not edit_bone.use_connect: # wasn't it overwritten?
edit_bone.use_connect = bool(mmd_bone_use_connect_str)
logger.debug(f"Connected bone: {edit_bone.name}")
del edit_bone["mmd_bone_use_connect"]
self.__editPhysicsBones(editor, {str(MODE_STATIC), str(MODE_DYNAMIC), str(MODE_DYNAMIC_BONE)})
+61 -59
View File
@@ -5,33 +5,35 @@
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import logging
import re
from typing import TYPE_CHECKING, Tuple, cast
from typing import TYPE_CHECKING, Tuple, cast, List, Dict, Optional, Set, Any, Union, Iterator
import bpy
import numpy as np
from bpy.types import Object, ShapeKey, Material, Mesh, Armature, PoseBone, Constraint
from .. import bpyutils, utils
from ..bpyutils import FnContext, FnObject, TransformConstraintOp
from ....core.logging_setup import logger
if TYPE_CHECKING:
from .model import Model
class FnMorph:
def __init__(self, morph, model: "Model"):
def __init__(self, morph: Any, model: "Model"):
self.__morph = morph
self.__rig = model
@classmethod
def storeShapeKeyOrder(cls, obj, shape_key_names):
def storeShapeKeyOrder(cls, obj: Object, shape_key_names: List[str]) -> None:
if len(shape_key_names) < 1:
return
assert FnContext.get_active_object(FnContext.ensure_context()) == obj
if obj.data.shape_keys is None:
bpy.ops.object.shape_key_add()
def __move_to_bottom(key_blocks, name):
def __move_to_bottom(key_blocks: bpy.types.bpy_prop_collection, name: str) -> None:
obj.active_shape_key_index = key_blocks.find(name)
bpy.ops.object.shape_key_move(type="BOTTOM")
@@ -43,7 +45,7 @@ class FnMorph:
__move_to_bottom(key_blocks, name)
@classmethod
def fixShapeKeyOrder(cls, obj, shape_key_names):
def fixShapeKeyOrder(cls, obj: Object, shape_key_names: List[str]) -> None:
if len(shape_key_names) < 1:
return
assert FnContext.get_active_object(FnContext.ensure_context()) == obj
@@ -58,11 +60,11 @@ class FnMorph:
bpy.ops.object.shape_key_move(type="BOTTOM")
@staticmethod
def get_morph_slider(rig):
def get_morph_slider(rig: "Model") -> "_MorphSlider":
return _MorphSlider(rig)
@staticmethod
def category_guess(morph):
def category_guess(morph: Any) -> None:
name_lower = morph.name.lower()
if "mouth" in name_lower:
morph.category = "MOUTH"
@@ -73,7 +75,7 @@ class FnMorph:
morph.category = "EYE"
@classmethod
def load_morphs(cls, rig):
def load_morphs(cls, rig: "Model") -> None:
mmd_root = rig.rootObject().mmd_root
vertex_morphs = mmd_root.vertex_morphs
uv_morphs = mmd_root.uv_morphs
@@ -92,7 +94,7 @@ class FnMorph:
cls.category_guess(item)
@staticmethod
def remove_shape_key(mesh_object: bpy.types.Object, shape_key_name: str):
def remove_shape_key(mesh_object: Object, shape_key_name: str) -> None:
assert isinstance(mesh_object.data, bpy.types.Mesh)
shape_keys = mesh_object.data.shape_keys
@@ -104,7 +106,7 @@ class FnMorph:
FnObject.mesh_remove_shape_key(mesh_object, key_blocks[shape_key_name])
@staticmethod
def copy_shape_key(mesh_object: bpy.types.Object, src_name: str, dest_name: str):
def copy_shape_key(mesh_object: Object, src_name: str, dest_name: str) -> None:
assert isinstance(mesh_object.data, bpy.types.Mesh)
shape_keys = mesh_object.data.shape_keys
@@ -126,13 +128,13 @@ class FnMorph:
mesh_object.active_shape_key_index = key_blocks.find(dest_name)
@staticmethod
def get_uv_morph_vertex_groups(obj, morph_name=None, offset_axes="XYZW"):
def get_uv_morph_vertex_groups(obj: Object, morph_name: Optional[str] = None, offset_axes: str = "XYZW") -> Iterator[Tuple[bpy.types.VertexGroup, str, str]]:
pattern = "UV_%s[+-][%s]$" % (morph_name or ".{1,}", offset_axes or "XYZW")
# yield (vertex_group, morph_name, axis),...
return ((g, g.name[3:-2], g.name[-2:]) for g in obj.vertex_groups if re.match(pattern, g.name))
@staticmethod
def copy_uv_morph_vertex_groups(obj, src_name, dest_name):
def copy_uv_morph_vertex_groups(obj: Object, src_name: str, dest_name: str) -> None:
for vg, n, x in FnMorph.get_uv_morph_vertex_groups(obj, dest_name):
obj.vertex_groups.remove(vg)
@@ -143,12 +145,12 @@ class FnMorph:
obj.vertex_groups.active.name = vg_name.replace(src_name, dest_name)
@staticmethod
def overwrite_bone_morphs_from_action_pose(armature_object):
def overwrite_bone_morphs_from_action_pose(armature_object: Object) -> None:
armature = armature_object.id_data
# Use animation_data and action instead of action_pose
if armature.animation_data is None or armature.animation_data.action is None:
logging.warning('[WARNING] armature "%s" has no animation data or action', armature_object.name)
logger.warning('Armature "%s" has no animation data or action', armature_object.name)
return
action = armature.animation_data.action
@@ -187,9 +189,9 @@ class FnMorph:
utils.selectAObject(root)
@staticmethod
def clean_uv_morph_vertex_groups(obj):
def clean_uv_morph_vertex_groups(obj: Object) -> None:
# remove empty vertex groups of uv morphs
vg_indices = {g.index for g, n, x in FnMorph.get_uv_morph_vertex_groups(obj)}
vg_indices: Set[int] = {g.index for g, n, x in FnMorph.get_uv_morph_vertex_groups(obj)}
vertex_groups = obj.vertex_groups
for v in obj.data.vertices:
for x in v.groups:
@@ -203,8 +205,8 @@ class FnMorph:
vertex_groups.remove(vg)
@staticmethod
def get_uv_morph_offset_map(obj, morph):
offset_map = {} # offset_map[vertex_index] = offset_xyzw
def get_uv_morph_offset_map(obj: Object, morph: Any) -> Dict[int, List[float]]:
offset_map: Dict[int, List[float]] = {} # offset_map[vertex_index] = offset_xyzw
if morph.data_type == "VERTEX_GROUP":
scale = morph.vertex_group_scale
axis_map = {g.index: x for g, n, x in FnMorph.get_uv_morph_vertex_groups(obj, morph.name)}
@@ -225,7 +227,7 @@ class FnMorph:
return offset_map
@staticmethod
def store_uv_morph_data(obj, morph, offsets=None, offset_axes="XYZW"):
def store_uv_morph_data(obj: Object, morph: Any, offsets: Optional[List[Any]] = None, offset_axes: str = "XYZW") -> None:
vertex_groups = obj.vertex_groups
morph_name = getattr(morph, "name", None)
if offset_axes:
@@ -250,7 +252,7 @@ class FnMorph:
vg = vertex_groups.get(vg_name, None) or vertex_groups.new(name=vg_name)
vg.add(index=[idx], weight=abs(val) / scale, type="REPLACE")
def update_mat_related_mesh(self, new_mesh=None):
def update_mat_related_mesh(self, new_mesh: Optional[Object] = None) -> None:
for offset in self.__morph.data:
# Use the new_mesh if provided
meshObj = new_mesh
@@ -270,11 +272,11 @@ class FnMorph:
offset.related_mesh = meshObj.data.name
@staticmethod
def clean_duplicated_material_morphs(mmd_root_object: bpy.types.Object):
def clean_duplicated_material_morphs(mmd_root_object: Object) -> None:
"""Clean duplicated material_morphs and data from mmd_root_object.mmd_root.material_morphs[].data[]"""
mmd_root = mmd_root_object.mmd_root
def morph_data_equals(l, r) -> bool:
def morph_data_equals(l: Any, r: Any) -> bool:
return (
l.related_mesh_data == r.related_mesh_data
and l.offset_type == r.offset_type
@@ -290,7 +292,7 @@ class FnMorph:
and all(a == b for a, b in zip(l.toon_texture_factor, r.toon_texture_factor))
)
def morph_equals(l, r) -> bool:
def morph_equals(l: Any, r: Any) -> bool:
return len(l.data) == len(r.data) and all(morph_data_equals(a, b) for a, b in zip(l.data, r.data))
# Remove duplicated mmd_root.material_morphs.data[]
@@ -325,7 +327,7 @@ class _MorphSlider:
def __init__(self, model: "Model"):
self.__rig = model
def placeholder(self, create=False, binded=False):
def placeholder(self, create: bool = False, binded: bool = False) -> Optional[Object]:
rig = self.__rig
root = rig.rootObject()
obj = next((x for x in root.children if x.mmd_type == "PLACEHOLDER" and x.type == "MESH"), None)
@@ -343,11 +345,11 @@ class _MorphSlider:
return obj
@property
def dummy_armature(self):
def dummy_armature(self) -> Optional[Object]:
obj = self.placeholder()
return self.__dummy_armature(obj) if obj else None
def __dummy_armature(self, obj, create=False):
def __dummy_armature(self, obj: Object, create: bool = False) -> Optional[Object]:
arm = next((x for x in obj.children if x.mmd_type == "PLACEHOLDER" and x.type == "ARMATURE"), None)
if create and arm is None:
arm = bpy.data.objects.new(name=".dummy_armature", object_data=bpy.data.armatures.new(name=".dummy_armature"))
@@ -360,7 +362,7 @@ class _MorphSlider:
FnBone.setup_special_bone_collections(arm)
return arm
def get(self, morph_name):
def get(self, morph_name: str) -> Optional[ShapeKey]:
obj = self.placeholder()
if obj is None:
return None
@@ -369,13 +371,13 @@ class _MorphSlider:
return None
return key_blocks.get(morph_name, None)
def create(self):
def create(self) -> Object:
self.__rig.loadMorphs()
obj = self.placeholder(create=True)
self.__load(obj, self.__rig.rootObject().mmd_root)
return obj
def __load(self, obj, mmd_root):
def __load(self, obj: Object, mmd_root: Any) -> None:
attr_list = ("group", "vertex", "bone", "uv", "material")
morph_sliders = obj.data.shape_keys.key_blocks
for m in (x for attr in attr_list for x in getattr(mmd_root, attr + "_morphs", ())):
@@ -386,7 +388,7 @@ class _MorphSlider:
obj.shape_key_add(name=name, from_mix=False)
@staticmethod
def __driver_variables(id_data, path, index=-1):
def __driver_variables(id_data: Any, path: str, index: int = -1) -> Tuple[Any, Any]:
d = id_data.driver_add(path, index)
variables = d.driver.variables
for x in variables:
@@ -394,7 +396,7 @@ class _MorphSlider:
return d.driver, variables
@staticmethod
def __add_single_prop(variables, id_obj, data_path, prefix):
def __add_single_prop(variables: Any, id_obj: Object, data_path: str, prefix: str) -> Any:
var = variables.new()
var.name = f"{prefix}{len(variables)}"
var.type = "SINGLE_PROP"
@@ -405,7 +407,7 @@ class _MorphSlider:
return var
@staticmethod
def __shape_key_driver_check(key_block, resolve_path=False):
def __shape_key_driver_check(key_block: ShapeKey, resolve_path: bool = False) -> bool:
if resolve_path:
try:
key_block.id_data.path_resolve(key_block.path_from_id())
@@ -419,7 +421,7 @@ class _MorphSlider:
d = next((i for i in key_block.id_data.animation_data.drivers if i.data_path == data_path), None)
return not d or d.driver.expression == "".join(("*w", "+g", "v")[-1 if i < 1 else i % 2] + str(i + 1) for i in range(len(d.driver.variables)))
def __cleanup(self, names_in_use=None):
def __cleanup(self, names_in_use: Optional[Dict[str, Any]] = None) -> None:
from math import ceil, floor
names_in_use = names_in_use or {}
@@ -427,7 +429,7 @@ class _MorphSlider:
morph_sliders = self.placeholder()
morph_sliders = morph_sliders.data.shape_keys.key_blocks if morph_sliders else {}
for mesh_object in rig.meshes():
for kb in getattr(mesh_object.data.shape_keys, "key_blocks", cast(Tuple[bpy.types.ShapeKey], ())):
for kb in getattr(mesh_object.data.shape_keys, "key_blocks", cast(Tuple[ShapeKey], ())):
if kb.name in names_in_use:
continue
@@ -465,7 +467,7 @@ class _MorphSlider:
c.driver_remove(attr)
b.constraints.remove(c)
def unbind(self):
def unbind(self) -> None:
mmd_root = self.__rig.rootObject().mmd_root
# after unbind, the weird lag problem will disappear.
@@ -488,7 +490,7 @@ class _MorphSlider:
b.driver_remove("rotation_quaternion")
self.__cleanup()
def bind(self):
def bind(self) -> None:
rig = self.__rig
root = rig.rootObject()
armObj = rig.armature()
@@ -502,10 +504,10 @@ class _MorphSlider:
morph_sliders = obj.data.shape_keys.key_blocks
# data gathering
group_map = {}
group_map: Dict[Tuple[str, str], List[List[Any]]] = {}
shape_key_map = {}
uv_morph_map = {}
shape_key_map: Dict[str, List[Tuple[ShapeKey, str, List[Any]]]] = {}
uv_morph_map: Dict[str, List[Tuple[str, str, str, List[Any]]]] = {}
for mesh_object in rig.meshes():
mesh_object.show_only_shape_key = False
key_blocks = getattr(mesh_object.data.shape_keys, "key_blocks", ())
@@ -526,7 +528,7 @@ class _MorphSlider:
kb_bind.slider_max = 10
data_path = 'data.shape_keys.key_blocks["%s"].value' % kb_name.replace('"', '\\"')
groups = []
groups: List[Any] = []
shape_key_map.setdefault(name_bind, []).append((kb_bind, data_path, groups))
group_map.setdefault(("vertex_morphs", kb_name), []).append(groups)
@@ -542,7 +544,7 @@ class _MorphSlider:
continue
name_bind = "mmd_bind%s" % hash(vg.name)
uv_morph_map.setdefault(name_bind, ())
uv_morph_map.setdefault(name_bind, [])
mod = mesh_object.modifiers.get(name_bind, None) or mesh_object.modifiers.new(name=name_bind, type="UV_WARP")
mod.show_expanded = False
mod.vertex_group = vg.name
@@ -555,13 +557,13 @@ class _MorphSlider:
else:
mod.bone_from, mod.bone_to = name_bind, "mmd_bind_ctrl_base"
bone_offset_map = {}
bone_offset_map: Dict[str, Tuple[str, Any, str, str, List[Any]]] = {}
with bpyutils.edit_object(arm) as data:
from .bone import FnBone
edit_bones = data.edit_bones
def __get_bone(name, parent):
def __get_bone(name: str, parent: Optional[bpy.types.EditBone]) -> bpy.types.EditBone:
b = edit_bones.get(name, None) or edit_bones.new(name=name)
b.head = (0, 0, 0)
b.tail = (0, 0, 1)
@@ -578,7 +580,7 @@ class _MorphSlider:
continue
d.name = name_bind = f"mmd_bind{hash(d)}"
b = FnBone.set_edit_bone_to_shadow(__get_bone(name_bind, None))
groups = []
groups: List[Any] = []
bone_offset_map[name_bind] = (m.name, d, b.name, data_path, groups)
group_map.setdefault(("bone_morphs", m.name), []).append(groups)
@@ -589,21 +591,21 @@ class _MorphSlider:
scale_path = f'mmd_root.uv_morphs["{morph_name}"].vertex_group_scale'
name_bind = f"mmd_bind{hash(m.name)}"
b = FnBone.set_edit_bone_to_dummy(__get_bone(name_bind, ctrl_base))
groups = []
groups: List[Any] = []
uv_morph_map.setdefault(name_bind, []).append((b.name, data_path, scale_path, groups))
group_map.setdefault(("uv_morphs", m.name), []).append(groups)
used_bone_names = bone_offset_map.keys() | uv_morph_map.keys()
used_bone_names: Set[str] = set(bone_offset_map.keys()) | set(uv_morph_map.keys())
used_bone_names.add(ctrl_base.name)
for b in edit_bones: # cleanup
if b.name.startswith("mmd_bind") and b.name not in used_bone_names:
edit_bones.remove(b)
material_offset_map = {}
material_offset_map: Dict[str, Any] = {}
for m in mmd_root.material_morphs:
morph_name = m.name.replace('"', '\\"')
data_path = f'data.shape_keys.key_blocks["{morph_name}"].value'
groups = []
groups: List[Any] = []
group_map.setdefault(("material_morphs", m.name), []).append(groups)
material_offset_map.setdefault("group_dict", {})[m.name] = (data_path, groups)
for d in m.data:
@@ -614,7 +616,7 @@ class _MorphSlider:
for m in mmd_root.group_morphs:
if len(m.data) != len(set(m.data.keys())):
logging.warning(' * Found duplicated morph data in Group Morph "%s"', m.name)
logger.warning('Found duplicated morph data in Group Morph "%s"', m.name)
morph_name = m.name.replace('"', '\\"')
morph_path = f'data.shape_keys.key_blocks["{morph_name}"].value'
for d in m.data:
@@ -625,7 +627,7 @@ class _MorphSlider:
self.__cleanup(shape_key_map.keys() | bone_offset_map.keys() | uv_morph_map.keys())
def __config_groups(variables, expression, groups):
def __config_groups(variables: Any, expression: str, groups: List[Any]) -> str:
for g_name, morph_path, factor_path in groups:
var = self.__add_single_prop(variables, obj, morph_path, "g")
fvar = self.__add_single_prop(variables, root, factor_path, "w")
@@ -644,7 +646,7 @@ class _MorphSlider:
kb_bind.mute = False
# bone morphs
def __config_bone_morph(constraints, map_type, attributes, val, val_str):
def __config_bone_morph(constraints: bpy.types.ArmatureConstraints, map_type: str, attributes: Set[str], val: float, val_str: str) -> None:
c_name = f"mmd_bind{hash(data)}.{map_type[:3]}"
c = TransformConstraintOp.create(constraints, c_name, map_type)
TransformConstraintOp.update_min_max(c, val, None)
@@ -692,7 +694,7 @@ class _MorphSlider:
group_dict = material_offset_map.get("group_dict", {})
def __config_material_morph(mat, morph_list):
def __config_material_morph(mat: Material, morph_list: List[Tuple[str, Any, str]]) -> None:
nodes = _MaterialMorph.setup_morph_nodes(mat, tuple(x[1] for x in morph_list))
for (morph_name, data, name_bind), node in zip(morph_list, nodes):
node.label, node.name = morph_name, name_bind
@@ -704,7 +706,7 @@ class _MorphSlider:
for mat in (m for m in rig.materials() if m and m.use_nodes and not m.name.startswith("mmd_")):
mul_all, add_all = material_offset_map.get("#", ([], []))
if mat.name == "":
logging.warning("Oh no. The material name should never empty.")
logger.warning("Oh no. The material name should never be empty.")
mul_list, add_list = [], []
else:
mat_name = "#" + mat.name
@@ -720,7 +722,7 @@ class _MorphSlider:
class MigrationFnMorph:
@staticmethod
def update_mmd_morph():
def update_mmd_morph() -> None:
from .material import FnMaterial
for root in bpy.data.objects:
@@ -762,11 +764,11 @@ class MigrationFnMorph:
morph_data.related_mesh_data = bpy.data.meshes[related_mesh]
@staticmethod
def ensure_material_id_not_conflict():
mat_ids_set = set()
def ensure_material_id_not_conflict() -> None:
mat_ids_set: Set[int] = set()
# The reference library properties cannot be modified and bypassed in advance.
need_update_mat = []
need_update_mat: List[Material] = []
for mat in bpy.data.materials:
if mat.mmd_material.material_id < 0:
continue
@@ -781,7 +783,7 @@ class MigrationFnMorph:
mat_ids_set.add(mat.mmd_material.material_id)
@staticmethod
def compatible_with_old_version_mmd_tools():
def compatible_with_old_version_mmd_tools() -> None:
MigrationFnMorph.ensure_material_id_not_conflict()
for root in bpy.data.objects:
+35 -12
View File
@@ -5,12 +5,13 @@
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import List, Optional
from typing import List, Optional, Tuple, Union, Dict, Any, Set, cast
import bpy
from mathutils import Euler, Vector
from mathutils import Euler, Vector, Matrix
from ..bpyutils import FnContext, Props
from ....core.logging_setup import logger
SHAPE_SPHERE = 0
SHAPE_BOX = 1
@@ -21,25 +22,30 @@ MODE_DYNAMIC = 1
MODE_DYNAMIC_BONE = 2
def shapeType(collision_shape):
def shapeType(collision_shape: str) -> int:
"""Convert collision shape name to type index"""
return ("SPHERE", "BOX", "CAPSULE").index(collision_shape)
def collisionShape(shape_type):
def collisionShape(shape_type: int) -> str:
"""Convert shape type index to collision shape name"""
return ("SPHERE", "BOX", "CAPSULE")[shape_type]
def setRigidBodyWorldEnabled(enable):
def setRigidBodyWorldEnabled(enable: bool) -> bool:
"""Enable or disable the rigid body world and return previous state"""
if bpy.ops.rigidbody.world_add.poll():
logger.debug("Creating rigid body world")
bpy.ops.rigidbody.world_add()
rigidbody_world = bpy.context.scene.rigidbody_world
enabled = rigidbody_world.enabled
rigidbody_world.enabled = enable
logger.debug(f"Rigid body world enabled: {enable} (was: {enabled})")
return enabled
class RigidBodyMaterial:
COLORS = [
COLORS: List[int] = [
0x7FDDD4,
0xF0E68C,
0xEE82EE,
@@ -59,10 +65,12 @@ class RigidBodyMaterial:
]
@classmethod
def getMaterial(cls, number):
def getMaterial(cls, number: int) -> bpy.types.Material:
"""Get or create a material for rigid bodies with the specified number"""
number = int(number)
material_name = "mmd_tools_rigid_%d" % (number)
material_name = f"mmd_tools_rigid_{number}"
if material_name not in bpy.data.materials:
logger.debug(f"Creating rigid body material: {material_name}")
mat = bpy.data.materials.new(material_name)
color = cls.COLORS[number]
mat.diffuse_color[:3] = [((0xFF0000 & color) >> 16) / float(255), ((0x00FF00 & color) >> 8) / float(255), (0x0000FF & color) / float(255)]
@@ -89,9 +97,11 @@ class RigidBodyMaterial:
class FnRigidBody:
@staticmethod
def new_rigid_body_objects(context: bpy.types.Context, parent_object: bpy.types.Object, count: int) -> List[bpy.types.Object]:
"""Create multiple rigid body objects parented to the specified object"""
if count < 1:
return []
logger.debug(f"Creating {count} rigid body objects parented to {parent_object.name}")
obj = FnRigidBody.new_rigid_body_object(context, parent_object)
if count == 1:
@@ -101,6 +111,8 @@ class FnRigidBody:
@staticmethod
def new_rigid_body_object(context: bpy.types.Context, parent_object: bpy.types.Object) -> bpy.types.Object:
"""Create a new rigid body object parented to the specified object"""
logger.debug(f"Creating new rigid body object parented to {parent_object.name}")
obj = FnContext.new_and_link_object(context, name="Rigidbody", object_data=bpy.data.meshes.new(name="Rigidbody"))
obj.parent = parent_object
obj.mmd_type = "RIGID_BODY"
@@ -118,11 +130,11 @@ class FnRigidBody:
@staticmethod
def setup_rigid_body_object(
obj: bpy.types.Object,
shape_type: str,
shape_type: int,
location: Vector,
rotation: Euler,
size: Vector,
dynamics_type: str,
dynamics_type: int,
collision_group_number: Optional[int] = None,
collision_group_mask: Optional[List[bool]] = None,
name: Optional[str] = None,
@@ -134,6 +146,8 @@ class FnRigidBody:
linear_damping: Optional[float] = None,
bounce: Optional[float] = None,
) -> bpy.types.Object:
"""Set up a rigid body object with the specified parameters"""
logger.debug(f"Setting up rigid body object: {obj.name}")
obj.location = location
obj.rotation_euler = rotation
@@ -175,7 +189,8 @@ class FnRigidBody:
return obj
@staticmethod
def get_rigid_body_size(obj: bpy.types.Object):
def get_rigid_body_size(obj: bpy.types.Object) -> Tuple[float, float, float]:
"""Get the size of a rigid body object based on its shape type"""
assert obj.mmd_type == "RIGID_BODY"
x0, y0, z0 = obj.bound_box[0]
@@ -195,10 +210,14 @@ class FnRigidBody:
height = abs((z1 - z0) - diameter)
return (radius, height, 0.0)
else:
raise ValueError(f"Invalid shape type: {shape}")
error_msg = f"Invalid shape type: {shape}"
logger.error(error_msg)
raise ValueError(error_msg)
@staticmethod
def new_joint_object(context: bpy.types.Context, parent_object: bpy.types.Object, empty_display_size: float) -> bpy.types.Object:
"""Create a new joint object parented to the specified object"""
logger.debug(f"Creating new joint object parented to {parent_object.name}")
obj = FnContext.new_and_link_object(context, name="Joint", object_data=None)
obj.parent = parent_object
obj.mmd_type = "JOINT"
@@ -230,9 +249,11 @@ class FnRigidBody:
@staticmethod
def new_joint_objects(context: bpy.types.Context, parent_object: bpy.types.Object, count: int, empty_display_size: float) -> List[bpy.types.Object]:
"""Create multiple joint objects parented to the specified object"""
if count < 1:
return []
logger.debug(f"Creating {count} joint objects parented to {parent_object.name}")
obj = FnRigidBody.new_joint_object(context, parent_object, empty_display_size)
if count == 1:
@@ -256,6 +277,8 @@ class FnRigidBody:
name: str,
name_e: Optional[str] = None,
) -> bpy.types.Object:
"""Set up a joint object with the specified parameters"""
logger.debug(f"Setting up joint object: {obj.name} with name {name}")
obj.name = f"J.{name}"
obj.location = location
+55 -29
View File
@@ -7,14 +7,19 @@
import logging
import time
from typing import Dict, List, Tuple, Set, Optional, Any, Union, cast, TypeVar, Callable
import bpy
from mathutils import Matrix, Vector
import numpy as np
from mathutils import Matrix, Vector, Quaternion, Euler
from bpy.types import Object, PoseBone, Pose, ShapeKey, Modifier, VertexGroup
from ..bpyutils import FnObject
from ....core.logging_setup import logger
T = TypeVar('T')
def _hash(v):
def _hash(v: Union[Object, PoseBone, Pose]) -> int:
if isinstance(v, (bpy.types.Object, bpy.types.PoseBone)):
return hash(type(v).__name__ + v.name)
elif isinstance(v, bpy.types.Pose):
@@ -24,23 +29,24 @@ def _hash(v):
class FnSDEF:
g_verts = {} # global cache
g_shapekey_data = {}
g_bone_check = {}
__g_armature_check = {}
SHAPEKEY_NAME = "mmd_sdef_skinning"
MASK_NAME = "mmd_sdef_mask"
g_verts: Dict[int, Dict[Tuple[int, int], Tuple[PoseBone, PoseBone, List[Tuple[int, float, float, Vector, Vector, Vector]], List[int]]]] = {} # global cache
g_shapekey_data: Dict[int, Optional[np.ndarray]] = {}
g_bone_check: Dict[int, Dict[Union[Tuple[int, int], str], Union[Tuple[Matrix, Matrix], bool]]] = {}
__g_armature_check: Dict[int, Optional[int]] = {}
SHAPEKEY_NAME: str = "mmd_sdef_skinning"
MASK_NAME: str = "mmd_sdef_mask"
def __init__(self):
def __init__(self) -> None:
raise NotImplementedError("not allowed")
@classmethod
def __init_cache(cls, obj, shapekey):
def __init_cache(cls, obj: Object, shapekey: ShapeKey) -> bool:
key = _hash(obj)
obj = getattr(obj, "original", obj)
mod = obj.modifiers.get("mmd_bone_order_override")
key_armature = _hash(mod.object.pose) if mod and mod.type == "ARMATURE" and mod.object else None
if key not in cls.g_verts or cls.__g_armature_check.get(key) != key_armature:
logger.debug(f"Initializing SDEF cache for {obj.name}")
cls.g_verts[key] = cls.__find_vertices(obj)
cls.g_bone_check[key] = {}
cls.__g_armature_check[key] = key_armature
@@ -49,7 +55,7 @@ class FnSDEF:
return False
@classmethod
def __check_bone_update(cls, obj, bone0, bone1):
def __check_bone_update(cls, obj: Object, bone0: PoseBone, bone1: PoseBone) -> bool:
check = cls.g_bone_check[_hash(obj)]
key = (_hash(bone0), _hash(bone1))
if key not in check or (bone0.matrix, bone1.matrix) != check[key]:
@@ -58,17 +64,18 @@ class FnSDEF:
return False
@classmethod
def mute_sdef_set(cls, obj, mute):
def mute_sdef_set(cls, obj: Object, mute: bool) -> None:
key_blocks = getattr(obj.data.shape_keys, "key_blocks", ())
if cls.SHAPEKEY_NAME in key_blocks:
shapekey = key_blocks[cls.SHAPEKEY_NAME]
shapekey.mute = mute
if cls.has_sdef_data(obj):
logger.debug(f"Setting SDEF mute state to {mute} for {obj.name}")
cls.__init_cache(obj, shapekey)
cls.__sdef_muted(obj, shapekey)
@classmethod
def __sdef_muted(cls, obj, shapekey):
def __sdef_muted(cls, obj: Object, shapekey: ShapeKey) -> bool:
mute = shapekey.mute
if mute != cls.g_bone_check[_hash(obj)].get("sdef_mute"):
mod = obj.modifiers.get("mmd_bone_order_override")
@@ -80,10 +87,11 @@ class FnSDEF:
mod.invert_vertex_group = True
shapekey.vertex_group = cls.MASK_NAME
cls.g_bone_check[_hash(obj)]["sdef_mute"] = mute
logger.debug(f"SDEF mute state updated to {mute} for {obj.name}")
return mute
@staticmethod
def has_sdef_data(obj):
def has_sdef_data(obj: Object) -> bool:
mod = obj.modifiers.get("mmd_bone_order_override")
if mod and mod.type == "ARMATURE" and mod.object:
kb = getattr(obj.data.shape_keys, "key_blocks", None)
@@ -91,18 +99,21 @@ class FnSDEF:
return False
@classmethod
def __find_vertices(cls, obj):
def __find_vertices(cls, obj: Object) -> Dict[Tuple[int, int], Tuple[PoseBone, PoseBone, List[Tuple[int, float, float, Vector, Vector, Vector]], List[int]]]:
if not cls.has_sdef_data(obj):
return {}
vertices = {}
vertices: Dict[Tuple[int, int], Tuple[PoseBone, PoseBone, List[Tuple[int, float, float, Vector, Vector, Vector]], List[int]]] = {}
pose_bones = obj.modifiers.get("mmd_bone_order_override").object.pose.bones
bone_map = {g.index: pose_bones[g.name] for g in obj.vertex_groups if g.name in pose_bones}
bone_map: Dict[int, PoseBone] = {g.index: pose_bones[g.name] for g in obj.vertex_groups if g.name in pose_bones}
sdef_c = obj.data.shape_keys.key_blocks["mmd_sdef_c"].data
sdef_r0 = obj.data.shape_keys.key_blocks["mmd_sdef_r0"].data
sdef_r1 = obj.data.shape_keys.key_blocks["mmd_sdef_r1"].data
vd = obj.data.vertices
logger.debug(f"Finding SDEF vertices for {obj.name}")
vertex_count = 0
for i in range(len(sdef_c)):
if vd[i].co != sdef_c[i].co:
bgs = [g for g in vd[i].groups if g.group in bone_map and g.weight] # bone groups
@@ -125,16 +136,19 @@ class FnSDEF:
vertices[key] = (bone_map[bgs[0].group], bone_map[bgs[1].group], [], [])
vertices[key][2].append((i, w0, w1, vd[i].co - c, (c + r0) / 2, (c + r1) / 2))
vertices[key][3].append(i)
vertex_count += 1
logger.debug(f"Found {vertex_count} SDEF vertices in {obj.name}")
return vertices
@classmethod
def driver_function_wrap(cls, obj_name, bulk_update, use_skip, use_scale):
def driver_function_wrap(cls, obj_name: str, bulk_update: bool, use_skip: bool, use_scale: bool) -> float:
obj = bpy.data.objects[obj_name]
shapekey = obj.data.shape_keys.key_blocks[cls.SHAPEKEY_NAME]
return cls.driver_function(shapekey, obj_name, bulk_update, use_skip, use_scale)
@classmethod
def driver_function(cls, shapekey, obj_name, bulk_update, use_skip, use_scale):
def driver_function(cls, shapekey: ShapeKey, obj_name: str, bulk_update: bool, use_skip: bool, use_scale: bool) -> float:
obj = bpy.data.objects[obj_name]
if getattr(shapekey.id_data, "is_evaluated", False):
# For Blender 2.8x, we should use evaluated object, and the only reference is the "obj" variable of SDEF driver
@@ -206,11 +220,11 @@ class FnSDEF:
rot1 = -rot1
s0, s1 = mat0.to_scale(), mat1.to_scale()
def scale(mat_rot, w0, w1):
def scale(mat_rot: Matrix, w0: float, w1: float) -> Matrix:
s = s0 * w0 + s1 * w1
return mat_rot @ Matrix([(s[0], 0, 0), (0, s[1], 0), (0, 0, s[2])])
def offset(mat_rot, pos_c, vid):
def offset(mat_rot: Matrix, pos_c: Vector, vid: int) -> Vector:
delta = sum(((key.data[vid].co - key.relative_key.data[vid].co) * key.value for key in key_blocks), Vector()) # assuming key.vertex_group = ''
return (mat_rot @ (pos_c + delta)) - delta
@@ -233,16 +247,19 @@ class FnSDEF:
return 1.0 # shapkey value
@classmethod
def register_driver_function(cls):
def register_driver_function(cls) -> None:
"""Register driver functions in Blender's driver namespace."""
if "mmd_sdef_driver" not in bpy.app.driver_namespace:
logger.debug("Registering SDEF driver function")
bpy.app.driver_namespace["mmd_sdef_driver"] = cls.driver_function
if "mmd_sdef_driver_wrap" not in bpy.app.driver_namespace:
logger.debug("Registering SDEF driver wrapper function")
bpy.app.driver_namespace["mmd_sdef_driver_wrap"] = cls.driver_function_wrap
BENCH_LOOP = 10
BENCH_LOOP: int = 10
@classmethod
def __get_benchmark_result(cls, obj, shapkey, use_scale, use_skip):
def __get_benchmark_result(cls, obj: Object, shapkey: ShapeKey, use_scale: bool, use_skip: bool) -> bool:
# warmed up
cls.driver_function(shapkey, obj.name, bulk_update=True, use_skip=False, use_scale=use_scale)
cls.driver_function(shapkey, obj.name, bulk_update=False, use_skip=False, use_scale=use_scale)
@@ -256,14 +273,15 @@ class FnSDEF:
cls.driver_function(shapkey, obj.name, bulk_update=True, use_skip=False, use_scale=use_scale)
bulk_time = time.time() - t
result = default_time > bulk_time
logging.info("FnSDEF:benchmark: default %.4f vs bulk_update %.4f => bulk_update=%s", default_time, bulk_time, result)
logger.info(f"SDEF benchmark for {obj.name}: default {default_time:.4f}s vs bulk_update {bulk_time:.4f}s => bulk_update={result}")
return result
@classmethod
def bind(cls, obj, bulk_update=None, use_skip=True, use_scale=False):
def bind(cls, obj: Object, bulk_update: Optional[bool] = None, use_skip: bool = True, use_scale: bool = False) -> bool:
# Unbind first
cls.unbind(obj)
if not cls.has_sdef_data(obj):
logger.debug(f"Object {obj.name} does not have SDEF data")
return False
# Create the shapekey for the driver
shapekey = obj.shape_key_add(name=cls.SHAPEKEY_NAME, from_mix=False)
@@ -294,32 +312,38 @@ class FnSDEF:
f.driver.use_self = True
param = (bulk_update, use_skip, use_scale)
f.driver.expression = "mmd_sdef_driver(self, obj, bulk_update={}, use_skip={}, use_scale={})".format(*param)
logger.info(f"Successfully bound SDEF to {obj.name} with bulk_update={bulk_update}, use_skip={use_skip}, use_scale={use_scale}")
return True
@classmethod
def unbind(cls, obj):
def unbind(cls, obj: Object) -> None:
if obj.data.shape_keys:
if cls.SHAPEKEY_NAME in obj.data.shape_keys.key_blocks:
logger.debug(f"Removing SDEF shape key from {obj.name}")
FnObject.mesh_remove_shape_key(obj, obj.data.shape_keys.key_blocks[cls.SHAPEKEY_NAME])
for mod in obj.modifiers:
if mod.type == "ARMATURE" and mod.vertex_group == cls.MASK_NAME:
logger.debug(f"Clearing SDEF vertex group from modifier in {obj.name}")
mod.vertex_group = ""
mod.invert_vertex_group = False
break
if cls.MASK_NAME in obj.vertex_groups:
logger.debug(f"Removing SDEF vertex group from {obj.name}")
obj.vertex_groups.remove(obj.vertex_groups[cls.MASK_NAME])
cls.clear_cache(obj)
@classmethod
def clear_cache(cls, obj=None, unused_only=False):
def clear_cache(cls, obj: Optional[Object] = None, unused_only: bool = False) -> None:
if unused_only:
valid_keys = set(_hash(i) for i in bpy.data.objects if i.type == "MESH" and i != obj)
for key in cls.g_verts.keys() - valid_keys:
removed_keys = cls.g_verts.keys() - valid_keys
for key in removed_keys:
del cls.g_verts[key]
for key in cls.g_shapekey_data.keys() - cls.g_verts.keys():
del cls.g_shapekey_data[key]
for key in cls.g_bone_check.keys() - cls.g_verts.keys():
del cls.g_bone_check[key]
logger.debug(f"Cleared {len(removed_keys)} unused SDEF cache entries")
elif obj:
key = _hash(obj)
if key in cls.g_verts:
@@ -328,7 +352,9 @@ class FnSDEF:
del cls.g_shapekey_data[key]
if key in cls.g_bone_check:
del cls.g_bone_check[key]
logger.debug(f"Cleared SDEF cache for {obj.name}")
else:
logger.debug("Cleared all SDEF cache")
cls.g_verts = {}
cls.g_bone_check = {}
cls.g_shapekey_data = {}
+56 -36
View File
@@ -5,25 +5,33 @@
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import Optional, Tuple, cast
from typing import Optional, Tuple, cast, List, Dict, Any, Union
import bpy
from bpy.types import (
ShaderNodeTree,
ShaderNode,
NodeGroupInput,
NodeGroupOutput,
Material
)
from ....core.logging_setup import logger
class _NodeTreeUtils:
def __init__(self, shader: bpy.types.ShaderNodeTree):
def __init__(self, shader: ShaderNodeTree):
self.shader = shader
self.nodes: bpy.types.bpy_prop_collection[bpy.types.ShaderNode] = shader.nodes # type: ignore
self.nodes: bpy.types.bpy_prop_collection[ShaderNode] = shader.nodes # type: ignore
self.links = shader.links
def _find_node(self, node_type: str) -> Optional[bpy.types.ShaderNode]:
def _find_node(self, node_type: str) -> Optional[ShaderNode]:
return next((n for n in self.nodes if n.bl_idname == node_type), None)
def new_node(self, idname: str, pos: Tuple[int, int]) -> bpy.types.ShaderNode:
node: bpy.types.ShaderNode = self.nodes.new(idname)
def new_node(self, idname: str, pos: Tuple[int, int]) -> ShaderNode:
node: ShaderNode = self.nodes.new(idname)
node.location = (pos[0] * 210, pos[1] * 220)
return node
def new_math_node(self, operation, pos, value1=None, value2=None):
def new_math_node(self, operation: str, pos: Tuple[int, int], value1: Optional[float] = None, value2: Optional[float] = None) -> ShaderNode:
node = self.new_node("ShaderNodeMath", pos)
node.operation = operation
if value1 is not None:
@@ -32,7 +40,7 @@ class _NodeTreeUtils:
node.inputs[1].default_value = value2
return node
def new_vector_math_node(self, operation, pos, vector1=None, vector2=None):
def new_vector_math_node(self, operation: str, pos: Tuple[int, int], vector1: Optional[Tuple[float, float, float, float]] = None, vector2: Optional[Tuple[float, float, float, float]] = None) -> ShaderNode:
node = self.new_node("ShaderNodeVectorMath", pos)
node.operation = operation
if vector1 is not None:
@@ -41,7 +49,7 @@ class _NodeTreeUtils:
node.inputs[1].default_value = vector2
return node
def new_mix_node(self, blend_type, pos, fac=None, color1=None, color2=None):
def new_mix_node(self, blend_type: str, pos: Tuple[int, int], fac: Optional[float] = None, color1: Optional[Tuple[float, float, float, float]] = None, color2: Optional[Tuple[float, float, float, float]] = None) -> ShaderNode:
node = self.new_node("ShaderNodeMixRGB", pos)
node.blend_type = blend_type
if fac is not None:
@@ -53,30 +61,30 @@ class _NodeTreeUtils:
return node
SOCKET_TYPE_MAPPING = {"NodeSocketFloatFactor": "NodeSocketFloat"}
SOCKET_TYPE_MAPPING: Dict[str, str] = {"NodeSocketFloatFactor": "NodeSocketFloat"}
SOCKET_SUBTYPE_MAPPING = {"NodeSocketFloatFactor": "FACTOR"}
SOCKET_SUBTYPE_MAPPING: Dict[str, str] = {"NodeSocketFloatFactor": "FACTOR"}
class _NodeGroupUtils(_NodeTreeUtils):
def __init__(self, shader: bpy.types.ShaderNodeTree):
def __init__(self, shader: ShaderNodeTree):
super().__init__(shader)
self.__node_input: Optional[bpy.types.NodeGroupInput] = None
self.__node_output: Optional[bpy.types.NodeGroupOutput] = None
self.__node_input: Optional[NodeGroupInput] = None
self.__node_output: Optional[NodeGroupOutput] = None
@property
def node_input(self) -> bpy.types.NodeGroupInput:
def node_input(self) -> NodeGroupInput:
if not self.__node_input:
self.__node_input = cast(bpy.types.NodeGroupInput, self._find_node("NodeGroupInput") or self.new_node("NodeGroupInput", (-2, 0)))
self.__node_input = cast(NodeGroupInput, self._find_node("NodeGroupInput") or self.new_node("NodeGroupInput", (-2, 0)))
return self.__node_input
@property
def node_output(self) -> bpy.types.NodeGroupOutput:
def node_output(self) -> NodeGroupOutput:
if not self.__node_output:
self.__node_output = cast(bpy.types.NodeGroupOutput, self._find_node("NodeGroupOutput") or self.new_node("NodeGroupOutput", (2, 0)))
self.__node_output = cast(NodeGroupOutput, self._find_node("NodeGroupOutput") or self.new_node("NodeGroupOutput", (2, 0)))
return self.__node_output
def hide_nodes(self, hide_sockets=True):
def hide_nodes(self, hide_sockets: bool = True) -> None:
skip_nodes = {self.__node_input, self.__node_output}
for n in (x for x in self.nodes if x not in skip_nodes):
n.hide = True
@@ -87,15 +95,15 @@ class _NodeGroupUtils(_NodeTreeUtils):
for s in n.outputs:
s.hide = not s.is_linked
def new_input_socket(self, io_name, socket, default_val=None, min_max=None, socket_type=None):
def new_input_socket(self, io_name: str, socket: Optional[bpy.types.NodeSocket], default_val: Optional[Union[float, Tuple[float, float, float, float]]] = None, min_max: Optional[Tuple[float, float]] = None, socket_type: Optional[str] = None) -> None:
self.__new_io("INPUT", self.node_input.outputs, io_name, socket, default_val, min_max, socket_type)
def new_output_socket(self, io_name, socket, default_val=None, min_max=None, socket_type=None):
def new_output_socket(self, io_name: str, socket: Optional[bpy.types.NodeSocket], default_val: Optional[Union[float, Tuple[float, float, float, float]]] = None, min_max: Optional[Tuple[float, float]] = None, socket_type: Optional[str] = None) -> None:
self.__new_io("OUTPUT", self.node_output.inputs, io_name, socket, default_val, min_max, socket_type)
def __new_io(self, in_out, io_sockets, io_name, socket, default_val=None, min_max=None, socket_type=None):
def __new_io(self, in_out: str, io_sockets: bpy.types.bpy_prop_collection, io_name: str, socket: Optional[bpy.types.NodeSocket], default_val: Optional[Union[float, Tuple[float, float, float, float]]] = None, min_max: Optional[Tuple[float, float]] = None, socket_type: Optional[str] = None) -> None:
if io_name not in io_sockets:
idname = socket_type or socket.bl_idname
idname = socket_type or (socket.bl_idname if socket else "NodeSocketFloat")
interface_socket = self.shader.interface.new_socket(name=io_name, in_out=in_out, socket_type=SOCKET_TYPE_MAPPING.get(idname, idname))
if idname in SOCKET_SUBTYPE_MAPPING:
interface_socket.subtype = SOCKET_SUBTYPE_MAPPING.get(idname, "")
@@ -114,14 +122,18 @@ class _NodeGroupUtils(_NodeTreeUtils):
class _MaterialMorph:
@classmethod
def update_morph_inputs(cls, material, morph):
def update_morph_inputs(cls, material: Optional[Material], morph: Any) -> None:
"""Update material morph inputs based on morph data"""
if material and material.node_tree and morph.name in material.node_tree.nodes:
logger.debug(f"Updating morph inputs for {morph.name} in {material.name}")
cls.__update_node_inputs(material.node_tree.nodes[morph.name], morph)
cls.update_morph_inputs(bpy.data.materials.get("mmd_edge." + material.name, None), morph)
@classmethod
def setup_morph_nodes(cls, material, morphs):
def setup_morph_nodes(cls, material: Material, morphs: List[Any]) -> List[ShaderNode]:
"""Set up morph nodes for a material"""
node, nodes = None, []
logger.debug(f"Setting up {len(morphs)} morph nodes for {material.name}")
for m in morphs:
node = cls.__morph_node_add(material, m, node)
nodes.append(node)
@@ -137,23 +149,25 @@ class _MaterialMorph:
return nodes
@classmethod
def reset_morph_links(cls, node):
def reset_morph_links(cls, node: ShaderNode) -> None:
"""Reset morph links for a node"""
logger.debug(f"Resetting morph links for {node.name}")
cls.__update_morph_links(node, reset=True)
@classmethod
def __update_morph_links(cls, node, reset=False):
def __update_morph_links(cls, node: ShaderNode, reset: bool = False) -> None:
nodes, links = node.id_data.nodes, node.id_data.links
if reset:
if any(l.from_node.name.startswith("mmd_bind") for i in node.inputs for l in i.links):
return
def __init_link(socket_morph, socket_shader):
def __init_link(socket_morph: bpy.types.NodeSocket, socket_shader: Optional[bpy.types.NodeSocket]) -> None:
if socket_shader and socket_morph.is_linked:
links.new(socket_morph.links[0].from_socket, socket_shader)
else:
def __init_link(socket_morph, socket_shader):
def __init_link(socket_morph: bpy.types.NodeSocket, socket_shader: Optional[bpy.types.NodeSocket]) -> None:
if socket_shader:
if socket_shader.is_linked:
links.new(socket_shader.links[0].from_socket, socket_morph)
@@ -178,7 +192,8 @@ class _MaterialMorph:
__init_link(node.inputs["Edge1 A"], shader.inputs["Alpha"])
@classmethod
def __update_node_inputs(cls, node, morph):
def __update_node_inputs(cls, node: ShaderNode, morph: Any) -> None:
"""Update node inputs based on morph data"""
node.inputs["Ambient2"].default_value[:3] = morph.ambient_color[:3]
node.inputs["Diffuse2"].default_value[:3] = morph.diffuse_color[:3]
node.inputs["Specular2"].default_value[:3] = morph.specular_color[:3]
@@ -196,7 +211,8 @@ class _MaterialMorph:
node.inputs["Sphere2 A"].default_value = morph.sphere_texture_factor[3]
@classmethod
def __morph_node_add(cls, material, morph, prev_node):
def __morph_node_add(cls, material: Material, morph: Optional[Any], prev_node: Optional[ShaderNode]) -> Optional[ShaderNode]:
"""Add a morph node to a material"""
nodes, links = material.node_tree.nodes, material.node_tree.links
shader = nodes.get("mmd_shader", None)
@@ -221,8 +237,9 @@ class _MaterialMorph:
return node
# connect last node to shader
if shader:
logger.debug(f"Connecting last node to shader for {material.name}")
def __soft_link(socket_out, socket_in):
def __soft_link(socket_out: Optional[bpy.types.NodeSocket], socket_in: Optional[bpy.types.NodeSocket]) -> None:
if socket_out and socket_in:
links.new(socket_out, socket_in)
@@ -244,12 +261,14 @@ class _MaterialMorph:
return shader
@classmethod
def __get_shader(cls, morph_type):
def __get_shader(cls, morph_type: str) -> ShaderNodeTree:
"""Get or create a shader node group for the specified morph type"""
group_name = "MMDMorph" + morph_type
shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
if len(shader.nodes):
return shader
logger.info(f"Creating new shader node group: {group_name}")
ng = _NodeGroupUtils(shader)
links = ng.links
@@ -260,7 +279,7 @@ class _MaterialMorph:
ng.new_input_socket("Fac", None, 0, socket_type="NodeSocketFloat")
ng.new_node("NodeGroupOutput", (3, 0))
def __blend_color_add(id_name, pos, tag=""):
def __blend_color_add(id_name: str, pos: Tuple[int, int], tag: str = "") -> ShaderNode:
# MA_RAMP_MULT: ColorMul = Color1 * (Fac * Color2 + (1 - Fac))
# MA_RAMP_ADD: ColorAdd = Color1 + Fac * Color2
# https://github.com/blender/blender/blob/594f47ecd2d5367ca936cf6fc6ec8168c2b360d0/source/blender/blenkernel/intern/material.c#L1400
@@ -271,7 +290,7 @@ class _MaterialMorph:
ng.new_output_socket(id_name + tag, node_mix.outputs["Color"])
return node_mix
def __blend_tex_color(id_name, pos, node_tex_rgb, node_tex_a_output):
def __blend_tex_color(id_name: str, pos: Tuple[int, int], node_tex_rgb: ShaderNode, node_tex_a_output: bpy.types.NodeSocket) -> None:
# Tex Color = tex_rgb * tex_a + (1 - tex_a)
# : tex_rgb = TexRGB * ColorMul + ColorAdd
# : tex_a = TexA * ValueMul + ValueAdd
@@ -294,7 +313,7 @@ class _MaterialMorph:
ng.new_output_socket(id_name + " Tex", node_add.outputs[0], socket_type="NodeSocketColor")
ng.new_output_socket(id_name + " Tex Add", node_scale.outputs[0], socket_type="NodeSocketColor")
def __add_sockets(id_name, input1, input2, output, tag=""):
def __add_sockets(id_name: str, input1: bpy.types.NodeSocket, input2: bpy.types.NodeSocket, output: bpy.types.NodeSocket, tag: str = "") -> None:
ng.new_input_socket(f"{id_name}1{tag}", input1, use_mul)
ng.new_input_socket(f"{id_name}2{tag}", input2, use_mul)
ng.new_output_socket(f"{id_name}{tag}", output)
@@ -343,4 +362,5 @@ class _MaterialMorph:
__blend_tex_color("Sphere", (pos_x + 3, -3.5), sphere_rgb, separate_basea_toona_spherea.outputs[2])
ng.hide_nodes()
logger.debug(f"Shader node group {group_name} created successfully")
return ng.shader