From 3c381c2a2174bebd693b923e307b85e23a42f560 Mon Sep 17 00:00:00 2001 From: libtxixi <991670424@qq.com> Date: Fri, 18 Jul 2025 20:47:36 +0800 Subject: [PATCH] =?UTF-8?q?freestyle=E7=BA=BF=E6=A1=86=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E5=9C=BA=E6=99=AF=E7=BA=BF=E6=9D=A1=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Isometquick-server/blender_service.py | 2330 +++++++++++++------------ Isometquick-server/test_client.py | 2 +- auto_room.py | 1996 ++++++++++----------- 3 files changed, 2197 insertions(+), 2131 deletions(-) diff --git a/Isometquick-server/blender_service.py b/Isometquick-server/blender_service.py index a1d05b0..6dfde70 100644 --- a/Isometquick-server/blender_service.py +++ b/Isometquick-server/blender_service.py @@ -1,1154 +1,1176 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Blender渲染服务 -处理Blender渲染逻辑,适配Linux服务器环境 -""" - -import os -import subprocess -import asyncio -import tempfile -import shutil -import math -import time -from typing import Optional, Tuple -import logging - -from models import RenderRequest - -logger = logging.getLogger(__name__) - - -class BlenderRenderService: - """Blender渲染服务类""" - - def __init__(self, blender_path: str = None): - """ - 初始化Blender服务 - - Args: - blender_path: Blender可执行文件路径,如果为None则自动检测 - """ - self.blender_path = blender_path or self._find_blender_path() - self.output_dir = "/data/Isometquick/" - self._ensure_output_dir() - - def _find_blender_path(self) -> str: - """自动查找Blender路径""" - # Linux常见路径 - possible_paths = [ - "/data/blender/blender-4.2.11-linux-x64/blender", - "blender" # 系统PATH中 - ] - - for path in possible_paths: - if shutil.which(path): - logger.info(f"找到Blender: {path}") - return path - - # 如果都找不到,使用默认值 - logger.warning("未找到Blender,使用默认路径") - return "blender" - - def _ensure_output_dir(self): - """确保输出目录存在""" - try: - # 检查目录是否存在 - if not os.path.exists(self.output_dir): - logger.info(f"创建输出目录: {self.output_dir}") - os.makedirs(self.output_dir, exist_ok=True) - - # 检查目录权限 - if not os.access(self.output_dir, os.W_OK): - logger.error(f"输出目录无写权限: {self.output_dir}") - # 尝试使用本地目录作为备用 - self.output_dir = "./renders" - os.makedirs(self.output_dir, exist_ok=True) - logger.info(f"使用备用输出目录: {self.output_dir}") - - logger.info(f"输出目录设置为: {self.output_dir}") - - # 测试写入权限 - test_file = os.path.join(self.output_dir, "test_write.tmp") - try: - with open(test_file, 'w') as f: - f.write("test") - os.remove(test_file) - logger.info("输出目录写入权限测试通过") - except Exception as e: - logger.error(f"输出目录写入权限测试失败: {e}") - raise - - except PermissionError as e: - logger.error(f"无法创建输出目录 {self.output_dir}: 权限不足") - # 使用当前目录作为备用 - self.output_dir = "./renders" - os.makedirs(self.output_dir, exist_ok=True) - logger.info(f"使用备用输出目录: {self.output_dir}") - except Exception as e: - logger.error(f"创建输出目录失败 {self.output_dir}: {e}") - raise - - def check_blender_available(self) -> bool: - """检查Blender是否可用""" - try: - result = subprocess.run( - [self.blender_path, "--version"], - capture_output=True, - text=True, - timeout=10 - ) - return result.returncode == 0 - except Exception as e: - logger.error(f"Blender检查失败: {e}") - return False - - def _adjust_room_dimensions(self, request: RenderRequest) -> Tuple[float, float, float]: - """ - 调整房间尺寸:确保length(Y轴)始终大于等于width(X轴) - - Returns: - (adjusted_length, adjusted_width, height) - 调整后的长、宽、高 - """ - original_length = request.room.length # Y轴(长度) - original_width = request.room.width # X轴(宽度) - height = request.room.height # Z轴(高度) - - # 判断长宽大小,确保length始终大于等于width - if original_length >= original_width: - # 长大于等于宽,保持原值 - adjusted_length = original_length - adjusted_width = original_width - logger.info( - f"房间尺寸无需调整: length={adjusted_length}m, width={adjusted_width}m, height={height}m") - else: - # 宽大于长,交换值 - adjusted_length = original_width - adjusted_width = original_length - logger.info( - f"房间尺寸已调整: 原length={original_length}m, 原width={original_width}m") - logger.info( - f"调整后: length={adjusted_length}m, width={adjusted_width}m, height={height}m") - - return adjusted_length, adjusted_width, height - - def _calculate_camera_position(self, request: RenderRequest, adjusted_width: float) -> tuple: - """ - 根据房间尺寸和视图类型计算摄像机位置 - - Args: - request: 渲染请求参数 - adjusted_width: 调整后的房间宽度(X轴) - - Returns: - (x, y, z) 摄像机位置坐标 - """ - camera_height = request.camera.height # Z轴 - - if request.camera.view_type == 1: - # 正视图逻辑:x = width/2, y = 1, z = 相机高度 - x = adjusted_width / 2 # X = width/2 - y = 1 # Y = 1 - z = camera_height # Z = 相机高度 - else: - # 侧视图逻辑:保持原有设置 - x = adjusted_width - 1 # X = width - 1 - y = 1 # Y = 1 - z = camera_height # Z = 相机高度 - - logger.info(f"相机位置计算: x={x}, y={y}, z={z}") - return (x, y, z) - - def _calculate_camera_rotation(self, request: RenderRequest) -> tuple: - """ - 根据视图类型计算摄像机旋转角度 - - Returns: - (rx, ry, rz) 摄像机旋转角度(弧度) - """ - if request.camera.view_type == 1: - # 正视图逻辑:90, 0, 0 - rx = math.radians(90) - ry = math.radians(0) - rz = math.radians(0) - else: - # 侧视图逻辑(保持原有逻辑) - rx = math.radians(90) - ry = math.radians(0) - rz = math.radians(request.camera.rotation_angle) - - return (rx, ry, rz) - - def _generate_blender_script(self, task_id: str, request: RenderRequest) -> str: - """生成Blender Python脚本""" - - # 调整房间尺寸 - adjusted_length, adjusted_width, height = self._adjust_room_dimensions( - request) - - # 计算摄像机位置和旋转 - camera_pos = self._calculate_camera_position(request, adjusted_width) - camera_rot = self._calculate_camera_rotation(request) - - # 设置输出文件路径(使用绝对路径) - output_file = os.path.abspath(os.path.join( - self.output_dir, f"render_{task_id}.png")) - - # 渲染引擎固定为EEVEE - render_engine = "BLENDER_EEVEE_NEXT" - - # 计算灯光高度(房间高度减一米) - light_height = height - 1.0 - - # 道具类型 - prop_type = request.room.prop_type - - # 计算道具位置参数 - door_width = 1.0 # 门宽度1米 - window_width = 3.45 # 窗宽度3.45米 - wall_distance = 0.8 # 距后墙80cm - - script = f''' -import bpy -import bmesh -import os -import math -import time - -# 设置输出路径 -OUTPUT_FILE = "{output_file}" -print(f"输出文件路径: {{OUTPUT_FILE}}") - -# 定义道具类型变量 -PROP_TYPE = {prop_type} - -def disable_problematic_addons(): - """禁用可能导致问题的插件""" - try: - problematic_addons = ['bl_tool'] - for addon in problematic_addons: - if addon in bpy.context.preferences.addons: - bpy.ops.preferences.addon_disable(module=addon) - print(f"已禁用插件: {{addon}}") - except: - pass - -def create_isometric_room(): - """创建等轴测房间,使用isometric_room_gen插件""" - try: - disable_problematic_addons() - - # 清除默认立方体 - if "Cube" in bpy.data.objects: - bpy.data.objects.remove(bpy.data.objects["Cube"], do_unlink=True) - - # 设置3D游标位置到原点 - bpy.context.scene.cursor.location = (0.0, 0.0, 0.0) - - # 检查isometric_room_gen插件 - if not hasattr(bpy.context.scene, 'sna_room_width'): - print("错误: isometric_room_gen插件未正确加载") - return False - - # 设置isometric_room_gen插件参数 - # 房间设置 - 注意参数对应关系 - bpy.context.scene.sna_wall_thickness = 0.10 # 墙体厚度10cm - bpy.context.scene.sna_room_width = {adjusted_width} # 房间宽度(X轴) - bpy.context.scene.sna_room_depth = {adjusted_length} # 房间深度(Y轴) - bpy.context.scene.sna_room_height = {height} # 房间高度(Z轴) - - # 窗户设置 - 道具类型1(窗户)不在这里设置,而是通过archimesh插件创建 - bpy.context.scene.sna_windows_enum = 'NONE' # 无窗户 - - # 设置房间类型为方形 - bpy.context.scene.sna_style = 'Square' - - # 设置拱门参数 - bpy.context.scene.sna_arch_settings = True # 启用拱门设置 - if PROP_TYPE == 2: # 拱门 - bpy.context.scene.sna_arch_placement = 'BACK' # 设置为后墙拱门 - else: - bpy.context.scene.sna_arch_placement = 'NONE' # 不设置拱门 - bpy.context.scene.sna_arch_width = 1.2 # 拱门宽度1.2米 - bpy.context.scene.sna_arch_height = 2.4 # 拱门高度2.4米 - bpy.context.scene.sna_arch_thickness = 0.10 # 拱门厚度0.1米 - - # 找到3D视图区域 - view3d_area = None - for area in bpy.context.screen.areas: - if area.type == 'VIEW_3D': - view3d_area = area - break - - if view3d_area is None: - print("错误: 找不到3D视图区域") - return False - - # 临时切换到3D视图上下文 - with bpy.context.temp_override(area=view3d_area): - # 使用isometric_room_gen插件创建房间 - result = bpy.ops.sna.gen_room_1803a() - - if result == {{'FINISHED'}}: - print("等轴测房间创建完成!") - print(f"房间尺寸: {{bpy.context.scene.sna_room_width}}m x {{bpy.context.scene.sna_room_depth}}m x {{bpy.context.scene.sna_room_height}}m") - print(f"墙体厚度: {{bpy.context.scene.sna_wall_thickness}}m") - print(f"道具类型: {{PROP_TYPE}} (0=无, 1=窗, 2=拱门, 3=门)") - return True - else: - print("房间创建失败") - return False - - except Exception as e: - print(f"创建房间时出现错误: {{str(e)}}") - import traceback - traceback.print_exc() - return False - -def create_door_with_archimesh(): - """使用archimesh插件创建门""" - try: - print("开始创建archimesh门...") - - # 检查archimesh插件是否可用 - if not hasattr(bpy.ops.mesh, 'archimesh_door'): - print("错误: archimesh插件未正确加载") - print("请确保archimesh插件已安装并启用") - return False - - # 设置3D游标到原点 - bpy.context.scene.cursor.location = (0.0, 0.0, 0.0) - - # 找到3D视图区域 - view3d_area = None - for area in bpy.context.screen.areas: - if area.type == 'VIEW_3D': - view3d_area = area - break - - if view3d_area is None: - print("错误: 找不到3D视图区域") - return False - - # 临时切换到3D视图上下文 - with bpy.context.temp_override(area=view3d_area): - # 先创建门(使用默认参数) - result = bpy.ops.mesh.archimesh_door() - - if result == {{'FINISHED'}}: - print("archimesh门创建完成!") - - # 等待一帧以确保对象创建完成 - bpy.context.view_layer.update() - - # 找到刚创建的门组对象(Door_Group) - door_group = None - for obj in bpy.data.objects: - if obj.name == "Door_Group": - door_group = obj - break - - if door_group: - print(f"找到门组: {{door_group.name}}") - - # 查找主门对象(DoorFrame) - main_door_obj = None - for obj in bpy.data.objects: - if obj.name == "DoorFrame": - main_door_obj = obj - break - - if main_door_obj and hasattr(main_door_obj, 'DoorObjectGenerator'): - # 获取门属性 - door_props = main_door_obj.DoorObjectGenerator[0] - - print("设置门属性...") - print(f"找到主门对象: {{main_door_obj.name}}") - - # 先设置基本属性 - door_props.frame_width = 1.0 # 门框宽度 - door_props.frame_height = 2.1 # 门框高度 - door_props.frame_thick = 0.08 # 门框厚度 - door_props.openside = '1' # 右侧开门 - - # 设置门模型和把手(这些会触发update_object回调) - print("设置门模型为2...") - door_props.model = '2' # 门模型2 - - print("设置门把手为1...") - door_props.handle = '1' # 门把手1 - - print(f"门属性设置完成:") - - # 等待更新完成 - bpy.context.view_layer.update() - - # 验证设置是否生效 - print("验证门属性设置:") - print(f" - 当前门模型: {{door_props.model}}") - print(f" - 当前门把手: {{door_props.handle}}") - - # 强制触发更新(如果需要) - try: - # 重新设置属性以触发更新 - current_model = door_props.model - current_handle = door_props.handle - - door_props.model = '1' # 临时设置为其他值 - bpy.context.view_layer.update() - - door_props.model = current_model # 设置回目标值 - bpy.context.view_layer.update() - - print("已强制触发门模型更新") - - except Exception as update_error: - print(f"强制更新时出现错误: {{update_error}}") - - # 在修改门全部属性后进行位移和旋转 - print("开始移动和旋转门组...") - try: - door_group1 = None - for obj in bpy.data.objects: - if obj.name == "Door_Group": - door_group1 = obj - break - - # 计算门的位置:x=0, y=房间长度-门宽度/2-0.8, z=0 - room_length = {adjusted_length} # 房间长度(Y轴) - door_width = {door_width} # 门宽度1米 - wall_distance = {wall_distance} # 距后墙80cm - - door_x = 0.0 - door_y = room_length - (door_width / 2) - wall_distance - door_z = 0.0 - - # 设置位置 - door_group1.location = (door_x, door_y, door_z) - print(f"已将门组移动到位置: x={{door_x}}, y={{door_y}}, z={{door_z}}") - print(f"房间长度: {{room_length}}m, 门宽度: {{door_width}}m, 距后墙: {{wall_distance}}m") - - # 设置旋转 - door_group1.rotation_euler = (math.radians(0), math.radians(0), math.radians(90)) # 旋转(0, 0, 90度) - print(f"已将门组旋转到: (0°, 0°, 90°)") - - # 强制更新 - bpy.context.view_layer.update() - print("门组位移和旋转完成") - - except Exception as move_error: - print(f"移动和旋转门组时出现错误: {{move_error}}") - - else: - print("警告: 未找到主门对象或DoorObjectGenerator属性") - # 尝试查找其他可能的门属性 - for obj in bpy.data.objects: - if hasattr(obj, 'archimesh_door'): - print(f"找到archimesh_door属性在对象: {{obj.name}}") - - # 打印门组的所有子对象信息 - print("门组包含的对象:") - for obj in bpy.data.objects: - if obj.parent == door_group: - print(f" - {{obj.name}} (位置: {{obj.location}})") - - return True - else: - print("警告: 未找到Door_Group对象") - # 尝试查找其他可能的门对象 - door_objects = [obj for obj in bpy.data.objects if "Door" in obj.name] - if door_objects: - print("找到的门相关对象:") - for obj in door_objects: - print(f" - {{obj.name}} (类型: {{obj.type}})") - - # 尝试移动第一个找到的门对象 - first_door = door_objects[0] - # 计算门的位置 - room_length = {adjusted_length} - door_width = {door_width} - wall_distance = {wall_distance} - - door_y = room_length - (door_width / 2) - wall_distance - first_door.location = (0.0, door_y, 0.0) - print(f"已移动 {{first_door.name}} 到位置: (0.0, {{door_y}}, 0.0)") - return True - else: - print("未找到任何门相关对象") - return False - else: - print("门创建失败") - return False - - except Exception as e: - print(f"创建门时出现错误: {{str(e)}}") - import traceback - traceback.print_exc() - return False - -def create_window_with_archimesh(): - """使用archimesh插件创建落地窗""" - try: - print("开始创建archimesh落地窗...") - - # 检查archimesh插件是否可用 - if not hasattr(bpy.ops.mesh, 'archimesh_winpanel'): - print("错误: archimesh插件未正确加载") - print("请确保archimesh插件已安装并启用") - return False - - # 设置3D游标到原点 - bpy.context.scene.cursor.location = (0.0, 0.0, 0.0) - - # 找到3D视图区域 - view3d_area = None - for area in bpy.context.screen.areas: - if area.type == 'VIEW_3D': - view3d_area = area - break - - if view3d_area is None: - print("错误: 找不到3D视图区域") - return False - - # 临时切换到3D视图上下文 - with bpy.context.temp_override(area=view3d_area): - # 创建窗户 - result = bpy.ops.mesh.archimesh_winpanel() - - if result == {{'FINISHED'}}: - print("archimesh落地窗创建完成!") - - # 等待一帧以确保对象创建完成 - bpy.context.view_layer.update() - - # 找到刚创建的窗户组对象(Window_Group) - window_group = None - for obj in bpy.data.objects: - if obj.name == "Window_Group": - window_group = obj - break - - if window_group: - print(f"找到窗户组: {{window_group.name}}") - - # 查找主窗户对象(Window) - main_window_obj = None - for obj in bpy.data.objects: - if obj.name == "Window": - main_window_obj = obj - break - - if main_window_obj and hasattr(main_window_obj, 'WindowPanelGenerator'): - # 获取窗户属性 - window_props = main_window_obj.WindowPanelGenerator[0] - - print("设置窗户属性...") - print(f"找到主窗户对象: {{main_window_obj.name}}") - - # 设置窗户的基本属性 - window_props.gen = 4 # 4扇窗户(水平) - window_props.yuk = 1 # 1行(垂直) - window_props.kl1 = 5 # 外框厚度5cm - window_props.kl2 = 5 # 竖框厚度5cm - window_props.fk = 2 # 内框厚度2cm - - # 设置窗户尺寸 - window_props.gnx0 = 80 # 第1扇窗户宽度80cm - window_props.gnx1 = 80 # 第2扇窗户宽度80cm - window_props.gnx2 = 80 # 第3扇窗户宽度80cm - window_props.gnx3 = 80 # 第4扇窗户宽度80cm - - # 计算窗户高度:房间高度-40cm - room_height = {height} # 房间高度 - window_height_cm = int((room_height - 0.4) * 100) # 转换为厘米并转为整数 - window_props.gny0 = window_height_cm # 窗户高度(房间高度-40cm) - - print(f"窗户高度设置: 房间高度{{room_height}}m - 40cm = {{window_height_cm}}cm") - - # 设置窗户类型为平窗 - window_props.UST = '1' # 平窗 - window_props.r = 0 # 旋转角度0度 - - # 设置窗台 - window_props.mr = False # 启用窗台 - - # 设置材质 - window_props.mt1 = '1' # 外框材质PVC - window_props.mt2 = '1' # 内框材质塑料 - - # 设置窗户开启状态 - window_props.k00 = True # 第1扇窗户开启 - window_props.k01 = True # 第2扇窗户开启 - window_props.k02 = True # 第3扇窗户开启 - window_props.k03 = True # 第4扇窗户开启(如果有的话) - - print(f"窗户属性设置完成:") - print(f" - 水平扇数: {{window_props.gen}}") - print(f" - 窗户高度: {{window_height_cm}}cm (房间高度{{room_height}}m - 40cm)") - - # 修复窗户玻璃材质 - print("修复窗户玻璃材质...") - fix_window_glass_material() - - # 等待更新完成 - bpy.context.view_layer.update() - - # 强制触发更新(如果需要) - try: - # 重新设置属性以触发更新 - current_gen = window_props.gen - current_gny0 = window_props.gny0 - - window_props.gen = 2 # 临时设置为其他值 - bpy.context.view_layer.update() - - window_props.gen = current_gen # 设置回目标值 - bpy.context.view_layer.update() - - print("已强制触发窗户更新") - - except Exception as update_error: - print(f"强制更新时出现错误: {{update_error}}") - - # 在修改落地窗全部属性后进行位移和旋转 - print("开始移动和旋转窗户组...") - try: - window_group1 = None - - for obj in bpy.data.objects: - if obj.name == "Window_Group": - window_group1 = obj - break - - # 计算窗户的位置:x=0, y=房间长度-窗宽度/2-0.8, z=0 - room_length = {adjusted_length} # 房间长度(Y轴) - window_width = {window_width} # 窗宽度3.45米 - wall_distance = {wall_distance} # 距后墙80cm - - window_x = 0.0 - window_y = room_length - (window_width / 2) - wall_distance - window_z = 0.0 - - # 设置位置 - window_group1.location = (window_x, window_y, window_z) - print(f"已将窗户组移动到位置: x={{window_x}}, y={{window_y}}, z={{window_z}}") - print(f"房间长度: {{room_length}}m, 窗宽度: {{window_width}}m, 距后墙: {{wall_distance}}m") - - # 设置旋转 - window_group1.rotation_euler = (math.radians(0), math.radians(0), math.radians(90)) # 旋转(0, 0, 90度) - print(f"已将窗户组旋转到: (0°, 0°, 90°)") - - # 强制更新 - bpy.context.view_layer.update() - print("窗户组位移和旋转完成") - - except Exception as move_error: - print(f"移动和旋转窗户组时出现错误: {{move_error}}") - - else: - print("警告: 未找到主窗户对象或WindowPanelGenerator属性") - # 尝试查找其他可能的窗户属性 - for obj in bpy.data.objects: - if hasattr(obj, 'archimesh_window'): - print(f"找到archimesh_window属性在对象: {{obj.name}}") - - # 打印窗户组的所有子对象信息 - print("窗户组包含的对象:") - for obj in bpy.data.objects: - if obj.parent == window_group: - print(f" - {{obj.name}} (位置: {{obj.location}})") - - return True - else: - print("警告: 未找到Window_Group对象") - # 尝试查找其他可能的窗户对象 - window_objects = [obj for obj in bpy.data.objects if "Window" in obj.name] - if window_objects: - print("找到的窗户相关对象:") - for obj in window_objects: - print(f" - {{obj.name}} (类型: {{obj.type}})") - - # 尝试移动第一个找到的窗户对象 - first_window = window_objects[0] - # 计算窗户的位置 - room_length = {adjusted_length} - window_width = {window_width} - wall_distance = {wall_distance} - - window_y = room_length - (window_width / 2) - wall_distance - first_window.location = (0.0, window_y, 0.0) - print(f"已移动 {{first_window.name}} 到位置: (0.0, {{window_y}}, 0.0)") - return True - else: - print("未找到任何窗户相关对象") - return False - - else: - print("窗户创建失败") - return False - - except Exception as e: - print(f"创建窗户时出现错误: {{str(e)}}") - import traceback - traceback.print_exc() - return False - -def fix_window_glass_material(): - """修复窗户玻璃材质,将其改为高透BSDF""" - try: - print("开始修复窗户玻璃材质...") - - # 查找Window_Group下的Window对象 - window_group = None - for obj in bpy.data.objects: - if obj.name == "Window_Group": - window_group = obj - break - - if not window_group: - print("未找到Window_Group对象") - return False - - # 查找Window对象(Window_Group的子对象) - window_obj = None - for obj in bpy.data.objects: - if obj.name == "Window" and obj.parent == window_group: - window_obj = obj - break - - - if not window_obj: - print("未找到Window对象") - return False - - print(f"找到Window对象: {{window_obj.name}}") - - # 检查Window对象的材质 - if not window_obj.material_slots: - print("Window对象没有材质槽") - return False - - # 遍历所有材质槽 - for slot in window_obj.material_slots: - if slot.material and "Glass" in slot.material.name: - print(f"找到Glass材质: {{slot.material.name}}") - - # 启用节点编辑 - slot.material.use_nodes = True - - # 清除所有现有节点 - slot.material.node_tree.nodes.clear() - - # 创建半透BSDF节点 - translucent_bsdf = slot.material.node_tree.nodes.new(type='ShaderNodeBsdfTranslucent') - translucent_bsdf.location = (0, 0) - - # 设置半透BSDF参数 - translucent_bsdf.inputs['Color'].default_value = (0.95, 0.98, 1.0, 1.0) # 几乎无色 - # 半透BSDF节点没有Weight参数,只有Color参数 - - # 创建材质输出节点 - material_output = slot.material.node_tree.nodes.new(type='ShaderNodeOutputMaterial') - material_output.location = (300, 0) - - # 连接节点 - slot.material.node_tree.links.new( - translucent_bsdf.outputs['BSDF'], - material_output.inputs['Surface'] - ) - - # 设置材质混合模式为Alpha Blend - slot.material.blend_method = 'BLEND' - - print(f"已成功修改Glass材质为半透BSDF") - print(f" - 半透颜色: 几乎无色") - print(f" - 混合模式: Alpha Blend") - - return True - - print("未找到Glass材质") - return False - - except Exception as e: - print(f"修复玻璃材质时出现错误: {{str(e)}}") - import traceback - traceback.print_exc() - return False - -def setup_render_settings(): - """设置渲染参数(固定EEVEE渲染,优化速度)""" - scene = bpy.context.scene - - # 设置渲染引擎固定为EEVEE - scene.render.engine = 'BLENDER_EEVEE_NEXT' - print("已设置渲染引擎为: BLENDER_EEVEE_NEXT") - - # 启用Freestyle线条渲染 - scene.render.use_freestyle = True - print("已启用Freestyle线条渲染") - - # 配置Freestyle线条设置 - if hasattr(scene, 'view_layers'): - view_layer = scene.view_layers[0] # 获取第一个视图层 - view_layer.use_freestyle = True - - # 获取Freestyle线条设置 - freestyle = view_layer.freestyle_settings - - # 启用线条渲染 - freestyle.use_smoothness = True - freestyle.use_culling = True - - # 设置线条宽度 - 使用正确的API 没有生效 - - - # 设置世界环境 - if hasattr(scene, 'world') and scene.world: - # 启用世界节点 - scene.world.use_nodes = True - - # 清除现有节点 - scene.world.node_tree.nodes.clear() - - # 创建自发光节点 - emission_node = scene.world.node_tree.nodes.new(type='ShaderNodeEmission') - emission_node.location = (0, 0) - - # 设置HSV颜色:色相0,饱和度0,明度0.051,Alpha 1 - # 转换为RGB:HSV(0, 0, 0.051) = RGB(0.051, 0.051, 0.051) - emission_node.inputs['Color'].default_value = (0.051, 0.051, 0.051, 1.0) # 深灰色 - emission_node.inputs['Strength'].default_value = 8.0 # 强度8 - - # 创建世界输出节点 - world_output = scene.world.node_tree.nodes.new(type='ShaderNodeOutputWorld') - world_output.location = (300, 0) - - # 连接节点 - scene.world.node_tree.links.new( - emission_node.outputs['Emission'], - world_output.inputs['Surface'] - ) - - print("已设置世界环境为自发光") - print(f" - 颜色: HSV(0, 0, 0.051) = RGB(0.051, 0.051, 0.051)") - print(f" - 强度: 8.0") - - # 设置EEVEE采样(降低采样数提高速度) - try: - if hasattr(scene.eevee, 'taa_render_samples'): - scene.eevee.taa_render_samples = 32 # 从64降低到32 - print(f"已设置EEVEE渲染采样: {{scene.eevee.taa_render_samples}}") - elif hasattr(scene.eevee, 'taa_samples'): - scene.eevee.taa_samples = 32 # 从64降低到32 - print(f"已设置EEVEE采样: {{scene.eevee.taa_samples}}") - else: - print("警告: 无法找到EEVEE采样设置,使用默认值") - - # 启用屏幕空间反射(简化设置) - if hasattr(scene.eevee, 'use_ssr'): - scene.eevee.use_ssr = True - print("已启用屏幕空间反射") - - # 启用环境光遮蔽(简化设置) - if hasattr(scene.eevee, 'use_gtao'): - scene.eevee.use_gtao = True - print("已启用环境光遮蔽") - - # 启用透明渲染(必要) - if hasattr(scene.eevee, 'use_transparent'): - scene.eevee.use_transparent = True - print("已启用透明渲染") - - # 设置透明混合模式(必要) - if hasattr(scene.render, 'film_transparent'): - scene.render.film_transparent = True - print("已启用透明背景") - - except AttributeError as e: - print(f"EEVEE设置警告: {{e}}") - print("使用默认EEVEE渲染设置") - - # 设置分辨率 - scene.render.resolution_x = {request.render.resolution_x} - scene.render.resolution_y = {request.render.resolution_y} - scene.render.resolution_percentage = 100 - - # 设置输出格式 - scene.render.image_settings.file_format = 'PNG' - scene.render.image_settings.color_mode = 'RGBA' # 支持透明 - - # 设置输出路径(使用绝对路径) - scene.render.filepath = OUTPUT_FILE - - print(f"渲染设置完成,输出路径: {{scene.render.filepath}}") - print(f"分辨率: {{scene.render.resolution_x}}x{{scene.render.resolution_y}}") - return scene.render.filepath - -def setup_camera_and_lighting(): - """设置摄像机和照明(160W点光源 + 150W日光)""" - # 计算灯光高度(房间高度减一米) - room_height = {height} - light_height = room_height - 1.0 - - # 设置摄像机 - camera = None - if "Camera" in bpy.data.objects: - camera = bpy.data.objects["Camera"] - elif "Isometric Camera" in bpy.data.objects: - camera = bpy.data.objects["Isometric Camera"] - else: - # 创建新摄像机 - bpy.ops.object.camera_add(location={camera_pos}) - camera = bpy.context.active_object - camera.name = "Isometric Camera" - - # 设置摄像机位置和旋转 - camera.location = {camera_pos} - camera.rotation_euler = {camera_rot} - - # 设置为透视投影 - camera.data.type = 'PERSP' - camera.data.lens = 35.0 # 35mm焦距 - camera.data.sensor_width = 35.0 # 35mm传感器 - camera.data.sensor_fit = 'AUTO' # 自动适配 - - # 设置为场景的活动摄像机 - bpy.context.scene.camera = camera - print("摄像机设置完成") - print(f"相机位置: ({{camera.location.x}}, {{camera.location.y}}, {{camera.location.z}})") - - # 隐藏ISO Emission灯光(如果存在) - light_objects = ["ISO Emission Left", "ISO Emission Right"] - for obj_name in light_objects: - if obj_name in bpy.data.objects: - light_obj = bpy.data.objects[obj_name] - # 隐藏发光对象 - light_obj.hide_render = True - light_obj.hide_viewport = True - print(f"已隐藏 {{obj_name}}") - - # # 创建第一个160W点光源(房间中心) - # try: - # # 计算点光源位置(房间中心上方) - # room_length = {adjusted_length} # Y轴 - # room_width = {adjusted_width} # X轴 - - # # 第一个点光源位置:房间中心上方 - # light_x = 0 # X轴中心 - # light_y = 0 # Y轴中心 - # light_z = light_height - - # # 创建第一个点光源 - # bpy.ops.object.light_add( - # type='POINT', location=(light_x, light_y, light_z)) - # point_light1 = bpy.context.active_object - # point_light1.name = "Main Point Light" - # point_light1.data.energy = 160 # 160W - - # # 设置软衰减(简化设置) - # point_light1.data.shadow_soft_size = 0.5 # 0.5米半径,产生柔和阴影 - # point_light1.data.use_shadow = True # 启用阴影 - - # print(f"已创建第一个点光源(160W)") - # print(f"点光源位置: x={{light_x}}, y={{light_y}}, z={{light_z}}") - - # except Exception as e: - # print(f"创建第一个点光源时出错: {{e}}") - - - - print("照明设置完成") - #print("灯光类型: 点光源 + 日光") - print(f"阴影设置: 启用,柔和阴影") - #print(f"主灯光高度: {{light_height}}米 (房间高度减一米)") - -def render_scene(): - """渲染场景""" - print("开始渲染...") - print(f"输出路径: {{OUTPUT_FILE}}") - - # 确保输出目录存在 - output_dir = os.path.dirname(OUTPUT_FILE) - if not os.path.exists(output_dir): - os.makedirs(output_dir, exist_ok=True) - print(f"创建输出目录: {{output_dir}}") - - bpy.ops.render.render(write_still=True) - print(f"渲染完成!") - - # 验证文件是否创建 - if os.path.exists(OUTPUT_FILE): - file_size = os.path.getsize(OUTPUT_FILE) - print(f"输出文件已创建: {{OUTPUT_FILE}} (大小: {{file_size}} bytes)") - else: - print(f"警告: 输出文件未找到: {{OUTPUT_FILE}}") - # 列出输出目录中的文件 - if os.path.exists(output_dir): - files = os.listdir(output_dir) - print(f"输出目录中的文件: {{files}}") - - return OUTPUT_FILE - -def main(): - """主函数""" - print("=" * 60) - print("开始API渲染任务") - print("=" * 60) - - try: - # 1. 创建房间 - print("1. 创建等轴测房间...") - if not create_isometric_room(): - print("房间创建失败,停止执行") - return False - - # 2. 根据道具类型创建道具 - prop_type = PROP_TYPE - if prop_type == 1: # 窗户 - print("2. 创建archimesh落地窗...") - if not create_window_with_archimesh(): - print("落地窗创建失败,但继续执行") - elif prop_type == 2: # 拱门 - print("2. 拱门已通过isometric_room_gen插件创建") - elif prop_type == 3: # 门 - print("2. 创建archimesh门...") - if not create_door_with_archimesh(): - print("门创建失败,但继续执行") - else: - print("2. 无道具设置") - - # 3. 设置渲染参数 - print("3. 设置渲染参数...") - output_path = setup_render_settings() - - # 4. 设置摄像机和照明 - print("4. 设置摄像机和照明...") - setup_camera_and_lighting() - - # 5. 渲染场景 - print("5. 开始渲染...") - final_path = render_scene() - - print("=" * 60) - print("API渲染任务完成!") - print(f"输出文件: {{final_path}}") - print("=" * 60) - - return True - - except Exception as e: - print(f"执行过程中出现错误: {{str(e)}}") - import traceback - traceback.print_exc() - return False - -# 执行主函数 -if __name__ == "__main__": - main() -''' - - return script.strip(), output_file - - async def render_room(self, task_id: str, request: RenderRequest) -> str: - """ - 执行房间渲染 - - Args: - task_id: 任务ID - request: 渲染请求参数 - - Returns: - 输出文件路径 - """ - try: - # 确保输出目录存在 - self._ensure_output_dir() - - # 生成Blender脚本 - script_content, output_file = self._generate_blender_script( - task_id, request) - - # 创建临时脚本文件 - with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f: - f.write(script_content) - script_path = f.name - - try: - # 构建Blender命令 - cmd = [ - self.blender_path, - "--background", - "--disable-crash-handler", - "--python", script_path - ] - - logger.info(f"执行Blender命令: {' '.join(cmd)}") - logger.info(f"输出文件路径: {output_file}") - - # 执行Blender渲染 - process = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - - stdout, stderr = await asyncio.wait_for( - process.communicate(), - timeout=300 # 5分钟超时 - ) - - # 记录Blender输出 - if stdout: - logger.info(f"Blender stdout: {stdout.decode('utf-8')}") - if stderr: - logger.warning(f"Blender stderr: {stderr.decode('utf-8')}") - - # 检查执行结果 - if process.returncode == 0: - # 检查输出文件是否存在 - if os.path.exists(output_file): - file_size = os.path.getsize(output_file) - logger.info( - f"渲染成功: {output_file} (大小: {file_size} bytes)") - return output_file - else: - # 检查输出目录中的文件 - output_dir = os.path.dirname(output_file) - if os.path.exists(output_dir): - files = os.listdir(output_dir) - logger.error(f"输出文件不存在,但目录中有文件: {files}") - else: - logger.error(f"输出目录不存在: {output_dir}") - - # 尝试查找可能的输出文件 - possible_files = [] - for root, dirs, files in os.walk(output_dir): - for file in files: - if file.endswith('.png') and 'render' in file: - possible_files.append( - os.path.join(root, file)) - - if possible_files: - logger.info(f"找到可能的输出文件: {possible_files}") - return possible_files[0] - else: - raise Exception(f"渲染完成但输出文件不存在: {output_file}") - else: - error_msg = stderr.decode('utf-8') if stderr else "未知错误" - raise Exception( - f"Blender执行失败 (返回码: {process.returncode}): {error_msg}") - - finally: - # 清理临时脚本文件 - try: - os.unlink(script_path) - except: - pass - - except asyncio.TimeoutError: - raise Exception("渲染超时(5分钟)") - except Exception as e: - logger.error(f"渲染失败: {e}") - raise +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Blender渲染服务 +处理Blender渲染逻辑,适配Linux服务器环境 +""" + +import os +import subprocess +import asyncio +import tempfile +import shutil +import math +import time +from typing import Optional, Tuple +import logging + +from models import RenderRequest + +logger = logging.getLogger(__name__) + + +class BlenderRenderService: + """Blender渲染服务类""" + + def __init__(self, blender_path: str = None): + """ + 初始化Blender服务 + + Args: + blender_path: Blender可执行文件路径,如果为None则自动检测 + """ + self.blender_path = blender_path or self._find_blender_path() + self.output_dir = "/data/Isometquick/" + self._ensure_output_dir() + + def _find_blender_path(self) -> str: + """自动查找Blender路径""" + # Linux常见路径 + possible_paths = [ + "/data/blender/blender-4.2.11-linux-x64/blender", + "blender" # 系统PATH中 + ] + + for path in possible_paths: + if shutil.which(path): + logger.info(f"找到Blender: {path}") + return path + + # 如果都找不到,使用默认值 + logger.warning("未找到Blender,使用默认路径") + return "blender" + + def _ensure_output_dir(self): + """确保输出目录存在""" + try: + # 检查目录是否存在 + if not os.path.exists(self.output_dir): + logger.info(f"创建输出目录: {self.output_dir}") + os.makedirs(self.output_dir, exist_ok=True) + + # 检查目录权限 + if not os.access(self.output_dir, os.W_OK): + logger.error(f"输出目录无写权限: {self.output_dir}") + # 尝试使用本地目录作为备用 + self.output_dir = "./renders" + os.makedirs(self.output_dir, exist_ok=True) + logger.info(f"使用备用输出目录: {self.output_dir}") + + logger.info(f"输出目录设置为: {self.output_dir}") + + # 测试写入权限 + test_file = os.path.join(self.output_dir, "test_write.tmp") + try: + with open(test_file, 'w') as f: + f.write("test") + os.remove(test_file) + logger.info("输出目录写入权限测试通过") + except Exception as e: + logger.error(f"输出目录写入权限测试失败: {e}") + raise + + except PermissionError as e: + logger.error(f"无法创建输出目录 {self.output_dir}: 权限不足") + # 使用当前目录作为备用 + self.output_dir = "./renders" + os.makedirs(self.output_dir, exist_ok=True) + logger.info(f"使用备用输出目录: {self.output_dir}") + except Exception as e: + logger.error(f"创建输出目录失败 {self.output_dir}: {e}") + raise + + def check_blender_available(self) -> bool: + """检查Blender是否可用""" + try: + result = subprocess.run( + [self.blender_path, "--version"], + capture_output=True, + text=True, + timeout=10 + ) + return result.returncode == 0 + except Exception as e: + logger.error(f"Blender检查失败: {e}") + return False + + def _adjust_room_dimensions(self, request: RenderRequest) -> Tuple[float, float, float]: + """ + 调整房间尺寸:确保length(Y轴)始终大于等于width(X轴) + + Returns: + (adjusted_length, adjusted_width, height) - 调整后的长、宽、高 + """ + original_length = request.room.length # Y轴(长度) + original_width = request.room.width # X轴(宽度) + height = request.room.height # Z轴(高度) + + # 判断长宽大小,确保length始终大于等于width + if original_length >= original_width: + # 长大于等于宽,保持原值 + adjusted_length = original_length + adjusted_width = original_width + logger.info( + f"房间尺寸无需调整: length={adjusted_length}m, width={adjusted_width}m, height={height}m") + else: + # 宽大于长,交换值 + adjusted_length = original_width + adjusted_width = original_length + logger.info( + f"房间尺寸已调整: 原length={original_length}m, 原width={original_width}m") + logger.info( + f"调整后: length={adjusted_length}m, width={adjusted_width}m, height={height}m") + + return adjusted_length, adjusted_width, height + + def _calculate_camera_position(self, request: RenderRequest, adjusted_width: float) -> tuple: + """ + 根据房间尺寸和视图类型计算摄像机位置 + + Args: + request: 渲染请求参数 + adjusted_width: 调整后的房间宽度(X轴) + + Returns: + (x, y, z) 摄像机位置坐标 + """ + camera_height = request.camera.height # Z轴 + + if request.camera.view_type == 1: + # 正视图逻辑:x = width/2, y = 1, z = 相机高度 + x = adjusted_width / 2 # X = width/2 + y = 1 # Y = 1 + z = camera_height # Z = 相机高度 + else: + # 侧视图逻辑:保持原有设置 + x = adjusted_width - 1 # X = width - 1 + y = 1 # Y = 1 + z = camera_height # Z = 相机高度 + + logger.info(f"相机位置计算: x={x}, y={y}, z={z}") + return (x, y, z) + + def _calculate_camera_rotation(self, request: RenderRequest) -> tuple: + """ + 根据视图类型计算摄像机旋转角度 + + Returns: + (rx, ry, rz) 摄像机旋转角度(弧度) + """ + if request.camera.view_type == 1: + # 正视图逻辑:90, 0, 0 + rx = math.radians(90) + ry = math.radians(0) + rz = math.radians(0) + else: + # 侧视图逻辑(保持原有逻辑) + rx = math.radians(90) + ry = math.radians(0) + rz = math.radians(request.camera.rotation_angle) + + return (rx, ry, rz) + + def _generate_blender_script(self, task_id: str, request: RenderRequest) -> str: + """生成Blender Python脚本""" + + # 调整房间尺寸 + adjusted_length, adjusted_width, height = self._adjust_room_dimensions( + request) + + # 计算摄像机位置和旋转 + camera_pos = self._calculate_camera_position(request, adjusted_width) + camera_rot = self._calculate_camera_rotation(request) + + # 设置输出文件路径(使用绝对路径) + output_file = os.path.abspath(os.path.join( + self.output_dir, f"render_{task_id}.png")) + + # 渲染引擎固定为EEVEE + render_engine = "BLENDER_EEVEE_NEXT" + + # 计算灯光高度(房间高度减一米) + light_height = height - 1.0 + + # 道具类型 + prop_type = request.room.prop_type + + # 计算道具位置参数 + door_width = 1.0 # 门宽度1米 + window_width = 3.45 # 窗宽度3.45米 + wall_distance = 0.8 # 距后墙80cm + + script = f''' +import bpy +import bmesh +import os +import math +import time + +# 设置输出路径 +OUTPUT_FILE = "{output_file}" +print(f"输出文件路径: {{OUTPUT_FILE}}") + +# 定义道具类型变量 +PROP_TYPE = {prop_type} + +def disable_problematic_addons(): + """禁用可能导致问题的插件""" + try: + problematic_addons = ['bl_tool'] + for addon in problematic_addons: + if addon in bpy.context.preferences.addons: + bpy.ops.preferences.addon_disable(module=addon) + print(f"已禁用插件: {{addon}}") + except: + pass + +def create_isometric_room(): + """创建等轴测房间,使用isometric_room_gen插件""" + try: + disable_problematic_addons() + + # 清除默认立方体 + if "Cube" in bpy.data.objects: + bpy.data.objects.remove(bpy.data.objects["Cube"], do_unlink=True) + + # 设置3D游标位置到原点 + bpy.context.scene.cursor.location = (0.0, 0.0, 0.0) + + # 检查isometric_room_gen插件 + if not hasattr(bpy.context.scene, 'sna_room_width'): + print("错误: isometric_room_gen插件未正确加载") + return False + + # 设置isometric_room_gen插件参数 + # 房间设置 - 注意参数对应关系 + bpy.context.scene.sna_wall_thickness = 0.10 # 墙体厚度10cm + bpy.context.scene.sna_room_width = {adjusted_width} # 房间宽度(X轴) + bpy.context.scene.sna_room_depth = {adjusted_length} # 房间深度(Y轴) + bpy.context.scene.sna_room_height = {height} # 房间高度(Z轴) + + # 窗户设置 - 道具类型1(窗户)不在这里设置,而是通过archimesh插件创建 + bpy.context.scene.sna_windows_enum = 'NONE' # 无窗户 + + # 设置房间类型为方形 + bpy.context.scene.sna_style = 'Square' + + # 设置拱门参数 + bpy.context.scene.sna_arch_settings = True # 启用拱门设置 + if PROP_TYPE == 2: # 拱门 + bpy.context.scene.sna_arch_placement = 'BACK' # 设置为后墙拱门 + else: + bpy.context.scene.sna_arch_placement = 'NONE' # 不设置拱门 + bpy.context.scene.sna_arch_width = 1.2 # 拱门宽度1.2米 + bpy.context.scene.sna_arch_height = 2.4 # 拱门高度2.4米 + bpy.context.scene.sna_arch_thickness = 0.10 # 拱门厚度0.1米 + + # 找到3D视图区域 + view3d_area = None + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + view3d_area = area + break + + if view3d_area is None: + print("错误: 找不到3D视图区域") + return False + + # 临时切换到3D视图上下文 + with bpy.context.temp_override(area=view3d_area): + # 使用isometric_room_gen插件创建房间 + result = bpy.ops.sna.gen_room_1803a() + + if result == {{'FINISHED'}}: + print("等轴测房间创建完成!") + print(f"房间尺寸: {{bpy.context.scene.sna_room_width}}m x {{bpy.context.scene.sna_room_depth}}m x {{bpy.context.scene.sna_room_height}}m") + print(f"墙体厚度: {{bpy.context.scene.sna_wall_thickness}}m") + print(f"道具类型: {{PROP_TYPE}} (0=无, 1=窗, 2=拱门, 3=门)") + return True + else: + print("房间创建失败") + return False + + except Exception as e: + print(f"创建房间时出现错误: {{str(e)}}") + import traceback + traceback.print_exc() + return False + +def create_door_with_archimesh(): + """使用archimesh插件创建门""" + try: + print("开始创建archimesh门...") + + # 检查archimesh插件是否可用 + if not hasattr(bpy.ops.mesh, 'archimesh_door'): + print("错误: archimesh插件未正确加载") + print("请确保archimesh插件已安装并启用") + return False + + # 设置3D游标到原点 + bpy.context.scene.cursor.location = (0.0, 0.0, 0.0) + + # 找到3D视图区域 + view3d_area = None + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + view3d_area = area + break + + if view3d_area is None: + print("错误: 找不到3D视图区域") + return False + + # 临时切换到3D视图上下文 + with bpy.context.temp_override(area=view3d_area): + # 先创建门(使用默认参数) + result = bpy.ops.mesh.archimesh_door() + + if result == {{'FINISHED'}}: + print("archimesh门创建完成!") + + # 等待一帧以确保对象创建完成 + bpy.context.view_layer.update() + + # 找到刚创建的门组对象(Door_Group) + door_group = None + for obj in bpy.data.objects: + if obj.name == "Door_Group": + door_group = obj + break + + if door_group: + print(f"找到门组: {{door_group.name}}") + + # 查找主门对象(DoorFrame) + main_door_obj = None + for obj in bpy.data.objects: + if obj.name == "DoorFrame": + main_door_obj = obj + break + + if main_door_obj and hasattr(main_door_obj, 'DoorObjectGenerator'): + # 获取门属性 + door_props = main_door_obj.DoorObjectGenerator[0] + + print("设置门属性...") + print(f"找到主门对象: {{main_door_obj.name}}") + + # 先设置基本属性 + door_props.frame_width = 1.0 # 门框宽度 + door_props.frame_height = 2.1 # 门框高度 + door_props.frame_thick = 0.08 # 门框厚度 + door_props.openside = '1' # 右侧开门 + + # 设置门模型和把手(这些会触发update_object回调) + print("设置门模型为2...") + door_props.model = '2' # 门模型2 + + print("设置门把手为1...") + door_props.handle = '1' # 门把手1 + + print(f"门属性设置完成:") + + # 等待更新完成 + bpy.context.view_layer.update() + + # 验证设置是否生效 + print("验证门属性设置:") + print(f" - 当前门模型: {{door_props.model}}") + print(f" - 当前门把手: {{door_props.handle}}") + + # 强制触发更新(如果需要) + try: + # 重新设置属性以触发更新 + current_model = door_props.model + current_handle = door_props.handle + + door_props.model = '1' # 临时设置为其他值 + bpy.context.view_layer.update() + + door_props.model = current_model # 设置回目标值 + bpy.context.view_layer.update() + + print("已强制触发门模型更新") + + except Exception as update_error: + print(f"强制更新时出现错误: {{update_error}}") + + # 在修改门全部属性后进行位移和旋转 + print("开始移动和旋转门组...") + try: + door_group1 = None + for obj in bpy.data.objects: + if obj.name == "Door_Group": + door_group1 = obj + break + + # 计算门的位置:x=0, y=房间长度-门宽度/2-0.8, z=0 + room_length = {adjusted_length} # 房间长度(Y轴) + door_width = {door_width} # 门宽度1米 + wall_distance = {wall_distance} # 距后墙80cm + + door_x = 0.0 + door_y = room_length - (door_width / 2) - wall_distance + door_z = 0.0 + + # 设置位置 + door_group1.location = (door_x, door_y, door_z) + print(f"已将门组移动到位置: x={{door_x}}, y={{door_y}}, z={{door_z}}") + print(f"房间长度: {{room_length}}m, 门宽度: {{door_width}}m, 距后墙: {{wall_distance}}m") + + # 设置旋转 + door_group1.rotation_euler = (math.radians(0), math.radians(0), math.radians(90)) # 旋转(0, 0, 90度) + print(f"已将门组旋转到: (0°, 0°, 90°)") + + # 强制更新 + bpy.context.view_layer.update() + print("门组位移和旋转完成") + + except Exception as move_error: + print(f"移动和旋转门组时出现错误: {{move_error}}") + + else: + print("警告: 未找到主门对象或DoorObjectGenerator属性") + # 尝试查找其他可能的门属性 + for obj in bpy.data.objects: + if hasattr(obj, 'archimesh_door'): + print(f"找到archimesh_door属性在对象: {{obj.name}}") + + # 打印门组的所有子对象信息 + print("门组包含的对象:") + for obj in bpy.data.objects: + if obj.parent == door_group: + print(f" - {{obj.name}} (位置: {{obj.location}})") + + return True + else: + print("警告: 未找到Door_Group对象") + # 尝试查找其他可能的门对象 + door_objects = [obj for obj in bpy.data.objects if "Door" in obj.name] + if door_objects: + print("找到的门相关对象:") + for obj in door_objects: + print(f" - {{obj.name}} (类型: {{obj.type}})") + + # 尝试移动第一个找到的门对象 + first_door = door_objects[0] + # 计算门的位置 + room_length = {adjusted_length} + door_width = {door_width} + wall_distance = {wall_distance} + + door_y = room_length - (door_width / 2) - wall_distance + first_door.location = (0.0, door_y, 0.0) + print(f"已移动 {{first_door.name}} 到位置: (0.0, {{door_y}}, 0.0)") + return True + else: + print("未找到任何门相关对象") + return False + else: + print("门创建失败") + return False + + except Exception as e: + print(f"创建门时出现错误: {{str(e)}}") + import traceback + traceback.print_exc() + return False + +def create_window_with_archimesh(): + """使用archimesh插件创建落地窗""" + try: + print("开始创建archimesh落地窗...") + + # 检查archimesh插件是否可用 + if not hasattr(bpy.ops.mesh, 'archimesh_winpanel'): + print("错误: archimesh插件未正确加载") + print("请确保archimesh插件已安装并启用") + return False + + # 设置3D游标到原点 + bpy.context.scene.cursor.location = (0.0, 0.0, 0.0) + + # 找到3D视图区域 + view3d_area = None + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + view3d_area = area + break + + if view3d_area is None: + print("错误: 找不到3D视图区域") + return False + + # 临时切换到3D视图上下文 + with bpy.context.temp_override(area=view3d_area): + # 创建窗户 + result = bpy.ops.mesh.archimesh_winpanel() + + if result == {{'FINISHED'}}: + print("archimesh落地窗创建完成!") + + # 等待一帧以确保对象创建完成 + bpy.context.view_layer.update() + + # 找到刚创建的窗户组对象(Window_Group) + window_group = None + for obj in bpy.data.objects: + if obj.name == "Window_Group": + window_group = obj + break + + if window_group: + print(f"找到窗户组: {{window_group.name}}") + + # 查找主窗户对象(Window) + main_window_obj = None + for obj in bpy.data.objects: + if obj.name == "Window": + main_window_obj = obj + break + + if main_window_obj and hasattr(main_window_obj, 'WindowPanelGenerator'): + # 获取窗户属性 + window_props = main_window_obj.WindowPanelGenerator[0] + + print("设置窗户属性...") + print(f"找到主窗户对象: {{main_window_obj.name}}") + + # 设置窗户的基本属性 + window_props.gen = 4 # 4扇窗户(水平) + window_props.yuk = 1 # 1行(垂直) + window_props.kl1 = 5 # 外框厚度5cm + window_props.kl2 = 5 # 竖框厚度5cm + window_props.fk = 2 # 内框厚度2cm + + # 设置窗户尺寸 + window_props.gnx0 = 80 # 第1扇窗户宽度80cm + window_props.gnx1 = 80 # 第2扇窗户宽度80cm + window_props.gnx2 = 80 # 第3扇窗户宽度80cm + window_props.gnx3 = 80 # 第4扇窗户宽度80cm + + # 计算窗户高度:房间高度-40cm + room_height = {height} # 房间高度 + window_height_cm = int((room_height - 0.4) * 100) # 转换为厘米并转为整数 + window_props.gny0 = window_height_cm # 窗户高度(房间高度-40cm) + + print(f"窗户高度设置: 房间高度{{room_height}}m - 40cm = {{window_height_cm}}cm") + + # 设置窗户类型为平窗 + window_props.UST = '1' # 平窗 + window_props.r = 0 # 旋转角度0度 + + # 设置窗台 + window_props.mr = False # 启用窗台 + + # 设置材质 + window_props.mt1 = '1' # 外框材质PVC + window_props.mt2 = '1' # 内框材质塑料 + + # 设置窗户开启状态 + window_props.k00 = True # 第1扇窗户开启 + window_props.k01 = True # 第2扇窗户开启 + window_props.k02 = True # 第3扇窗户开启 + window_props.k03 = True # 第4扇窗户开启(如果有的话) + + print(f"窗户属性设置完成:") + print(f" - 水平扇数: {{window_props.gen}}") + print(f" - 窗户高度: {{window_height_cm}}cm (房间高度{{room_height}}m - 40cm)") + + # 修复窗户玻璃材质 + print("修复窗户玻璃材质...") + fix_window_glass_material() + + # 等待更新完成 + bpy.context.view_layer.update() + + # 强制触发更新(如果需要) + try: + # 重新设置属性以触发更新 + current_gen = window_props.gen + current_gny0 = window_props.gny0 + + window_props.gen = 2 # 临时设置为其他值 + bpy.context.view_layer.update() + + window_props.gen = current_gen # 设置回目标值 + bpy.context.view_layer.update() + + print("已强制触发窗户更新") + + except Exception as update_error: + print(f"强制更新时出现错误: {{update_error}}") + + # 在修改落地窗全部属性后进行位移和旋转 + print("开始移动和旋转窗户组...") + try: + window_group1 = None + + for obj in bpy.data.objects: + if obj.name == "Window_Group": + window_group1 = obj + break + + # 计算窗户的位置:x=0, y=房间长度-窗宽度/2-0.8, z=0 + room_length = {adjusted_length} # 房间长度(Y轴) + window_width = {window_width} # 窗宽度3.45米 + wall_distance = {wall_distance} # 距后墙80cm + + window_x = 0.0 + window_y = room_length - (window_width / 2) - wall_distance + window_z = 0.0 + + # 设置位置 + window_group1.location = (window_x, window_y, window_z) + print(f"已将窗户组移动到位置: x={{window_x}}, y={{window_y}}, z={{window_z}}") + print(f"房间长度: {{room_length}}m, 窗宽度: {{window_width}}m, 距后墙: {{wall_distance}}m") + + # 设置旋转 + window_group1.rotation_euler = (math.radians(0), math.radians(0), math.radians(90)) # 旋转(0, 0, 90度) + print(f"已将窗户组旋转到: (0°, 0°, 90°)") + + # 强制更新 + bpy.context.view_layer.update() + print("窗户组位移和旋转完成") + + except Exception as move_error: + print(f"移动和旋转窗户组时出现错误: {{move_error}}") + + else: + print("警告: 未找到主窗户对象或WindowPanelGenerator属性") + # 尝试查找其他可能的窗户属性 + for obj in bpy.data.objects: + if hasattr(obj, 'archimesh_window'): + print(f"找到archimesh_window属性在对象: {{obj.name}}") + + # 打印窗户组的所有子对象信息 + print("窗户组包含的对象:") + for obj in bpy.data.objects: + if obj.parent == window_group: + print(f" - {{obj.name}} (位置: {{obj.location}})") + + return True + else: + print("警告: 未找到Window_Group对象") + # 尝试查找其他可能的窗户对象 + window_objects = [obj for obj in bpy.data.objects if "Window" in obj.name] + if window_objects: + print("找到的窗户相关对象:") + for obj in window_objects: + print(f" - {{obj.name}} (类型: {{obj.type}})") + + # 尝试移动第一个找到的窗户对象 + first_window = window_objects[0] + # 计算窗户的位置 + room_length = {adjusted_length} + window_width = {window_width} + wall_distance = {wall_distance} + + window_y = room_length - (window_width / 2) - wall_distance + first_window.location = (0.0, window_y, 0.0) + print(f"已移动 {{first_window.name}} 到位置: (0.0, {{window_y}}, 0.0)") + return True + else: + print("未找到任何窗户相关对象") + return False + + else: + print("窗户创建失败") + return False + + except Exception as e: + print(f"创建窗户时出现错误: {{str(e)}}") + import traceback + traceback.print_exc() + return False + +def fix_window_glass_material(): + """修复窗户玻璃材质,将其改为高透BSDF""" + try: + print("开始修复窗户玻璃材质...") + + # 查找Window_Group下的Window对象 + window_group = None + for obj in bpy.data.objects: + if obj.name == "Window_Group": + window_group = obj + break + + if not window_group: + print("未找到Window_Group对象") + return False + + # 查找Window对象(Window_Group的子对象) + window_obj = None + for obj in bpy.data.objects: + if obj.name == "Window" and obj.parent == window_group: + window_obj = obj + break + + + if not window_obj: + print("未找到Window对象") + return False + + print(f"找到Window对象: {{window_obj.name}}") + + # 检查Window对象的材质 + if not window_obj.material_slots: + print("Window对象没有材质槽") + return False + + # 遍历所有材质槽 + for slot in window_obj.material_slots: + if slot.material and "Glass" in slot.material.name: + print(f"找到Glass材质: {{slot.material.name}}") + + # 启用节点编辑 + slot.material.use_nodes = True + + # 清除所有现有节点 + slot.material.node_tree.nodes.clear() + + # 创建半透BSDF节点 + translucent_bsdf = slot.material.node_tree.nodes.new(type='ShaderNodeBsdfTranslucent') + translucent_bsdf.location = (0, 0) + + # 设置半透BSDF参数 + translucent_bsdf.inputs['Color'].default_value = (0.95, 0.98, 1.0, 1.0) # 几乎无色 + # 半透BSDF节点没有Weight参数,只有Color参数 + + # 创建材质输出节点 + material_output = slot.material.node_tree.nodes.new(type='ShaderNodeOutputMaterial') + material_output.location = (300, 0) + + # 连接节点 + slot.material.node_tree.links.new( + translucent_bsdf.outputs['BSDF'], + material_output.inputs['Surface'] + ) + + # 设置材质混合模式为Alpha Blend + slot.material.blend_method = 'BLEND' + + print(f"已成功修改Glass材质为半透BSDF") + print(f" - 半透颜色: 几乎无色") + print(f" - 混合模式: Alpha Blend") + + return True + + print("未找到Glass材质") + return False + + except Exception as e: + print(f"修复玻璃材质时出现错误: {{str(e)}}") + import traceback + traceback.print_exc() + return False + +def setup_render_settings(): + """设置渲染参数(固定EEVEE渲染,优化速度)""" + scene = bpy.context.scene + + # 设置渲染引擎固定为EEVEE + scene.render.engine = 'BLENDER_EEVEE_NEXT' + print("已设置渲染引擎为: BLENDER_EEVEE_NEXT") + + # 启用Freestyle线条渲染 + # scene.render.use_freestyle = True # False True + # #print("已启用Freestyle线条渲染") + + # # 配置Freestyle线条设置 + # if hasattr(scene, 'view_layers'): + # view_layer = scene.view_layers[0] # 获取第一个视图层 + # view_layer.use_freestyle = True + + # # 获取Freestyle线条设置 + # freestyle = view_layer.freestyle_settings + + # # 启用线条渲染 + # freestyle.use_smoothness = True + # freestyle.use_culling = True + + # 设置线条宽度 - 使用正确的API + #bpy.data.scenes["Scene"].render.line_thickness = 1.5 + + # 设置世界环境 + if hasattr(scene, 'world') and scene.world: + # 启用世界节点 + scene.world.use_nodes = True + + # 清除现有节点 + scene.world.node_tree.nodes.clear() + + # 创建自发光节点 + emission_node = scene.world.node_tree.nodes.new(type='ShaderNodeEmission') + emission_node.location = (0, 0) + + # 设置HSV颜色:色相0,饱和度0,明度0.051,Alpha 1 + # 转换为RGB:HSV(0, 0, 0.051) = RGB(0.051, 0.051, 0.051) + emission_node.inputs['Color'].default_value = (0.051, 0.051, 0.051, 1.0) # 深灰色 + emission_node.inputs['Strength'].default_value = 8.0 # 强度8 + + # 创建世界输出节点 + world_output = scene.world.node_tree.nodes.new(type='ShaderNodeOutputWorld') + world_output.location = (300, 0) + + # 连接节点 + scene.world.node_tree.links.new( + emission_node.outputs['Emission'], + world_output.inputs['Surface'] + ) + + print("已设置世界环境为自发光") + print(f" - 颜色: HSV(0, 0, 0.051) = RGB(0.051, 0.051, 0.051)") + print(f" - 强度: 8.0") + + # 设置EEVEE采样(降低采样数提高速度) + try: + if hasattr(scene.eevee, 'taa_render_samples'): + scene.eevee.taa_render_samples = 32 # 从64降低到32 + print(f"已设置EEVEE渲染采样: {{scene.eevee.taa_render_samples}}") + elif hasattr(scene.eevee, 'taa_samples'): + scene.eevee.taa_samples = 32 # 从64降低到32 + print(f"已设置EEVEE采样: {{scene.eevee.taa_samples}}") + else: + print("警告: 无法找到EEVEE采样设置,使用默认值") + + # 启用屏幕空间反射(简化设置) + if hasattr(scene.eevee, 'use_ssr'): + scene.eevee.use_ssr = True + print("已启用屏幕空间反射") + + # 启用环境光遮蔽(简化设置) + if hasattr(scene.eevee, 'use_gtao'): + scene.eevee.use_gtao = True + print("已启用环境光遮蔽") + + # 启用透明渲染(必要) + if hasattr(scene.eevee, 'use_transparent'): + scene.eevee.use_transparent = True + print("已启用透明渲染") + + # 设置透明混合模式(必要) + if hasattr(scene.render, 'film_transparent'): + scene.render.film_transparent = True + print("已启用透明背景") + + except AttributeError as e: + print(f"EEVEE设置警告: {{e}}") + print("使用默认EEVEE渲染设置") + + # 设置分辨率 + scene.render.resolution_x = {request.render.resolution_x} + scene.render.resolution_y = {request.render.resolution_y} + scene.render.resolution_percentage = 100 + + # 设置输出格式 + scene.render.image_settings.file_format = 'PNG' + scene.render.image_settings.color_mode = 'RGBA' # 支持透明 + + # 设置输出路径(使用绝对路径) + scene.render.filepath = OUTPUT_FILE + + print(f"渲染设置完成,输出路径: {{scene.render.filepath}}") + print(f"分辨率: {{scene.render.resolution_x}}x{{scene.render.resolution_y}}") + return scene.render.filepath + +def setup_camera_and_lighting(): + """设置摄像机和照明(160W点光源 + 150W日光)""" + # 计算灯光高度(房间高度减一米) + room_height = {height} + light_height = room_height - 1.0 + + # 设置摄像机 + camera = None + if "Camera" in bpy.data.objects: + camera = bpy.data.objects["Camera"] + elif "Isometric Camera" in bpy.data.objects: + camera = bpy.data.objects["Isometric Camera"] + else: + # 创建新摄像机 + bpy.ops.object.camera_add(location={camera_pos}) + camera = bpy.context.active_object + camera.name = "Isometric Camera" + + # 设置摄像机位置和旋转 + camera.location = {camera_pos} + camera.rotation_euler = {camera_rot} + + # 设置为透视投影 + camera.data.type = 'PERSP' + camera.data.lens = 35.0 # 35mm焦距 + camera.data.sensor_width = 35.0 # 35mm传感器 + camera.data.sensor_fit = 'AUTO' # 自动适配 + + # 设置为场景的活动摄像机 + bpy.context.scene.camera = camera + print("摄像机设置完成") + print(f"相机位置: ({{camera.location.x}}, {{camera.location.y}}, {{camera.location.z}})") + + # 设置蜡笔-场景线条画 + print("设置蜡笔-场景线条画...") + try: + # 启用蜡笔渲染 - 在Blender 4.2中使用不同的属性 + if hasattr(bpy.context.scene.render, 'use_grease_pencil'): + bpy.context.scene.render.use_grease_pencil = True + print("已启用蜡笔渲染") + else: + # 在Blender 4.2中,蜡笔渲染可能默认启用 + print("蜡笔渲染已默认启用") + + # 创建蜡笔对象(如果不存在)- 使用LINEART_SCENE类型 + grease_pencil_name = "Grease_pencil" + grease_pencil_obj = None + + # 检查是否已存在蜡笔对象 + for obj in bpy.data.objects: + if obj.name == grease_pencil_name and obj.type == 'GPENCIL': + grease_pencil_obj = obj + print(f"找到现有蜡笔对象: {{grease_pencil_name}}") + break + + if grease_pencil_obj is None: + # 创建新的蜡笔对象 + bpy.ops.object.gpencil_add(type='LINEART_SCENE') + grease_pencil_obj = bpy.context.active_object + grease_pencil_obj.name = grease_pencil_name + + # 等待一帧以确保数据创建完成 + bpy.context.view_layer.update() + + print(f"已创建蜡笔场景线条画对象: {{grease_pencil_name}}") + + # 设置笔画厚度缩放为0.4 + if grease_pencil_obj and grease_pencil_obj.data: + grease_pencil_data = grease_pencil_obj.data + grease_pencil_data.pixel_factor = 0.4 + print(f"已设置蜡笔笔画厚度缩放: {{grease_pencil_data.pixel_factor}}") + else: + print("警告: 未找到蜡笔数据") + + + # 确保蜡笔对象在渲染时可见 + if grease_pencil_obj: + grease_pencil_obj.hide_render = False + grease_pencil_obj.hide_viewport = False + print("蜡笔对象已设置为可见") + + print("蜡笔-场景线条画设置完成") + + except Exception as e: + print(f"设置蜡笔时出现错误: {{e}}") + print("继续执行其他设置...") + + # 隐藏ISO Emission灯光(如果存在) + light_objects = ["ISO Emission Left", "ISO Emission Right"] + for obj_name in light_objects: + if obj_name in bpy.data.objects: + light_obj = bpy.data.objects[obj_name] + # 隐藏发光对象 + light_obj.hide_render = True + light_obj.hide_viewport = True + print(f"已隐藏 {{obj_name}}") + + print("照明设置完成") + print(f"阴影设置: 启用,柔和阴影") + +def render_scene(): + """渲染场景""" + print("开始渲染...") + print(f"输出路径: {{OUTPUT_FILE}}") + + # 确保输出目录存在 + output_dir = os.path.dirname(OUTPUT_FILE) + if not os.path.exists(output_dir): + os.makedirs(output_dir, exist_ok=True) + print(f"创建输出目录: {{output_dir}}") + + bpy.ops.render.render(write_still=True) + print(f"渲染完成!") + + # 验证文件是否创建 + if os.path.exists(OUTPUT_FILE): + file_size = os.path.getsize(OUTPUT_FILE) + print(f"输出文件已创建: {{OUTPUT_FILE}} (大小: {{file_size}} bytes)") + else: + print(f"警告: 输出文件未找到: {{OUTPUT_FILE}}") + # 列出输出目录中的文件 + if os.path.exists(output_dir): + files = os.listdir(output_dir) + print(f"输出目录中的文件: {{files}}") + + return OUTPUT_FILE + +def main(): + """主函数""" + print("=" * 60) + print("开始API渲染任务") + print("=" * 60) + + try: + # 1. 创建房间 + print("1. 创建等轴测房间...") + if not create_isometric_room(): + print("房间创建失败,停止执行") + return False + + # 2. 根据道具类型创建道具 + prop_type = PROP_TYPE + if prop_type == 1: # 窗户 + print("2. 创建archimesh落地窗...") + if not create_window_with_archimesh(): + print("落地窗创建失败,但继续执行") + elif prop_type == 2: # 拱门 + print("2. 拱门已通过isometric_room_gen插件创建") + elif prop_type == 3: # 门 + print("2. 创建archimesh门...") + if not create_door_with_archimesh(): + print("门创建失败,但继续执行") + else: + print("2. 无道具设置") + + # 3. 设置渲染参数 + print("3. 设置渲染参数...") + output_path = setup_render_settings() + + # 4. 设置摄像机和照明 + print("4. 设置摄像机和照明...") + setup_camera_and_lighting() + + # 5. 渲染场景 + print("5. 开始渲染...") + final_path = render_scene() + + print("=" * 60) + print("API渲染任务完成!") + print(f"输出文件: {{final_path}}") + print("=" * 60) + + return True + + except Exception as e: + print(f"执行过程中出现错误: {{str(e)}}") + import traceback + traceback.print_exc() + return False + +# 执行主函数 +if __name__ == "__main__": + main() +''' + + return script.strip(), output_file + + async def render_room(self, task_id: str, request: RenderRequest) -> str: + """ + 执行房间渲染 + + Args: + task_id: 任务ID + request: 渲染请求参数 + + Returns: + 输出文件路径 + """ + try: + # 确保输出目录存在 + self._ensure_output_dir() + + # 生成Blender脚本 + script_content, output_file = self._generate_blender_script( + task_id, request) + + # 创建临时脚本文件 + with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f: + f.write(script_content) + script_path = f.name + + try: + # 构建Blender命令 + cmd = [ + self.blender_path, + "--background", + "--disable-crash-handler", + "--python", script_path + ] + + logger.info(f"执行Blender命令: {' '.join(cmd)}") + logger.info(f"输出文件路径: {output_file}") + + # 执行Blender渲染 + process = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + + stdout, stderr = await asyncio.wait_for( + process.communicate(), + timeout=300 # 5分钟超时 + ) + + # 记录Blender输出 + if stdout: + logger.info(f"Blender stdout: {stdout.decode('utf-8')}") + if stderr: + logger.warning(f"Blender stderr: {stderr.decode('utf-8')}") + + # 检查执行结果 + if process.returncode == 0: + # 检查输出文件是否存在 + if os.path.exists(output_file): + file_size = os.path.getsize(output_file) + logger.info( + f"渲染成功: {output_file} (大小: {file_size} bytes)") + return output_file + else: + # 检查输出目录中的文件 + output_dir = os.path.dirname(output_file) + if os.path.exists(output_dir): + files = os.listdir(output_dir) + logger.error(f"输出文件不存在,但目录中有文件: {files}") + else: + logger.error(f"输出目录不存在: {output_dir}") + + # 尝试查找可能的输出文件 + possible_files = [] + for root, dirs, files in os.walk(output_dir): + for file in files: + if file.endswith('.png') and 'render' in file: + possible_files.append( + os.path.join(root, file)) + + if possible_files: + logger.info(f"找到可能的输出文件: {possible_files}") + return possible_files[0] + else: + raise Exception(f"渲染完成但输出文件不存在: {output_file}") + else: + error_msg = stderr.decode('utf-8') if stderr else "未知错误" + raise Exception( + f"Blender执行失败 (返回码: {process.returncode}): {error_msg}") + + finally: + # 清理临时脚本文件 + try: + os.unlink(script_path) + except: + pass + + except asyncio.TimeoutError: + raise Exception("渲染超时(5分钟)") + except Exception as e: + logger.error(f"渲染失败: {e}") + raise diff --git a/Isometquick-server/test_client.py b/Isometquick-server/test_client.py index 952e70e..47b2ef8 100644 --- a/Isometquick-server/test_client.py +++ b/Isometquick-server/test_client.py @@ -110,7 +110,7 @@ def test_basic_render(): "length": 7.0, "width": 4.0, "height": 3.0, - "prop_type": 0 # 0=无, 1=落地窗, 2=拱门, 3=门" + "prop_type": 0 # 0=无, 1=落地窗, 2=拱门, 3=门 }, "camera": { "height": 1.3, diff --git a/auto_room.py b/auto_room.py index 3a8ce55..75ab23a 100644 --- a/auto_room.py +++ b/auto_room.py @@ -1,976 +1,1020 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Blender 4.2兼容版自动化脚本(白模版本) -自动创建等轴测房间并渲染白模 -""" - -import subprocess -import os -import sys - -# Blender可执行文件路径 -BLENDER_PATH = r"D:\Program Files\Blender Foundation\blender-4.2.11-windows-x64\blender.exe" - -# 简化版Blender脚本(只渲染白模) -BLENDER_SIMPLE_SCRIPT = ''' -import bpy -import bmesh -import os -import math -import time - -def disable_problematic_addons(): - """禁用可能导致问题的插件""" - try: - # 禁用可能冲突的插件 - problematic_addons = ['bl_tool'] - for addon in problematic_addons: - if addon in bpy.context.preferences.addons: - bpy.ops.preferences.addon_disable(module=addon) - print(f"已禁用插件: {addon}") - except: - pass - -def create_isometric_room(): - """创建等轴测房间,使用isometric_room_gen插件""" - try: - # 禁用问题插件 - #disable_problematic_addons() - - # 清除默认立方体 - if "Cube" in bpy.data.objects: - bpy.data.objects.remove(bpy.data.objects["Cube"], do_unlink=True) - - # 设置3D游标位置到原点 - bpy.context.scene.cursor.location = (0.0, 0.0, 0.0) - - # 检查isometric_room_gen插件 - if not hasattr(bpy.context.scene, 'sna_room_width'): - print("错误: isometric_room_gen插件未正确加载") - return False - - # 设置isometric_room_gen插件参数 - # 房间设置 - bpy.context.scene.sna_wall_thickness = 0.10 # 墙体厚度10cm - bpy.context.scene.sna_room_width = 5.0 # 房间宽度8米 - bpy.context.scene.sna_room_depth = 8.0 # 房间深度4米 - bpy.context.scene.sna_room_height = 3.0 # 房间高度3米 - - # 窗户设置 - 不设置窗户 - bpy.context.scene.sna_windows_enum = 'NONE' # 无窗户 - - # 设置房间类型为方形 - bpy.context.scene.sna_style = 'Square' - - # 设置拱门参数 - bpy.context.scene.sna_arch_settings = True # 启用拱门设置 - bpy.context.scene.sna_arch_placement = 'NONE' # 设置为后墙拱门BACK 不设置拱门NONE - bpy.context.scene.sna_arch_width = 1.2 # 拱门宽度1.2米 - bpy.context.scene.sna_arch_height = 2.4 # 拱门高度2.4米 - bpy.context.scene.sna_arch_thickness = 0.10 # 拱门厚度0.1米 - - # 找到3D视图区域 - view3d_area = None - for area in bpy.context.screen.areas: - if area.type == 'VIEW_3D': - view3d_area = area - break - - if view3d_area is None: - print("错误: 找不到3D视图区域") - return False - - # 临时切换到3D视图上下文 - with bpy.context.temp_override(area=view3d_area): - # 使用isometric_room_gen插件创建房间 - result = bpy.ops.sna.gen_room_1803a() - - if result == {'FINISHED'}: - print("等轴测房间创建完成!") - print(f"房间尺寸: {bpy.context.scene.sna_room_width}m x {bpy.context.scene.sna_room_depth}m x {bpy.context.scene.sna_room_height}m") - print(f"墙体厚度: {bpy.context.scene.sna_wall_thickness}m") - - # 将生成的房间在Z轴上向下移动墙体厚度的距离 - wall_thickness = bpy.context.scene.sna_wall_thickness - - # 查找生成的房间对象 - room_object = None - for obj in bpy.data.objects: - if obj.name == "IRG_IsoRoom_WithCeiling": - room_object = obj - break - - - return True - else: - print("房间创建失败") - return False - - except Exception as e: - print(f"创建房间时出现错误: {str(e)}") - import traceback - traceback.print_exc() - return False - - -def create_door_with_archimesh(): - """使用archimesh插件创建门""" - try: - print("开始创建archimesh门...") - - # 检查archimesh插件是否可用 - if not hasattr(bpy.ops.mesh, 'archimesh_door'): - print("错误: archimesh插件未正确加载") - print("请确保archimesh插件已安装并启用") - return False - - # 设置3D游标到原点 - bpy.context.scene.cursor.location = (0.0, 0.0, 0.0) - - # 找到3D视图区域 - view3d_area = None - for area in bpy.context.screen.areas: - if area.type == 'VIEW_3D': - view3d_area = area - break - - if view3d_area is None: - print("错误: 找不到3D视图区域") - return False - - # 临时切换到3D视图上下文 - with bpy.context.temp_override(area=view3d_area): - # 先创建门(使用默认参数) - result = bpy.ops.mesh.archimesh_door() - - if result == {'FINISHED'}: - print("archimesh门创建完成!") - - # 等待一帧以确保对象创建完成 - bpy.context.view_layer.update() - - # 找到刚创建的门组对象(Door_Group) - door_group = None - for obj in bpy.data.objects: - if obj.name == "Door_Group": - door_group = obj - break - - if door_group: - print(f"找到门组: {door_group.name}") - - # 查找主门对象(DoorFrame) - main_door_obj = None - for obj in bpy.data.objects: - if obj.name == "DoorFrame": - main_door_obj = obj - break - - if main_door_obj and hasattr(main_door_obj, 'DoorObjectGenerator'): - # 获取门属性 - door_props = main_door_obj.DoorObjectGenerator[0] - - print("设置门属性...") - print(f"找到主门对象: {main_door_obj.name}") - - # 先设置基本属性 - door_props.frame_width = 1.0 # 门框宽度 - door_props.frame_height = 2.1 # 门框高度 - door_props.frame_thick = 0.08 # 门框厚度 - door_props.openside = '1' # 右侧开门 - - # 设置门模型和把手(这些会触发update_object回调) - print("设置门模型为2...") - door_props.model = '2' # 门模型2 - - print("设置门把手为1...") - door_props.handle = '1' # 门把手1 - - print(f"门属性设置完成:") - print(f" - 门框宽度: {door_props.frame_width}") - print(f" - 门框高度: {door_props.frame_height}") - print(f" - 门框厚度: {door_props.frame_thick}") - print(f" - 开门方向: {door_props.openside}") - print(f" - 门模型: {door_props.model}") - print(f" - 门把手: {door_props.handle}") - - # 等待更新完成 - bpy.context.view_layer.update() - - # 验证设置是否生效 - print("验证门属性设置:") - print(f" - 当前门模型: {door_props.model}") - print(f" - 当前门把手: {door_props.handle}") - - # 强制触发更新(如果需要) - try: - # 重新设置属性以触发更新 - current_model = door_props.model - current_handle = door_props.handle - - door_props.model = '1' # 临时设置为其他值 - bpy.context.view_layer.update() - - door_props.model = current_model # 设置回目标值 - bpy.context.view_layer.update() - - print("已强制触发门模型更新") - - except Exception as update_error: - print(f"强制更新时出现错误: {update_error}") - - # 在修改门全部属性后进行位移和旋转 - print("开始移动和旋转门组...") - try: - door_group1 = None - for obj in bpy.data.objects: - if obj.name == "Door_Group": - door_group1 = obj - break - # 设置位置 - door_group1.location = (0.0, 6.0, 0.0) - print(f"已将门组移动到位置: {door_group1.location}") - - # 设置旋转 - door_group1.rotation_euler = (math.radians(0), math.radians(0), math.radians(90)) # 旋转(0, 0, 90度) - print(f"已将门组旋转到: (0°, 0°, 90°)") - - # 强制更新 - bpy.context.view_layer.update() - print("门组位移和旋转完成") - - except Exception as move_error: - print(f"移动和旋转门组时出现错误: {move_error}") - - else: - print("警告: 未找到主门对象或DoorObjectGenerator属性") - # 尝试查找其他可能的门属性 - for obj in bpy.data.objects: - if hasattr(obj, 'archimesh_door'): - print(f"找到archimesh_door属性在对象: {obj.name}") - - # 打印门组的所有子对象信息 - print("门组包含的对象:") - for obj in bpy.data.objects: - if obj.parent == door_group: - print(f" - {obj.name} (位置: {obj.location})") - - return True - else: - print("警告: 未找到Door_Group对象") - # 尝试查找其他可能的门对象 - door_objects = [obj for obj in bpy.data.objects if "Door" in obj.name] - if door_objects: - print("找到的门相关对象:") - for obj in door_objects: - print(f" - {obj.name} (类型: {obj.type})") - - # 尝试移动第一个找到的门对象 - first_door = door_objects[0] - first_door.location = (0.0, 2.0, 0.0) - print(f"已移动 {first_door.name} 到位置: {first_door.location}") - return True - else: - print("未找到任何门相关对象") - return False - else: - print("门创建失败") - return False - - except Exception as e: - print(f"创建门时出现错误: {str(e)}") - import traceback - traceback.print_exc() - return False - - -def create_window_with_archimesh(): - """使用archimesh插件创建落地窗""" - try: - print("开始创建archimesh落地窗...") - - # 检查archimesh插件是否可用 - if not hasattr(bpy.ops.mesh, 'archimesh_winpanel'): - print("错误: archimesh插件未正确加载") - print("请确保archimesh插件已安装并启用") - return False - - # 设置3D游标到原点 - bpy.context.scene.cursor.location = (0.0, 0.0, 0.0) - - # 找到3D视图区域 - view3d_area = None - for area in bpy.context.screen.areas: - if area.type == 'VIEW_3D': - view3d_area = area - break - - if view3d_area is None: - print("错误: 找不到3D视图区域") - return False - - # 临时切换到3D视图上下文 - with bpy.context.temp_override(area=view3d_area): - # 创建窗户 - result = bpy.ops.mesh.archimesh_winpanel() - - if result == {'FINISHED'}: - print("archimesh落地窗创建完成!") - - # 等待一帧以确保对象创建完成 - bpy.context.view_layer.update() - - # 找到刚创建的窗户组对象(Window_Group) - window_group = None - for obj in bpy.data.objects: - if obj.name == "Window_Group": - window_group = obj - break - - if window_group: - print(f"找到窗户组: {window_group.name}") - - # 查找主窗户对象(Window) - main_window_obj = None - for obj in bpy.data.objects: - if obj.name == "Window": - main_window_obj = obj - break - - if main_window_obj and hasattr(main_window_obj, 'WindowPanelGenerator'): - # 获取窗户属性 - window_props = main_window_obj.WindowPanelGenerator[0] - - print("设置窗户属性...") - print(f"找到主窗户对象: {main_window_obj.name}") - - # 设置窗户的基本属性 - window_props.gen = 4 # 4扇窗户(水平) - window_props.yuk = 1 # 1行(垂直) - window_props.kl1 = 5 # 外框厚度5cm - window_props.kl2 = 5 # 竖框厚度5cm - window_props.fk = 2 # 内框厚度2cm - - # 设置窗户尺寸 - window_props.gnx0 = 80 # 第1扇窗户宽度80cm - window_props.gnx1 = 80 # 第2扇窗户宽度80cm - window_props.gnx2 = 80 # 第3扇窗户宽度80cm - window_props.gnx3 = 80 # 第4扇窗户宽度80cm - window_props.gny0 = 250 # 窗户高度250cm(落地窗) - - # 设置窗户类型为平窗 - window_props.UST = '1' # 平窗 - window_props.r = 0 # 旋转角度0度 - - # 设置窗台 - window_props.mr = False # 启用窗台 - #window_props.mr1 = 4 # 窗台高度4cm - #window_props.mr2 = 4 # 窗台深度4cm - #window_props.mr3 = 20 # 窗台宽度20cm - #window_props.mr4 = 0 # 窗台延伸0cm - - # 设置材质 - window_props.mt1 = '1' # 外框材质PVC - window_props.mt2 = '1' # 内框材质塑料 - - # 设置窗户开启状态 - window_props.k00 = True # 第1扇窗户开启 - window_props.k01 = True # 第2扇窗户开启 - window_props.k02 = True # 第3扇窗户开启 - window_props.k03 = True # 第4扇窗户开启(如果有的话) - - print(f"窗户属性设置完成:") - print(f" - 水平扇数: {window_props.gen}") - print(f" - 垂直行数: {window_props.yuk}") - print(f" - 外框厚度: {window_props.kl1}cm") - print(f" - 竖框厚度: {window_props.kl2}cm") - print(f" - 内框厚度: {window_props.fk}cm") - print(f" - 窗户高度: {window_props.gny0}cm") - print(f" - 窗户类型: 平窗") - print(f" - 窗台: 启用") - print(f" - 窗户开启状态: k00={window_props.k00}, k01={window_props.k01}, k02={window_props.k02}, k03={window_props.k03}") - - - # 修复窗户玻璃材质 - print("修复窗户玻璃材质...") - fix_window_glass_material() - - # 等待更新完成 - bpy.context.view_layer.update() - - # 强制触发更新(如果需要) - try: - # 重新设置属性以触发更新 - current_gen = window_props.gen - current_gny0 = window_props.gny0 - - window_props.gen = 2 # 临时设置为其他值 - bpy.context.view_layer.update() - - window_props.gen = current_gen # 设置回目标值 - bpy.context.view_layer.update() - - print("已强制触发窗户更新") - - except Exception as update_error: - print(f"强制更新时出现错误: {update_error}") - - # 在修改落地窗全部属性后进行位移和旋转 - print("开始移动和旋转窗户组...") - try: - window_group1 = None - - for obj in bpy.data.objects: - if obj.name == "Window_Group": - window_group1 = obj - break - - # 设置位置 - window_group1.location = (0, 4.0, 0.0) - print(f"已将窗户组移动到位置: {window_group1.location}") - - # 设置旋转 - window_group1.rotation_euler = (math.radians(0), math.radians(0), math.radians(90)) # 旋转(0, 0, 90度) - print(f"已将窗户组旋转到: (0°, 0°, 90°)") - - # 强制更新 - bpy.context.view_layer.update() - print("窗户组位移和旋转完成") - - except Exception as move_error: - print(f"移动和旋转窗户组时出现错误: {move_error}") - - else: - print("警告: 未找到主窗户对象或WindowPanelGenerator属性") - # 尝试查找其他可能的窗户属性 - for obj in bpy.data.objects: - if hasattr(obj, 'archimesh_window'): - print(f"找到archimesh_window属性在对象: {obj.name}") - - # 打印窗户组的所有子对象信息 - print("窗户组包含的对象:") - for obj in bpy.data.objects: - if obj.parent == window_group: - print(f" - {obj.name} (位置: {obj.location})") - - - return True - else: - print("警告: 未找到Window_Group对象") - # 尝试查找其他可能的窗户对象 - window_objects = [obj for obj in bpy.data.objects if "Window" in obj.name] - if window_objects: - print("找到的窗户相关对象:") - for obj in window_objects: - print(f" - {obj.name} (类型: {obj.type})") - - # 尝试移动第一个找到的窗户对象 - first_window = window_objects[0] - first_window.location = (0.0, 1.96, 0.0) - print(f"已移动 {first_window.name} 到位置: {first_window.location}") - return True - else: - print("未找到任何窗户相关对象") - return False - - - - else: - print("窗户创建失败") - return False - - except Exception as e: - print(f"创建窗户时出现错误: {str(e)}") - import traceback - traceback.print_exc() - return False - - -def fix_window_glass_material(): - """修复窗户玻璃材质,将其改为高透BSDF""" - try: - print("开始修复窗户玻璃材质...") - - # 查找Window_Group下的Window对象 - window_group = None - for obj in bpy.data.objects: - if obj.name == "Window_Group": - window_group = obj - break - - if not window_group: - print("未找到Window_Group对象") - return False - - # 查找Window对象(Window_Group的子对象) - window_obj = None - for obj in bpy.data.objects: - if obj.name == "Window" and obj.parent == window_group: - window_obj = obj - break - - - if not window_obj: - print("未找到Window对象") - return False - - print(f"找到Window对象: {window_obj.name}") - - # 检查Window对象的材质 - if not window_obj.material_slots: - print("Window对象没有材质槽") - return False - - # 遍历所有材质槽 - for slot in window_obj.material_slots: - if slot.material and "Glass" in slot.material.name: - print(f"找到Glass材质: {slot.material.name}") - - # 启用节点编辑 - slot.material.use_nodes = True - - # 清除所有现有节点 - slot.material.node_tree.nodes.clear() - - # 创建半透BSDF节点 - translucent_bsdf = slot.material.node_tree.nodes.new(type='ShaderNodeBsdfTranslucent') - translucent_bsdf.location = (0, 0) - - # 设置半透BSDF参数 - translucent_bsdf.inputs['Color'].default_value = (0.95, 0.98, 1.0, 1.0) # 几乎无色 - # 半透BSDF节点没有Weight参数,只有Color参数 - - # 创建材质输出节点 - material_output = slot.material.node_tree.nodes.new(type='ShaderNodeOutputMaterial') - material_output.location = (300, 0) - - # 连接节点 - slot.material.node_tree.links.new( - translucent_bsdf.outputs['BSDF'], - material_output.inputs['Surface'] - ) - - # 设置材质混合模式为Alpha Blend - slot.material.blend_method = 'BLEND' - - print(f"已成功修改Glass材质为半透BSDF") - print(f" - 半透颜色: 几乎无色") - print(f" - 混合模式: Alpha Blend") - - return True - - print("未找到Glass材质") - return False - - except Exception as e: - print(f"修复玻璃材质时出现错误: {str(e)}") - import traceback - traceback.print_exc() - return False - -def setup_render_settings(): - """设置渲染参数(EEVEE渲染,优化速度)""" - scene = bpy.context.scene - - # 设置渲染引擎为EEVEE - scene.render.engine = 'BLENDER_EEVEE_NEXT' - print("已设置渲染引擎为EEVEE") - - # 启用Freestyle线条渲染 - scene.render.use_freestyle = True - print("已启用Freestyle线条渲染") - - # 配置Freestyle线条设置 - if hasattr(scene, 'view_layers'): - view_layer = scene.view_layers[0] # 获取第一个视图层 - view_layer.use_freestyle = True - - # 获取Freestyle线条设置 - freestyle = view_layer.freestyle_settings - - # 启用线条渲染 - freestyle.use_smoothness = True - freestyle.use_culling = True - - # 设置线条宽度 - 使用正确的API 没有生效 - - - # 设置世界环境 - if hasattr(scene, 'world') and scene.world: - # 启用世界节点 - scene.world.use_nodes = True - - # 清除现有节点 - scene.world.node_tree.nodes.clear() - - # 创建自发光节点 - emission_node = scene.world.node_tree.nodes.new(type='ShaderNodeEmission') - emission_node.location = (0, 0) - - # 设置HSV颜色:色相0,饱和度0,明度0.051,Alpha 1 - # 转换为RGB:HSV(0, 0, 0.051) = RGB(0.051, 0.051, 0.051) - emission_node.inputs['Color'].default_value = (0.051, 0.051, 0.051, 1.0) # 深灰色 - emission_node.inputs['Strength'].default_value = 8.0 # 强度8 - - # 创建世界输出节点 - world_output = scene.world.node_tree.nodes.new(type='ShaderNodeOutputWorld') - world_output.location = (300, 0) - - # 连接节点 - scene.world.node_tree.links.new( - emission_node.outputs['Emission'], - world_output.inputs['Surface'] - ) - - print("已设置世界环境为自发光") - print(f" - 颜色: HSV(0, 0, 0.051) = RGB(0.051, 0.051, 0.051)") - print(f" - 强度: 8.0") - - # 设置EEVEE采样(降低采样数提高速度) - try: - if hasattr(scene.eevee, 'taa_render_samples'): - scene.eevee.taa_render_samples = 32 # 从64降低到32 - print(f"已设置EEVEE渲染采样: {scene.eevee.taa_render_samples}") - elif hasattr(scene.eevee, 'taa_samples'): - scene.eevee.taa_samples = 32 # 从64降低到32 - print(f"已设置EEVEE采样: {scene.eevee.taa_samples}") - else: - print("警告: 无法找到EEVEE采样设置,使用默认值") - - # 启用屏幕空间反射(简化设置) - if hasattr(scene.eevee, 'use_ssr'): - scene.eevee.use_ssr = True - print("已启用屏幕空间反射") - - # 启用环境光遮蔽(简化设置) - if hasattr(scene.eevee, 'use_gtao'): - scene.eevee.use_gtao = True - print("已启用环境光遮蔽") - - # 启用透明渲染(必要) - if hasattr(scene.eevee, 'use_transparent'): - scene.eevee.use_transparent = True - print("已启用透明渲染") - - # 设置透明混合模式(必要) - if hasattr(scene.render, 'film_transparent'): - scene.render.film_transparent = True - print("已启用透明背景") - - except AttributeError as e: - print(f"EEVEE设置警告: {e}") - print("使用默认EEVEE渲染设置") - - # 设置分辨率 - scene.render.resolution_x = 1080 # 宽度:1080px - scene.render.resolution_y = 2400 # 高度:2400px - scene.render.resolution_percentage = 100 - - # 设置输出格式 - scene.render.image_settings.file_format = 'PNG' - scene.render.image_settings.color_mode = 'RGBA' # 支持透明 - - # 设置输出路径到桌面 - desktop_path = os.path.join(os.path.expanduser("~"), "Desktop") - timestamp = time.strftime("%m%d_%H%M%S") # 日期_时分秒格式 - filename = f"isometric_{timestamp}.png" - scene.render.filepath = os.path.join(desktop_path, filename) - - print(f"EEVEE渲染设置完成,输出路径: {scene.render.filepath}") - return scene.render.filepath - -def setup_camera_and_lighting(): - """设置摄像机和照明(160W点光源 + 150W日光)""" - # 计算灯光高度(房间高度减一米) - room_height = 3.0 - light_height = room_height - 1.0 - - # 设置摄像机 - camera = None - if "Camera" in bpy.data.objects: - camera = bpy.data.objects["Camera"] - elif "Isometric Camera" in bpy.data.objects: - camera = bpy.data.objects["Isometric Camera"] - else: - # 创建新摄像机 - bpy.ops.object.camera_add(location=(7, -7, 5)) - camera = bpy.context.active_object - camera.name = "Isometric Camera" - - # 设置摄像机位置和旋转 - # 房间宽度5.0米,相机位置调整为(房间宽度-1, 1, 1.3) - room_width = 5.0 - camera.location = (room_width - 1, 1, 1.3) # 相机位置:(4, 1, 1.3) - camera.rotation_euler = (math.radians(90), math.radians(0), math.radians(30)) # 相机旋转 - - # 设置为透视投影 - camera.data.type = 'PERSP' - camera.data.lens = 35.0 # 35mm焦距 - camera.data.sensor_width = 35.0 # 35mm传感器 - camera.data.sensor_fit = 'AUTO' # 自动适配 - - # 设置为场景的活动摄像机 - bpy.context.scene.camera = camera - print("摄像机设置完成") - print(f"相机位置: ({camera.location.x}, {camera.location.y}, {camera.location.z})") - - # 隐藏ISO Emission灯光(如果存在) - light_objects = ["ISO Emission Left", "ISO Emission Right"] - for obj_name in light_objects: - if obj_name in bpy.data.objects: - light_obj = bpy.data.objects[obj_name] - # 隐藏发光对象 - light_obj.hide_render = True - light_obj.hide_viewport = True - print(f"已隐藏 {obj_name}") - - # 创建第一个160W点光源(房间中心) - # try: - # # 计算点光源位置(房间中心上方) - # room_length = 4.0 # Y轴 - # room_width = 8.0 # X轴(4+4) - - # # 第一个点光源位置:房间中心上方 - # light_x = 0 # X轴中心 - # light_y = 0 # Y轴中心 - # light_z = light_height - - # # 创建第一个点光源 - # bpy.ops.object.light_add(type='POINT', location=(light_x, light_y, light_z)) - # point_light1 = bpy.context.active_object - # point_light1.name = "Main Point Light" - # point_light1.data.energy = 160 # 160W - - # # 设置软衰减(简化设置) - # point_light1.data.shadow_soft_size = 0.5 # 0.5米半径,产生柔和阴影 - # point_light1.data.use_shadow = True # 启用阴影 - - # print(f"已创建第一个点光源(160W)") - # print(f"点光源位置: x={light_x}, y={light_y}, z={light_z}") - - # except Exception as e: - # print(f"创建第一个点光源时出错: {e}") - - # 创建第二个150W日光(位置12, 7, 6) - # try: - # # 第二个光源位置 - 调整到窗户后方更远的位置 - # light2_x = -6 - # light2_y = 7 # 让日光更远 - # light2_z = 6 # 提高高度 - - # # 创建日光 - # bpy.ops.object.light_add(type='SUN', location=(light2_x, light2_y, light2_z)) - # sun_light = bpy.context.active_object - # sun_light.name = "Sun Light" - # sun_light.data.energy = 20 - - # # 调整日光旋转角度,让光线更直接地照射窗户 - # sun_light.rotation_euler = (math.radians(0), math.radians(-60), math.radians(0)) # 简化旋转角度 - - # # 设置日光属性(简化设置) - # sun_light.data.angle = 0.05 # 从0.1改为0.05,让阴影更锐利 - # sun_light.data.use_shadow = True # 启用阴影 - - # print(f"已创建日光(20W)") - # print(f"日光位置: x={light2_x}, y={light2_y}, z={light2_z}") - # print(f"日光旋转: x={-70}°, y={0}°, z={0}°") - - # except Exception as e: - # print(f"创建日光时出错: {e}") - - print("照明设置完成") - print("灯光类型: 点光源 + 日光") - print(f"衰减类型: 点光源软衰减 + 日光平行光") - print(f"阴影设置: 启用,柔和阴影") - print(f"主灯光高度: {light_height}米 (房间高度减一米)") - print(f"日光位置: (0, 10, 6)米,旋转: (-70°, 0°, 0°)") - -def render_scene(): - """渲染场景""" - print("开始EEVEE渲染...") - - # 执行渲染 - bpy.ops.render.render(write_still=True) - print(f"EEVEE渲染完成! 图片保存在: {bpy.context.scene.render.filepath}") - return bpy.context.scene.render.filepath - -def main(): - """主函数""" - print("=" * 60) - print("开始自动创建等轴测房间并EEVEE渲染") - print("=" * 60) - - try: - # 1. 创建房间 - print("1. 创建等轴测房间...") - if not create_isometric_room(): - print("房间创建失败,停止执行") - return False - - # 2. 创建落地窗 - print("2. 创建archimesh落地窗...") - if not create_window_with_archimesh(): - print("落地窗创建失败,但继续执行") - # 不中断执行,窗户创建失败不影响渲染 - # 2. 创建门 - print("2. 创建archimesh门...") - if not create_door_with_archimesh(): - print("落地窗创建失败,但继续执行") - # 不中断执行,窗户创建失败不影响渲染 - - # 3. 设置渲染参数(EEVEE) - print("3. 设置EEVEE渲染参数...") - output_path = setup_render_settings() - - # 4. 设置摄像机和照明 - print("4. 设置摄像机和照明...") - setup_camera_and_lighting() - - # 5. 渲染场景 - print("5. 开始EEVEE渲染...") - final_path = render_scene() - - print("=" * 60) - print("EEVEE渲染完成!") - print(f"渲染图片已保存到桌面: {final_path}") - print("=" * 60) - - return True - - except Exception as e: - print(f"执行过程中出现错误: {str(e)}") - import traceback - traceback.print_exc() - return False - -# 执行主函数 -if __name__ == "__main__": - main() -''' - - -def check_blender_exists(): - """检查Blender是否存在于指定路径""" - if not os.path.exists(BLENDER_PATH): - print(f"错误: 在路径 '{BLENDER_PATH}' 找不到Blender") - print("请检查Blender安装路径是否正确") - return False - return True - - -def create_temp_script(): - """创建临时的Blender脚本文件""" - script_path = os.path.join(os.getcwd(), "temp_simple_blender_script.py") - with open(script_path, 'w', encoding='utf-8') as f: - f.write(BLENDER_SIMPLE_SCRIPT) - return script_path - - -def launch_blender_simple(): - """启动Blender并执行简化版自动化脚本""" - if not check_blender_exists(): - return False - - # 创建临时脚本文件 - script_path = create_temp_script() - - try: - print("正在启动Blender(白模版本)...") - print(f"Blender路径: {BLENDER_PATH}") - print("特点:") - print("- 不设置材质,使用默认白色材质") - print("- 快速渲染") - print("- 兼容Blender 4.2") - print("- 自动保存到桌面") - - # 构建命令行参数 - cmd = [ - BLENDER_PATH, - "--background", # 后台运行模式 - "--disable-crash-handler", # 禁用崩溃处理器 - "--python", script_path # 执行Python脚本 - ] - - print("\n开始执行白模渲染流程...") - - # 启动Blender并等待完成 - process = subprocess.run( - cmd, - capture_output=True, - text=True, - encoding='utf-8', - timeout=300 # 5分钟超时 - ) - - # 输出结果 - if process.stdout: - print("Blender输出:") - # 过滤掉重复的错误信息 - lines = process.stdout.split('\n') - filtered_lines = [] - for line in lines: - if not ("AttributeError: 'NoneType' object has no attribute 'idname'" in line or - "Error in bpy.app.handlers.depsgraph_update_post" in line): - filtered_lines.append(line) - print('\n'.join(filtered_lines)) - - if process.stderr and not "rotate_tool.py" in process.stderr: - print("重要错误信息:") - print(process.stderr) - - if process.returncode == 0: - print("\n✅ 白模渲染执行成功!") - print("白模图片应该已经保存到桌面") - return True - else: - print(f"\n❌ 执行失败,返回码: {process.returncode}") - return False - - except subprocess.TimeoutExpired: - print("\n⏰ 执行超时(5分钟),可能Blender仍在运行") - print("请检查桌面是否有生成的图片") - return False - except Exception as e: - print(f"启动Blender时出现错误: {str(e)}") - return False - - finally: - # 清理临时文件 - try: - if os.path.exists(script_path): - os.remove(script_path) - print(f"已清理临时文件: {script_path}") - except: - pass - - -def main(): - """主函数""" - print("=" * 70) - print("Blender 4.2兼容版自动化脚本 - 白模渲染") - print("=" * 70) - print("特点:") - print("✓ 移除所有材质设置") - print("✓ 使用默认白色材质") - print("✓ 快速渲染") - print("✓ 兼容Blender 4.2") - print("✓ 自动保存到桌面") - print("=" * 70) - - success = launch_blender_simple() - - if success: - print("\n🎉 白模渲染完成!") - print("请检查桌面上的渲染图片") - print("图片文件名格式: isometric_room_white_[时间戳].png") - else: - print("\n❌ 执行失败!") - print("可能的解决方案:") - print("1. 确保Isometquick插件已正确安装") - print("2. 禁用可能冲突的其他插件") - print("3. 检查磁盘空间是否足够") - - -if __name__ == "__main__": - main() +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Blender 4.2兼容版自动化脚本(白模版本) +自动创建等轴测房间并渲染白模 +""" + +import subprocess +import os +import sys + +# Blender可执行文件路径 +BLENDER_PATH = r"D:\Program Files\Blender Foundation\blender-4.2.11-windows-x64\blender.exe" + +# 简化版Blender脚本(只渲染白模) +BLENDER_SIMPLE_SCRIPT = ''' +import bpy +import bmesh +import os +import math +import time + +def disable_problematic_addons(): + """禁用可能导致问题的插件""" + try: + # 禁用可能冲突的插件 + problematic_addons = ['bl_tool'] + for addon in problematic_addons: + if addon in bpy.context.preferences.addons: + bpy.ops.preferences.addon_disable(module=addon) + print(f"已禁用插件: {addon}") + except: + pass + +def create_isometric_room(): + """创建等轴测房间,使用isometric_room_gen插件""" + try: + # 禁用问题插件 + #disable_problematic_addons() + + # 清除默认立方体 + if "Cube" in bpy.data.objects: + bpy.data.objects.remove(bpy.data.objects["Cube"], do_unlink=True) + + # 设置3D游标位置到原点 + bpy.context.scene.cursor.location = (0.0, 0.0, 0.0) + + # 检查isometric_room_gen插件 + if not hasattr(bpy.context.scene, 'sna_room_width'): + print("错误: isometric_room_gen插件未正确加载") + return False + + # 设置isometric_room_gen插件参数 + # 房间设置 + bpy.context.scene.sna_wall_thickness = 0.10 # 墙体厚度10cm + bpy.context.scene.sna_room_width = 5.0 # 房间宽度8米 + bpy.context.scene.sna_room_depth = 8.0 # 房间深度4米 + bpy.context.scene.sna_room_height = 3.0 # 房间高度3米 + + # 窗户设置 - 不设置窗户 + bpy.context.scene.sna_windows_enum = 'NONE' # 无窗户 + + # 设置房间类型为方形 + bpy.context.scene.sna_style = 'Square' + + # 设置拱门参数 + bpy.context.scene.sna_arch_settings = True # 启用拱门设置 + bpy.context.scene.sna_arch_placement = 'NONE' # 设置为后墙拱门BACK 不设置拱门NONE + bpy.context.scene.sna_arch_width = 1.2 # 拱门宽度1.2米 + bpy.context.scene.sna_arch_height = 2.4 # 拱门高度2.4米 + bpy.context.scene.sna_arch_thickness = 0.10 # 拱门厚度0.1米 + + # 找到3D视图区域 + view3d_area = None + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + view3d_area = area + break + + if view3d_area is None: + print("错误: 找不到3D视图区域") + return False + + # 临时切换到3D视图上下文 + with bpy.context.temp_override(area=view3d_area): + # 使用isometric_room_gen插件创建房间 + result = bpy.ops.sna.gen_room_1803a() + + if result == {'FINISHED'}: + print("等轴测房间创建完成!") + print(f"房间尺寸: {bpy.context.scene.sna_room_width}m x {bpy.context.scene.sna_room_depth}m x {bpy.context.scene.sna_room_height}m") + print(f"墙体厚度: {bpy.context.scene.sna_wall_thickness}m") + + # 将生成的房间在Z轴上向下移动墙体厚度的距离 + wall_thickness = bpy.context.scene.sna_wall_thickness + + # 查找生成的房间对象 + room_object = None + for obj in bpy.data.objects: + if obj.name == "IRG_IsoRoom_WithCeiling": + room_object = obj + break + + + return True + else: + print("房间创建失败") + return False + + except Exception as e: + print(f"创建房间时出现错误: {str(e)}") + import traceback + traceback.print_exc() + return False + + +def create_door_with_archimesh(): + """使用archimesh插件创建门""" + try: + print("开始创建archimesh门...") + + # 检查archimesh插件是否可用 + if not hasattr(bpy.ops.mesh, 'archimesh_door'): + print("错误: archimesh插件未正确加载") + print("请确保archimesh插件已安装并启用") + return False + + # 设置3D游标到原点 + bpy.context.scene.cursor.location = (0.0, 0.0, 0.0) + + # 找到3D视图区域 + view3d_area = None + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + view3d_area = area + break + + if view3d_area is None: + print("错误: 找不到3D视图区域") + return False + + # 临时切换到3D视图上下文 + with bpy.context.temp_override(area=view3d_area): + # 先创建门(使用默认参数) + result = bpy.ops.mesh.archimesh_door() + + if result == {'FINISHED'}: + print("archimesh门创建完成!") + + # 等待一帧以确保对象创建完成 + bpy.context.view_layer.update() + + # 找到刚创建的门组对象(Door_Group) + door_group = None + for obj in bpy.data.objects: + if obj.name == "Door_Group": + door_group = obj + break + + if door_group: + print(f"找到门组: {door_group.name}") + + # 查找主门对象(DoorFrame) + main_door_obj = None + for obj in bpy.data.objects: + if obj.name == "DoorFrame": + main_door_obj = obj + break + + if main_door_obj and hasattr(main_door_obj, 'DoorObjectGenerator'): + # 获取门属性 + door_props = main_door_obj.DoorObjectGenerator[0] + + print("设置门属性...") + print(f"找到主门对象: {main_door_obj.name}") + + # 先设置基本属性 + door_props.frame_width = 1.0 # 门框宽度 + door_props.frame_height = 2.1 # 门框高度 + door_props.frame_thick = 0.08 # 门框厚度 + door_props.openside = '1' # 右侧开门 + + # 设置门模型和把手(这些会触发update_object回调) + print("设置门模型为2...") + door_props.model = '2' # 门模型2 + + print("设置门把手为1...") + door_props.handle = '1' # 门把手1 + + print(f"门属性设置完成:") + print(f" - 门框宽度: {door_props.frame_width}") + print(f" - 门框高度: {door_props.frame_height}") + print(f" - 门框厚度: {door_props.frame_thick}") + print(f" - 开门方向: {door_props.openside}") + print(f" - 门模型: {door_props.model}") + print(f" - 门把手: {door_props.handle}") + + # 等待更新完成 + bpy.context.view_layer.update() + + # 验证设置是否生效 + print("验证门属性设置:") + print(f" - 当前门模型: {door_props.model}") + print(f" - 当前门把手: {door_props.handle}") + + # 强制触发更新(如果需要) + try: + # 重新设置属性以触发更新 + current_model = door_props.model + current_handle = door_props.handle + + door_props.model = '1' # 临时设置为其他值 + bpy.context.view_layer.update() + + door_props.model = current_model # 设置回目标值 + bpy.context.view_layer.update() + + print("已强制触发门模型更新") + + except Exception as update_error: + print(f"强制更新时出现错误: {update_error}") + + # 在修改门全部属性后进行位移和旋转 + print("开始移动和旋转门组...") + try: + door_group1 = None + for obj in bpy.data.objects: + if obj.name == "Door_Group": + door_group1 = obj + break + # 设置位置 + door_group1.location = (0.0, 6.0, 0.0) + print(f"已将门组移动到位置: {door_group1.location}") + + # 设置旋转 + door_group1.rotation_euler = (math.radians(0), math.radians(0), math.radians(90)) # 旋转(0, 0, 90度) + print(f"已将门组旋转到: (0°, 0°, 90°)") + + # 强制更新 + bpy.context.view_layer.update() + print("门组位移和旋转完成") + + except Exception as move_error: + print(f"移动和旋转门组时出现错误: {move_error}") + + else: + print("警告: 未找到主门对象或DoorObjectGenerator属性") + # 尝试查找其他可能的门属性 + for obj in bpy.data.objects: + if hasattr(obj, 'archimesh_door'): + print(f"找到archimesh_door属性在对象: {obj.name}") + + # 打印门组的所有子对象信息 + print("门组包含的对象:") + for obj in bpy.data.objects: + if obj.parent == door_group: + print(f" - {obj.name} (位置: {obj.location})") + + return True + else: + print("警告: 未找到Door_Group对象") + # 尝试查找其他可能的门对象 + door_objects = [obj for obj in bpy.data.objects if "Door" in obj.name] + if door_objects: + print("找到的门相关对象:") + for obj in door_objects: + print(f" - {obj.name} (类型: {obj.type})") + + # 尝试移动第一个找到的门对象 + first_door = door_objects[0] + first_door.location = (0.0, 2.0, 0.0) + print(f"已移动 {first_door.name} 到位置: {first_door.location}") + return True + else: + print("未找到任何门相关对象") + return False + else: + print("门创建失败") + return False + + except Exception as e: + print(f"创建门时出现错误: {str(e)}") + import traceback + traceback.print_exc() + return False + + +def create_window_with_archimesh(): + """使用archimesh插件创建落地窗""" + try: + print("开始创建archimesh落地窗...") + + # 检查archimesh插件是否可用 + if not hasattr(bpy.ops.mesh, 'archimesh_winpanel'): + print("错误: archimesh插件未正确加载") + print("请确保archimesh插件已安装并启用") + return False + + # 设置3D游标到原点 + bpy.context.scene.cursor.location = (0.0, 0.0, 0.0) + + # 找到3D视图区域 + view3d_area = None + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + view3d_area = area + break + + if view3d_area is None: + print("错误: 找不到3D视图区域") + return False + + # 临时切换到3D视图上下文 + with bpy.context.temp_override(area=view3d_area): + # 创建窗户 + result = bpy.ops.mesh.archimesh_winpanel() + + if result == {'FINISHED'}: + print("archimesh落地窗创建完成!") + + # 等待一帧以确保对象创建完成 + bpy.context.view_layer.update() + + # 找到刚创建的窗户组对象(Window_Group) + window_group = None + for obj in bpy.data.objects: + if obj.name == "Window_Group": + window_group = obj + break + + if window_group: + print(f"找到窗户组: {window_group.name}") + + # 查找主窗户对象(Window) + main_window_obj = None + for obj in bpy.data.objects: + if obj.name == "Window": + main_window_obj = obj + break + + if main_window_obj and hasattr(main_window_obj, 'WindowPanelGenerator'): + # 获取窗户属性 + window_props = main_window_obj.WindowPanelGenerator[0] + + print("设置窗户属性...") + print(f"找到主窗户对象: {main_window_obj.name}") + + # 设置窗户的基本属性 + window_props.gen = 4 # 4扇窗户(水平) + window_props.yuk = 1 # 1行(垂直) + window_props.kl1 = 5 # 外框厚度5cm + window_props.kl2 = 5 # 竖框厚度5cm + window_props.fk = 2 # 内框厚度2cm + + # 设置窗户尺寸 + window_props.gnx0 = 80 # 第1扇窗户宽度80cm + window_props.gnx1 = 80 # 第2扇窗户宽度80cm + window_props.gnx2 = 80 # 第3扇窗户宽度80cm + window_props.gnx3 = 80 # 第4扇窗户宽度80cm + window_props.gny0 = 250 # 窗户高度250cm(落地窗) + + # 设置窗户类型为平窗 + window_props.UST = '1' # 平窗 + window_props.r = 0 # 旋转角度0度 + + # 设置窗台 + window_props.mr = False # 启用窗台 + #window_props.mr1 = 4 # 窗台高度4cm + #window_props.mr2 = 4 # 窗台深度4cm + #window_props.mr3 = 20 # 窗台宽度20cm + #window_props.mr4 = 0 # 窗台延伸0cm + + # 设置材质 + window_props.mt1 = '1' # 外框材质PVC + window_props.mt2 = '1' # 内框材质塑料 + + # 设置窗户开启状态 + window_props.k00 = True # 第1扇窗户开启 + window_props.k01 = True # 第2扇窗户开启 + window_props.k02 = True # 第3扇窗户开启 + window_props.k03 = True # 第4扇窗户开启(如果有的话) + + print(f"窗户属性设置完成:") + print(f" - 水平扇数: {window_props.gen}") + print(f" - 垂直行数: {window_props.yuk}") + print(f" - 外框厚度: {window_props.kl1}cm") + print(f" - 竖框厚度: {window_props.kl2}cm") + print(f" - 内框厚度: {window_props.fk}cm") + print(f" - 窗户高度: {window_props.gny0}cm") + print(f" - 窗户类型: 平窗") + print(f" - 窗台: 启用") + print(f" - 窗户开启状态: k00={window_props.k00}, k01={window_props.k01}, k02={window_props.k02}, k03={window_props.k03}") + + + # 修复窗户玻璃材质 + print("修复窗户玻璃材质...") + fix_window_glass_material() + + # 等待更新完成 + bpy.context.view_layer.update() + + # 强制触发更新(如果需要) + try: + # 重新设置属性以触发更新 + current_gen = window_props.gen + current_gny0 = window_props.gny0 + + window_props.gen = 2 # 临时设置为其他值 + bpy.context.view_layer.update() + + window_props.gen = current_gen # 设置回目标值 + bpy.context.view_layer.update() + + print("已强制触发窗户更新") + + except Exception as update_error: + print(f"强制更新时出现错误: {update_error}") + + # 在修改落地窗全部属性后进行位移和旋转 + print("开始移动和旋转窗户组...") + try: + window_group1 = None + + for obj in bpy.data.objects: + if obj.name == "Window_Group": + window_group1 = obj + break + + # 设置位置 + window_group1.location = (0, 4.0, 0.0) + print(f"已将窗户组移动到位置: {window_group1.location}") + + # 设置旋转 + window_group1.rotation_euler = (math.radians(0), math.radians(0), math.radians(90)) # 旋转(0, 0, 90度) + print(f"已将窗户组旋转到: (0°, 0°, 90°)") + + # 强制更新 + bpy.context.view_layer.update() + print("窗户组位移和旋转完成") + + except Exception as move_error: + print(f"移动和旋转窗户组时出现错误: {move_error}") + + else: + print("警告: 未找到主窗户对象或WindowPanelGenerator属性") + # 尝试查找其他可能的窗户属性 + for obj in bpy.data.objects: + if hasattr(obj, 'archimesh_window'): + print(f"找到archimesh_window属性在对象: {obj.name}") + + # 打印窗户组的所有子对象信息 + print("窗户组包含的对象:") + for obj in bpy.data.objects: + if obj.parent == window_group: + print(f" - {obj.name} (位置: {obj.location})") + + + return True + else: + print("警告: 未找到Window_Group对象") + # 尝试查找其他可能的窗户对象 + window_objects = [obj for obj in bpy.data.objects if "Window" in obj.name] + if window_objects: + print("找到的窗户相关对象:") + for obj in window_objects: + print(f" - {obj.name} (类型: {obj.type})") + + # 尝试移动第一个找到的窗户对象 + first_window = window_objects[0] + first_window.location = (0.0, 1.96, 0.0) + print(f"已移动 {first_window.name} 到位置: {first_window.location}") + return True + else: + print("未找到任何窗户相关对象") + return False + + + + else: + print("窗户创建失败") + return False + + except Exception as e: + print(f"创建窗户时出现错误: {str(e)}") + import traceback + traceback.print_exc() + return False + + +def fix_window_glass_material(): + """修复窗户玻璃材质,将其改为高透BSDF""" + try: + print("开始修复窗户玻璃材质...") + + # 查找Window_Group下的Window对象 + window_group = None + for obj in bpy.data.objects: + if obj.name == "Window_Group": + window_group = obj + break + + if not window_group: + print("未找到Window_Group对象") + return False + + # 查找Window对象(Window_Group的子对象) + window_obj = None + for obj in bpy.data.objects: + if obj.name == "Window" and obj.parent == window_group: + window_obj = obj + break + + + if not window_obj: + print("未找到Window对象") + return False + + print(f"找到Window对象: {window_obj.name}") + + # 检查Window对象的材质 + if not window_obj.material_slots: + print("Window对象没有材质槽") + return False + + # 遍历所有材质槽 + for slot in window_obj.material_slots: + if slot.material and "Glass" in slot.material.name: + print(f"找到Glass材质: {slot.material.name}") + + # 启用节点编辑 + slot.material.use_nodes = True + + # 清除所有现有节点 + slot.material.node_tree.nodes.clear() + + # 创建半透BSDF节点 + translucent_bsdf = slot.material.node_tree.nodes.new(type='ShaderNodeBsdfTranslucent') + translucent_bsdf.location = (0, 0) + + # 设置半透BSDF参数 + translucent_bsdf.inputs['Color'].default_value = (0.95, 0.98, 1.0, 1.0) # 几乎无色 + # 半透BSDF节点没有Weight参数,只有Color参数 + + # 创建材质输出节点 + material_output = slot.material.node_tree.nodes.new(type='ShaderNodeOutputMaterial') + material_output.location = (300, 0) + + # 连接节点 + slot.material.node_tree.links.new( + translucent_bsdf.outputs['BSDF'], + material_output.inputs['Surface'] + ) + + # 设置材质混合模式为Alpha Blend + slot.material.blend_method = 'BLEND' + + print(f"已成功修改Glass材质为半透BSDF") + print(f" - 半透颜色: 几乎无色") + print(f" - 混合模式: Alpha Blend") + + return True + + print("未找到Glass材质") + return False + + except Exception as e: + print(f"修复玻璃材质时出现错误: {str(e)}") + import traceback + traceback.print_exc() + return False + +def setup_render_settings(): + """设置渲染参数(EEVEE渲染,优化速度)""" + scene = bpy.context.scene + + # 设置渲染引擎为EEVEE + scene.render.engine = 'BLENDER_EEVEE_NEXT' + print("已设置渲染引擎为EEVEE") + + # 启用Freestyle线条渲染 + # scene.render.use_freestyle = True + # print("已启用Freestyle线条渲染") + + # # 配置Freestyle线条设置 + # if hasattr(scene, 'view_layers'): + # view_layer = scene.view_layers[0] # 获取第一个视图层 + # view_layer.use_freestyle = True + + # # 获取Freestyle线条设置 + # freestyle = view_layer.freestyle_settings + + # # 启用线条渲染 + # freestyle.use_smoothness = True + # freestyle.use_culling = True + + # 设置线条宽度 - 使用正确的API 没有生效 + + + # 设置世界环境 + if hasattr(scene, 'world') and scene.world: + # 启用世界节点 + scene.world.use_nodes = True + + # 清除现有节点 + scene.world.node_tree.nodes.clear() + + # 创建自发光节点 + emission_node = scene.world.node_tree.nodes.new(type='ShaderNodeEmission') + emission_node.location = (0, 0) + + # 设置HSV颜色:色相0,饱和度0,明度0.051,Alpha 1 + # 转换为RGB:HSV(0, 0, 0.051) = RGB(0.051, 0.051, 0.051) + emission_node.inputs['Color'].default_value = (0.051, 0.051, 0.051, 1.0) # 深灰色 + emission_node.inputs['Strength'].default_value = 8.0 # 强度8 + + # 创建世界输出节点 + world_output = scene.world.node_tree.nodes.new(type='ShaderNodeOutputWorld') + world_output.location = (300, 0) + + # 连接节点 + scene.world.node_tree.links.new( + emission_node.outputs['Emission'], + world_output.inputs['Surface'] + ) + + print("已设置世界环境为自发光") + print(f" - 颜色: HSV(0, 0, 0.051) = RGB(0.051, 0.051, 0.051)") + print(f" - 强度: 8.0") + + # 设置EEVEE采样(降低采样数提高速度) + try: + if hasattr(scene.eevee, 'taa_render_samples'): + scene.eevee.taa_render_samples = 32 # 从64降低到32 + print(f"已设置EEVEE渲染采样: {scene.eevee.taa_render_samples}") + elif hasattr(scene.eevee, 'taa_samples'): + scene.eevee.taa_samples = 32 # 从64降低到32 + print(f"已设置EEVEE采样: {scene.eevee.taa_samples}") + else: + print("警告: 无法找到EEVEE采样设置,使用默认值") + + # 启用屏幕空间反射(简化设置) + if hasattr(scene.eevee, 'use_ssr'): + scene.eevee.use_ssr = True + print("已启用屏幕空间反射") + + # 启用环境光遮蔽(简化设置) + if hasattr(scene.eevee, 'use_gtao'): + scene.eevee.use_gtao = True + print("已启用环境光遮蔽") + + # 启用透明渲染(必要) + if hasattr(scene.eevee, 'use_transparent'): + scene.eevee.use_transparent = True + print("已启用透明渲染") + + # 设置透明混合模式(必要) + if hasattr(scene.render, 'film_transparent'): + scene.render.film_transparent = True + print("已启用透明背景") + + except AttributeError as e: + print(f"EEVEE设置警告: {e}") + print("使用默认EEVEE渲染设置") + + # 设置分辨率 + scene.render.resolution_x = 1080 # 宽度:1080px + scene.render.resolution_y = 2400 # 高度:2400px + scene.render.resolution_percentage = 100 + + # 设置输出格式 + scene.render.image_settings.file_format = 'PNG' + scene.render.image_settings.color_mode = 'RGBA' # 支持透明 + + # 设置输出路径到桌面 + desktop_path = os.path.join(os.path.expanduser("~"), "Desktop") + timestamp = time.strftime("%m%d_%H%M%S") # 日期_时分秒格式 + filename = f"isometric_{timestamp}.png" + scene.render.filepath = os.path.join(desktop_path, filename) + + print(f"EEVEE渲染设置完成,输出路径: {scene.render.filepath}") + return scene.render.filepath + +def setup_camera_and_lighting(): + """设置摄像机和照明(160W点光源 + 150W日光)""" + # 计算灯光高度(房间高度减一米) + room_height = 3.0 + light_height = room_height - 1.0 + + # 设置摄像机 + camera = None + if "Camera" in bpy.data.objects: + camera = bpy.data.objects["Camera"] + elif "Isometric Camera" in bpy.data.objects: + camera = bpy.data.objects["Isometric Camera"] + else: + # 创建新摄像机 + bpy.ops.object.camera_add(location=(7, -7, 5)) + camera = bpy.context.active_object + camera.name = "Isometric Camera" + + # 设置摄像机位置和旋转 + # 房间宽度5.0米,相机位置调整为(房间宽度-1, 1, 1.3) + room_width = 5.0 + camera.location = (room_width - 1, 1, 1.3) # 相机位置:(4, 1, 1.3) + camera.rotation_euler = (math.radians(90), math.radians(0), math.radians(30)) # 相机旋转 + + # 设置为透视投影 + camera.data.type = 'PERSP' + camera.data.lens = 35.0 # 35mm焦距 + camera.data.sensor_width = 35.0 # 35mm传感器 + camera.data.sensor_fit = 'AUTO' # 自动适配 + + # 设置为场景的活动摄像机 + bpy.context.scene.camera = camera + print("摄像机设置完成") + print(f"相机位置: ({camera.location.x}, {camera.location.y}, {camera.location.z})") + + # 设置蜡笔-场景线条画 + print("设置蜡笔-场景线条画...") + try: + # 启用蜡笔渲染 - 在Blender 4.2中使用不同的属性 + if hasattr(bpy.context.scene.render, 'use_grease_pencil'): + bpy.context.scene.render.use_grease_pencil = True + print("已启用蜡笔渲染") + else: + # 在Blender 4.2中,蜡笔渲染可能默认启用 + print("蜡笔渲染已默认启用") + + # 创建蜡笔对象(如果不存在)- 使用LINEART SCENE类型 + grease_pencil_name = "Grease_pencil" + grease_pencil_obj = None + + # 检查是否已存在蜡笔对象 + for obj in bpy.data.objects: + if obj.name == grease_pencil_name and obj.type == 'GPENCIL': + grease_pencil_obj = obj + print(f"找到现有蜡笔对象: {grease_pencil_name}") + break + + if grease_pencil_obj is None: + # 创建新的蜡笔对象 + bpy.ops.object.gpencil_add(type='LINEART_SCENE') + grease_pencil_obj = bpy.context.active_object + grease_pencil_obj.name = grease_pencil_name + + # 等待一帧以确保数据创建完成 + bpy.context.view_layer.update() + + print(f"已创建蜡笔场景线条画对象: {grease_pencil_name}") + + # 设置笔画厚度缩放为0.38 + if grease_pencil_obj and grease_pencil_obj.data: + grease_pencil_data = grease_pencil_obj.data + grease_pencil_data.pixel_factor = 0.38 + print(f"已设置蜡笔笔画厚度缩放: {grease_pencil_data.pixel_factor}") + else: + print("警告: 未找到蜡笔数据") + + # 确保蜡笔对象在渲染时可见 + if grease_pencil_obj: + grease_pencil_obj.hide_render = False + grease_pencil_obj.hide_viewport = False + print("蜡笔对象已设置为可见") + + print("蜡笔-场景线条画设置完成") + + except Exception as e: + print(f"设置蜡笔时出现错误: {e}") + print("继续执行其他设置...") + + + # 创建第一个160W点光源(房间中心) + # try: + # # 计算点光源位置(房间中心上方) + # room_length = 4.0 # Y轴 + # room_width = 8.0 # X轴(4+4) + + # # 第一个点光源位置:房间中心上方 + # light_x = 0 # X轴中心 + # light_y = 0 # Y轴中心 + # light_z = light_height + + # # 创建第一个点光源 + # bpy.ops.object.light_add(type='POINT', location=(light_x, light_y, light_z)) + # point_light1 = bpy.context.active_object + # point_light1.name = "Main Point Light" + # point_light1.data.energy = 160 # 160W + + # # 设置软衰减(简化设置) + # point_light1.data.shadow_soft_size = 0.5 # 0.5米半径,产生柔和阴影 + # point_light1.data.use_shadow = True # 启用阴影 + + # print(f"已创建第一个点光源(160W)") + # print(f"点光源位置: x={light_x}, y={light_y}, z={light_z}") + + # except Exception as e: + # print(f"创建第一个点光源时出错: {e}") + + # 创建第二个150W日光(位置12, 7, 6) + # try: + # # 第二个光源位置 - 调整到窗户后方更远的位置 + # light2_x = -6 + # light2_y = 7 # 让日光更远 + # light2_z = 6 # 提高高度 + + # # 创建日光 + # bpy.ops.object.light_add(type='SUN', location=(light2_x, light2_y, light2_z)) + # sun_light = bpy.context.active_object + # sun_light.name = "Sun Light" + # sun_light.data.energy = 20 + + # # 调整日光旋转角度,让光线更直接地照射窗户 + # sun_light.rotation_euler = (math.radians(0), math.radians(-60), math.radians(0)) # 简化旋转角度 + + # # 设置日光属性(简化设置) + # sun_light.data.angle = 0.05 # 从0.1改为0.05,让阴影更锐利 + # sun_light.data.use_shadow = True # 启用阴影 + + # print(f"已创建日光(20W)") + # print(f"日光位置: x={light2_x}, y={light2_y}, z={light2_z}") + # print(f"日光旋转: x={-70}°, y={0}°, z={0}°") + + # except Exception as e: + # print(f"创建日光时出错: {e}") + + print("照明设置完成") + print("灯光类型: 点光源 + 日光") + print(f"衰减类型: 点光源软衰减 + 日光平行光") + print(f"阴影设置: 启用,柔和阴影") + print(f"主灯光高度: {light_height}米 (房间高度减一米)") + print(f"日光位置: (0, 10, 6)米,旋转: (-70°, 0°, 0°)") + +def render_scene(): + """渲染场景""" + print("开始EEVEE渲染...") + + # 执行渲染 + bpy.ops.render.render(write_still=True) + print(f"EEVEE渲染完成! 图片保存在: {bpy.context.scene.render.filepath}") + return bpy.context.scene.render.filepath + +def main(): + """主函数""" + print("=" * 60) + print("开始自动创建等轴测房间并EEVEE渲染") + print("=" * 60) + + try: + # 1. 创建房间 + print("1. 创建等轴测房间...") + if not create_isometric_room(): + print("房间创建失败,停止执行") + return False + + # 2. 创建落地窗 + print("2. 创建archimesh落地窗...") + if not create_window_with_archimesh(): + print("落地窗创建失败,但继续执行") + # 不中断执行,窗户创建失败不影响渲染 + # 2. 创建门 + print("2. 创建archimesh门...") + if not create_door_with_archimesh(): + print("落地窗创建失败,但继续执行") + # 不中断执行,窗户创建失败不影响渲染 + + # 3. 设置渲染参数(EEVEE) + print("3. 设置EEVEE渲染参数...") + output_path = setup_render_settings() + + # 4. 设置摄像机和照明 + print("4. 设置摄像机和照明...") + setup_camera_and_lighting() + + # 5. 渲染场景 + print("5. 开始EEVEE渲染...") + final_path = render_scene() + + print("=" * 60) + print("EEVEE渲染完成!") + print(f"渲染图片已保存到桌面: {final_path}") + print("=" * 60) + + return True + + except Exception as e: + print(f"执行过程中出现错误: {str(e)}") + import traceback + traceback.print_exc() + return False + +# 执行主函数 +if __name__ == "__main__": + main() +''' + + +def check_blender_exists(): + """检查Blender是否存在于指定路径""" + if not os.path.exists(BLENDER_PATH): + print(f"错误: 在路径 '{BLENDER_PATH}' 找不到Blender") + print("请检查Blender安装路径是否正确") + return False + return True + + +def create_temp_script(): + """创建临时的Blender脚本文件""" + script_path = os.path.join(os.getcwd(), "temp_simple_blender_script.py") + with open(script_path, 'w', encoding='utf-8') as f: + f.write(BLENDER_SIMPLE_SCRIPT) + return script_path + + +def launch_blender_simple(): + """启动Blender并执行简化版自动化脚本""" + if not check_blender_exists(): + return False + + # 创建临时脚本文件 + script_path = create_temp_script() + + try: + print("正在启动Blender(白模版本)...") + print(f"Blender路径: {BLENDER_PATH}") + print("特点:") + print("- 不设置材质,使用默认白色材质") + print("- 快速渲染") + print("- 兼容Blender 4.2") + print("- 自动保存到桌面") + + # 构建命令行参数 + cmd = [ + BLENDER_PATH, + "--background", # 后台运行模式 + "--disable-crash-handler", # 禁用崩溃处理器 + "--python", script_path # 执行Python脚本 + ] + + print("\n开始执行白模渲染流程...") + + # 启动Blender并等待完成 + process = subprocess.run( + cmd, + capture_output=True, + text=True, + encoding='utf-8', + timeout=300 # 5分钟超时 + ) + + # 输出结果 + if process.stdout: + print("Blender输出:") + # 过滤掉重复的错误信息 + lines = process.stdout.split('\n') + filtered_lines = [] + for line in lines: + if not ("AttributeError: 'NoneType' object has no attribute 'idname'" in line or + "Error in bpy.app.handlers.depsgraph_update_post" in line): + filtered_lines.append(line) + print('\n'.join(filtered_lines)) + + if process.stderr and not "rotate_tool.py" in process.stderr: + print("重要错误信息:") + print(process.stderr) + + if process.returncode == 0: + print("\n✅ 白模渲染执行成功!") + print("白模图片应该已经保存到桌面") + return True + else: + print(f"\n❌ 执行失败,返回码: {process.returncode}") + return False + + except subprocess.TimeoutExpired: + print("\n⏰ 执行超时(5分钟),可能Blender仍在运行") + print("请检查桌面是否有生成的图片") + return False + except Exception as e: + print(f"启动Blender时出现错误: {str(e)}") + return False + + finally: + # 清理临时文件 + try: + if os.path.exists(script_path): + os.remove(script_path) + print(f"已清理临时文件: {script_path}") + except: + pass + + +def main(): + """主函数""" + print("=" * 70) + print("Blender 4.2兼容版自动化脚本 - 白模渲染") + print("=" * 70) + print("特点:") + print("✓ 移除所有材质设置") + print("✓ 使用默认白色材质") + print("✓ 快速渲染") + print("✓ 兼容Blender 4.2") + print("✓ 自动保存到桌面") + print("=" * 70) + + success = launch_blender_simple() + + if success: + print("\n🎉 白模渲染完成!") + print("请检查桌面上的渲染图片") + print("图片文件名格式: isometric_room_white_[时间戳].png") + else: + print("\n❌ 执行失败!") + print("可能的解决方案:") + print("1. 确保Isometquick插件已正确安装") + print("2. 禁用可能冲突的其他插件") + print("3. 检查磁盘空间是否足够") + + +if __name__ == "__main__": + main()