PMX Import now works
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