Holy shit this was a pain
- Truly fixes PMX Import lol, i messed up completely - Updated MMD Tools to use Cats One
This commit is contained in:
+143
-99
@@ -1,22 +1,16 @@
|
||||
# -*- 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.
|
||||
# This file is part of MMD Tools.
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import bpy
|
||||
from bpy.props import BoolProperty, StringProperty, FloatProperty
|
||||
from bpy.types import Operator, Context, Object, Material
|
||||
|
||||
from typing import Set, Dict, Any, List, Tuple, Optional, Union, cast
|
||||
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
|
||||
from ....core.logging_setup import logger
|
||||
import traceback
|
||||
|
||||
|
||||
class ConvertMaterialsForCycles(Operator):
|
||||
@@ -25,14 +19,14 @@ class ConvertMaterialsForCycles(Operator):
|
||||
bl_description = "Convert materials of selected objects for Cycles."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
use_principled: BoolProperty(
|
||||
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: BoolProperty(
|
||||
clean_nodes: bpy.props.BoolProperty(
|
||||
name="Clean Nodes",
|
||||
description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
|
||||
default=False,
|
||||
@@ -40,27 +34,22 @@ class ConvertMaterialsForCycles(Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return next((x for x in context.selected_objects if x.type == "MESH"), None) is not None
|
||||
def poll(cls, context):
|
||||
return any(x.type == "MESH" for x in context.selected_objects)
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.prop(self, "use_principled")
|
||||
layout.prop(self, "clean_nodes")
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
try:
|
||||
context.scene.render.engine = "CYCLES"
|
||||
except Exception:
|
||||
logger.error(f"Failed to change to Cycles render engine: {traceback.format_exc()}")
|
||||
self.report({"ERROR"}, " * Failed to change to Cycles render engine.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
logger.info(f"Converting materials for Cycles with principled={self.use_principled}, clean_nodes={self.clean_nodes}")
|
||||
for obj in (x for x in context.selected_objects if x.type == "MESH"):
|
||||
logger.debug(f"Converting materials for object: {obj.name}")
|
||||
cycles_converter.convertToCyclesShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -70,21 +59,21 @@ class ConvertMaterials(Operator):
|
||||
bl_description = "Convert materials of selected objects."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
use_principled: BoolProperty(
|
||||
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: BoolProperty(
|
||||
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: FloatProperty(
|
||||
subsurface: bpy.props.FloatProperty(
|
||||
name="Subsurface",
|
||||
default=0.001,
|
||||
soft_min=0.000,
|
||||
@@ -94,41 +83,130 @@ class ConvertMaterials(Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return next((x for x in context.selected_objects if x.type == "MESH"), None) is not None
|
||||
def poll(cls, context):
|
||||
return any(x.type == "MESH" for x in context.selected_objects)
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
logger.info(f"Converting materials with principled={self.use_principled}, clean_nodes={self.clean_nodes}, subsurface={self.subsurface}")
|
||||
def execute(self, context):
|
||||
for obj in context.selected_objects:
|
||||
if obj.type != "MESH":
|
||||
continue
|
||||
logger.debug(f"Converting materials for object: {obj.name}")
|
||||
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'}
|
||||
|
||||
class MergeMaterials(Operator):
|
||||
bl_idname = "mmd_tools.merge_materials"
|
||||
bl_label = "Merge Materials"
|
||||
bl_description = "Merge materials with the same texture in selected objects. Only merges materials with exactly one texture node. Materials with no texture or with multiple textures are not merged. Please convert to Blender materials first."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return next((x for x in context.selected_objects if x.type == 'MESH'), None) is not None
|
||||
def poll(cls, context):
|
||||
return any(x.type == "MESH" for x in context.selected_objects)
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
logger.info("Converting BSDF materials to MMD shader")
|
||||
def execute(self, context):
|
||||
# Process all selected mesh objects
|
||||
for obj in context.selected_objects:
|
||||
if obj.type != 'MESH':
|
||||
if obj.type != "MESH":
|
||||
continue
|
||||
|
||||
self.merge_materials_for_object(context, obj)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def merge_materials_for_object(self, context, obj):
|
||||
"""Merge materials with same texture for a single object"""
|
||||
if not obj.data.materials:
|
||||
self.report({"INFO"}, f"Object '{obj.name}' has no materials")
|
||||
return
|
||||
|
||||
# Map texture paths to material indices and names
|
||||
texture_to_materials = defaultdict(list)
|
||||
|
||||
# Check each material
|
||||
for i, material in enumerate(obj.data.materials):
|
||||
# use_nodes is deprecated in 5.0 but always returns True, so check is safe
|
||||
if not material or not material.use_nodes:
|
||||
continue
|
||||
|
||||
# 1. Check texture node count (must be exactly 1)
|
||||
texture_nodes = [node for node in material.node_tree.nodes if node.type == "TEX_IMAGE"]
|
||||
if len(texture_nodes) != 1:
|
||||
continue
|
||||
|
||||
# 2. Record texture path and material info
|
||||
texture_node = texture_nodes[0]
|
||||
if texture_node.image:
|
||||
texture_path = bpy.path.abspath(texture_node.image.filepath)
|
||||
texture_to_materials[texture_path].append({"index": i, "name": material.name})
|
||||
|
||||
# Find material groups that need merging
|
||||
materials_to_merge = {path: materials for path, materials in texture_to_materials.items() if len(materials) > 1}
|
||||
|
||||
if not materials_to_merge:
|
||||
self.report({"INFO"}, f"No materials to merge in object '{obj.name}'")
|
||||
return
|
||||
|
||||
# Process each texture group
|
||||
context.view_layer.objects.active = obj
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
merge_details = []
|
||||
for texture_path, materials in materials_to_merge.items():
|
||||
# Use first material as target
|
||||
target_material = materials[0]
|
||||
target_index = target_material["index"]
|
||||
target_name = target_material["name"]
|
||||
|
||||
source_materials = []
|
||||
|
||||
# Reassign faces from other materials to target material
|
||||
for source_material in materials[1:]:
|
||||
source_index = source_material["index"]
|
||||
source_name = source_material["name"]
|
||||
source_materials.append(source_name)
|
||||
|
||||
bpy.ops.mesh.select_all(action="DESELECT")
|
||||
obj.active_material_index = source_index
|
||||
bpy.ops.object.material_slot_select()
|
||||
obj.active_material_index = target_index
|
||||
bpy.ops.object.material_slot_assign()
|
||||
|
||||
# Record merge details
|
||||
texture_name = bpy.path.basename(texture_path)
|
||||
merge_details.append({"texture": texture_name, "target": target_name, "sources": source_materials})
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.ops.object.material_slot_remove_unused()
|
||||
|
||||
merged_count = sum(len(details["sources"]) for details in merge_details)
|
||||
self.report({"INFO"}, f"Object '{obj.name}': Merged {merged_count} materials")
|
||||
|
||||
for details in merge_details:
|
||||
sources_text = ", ".join(details["sources"])
|
||||
self.report({"INFO"}, f"Same Texture '{details['texture']}': Merged materials [{sources_text}] into '{details['target']}'")
|
||||
|
||||
|
||||
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 any(x.type == "MESH" for x in context.selected_objects)
|
||||
|
||||
def execute(self, context):
|
||||
for obj in context.selected_objects:
|
||||
if obj.type != "MESH":
|
||||
continue
|
||||
logger.debug(f"Converting BSDF materials for object: {obj.name}")
|
||||
cycles_converter.convertToMMDShader(obj)
|
||||
return {'FINISHED'}
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class _OpenTextureBase:
|
||||
"""Create a texture for mmd model material."""
|
||||
|
||||
bl_options: Set[str] = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
filepath: StringProperty(
|
||||
name="File Path",
|
||||
@@ -142,7 +220,7 @@ class _OpenTextureBase:
|
||||
options={"HIDDEN"},
|
||||
)
|
||||
|
||||
def invoke(self, context: Context, event: Any) -> Set[str]:
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
@@ -152,13 +230,8 @@ class OpenTexture(Operator, _OpenTextureBase):
|
||||
bl_label = "Open Texture"
|
||||
bl_description = "Create main texture of active material"
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
mat = context.active_object.active_material
|
||||
if not mat:
|
||||
logger.error("No active material found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
logger.info(f"Creating texture for material: {mat.name} from {self.filepath}")
|
||||
fnMat = FnMaterial(mat)
|
||||
fnMat.create_texture(self.filepath)
|
||||
return {"FINISHED"}
|
||||
@@ -172,13 +245,8 @@ class RemoveTexture(Operator):
|
||||
bl_description = "Remove main texture of active material"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
mat = context.active_object.active_material
|
||||
if not mat:
|
||||
logger.error("No active material found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
logger.info(f"Removing texture from material: {mat.name}")
|
||||
fnMat = FnMaterial(mat)
|
||||
fnMat.remove_texture()
|
||||
return {"FINISHED"}
|
||||
@@ -191,13 +259,8 @@ class OpenSphereTextureSlot(Operator, _OpenTextureBase):
|
||||
bl_label = "Open Sphere Texture"
|
||||
bl_description = "Create sphere texture of active material"
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
mat = context.active_object.active_material
|
||||
if not mat:
|
||||
logger.error("No active material found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
logger.info(f"Creating sphere texture for material: {mat.name} from {self.filepath}")
|
||||
fnMat = FnMaterial(mat)
|
||||
fnMat.create_sphere_texture(self.filepath, context.active_object)
|
||||
return {"FINISHED"}
|
||||
@@ -211,13 +274,8 @@ class RemoveSphereTexture(Operator):
|
||||
bl_description = "Remove sphere texture of active material"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
mat = context.active_object.active_material
|
||||
if not mat:
|
||||
logger.error("No active material found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
logger.info(f"Removing sphere texture from material: {mat.name}")
|
||||
fnMat = FnMaterial(mat)
|
||||
fnMat.remove_sphere_texture()
|
||||
return {"FINISHED"}
|
||||
@@ -230,21 +288,17 @@ class MoveMaterialUp(Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE"
|
||||
return bool(valid_mesh and obj.active_material_index > 0)
|
||||
return obj is not None and obj.type == "MESH" and obj.mmd_type == "NONE" and obj.active_material_index > 0
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
current_idx = obj.active_material_index
|
||||
prev_index = current_idx - 1
|
||||
|
||||
logger.debug(f"Moving material {current_idx} up to position {prev_index} for object {obj.name}")
|
||||
try:
|
||||
FnMaterial.swap_materials(obj, current_idx, prev_index, reverse=True, swap_slots=True)
|
||||
except MaterialNotFoundError:
|
||||
logger.error(f"Materials not found for indices {current_idx} and {prev_index}")
|
||||
self.report({"ERROR"}, "Materials not found")
|
||||
return {"CANCELLED"}
|
||||
obj.active_material_index = prev_index
|
||||
@@ -259,21 +313,17 @@ class MoveMaterialDown(Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE"
|
||||
return bool(valid_mesh and obj.active_material_index < len(obj.material_slots) - 1)
|
||||
return obj is not None and obj.type == "MESH" and obj.mmd_type == "NONE" and obj.active_material_index < len(obj.material_slots) - 1
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
current_idx = obj.active_material_index
|
||||
next_index = current_idx + 1
|
||||
|
||||
logger.debug(f"Moving material {current_idx} down to position {next_index} for object {obj.name}")
|
||||
try:
|
||||
FnMaterial.swap_materials(obj, current_idx, next_index, reverse=True, swap_slots=True)
|
||||
except MaterialNotFoundError:
|
||||
logger.error(f"Materials not found for indices {current_idx} and {next_index}")
|
||||
self.report({"ERROR"}, "Materials not found")
|
||||
return {"CANCELLED"}
|
||||
obj.active_material_index = next_index
|
||||
@@ -296,31 +346,26 @@ class EdgePreviewSetup(Operator):
|
||||
default="CREATE",
|
||||
)
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
from ..core.model import FnModel
|
||||
|
||||
root = FnModel.find_root_object(context.active_object)
|
||||
if root is None:
|
||||
logger.error("No MMD model root found")
|
||||
self.report({"ERROR"}, "Select a MMD model")
|
||||
return {"CANCELLED"}
|
||||
|
||||
if self.action == "CLEAN":
|
||||
logger.info(f"Cleaning toon edge for model: {root.name}")
|
||||
for obj in FnModel.iterate_mesh_objects(root):
|
||||
self.__clean_toon_edge(obj)
|
||||
else:
|
||||
from ..bpyutils import Props
|
||||
|
||||
logger.info(f"Creating toon edge for model: {root.name}")
|
||||
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))
|
||||
logger.info(f"Created {counts} toon edge(s)")
|
||||
self.report({"INFO"}, "Created %d toon edge(s)" % counts)
|
||||
return {"FINISHED"}
|
||||
|
||||
def __clean_toon_edge(self, obj: Object) -> None:
|
||||
logger.debug(f"Cleaning toon edge for object: {obj.name}")
|
||||
def __clean_toon_edge(self, obj):
|
||||
if "mmd_edge_preview" in obj.modifiers:
|
||||
obj.modifiers.remove(obj.modifiers["mmd_edge_preview"])
|
||||
|
||||
@@ -329,8 +374,7 @@ class EdgePreviewSetup(Operator):
|
||||
|
||||
FnMaterial.clean_materials(obj, can_remove=lambda m: m and m.name.startswith("mmd_edge."))
|
||||
|
||||
def __create_toon_edge(self, obj: Object, scale: float = 1.0) -> int:
|
||||
logger.debug(f"Creating toon edge for object: {obj.name} with scale {scale}")
|
||||
def __create_toon_edge(self, obj, scale=1.0):
|
||||
self.__clean_toon_edge(obj)
|
||||
materials = obj.data.materials
|
||||
material_offset = len(materials)
|
||||
@@ -355,10 +399,10 @@ class EdgePreviewSetup(Operator):
|
||||
mod.vertex_group = "mmd_edge_preview"
|
||||
return len(materials) - material_offset
|
||||
|
||||
def __create_edge_preview_group(self, obj: Object) -> None:
|
||||
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: Dict[int, float] = {}
|
||||
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}
|
||||
@@ -367,7 +411,7 @@ class EdgePreviewSetup(Operator):
|
||||
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: str, edge_color: Tuple[float, float, float, float], materials: List[Material]) -> Material:
|
||||
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)
|
||||
@@ -385,8 +429,8 @@ class EdgePreviewSetup(Operator):
|
||||
self.__make_shader(mat)
|
||||
return mat
|
||||
|
||||
def __make_shader(self, m: Material) -> None:
|
||||
# Note: material.use_nodes is deprecated in Blender 5.0 - materials always use nodes
|
||||
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)
|
||||
@@ -406,7 +450,7 @@ class EdgePreviewSetup(Operator):
|
||||
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) -> bpy.types.NodeTree:
|
||||
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):
|
||||
@@ -414,8 +458,8 @@ class EdgePreviewSetup(Operator):
|
||||
|
||||
ng = _NodeGroupUtils(shader)
|
||||
|
||||
node_input = ng.new_node("NodeGroupInput", (-5, 0))
|
||||
node_output = ng.new_node("NodeGroupOutput", (3, 0))
|
||||
ng.new_node("NodeGroupInput", (-5, 0))
|
||||
ng.new_node("NodeGroupOutput", (3, 0))
|
||||
|
||||
############################################################################
|
||||
node_color = ng.new_node("ShaderNodeMixRGB", (-1, -1.5))
|
||||
|
||||
Reference in New Issue
Block a user