Update Logging
You can choose between errors, warning, info or full debug, errors will always log to ensure we don't have silent failures with debug on or off.
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 MMD Tools authors
|
||||
# This file was originally part of the MMD Tools add-on for Blender
|
||||
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
|
||||
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
@@ -0,0 +1,406 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 MMD Tools authors
|
||||
# This file was originally part of the MMD Tools add-on for Blender
|
||||
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
|
||||
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
import bpy
|
||||
from bpy.props import BoolProperty, StringProperty
|
||||
from bpy.types import Operator
|
||||
|
||||
from .. import cycles_converter
|
||||
from ..core.exceptions import MaterialNotFoundError
|
||||
from ..core.material import FnMaterial
|
||||
from ..core.shader import _NodeGroupUtils
|
||||
|
||||
|
||||
class ConvertMaterialsForCycles(Operator):
|
||||
bl_idname = "mmd_tools.convert_materials_for_cycles"
|
||||
bl_label = "Convert Materials For Cycles"
|
||||
bl_description = "Convert materials of selected objects for Cycles."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
use_principled: bpy.props.BoolProperty(
|
||||
name="Convert to Principled BSDF",
|
||||
description="Convert MMD shader nodes to Principled BSDF as well if enabled",
|
||||
default=False,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
clean_nodes: bpy.props.BoolProperty(
|
||||
name="Clean Nodes",
|
||||
description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
|
||||
default=False,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return next((x for x in context.selected_objects if x.type == "MESH"), None)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.prop(self, "use_principled")
|
||||
layout.prop(self, "clean_nodes")
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
context.scene.render.engine = "CYCLES"
|
||||
except:
|
||||
self.report({"ERROR"}, " * Failed to change to Cycles render engine.")
|
||||
return {"CANCELLED"}
|
||||
for obj in (x for x in context.selected_objects if x.type == "MESH"):
|
||||
cycles_converter.convertToCyclesShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ConvertMaterials(Operator):
|
||||
bl_idname = "mmd_tools.convert_materials"
|
||||
bl_label = "Convert Materials"
|
||||
bl_description = "Convert materials of selected objects."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
use_principled: bpy.props.BoolProperty(
|
||||
name="Convert to Principled BSDF",
|
||||
description="Convert MMD shader nodes to Principled BSDF as well if enabled",
|
||||
default=True,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
clean_nodes: bpy.props.BoolProperty(
|
||||
name="Clean Nodes",
|
||||
description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
|
||||
default=True,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
subsurface: bpy.props.FloatProperty(
|
||||
name="Subsurface",
|
||||
default=0.001,
|
||||
soft_min=0.000,
|
||||
soft_max=1.000,
|
||||
precision=3,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return next((x for x in context.selected_objects if x.type == "MESH"), None)
|
||||
|
||||
def execute(self, context):
|
||||
for obj in context.selected_objects:
|
||||
if obj.type != "MESH":
|
||||
continue
|
||||
cycles_converter.convertToBlenderShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes, subsurface=self.subsurface)
|
||||
return {"FINISHED"}
|
||||
|
||||
class ConvertBSDFMaterials(Operator):
|
||||
bl_idname = 'mmd_tools.convert_bsdf_materials'
|
||||
bl_label = 'Convert Blender Materials'
|
||||
bl_description = 'Convert materials of selected objects.'
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return next((x for x in context.selected_objects if x.type == 'MESH'), None)
|
||||
|
||||
def execute(self, context):
|
||||
for obj in context.selected_objects:
|
||||
if obj.type != 'MESH':
|
||||
continue
|
||||
cycles_converter.convertToMMDShader(obj)
|
||||
return {'FINISHED'}
|
||||
|
||||
class _OpenTextureBase:
|
||||
"""Create a texture for mmd model material."""
|
||||
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
filepath: StringProperty(
|
||||
name="File Path",
|
||||
description="Filepath used for importing the file",
|
||||
maxlen=1024,
|
||||
subtype="FILE_PATH",
|
||||
)
|
||||
|
||||
use_filter_image: BoolProperty(
|
||||
default=True,
|
||||
options={"HIDDEN"},
|
||||
)
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class OpenTexture(Operator, _OpenTextureBase):
|
||||
bl_idname = "mmd_tools.material_open_texture"
|
||||
bl_label = "Open Texture"
|
||||
bl_description = "Create main texture of active material"
|
||||
|
||||
def execute(self, context):
|
||||
mat = context.active_object.active_material
|
||||
fnMat = FnMaterial(mat)
|
||||
fnMat.create_texture(self.filepath)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemoveTexture(Operator):
|
||||
"""Create a texture for mmd model material."""
|
||||
|
||||
bl_idname = "mmd_tools.material_remove_texture"
|
||||
bl_label = "Remove Texture"
|
||||
bl_description = "Remove main texture of active material"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
mat = context.active_object.active_material
|
||||
fnMat = FnMaterial(mat)
|
||||
fnMat.remove_texture()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class OpenSphereTextureSlot(Operator, _OpenTextureBase):
|
||||
"""Create a texture for mmd model material."""
|
||||
|
||||
bl_idname = "mmd_tools.material_open_sphere_texture"
|
||||
bl_label = "Open Sphere Texture"
|
||||
bl_description = "Create sphere texture of active material"
|
||||
|
||||
def execute(self, context):
|
||||
mat = context.active_object.active_material
|
||||
fnMat = FnMaterial(mat)
|
||||
fnMat.create_sphere_texture(self.filepath, context.active_object)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemoveSphereTexture(Operator):
|
||||
"""Create a texture for mmd model material."""
|
||||
|
||||
bl_idname = "mmd_tools.material_remove_sphere_texture"
|
||||
bl_label = "Remove Sphere Texture"
|
||||
bl_description = "Remove sphere texture of active material"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
mat = context.active_object.active_material
|
||||
fnMat = FnMaterial(mat)
|
||||
fnMat.remove_sphere_texture()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class MoveMaterialUp(Operator):
|
||||
bl_idname = "mmd_tools.move_material_up"
|
||||
bl_label = "Move Material Up"
|
||||
bl_description = "Moves selected material one slot up"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE"
|
||||
return valid_mesh and obj.active_material_index > 0
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
current_idx = obj.active_material_index
|
||||
prev_index = current_idx - 1
|
||||
try:
|
||||
FnMaterial.swap_materials(obj, current_idx, prev_index, reverse=True, swap_slots=True)
|
||||
except MaterialNotFoundError:
|
||||
self.report({"ERROR"}, "Materials not found")
|
||||
return {"CANCELLED"}
|
||||
obj.active_material_index = prev_index
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class MoveMaterialDown(Operator):
|
||||
bl_idname = "mmd_tools.move_material_down"
|
||||
bl_label = "Move Material Down"
|
||||
bl_description = "Moves the selected material one slot down"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE"
|
||||
return valid_mesh and obj.active_material_index < len(obj.material_slots) - 1
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
current_idx = obj.active_material_index
|
||||
next_index = current_idx + 1
|
||||
try:
|
||||
FnMaterial.swap_materials(obj, current_idx, next_index, reverse=True, swap_slots=True)
|
||||
except MaterialNotFoundError:
|
||||
self.report({"ERROR"}, "Materials not found")
|
||||
return {"CANCELLED"}
|
||||
obj.active_material_index = next_index
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EdgePreviewSetup(Operator):
|
||||
bl_idname = "mmd_tools.edge_preview_setup"
|
||||
bl_label = "Edge Preview Setup"
|
||||
bl_description = 'Preview toon edge settings of active model using "Solidify" modifier'
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
action: bpy.props.EnumProperty(
|
||||
name="Action",
|
||||
description="Select action",
|
||||
items=[
|
||||
("CREATE", "Create", "Create toon edge", 0),
|
||||
("CLEAN", "Clean", "Clear toon edge", 1),
|
||||
],
|
||||
default="CREATE",
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
from ..core.model import FnModel
|
||||
|
||||
root = FnModel.find_root_object(context.active_object)
|
||||
if root is None:
|
||||
self.report({"ERROR"}, "Select a MMD model")
|
||||
return {"CANCELLED"}
|
||||
|
||||
if self.action == "CLEAN":
|
||||
for obj in FnModel.iterate_mesh_objects(root):
|
||||
self.__clean_toon_edge(obj)
|
||||
else:
|
||||
from ..bpyutils import Props
|
||||
|
||||
scale = 0.2 * getattr(root, Props.empty_display_size)
|
||||
counts = sum(self.__create_toon_edge(obj, scale) for obj in FnModel.iterate_mesh_objects(root))
|
||||
self.report({"INFO"}, "Created %d toon edge(s)" % counts)
|
||||
return {"FINISHED"}
|
||||
|
||||
def __clean_toon_edge(self, obj):
|
||||
if "mmd_edge_preview" in obj.modifiers:
|
||||
obj.modifiers.remove(obj.modifiers["mmd_edge_preview"])
|
||||
|
||||
if "mmd_edge_preview" in obj.vertex_groups:
|
||||
obj.vertex_groups.remove(obj.vertex_groups["mmd_edge_preview"])
|
||||
|
||||
FnMaterial.clean_materials(obj, can_remove=lambda m: m and m.name.startswith("mmd_edge."))
|
||||
|
||||
def __create_toon_edge(self, obj, scale=1.0):
|
||||
self.__clean_toon_edge(obj)
|
||||
materials = obj.data.materials
|
||||
material_offset = len(materials)
|
||||
for m in tuple(materials):
|
||||
if m and m.mmd_material.enabled_toon_edge:
|
||||
mat_edge = self.__get_edge_material("mmd_edge." + m.name, m.mmd_material.edge_color, materials)
|
||||
materials.append(mat_edge)
|
||||
elif material_offset > 1:
|
||||
mat_edge = self.__get_edge_material("mmd_edge.disabled", (0, 0, 0, 0), materials)
|
||||
materials.append(mat_edge)
|
||||
if len(materials) > material_offset:
|
||||
mod = obj.modifiers.get("mmd_edge_preview", None)
|
||||
if mod is None:
|
||||
mod = obj.modifiers.new("mmd_edge_preview", "SOLIDIFY")
|
||||
mod.material_offset = material_offset
|
||||
mod.thickness_vertex_group = 1e-3 # avoid overlapped faces
|
||||
mod.use_flip_normals = True
|
||||
mod.use_rim = False
|
||||
mod.offset = 1
|
||||
self.__create_edge_preview_group(obj)
|
||||
mod.thickness = scale
|
||||
mod.vertex_group = "mmd_edge_preview"
|
||||
return len(materials) - material_offset
|
||||
|
||||
def __create_edge_preview_group(self, obj):
|
||||
vertices, materials = obj.data.vertices, obj.data.materials
|
||||
weight_map = {i: m.mmd_material.edge_weight for i, m in enumerate(materials) if m}
|
||||
scale_map = {}
|
||||
vg_scale_index = obj.vertex_groups.find("mmd_edge_scale")
|
||||
if vg_scale_index >= 0:
|
||||
scale_map = {v.index: g.weight for v in vertices for g in v.groups if g.group == vg_scale_index}
|
||||
vg_edge_preview = obj.vertex_groups.new(name="mmd_edge_preview")
|
||||
for i, mi in {v: f.material_index for f in reversed(obj.data.polygons) for v in f.vertices}.items():
|
||||
weight = scale_map.get(i, 1.0) * weight_map.get(mi, 1.0) * 0.02
|
||||
vg_edge_preview.add(index=[i], weight=weight, type="REPLACE")
|
||||
|
||||
def __get_edge_material(self, mat_name, edge_color, materials):
|
||||
if mat_name in materials:
|
||||
return materials[mat_name]
|
||||
mat = bpy.data.materials.get(mat_name, None)
|
||||
if mat is None:
|
||||
mat = bpy.data.materials.new(mat_name)
|
||||
mmd_mat = mat.mmd_material
|
||||
# note: edge affects ground shadow
|
||||
mmd_mat.is_double_sided = mmd_mat.enabled_drop_shadow = False
|
||||
mmd_mat.enabled_self_shadow_map = mmd_mat.enabled_self_shadow = False
|
||||
# mmd_mat.enabled_self_shadow_map = True # for blender 2.78+ BI viewport only
|
||||
mmd_mat.diffuse_color = mmd_mat.specular_color = (0, 0, 0)
|
||||
mmd_mat.ambient_color = edge_color[:3]
|
||||
mmd_mat.alpha = edge_color[3]
|
||||
mmd_mat.edge_color = edge_color
|
||||
self.__make_shader(mat)
|
||||
return mat
|
||||
|
||||
def __make_shader(self, m):
|
||||
m.use_nodes = True
|
||||
nodes, links = m.node_tree.nodes, m.node_tree.links
|
||||
|
||||
node_shader = nodes.get("mmd_edge_preview", None)
|
||||
if node_shader is None or not any(s.is_linked for s in node_shader.outputs):
|
||||
XPOS, YPOS = 210, 110
|
||||
nodes.clear()
|
||||
node_shader = nodes.new("ShaderNodeGroup")
|
||||
node_shader.name = "mmd_edge_preview"
|
||||
node_shader.location = (0, 0)
|
||||
node_shader.width = 200
|
||||
node_shader.node_tree = self.__get_edge_preview_shader()
|
||||
|
||||
node_out = nodes.new("ShaderNodeOutputMaterial")
|
||||
node_out.location = (XPOS * 2, YPOS * 0)
|
||||
links.new(node_shader.outputs["Shader"], node_out.inputs["Surface"])
|
||||
|
||||
node_shader.inputs["Color"].default_value = m.mmd_material.edge_color
|
||||
node_shader.inputs["Alpha"].default_value = m.mmd_material.edge_color[3]
|
||||
|
||||
def __get_edge_preview_shader(self):
|
||||
group_name = "MMDEdgePreview"
|
||||
shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
|
||||
if len(shader.nodes):
|
||||
return shader
|
||||
|
||||
ng = _NodeGroupUtils(shader)
|
||||
|
||||
node_input = ng.new_node("NodeGroupInput", (-5, 0))
|
||||
node_output = ng.new_node("NodeGroupOutput", (3, 0))
|
||||
|
||||
############################################################################
|
||||
node_color = ng.new_node("ShaderNodeMixRGB", (-1, -1.5))
|
||||
node_color.mute = True
|
||||
|
||||
ng.new_input_socket("Color", node_color.inputs["Color1"])
|
||||
|
||||
############################################################################
|
||||
node_ray = ng.new_node("ShaderNodeLightPath", (-3, 1.5))
|
||||
node_geo = ng.new_node("ShaderNodeNewGeometry", (-3, 0))
|
||||
node_max = ng.new_math_node("MAXIMUM", (-2, 1.5))
|
||||
node_max.mute = True
|
||||
node_gt = ng.new_math_node("GREATER_THAN", (-1, 1))
|
||||
node_alpha = ng.new_math_node("MULTIPLY", (0, 1))
|
||||
node_trans = ng.new_node("ShaderNodeBsdfTransparent", (0, 0))
|
||||
node_rgb = ng.new_node("ShaderNodeBackground", (0, -0.5))
|
||||
node_mix = ng.new_node("ShaderNodeMixShader", (1, 0.5))
|
||||
|
||||
links = ng.links
|
||||
links.new(node_ray.outputs["Is Camera Ray"], node_max.inputs[0])
|
||||
links.new(node_ray.outputs["Is Glossy Ray"], node_max.inputs[1])
|
||||
links.new(node_max.outputs["Value"], node_gt.inputs[0])
|
||||
links.new(node_geo.outputs["Backfacing"], node_gt.inputs[1])
|
||||
links.new(node_gt.outputs["Value"], node_alpha.inputs[0])
|
||||
links.new(node_alpha.outputs["Value"], node_mix.inputs["Fac"])
|
||||
links.new(node_trans.outputs["BSDF"], node_mix.inputs[1])
|
||||
links.new(node_rgb.outputs[0], node_mix.inputs[2])
|
||||
links.new(node_color.outputs["Color"], node_rgb.inputs["Color"])
|
||||
|
||||
ng.new_input_socket("Alpha", node_alpha.inputs[1])
|
||||
ng.new_output_socket("Shader", node_mix.outputs["Shader"])
|
||||
|
||||
return shader
|
||||
@@ -0,0 +1,310 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 MMD Tools authors
|
||||
# This file was originally part of the MMD Tools add-on for Blender
|
||||
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
|
||||
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
import re
|
||||
|
||||
import bpy
|
||||
|
||||
from .. import utils
|
||||
from ..bpyutils import FnContext, FnObject
|
||||
from ..core.bone import FnBone
|
||||
from ..core.model import FnModel, Model
|
||||
from ..core.morph import FnMorph
|
||||
|
||||
|
||||
class SelectObject(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.object_select"
|
||||
bl_label = "Select Object"
|
||||
bl_description = "Select the object"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
name: bpy.props.StringProperty(
|
||||
name="Name",
|
||||
description="The object name",
|
||||
default="",
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
utils.selectAObject(context.scene.objects[self.name])
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class MoveObject(bpy.types.Operator, utils.ItemMoveOp):
|
||||
bl_idname = "mmd_tools.object_move"
|
||||
bl_label = "Move Object"
|
||||
bl_description = "Move active object up/down in the list"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
__PREFIX_REGEXP = re.compile(r"(?P<prefix>[0-9A-Z]{3}_)(?P<name>.*)")
|
||||
|
||||
@classmethod
|
||||
def set_index(cls, obj, index):
|
||||
m = cls.__PREFIX_REGEXP.match(obj.name)
|
||||
name = m.group("name") if m else obj.name
|
||||
obj.name = "%s_%s" % (utils.int2base(index, 36, 3), name)
|
||||
|
||||
@classmethod
|
||||
def get_name(cls, obj, prefix=None):
|
||||
m = cls.__PREFIX_REGEXP.match(obj.name)
|
||||
name = m.group("name") if m else obj.name
|
||||
return name[len(prefix) :] if prefix and name.startswith(prefix) else name
|
||||
|
||||
@classmethod
|
||||
def normalize_indices(cls, objects):
|
||||
for i, x in enumerate(objects):
|
||||
cls.set_index(x, i)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.active_object
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
objects = self.__get_objects(obj)
|
||||
if obj not in objects:
|
||||
self.report({"ERROR"}, 'Can not move object "%s"' % obj.name)
|
||||
return {"CANCELLED"}
|
||||
|
||||
objects.sort(key=lambda x: x.name)
|
||||
self.move(objects, objects.index(obj), self.type)
|
||||
self.normalize_indices(objects)
|
||||
return {"FINISHED"}
|
||||
|
||||
def __get_objects(self, obj):
|
||||
class __MovableList(list):
|
||||
def move(self, index_old, index_new):
|
||||
item = self[index_old]
|
||||
self.remove(item)
|
||||
self.insert(index_new, item)
|
||||
|
||||
objects = []
|
||||
root = FnModel.find_root_object(obj)
|
||||
if root:
|
||||
rig = Model(root)
|
||||
if obj.mmd_type == "NONE" and obj.type == "MESH":
|
||||
objects = rig.meshes()
|
||||
elif obj.mmd_type == "RIGID_BODY":
|
||||
objects = rig.rigidBodies()
|
||||
elif obj.mmd_type == "JOINT":
|
||||
objects = rig.joints()
|
||||
return __MovableList(objects)
|
||||
|
||||
|
||||
class CleanShapeKeys(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.clean_shape_keys"
|
||||
bl_label = "Clean Shape Keys"
|
||||
bl_description = "Remove unused shape keys of selected mesh objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return any(o.type == "MESH" for o in context.selected_objects)
|
||||
|
||||
@staticmethod
|
||||
def __can_remove(key_block):
|
||||
if key_block.relative_key == key_block:
|
||||
return False # Basis
|
||||
for v0, v1 in zip(key_block.relative_key.data, key_block.data):
|
||||
if v0.co != v1.co:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __shape_key_clean(self, obj, key_blocks):
|
||||
for kb in key_blocks:
|
||||
if self.__can_remove(kb):
|
||||
FnObject.mesh_remove_shape_key(obj, kb)
|
||||
if len(key_blocks) == 1:
|
||||
FnObject.mesh_remove_shape_key(obj, key_blocks[0])
|
||||
|
||||
def execute(self, context):
|
||||
obj: bpy.types.Object
|
||||
for obj in context.selected_objects:
|
||||
if obj.type != "MESH" or obj.data.shape_keys is None:
|
||||
continue
|
||||
if not obj.data.shape_keys.use_relative:
|
||||
continue # not be considered yet
|
||||
self.__shape_key_clean(obj, obj.data.shape_keys.key_blocks)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SeparateByMaterials(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.separate_by_materials"
|
||||
bl_label = "Separate By Materials"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
clean_shape_keys: bpy.props.BoolProperty(
|
||||
name="Clean Shape Keys",
|
||||
description="Remove unused shape keys of separated objects",
|
||||
default=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
return obj and obj.type == "MESH"
|
||||
|
||||
def __separate_by_materials(self, obj):
|
||||
utils.separateByMaterials(obj)
|
||||
if self.clean_shape_keys:
|
||||
bpy.ops.mmd_tools.clean_shape_keys()
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
if root is None:
|
||||
self.__separate_by_materials(obj)
|
||||
else:
|
||||
bpy.ops.mmd_tools.clear_temp_materials()
|
||||
bpy.ops.mmd_tools.clear_uv_morph_view()
|
||||
|
||||
# Store the current material names
|
||||
rig = Model(root)
|
||||
mat_names = [getattr(mat, "name", None) for mat in rig.materials()]
|
||||
self.__separate_by_materials(obj)
|
||||
for mesh in rig.meshes():
|
||||
FnMorph.clean_uv_morph_vertex_groups(mesh)
|
||||
if len(mesh.data.materials) > 0:
|
||||
mat = mesh.data.materials[0]
|
||||
idx = mat_names.index(getattr(mat, "name", None))
|
||||
MoveObject.set_index(mesh, idx)
|
||||
|
||||
for morph in root.mmd_root.material_morphs:
|
||||
FnMorph(morph, rig).update_mat_related_mesh()
|
||||
utils.clearUnusedMeshes()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class JoinMeshes(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.join_meshes"
|
||||
bl_label = "Join Meshes"
|
||||
bl_description = "Join the Model meshes into a single one"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
sort_shape_keys: bpy.props.BoolProperty(
|
||||
name="Sort Shape Keys",
|
||||
description="Sort shape keys in the order of vertex morph",
|
||||
default=True,
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
if root is None:
|
||||
self.report({"ERROR"}, "Select a MMD model")
|
||||
return {"CANCELLED"}
|
||||
|
||||
bpy.ops.mmd_tools.clear_temp_materials()
|
||||
bpy.ops.mmd_tools.clear_uv_morph_view()
|
||||
|
||||
# Find all the meshes in mmd_root
|
||||
rig = Model(root)
|
||||
meshes_list = sorted(rig.meshes(), key=lambda x: x.name)
|
||||
if not meshes_list:
|
||||
self.report({"ERROR"}, "The model does not have any meshes")
|
||||
return {"CANCELLED"}
|
||||
active_mesh = meshes_list[0]
|
||||
|
||||
FnContext.select_objects(context, *meshes_list)
|
||||
FnContext.set_active_object(context, active_mesh)
|
||||
|
||||
# Store the current order of the materials
|
||||
for m in meshes_list[1:]:
|
||||
for mat in m.data.materials:
|
||||
if mat not in active_mesh.data.materials[:]:
|
||||
active_mesh.data.materials.append(mat)
|
||||
|
||||
# Join selected meshes
|
||||
bpy.ops.object.join()
|
||||
|
||||
if self.sort_shape_keys:
|
||||
FnMorph.fixShapeKeyOrder(active_mesh, root.mmd_root.vertex_morphs.keys())
|
||||
active_mesh.active_shape_key_index = 0
|
||||
for morph in root.mmd_root.material_morphs:
|
||||
FnMorph(morph, rig).update_mat_related_mesh(active_mesh)
|
||||
utils.clearUnusedMeshes()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AttachMeshesToMMD(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.attach_meshes"
|
||||
bl_label = "Attach Meshes to Model"
|
||||
bl_description = "Finds existing meshes and attaches them to the selected MMD model"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
add_armature_modifier: bpy.props.BoolProperty(default=True)
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
root = FnModel.find_root_object(context.active_object)
|
||||
if root is None:
|
||||
self.report({"ERROR"}, "Select a MMD model")
|
||||
return {"CANCELLED"}
|
||||
|
||||
armObj = FnModel.find_armature_object(root)
|
||||
if armObj is None:
|
||||
self.report({"ERROR"}, "Model Armature not found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
FnModel.attach_mesh_objects(root, context.visible_objects, self.add_armature_modifier)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ChangeMMDIKLoopFactor(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.change_mmd_ik_loop_factor"
|
||||
bl_label = "Change MMD IK Loop Factor"
|
||||
bl_description = "Multiplier for all bones' IK iterations in Blender"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
mmd_ik_loop_factor: bpy.props.IntProperty(
|
||||
name="MMD IK Loop Factor",
|
||||
description="Scaling factor of MMD IK loop",
|
||||
min=1,
|
||||
soft_max=10,
|
||||
max=100,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return FnModel.find_root_object(context.active_object) is not None
|
||||
|
||||
def invoke(self, context, event):
|
||||
root_object = FnModel.find_root_object(context.active_object)
|
||||
self.mmd_ik_loop_factor = root_object.mmd_root.ik_loop_factor
|
||||
vm = context.window_manager
|
||||
return vm.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context):
|
||||
root_object = FnModel.find_root_object(context.active_object)
|
||||
FnModel.change_mmd_ik_loop_factor(root_object, self.mmd_ik_loop_factor)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RecalculateBoneRoll(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.recalculate_bone_roll"
|
||||
bl_label = "Recalculate bone roll"
|
||||
bl_description = "Recalculate bone roll for arm related bones"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
return obj and obj.type == "ARMATURE"
|
||||
|
||||
def invoke(self, context, event):
|
||||
vm = context.window_manager
|
||||
return vm.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
c = layout.column()
|
||||
c.label(text="This operation will break existing f-curve/action.", icon="QUESTION")
|
||||
c.label(text="Click [OK] to run the operation.")
|
||||
|
||||
def execute(self, context):
|
||||
arm = context.active_object
|
||||
FnBone.apply_auto_bone_roll(arm)
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,486 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 MMD Tools authors
|
||||
# This file was originally part of the MMD Tools add-on for Blender
|
||||
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
|
||||
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
import bpy
|
||||
|
||||
from ..bpyutils import FnContext
|
||||
from ..core.bone import FnBone, MigrationFnBone
|
||||
from ..core.model import FnModel, Model
|
||||
|
||||
|
||||
class MorphSliderSetup(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.morph_slider_setup"
|
||||
bl_label = "Morph Slider Setup"
|
||||
bl_description = "Translate MMD morphs of selected object into format usable by Blender"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
type: bpy.props.EnumProperty(
|
||||
name="Type",
|
||||
description="Select type",
|
||||
items=[
|
||||
("CREATE", "Create", "Create placeholder object for morph sliders", "SHAPEKEY_DATA", 0),
|
||||
("BIND", "Bind", "Bind morph sliders", "DRIVER", 1),
|
||||
("UNBIND", "Unbind", "Unbind morph sliders", "X", 2),
|
||||
],
|
||||
default="CREATE",
|
||||
)
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
active_object = context.active_object
|
||||
root_object = FnModel.find_root_object(active_object)
|
||||
assert root_object is not None
|
||||
|
||||
with FnContext.temp_override_active_layer_collection(context, root_object):
|
||||
rig = Model(root_object)
|
||||
if self.type == "BIND":
|
||||
rig.morph_slider.bind()
|
||||
elif self.type == "UNBIND":
|
||||
rig.morph_slider.unbind()
|
||||
else:
|
||||
rig.morph_slider.create()
|
||||
FnContext.set_active_object(context, active_object)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CleanRiggingObjects(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.clean_rig"
|
||||
bl_label = "Clean Rig"
|
||||
bl_description = "Delete temporary physics objects of selected object and revert physics to default MMD state"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
root_object = FnModel.find_root_object(context.active_object)
|
||||
assert root_object is not None
|
||||
|
||||
rig = Model(root_object)
|
||||
rig.clean()
|
||||
FnContext.set_active_object(context, root_object)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BuildRig(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.build_rig"
|
||||
bl_label = "Build Rig"
|
||||
bl_description = "Translate physics of selected object into format usable by Blender"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
non_collision_distance_scale: bpy.props.FloatProperty(
|
||||
name="Non-Collision Distance Scale",
|
||||
description="The distance scale for creating extra non-collision constraints while building physics",
|
||||
min=0,
|
||||
soft_max=10,
|
||||
default=1.5,
|
||||
)
|
||||
|
||||
collision_margin: bpy.props.FloatProperty(
|
||||
name="Collision Margin",
|
||||
description="The collision margin between rigid bodies. If 0, the default value for each shape is adopted.",
|
||||
unit="LENGTH",
|
||||
min=0,
|
||||
soft_max=10,
|
||||
default=1e-06,
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
root_object = FnModel.find_root_object(context.active_object)
|
||||
|
||||
with FnContext.temp_override_active_layer_collection(context, root_object):
|
||||
rig = Model(root_object)
|
||||
rig.build(self.non_collision_distance_scale, self.collision_margin)
|
||||
FnContext.set_active_object(context, root_object)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CleanAdditionalTransformConstraints(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.clean_additional_transform"
|
||||
bl_label = "Clean Additional Transform"
|
||||
bl_description = "Delete shadow bones of selected object and revert bones to default MMD state"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
active_object = context.active_object
|
||||
root_object = FnModel.find_root_object(active_object)
|
||||
assert root_object is not None
|
||||
FnBone.clean_additional_transformation(FnModel.find_armature_object(root_object))
|
||||
FnContext.set_active_object(context, active_object)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ApplyAdditionalTransformConstraints(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.apply_additional_transform"
|
||||
bl_label = "Apply Additional Transform"
|
||||
bl_description = "Translate appended bones of selected object for Blender"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
active_object = context.active_object
|
||||
root_object = FnModel.find_root_object(active_object)
|
||||
assert root_object is not None
|
||||
|
||||
armature_object = FnModel.find_armature_object(root_object)
|
||||
assert armature_object is not None
|
||||
|
||||
MigrationFnBone.fix_mmd_ik_limit_override(armature_object)
|
||||
FnBone.apply_additional_transformation(armature_object)
|
||||
FnContext.set_active_object(context, active_object)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SetupBoneFixedAxes(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.bone_fixed_axis_setup"
|
||||
bl_label = "Setup Bone Fixed Axis"
|
||||
bl_description = "Setup fixed axis of selected bones"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
type: bpy.props.EnumProperty(
|
||||
name="Type",
|
||||
description="Select type",
|
||||
items=[
|
||||
("DISABLE", "Disable", "Disable MMD fixed axis of selected bones", 0),
|
||||
("LOAD", "Load", "Load/Enable MMD fixed axis of selected bones from their Y-axis or the only rotatable axis", 1),
|
||||
("APPLY", "Apply", "Align bone axes to MMD fixed axis of each bone", 2),
|
||||
],
|
||||
default="LOAD",
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
armature_object = context.active_object
|
||||
if not armature_object or armature_object.type != "ARMATURE":
|
||||
self.report({"ERROR"}, "Active object is not an armature object")
|
||||
return {"CANCELLED"}
|
||||
|
||||
if self.type == "APPLY":
|
||||
FnBone.apply_bone_fixed_axis(armature_object)
|
||||
FnBone.apply_additional_transformation(armature_object)
|
||||
else:
|
||||
FnBone.load_bone_fixed_axis(armature_object, enable=(self.type == "LOAD"))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SetupBoneLocalAxes(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.bone_local_axes_setup"
|
||||
bl_label = "Setup Bone Local Axes"
|
||||
bl_description = "Setup local axes of each bone"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
type: bpy.props.EnumProperty(
|
||||
name="Type",
|
||||
description="Select type",
|
||||
items=[
|
||||
("DISABLE", "Disable", "Disable MMD local axes of selected bones", 0),
|
||||
("LOAD", "Load", "Load/Enable MMD local axes of selected bones from their bone axes", 1),
|
||||
("APPLY", "Apply", "Align bone axes to MMD local axes of each bone", 2),
|
||||
],
|
||||
default="LOAD",
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
armature_object = context.active_object
|
||||
if not armature_object or armature_object.type != "ARMATURE":
|
||||
self.report({"ERROR"}, "Active object is not an armature object")
|
||||
return {"CANCELLED"}
|
||||
|
||||
if self.type == "APPLY":
|
||||
FnBone.apply_bone_local_axes(armature_object)
|
||||
FnBone.apply_additional_transformation(armature_object)
|
||||
else:
|
||||
FnBone.load_bone_local_axes(armature_object, enable=(self.type == "LOAD"))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddMissingVertexGroupsFromBones(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.add_missing_vertex_groups_from_bones"
|
||||
bl_label = "Add Missing Vertex Groups from Bones"
|
||||
bl_description = "Add the missing vertex groups to the selected mesh"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
search_in_all_meshes: bpy.props.BoolProperty(
|
||||
name="Search in all meshes",
|
||||
description="Search for vertex groups in all meshes",
|
||||
default=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context):
|
||||
return FnModel.find_root_object(context.active_object) is not None
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
active_object: bpy.types.Object = context.active_object
|
||||
root_object = FnModel.find_root_object(active_object)
|
||||
assert root_object is not None
|
||||
|
||||
bone_order_mesh_object = FnModel.find_bone_order_mesh_object(root_object)
|
||||
if bone_order_mesh_object is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
FnModel.add_missing_vertex_groups_from_bones(root_object, bone_order_mesh_object, self.search_in_all_meshes)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CreateMMDModelRoot(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.create_mmd_model_root_object"
|
||||
bl_label = "Create a MMD Model Root Object"
|
||||
bl_description = "Create a MMD model root object with a basic armature"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
name_j: bpy.props.StringProperty(
|
||||
name="Name",
|
||||
description="The name of the MMD model",
|
||||
default="New MMD Model",
|
||||
)
|
||||
name_e: bpy.props.StringProperty(
|
||||
name="Name(Eng)",
|
||||
description="The english name of the MMD model",
|
||||
default="New MMD Model",
|
||||
)
|
||||
scale: bpy.props.FloatProperty(
|
||||
name="Scale",
|
||||
description="Scale",
|
||||
default=0.08,
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
rig = Model.create(self.name_j, self.name_e, self.scale, add_root_bone=True)
|
||||
rig.initialDisplayFrames()
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
vm = context.window_manager
|
||||
return vm.invoke_props_dialog(self)
|
||||
|
||||
|
||||
class ConvertToMMDModel(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.convert_to_mmd_model"
|
||||
bl_label = "Convert to a MMD Model"
|
||||
bl_description = "Convert active armature with its meshes to a MMD model (experimental)"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
ambient_color_source: bpy.props.EnumProperty(
|
||||
name="Ambient Color Source",
|
||||
description="Select ambient color source",
|
||||
items=[
|
||||
("DIFFUSE", "Diffuse", "Diffuse color", 0),
|
||||
("MIRROR", "Mirror", 'Mirror color (if property "mirror_color" is available)', 1),
|
||||
],
|
||||
default="DIFFUSE",
|
||||
)
|
||||
edge_threshold: bpy.props.FloatProperty(
|
||||
name="Edge Threshold",
|
||||
description="MMD toon edge will not be enabled if freestyle line color alpha less than this value",
|
||||
min=0,
|
||||
max=1.001,
|
||||
precision=3,
|
||||
step=0.1,
|
||||
default=0.1,
|
||||
)
|
||||
edge_alpha_min: bpy.props.FloatProperty(
|
||||
name="Minimum Edge Alpha",
|
||||
description="Minimum alpha of MMD toon edge color",
|
||||
min=0,
|
||||
max=1,
|
||||
precision=3,
|
||||
step=0.1,
|
||||
default=0.5,
|
||||
)
|
||||
scale: bpy.props.FloatProperty(
|
||||
name="Scale",
|
||||
description="Scaling factor for converting the model",
|
||||
default=0.08,
|
||||
)
|
||||
convert_material_nodes: bpy.props.BoolProperty(
|
||||
name="Convert Material Nodes",
|
||||
default=True,
|
||||
)
|
||||
middle_joint_bones_lock: bpy.props.BoolProperty(
|
||||
name="Middle Joint Bones Lock",
|
||||
description="Lock specific bones for backward compatibility.",
|
||||
default=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
return obj and obj.type == "ARMATURE" and obj.mode != "EDIT"
|
||||
|
||||
def invoke(self, context, event):
|
||||
vm = context.window_manager
|
||||
return vm.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context):
|
||||
# TODO convert some basic MMD properties
|
||||
armature_object = context.active_object
|
||||
scale = self.scale
|
||||
model_name = "New MMD Model"
|
||||
|
||||
root_object = FnModel.find_root_object(armature_object)
|
||||
if root_object is None or root_object != armature_object.parent:
|
||||
Model.create(model_name, model_name, scale, armature_object=armature_object)
|
||||
|
||||
self.__attach_meshes_to(armature_object, FnContext.get_scene_objects(context))
|
||||
self.__configure_rig(context, Model(armature_object.parent))
|
||||
return {"FINISHED"}
|
||||
|
||||
def __attach_meshes_to(self, armature_object: bpy.types.Object, objects: bpy.types.SceneObjects):
|
||||
def __is_child_of_armature(mesh):
|
||||
if mesh.parent is None:
|
||||
return False
|
||||
return mesh.parent == armature_object or __is_child_of_armature(mesh.parent)
|
||||
|
||||
def __is_using_armature(mesh):
|
||||
for m in mesh.modifiers:
|
||||
if m.type == "ARMATURE" and m.object == armature_object:
|
||||
return True
|
||||
return False
|
||||
|
||||
def __get_root(mesh):
|
||||
if mesh.parent is None:
|
||||
return mesh
|
||||
return __get_root(mesh.parent)
|
||||
|
||||
for x in objects:
|
||||
if __is_using_armature(x) and not __is_child_of_armature(x):
|
||||
x_root = __get_root(x)
|
||||
m = x_root.matrix_world
|
||||
x_root.parent_type = "OBJECT"
|
||||
x_root.parent = armature_object
|
||||
x_root.matrix_world = m
|
||||
|
||||
def __configure_rig(self, context: bpy.types.Context, mmd_model: Model):
|
||||
root_object = mmd_model.rootObject()
|
||||
armature_object = mmd_model.armature()
|
||||
mesh_objects = tuple(mmd_model.meshes())
|
||||
|
||||
mmd_model.loadMorphs()
|
||||
|
||||
if self.middle_joint_bones_lock:
|
||||
vertex_groups = {g.name for mesh in mesh_objects for g in mesh.vertex_groups}
|
||||
for pose_bone in armature_object.pose.bones:
|
||||
if not pose_bone.parent:
|
||||
continue
|
||||
if not pose_bone.bone.use_connect and pose_bone.name not in vertex_groups:
|
||||
continue
|
||||
pose_bone.lock_location = (True, True, True)
|
||||
|
||||
from ..core.material import FnMaterial
|
||||
|
||||
FnMaterial.set_nodes_are_readonly(not self.convert_material_nodes)
|
||||
try:
|
||||
for m in (x for mesh in mesh_objects for x in mesh.data.materials if x):
|
||||
FnMaterial.convert_to_mmd_material(m, context)
|
||||
mmd_material = m.mmd_material
|
||||
if self.ambient_color_source == "MIRROR" and hasattr(m, "mirror_color"):
|
||||
mmd_material.ambient_color = m.mirror_color
|
||||
else:
|
||||
mmd_material.ambient_color = [0.5 * c for c in mmd_material.diffuse_color]
|
||||
|
||||
if hasattr(m, "line_color"): # freestyle line color
|
||||
line_color = list(m.line_color)
|
||||
mmd_material.enabled_toon_edge = line_color[3] >= self.edge_threshold
|
||||
mmd_material.edge_color = line_color[:3] + [max(line_color[3], self.edge_alpha_min)]
|
||||
finally:
|
||||
FnMaterial.set_nodes_are_readonly(False)
|
||||
from .display_item import DisplayItemQuickSetup
|
||||
|
||||
FnBone.sync_display_item_frames_from_bone_collections(armature_object)
|
||||
mmd_model.initialDisplayFrames(reset=False) # ensure default frames
|
||||
DisplayItemQuickSetup.load_facial_items(root_object.mmd_root)
|
||||
root_object.mmd_root.active_display_item_frame = 0
|
||||
|
||||
|
||||
class ResetObjectVisibility(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.reset_object_visibility"
|
||||
bl_label = "Reset Object Visivility"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context):
|
||||
active_object: bpy.types.Object = context.active_object
|
||||
return FnModel.find_root_object(active_object) is not None
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
active_object: bpy.types.Object = context.active_object
|
||||
mmd_root_object = FnModel.find_root_object(active_object)
|
||||
assert mmd_root_object is not None
|
||||
mmd_root = mmd_root_object.mmd_root
|
||||
|
||||
mmd_root_object.hide_set(False)
|
||||
|
||||
rigid_group_object = FnModel.find_rigid_group_object(mmd_root_object)
|
||||
if rigid_group_object:
|
||||
rigid_group_object.hide_set(True)
|
||||
|
||||
joint_group_object = FnModel.find_joint_group_object(mmd_root_object)
|
||||
if joint_group_object:
|
||||
joint_group_object.hide_set(True)
|
||||
|
||||
temporary_group_object = FnModel.find_temporary_group_object(mmd_root_object)
|
||||
if temporary_group_object:
|
||||
temporary_group_object.hide_set(True)
|
||||
|
||||
mmd_root.show_meshes = True
|
||||
mmd_root.show_armature = True
|
||||
mmd_root.show_temporary_objects = False
|
||||
mmd_root.show_rigid_bodies = False
|
||||
mmd_root.show_names_of_rigid_bodies = False
|
||||
mmd_root.show_joints = False
|
||||
mmd_root.show_names_of_joints = False
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AssembleAll(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.assemble_all"
|
||||
bl_label = "Assemble All"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
active_object = context.active_object
|
||||
root_object = FnModel.find_root_object(active_object)
|
||||
assert root_object is not None
|
||||
|
||||
with FnContext.temp_override_active_layer_collection(context, root_object) as context:
|
||||
rig = Model(root_object)
|
||||
MigrationFnBone.fix_mmd_ik_limit_override(rig.armature())
|
||||
FnBone.apply_additional_transformation(rig.armature())
|
||||
rig.build()
|
||||
rig.morph_slider.bind()
|
||||
|
||||
with context.temp_override(selected_objects=[active_object]):
|
||||
bpy.ops.mmd_tools.sdef_bind()
|
||||
root_object.mmd_root.use_property_driver = True
|
||||
|
||||
FnContext.set_active_object(context, active_object)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisassembleAll(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.disassemble_all"
|
||||
bl_label = "Disassemble All"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
active_object = context.active_object
|
||||
root_object = FnModel.find_root_object(active_object)
|
||||
assert root_object is not None
|
||||
|
||||
with FnContext.temp_override_active_layer_collection(context, root_object) as context:
|
||||
root_object.mmd_root.use_property_driver = False
|
||||
with context.temp_override(selected_objects=[active_object]):
|
||||
bpy.ops.mmd_tools.sdef_unbind()
|
||||
|
||||
rig = Model(root_object)
|
||||
rig.morph_slider.unbind()
|
||||
rig.clean()
|
||||
FnBone.clean_additional_transformation(rig.armature())
|
||||
|
||||
FnContext.set_active_object(context, active_object)
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,313 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 MMD Tools authors
|
||||
# This file was originally part of the MMD Tools add-on for Blender
|
||||
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
|
||||
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
import itertools
|
||||
from operator import itemgetter
|
||||
from typing import Dict, List, Optional, Set
|
||||
|
||||
import bmesh
|
||||
import bpy
|
||||
|
||||
from ..bpyutils import FnContext
|
||||
from ..core.model import FnModel, Model
|
||||
|
||||
|
||||
class MessageException(Exception):
|
||||
"""Class for error with message."""
|
||||
|
||||
|
||||
class ModelJoinByBonesOperator(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.model_join_by_bones"
|
||||
bl_label = "Model Join by Bones"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
join_type: bpy.props.EnumProperty(
|
||||
name="Join Type",
|
||||
items=[
|
||||
("CONNECTED", "Connected", ""),
|
||||
("OFFSET", "Keep Offset", ""),
|
||||
],
|
||||
default="OFFSET",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context):
|
||||
active_object: Optional[bpy.types.Object] = context.active_object
|
||||
|
||||
if context.mode != "POSE":
|
||||
return False
|
||||
|
||||
if active_object is None:
|
||||
return False
|
||||
|
||||
if active_object.type != "ARMATURE":
|
||||
return False
|
||||
|
||||
if len(list(filter(lambda o: o.type == "ARMATURE", context.selected_objects))) < 2:
|
||||
return False
|
||||
|
||||
return len(context.selected_pose_bones) > 0
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
try:
|
||||
self.join(context)
|
||||
except MessageException as ex:
|
||||
self.report(type={"ERROR"}, message=str(ex))
|
||||
return {"CANCELLED"}
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def join(self, context: bpy.types.Context):
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
parent_root_object = FnModel.find_root_object(context.active_object)
|
||||
child_root_objects = {FnModel.find_root_object(o) for o in context.selected_objects}
|
||||
child_root_objects.remove(parent_root_object)
|
||||
|
||||
if parent_root_object is None or len(child_root_objects) == 0:
|
||||
raise MessageException("No MMD Models selected")
|
||||
|
||||
with FnContext.temp_override_active_layer_collection(context, parent_root_object):
|
||||
FnModel.join_models(parent_root_object, child_root_objects)
|
||||
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
bpy.ops.armature.parent_set(type="OFFSET")
|
||||
|
||||
# Connect child bones
|
||||
if self.join_type == "CONNECTED":
|
||||
parent_edit_bone: bpy.types.EditBone = context.active_bone
|
||||
child_edit_bones: Set[bpy.types.EditBone] = set(context.selected_bones)
|
||||
child_edit_bones.remove(parent_edit_bone)
|
||||
|
||||
child_edit_bone: bpy.types.EditBone
|
||||
for child_edit_bone in child_edit_bones:
|
||||
child_edit_bone.use_connect = True
|
||||
|
||||
bpy.ops.object.mode_set(mode="POSE")
|
||||
|
||||
|
||||
class ModelSeparateByBonesOperator(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.model_separate_by_bones"
|
||||
bl_label = "Model Separate by Bones"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
separate_armature: bpy.props.BoolProperty(name="Separate Armature", default=True)
|
||||
include_descendant_bones: bpy.props.BoolProperty(name="Include Descendant Bones", default=True)
|
||||
weight_threshold: bpy.props.FloatProperty(name="Weight Threshold", default=0.001, min=0.0, max=1.0, precision=4, subtype="FACTOR")
|
||||
boundary_joint_owner: bpy.props.EnumProperty(
|
||||
name="Boundary Joint Owner",
|
||||
items=[
|
||||
("SOURCE", "Source Model", ""),
|
||||
("DESTINATION", "Destination Model", ""),
|
||||
],
|
||||
default="DESTINATION",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context):
|
||||
active_object: Optional[bpy.types.Object] = context.active_object
|
||||
|
||||
if context.mode != "POSE":
|
||||
return False
|
||||
|
||||
if active_object is None:
|
||||
return False
|
||||
|
||||
if active_object.type != "ARMATURE":
|
||||
return False
|
||||
|
||||
if FnModel.find_root_object(active_object) is None:
|
||||
return False
|
||||
|
||||
return len(context.selected_pose_bones) > 0
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
try:
|
||||
self.separate(context)
|
||||
except MessageException as ex:
|
||||
self.report(type={"ERROR"}, message=str(ex))
|
||||
return {"CANCELLED"}
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def separate(self, context: bpy.types.Context):
|
||||
weight_threshold: float = self.weight_threshold
|
||||
mmd_scale = 0.08
|
||||
|
||||
target_armature_object: bpy.types.Object = context.active_object
|
||||
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
root_bones: Set[bpy.types.EditBone] = set(context.selected_bones)
|
||||
|
||||
if self.include_descendant_bones:
|
||||
for edit_bone in root_bones:
|
||||
with context.temp_override(active_bone=edit_bone):
|
||||
bpy.ops.armature.select_similar(type="CHILDREN", threshold=0.1)
|
||||
|
||||
separate_bones: Dict[str, bpy.types.EditBone] = {b.name: b for b in context.selected_bones}
|
||||
deform_bones: Dict[str, bpy.types.EditBone] = {b.name: b for b in target_armature_object.data.edit_bones if b.use_deform}
|
||||
|
||||
mmd_root_object: bpy.types.Object = FnModel.find_root_object(context.active_object)
|
||||
mmd_model = Model(mmd_root_object)
|
||||
mmd_model_mesh_objects: List[bpy.types.Object] = list(mmd_model.meshes())
|
||||
|
||||
mmd_model_mesh_objects = list(self.select_weighted_vertices(mmd_model_mesh_objects, separate_bones, deform_bones, weight_threshold).keys())
|
||||
|
||||
# separate armature bones
|
||||
separate_armature_object: Optional[bpy.types.Object]
|
||||
if self.separate_armature:
|
||||
target_armature_object.select_set(True)
|
||||
bpy.ops.armature.separate()
|
||||
separate_armature_object = next(iter([a for a in context.selected_objects if a != target_armature_object]), None)
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
# collect separate rigid bodies
|
||||
separate_rigid_bodies: Set[bpy.types.Object] = {rigid_body_object for rigid_body_object in mmd_model.rigidBodies() if rigid_body_object.mmd_rigid.bone in separate_bones}
|
||||
|
||||
boundary_joint_owner_condition = any if self.boundary_joint_owner == "DESTINATION" else all
|
||||
|
||||
# collect separate joints
|
||||
separate_joints: Set[bpy.types.Object] = {
|
||||
joint_object
|
||||
for joint_object in mmd_model.joints()
|
||||
if boundary_joint_owner_condition(
|
||||
[
|
||||
joint_object.rigid_body_constraint.object1 in separate_rigid_bodies,
|
||||
joint_object.rigid_body_constraint.object2 in separate_rigid_bodies,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
separate_mesh_objects: Set[bpy.types.Object]
|
||||
model2separate_mesh_objects: Dict[bpy.types.Object, bpy.types.Object]
|
||||
if len(mmd_model_mesh_objects) == 0:
|
||||
separate_mesh_objects = set()
|
||||
model2separate_mesh_objects = dict()
|
||||
else:
|
||||
# select meshes
|
||||
obj: bpy.types.Object
|
||||
for obj in context.view_layer.objects:
|
||||
obj.select_set(obj in mmd_model_mesh_objects)
|
||||
context.view_layer.objects.active = mmd_model_mesh_objects[0]
|
||||
|
||||
# separate mesh by selected vertices
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
bpy.ops.mesh.separate(type="SELECTED")
|
||||
separate_mesh_objects: List[bpy.types.Object] = [m for m in context.selected_objects if m.type == "MESH" and m not in mmd_model_mesh_objects]
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
model2separate_mesh_objects = dict(zip(mmd_model_mesh_objects, separate_mesh_objects))
|
||||
|
||||
separate_model: Model = Model.create(mmd_root_object.mmd_root.name, mmd_root_object.mmd_root.name_e, mmd_scale, add_root_bone=False)
|
||||
|
||||
separate_model.initialDisplayFrames()
|
||||
separate_root_object = separate_model.rootObject()
|
||||
separate_root_object.matrix_world = mmd_root_object.matrix_world
|
||||
separate_model_armature_object = separate_model.armature()
|
||||
|
||||
if self.separate_armature:
|
||||
with context.temp_override(
|
||||
active_object=separate_model_armature_object,
|
||||
selected_editable_objects=[separate_model_armature_object, separate_armature_object],
|
||||
):
|
||||
bpy.ops.object.join()
|
||||
|
||||
# add mesh
|
||||
with context.temp_override(
|
||||
object=separate_model_armature_object,
|
||||
selected_editable_objects=[separate_model_armature_object, *separate_mesh_objects],
|
||||
):
|
||||
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
|
||||
|
||||
# replace mesh armature modifier.object
|
||||
for separate_mesh in separate_mesh_objects:
|
||||
armature_modifier: Optional[bpy.types.ArmatureModifier] = next(iter([m for m in separate_mesh.modifiers if m.type == "ARMATURE"]), None)
|
||||
if armature_modifier is None:
|
||||
armature_modifier: bpy.types.ArmatureModifier = separate_mesh.modifiers.new("mmd_bone_order_override", "ARMATURE")
|
||||
|
||||
armature_modifier.object = separate_model_armature_object
|
||||
|
||||
with context.temp_override(
|
||||
object=separate_model.rigidGroupObject(),
|
||||
selected_editable_objects=[separate_model.rigidGroupObject(), *separate_rigid_bodies],
|
||||
):
|
||||
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
|
||||
|
||||
with context.temp_override(
|
||||
object=separate_model.jointGroupObject(),
|
||||
selected_editable_objects=[separate_model.jointGroupObject(), *separate_joints],
|
||||
):
|
||||
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
|
||||
|
||||
# move separate objects to new collection
|
||||
mmd_layer_collection = FnContext.find_user_layer_collection_by_object(context, mmd_root_object)
|
||||
assert mmd_layer_collection is not None
|
||||
|
||||
separate_layer_collection = FnContext.find_user_layer_collection_by_object(context, separate_root_object)
|
||||
assert separate_layer_collection is not None
|
||||
|
||||
if mmd_layer_collection.name != separate_layer_collection.name:
|
||||
for separate_object in itertools.chain(separate_mesh_objects, separate_rigid_bodies, separate_joints):
|
||||
separate_layer_collection.collection.objects.link(separate_object)
|
||||
mmd_layer_collection.collection.objects.unlink(separate_object)
|
||||
|
||||
FnModel.copy_mmd_root(
|
||||
separate_root_object,
|
||||
mmd_root_object,
|
||||
overwrite=True,
|
||||
replace_name2values={
|
||||
# replace related_mesh property values
|
||||
"related_mesh": {m.data.name: s.data.name for m, s in model2separate_mesh_objects.items()}
|
||||
},
|
||||
)
|
||||
|
||||
def select_weighted_vertices(self, mmd_model_mesh_objects: List[bpy.types.Object], separate_bones: Dict[str, bpy.types.EditBone], deform_bones: Dict[str, bpy.types.EditBone], weight_threshold: float) -> Dict[bpy.types.Object, int]:
|
||||
mesh2selected_vertex_count: Dict[bpy.types.Object, int] = dict()
|
||||
target_bmesh: bmesh.types.BMesh = bmesh.new()
|
||||
for mesh_object in mmd_model_mesh_objects:
|
||||
vertex_groups: bpy.types.VertexGroups = mesh_object.vertex_groups
|
||||
|
||||
mesh: bpy.types.Mesh = mesh_object.data
|
||||
target_bmesh.from_mesh(mesh, face_normals=False)
|
||||
target_bmesh.select_mode |= {"VERT"}
|
||||
deform_layer = target_bmesh.verts.layers.deform.verify()
|
||||
|
||||
selected_vertex_count = 0
|
||||
vert: bmesh.types.BMVert
|
||||
for vert in target_bmesh.verts:
|
||||
vert.select_set(False)
|
||||
|
||||
# Find the largest weight vertex group
|
||||
weights = [(group_index, weight) for group_index, weight in vert[deform_layer].items() if vertex_groups[group_index].name in deform_bones]
|
||||
|
||||
weights.sort(key=lambda i: vertex_groups[i[0]].name in separate_bones, reverse=True)
|
||||
weights.sort(key=itemgetter(1), reverse=True)
|
||||
group_index, weight = next(iter(weights), (0, -1))
|
||||
|
||||
if weight < weight_threshold:
|
||||
continue
|
||||
|
||||
if vertex_groups[group_index].name not in separate_bones:
|
||||
continue
|
||||
|
||||
selected_vertex_count += 1
|
||||
vert.select_set(True)
|
||||
|
||||
if selected_vertex_count > 0:
|
||||
mesh2selected_vertex_count[mesh_object] = selected_vertex_count
|
||||
target_bmesh.select_flush_mode()
|
||||
target_bmesh.to_mesh(mesh)
|
||||
|
||||
target_bmesh.clear()
|
||||
|
||||
return mesh2selected_vertex_count
|
||||
@@ -0,0 +1,776 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 MMD Tools authors
|
||||
# This file was originally part of the MMD Tools add-on for Blender
|
||||
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
|
||||
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
from typing import Optional, cast
|
||||
|
||||
import bpy
|
||||
from mathutils import Quaternion, Vector
|
||||
|
||||
from ..core.model import FnModel
|
||||
from .. import bpyutils, utils
|
||||
from ..core.exceptions import MaterialNotFoundError
|
||||
from ..core.material import FnMaterial
|
||||
from ..core.morph import FnMorph
|
||||
from ..utils import ItemMoveOp, ItemOp
|
||||
|
||||
|
||||
# Util functions
|
||||
def divide_vector_components(vec1, vec2):
|
||||
if len(vec1) != len(vec2):
|
||||
raise ValueError("Vectors should have the same number of components")
|
||||
result = []
|
||||
for v1, v2 in zip(vec1, vec2):
|
||||
if v2 == 0:
|
||||
if v1 == 0:
|
||||
v2 = 1 # If we have a 0/0 case we change the divisor to 1
|
||||
else:
|
||||
raise ZeroDivisionError("Invalid Input: a non-zero value can't be divided by zero")
|
||||
result.append(v1 / v2)
|
||||
return result
|
||||
|
||||
|
||||
def multiply_vector_components(vec1, vec2):
|
||||
if len(vec1) != len(vec2):
|
||||
raise ValueError("Vectors should have the same number of components")
|
||||
result = []
|
||||
for v1, v2 in zip(vec1, vec2):
|
||||
result.append(v1 * v2)
|
||||
return result
|
||||
|
||||
|
||||
def special_division(n1, n2):
|
||||
"""This function returns 0 in case of 0/0. If non-zero divided by zero case is found, an Exception is raised"""
|
||||
if n2 == 0:
|
||||
if n1 == 0:
|
||||
n2 = 1
|
||||
else:
|
||||
raise ZeroDivisionError("Invalid Input: a non-zero value can't be divided by zero")
|
||||
return n1 / n2
|
||||
|
||||
|
||||
class AddMorph(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.morph_add"
|
||||
bl_label = "Add Morph"
|
||||
bl_description = "Add a morph item to active morph list"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
morph_type = mmd_root.active_morph_type
|
||||
morphs = getattr(mmd_root, morph_type)
|
||||
morph, mmd_root.active_morph = ItemOp.add_after(morphs, mmd_root.active_morph)
|
||||
morph.name = "New Morph"
|
||||
if morph_type.startswith("uv"):
|
||||
morph.data_type = "VERTEX_GROUP"
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemoveMorph(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.morph_remove"
|
||||
bl_label = "Remove Morph"
|
||||
bl_description = "Remove morph item(s) from the list"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
all: bpy.props.BoolProperty(
|
||||
name="All",
|
||||
description="Delete all morph items",
|
||||
default=False,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
|
||||
morph_type = mmd_root.active_morph_type
|
||||
if morph_type.startswith("material"):
|
||||
bpy.ops.mmd_tools.clear_temp_materials()
|
||||
elif morph_type.startswith("uv"):
|
||||
bpy.ops.mmd_tools.clear_uv_morph_view()
|
||||
|
||||
morphs = getattr(mmd_root, morph_type)
|
||||
if self.all:
|
||||
morphs.clear()
|
||||
mmd_root.active_morph = 0
|
||||
else:
|
||||
morphs.remove(mmd_root.active_morph)
|
||||
mmd_root.active_morph = max(0, mmd_root.active_morph - 1)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class MoveMorph(bpy.types.Operator, ItemMoveOp):
|
||||
bl_idname = "mmd_tools.morph_move"
|
||||
bl_label = "Move Morph"
|
||||
bl_description = "Move active morph item up/down in the list"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
mmd_root.active_morph = self.move(
|
||||
getattr(mmd_root, mmd_root.active_morph_type),
|
||||
mmd_root.active_morph,
|
||||
self.type,
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CopyMorph(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.morph_copy"
|
||||
bl_label = "Copy Morph"
|
||||
bl_description = "Make a copy of active morph in the list"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
mmd_root = root.mmd_root
|
||||
|
||||
morph_type = mmd_root.active_morph_type
|
||||
morphs = getattr(mmd_root, morph_type)
|
||||
morph = ItemOp.get_by_index(morphs, mmd_root.active_morph)
|
||||
if morph is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
name_orig, name_tmp = morph.name, "_tmp%s" % str(morph.as_pointer())
|
||||
|
||||
if morph_type.startswith("vertex"):
|
||||
for obj in FnModel.iterate_mesh_objects(root):
|
||||
FnMorph.copy_shape_key(obj, name_orig, name_tmp)
|
||||
|
||||
elif morph_type.startswith("uv"):
|
||||
if morph.data_type == "VERTEX_GROUP":
|
||||
for obj in FnModel.iterate_mesh_objects(root):
|
||||
FnMorph.copy_uv_morph_vertex_groups(obj, name_orig, name_tmp)
|
||||
|
||||
morph_new, mmd_root.active_morph = ItemOp.add_after(morphs, mmd_root.active_morph)
|
||||
for k, v in morph.items():
|
||||
morph_new[k] = v if k != "name" else name_tmp
|
||||
morph_new.name = name_orig + "_copy" # trigger name check
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class OverwriteBoneMorphsFromActionPose(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.morph_overwrite_from_active_action_pose"
|
||||
bl_label = "Overwrite Bone Morphs from active Action Pose"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
root = FnModel.find_root_object(context.active_object)
|
||||
if root is None:
|
||||
return False
|
||||
|
||||
return root.mmd_root.active_morph_type == "bone_morphs"
|
||||
|
||||
def execute(self, context):
|
||||
root = FnModel.find_root_object(context.active_object)
|
||||
FnMorph.overwrite_bone_morphs_from_action_pose(FnModel.find_armature_object(root))
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddMorphOffset(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.morph_offset_add"
|
||||
bl_label = "Add Morph Offset"
|
||||
bl_description = "Add a morph offset item to the list"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
morph_type = mmd_root.active_morph_type
|
||||
morph = ItemOp.get_by_index(getattr(mmd_root, morph_type), mmd_root.active_morph)
|
||||
if morph is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
item, morph.active_data = ItemOp.add_after(morph.data, morph.active_data)
|
||||
|
||||
if morph_type.startswith("material"):
|
||||
if obj.type == "MESH" and obj.mmd_type == "NONE":
|
||||
item.related_mesh = obj.data.name
|
||||
active_material = obj.active_material
|
||||
if active_material and "_temp" not in active_material.name:
|
||||
item.material = active_material.name
|
||||
|
||||
elif morph_type.startswith("bone"):
|
||||
pose_bone = context.active_pose_bone
|
||||
if pose_bone:
|
||||
item.bone = pose_bone.name
|
||||
item.location = pose_bone.location
|
||||
item.rotation = pose_bone.rotation_quaternion
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemoveMorphOffset(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.morph_offset_remove"
|
||||
bl_label = "Remove Morph Offset"
|
||||
bl_description = "Remove morph offset item(s) from the list"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
all: bpy.props.BoolProperty(
|
||||
name="All",
|
||||
description="Delete all morph offset items",
|
||||
default=False,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
mmd_root = root.mmd_root
|
||||
morph_type = mmd_root.active_morph_type
|
||||
morph = ItemOp.get_by_index(getattr(mmd_root, morph_type), mmd_root.active_morph)
|
||||
if morph is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
if morph_type.startswith("material"):
|
||||
bpy.ops.mmd_tools.clear_temp_materials()
|
||||
|
||||
if self.all:
|
||||
if morph_type.startswith("vertex"):
|
||||
for obj in FnModel.iterate_mesh_objects(root):
|
||||
FnMorph.remove_shape_key(obj, morph.name)
|
||||
return {"FINISHED"}
|
||||
elif morph_type.startswith("uv"):
|
||||
if morph.data_type == "VERTEX_GROUP":
|
||||
for obj in FnModel.iterate_mesh_objects(root):
|
||||
FnMorph.store_uv_morph_data(obj, morph)
|
||||
return {"FINISHED"}
|
||||
morph.data.clear()
|
||||
morph.active_data = 0
|
||||
else:
|
||||
morph.data.remove(morph.active_data)
|
||||
morph.active_data = max(0, morph.active_data - 1)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class InitMaterialOffset(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.material_morph_offset_init"
|
||||
bl_label = "Init Material Offset"
|
||||
bl_description = "Set all offset values to target value"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
target_value: bpy.props.FloatProperty(
|
||||
name="Target Value",
|
||||
description="Target value",
|
||||
default=0,
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
morph = mmd_root.material_morphs[mmd_root.active_morph]
|
||||
mat_data = morph.data[morph.active_data]
|
||||
|
||||
val = self.target_value
|
||||
mat_data.diffuse_color = mat_data.edge_color = (val,) * 4
|
||||
mat_data.specular_color = mat_data.ambient_color = (val,) * 3
|
||||
mat_data.shininess = mat_data.edge_weight = val
|
||||
mat_data.texture_factor = mat_data.toon_texture_factor = mat_data.sphere_texture_factor = (val,) * 4
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ApplyMaterialOffset(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.apply_material_morph_offset"
|
||||
bl_label = "Apply Material Offset"
|
||||
bl_description = "Calculates the offsets and apply them, then the temporary material is removed"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
morph = mmd_root.material_morphs[mmd_root.active_morph]
|
||||
mat_data = morph.data[morph.active_data]
|
||||
|
||||
if not mat_data.related_mesh:
|
||||
self.report({"ERROR"}, "You need to choose a Related Mesh first")
|
||||
return {"CANCELLED"}
|
||||
meshObj = FnModel.find_mesh_object_by_name(morph.id_data, mat_data.related_mesh)
|
||||
if meshObj is None:
|
||||
self.report({"ERROR"}, "The model mesh can't be found")
|
||||
return {"CANCELLED"}
|
||||
try:
|
||||
work_mat_name = mat_data.material + "_temp"
|
||||
work_mat, base_mat = FnMaterial.swap_materials(meshObj, work_mat_name, mat_data.material)
|
||||
except MaterialNotFoundError:
|
||||
self.report({"ERROR"}, "Material not found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
base_mmd_mat = base_mat.mmd_material
|
||||
work_mmd_mat = work_mat.mmd_material
|
||||
|
||||
if mat_data.offset_type == "MULT":
|
||||
try:
|
||||
diffuse_offset = divide_vector_components(work_mmd_mat.diffuse_color, base_mmd_mat.diffuse_color) + [special_division(work_mmd_mat.alpha, base_mmd_mat.alpha)]
|
||||
specular_offset = divide_vector_components(work_mmd_mat.specular_color, base_mmd_mat.specular_color)
|
||||
edge_offset = divide_vector_components(work_mmd_mat.edge_color, base_mmd_mat.edge_color)
|
||||
mat_data.diffuse_color = diffuse_offset
|
||||
mat_data.specular_color = specular_offset
|
||||
mat_data.shininess = special_division(work_mmd_mat.shininess, base_mmd_mat.shininess)
|
||||
mat_data.ambient_color = divide_vector_components(work_mmd_mat.ambient_color, base_mmd_mat.ambient_color)
|
||||
mat_data.edge_color = edge_offset
|
||||
mat_data.edge_weight = special_division(work_mmd_mat.edge_weight, base_mmd_mat.edge_weight)
|
||||
|
||||
except ZeroDivisionError:
|
||||
mat_data.offset_type = "ADD" # If there is any 0 division we automatically switch it to type ADD
|
||||
except ValueError:
|
||||
self.report({"ERROR"}, "An unexpected error happened")
|
||||
# We should stop on our tracks and re-raise the exception
|
||||
raise
|
||||
|
||||
if mat_data.offset_type == "ADD":
|
||||
diffuse_offset = list(work_mmd_mat.diffuse_color - base_mmd_mat.diffuse_color) + [work_mmd_mat.alpha - base_mmd_mat.alpha]
|
||||
specular_offset = list(work_mmd_mat.specular_color - base_mmd_mat.specular_color)
|
||||
edge_offset = Vector(work_mmd_mat.edge_color) - Vector(base_mmd_mat.edge_color)
|
||||
mat_data.diffuse_color = diffuse_offset
|
||||
mat_data.specular_color = specular_offset
|
||||
mat_data.shininess = work_mmd_mat.shininess - base_mmd_mat.shininess
|
||||
mat_data.ambient_color = work_mmd_mat.ambient_color - base_mmd_mat.ambient_color
|
||||
mat_data.edge_color = list(edge_offset)
|
||||
mat_data.edge_weight = work_mmd_mat.edge_weight - base_mmd_mat.edge_weight
|
||||
|
||||
FnMaterial.clean_materials(meshObj, can_remove=lambda m: m == work_mat)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CreateWorkMaterial(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.create_work_material"
|
||||
bl_label = "Create Work Material"
|
||||
bl_description = "Creates a temporary material to edit this offset"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
morph = mmd_root.material_morphs[mmd_root.active_morph]
|
||||
mat_data = morph.data[morph.active_data]
|
||||
|
||||
if not mat_data.related_mesh:
|
||||
self.report({"ERROR"}, "You need to choose a Related Mesh first")
|
||||
return {"CANCELLED"}
|
||||
meshObj = FnModel.find_mesh_object_by_name(morph.id_data, mat_data.related_mesh)
|
||||
if meshObj is None:
|
||||
self.report({"ERROR"}, "The model mesh can't be found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
base_mat = meshObj.data.materials.get(mat_data.material, None)
|
||||
if base_mat is None:
|
||||
self.report({"ERROR"}, 'Material "%s" not found' % mat_data.material)
|
||||
return {"CANCELLED"}
|
||||
|
||||
work_mat_name = base_mat.name + "_temp"
|
||||
if work_mat_name in bpy.data.materials:
|
||||
self.report({"ERROR"}, 'Temporary material "%s" is in use' % work_mat_name)
|
||||
return {"CANCELLED"}
|
||||
|
||||
work_mat = base_mat.copy()
|
||||
work_mat.name = work_mat_name
|
||||
meshObj.data.materials.append(work_mat)
|
||||
FnMaterial.swap_materials(meshObj, base_mat.name, work_mat.name)
|
||||
base_mmd_mat = base_mat.mmd_material
|
||||
work_mmd_mat = work_mat.mmd_material
|
||||
work_mmd_mat.material_id = -1
|
||||
|
||||
# Apply the offsets
|
||||
if mat_data.offset_type == "MULT":
|
||||
diffuse_offset = multiply_vector_components(base_mmd_mat.diffuse_color, mat_data.diffuse_color[0:3])
|
||||
specular_offset = multiply_vector_components(base_mmd_mat.specular_color, mat_data.specular_color)
|
||||
edge_offset = multiply_vector_components(base_mmd_mat.edge_color, mat_data.edge_color)
|
||||
ambient_offset = multiply_vector_components(base_mmd_mat.ambient_color, mat_data.ambient_color)
|
||||
work_mmd_mat.diffuse_color = diffuse_offset
|
||||
work_mmd_mat.alpha *= mat_data.diffuse_color[3]
|
||||
work_mmd_mat.specular_color = specular_offset
|
||||
work_mmd_mat.shininess *= mat_data.shininess
|
||||
work_mmd_mat.ambient_color = ambient_offset
|
||||
work_mmd_mat.edge_color = edge_offset
|
||||
work_mmd_mat.edge_weight *= mat_data.edge_weight
|
||||
elif mat_data.offset_type == "ADD":
|
||||
diffuse_offset = Vector(base_mmd_mat.diffuse_color) + Vector(mat_data.diffuse_color[0:3])
|
||||
specular_offset = Vector(base_mmd_mat.specular_color) + Vector(mat_data.specular_color)
|
||||
edge_offset = Vector(base_mmd_mat.edge_color) + Vector(mat_data.edge_color)
|
||||
ambient_offset = Vector(base_mmd_mat.ambient_color) + Vector(mat_data.ambient_color)
|
||||
work_mmd_mat.diffuse_color = list(diffuse_offset)
|
||||
work_mmd_mat.alpha += mat_data.diffuse_color[3]
|
||||
work_mmd_mat.specular_color = list(specular_offset)
|
||||
work_mmd_mat.shininess += mat_data.shininess
|
||||
work_mmd_mat.ambient_color = list(ambient_offset)
|
||||
work_mmd_mat.edge_color = list(edge_offset)
|
||||
work_mmd_mat.edge_weight += mat_data.edge_weight
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ClearTempMaterials(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.clear_temp_materials"
|
||||
bl_label = "Clear Temp Materials"
|
||||
bl_description = "Clears all the temporary materials"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
for meshObj in FnModel.iterate_mesh_objects(root):
|
||||
|
||||
def __pre_remove(m):
|
||||
if m and "_temp" in m.name:
|
||||
base_mat_name = m.name.split("_temp")[0]
|
||||
try:
|
||||
FnMaterial.swap_materials(meshObj, m.name, base_mat_name)
|
||||
return True
|
||||
except MaterialNotFoundError:
|
||||
self.report({"WARNING"}, "Base material for %s was not found" % m.name)
|
||||
return False
|
||||
|
||||
FnMaterial.clean_materials(meshObj, can_remove=__pre_remove)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ViewBoneMorph(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.view_bone_morph"
|
||||
bl_label = "View Bone Morph"
|
||||
bl_description = "View the result of active bone morph"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
mmd_root = root.mmd_root
|
||||
armature = FnModel.find_armature_object(root)
|
||||
utils.selectSingleBone(context, armature, None, True)
|
||||
morph = mmd_root.bone_morphs[mmd_root.active_morph]
|
||||
for morph_data in morph.data:
|
||||
p_bone: Optional[bpy.types.PoseBone] = armature.pose.bones.get(morph_data.bone, None)
|
||||
if p_bone:
|
||||
p_bone.bone.select = True
|
||||
mtx = (p_bone.matrix_basis.to_3x3() @ Quaternion(*morph_data.rotation.to_axis_angle()).to_matrix()).to_4x4()
|
||||
mtx.translation = p_bone.location + morph_data.location
|
||||
p_bone.matrix_basis = mtx
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ClearBoneMorphView(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.clear_bone_morph_view"
|
||||
bl_label = "Clear Bone Morph View"
|
||||
bl_description = "Reset transforms of all bones to their default values"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
armature = FnModel.find_armature_object(root)
|
||||
for p_bone in armature.pose.bones:
|
||||
p_bone.matrix_basis.identity()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ApplyBoneMorph(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.apply_bone_morph"
|
||||
bl_label = "Apply Bone Morph"
|
||||
bl_description = "Apply current pose to active bone morph"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
armature = FnModel.find_armature_object(root)
|
||||
mmd_root = root.mmd_root
|
||||
morph = mmd_root.bone_morphs[mmd_root.active_morph]
|
||||
morph.data.clear()
|
||||
morph.active_data = 0
|
||||
for p_bone in armature.pose.bones:
|
||||
if p_bone.location.length > 0 or p_bone.matrix_basis.decompose()[1].angle > 0:
|
||||
item = morph.data.add()
|
||||
item.bone = p_bone.name
|
||||
item.location = p_bone.location
|
||||
item.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion()
|
||||
p_bone.bone.select = True
|
||||
else:
|
||||
p_bone.bone.select = False
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectRelatedBone(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.select_bone_morph_offset_bone"
|
||||
bl_label = "Select Related Bone"
|
||||
bl_description = "Select the bone assigned to this offset in the armature"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
mmd_root = root.mmd_root
|
||||
armature = FnModel.find_armature_object(root)
|
||||
morph = mmd_root.bone_morphs[mmd_root.active_morph]
|
||||
morph_data = morph.data[morph.active_data]
|
||||
utils.selectSingleBone(context, armature, morph_data.bone)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EditBoneOffset(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.edit_bone_morph_offset"
|
||||
bl_label = "Edit Related Bone"
|
||||
bl_description = "Applies the location and rotation of this offset to the bone"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
mmd_root = root.mmd_root
|
||||
armature = FnModel.find_armature_object(root)
|
||||
morph = mmd_root.bone_morphs[mmd_root.active_morph]
|
||||
morph_data = morph.data[morph.active_data]
|
||||
p_bone = armature.pose.bones[morph_data.bone]
|
||||
mtx = Quaternion(*morph_data.rotation.to_axis_angle()).to_matrix().to_4x4()
|
||||
mtx.translation = morph_data.location
|
||||
p_bone.matrix_basis = mtx
|
||||
utils.selectSingleBone(context, armature, p_bone.name)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ApplyBoneOffset(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.apply_bone_morph_offset"
|
||||
bl_label = "Apply Bone Morph Offset"
|
||||
bl_description = "Stores the current bone location and rotation into this offset"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
mmd_root = root.mmd_root
|
||||
armature = FnModel.find_armature_object(root)
|
||||
assert armature is not None
|
||||
morph = mmd_root.bone_morphs[mmd_root.active_morph]
|
||||
morph_data = morph.data[morph.active_data]
|
||||
p_bone = armature.pose.bones[morph_data.bone]
|
||||
morph_data.location = p_bone.location
|
||||
morph_data.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ViewUVMorph(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.view_uv_morph"
|
||||
bl_label = "View UV Morph"
|
||||
bl_description = "View the result of active UV morph on current mesh object"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
mmd_root = root.mmd_root
|
||||
|
||||
meshes = tuple(FnModel.iterate_mesh_objects(root))
|
||||
if len(meshes) == 1:
|
||||
obj = meshes[0]
|
||||
elif obj not in meshes:
|
||||
self.report({"ERROR"}, "Please select a mesh object")
|
||||
return {"CANCELLED"}
|
||||
meshObj = obj
|
||||
|
||||
bpy.ops.mmd_tools.clear_uv_morph_view()
|
||||
|
||||
selected = meshObj.select_get()
|
||||
with bpyutils.select_object(meshObj):
|
||||
mesh = cast(bpy.types.Mesh, meshObj.data)
|
||||
morph = mmd_root.uv_morphs[mmd_root.active_morph]
|
||||
uv_textures = mesh.uv_layers
|
||||
|
||||
base_uv_layers = [l for l in mesh.uv_layers if not l.name.startswith("_")]
|
||||
if morph.uv_index >= len(base_uv_layers):
|
||||
self.report({"ERROR"}, "Invalid uv index: %d" % morph.uv_index)
|
||||
return {"CANCELLED"}
|
||||
|
||||
uv_layer_name = base_uv_layers[morph.uv_index].name
|
||||
if morph.uv_index == 0 or uv_textures.active.name not in {uv_layer_name, "_" + uv_layer_name}:
|
||||
uv_textures.active = uv_textures[uv_layer_name]
|
||||
|
||||
uv_layer_name = uv_textures.active.name
|
||||
uv_tex = uv_textures.new(name="__uv.%s" % uv_layer_name)
|
||||
if uv_tex is None:
|
||||
self.report({"ERROR"}, "Failed to create a temporary uv layer")
|
||||
return {"CANCELLED"}
|
||||
|
||||
offsets = FnMorph.get_uv_morph_offset_map(meshObj, morph).items()
|
||||
offsets = {k: getattr(Vector(v), "zw" if uv_layer_name.startswith("_") else "xy") for k, v in offsets}
|
||||
if len(offsets) > 0:
|
||||
base_uv_data = mesh.uv_layers.active.data
|
||||
temp_uv_data = mesh.uv_layers[uv_tex.name].data
|
||||
for i, l in enumerate(mesh.loops):
|
||||
select = temp_uv_data[i].select = l.vertex_index in offsets
|
||||
if select:
|
||||
temp_uv_data[i].uv = base_uv_data[i].uv + offsets[l.vertex_index]
|
||||
|
||||
uv_textures.active = uv_tex
|
||||
uv_tex.active_render = True
|
||||
meshObj.hide_set(False)
|
||||
meshObj.select_set(selected)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ClearUVMorphView(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.clear_uv_morph_view"
|
||||
bl_label = "Clear UV Morph View"
|
||||
bl_description = "Clear all temporary data of UV morphs"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
for m in FnModel.iterate_mesh_objects(root):
|
||||
mesh = m.data
|
||||
uv_textures = getattr(mesh, "uv_textures", mesh.uv_layers)
|
||||
for t in uv_textures:
|
||||
if t.name.startswith("__uv."):
|
||||
uv_textures.remove(t)
|
||||
if len(uv_textures) > 0:
|
||||
uv_textures[0].active_render = True
|
||||
uv_textures.active_index = 0
|
||||
|
||||
animation_data = mesh.animation_data
|
||||
if animation_data:
|
||||
nla_tracks = animation_data.nla_tracks
|
||||
for t in nla_tracks:
|
||||
if t.name.startswith("__uv."):
|
||||
nla_tracks.remove(t)
|
||||
if animation_data.action and animation_data.action.name.startswith("__uv."):
|
||||
animation_data.action = None
|
||||
if animation_data.action is None and len(nla_tracks) == 0:
|
||||
mesh.animation_data_clear()
|
||||
|
||||
for act in bpy.data.actions:
|
||||
if act.name.startswith("__uv.") and act.users < 1:
|
||||
bpy.data.actions.remove(act)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EditUVMorph(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.edit_uv_morph"
|
||||
bl_label = "Edit UV Morph"
|
||||
bl_description = "Edit UV morph on a temporary UV layer (use UV Editor to edit the result)"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
if obj.type != "MESH":
|
||||
return False
|
||||
active_uv_layer = obj.data.uv_layers.active
|
||||
return active_uv_layer and active_uv_layer.name.startswith("__uv.")
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
meshObj = obj
|
||||
|
||||
selected = meshObj.select_get()
|
||||
with bpyutils.select_object(meshObj):
|
||||
mesh = cast(bpy.types.Mesh, meshObj.data)
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
bpy.ops.mesh.select_mode(type="VERT", action="ENABLE")
|
||||
bpy.ops.mesh.reveal() # unhide all vertices
|
||||
bpy.ops.mesh.select_all(action="DESELECT")
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
vertices = mesh.vertices
|
||||
for l, d in zip(mesh.loops, mesh.uv_layers.active.data):
|
||||
if d.select:
|
||||
vertices[l.vertex_index].select = True
|
||||
|
||||
polygons = mesh.polygons
|
||||
polygons.active = getattr(next((p for p in polygons if all(vertices[i].select for i in p.vertices)), None), "index", polygons.active)
|
||||
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
meshObj.select_set(selected)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ApplyUVMorph(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.apply_uv_morph"
|
||||
bl_label = "Apply UV Morph"
|
||||
bl_description = "Calculate the UV offsets of selected vertices and apply to active UV morph"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
if obj.type != "MESH":
|
||||
return False
|
||||
active_uv_layer = obj.data.uv_layers.active
|
||||
return active_uv_layer and active_uv_layer.name.startswith("__uv.")
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
meshObj = obj
|
||||
|
||||
selected = meshObj.select_get()
|
||||
with bpyutils.select_object(meshObj):
|
||||
mesh = cast(bpy.types.Mesh, meshObj.data)
|
||||
morph = mmd_root.uv_morphs[mmd_root.active_morph]
|
||||
|
||||
base_uv_name = mesh.uv_layers.active.name[5:]
|
||||
if base_uv_name not in mesh.uv_layers:
|
||||
self.report({"ERROR"}, ' * UV map "%s" not found' % base_uv_name)
|
||||
return {"CANCELLED"}
|
||||
|
||||
base_uv_data = mesh.uv_layers[base_uv_name].data
|
||||
temp_uv_data = mesh.uv_layers.active.data
|
||||
axis_type = "ZW" if base_uv_name.startswith("_") else "XY"
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
__OffsetData = namedtuple("OffsetData", "index, offset")
|
||||
offsets = {}
|
||||
vertices = mesh.vertices
|
||||
for l, i0, i1 in zip(mesh.loops, base_uv_data, temp_uv_data):
|
||||
if vertices[l.vertex_index].select and l.vertex_index not in offsets:
|
||||
dx, dy = i1.uv - i0.uv
|
||||
if abs(dx) > 0.0001 or abs(dy) > 0.0001:
|
||||
offsets[l.vertex_index] = __OffsetData(l.vertex_index, (dx, dy, dx, dy))
|
||||
|
||||
FnMorph.store_uv_morph_data(meshObj, morph, offsets.values(), axis_type)
|
||||
morph.data_type = "VERTEX_GROUP"
|
||||
|
||||
meshObj.select_set(selected)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CleanDuplicatedMaterialMorphs(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.clean_duplicated_material_morphs"
|
||||
bl_label = "Clean Duplicated Material Morphs"
|
||||
bl_description = "Clean duplicated material morphs"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return FnModel.find_root_object(context.active_object) is not None
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
mmd_root_object = FnModel.find_root_object(context.active_object)
|
||||
FnMorph.clean_duplicated_material_morphs(mmd_root_object)
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,579 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 MMD Tools authors
|
||||
# This file was originally part of the MMD Tools add-on for Blender
|
||||
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
|
||||
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
import math
|
||||
from typing import Dict, Optional, Tuple, cast
|
||||
|
||||
import bpy
|
||||
from mathutils import Euler, Vector
|
||||
|
||||
from .. import utils
|
||||
from ..bpyutils import FnContext, Props
|
||||
from ..core import rigid_body
|
||||
from ..core.model import FnModel, Model
|
||||
from ..core.rigid_body import FnRigidBody
|
||||
|
||||
|
||||
class SelectRigidBody(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.rigid_body_select"
|
||||
bl_label = "Select Rigid Body"
|
||||
bl_description = "Select similar rigidbody objects which have the same property values with active rigidbody object"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
properties: bpy.props.EnumProperty(
|
||||
name="Properties",
|
||||
description="Select the properties to be compared",
|
||||
options={"ENUM_FLAG"},
|
||||
items=[
|
||||
("collision_group_number", "Collision Group", "Collision group", 1),
|
||||
("collision_group_mask", "Collision Group Mask", "Collision group mask", 2),
|
||||
("type", "Rigid Type", "Rigid type", 4),
|
||||
("shape", "Shape", "Collision shape", 8),
|
||||
("bone", "Bone", "Target bone", 16),
|
||||
],
|
||||
default=set(),
|
||||
)
|
||||
hide_others: bpy.props.BoolProperty(
|
||||
name="Hide Others",
|
||||
description="Hide the rigidbody object which does not have the same property values with active rigidbody object",
|
||||
default=False,
|
||||
)
|
||||
|
||||
def invoke(self, context, event):
|
||||
vm = context.window_manager
|
||||
return vm.invoke_props_dialog(self)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return FnModel.is_rigid_body_object(context.active_object)
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
if root is None:
|
||||
self.report({"ERROR"}, "The model root can't be found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
selection = set(FnModel.iterate_rigid_body_objects(root))
|
||||
|
||||
for prop_name in self.properties:
|
||||
prop_value = getattr(obj.mmd_rigid, prop_name)
|
||||
if prop_name == "collision_group_mask":
|
||||
prop_value = tuple(prop_value)
|
||||
for i in selection.copy():
|
||||
if tuple(i.mmd_rigid.collision_group_mask) != prop_value:
|
||||
selection.remove(i)
|
||||
if self.hide_others:
|
||||
i.select_set(False)
|
||||
i.hide_set(True)
|
||||
else:
|
||||
for i in selection.copy():
|
||||
if getattr(i.mmd_rigid, prop_name) != prop_value:
|
||||
selection.remove(i)
|
||||
if self.hide_others:
|
||||
i.select_set(False)
|
||||
i.hide_set(True)
|
||||
|
||||
for i in selection:
|
||||
i.hide_set(False)
|
||||
i.select_set(True)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddRigidBody(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.rigid_body_add"
|
||||
bl_label = "Add Rigid Body"
|
||||
bl_description = "Add Rigid Bodies to selected bones"
|
||||
bl_options = {"REGISTER", "UNDO", "PRESET", "INTERNAL"}
|
||||
|
||||
name_j: bpy.props.StringProperty(
|
||||
name="Name",
|
||||
description="The name of rigid body ($name_j means use the japanese name of target bone)",
|
||||
default="$name_j",
|
||||
)
|
||||
name_e: bpy.props.StringProperty(
|
||||
name="Name(Eng)",
|
||||
description="The english name of rigid body ($name_e means use the english name of target bone)",
|
||||
default="$name_e",
|
||||
)
|
||||
collision_group_number: bpy.props.IntProperty(
|
||||
name="Collision Group",
|
||||
description="The collision group of the object",
|
||||
min=0,
|
||||
max=15,
|
||||
)
|
||||
collision_group_mask: bpy.props.BoolVectorProperty(
|
||||
name="Collision Group Mask",
|
||||
description="The groups the object can not collide with",
|
||||
size=16,
|
||||
subtype="LAYER",
|
||||
)
|
||||
rigid_type: bpy.props.EnumProperty(
|
||||
name="Rigid Type",
|
||||
description="Select rigid type",
|
||||
items=[
|
||||
(str(rigid_body.MODE_STATIC), "Bone", "Rigid body's orientation completely determined by attached bone", 1),
|
||||
(str(rigid_body.MODE_DYNAMIC), "Physics", "Attached bone's orientation completely determined by rigid body", 2),
|
||||
(str(rigid_body.MODE_DYNAMIC_BONE), "Physics + Bone", "Bone determined by combination of parent and attached rigid body", 3),
|
||||
],
|
||||
)
|
||||
rigid_shape: bpy.props.EnumProperty(
|
||||
name="Shape",
|
||||
description="Select the collision shape",
|
||||
items=[
|
||||
("SPHERE", "Sphere", "", 1),
|
||||
("BOX", "Box", "", 2),
|
||||
("CAPSULE", "Capsule", "", 3),
|
||||
],
|
||||
)
|
||||
size: bpy.props.FloatVectorProperty(
|
||||
name="Size",
|
||||
description="Size of the object, the values will multiply the length of target bone",
|
||||
subtype="XYZ",
|
||||
size=3,
|
||||
min=0,
|
||||
default=[0.6, 0.6, 0.6],
|
||||
)
|
||||
mass: bpy.props.FloatProperty(
|
||||
name="Mass",
|
||||
description="How much the object 'weights' irrespective of gravity",
|
||||
min=0.001,
|
||||
default=1,
|
||||
)
|
||||
friction: bpy.props.FloatProperty(
|
||||
name="Friction",
|
||||
description="Resistance of object to movement",
|
||||
min=0,
|
||||
soft_max=1,
|
||||
default=0.5,
|
||||
)
|
||||
bounce: bpy.props.FloatProperty(
|
||||
name="Restitution",
|
||||
description="Tendency of object to bounce after colliding with another (0 = stays still, 1 = perfectly elastic)",
|
||||
min=0,
|
||||
soft_max=1,
|
||||
)
|
||||
linear_damping: bpy.props.FloatProperty(
|
||||
name="Linear Damping",
|
||||
description="Amount of linear velocity that is lost over time",
|
||||
min=0,
|
||||
max=1,
|
||||
default=0.04,
|
||||
)
|
||||
angular_damping: bpy.props.FloatProperty(
|
||||
name="Angular Damping",
|
||||
description="Amount of angular velocity that is lost over time",
|
||||
min=0,
|
||||
max=1,
|
||||
default=0.1,
|
||||
)
|
||||
|
||||
def __add_rigid_body(self, context: bpy.types.Context, root_object: bpy.types.Object, pose_bone: Optional[bpy.types.PoseBone] = None):
|
||||
name_j: str = self.name_j
|
||||
name_e: str = self.name_e
|
||||
size = self.size.copy()
|
||||
loc = Vector((0.0, 0.0, 0.0))
|
||||
rot = Euler((0.0, 0.0, 0.0))
|
||||
bone_name: Optional[str] = None
|
||||
|
||||
if pose_bone is None:
|
||||
size *= getattr(root_object, Props.empty_display_size)
|
||||
else:
|
||||
bone_name = pose_bone.name
|
||||
mmd_bone = pose_bone.mmd_bone
|
||||
name_j = name_j.replace("$name_j", mmd_bone.name_j or bone_name)
|
||||
name_e = name_e.replace("$name_e", mmd_bone.name_e or bone_name)
|
||||
|
||||
target_bone = pose_bone.bone
|
||||
loc = (target_bone.head_local + target_bone.tail_local) / 2
|
||||
rot = target_bone.matrix_local.to_euler("YXZ")
|
||||
rot.rotate_axis("X", math.pi / 2)
|
||||
|
||||
size *= target_bone.length
|
||||
if 1:
|
||||
pass # bypass resizing
|
||||
elif self.rigid_shape == "SPHERE":
|
||||
size.x *= 0.8
|
||||
elif self.rigid_shape == "BOX":
|
||||
size.x /= 3
|
||||
size.y /= 3
|
||||
size.z *= 0.8
|
||||
elif self.rigid_shape == "CAPSULE":
|
||||
size.x /= 3
|
||||
|
||||
return FnRigidBody.setup_rigid_body_object(
|
||||
obj=FnRigidBody.new_rigid_body_object(context, FnModel.ensure_rigid_group_object(context, root_object)),
|
||||
shape_type=rigid_body.shapeType(self.rigid_shape),
|
||||
location=loc,
|
||||
rotation=rot,
|
||||
size=size,
|
||||
dynamics_type=int(self.rigid_type),
|
||||
name=name_j,
|
||||
name_e=name_e,
|
||||
collision_group_number=self.collision_group_number,
|
||||
collision_group_mask=self.collision_group_mask,
|
||||
mass=self.mass,
|
||||
friction=self.friction,
|
||||
bounce=self.bounce,
|
||||
linear_damping=self.linear_damping,
|
||||
angular_damping=self.angular_damping,
|
||||
bone=bone_name,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
root_object = FnModel.find_root_object(context.active_object)
|
||||
if root_object is None:
|
||||
return False
|
||||
|
||||
armature_object = FnModel.find_armature_object(root_object)
|
||||
if armature_object is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
active_object = context.active_object
|
||||
|
||||
root_object = cast(bpy.types.Object, FnModel.find_root_object(active_object))
|
||||
armature_object = cast(bpy.types.Object, FnModel.find_armature_object(root_object))
|
||||
|
||||
if active_object != armature_object:
|
||||
FnContext.select_single_object(context, root_object).select_set(False)
|
||||
elif armature_object.mode != "POSE":
|
||||
bpy.ops.object.mode_set(mode="POSE")
|
||||
|
||||
selected_pose_bones = []
|
||||
if context.selected_pose_bones:
|
||||
selected_pose_bones = context.selected_pose_bones
|
||||
|
||||
armature_object.select_set(False)
|
||||
if len(selected_pose_bones) > 0:
|
||||
for pose_bone in selected_pose_bones:
|
||||
rigid = self.__add_rigid_body(context, root_object, pose_bone)
|
||||
rigid.select_set(True)
|
||||
else:
|
||||
rigid = self.__add_rigid_body(context, root_object)
|
||||
rigid.select_set(True)
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
no_bone = True
|
||||
if context.selected_bones and len(context.selected_bones) > 0:
|
||||
no_bone = False
|
||||
elif context.selected_pose_bones and len(context.selected_pose_bones) > 0:
|
||||
no_bone = False
|
||||
|
||||
if no_bone:
|
||||
self.name_j = "Rigid"
|
||||
self.name_e = "Rigid_e"
|
||||
else:
|
||||
if self.name_j == "Rigid":
|
||||
self.name_j = "$name_j"
|
||||
if self.name_e == "Rigid_e":
|
||||
self.name_e = "$name_e"
|
||||
vm = context.window_manager
|
||||
return vm.invoke_props_dialog(self)
|
||||
|
||||
|
||||
class RemoveRigidBody(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.rigid_body_remove"
|
||||
bl_label = "Remove Rigid Body"
|
||||
bl_description = "Deletes the currently selected Rigid Body"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return FnModel.is_rigid_body_object(context.active_object)
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
utils.selectAObject(obj) # ensure this is the only one object select
|
||||
bpy.ops.object.delete(use_global=True)
|
||||
if root:
|
||||
utils.selectAObject(root)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RigidBodyBake(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.ptcache_rigid_body_bake"
|
||||
bl_label = "Bake"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache):
|
||||
bpy.ops.ptcache.bake("INVOKE_DEFAULT", bake=True)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RigidBodyDeleteBake(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.ptcache_rigid_body_delete_bake"
|
||||
bl_label = "Delete Bake"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache):
|
||||
bpy.ops.ptcache.free_bake("INVOKE_DEFAULT")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddJoint(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.joint_add"
|
||||
bl_label = "Add Joint"
|
||||
bl_description = "Add Joint(s) to selected rigidbody objects"
|
||||
bl_options = {"REGISTER", "UNDO", "PRESET", "INTERNAL"}
|
||||
|
||||
use_bone_rotation: bpy.props.BoolProperty(
|
||||
name="Use Bone Rotation",
|
||||
description="Match joint orientation to bone orientation if enabled",
|
||||
default=True,
|
||||
)
|
||||
limit_linear_lower: bpy.props.FloatVectorProperty(
|
||||
name="Limit Linear Lower",
|
||||
description="Lower limit of translation",
|
||||
subtype="XYZ",
|
||||
size=3,
|
||||
)
|
||||
limit_linear_upper: bpy.props.FloatVectorProperty(
|
||||
name="Limit Linear Upper",
|
||||
description="Upper limit of translation",
|
||||
subtype="XYZ",
|
||||
size=3,
|
||||
)
|
||||
limit_angular_lower: bpy.props.FloatVectorProperty(
|
||||
name="Limit Angular Lower",
|
||||
description="Lower limit of rotation",
|
||||
subtype="EULER",
|
||||
size=3,
|
||||
min=-math.pi * 2,
|
||||
max=math.pi * 2,
|
||||
default=[-math.pi / 4] * 3,
|
||||
)
|
||||
limit_angular_upper: bpy.props.FloatVectorProperty(
|
||||
name="Limit Angular Upper",
|
||||
description="Upper limit of rotation",
|
||||
subtype="EULER",
|
||||
size=3,
|
||||
min=-math.pi * 2,
|
||||
max=math.pi * 2,
|
||||
default=[math.pi / 4] * 3,
|
||||
)
|
||||
spring_linear: bpy.props.FloatVectorProperty(
|
||||
name="Spring(Linear)",
|
||||
description="Spring constant of movement",
|
||||
subtype="XYZ",
|
||||
size=3,
|
||||
min=0,
|
||||
)
|
||||
spring_angular: bpy.props.FloatVectorProperty(
|
||||
name="Spring(Angular)",
|
||||
description="Spring constant of rotation",
|
||||
subtype="XYZ",
|
||||
size=3,
|
||||
min=0,
|
||||
)
|
||||
|
||||
def __enumerate_rigid_pair(self, bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]):
|
||||
obj_seq = tuple(bone_map.keys())
|
||||
for rigid_a, bone_a in bone_map.items():
|
||||
for rigid_b, bone_b in bone_map.items():
|
||||
if bone_a and bone_b and bone_b.parent == bone_a:
|
||||
obj_seq = ()
|
||||
yield (rigid_a, rigid_b)
|
||||
if len(obj_seq) == 2:
|
||||
if obj_seq[1].mmd_rigid.type == str(rigid_body.MODE_STATIC):
|
||||
yield (obj_seq[1], obj_seq[0])
|
||||
else:
|
||||
yield obj_seq
|
||||
|
||||
def __add_joint(self, context: bpy.types.Context, root_object: bpy.types.Object, rigid_pair: Tuple[bpy.types.Object, bpy.types.Object], bone_map):
|
||||
loc: Optional[Vector] = None
|
||||
rot = Euler((0.0, 0.0, 0.0))
|
||||
rigid_a, rigid_b = rigid_pair
|
||||
bone_a = bone_map[rigid_a]
|
||||
bone_b = bone_map[rigid_b]
|
||||
if bone_a and bone_b:
|
||||
if bone_a.parent == bone_b:
|
||||
rigid_b, rigid_a = rigid_a, rigid_b
|
||||
bone_b, bone_a = bone_a, bone_b
|
||||
if bone_b.parent == bone_a:
|
||||
loc = bone_b.head_local
|
||||
if self.use_bone_rotation:
|
||||
rot = bone_b.matrix_local.to_euler("YXZ")
|
||||
rot.rotate_axis("X", math.pi / 2)
|
||||
if loc is None:
|
||||
loc = (rigid_a.location + rigid_b.location) / 2
|
||||
|
||||
name_j = rigid_b.mmd_rigid.name_j or rigid_b.name
|
||||
name_e = rigid_b.mmd_rigid.name_e or rigid_b.name
|
||||
|
||||
return FnRigidBody.setup_joint_object(
|
||||
obj=FnRigidBody.new_joint_object(context, FnModel.ensure_joint_group_object(context, root_object), FnModel.get_empty_display_size(root_object)),
|
||||
name=name_j,
|
||||
name_e=name_e,
|
||||
location=loc,
|
||||
rotation=rot,
|
||||
rigid_a=rigid_a,
|
||||
rigid_b=rigid_b,
|
||||
maximum_location=self.limit_linear_upper,
|
||||
minimum_location=self.limit_linear_lower,
|
||||
maximum_rotation=self.limit_angular_upper,
|
||||
minimum_rotation=self.limit_angular_lower,
|
||||
spring_linear=self.spring_linear,
|
||||
spring_angular=self.spring_angular,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
root_object = FnModel.find_root_object(context.active_object)
|
||||
if root_object is None:
|
||||
return False
|
||||
|
||||
armature_object = FnModel.find_armature_object(root_object)
|
||||
if armature_object is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
active_object = context.active_object
|
||||
root_object = cast(bpy.types.Object, FnModel.find_root_object(active_object))
|
||||
armature_object = cast(bpy.types.Object, FnModel.find_armature_object(root_object))
|
||||
bones = cast(bpy.types.Armature, armature_object.data).bones
|
||||
bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]] = {r: bones.get(r.mmd_rigid.bone, None) for r in FnModel.iterate_rigid_body_objects(root_object) if r.select_get()}
|
||||
|
||||
if len(bone_map) < 2:
|
||||
self.report({"ERROR"}, "Please select two or more mmd rigid objects")
|
||||
return {"CANCELLED"}
|
||||
|
||||
FnContext.select_single_object(context, root_object).select_set(False)
|
||||
if context.scene.rigidbody_world is None:
|
||||
bpy.ops.rigidbody.world_add()
|
||||
|
||||
for pair in self.__enumerate_rigid_pair(bone_map):
|
||||
joint = self.__add_joint(context, root_object, pair, bone_map)
|
||||
joint.select_set(True)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
vm = context.window_manager
|
||||
return vm.invoke_props_dialog(self)
|
||||
|
||||
|
||||
class RemoveJoint(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.joint_remove"
|
||||
bl_label = "Remove Joint"
|
||||
bl_description = "Deletes the currently selected Joint"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return FnModel.is_joint_object(context.active_object)
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
utils.selectAObject(obj) # ensure this is the only one object select
|
||||
bpy.ops.object.delete(use_global=True)
|
||||
if root:
|
||||
utils.selectAObject(root)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class UpdateRigidBodyWorld(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.rigid_body_world_update"
|
||||
bl_label = "Update Rigid Body World"
|
||||
bl_description = "Update rigid body world and references of rigid body constraint according to current scene objects (experimental)"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@staticmethod
|
||||
def __get_rigid_body_world_objects():
|
||||
rigid_body.setRigidBodyWorldEnabled(True)
|
||||
rbw = bpy.context.scene.rigidbody_world
|
||||
if not rbw.collection:
|
||||
rbw.collection = bpy.data.collections.new("RigidBodyWorld")
|
||||
rbw.collection.use_fake_user = True
|
||||
if not rbw.constraints:
|
||||
rbw.constraints = bpy.data.collections.new("RigidBodyConstraints")
|
||||
rbw.constraints.use_fake_user = True
|
||||
|
||||
bpy.context.scene.rigidbody_world.substeps_per_frame = 6
|
||||
bpy.context.scene.rigidbody_world.solver_iterations = 10
|
||||
|
||||
return rbw.collection.objects, rbw.constraints.objects
|
||||
|
||||
def execute(self, context):
|
||||
scene = context.scene
|
||||
scene_objs = set(scene.objects)
|
||||
scene_objs.union(o for x in scene.objects if x.instance_type == "COLLECTION" and x.instance_collection for o in x.instance_collection.objects)
|
||||
|
||||
def _update_group(obj, group):
|
||||
if obj in scene_objs:
|
||||
if obj not in group.values():
|
||||
group.link(obj)
|
||||
return True
|
||||
elif obj in group.values():
|
||||
group.unlink(obj)
|
||||
return False
|
||||
|
||||
def _references(obj):
|
||||
yield obj
|
||||
if getattr(obj, "proxy", None):
|
||||
yield from _references(obj.proxy)
|
||||
if getattr(obj, "override_library", None):
|
||||
yield from _references(obj.override_library.reference)
|
||||
|
||||
need_rebuild_physics = scene.rigidbody_world is None or scene.rigidbody_world.collection is None or scene.rigidbody_world.constraints is None
|
||||
rb_objs, rbc_objs = self.__get_rigid_body_world_objects()
|
||||
objects = bpy.data.objects
|
||||
table = {}
|
||||
|
||||
# Perhaps due to a bug in Blender,
|
||||
# when bpy.ops.rigidbody.world_remove(),
|
||||
# Object.rigid_body are removed,
|
||||
# but Object.rigid_body_constraint are retained.
|
||||
# Therefore, it must be checked with Object.mmd_type.
|
||||
for i in (x for x in objects if x.mmd_type == "RIGID_BODY"):
|
||||
if not _update_group(i, rb_objs):
|
||||
continue
|
||||
|
||||
rb_map = table.setdefault(FnModel.find_root_object(i), {})
|
||||
if i in rb_map: # means rb_map[i] will replace i
|
||||
rb_objs.unlink(i)
|
||||
continue
|
||||
for r in _references(i):
|
||||
rb_map[r] = i
|
||||
|
||||
# TODO Modify mmd_rigid to allow recovery of the remaining rigidbody parameters.
|
||||
# mass, friction, restitution, linear_dumping, angular_dumping
|
||||
|
||||
for i in (x for x in objects if x.rigid_body_constraint):
|
||||
if not _update_group(i, rbc_objs):
|
||||
continue
|
||||
|
||||
rbc, root_object = i.rigid_body_constraint, FnModel.find_root_object(i)
|
||||
rb_map = table.get(root_object, {})
|
||||
rbc.object1 = rb_map.get(rbc.object1, rbc.object1)
|
||||
rbc.object2 = rb_map.get(rbc.object2, rbc.object2)
|
||||
|
||||
if need_rebuild_physics:
|
||||
for root_object in scene.objects:
|
||||
if root_object.mmd_type != "ROOT":
|
||||
continue
|
||||
if not root_object.mmd_root.is_built:
|
||||
continue
|
||||
with FnContext.temp_override_active_layer_collection(context, root_object):
|
||||
Model(root_object).build()
|
||||
# After rebuild. First play. Will be crash!
|
||||
# But saved it before. Reload after crash. The play can be work.
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,110 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 MMD Tools authors
|
||||
# This file was originally part of the MMD Tools add-on for Blender
|
||||
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
|
||||
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
from typing import Set
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
|
||||
from ..core.model import FnModel
|
||||
from ..core.sdef import FnSDEF
|
||||
|
||||
|
||||
def _get_target_objects(context):
|
||||
root_objects: Set[bpy.types.Object] = set()
|
||||
selected_objects: Set[bpy.types.Object] = set()
|
||||
for i in context.selected_objects:
|
||||
if i.type == "MESH":
|
||||
selected_objects.add(i)
|
||||
continue
|
||||
|
||||
root_object = FnModel.find_root_object(i)
|
||||
if root_object is None:
|
||||
continue
|
||||
if root_object in root_objects:
|
||||
continue
|
||||
|
||||
root_objects.add(root_object)
|
||||
|
||||
selected_objects |= set(FnModel.iterate_mesh_objects(root_object))
|
||||
return selected_objects, root_objects
|
||||
|
||||
|
||||
class ResetSDEFCache(Operator):
|
||||
bl_idname = "mmd_tools.sdef_cache_reset"
|
||||
bl_label = "Reset MMD SDEF cache"
|
||||
bl_description = "Reset MMD SDEF cache of selected objects and clean unused cache"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
target_meshes, _ = _get_target_objects(context)
|
||||
for i in target_meshes:
|
||||
FnSDEF.clear_cache(i)
|
||||
FnSDEF.clear_cache(unused_only=True)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BindSDEF(Operator):
|
||||
bl_idname = "mmd_tools.sdef_bind"
|
||||
bl_label = "Bind SDEF Driver"
|
||||
bl_description = "Bind MMD SDEF data of selected objects"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
mode: bpy.props.EnumProperty(
|
||||
name="Mode",
|
||||
description="Select mode",
|
||||
items=[
|
||||
("2", "Bulk", "Speed up with numpy (may be slower in some cases)", 2),
|
||||
("1", "Normal", "Normal mode", 1),
|
||||
("0", "- Auto -", "Select best mode by benchmark result", 0),
|
||||
],
|
||||
default="0",
|
||||
)
|
||||
use_skip: bpy.props.BoolProperty(
|
||||
name="Skip",
|
||||
description="Skip when the bones are not moving",
|
||||
default=True,
|
||||
)
|
||||
use_scale: bpy.props.BoolProperty(
|
||||
name="Scale",
|
||||
description="Support bone scaling (slow)",
|
||||
default=False,
|
||||
)
|
||||
|
||||
def invoke(self, context, event):
|
||||
vm = context.window_manager
|
||||
return vm.invoke_props_dialog(self)
|
||||
|
||||
# TODO: Utility Functionalize
|
||||
def execute(self, context):
|
||||
target_meshes, root_objects = _get_target_objects(context)
|
||||
|
||||
for r in root_objects:
|
||||
r.mmd_root.use_sdef = True
|
||||
|
||||
param = ((None, False, True)[int(self.mode)], self.use_skip, self.use_scale)
|
||||
count = sum(FnSDEF.bind(i, *param) for i in target_meshes)
|
||||
self.report({"INFO"}, f"Binded {count} of {len(target_meshes)} selected mesh(es)")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class UnbindSDEF(Operator):
|
||||
bl_idname = "mmd_tools.sdef_unbind"
|
||||
bl_label = "Unbind SDEF Driver"
|
||||
bl_description = "Unbind MMD SDEF data of selected objects"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
# TODO: Utility Functionalize
|
||||
def execute(self, context):
|
||||
target_meshes, root_objects = _get_target_objects(context)
|
||||
for i in target_meshes:
|
||||
FnSDEF.unbind(i)
|
||||
|
||||
for r in root_objects:
|
||||
r.mmd_root.use_sdef = False
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,336 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 MMD Tools authors
|
||||
# This file was originally part of the MMD Tools add-on for Blender
|
||||
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
|
||||
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
import bpy
|
||||
|
||||
from ..core.model import FnModel, Model
|
||||
from ..core.translations import MMD_DATA_TYPE_TO_HANDLERS, FnTranslations
|
||||
from ..translations import DictionaryEnum
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..properties.translations import MMDTranslation, MMDTranslationElement, MMDTranslationElementIndex
|
||||
|
||||
|
||||
class TranslateMMDModel(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.translate_mmd_model"
|
||||
bl_label = "Translate a MMD Model"
|
||||
bl_description = "Translate Japanese names of a MMD model"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
dictionary: bpy.props.EnumProperty(
|
||||
name="Dictionary",
|
||||
items=DictionaryEnum.get_dictionary_items,
|
||||
description="Translate names from Japanese to English using selected dictionary",
|
||||
)
|
||||
types: bpy.props.EnumProperty(
|
||||
name="Types",
|
||||
description="Select which parts will be translated",
|
||||
options={"ENUM_FLAG"},
|
||||
items=[
|
||||
("BONE", "Bones", "Bones", 1),
|
||||
("MORPH", "Morphs", "Morphs", 2),
|
||||
("MATERIAL", "Materials", "Materials", 4),
|
||||
("DISPLAY", "Display", "Display frames", 8),
|
||||
("PHYSICS", "Physics", "Rigidbodies and joints", 16),
|
||||
("INFO", "Information", "Model name and comments", 32),
|
||||
],
|
||||
default={
|
||||
"BONE",
|
||||
"MORPH",
|
||||
"MATERIAL",
|
||||
"DISPLAY",
|
||||
"PHYSICS",
|
||||
},
|
||||
)
|
||||
modes: bpy.props.EnumProperty(
|
||||
name="Modes",
|
||||
description="Select translation mode",
|
||||
options={"ENUM_FLAG"},
|
||||
items=[
|
||||
("MMD", "MMD Names", "Fill MMD English names", 1),
|
||||
("BLENDER", "Blender Names", "Translate blender names (experimental)", 2),
|
||||
],
|
||||
default={"MMD"},
|
||||
)
|
||||
use_morph_prefix: bpy.props.BoolProperty(
|
||||
name="Use Morph Prefix",
|
||||
description="Add/remove prefix to English name of morph",
|
||||
default=False,
|
||||
)
|
||||
overwrite: bpy.props.BoolProperty(
|
||||
name="Overwrite",
|
||||
description="Overwrite a translated English name",
|
||||
default=False,
|
||||
)
|
||||
allow_fails: bpy.props.BoolProperty(
|
||||
name="Allow Fails",
|
||||
description="Allow incompletely translated names",
|
||||
default=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
return obj in context.selected_objects and FnModel.find_root_object(obj)
|
||||
|
||||
def invoke(self, context, event):
|
||||
vm = context.window_manager
|
||||
return vm.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
self.__translator = DictionaryEnum.get_translator(self.dictionary)
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, "Failed to load dictionary: %s" % e)
|
||||
return {"CANCELLED"}
|
||||
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
rig = Model(root)
|
||||
|
||||
if "MMD" in self.modes:
|
||||
for i in self.types:
|
||||
getattr(self, "translate_%s" % i.lower())(rig)
|
||||
|
||||
if "BLENDER" in self.modes:
|
||||
self.translate_blender_names(rig)
|
||||
|
||||
translator = self.__translator
|
||||
txt = translator.save_fails()
|
||||
if translator.fails:
|
||||
self.report({"WARNING"}, "Failed to translate %d names, see '%s' in text editor" % (len(translator.fails), txt.name))
|
||||
return {"FINISHED"}
|
||||
|
||||
def translate(self, name_j, name_e):
|
||||
if not self.overwrite and name_e and self.__translator.is_translated(name_e):
|
||||
return name_e
|
||||
if self.allow_fails:
|
||||
name_e = None
|
||||
return self.__translator.translate(name_j, name_e)
|
||||
|
||||
def translate_blender_names(self, rig: Model):
|
||||
if "BONE" in self.types:
|
||||
for b in rig.armature().pose.bones:
|
||||
rig.renameBone(b.name, self.translate(b.name, b.name))
|
||||
|
||||
if "MORPH" in self.types:
|
||||
for i in (x for x in rig.meshes() if x.data.shape_keys):
|
||||
for kb in i.data.shape_keys.key_blocks:
|
||||
kb.name = self.translate(kb.name, kb.name)
|
||||
|
||||
if "MATERIAL" in self.types:
|
||||
for m in (x for x in rig.materials() if x):
|
||||
m.name = self.translate(m.name, m.name)
|
||||
|
||||
if "DISPLAY" in self.types:
|
||||
g: bpy.types.BoneCollection
|
||||
for g in cast(bpy.types.Armature, rig.armature().data).collections:
|
||||
g.name = self.translate(g.name, g.name)
|
||||
|
||||
if "PHYSICS" in self.types:
|
||||
for i in rig.rigidBodies():
|
||||
i.name = self.translate(i.name, i.name)
|
||||
|
||||
for i in rig.joints():
|
||||
i.name = self.translate(i.name, i.name)
|
||||
|
||||
if "INFO" in self.types:
|
||||
objects = [rig.rootObject(), rig.armature()]
|
||||
objects.extend(rig.meshes())
|
||||
for i in objects:
|
||||
i.name = self.translate(i.name, i.name)
|
||||
|
||||
def translate_info(self, rig):
|
||||
mmd_root = rig.rootObject().mmd_root
|
||||
mmd_root.name_e = self.translate(mmd_root.name, mmd_root.name_e)
|
||||
|
||||
comment_text = bpy.data.texts.get(mmd_root.comment_text, None)
|
||||
comment_e_text = bpy.data.texts.get(mmd_root.comment_e_text, None)
|
||||
if comment_text and comment_e_text:
|
||||
comment_e = self.translate(comment_text.as_string(), comment_e_text.as_string())
|
||||
comment_e_text.from_string(comment_e)
|
||||
|
||||
def translate_bone(self, rig):
|
||||
bones = rig.armature().pose.bones
|
||||
for b in bones:
|
||||
if b.is_mmd_shadow_bone:
|
||||
continue
|
||||
b.mmd_bone.name_e = self.translate(b.mmd_bone.name_j, b.mmd_bone.name_e)
|
||||
|
||||
def translate_morph(self, rig):
|
||||
mmd_root = rig.rootObject().mmd_root
|
||||
attr_list = ("group", "vertex", "bone", "uv", "material")
|
||||
prefix_list = ("G_", "", "B_", "UV_", "M_")
|
||||
for attr, prefix in zip(attr_list, prefix_list):
|
||||
for m in getattr(mmd_root, attr + "_morphs", []):
|
||||
m.name_e = self.translate(m.name, m.name_e)
|
||||
if not prefix:
|
||||
continue
|
||||
if self.use_morph_prefix:
|
||||
if not m.name_e.startswith(prefix):
|
||||
m.name_e = prefix + m.name_e
|
||||
elif m.name_e.startswith(prefix):
|
||||
m.name_e = m.name_e[len(prefix) :]
|
||||
|
||||
def translate_material(self, rig):
|
||||
for m in rig.materials():
|
||||
if m is None:
|
||||
continue
|
||||
m.mmd_material.name_e = self.translate(m.mmd_material.name_j, m.mmd_material.name_e)
|
||||
|
||||
def translate_display(self, rig):
|
||||
mmd_root = rig.rootObject().mmd_root
|
||||
for f in mmd_root.display_item_frames:
|
||||
f.name_e = self.translate(f.name, f.name_e)
|
||||
|
||||
def translate_physics(self, rig):
|
||||
for i in rig.rigidBodies():
|
||||
i.mmd_rigid.name_e = self.translate(i.mmd_rigid.name_j, i.mmd_rigid.name_e)
|
||||
|
||||
for i in rig.joints():
|
||||
i.mmd_joint.name_e = self.translate(i.mmd_joint.name_j, i.mmd_joint.name_e)
|
||||
|
||||
|
||||
DEFAULT_SHOW_ROW_COUNT = 20
|
||||
|
||||
|
||||
class MMD_TOOLS_UL_MMDTranslationElementIndex(bpy.types.UIList):
|
||||
def draw_item(self, context, layout: bpy.types.UILayout, data, mmd_translation_element_index: "MMDTranslationElementIndex", icon, active_data, active_propname, index: int):
|
||||
mmd_translation_element: "MMDTranslationElement" = data.translation_elements[mmd_translation_element_index.value]
|
||||
MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type].draw_item(layout, mmd_translation_element, index)
|
||||
|
||||
|
||||
class RestoreMMDDataReferenceOperator(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.restore_mmd_translation_element_name"
|
||||
bl_label = "Restore this Name"
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
index: bpy.props.IntProperty()
|
||||
prop_name: bpy.props.StringProperty()
|
||||
restore_value: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
root_object = FnModel.find_root_object(context.object)
|
||||
mmd_translation_element_index = root_object.mmd_root.translation.filtered_translation_element_indices[self.index].value
|
||||
mmd_translation_element = root_object.mmd_root.translation.translation_elements[mmd_translation_element_index]
|
||||
setattr(mmd_translation_element, self.prop_name, self.restore_value)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GlobalTranslationPopup(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.global_translation_popup"
|
||||
bl_label = "Global Translation Popup"
|
||||
bl_options = {"INTERNAL", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return FnModel.find_root_object(context.object) is not None
|
||||
|
||||
def draw(self, _context):
|
||||
layout = self.layout
|
||||
mmd_translation = self._mmd_translation
|
||||
|
||||
col = layout.column(align=True)
|
||||
col.label(text="Filter", icon="FILTER")
|
||||
row = col.row()
|
||||
row.prop(mmd_translation, "filter_types")
|
||||
|
||||
group = row.row(align=True, heading="is Blank:")
|
||||
group.alignment = "RIGHT"
|
||||
group.prop(mmd_translation, "filter_japanese_blank", toggle=True, text="Japanese")
|
||||
group.prop(mmd_translation, "filter_english_blank", toggle=True, text="English")
|
||||
|
||||
group = row.row(align=True)
|
||||
group.prop(mmd_translation, "filter_restorable", toggle=True, icon="FILE_REFRESH", icon_only=True)
|
||||
group.prop(mmd_translation, "filter_selected", toggle=True, icon="RESTRICT_SELECT_OFF", icon_only=True)
|
||||
group.prop(mmd_translation, "filter_visible", toggle=True, icon="HIDE_OFF", icon_only=True)
|
||||
|
||||
col = layout.column(align=True)
|
||||
box = col.box().column(align=True)
|
||||
row = box.row(align=True)
|
||||
row.label(text="Select the target column for Batch Operations:", icon="TRACKER")
|
||||
row = box.row(align=True)
|
||||
row.label(text="", icon="BLANK1")
|
||||
row.prop(mmd_translation, "batch_operation_target", expand=True)
|
||||
row.label(text="", icon="RESTRICT_SELECT_OFF")
|
||||
row.label(text="", icon="HIDE_OFF")
|
||||
|
||||
if len(mmd_translation.filtered_translation_element_indices) > DEFAULT_SHOW_ROW_COUNT:
|
||||
row.label(text="", icon="BLANK1")
|
||||
|
||||
col.template_list(
|
||||
"MMD_TOOLS_UL_MMDTranslationElementIndex",
|
||||
"",
|
||||
mmd_translation,
|
||||
"filtered_translation_element_indices",
|
||||
mmd_translation,
|
||||
"filtered_translation_element_indices_active_index",
|
||||
rows=DEFAULT_SHOW_ROW_COUNT,
|
||||
)
|
||||
|
||||
box = layout.box().column(align=True)
|
||||
box.label(text="Batch Operation:", icon="MODIFIER")
|
||||
box.prop(mmd_translation, "batch_operation_script", text="", icon="SCRIPT")
|
||||
|
||||
box.separator()
|
||||
row = box.row()
|
||||
row.prop(mmd_translation, "batch_operation_script_preset", text="Preset", icon="CON_TRANSFORM_CACHE")
|
||||
row.operator(ExecuteTranslationBatchOperator.bl_idname, text="Execute")
|
||||
|
||||
box.separator()
|
||||
translation_box = box.box().column(align=True)
|
||||
translation_box.label(text="Dictionaries:", icon="HELP")
|
||||
row = translation_box.row()
|
||||
row.prop(mmd_translation, "dictionary", text="to_english")
|
||||
# row.operator(ExecuteTranslationScriptOperator.bl_idname, text='Write to .csv')
|
||||
|
||||
translation_box.separator()
|
||||
row = translation_box.row()
|
||||
row.prop(mmd_translation, "dictionary", text="replace")
|
||||
|
||||
def invoke(self, context: bpy.types.Context, _event):
|
||||
root_object = FnModel.find_root_object(context.object)
|
||||
if root_object is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
mmd_translation: "MMDTranslation" = root_object.mmd_root.translation
|
||||
self._mmd_translation = mmd_translation
|
||||
FnTranslations.clear_data(mmd_translation)
|
||||
FnTranslations.collect_data(mmd_translation)
|
||||
FnTranslations.update_query(mmd_translation)
|
||||
|
||||
return context.window_manager.invoke_props_dialog(self, width=800)
|
||||
|
||||
def execute(self, context):
|
||||
root_object = FnModel.find_root_object(context.object)
|
||||
if root_object is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
FnTranslations.apply_translations(root_object)
|
||||
FnTranslations.clear_data(root_object.mmd_root.translation)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ExecuteTranslationBatchOperator(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.execute_translation_batch"
|
||||
bl_label = "Execute Translation Batch"
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
root = FnModel.find_root_object(context.object)
|
||||
if root is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
fails, text = FnTranslations.execute_translation_batch(root)
|
||||
if fails:
|
||||
self.report({"WARNING"}, "Failed to translate %d names, see '%s' in text editor" % (len(fails), text.name))
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,150 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 MMD Tools authors
|
||||
# This file was originally part of the MMD Tools add-on for Blender
|
||||
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
|
||||
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
import re
|
||||
|
||||
from bpy.types import Operator
|
||||
from mathutils import Matrix
|
||||
|
||||
|
||||
class _SetShadingBase:
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@staticmethod
|
||||
def _get_view3d_spaces(context):
|
||||
if getattr(context.area, "type", None) == "VIEW_3D":
|
||||
return (context.area.spaces[0],)
|
||||
return (area.spaces[0] for area in getattr(context.screen, "areas", ()) if area.type == "VIEW_3D")
|
||||
|
||||
@staticmethod
|
||||
def _reset_color_management(context, use_display_device=True):
|
||||
try:
|
||||
context.scene.display_settings.display_device = ("None", "sRGB")[use_display_device]
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _reset_material_shading(context, use_shadeless=False):
|
||||
for i in (x for x in context.scene.objects if x.type == "MESH" and x.mmd_type == "NONE"):
|
||||
for s in i.material_slots:
|
||||
if s.material is None:
|
||||
continue
|
||||
s.material.use_nodes = False
|
||||
s.material.use_shadeless = use_shadeless
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.render.engine = "BLENDER_EEVEE_NEXT"
|
||||
|
||||
shading_mode = getattr(self, "_shading_mode", None)
|
||||
for space in self._get_view3d_spaces(context):
|
||||
shading = space.shading
|
||||
shading.type = "SOLID"
|
||||
shading.light = "FLAT" if shading_mode == "SHADELESS" else "STUDIO"
|
||||
shading.color_type = "TEXTURE" if shading_mode else "MATERIAL"
|
||||
shading.show_object_outline = False
|
||||
shading.show_backface_culling = False
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SetGLSLShading(Operator, _SetShadingBase):
|
||||
bl_idname = "mmd_tools.set_glsl_shading"
|
||||
bl_label = "GLSL View"
|
||||
bl_description = "Use GLSL shading with additional lighting"
|
||||
|
||||
_shading_mode = "GLSL"
|
||||
|
||||
|
||||
class SetShadelessGLSLShading(Operator, _SetShadingBase):
|
||||
bl_idname = "mmd_tools.set_shadeless_glsl_shading"
|
||||
bl_label = "Shadeless GLSL View"
|
||||
bl_description = "Use only toon shading"
|
||||
|
||||
_shading_mode = "SHADELESS"
|
||||
|
||||
|
||||
class ResetShading(Operator, _SetShadingBase):
|
||||
bl_idname = "mmd_tools.reset_shading"
|
||||
bl_label = "Reset View"
|
||||
bl_description = "Reset to default Blender shading"
|
||||
|
||||
|
||||
class FlipPose(Operator):
|
||||
bl_idname = "mmd_tools.flip_pose"
|
||||
bl_label = "Flip Pose"
|
||||
bl_description = "Apply the current pose of selected bones to matching bone on opposite side of X-Axis."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
# https://docs.blender.org/manual/en/dev/rigging/armatures/bones/editing/naming.html
|
||||
__LR_REGEX = [
|
||||
{"re": re.compile(r"^(.+)(RIGHT|LEFT)(\.\d+)?$", re.IGNORECASE), "lr": 1},
|
||||
{"re": re.compile(r"^(.+)([\.\- _])(L|R)(\.\d+)?$", re.IGNORECASE), "lr": 2},
|
||||
{"re": re.compile(r"^(LEFT|RIGHT)(.+)$", re.IGNORECASE), "lr": 0},
|
||||
{"re": re.compile(r"^(L|R)([\.\- _])(.+)$", re.IGNORECASE), "lr": 0},
|
||||
{"re": re.compile(r"^(.+)(左|右)(\.\d+)?$"), "lr": 1},
|
||||
{"re": re.compile(r"^(左|右)(.+)$"), "lr": 0},
|
||||
]
|
||||
__LR_MAP = {
|
||||
"RIGHT": "LEFT",
|
||||
"Right": "Left",
|
||||
"right": "left",
|
||||
"LEFT": "RIGHT",
|
||||
"Left": "Right",
|
||||
"left": "right",
|
||||
"L": "R",
|
||||
"l": "r",
|
||||
"R": "L",
|
||||
"r": "l",
|
||||
"左": "右",
|
||||
"右": "左",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def flip_name(cls, name):
|
||||
for regex in cls.__LR_REGEX:
|
||||
match = regex["re"].match(name)
|
||||
if match:
|
||||
groups = match.groups()
|
||||
lr = groups[regex["lr"]]
|
||||
if lr in cls.__LR_MAP:
|
||||
flip_lr = cls.__LR_MAP[lr]
|
||||
name = ""
|
||||
for i, s in enumerate(groups):
|
||||
if i == regex["lr"]:
|
||||
name += flip_lr
|
||||
elif s:
|
||||
name += s
|
||||
return name
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def __cmul(vec1, vec2):
|
||||
return type(vec1)([x * y for x, y in zip(vec1, vec2)])
|
||||
|
||||
@staticmethod
|
||||
def __matrix_compose(loc, rot, scale):
|
||||
return (Matrix.Translation(loc) @ rot.to_matrix().to_4x4()) @ Matrix([(scale[0], 0, 0, 0), (0, scale[1], 0, 0), (0, 0, scale[2], 0), (0, 0, 0, 1)])
|
||||
|
||||
@classmethod
|
||||
def __flip_pose(cls, matrix_basis, bone_src, bone_dest):
|
||||
from mathutils import Quaternion
|
||||
|
||||
m = bone_dest.bone.matrix_local.to_3x3().transposed()
|
||||
mi = bone_src.bone.matrix_local.to_3x3().transposed().inverted() if bone_src != bone_dest else m.inverted()
|
||||
loc, rot, scale = matrix_basis.decompose()
|
||||
loc = cls.__cmul(mi @ loc, (-1, 1, 1))
|
||||
rot = cls.__cmul(Quaternion(mi @ rot.axis, rot.angle).normalized(), (1, 1, -1, -1))
|
||||
bone_dest.matrix_basis = cls.__matrix_compose(m @ loc, Quaternion(m @ rot.axis, rot.angle).normalized(), scale)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.active_object and context.active_object.type == "ARMATURE" and context.active_object.mode == "POSE"
|
||||
|
||||
def execute(self, context):
|
||||
pose_bones = context.active_object.pose.bones
|
||||
for b, mat in [(x, x.matrix_basis.copy()) for x in context.selected_pose_bones]:
|
||||
self.__flip_pose(mat, b, pose_bones.get(self.flip_name(b.name), b))
|
||||
return {"FINISHED"}
|
||||
Reference in New Issue
Block a user