# -*- 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