1177 lines
48 KiB
Python
1177 lines
48 KiB
Python
#!/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
|