c31d25dd01
You can choose between errors, warning, info or full debug, errors will always log to ensure we don't have silent failures with debug on or off.
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
|