irg/Isometquick-server/blender_service.py

1177 lines
48 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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]:
"""
调整房间尺寸确保lengthY轴始终大于等于widthX轴
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.051Alpha 1
# 转换为RGBHSV(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