#!/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