blenderpython/suw_core/material_manager.py

842 lines
31 KiB
Python
Raw Normal View History

2025-08-01 17:13:30 +08:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
SUW Core - Material Manager Module
拆分自: suw_impl.py (Line 880-1200, 6470-6950)
用途: Blender材质管理纹理处理材质应用
版本: 1.0.0
作者: SUWood Team
"""
from .memory_manager import memory_manager
import time
import logging
from typing import Dict, Any, Optional
# 设置日志
logger = logging.getLogger(__name__)
# 检查Blender可用性
try:
import bpy
BLENDER_AVAILABLE = True
except ImportError:
BLENDER_AVAILABLE = False
# 【新增】材质类型常量 - 按照Ruby代码定义
MAT_TYPE_NORMAL = 0
MAT_TYPE_OBVERSE = 1
MAT_TYPE_NATURE = 2
# 导入内存管理器
# ==================== 材质管理器类 ====================
class MaterialManager:
"""材质管理器 - 负责所有材质相关操作"""
def __init__(self):
"""
初始化材质管理器 - 完全独立不依赖suw_impl
"""
self.textures = {} # 材质缓存
self.material_cache = {} # 【修复】添加缺少的材质缓存字典
self.material_stats = {
"materials_created": 0,
"textures_loaded": 0,
"creation_errors": 0
}
# 材质类型配置
self.mat_type = MAT_TYPE_NORMAL # 当前材质类型
self.back_material = True # 是否应用背面材质
logger.info("MaterialManager 初始化完成")
def init_materials(self):
"""初始化材质 - 减少注册调用"""
try:
if not BLENDER_AVAILABLE:
return
logger.debug("初始化材质...")
# 创建基础材质
materials_to_create = [
("mat_default", (0.8, 0.8, 0.8, 1.0)),
("mat_select", (1.0, 0.5, 0.0, 1.0)),
("mat_normal", (0.7, 0.7, 0.7, 1.0)),
("mat_obverse", (0.9, 0.9, 0.9, 1.0)),
("mat_reverse", (0.6, 0.6, 0.6, 1.0)),
("mat_thin", (0.5, 0.5, 0.5, 1.0)),
# 【新增】加工相关材质
("mat_machining", (0.0, 0.5, 1.0, 1.0)), # 蓝色 - 有效加工
("mat_cancelled", (0.5, 0.5, 0.5, 1.0)), # 灰色 - 取消的加工
]
for mat_name, color in materials_to_create:
if mat_name not in bpy.data.materials:
material = bpy.data.materials.new(name=mat_name)
material.use_nodes = True
# 设置基础颜色
if material.node_tree:
principled = material.node_tree.nodes.get(
"Principled BSDF")
if principled:
principled.inputs['Base Color'].default_value = color
# 只注册一次
memory_manager.register_object(material)
self.textures[mat_name] = material
else:
# 如果材质已存在,直接使用
self.textures[mat_name] = bpy.data.materials[mat_name]
logger.info("材质初始化完成")
except Exception as e:
logger.error(f"初始化材质失败: {e}")
def add_mat_rgb(self, mat_id: str, alpha: float, r: int, g: int, b: int):
"""添加RGB材质"""
try:
if not BLENDER_AVAILABLE:
return None
# 检查材质是否已存在
if mat_id in self.material_cache:
material_name = self.material_cache[mat_id]
if material_name in bpy.data.materials:
return bpy.data.materials[material_name]
# 创建新材质
material = bpy.data.materials.new(mat_id)
material.use_nodes = True
# 设置颜色
if material.node_tree:
principled = material.node_tree.nodes.get("Principled BSDF")
if principled:
color = (r/255.0, g/255.0, b/255.0, alpha)
principled.inputs[0].default_value = color
# 设置透明度
if alpha < 1.0:
material.blend_method = 'BLEND'
# Alpha input
principled.inputs[21].default_value = alpha
# 缓存材质
self.material_cache[mat_id] = material.name
self.textures[mat_id] = material
memory_manager.register_object(material)
logger.info(f"创建RGB材质: {mat_id}")
return material
except Exception as e:
logger.error(f"创建RGB材质失败: {e}")
return None
def get_texture(self, key: str):
"""获取纹理材质 - 修复版本支持Default_前缀查找"""
if not BLENDER_AVAILABLE:
return None
try:
# 检查键是否有效
if not key:
return self.textures.get("mat_default")
# 【修复1】从缓存中获取
if key in self.textures:
material = self.textures[key]
# 验证材质是否仍然有效
if material and material.name in bpy.data.materials:
return material
else:
# 清理无效的缓存
del self.textures[key]
# 【修复2】在现有材质中查找 - 支持多种匹配方式
for material in bpy.data.materials:
material_name = material.name
# 精确匹配
if key == material_name:
self.textures[key] = material
logger.info(f"✅ 找到精确匹配材质: {key}")
return material
# 包含匹配处理Default_前缀
if key in material_name:
self.textures[key] = material
logger.info(f"✅ 找到包含匹配材质: {key} -> {material_name}")
return material
# Default_前缀匹配
if material_name.startswith(f"Default_{key}"):
self.textures[key] = material
logger.info(f"✅ 找到Default_前缀材质: {key} -> {material_name}")
return material
# 【修复3】如果没找到尝试创建默认材质
logger.warning(f"未找到纹理: {key},尝试创建默认材质")
try:
# 创建默认材质
default_material = bpy.data.materials.new(
name=f"Default_{key}")
default_material.use_nodes = True
# 设置基础颜色
if default_material.node_tree:
principled = default_material.node_tree.nodes.get(
"Principled BSDF")
if principled:
# 使用灰色作为默认颜色
principled.inputs['Base Color'].default_value = (
0.7, 0.7, 0.7, 1.0)
# 缓存材质
self.textures[key] = default_material
if memory_manager:
memory_manager.register_object(default_material)
logger.info(f"✅ 创建默认材质: Default_{key}")
return default_material
except Exception as create_error:
logger.error(f"创建默认材质失败: {create_error}")
# 【修复4】返回默认材质
default_material = self.textures.get("mat_default")
if default_material and default_material.name in bpy.data.materials:
logger.warning(f"使用系统默认材质: {key}")
return default_material
logger.warning(f"未找到纹理: {key}")
return None
except Exception as e:
logger.error(f"获取纹理失败: {e}")
return None
def apply_material_to_face(self, face, material):
"""为面应用材质"""
try:
if not face or not material or not BLENDER_AVAILABLE:
return
if hasattr(face, 'data') and face.data:
if not face.data.materials:
face.data.materials.append(material)
else:
face.data.materials[0] = material
except Exception as e:
logger.error(f"为面应用材质失败: {e}")
def create_transparent_material(self):
"""创建透明材质"""
try:
if not BLENDER_AVAILABLE:
return None
# 检查是否已存在透明材质
transparent_mat_name = "mat_transparent"
if transparent_mat_name in self.textures:
return self.textures[transparent_mat_name]
# 创建透明材质
material = bpy.data.materials.new(name=transparent_mat_name)
material.use_nodes = True
material.blend_method = 'BLEND'
# 设置透明属性
if material.node_tree:
principled = material.node_tree.nodes.get("Principled BSDF")
if principled:
# 设置基础颜色为半透明白色
principled.inputs['Base Color'].default_value = (
1.0, 1.0, 1.0, 0.5)
# 设置Alpha
principled.inputs['Alpha'].default_value = 0.5
# 缓存材质
self.textures[transparent_mat_name] = material
memory_manager.register_object(material)
logger.info("创建透明材质完成")
return material
except Exception as e:
logger.error(f"创建透明材质失败: {e}")
return None
# ==================== 【修复】添加缺少的c02方法 ====================
def c02(self, data: Dict[str, Any]):
"""add_texture - 添加纹理"""
try:
logger.info(
f"🎨 MaterialManager.c02: 处理纹理 {data.get('ckey', 'unknown')}")
if not BLENDER_AVAILABLE:
logger.warning("Blender不可用跳过纹理创建")
return None
ckey = data.get("ckey")
if not ckey:
logger.warning("纹理键为空,跳过创建")
return None
# 检查纹理是否已存在
if ckey in self.textures:
existing_material = self.textures[ckey]
if existing_material and existing_material.name in bpy.data.materials:
logger.info(f"✅ 纹理 {ckey} 已存在")
return existing_material
else:
# 清理无效缓存
del self.textures[ckey]
# 创建新材质
material = bpy.data.materials.new(name=ckey)
material.use_nodes = True
# 获取材质节点
nodes = material.node_tree.nodes
links = material.node_tree.links
# 清理默认节点
nodes.clear()
# 创建基础节点
principled = nodes.new(type='ShaderNodeBsdfPrincipled')
principled.location = (0, 0)
output = nodes.new(type='ShaderNodeOutputMaterial')
output.location = (300, 0)
# 连接基础节点
links.new(principled.outputs['BSDF'], output.inputs['Surface'])
# 设置纹理图像
src_path = data.get("src")
if src_path:
try:
import os
if os.path.exists(src_path):
# 加载图像
image_name = os.path.basename(src_path)
image = bpy.data.images.get(image_name)
if not image:
image = bpy.data.images.load(src_path)
if memory_manager:
memory_manager.register_image(image)
# 创建纹理节点
tex_coord = nodes.new(type='ShaderNodeTexCoord')
tex_coord.location = (-600, 0)
tex_image = nodes.new(type='ShaderNodeTexImage')
tex_image.image = image
tex_image.location = (-300, 0)
# 连接节点
links.new(
tex_coord.outputs['UV'], tex_image.inputs['Vector'])
links.new(
tex_image.outputs['Color'], principled.inputs['Base Color'])
# 透明度
alpha_value = data.get("alpha", 1.0)
if alpha_value < 1.0:
links.new(
tex_image.outputs['Alpha'], tex_image.inputs['Alpha'])
material.blend_method = 'BLEND'
else:
# 文件不存在,使用纯色
principled.inputs['Base Color'].default_value = (
0.5, 0.5, 0.5, 1.0)
logger.warning(f"纹理文件不存在: {src_path}")
except Exception as img_error:
logger.error(f"加载图像失败: {img_error}")
# 红色表示错误
principled.inputs['Base Color'].default_value = (
1.0, 0.0, 0.0, 1.0)
else:
# 没有图片路径使用RGB数据
r = data.get("r", 128) / 255.0
g = data.get("g", 128) / 255.0
b = data.get("b", 128) / 255.0
principled.inputs['Base Color'].default_value = (r, g, b, 1.0)
# 设置透明度
alpha_value = data.get("alpha", 1.0)
principled.inputs['Alpha'].default_value = alpha_value
if alpha_value < 1.0:
material.blend_method = 'BLEND'
# 设置其他属性
if "reflection" in data:
metallic_value = data["reflection"]
principled.inputs['Metallic'].default_value = metallic_value
if "reflection_glossiness" in data:
roughness_value = 1.0 - data["reflection_glossiness"]
principled.inputs['Roughness'].default_value = roughness_value
# 缓存材质
self.textures[ckey] = material
if memory_manager:
memory_manager.register_object(material)
# 更新统计
if hasattr(self, 'material_stats'):
self.material_stats["materials_created"] += 1
logger.info(f"✅ 创建纹理材质成功: {ckey}")
return material
except Exception as e:
logger.error(f"❌ MaterialManager.c02 执行失败: {e}")
self.material_stats["creation_errors"] += 1
return None
# ==================== 其他方法继续 ====================
def textured_surf(self, face, back_material, color, saved_color=None, scale_a=None, angle_a=None):
"""为表面应用纹理 - 保持原始方法名和参数"""
try:
if not face or not BLENDER_AVAILABLE:
return
# 获取材质
material = None
if color:
material = self.get_texture(color)
if not material and saved_color:
material = self.get_texture(saved_color)
if not material:
material = self.get_texture("mat_default")
# 应用材质
if material:
self.apply_material_to_face(face, material)
# 应用纹理变换
if scale_a or angle_a:
self.apply_texture_transform(face, material, scale_a, angle_a)
except Exception as e:
logger.error(f"应用表面纹理失败: {e}")
def apply_texture_transform(self, face, material, scale=None, angle=None):
"""应用纹理变换 - 保持原始方法名和参数"""
try:
if not face or not material or not BLENDER_AVAILABLE:
return
if not hasattr(face, 'data') or not face.data:
return
mesh = face.data
# 确保有UV层
if not mesh.uv_layers:
mesh.uv_layers.new(name="UVMap")
uv_layer = mesh.uv_layers.active
if uv_layer:
self.apply_uv_transform(uv_layer, scale, angle)
except Exception as e:
logger.error(f"应用纹理变换失败: {e}")
def apply_uv_transform(self, uv_layer, scale, angle):
"""应用UV变换 - 保持原始方法名和参数"""
try:
if not uv_layer:
return
import math
# 应用缩放和旋转
if scale or angle:
for loop in uv_layer.data:
u, v = loop.uv
# 应用缩放
if scale:
u *= scale
v *= scale
# 应用旋转
if angle:
angle_rad = math.radians(angle)
cos_a = math.cos(angle_rad)
sin_a = math.sin(angle_rad)
# 绕中心点旋转
u_centered = u - 0.5
v_centered = v - 0.5
u_new = u_centered * cos_a - v_centered * sin_a + 0.5
v_new = u_centered * sin_a + v_centered * cos_a + 0.5
u, v = u_new, v_new
loop.uv = (u, v)
except Exception as e:
logger.error(f"应用UV变换失败: {e}")
def rotate_texture(self, face, scale, angle):
"""旋转纹理 - 保持原始方法名和参数"""
try:
if not face or not BLENDER_AVAILABLE:
return
if not hasattr(face, 'data') or not face.data:
return
mesh = face.data
if not mesh.uv_layers:
return
uv_layer = mesh.uv_layers.active
if uv_layer:
self.apply_uv_transform(uv_layer, scale, angle)
except Exception as e:
logger.error(f"旋转纹理失败: {e}")
def set_mat_type(self, mat_type: int):
"""设置材质类型"""
self.mat_type = mat_type
logger.info(f"设置材质类型: {mat_type}")
def get_mat_type(self) -> int:
"""获取当前材质类型"""
return self.mat_type
def clear_material_cache(self):
"""清理材质缓存"""
try:
if hasattr(self, 'material_cache'):
self.material_cache.clear()
# 保留基础材质,清理其他缓存
base_materials = ["mat_default", "mat_select",
"mat_normal", "mat_obverse", "mat_reverse", "mat_thin"]
filtered_textures = {
k: v for k, v in self.textures.items() if k in base_materials}
self.textures = filtered_textures
logger.info("材质缓存清理完成")
except Exception as e:
logger.error(f"清理材质缓存失败: {e}")
# ==================== 【新增】c11和c30命令方法 ====================
def c11(self, data: Dict[str, Any]):
"""part_obverse - 设置零件正面显示 - 按照Ruby逻辑实现"""
try:
if not BLENDER_AVAILABLE:
logger.warning("Blender不可用跳过零件正面显示设置")
return 0
uid = data.get("uid")
v = data.get("v", False)
# 【按照Ruby逻辑】设置材质类型
if v:
self.mat_type = MAT_TYPE_OBVERSE # MAT_TYPE_OBVERSE = 1
logger.info("设置材质类型为正面显示")
else:
self.mat_type = MAT_TYPE_NORMAL # MAT_TYPE_NORMAL = 0
logger.info("设置材质类型为正常显示")
# 获取零件数据
from .data_manager import get_data_manager
data_manager = get_data_manager()
parts_data = data_manager.get_parts({"uid": uid})
processed_count = 0
for root, part in parts_data.items():
if part and hasattr(part, 'data'):
try:
self._textured_part(part, False)
processed_count += 1
except Exception as e:
logger.warning(f"处理零件失败: {root}, {e}")
logger.info(f"✅ 设置零件正面显示: {processed_count}")
return processed_count
except Exception as e:
logger.error(f"❌ 设置零件正面显示失败: {e}")
return 0
def c30(self, data: Dict[str, Any]):
"""part_nature - 设置零件自然显示 - 按照Ruby逻辑实现"""
try:
if not BLENDER_AVAILABLE:
logger.warning("Blender不可用跳过零件自然显示设置")
return 0
uid = data.get("uid")
v = data.get("v", False)
# 【按照Ruby逻辑】设置材质类型
if v:
self.mat_type = MAT_TYPE_NATURE # MAT_TYPE_NATURE = 2
logger.info("设置材质类型为自然显示")
else:
self.mat_type = MAT_TYPE_NORMAL # MAT_TYPE_NORMAL = 0
logger.info("设置材质类型为正常显示")
# 获取零件数据
from .data_manager import get_data_manager
data_manager = get_data_manager()
parts_data = data_manager.get_parts({"uid": uid})
processed_count = 0
for root, part in parts_data.items():
if part and hasattr(part, 'data'):
try:
self._textured_part(part, False)
processed_count += 1
except Exception as e:
logger.warning(f"处理零件失败: {root}, {e}")
logger.info(f"✅ 设置零件自然显示: {processed_count}")
return processed_count
except Exception as e:
logger.error(f"❌ 设置零件自然显示失败: {e}")
return 0
def _textured_part(self, part, selected: bool):
"""为零件应用纹理 - 按照Ruby逻辑实现"""
try:
if not part or not hasattr(part, 'data'):
return
# 【按照Ruby逻辑】处理零件的每个子对象
for child in part.children:
if not child:
continue
# 跳过非模型部件
child_type = self._get_part_attribute(child, "typ", "")
if child_type != "cp":
continue
# 跳过加工和拉手
if child_type in ["work", "pull"]:
continue
# 【按照Ruby逻辑】处理可见性
if self.mat_type == MAT_TYPE_NATURE:
# 自然模式下,模型部件隐藏,虚拟部件显示
if hasattr(child, 'type') and child.type == 'MESH':
child.hide_viewport = True
child.hide_render = True
elif self._get_part_attribute(child, "virtual", False):
child.hide_viewport = False
child.hide_render = False
else:
# 其他模式下,模型部件显示,虚拟部件隐藏
if hasattr(child, 'type') and child.type == 'MESH':
child.hide_viewport = False
child.hide_render = False
elif self._get_part_attribute(child, "virtual", False):
child.hide_viewport = True
child.hide_render = True
# 【按照Ruby逻辑】为面应用材质
self._apply_part_materials(child, selected)
except Exception as e:
logger.error(f"为零件应用纹理失败: {e}")
def _apply_part_materials(self, obj, selected: bool):
"""为对象应用材质 - 按照Ruby逻辑实现"""
try:
if not obj:
return
# 确定材质类型
material_key = None
if selected:
material_key = "mat_select"
elif self.mat_type == MAT_TYPE_NATURE:
# 自然模式下根据材质编号选择材质
mn = self._get_part_attribute(obj, "mn", 0)
if mn == 1:
material_key = "mat_obverse" # 门板
elif mn == 2:
material_key = "mat_reverse" # 柜体
elif mn == 3:
material_key = "mat_thin" # 背板
else:
# 正常模式或正面模式,使用原始材质
material_key = self._get_part_attribute(
obj, "ckey", "mat_default")
# 获取材质
material = self.get_texture(material_key)
if not material:
material = self.get_texture("mat_default")
# 应用材质到对象
if hasattr(obj, 'data') and obj.data:
if not obj.data.materials:
obj.data.materials.append(material)
else:
obj.data.materials[0] = material
except Exception as e:
logger.error(f"为对象应用材质失败: {e}")
def _get_part_attribute(self, obj, attr_name: str, default_value=None):
"""获取零件属性 - 支持多种对象类型"""
try:
if hasattr(obj, 'get'):
# 如果是字典或类似对象
return obj.get(attr_name, default_value)
elif hasattr(obj, 'sw'):
# 如果有sw属性
return obj.sw.get(attr_name, default_value)
elif isinstance(obj, dict):
# 如果是字典
return obj.get(attr_name, default_value)
else:
# 尝试从Blender对象的自定义属性获取
try:
if hasattr(obj, attr_name):
return getattr(obj, attr_name)
elif hasattr(obj, 'sw'):
return obj.sw.get(attr_name, default_value)
elif hasattr(obj, 'get'):
return obj.get(attr_name, default_value)
except:
pass
return default_value
except Exception as e:
logger.debug(f"获取零件属性失败: {e}")
return default_value
def get_material_stats(self) -> Dict[str, Any]:
"""获取材质管理器统计信息"""
try:
stats = {
"manager_type": "MaterialManager",
"cached_textures": len(self.textures),
"cached_materials": len(getattr(self, 'material_cache', {})),
"current_mat_type": self.mat_type,
"back_material": self.back_material,
"blender_available": BLENDER_AVAILABLE
}
if BLENDER_AVAILABLE:
stats["total_blender_materials"] = len(bpy.data.materials)
return stats
except Exception as e:
logger.error(f"获取材质统计失败: {e}")
return {"error": str(e)}
# 【新增】加工材质应用方法
def apply_machining_material(self, obj):
"""应用加工材质 - 蓝色,表示有效加工"""
try:
if not BLENDER_AVAILABLE or not obj:
return
material = self.get_texture("mat_machining")
if not material:
# 如果材质不存在,创建一个
material = bpy.data.materials.new(name="mat_machining")
material.use_nodes = True
if material.node_tree:
principled = material.node_tree.nodes.get(
"Principled BSDF")
if principled:
principled.inputs['Base Color'].default_value = (
0.0, 0.5, 1.0, 1.0) # 蓝色
self.textures["mat_machining"] = material
# 应用材质到对象
if hasattr(obj, 'data') and obj.data:
if not obj.data.materials:
obj.data.materials.append(material)
else:
obj.data.materials[0] = material
except Exception as e:
logger.error(f"应用加工材质失败: {e}")
def apply_cancelled_machining_material(self, obj):
"""应用取消加工材质 - 灰色,表示取消的加工"""
try:
if not BLENDER_AVAILABLE or not obj:
return
material = self.get_texture("mat_cancelled")
if not material:
# 如果材质不存在,创建一个
material = bpy.data.materials.new(name="mat_cancelled")
material.use_nodes = True
if material.node_tree:
principled = material.node_tree.nodes.get(
"Principled BSDF")
if principled:
principled.inputs['Base Color'].default_value = (
0.5, 0.5, 0.5, 1.0) # 灰色
self.textures["mat_cancelled"] = material
# 应用材质到对象
if hasattr(obj, 'data') and obj.data:
if not obj.data.materials:
obj.data.materials.append(material)
else:
obj.data.materials[0] = material
except Exception as e:
logger.error(f"应用取消加工材质失败: {e}")
# ==================== 模块实例 ====================
# 全局实例将由SUWImpl初始化时设置
material_manager = None
def init_material_manager():
"""初始化材质管理器 - 不再需要suw_impl参数"""
global material_manager
material_manager = MaterialManager()
return material_manager
def get_material_manager():
"""获取全局材质管理器实例"""
global material_manager
if material_manager is None:
material_manager = init_material_manager()
return material_manager
# 自动初始化
material_manager = init_material_manager()