irg/Isometquick-server/blender_service0716).py

593 lines
21 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:
os.makedirs(self.output_dir, exist_ok=True)
logger.info(f"输出目录设置为: {self.output_dir}")
except PermissionError as e:
logger.error(f"无法创建输出目录 {self.output_dir}: 权限不足")
raise
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 _calculate_room_parameters(self, request: RenderRequest) -> Tuple[float, float, float]:
"""
计算房间参数floor_scale_value, x_extrude_value, y_extrude_value
Returns:
(floor_scale_value, x_extrude_value, y_extrude_value)
"""
room_length = request.room.length # Y轴长度
room_width = request.room.width # X轴宽度
# 判断长宽是否相等
if room_length == room_width:
# 长宽相等,按原逻辑
floor_scale_value = room_width # 使用宽度作为基准
x_extrude_value = 0.0
y_extrude_value = 0.0
else:
# 长宽不等,需要调整
if room_length > room_width:
# 长比宽大Y轴比X轴大
floor_scale_value = room_width # 使用较小的值(宽度)
x_extrude_value = 0.0
y_extrude_value = room_length - room_width # Y轴延伸
else:
# 宽比长大X轴比Y轴大
floor_scale_value = room_length # 使用较小的值(长度)
x_extrude_value = room_width - room_length # X轴延伸
y_extrude_value = 0.0
logger.info(
f"房间参数计算: floor_scale={floor_scale_value}, x_extrude={x_extrude_value}, y_extrude={y_extrude_value}")
return floor_scale_value, x_extrude_value, y_extrude_value
def _calculate_camera_position(self, request: RenderRequest) -> tuple:
"""
根据房间尺寸和视图类型计算摄像机位置
Returns:
(x, y, z) 摄像机位置坐标
"""
room_length = request.room.length # Y轴长度
room_width = request.room.width # X轴宽度
camera_height = request.camera.height # Z轴
# 判断长宽是否相等
if room_length == room_width:
# 长宽相等,使用原来的逻辑
if request.camera.view_type == 1:
# 正视图: x=0, y=-(房间的长度/2-1), z=摄像机垂直高度
x = 0
y = -(room_length / 2 - 1)
z = camera_height
else:
# 侧视图: x=(房间的宽度/2-1), y=-(房间的长度/2-1), z=摄像机垂直高度
x = room_width / 2 - 1
y = -(room_length / 2 - 1)
z = camera_height
else:
# 长宽不等,使用新的逻辑
if room_length > room_width:
# 长比宽大Y轴比X轴大
if request.camera.view_type == 1:
# 正视图: x=0, y=-(房间的长度-(房间的宽度/2)-1), z=摄像机垂直高度
x = 0
y = -(room_length - (room_width / 2) - 1)
z = camera_height
else:
# 侧视图: x=(房间的宽度/2-1), y=-(房间的长度-(房间的宽度/2)-1), z=摄像机垂直高度
x = room_width / 2 - 1
y = -(room_length - (room_width / 2) - 1)
z = camera_height
else:
# 宽比长大X轴比Y轴大
if request.camera.view_type == 1:
# 正视图: x=(房间的宽度-房间的长度)/2, y=-(房间的长度/2-1), z=摄像机垂直高度
x = (room_width - room_length) / 2
y = -(room_length / 2 - 1)
z = camera_height
else:
# 侧视图: x=(房间的宽度-(房间的长度/2)-1), y=-(房间的长度/2-1), z=摄像机垂直高度
x = room_width - (room_length / 2) - 1
y = -(room_length / 2 - 1)
z = camera_height
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:
# 侧视图: 90, 0, 相机旋转角度
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脚本"""
# 计算房间参数
floor_scale_value, x_extrude_value, y_extrude_value = self._calculate_room_parameters(
request)
# 计算摄像机位置和旋转
camera_pos = self._calculate_camera_position(request)
camera_rot = self._calculate_camera_rotation(request)
# 设置输出文件路径
output_file = os.path.join(self.output_dir, f"render_{task_id}.png")
# 渲染引擎映射
engine_map = {
"workbench": "BLENDER_WORKBENCH",
"eevee": "BLENDER_EEVEE_NEXT",
"cycles": "CYCLES"
}
render_engine = engine_map.get(
request.render.engine, "BLENDER_WORKBENCH")
# 计算灯光高度(房间高度减一米)
light_height = request.room.height - 1.0
script = f'''
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():
"""创建等轴测房间"""
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)
# 检查Isometquick插件
if not hasattr(bpy.context.scene, 'iso_tool'):
print("错误: Isometquick插件未正确加载")
return False
# 获取Isometquick设置
iso_tool = bpy.context.scene.iso_tool
# 启用需要的组件
iso_tool.create_floor = True
iso_tool.create_left_wall = True
iso_tool.create_right_wall = True
iso_tool.create_left_light = True
iso_tool.create_right_light = True
iso_tool.create_hidden_ceiling = True
iso_tool.create_hidden_leftwall = False
iso_tool.create_hidden_rightwall = False
# 找到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):
# 使用计算出的房间参数创建房间
print(f"房间参数: floor_scale={floor_scale_value}, x_extrude={x_extrude_value}, y_extrude={y_extrude_value}")
result = bpy.ops.object.isometric_operator(
floor_scale_value={floor_scale_value},
wall_height_value={request.room.height},
equal_thickness_value=0.15,
wall_thickness_value=0.0,
floor_thickness_value=0.0,
x_extrude_value={x_extrude_value},
y_extrude_value={y_extrude_value}
)
if result == {{'FINISHED'}}:
print("等轴测房间创建完成!")
fix_visibility()
return True
else:
print("房间创建失败")
return False
except Exception as e:
print(f"创建房间时出现错误: {{str(e)}}")
import traceback
traceback.print_exc()
return False
def fix_visibility():
"""修复对象可见性设置"""
try:
# 设置光源不可见
light_objects = ["ISO Emission Left", "ISO Emission Right"]
for obj_name in light_objects:
if obj_name in bpy.data.objects:
obj = bpy.data.objects[obj_name]
if hasattr(obj, 'visibility_camera'):
obj.visibility_camera = False
elif hasattr(obj, 'cycles_visibility'):
obj.cycles_visibility.camera = False
print(f"已设置 {{obj_name}} 的可见性")
# 设置天花板可见
if "Hidden Ceiling" in bpy.data.objects:
ceiling_obj = bpy.data.objects["Hidden Ceiling"]
if hasattr(ceiling_obj, 'visibility_camera'):
ceiling_obj.visibility_camera = True
elif hasattr(ceiling_obj, 'cycles_visibility'):
ceiling_obj.cycles_visibility.camera = True
ceiling_obj.hide_viewport = False
ceiling_obj.hide_render = False
print("已设置 Hidden Ceiling 为可见")
except Exception as e:
print(f"设置可见性时出现错误: {{str(e)}}")
def setup_render_settings():
"""设置渲染参数"""
scene = bpy.context.scene
# 设置渲染引擎为EEVEE_NEXT
scene.render.engine = 'BLENDER_EEVEE_NEXT'
print(f"已设置渲染引擎为: BLENDER_EEVEE_NEXT")
# 设置EEVEE参数
try:
if hasattr(scene.eevee, 'taa_render_samples'):
scene.eevee.taa_render_samples = 64
print(f"已设置EEVEE渲染采样: {{scene.eevee.taa_render_samples}}")
elif hasattr(scene.eevee, 'taa_samples'):
scene.eevee.taa_samples = 64
print(f"已设置EEVEE采样: {{scene.eevee.taa_samples}}")
else:
print("警告: 无法找到EEVEE采样设置使用默认值")
# 启用屏幕空间反射
if hasattr(scene.eevee, 'use_ssr'):
scene.eevee.use_ssr = True
scene.eevee.use_ssr_refraction = True
print("已启用屏幕空间反射")
# 启用环境光遮蔽
if hasattr(scene.eevee, 'use_gtao'):
scene.eevee.use_gtao = True
print("已启用环境光遮蔽")
# 启用体积雾
if hasattr(scene.eevee, 'use_volumetric_lights'):
scene.eevee.use_volumetric_lights = 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():
"""设置摄像机和照明180W点光源高度为房间高度减一米"""
# 计算灯光高度(房间高度减一米)
room_height = {request.room.height}
light_height = room_height - 1.0
# 设置摄像机
camera = None
if "Camera" in bpy.data.objects:
camera = bpy.data.objects["Camera"]
else:
bpy.ops.object.camera_add(location={camera_pos})
camera = bpy.context.active_object
camera.name = "API Camera"
# 设置摄像机位置和旋转
camera.location = {camera_pos}
camera.rotation_euler = {camera_rot}
# 设置为透视投影
camera.data.type = 'PERSP'
camera.data.lens = 35.0
camera.data.sensor_width = 35.0
camera.data.sensor_fit = 'AUTO'
# 设置为场景的活动摄像机
bpy.context.scene.camera = camera
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}}")
# 创建单个180W点光源
try:
# 计算点光源位置(房间中心上方)
room_length = {request.room.length} # Y轴
room_width = {request.room.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_light = bpy.context.active_object
point_light.name = "Main Point Light"
point_light.data.energy = 180 # 180W
# 设置软衰减
point_light.data.falloff_type = 'INVERSE_SQUARE' # 平方反比衰减(物理准确)
# 设置阴影
point_light.data.use_shadow = True # 启用阴影
point_light.data.shadow_soft_size = 0.5 # 0.5米半径,产生柔和阴影
# EEVEE渲染器的阴影设置
if hasattr(point_light.data, 'shadow_buffer_size'):
point_light.data.shadow_buffer_size = 2048 # 阴影贴图分辨率
# Cycles渲染器的阴影设置
if hasattr(point_light.data, 'cycles'):
point_light.data.cycles.cast_shadow = True # Cycles中启用阴影投射
point_light.data.cycles.use_multiple_importance_sampling = True # 启用重要性采样
# 设置衰减距离(可选)
# point_light.data.cutoff_distance = 15.0 # 15米后完全衰减
# point_light.data.use_custom_distance = True
print(f"已创建180W点光源软衰减+阴影)")
print(f"点光源位置: x={{light_x}}, y={{light_y}}, z={{light_z}}")
print("总照明功率: 180W")
print("灯光类型: 点光源(软衰减+阴影)")
print(f"衰减类型: 平方反比衰减")
print(f"阴影设置: 启用柔和阴影半径0.5米")
print(f"灯光高度: {{light_height}}米 (房间高度减一米)")
except Exception as e:
print(f"创建点光源时出错: {{e}}")
print("将使用默认的ISO Emission灯光")
print("照明设置完成")
def render_scene():
"""渲染场景"""
print("开始渲染...")
bpy.ops.render.render(write_still=True)
print(f"渲染完成!")
return bpy.context.scene.render.filepath
def main():
"""主函数"""
print("=" * 60)
print("开始API渲染任务")
print("=" * 60)
try:
# 1. 创建房间
print("1. 创建等轴测房间...")
if not create_isometric_room():
print("房间创建失败,停止执行")
return False
# 2. 设置渲染参数
print("2. 设置渲染参数...")
output_path = setup_render_settings()
# 3. 设置摄像机和照明
print("3. 设置摄像机和照明...")
setup_camera_and_lighting()
# 4. 渲染场景
print("4. 开始渲染...")
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:
# 生成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)}")
# 执行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分钟超时
)
# 检查执行结果
if process.returncode == 0:
if os.path.exists(output_file):
logger.info(f"渲染成功: {output_file}")
return output_file
else:
raise Exception("渲染完成但输出文件不存在")
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