407 lines
15 KiB
Python
407 lines
15 KiB
Python
# -*- 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
|