diff --git a/Isometquick-server/README.md b/Isometquick-server/README.md new file mode 100644 index 0000000..57f60b4 --- /dev/null +++ b/Isometquick-server/README.md @@ -0,0 +1,295 @@ +# Isometquick Blender渲染微服务 + +基于FastAPI的Blender等轴测房间渲染API服务,专为Linux服务器环境设计。 + +## 功能特性 + +- 🏠 **等轴测房间渲染**: 支持自定义房间尺寸 +- 📷 **多视角渲染**: 正视图和侧视图 +- 🎨 **多渲染引擎**: 工作台、EEVEE Next、Cycles +- 🚀 **异步处理**: 后台渲染任务 +- 📊 **任务管理**: 任务状态查询和文件下载 +- 🔍 **API文档**: 自动生成的Swagger文档 + +## 系统要求 + +- Python 3.8+ +- Blender 4.3+ +- Linux服务器环境 +- Isometquick插件已安装 +- 确保 `/data/Isometquick/` 目录存在且有写入权限 + +## 快速开始 + +### 1. 创建输出目录 + +```bash +sudo mkdir -p /data/Isometquick/ +sudo chown $USER:$USER /data/Isometquick/ +sudo chmod 755 /data/Isometquick/ +``` + +### 2. 安装依赖 + +```bash +cd Isometquick-server +pip install -r requirements.txt +``` + +### 3. 配置Blender路径(可选) + +```bash +export BLENDER_PATH="/usr/local/bin/blender" +``` + +### 4. 启动服务 + +```bash +python start.py +``` + +或使用uvicorn直接启动: + +```bash +uvicorn main:app --host 0.0.0.0 --port 8003 +``` + +### 5. 访问API文档 + +打开浏览器访问:`http://localhost:8003/docs` + +## API接口 + +### 创建渲染任务 + +**POST** `/render` + +```json #prop_type:description="道具类型: 0=无, 1=窗, 2=拱门, 3=门" +{ + "room": { + "length": 4.0, + "width": 4.0, + "height": 3.0, + "prop_type":0, + }, + "camera": { + "height": 1.3, + "view_type": 2, + "rotation_angle": 45.0 + }, + "render": { + "resolution_x": 1080, + "resolution_y": 2400, + "engine": "workbench" + } +} +``` + +### 查询任务状态 + +**GET** `/render/{task_id}` + +### 下载渲染结果 + +**GET** `/render/{task_id}/download` + +**注意**: 渲染图片保存在 `/data/Isometquick/` 目录下,文件名格式为 `render_{task_id}.png` + +### 删除任务 + +**DELETE** `/render/{task_id}` + +## 参数说明 + +### 房间尺寸 (room) + +- `length`: 房间长度(X轴),默认4.0米 +- `width`: 房间宽度(Y轴),默认4.0米 +- `height`: 房间高度(Z轴),默认3.0米 + +### 摄像机设置 (camera) + +- `height`: 摄像机垂直高度,默认1.3米 +- `view_type`: 视图类型,1=正视图,2=侧视图(**默认侧视图**) +- `rotation_angle`: 摄像机旋转角度(仅侧视图),默认45度 + +#### 摄像机位置计算 + +**正视图 (view_type=1)**: +- 位置: `(0, -(width/2-1), height)` +- 旋转: `(90°, 0°, 0°)` + +**侧视图 (view_type=2) - 默认**: +- 位置: `((length/2-1), -(width/2-1), height)` +- 旋转: `(90°, 0°, rotation_angle)` + +### 渲染设置 (render) + +- `resolution_x`: 渲染宽度,默认1080px +- `resolution_y`: 渲染高度,默认2400px +- `engine`: 渲染引擎,支持workbench/eevee/cycles + +## 文件存储 + +- **输出目录**: `/data/Isometquick/` +- **文件命名**: `render_{task_id}.png` +- **文件权限**: 确保服务进程对该目录有读写权限 +- **清理策略**: 24小时后自动清理旧文件 + +## 使用示例 + +### Python客户端示例 + +```python +import requests +import time + +# 创建渲染任务(使用默认侧视图) +response = requests.post("http://localhost:8003/render", json={ + "room": { + "length": 5.0, + "width": 4.0, + "height": 3.5 + }, + "camera": { + "height": 1.5, + "view_type": 2, + "rotation_angle": 30.0 + }, + "render": { + "resolution_x": 1920, + "resolution_y": 1080, + "engine": "workbench" + } +}) + +task_id = response.json()["task_id"] + +# 轮询任务状态 +while True: + status_response = requests.get(f"http://localhost:8003/render/{task_id}") + status = status_response.json()["status"] + + if status == "completed": + # 下载结果 + download_response = requests.get(f"http://localhost:8003/render/{task_id}/download") + with open(f"render_{task_id}.png", "wb") as f: + f.write(download_response.content) + break + elif status == "failed": + print("渲染失败") + break + + time.sleep(2) +``` + +### curl示例 + +```bash +# 创建任务(使用默认侧视图) +curl -X POST "http://localhost:8003/render" \ + -H "Content-Type: application/json" \ + -d '{ + "room": {"length": 4.0, "width": 4.0, "height": 3.0}, + "camera": {"height": 1.3, "view_type": 2, "rotation_angle": 45.0}, + "render": {"resolution_x": 1080, "resolution_y": 2400, "engine": "workbench"} + }' + +# 查询状态 +curl "http://localhost:8003/render/{task_id}" + +# 下载结果 +curl "http://localhost:8003/render/{task_id}/download" -o render.png +``` + +## 部署 + +### 目录准备 + +```bash +# 创建输出目录 +sudo mkdir -p /data/Isometquick/ +sudo chown www-data:www-data /data/Isometquick/ # 或适当的用户 +sudo chmod 755 /data/Isometquick/ +``` + +### Docker部署 + +```dockerfile +FROM python:3.9-slim + +# 安装Blender +RUN apt-get update && apt-get install -y blender + +# 创建输出目录 +RUN mkdir -p /data/Isometquick/ + +WORKDIR /app +COPY requirements.txt . +RUN pip install -r requirements.txt + +COPY . . + +# 确保输出目录权限 +RUN chmod 755 /data/Isometquick/ + +EXPOSE 8003 +CMD ["python", "start.py"] +``` + +### systemd服务 + +```ini +[Unit] +Description=Isometquick Blender Render Service +After=network.target + +[Service] +Type=simple +User=blender +WorkingDirectory=/opt/isometquick-server +ExecStart=/usr/bin/python3 start.py +Restart=always +RestartSec=3 + +# 确保有访问输出目录的权限 +ReadWritePaths=/data/Isometquick/ + +[Install] +WantedBy=multi-user.target +``` + +## 性能优化 + +- 调整 `MAX_CONCURRENT_RENDERS` 控制并发渲染数量 +- 使用工作台引擎获得最快渲染速度 +- 定期清理旧的渲染文件 +- 考虑使用Redis存储任务状态(生产环境) +- 监控 `/data/Isometquick/` 目录磁盘使用情况 + +## 故障排除 + +### Blender未找到 + +确保Blender已安装并在系统PATH中,或设置`BLENDER_PATH`环境变量。 + +### 插件未加载 + +确保Isometquick插件已正确安装在Blender中。 + +### 渲染超时 + +调整`BLENDER_TIMEOUT`配置或优化渲染参数。 + +### 文件权限错误 + +确保服务进程对 `/data/Isometquick/` 目录有读写权限: + +```bash +sudo chown -R $SERVICE_USER:$SERVICE_GROUP /data/Isometquick/ +sudo chmod -R 755 /data/Isometquick/ +``` + +## 许可证 + +与原Isometquick插件相同的许可证。 \ No newline at end of file diff --git a/Isometquick-server/blender_service.py b/Isometquick-server/blender_service.py new file mode 100644 index 0000000..a1d05b0 --- /dev/null +++ b/Isometquick-server/blender_service.py @@ -0,0 +1,1154 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Blender渲染服务 +处理Blender渲染逻辑,适配Linux服务器环境 +""" + +import os +import subprocess +import asyncio +import tempfile +import shutil +import math +import time +from typing import Optional, Tuple +import logging + +from models import RenderRequest + +logger = logging.getLogger(__name__) + + +class BlenderRenderService: + """Blender渲染服务类""" + + def __init__(self, blender_path: str = None): + """ + 初始化Blender服务 + + Args: + blender_path: Blender可执行文件路径,如果为None则自动检测 + """ + self.blender_path = blender_path or self._find_blender_path() + self.output_dir = "/data/Isometquick/" + self._ensure_output_dir() + + def _find_blender_path(self) -> str: + """自动查找Blender路径""" + # Linux常见路径 + possible_paths = [ + "/data/blender/blender-4.2.11-linux-x64/blender", + "blender" # 系统PATH中 + ] + + for path in possible_paths: + if shutil.which(path): + logger.info(f"找到Blender: {path}") + return path + + # 如果都找不到,使用默认值 + logger.warning("未找到Blender,使用默认路径") + return "blender" + + def _ensure_output_dir(self): + """确保输出目录存在""" + try: + # 检查目录是否存在 + if not os.path.exists(self.output_dir): + logger.info(f"创建输出目录: {self.output_dir}") + os.makedirs(self.output_dir, exist_ok=True) + + # 检查目录权限 + if not os.access(self.output_dir, os.W_OK): + logger.error(f"输出目录无写权限: {self.output_dir}") + # 尝试使用本地目录作为备用 + self.output_dir = "./renders" + os.makedirs(self.output_dir, exist_ok=True) + logger.info(f"使用备用输出目录: {self.output_dir}") + + logger.info(f"输出目录设置为: {self.output_dir}") + + # 测试写入权限 + test_file = os.path.join(self.output_dir, "test_write.tmp") + try: + with open(test_file, 'w') as f: + f.write("test") + os.remove(test_file) + logger.info("输出目录写入权限测试通过") + except Exception as e: + logger.error(f"输出目录写入权限测试失败: {e}") + raise + + except PermissionError as e: + logger.error(f"无法创建输出目录 {self.output_dir}: 权限不足") + # 使用当前目录作为备用 + self.output_dir = "./renders" + os.makedirs(self.output_dir, exist_ok=True) + logger.info(f"使用备用输出目录: {self.output_dir}") + except Exception as e: + logger.error(f"创建输出目录失败 {self.output_dir}: {e}") + raise + + def check_blender_available(self) -> bool: + """检查Blender是否可用""" + try: + result = subprocess.run( + [self.blender_path, "--version"], + capture_output=True, + text=True, + timeout=10 + ) + return result.returncode == 0 + except Exception as e: + logger.error(f"Blender检查失败: {e}") + return False + + def _adjust_room_dimensions(self, request: RenderRequest) -> Tuple[float, float, float]: + """ + 调整房间尺寸:确保length(Y轴)始终大于等于width(X轴) + + Returns: + (adjusted_length, adjusted_width, height) - 调整后的长、宽、高 + """ + original_length = request.room.length # Y轴(长度) + original_width = request.room.width # X轴(宽度) + height = request.room.height # Z轴(高度) + + # 判断长宽大小,确保length始终大于等于width + if original_length >= original_width: + # 长大于等于宽,保持原值 + adjusted_length = original_length + adjusted_width = original_width + logger.info( + f"房间尺寸无需调整: length={adjusted_length}m, width={adjusted_width}m, height={height}m") + else: + # 宽大于长,交换值 + adjusted_length = original_width + adjusted_width = original_length + logger.info( + f"房间尺寸已调整: 原length={original_length}m, 原width={original_width}m") + logger.info( + f"调整后: length={adjusted_length}m, width={adjusted_width}m, height={height}m") + + return adjusted_length, adjusted_width, height + + def _calculate_camera_position(self, request: RenderRequest, adjusted_width: float) -> tuple: + """ + 根据房间尺寸和视图类型计算摄像机位置 + + Args: + request: 渲染请求参数 + adjusted_width: 调整后的房间宽度(X轴) + + Returns: + (x, y, z) 摄像机位置坐标 + """ + camera_height = request.camera.height # Z轴 + + if request.camera.view_type == 1: + # 正视图逻辑:x = width/2, y = 1, z = 相机高度 + x = adjusted_width / 2 # X = width/2 + y = 1 # Y = 1 + z = camera_height # Z = 相机高度 + else: + # 侧视图逻辑:保持原有设置 + x = adjusted_width - 1 # X = width - 1 + y = 1 # Y = 1 + z = camera_height # Z = 相机高度 + + logger.info(f"相机位置计算: x={x}, y={y}, z={z}") + return (x, y, z) + + def _calculate_camera_rotation(self, request: RenderRequest) -> tuple: + """ + 根据视图类型计算摄像机旋转角度 + + Returns: + (rx, ry, rz) 摄像机旋转角度(弧度) + """ + if request.camera.view_type == 1: + # 正视图逻辑:90, 0, 0 + rx = math.radians(90) + ry = math.radians(0) + rz = math.radians(0) + else: + # 侧视图逻辑(保持原有逻辑) + rx = math.radians(90) + ry = math.radians(0) + rz = math.radians(request.camera.rotation_angle) + + return (rx, ry, rz) + + def _generate_blender_script(self, task_id: str, request: RenderRequest) -> str: + """生成Blender Python脚本""" + + # 调整房间尺寸 + adjusted_length, adjusted_width, height = self._adjust_room_dimensions( + request) + + # 计算摄像机位置和旋转 + camera_pos = self._calculate_camera_position(request, adjusted_width) + camera_rot = self._calculate_camera_rotation(request) + + # 设置输出文件路径(使用绝对路径) + output_file = os.path.abspath(os.path.join( + self.output_dir, f"render_{task_id}.png")) + + # 渲染引擎固定为EEVEE + render_engine = "BLENDER_EEVEE_NEXT" + + # 计算灯光高度(房间高度减一米) + light_height = height - 1.0 + + # 道具类型 + prop_type = request.room.prop_type + + # 计算道具位置参数 + door_width = 1.0 # 门宽度1米 + window_width = 3.45 # 窗宽度3.45米 + wall_distance = 0.8 # 距后墙80cm + + script = f''' +import bpy +import bmesh +import os +import math +import time + +# 设置输出路径 +OUTPUT_FILE = "{output_file}" +print(f"输出文件路径: {{OUTPUT_FILE}}") + +# 定义道具类型变量 +PROP_TYPE = {prop_type} + +def disable_problematic_addons(): + """禁用可能导致问题的插件""" + try: + problematic_addons = ['bl_tool'] + for addon in problematic_addons: + if addon in bpy.context.preferences.addons: + bpy.ops.preferences.addon_disable(module=addon) + print(f"已禁用插件: {{addon}}") + except: + pass + +def create_isometric_room(): + """创建等轴测房间,使用isometric_room_gen插件""" + try: + disable_problematic_addons() + + # 清除默认立方体 + if "Cube" in bpy.data.objects: + bpy.data.objects.remove(bpy.data.objects["Cube"], do_unlink=True) + + # 设置3D游标位置到原点 + bpy.context.scene.cursor.location = (0.0, 0.0, 0.0) + + # 检查isometric_room_gen插件 + if not hasattr(bpy.context.scene, 'sna_room_width'): + print("错误: isometric_room_gen插件未正确加载") + return False + + # 设置isometric_room_gen插件参数 + # 房间设置 - 注意参数对应关系 + bpy.context.scene.sna_wall_thickness = 0.10 # 墙体厚度10cm + bpy.context.scene.sna_room_width = {adjusted_width} # 房间宽度(X轴) + bpy.context.scene.sna_room_depth = {adjusted_length} # 房间深度(Y轴) + bpy.context.scene.sna_room_height = {height} # 房间高度(Z轴) + + # 窗户设置 - 道具类型1(窗户)不在这里设置,而是通过archimesh插件创建 + bpy.context.scene.sna_windows_enum = 'NONE' # 无窗户 + + # 设置房间类型为方形 + bpy.context.scene.sna_style = 'Square' + + # 设置拱门参数 + bpy.context.scene.sna_arch_settings = True # 启用拱门设置 + if PROP_TYPE == 2: # 拱门 + bpy.context.scene.sna_arch_placement = 'BACK' # 设置为后墙拱门 + else: + bpy.context.scene.sna_arch_placement = 'NONE' # 不设置拱门 + bpy.context.scene.sna_arch_width = 1.2 # 拱门宽度1.2米 + bpy.context.scene.sna_arch_height = 2.4 # 拱门高度2.4米 + bpy.context.scene.sna_arch_thickness = 0.10 # 拱门厚度0.1米 + + # 找到3D视图区域 + view3d_area = None + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + view3d_area = area + break + + if view3d_area is None: + print("错误: 找不到3D视图区域") + return False + + # 临时切换到3D视图上下文 + with bpy.context.temp_override(area=view3d_area): + # 使用isometric_room_gen插件创建房间 + result = bpy.ops.sna.gen_room_1803a() + + if result == {{'FINISHED'}}: + print("等轴测房间创建完成!") + print(f"房间尺寸: {{bpy.context.scene.sna_room_width}}m x {{bpy.context.scene.sna_room_depth}}m x {{bpy.context.scene.sna_room_height}}m") + print(f"墙体厚度: {{bpy.context.scene.sna_wall_thickness}}m") + print(f"道具类型: {{PROP_TYPE}} (0=无, 1=窗, 2=拱门, 3=门)") + return True + else: + print("房间创建失败") + return False + + except Exception as e: + print(f"创建房间时出现错误: {{str(e)}}") + import traceback + traceback.print_exc() + return False + +def create_door_with_archimesh(): + """使用archimesh插件创建门""" + try: + print("开始创建archimesh门...") + + # 检查archimesh插件是否可用 + if not hasattr(bpy.ops.mesh, 'archimesh_door'): + print("错误: archimesh插件未正确加载") + print("请确保archimesh插件已安装并启用") + return False + + # 设置3D游标到原点 + bpy.context.scene.cursor.location = (0.0, 0.0, 0.0) + + # 找到3D视图区域 + view3d_area = None + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + view3d_area = area + break + + if view3d_area is None: + print("错误: 找不到3D视图区域") + return False + + # 临时切换到3D视图上下文 + with bpy.context.temp_override(area=view3d_area): + # 先创建门(使用默认参数) + result = bpy.ops.mesh.archimesh_door() + + if result == {{'FINISHED'}}: + print("archimesh门创建完成!") + + # 等待一帧以确保对象创建完成 + bpy.context.view_layer.update() + + # 找到刚创建的门组对象(Door_Group) + door_group = None + for obj in bpy.data.objects: + if obj.name == "Door_Group": + door_group = obj + break + + if door_group: + print(f"找到门组: {{door_group.name}}") + + # 查找主门对象(DoorFrame) + main_door_obj = None + for obj in bpy.data.objects: + if obj.name == "DoorFrame": + main_door_obj = obj + break + + if main_door_obj and hasattr(main_door_obj, 'DoorObjectGenerator'): + # 获取门属性 + door_props = main_door_obj.DoorObjectGenerator[0] + + print("设置门属性...") + print(f"找到主门对象: {{main_door_obj.name}}") + + # 先设置基本属性 + door_props.frame_width = 1.0 # 门框宽度 + door_props.frame_height = 2.1 # 门框高度 + door_props.frame_thick = 0.08 # 门框厚度 + door_props.openside = '1' # 右侧开门 + + # 设置门模型和把手(这些会触发update_object回调) + print("设置门模型为2...") + door_props.model = '2' # 门模型2 + + print("设置门把手为1...") + door_props.handle = '1' # 门把手1 + + print(f"门属性设置完成:") + + # 等待更新完成 + bpy.context.view_layer.update() + + # 验证设置是否生效 + print("验证门属性设置:") + print(f" - 当前门模型: {{door_props.model}}") + print(f" - 当前门把手: {{door_props.handle}}") + + # 强制触发更新(如果需要) + try: + # 重新设置属性以触发更新 + current_model = door_props.model + current_handle = door_props.handle + + door_props.model = '1' # 临时设置为其他值 + bpy.context.view_layer.update() + + door_props.model = current_model # 设置回目标值 + bpy.context.view_layer.update() + + print("已强制触发门模型更新") + + except Exception as update_error: + print(f"强制更新时出现错误: {{update_error}}") + + # 在修改门全部属性后进行位移和旋转 + print("开始移动和旋转门组...") + try: + door_group1 = None + for obj in bpy.data.objects: + if obj.name == "Door_Group": + door_group1 = obj + break + + # 计算门的位置:x=0, y=房间长度-门宽度/2-0.8, z=0 + room_length = {adjusted_length} # 房间长度(Y轴) + door_width = {door_width} # 门宽度1米 + wall_distance = {wall_distance} # 距后墙80cm + + door_x = 0.0 + door_y = room_length - (door_width / 2) - wall_distance + door_z = 0.0 + + # 设置位置 + door_group1.location = (door_x, door_y, door_z) + print(f"已将门组移动到位置: x={{door_x}}, y={{door_y}}, z={{door_z}}") + print(f"房间长度: {{room_length}}m, 门宽度: {{door_width}}m, 距后墙: {{wall_distance}}m") + + # 设置旋转 + door_group1.rotation_euler = (math.radians(0), math.radians(0), math.radians(90)) # 旋转(0, 0, 90度) + print(f"已将门组旋转到: (0°, 0°, 90°)") + + # 强制更新 + bpy.context.view_layer.update() + print("门组位移和旋转完成") + + except Exception as move_error: + print(f"移动和旋转门组时出现错误: {{move_error}}") + + else: + print("警告: 未找到主门对象或DoorObjectGenerator属性") + # 尝试查找其他可能的门属性 + for obj in bpy.data.objects: + if hasattr(obj, 'archimesh_door'): + print(f"找到archimesh_door属性在对象: {{obj.name}}") + + # 打印门组的所有子对象信息 + print("门组包含的对象:") + for obj in bpy.data.objects: + if obj.parent == door_group: + print(f" - {{obj.name}} (位置: {{obj.location}})") + + return True + else: + print("警告: 未找到Door_Group对象") + # 尝试查找其他可能的门对象 + door_objects = [obj for obj in bpy.data.objects if "Door" in obj.name] + if door_objects: + print("找到的门相关对象:") + for obj in door_objects: + print(f" - {{obj.name}} (类型: {{obj.type}})") + + # 尝试移动第一个找到的门对象 + first_door = door_objects[0] + # 计算门的位置 + room_length = {adjusted_length} + door_width = {door_width} + wall_distance = {wall_distance} + + door_y = room_length - (door_width / 2) - wall_distance + first_door.location = (0.0, door_y, 0.0) + print(f"已移动 {{first_door.name}} 到位置: (0.0, {{door_y}}, 0.0)") + return True + else: + print("未找到任何门相关对象") + return False + else: + print("门创建失败") + return False + + except Exception as e: + print(f"创建门时出现错误: {{str(e)}}") + import traceback + traceback.print_exc() + return False + +def create_window_with_archimesh(): + """使用archimesh插件创建落地窗""" + try: + print("开始创建archimesh落地窗...") + + # 检查archimesh插件是否可用 + if not hasattr(bpy.ops.mesh, 'archimesh_winpanel'): + print("错误: archimesh插件未正确加载") + print("请确保archimesh插件已安装并启用") + return False + + # 设置3D游标到原点 + bpy.context.scene.cursor.location = (0.0, 0.0, 0.0) + + # 找到3D视图区域 + view3d_area = None + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + view3d_area = area + break + + if view3d_area is None: + print("错误: 找不到3D视图区域") + return False + + # 临时切换到3D视图上下文 + with bpy.context.temp_override(area=view3d_area): + # 创建窗户 + result = bpy.ops.mesh.archimesh_winpanel() + + if result == {{'FINISHED'}}: + print("archimesh落地窗创建完成!") + + # 等待一帧以确保对象创建完成 + bpy.context.view_layer.update() + + # 找到刚创建的窗户组对象(Window_Group) + window_group = None + for obj in bpy.data.objects: + if obj.name == "Window_Group": + window_group = obj + break + + if window_group: + print(f"找到窗户组: {{window_group.name}}") + + # 查找主窗户对象(Window) + main_window_obj = None + for obj in bpy.data.objects: + if obj.name == "Window": + main_window_obj = obj + break + + if main_window_obj and hasattr(main_window_obj, 'WindowPanelGenerator'): + # 获取窗户属性 + window_props = main_window_obj.WindowPanelGenerator[0] + + print("设置窗户属性...") + print(f"找到主窗户对象: {{main_window_obj.name}}") + + # 设置窗户的基本属性 + window_props.gen = 4 # 4扇窗户(水平) + window_props.yuk = 1 # 1行(垂直) + window_props.kl1 = 5 # 外框厚度5cm + window_props.kl2 = 5 # 竖框厚度5cm + window_props.fk = 2 # 内框厚度2cm + + # 设置窗户尺寸 + window_props.gnx0 = 80 # 第1扇窗户宽度80cm + window_props.gnx1 = 80 # 第2扇窗户宽度80cm + window_props.gnx2 = 80 # 第3扇窗户宽度80cm + window_props.gnx3 = 80 # 第4扇窗户宽度80cm + + # 计算窗户高度:房间高度-40cm + room_height = {height} # 房间高度 + window_height_cm = int((room_height - 0.4) * 100) # 转换为厘米并转为整数 + window_props.gny0 = window_height_cm # 窗户高度(房间高度-40cm) + + print(f"窗户高度设置: 房间高度{{room_height}}m - 40cm = {{window_height_cm}}cm") + + # 设置窗户类型为平窗 + window_props.UST = '1' # 平窗 + window_props.r = 0 # 旋转角度0度 + + # 设置窗台 + window_props.mr = False # 启用窗台 + + # 设置材质 + window_props.mt1 = '1' # 外框材质PVC + window_props.mt2 = '1' # 内框材质塑料 + + # 设置窗户开启状态 + window_props.k00 = True # 第1扇窗户开启 + window_props.k01 = True # 第2扇窗户开启 + window_props.k02 = True # 第3扇窗户开启 + window_props.k03 = True # 第4扇窗户开启(如果有的话) + + print(f"窗户属性设置完成:") + print(f" - 水平扇数: {{window_props.gen}}") + print(f" - 窗户高度: {{window_height_cm}}cm (房间高度{{room_height}}m - 40cm)") + + # 修复窗户玻璃材质 + print("修复窗户玻璃材质...") + fix_window_glass_material() + + # 等待更新完成 + bpy.context.view_layer.update() + + # 强制触发更新(如果需要) + try: + # 重新设置属性以触发更新 + current_gen = window_props.gen + current_gny0 = window_props.gny0 + + window_props.gen = 2 # 临时设置为其他值 + bpy.context.view_layer.update() + + window_props.gen = current_gen # 设置回目标值 + bpy.context.view_layer.update() + + print("已强制触发窗户更新") + + except Exception as update_error: + print(f"强制更新时出现错误: {{update_error}}") + + # 在修改落地窗全部属性后进行位移和旋转 + print("开始移动和旋转窗户组...") + try: + window_group1 = None + + for obj in bpy.data.objects: + if obj.name == "Window_Group": + window_group1 = obj + break + + # 计算窗户的位置:x=0, y=房间长度-窗宽度/2-0.8, z=0 + room_length = {adjusted_length} # 房间长度(Y轴) + window_width = {window_width} # 窗宽度3.45米 + wall_distance = {wall_distance} # 距后墙80cm + + window_x = 0.0 + window_y = room_length - (window_width / 2) - wall_distance + window_z = 0.0 + + # 设置位置 + window_group1.location = (window_x, window_y, window_z) + print(f"已将窗户组移动到位置: x={{window_x}}, y={{window_y}}, z={{window_z}}") + print(f"房间长度: {{room_length}}m, 窗宽度: {{window_width}}m, 距后墙: {{wall_distance}}m") + + # 设置旋转 + window_group1.rotation_euler = (math.radians(0), math.radians(0), math.radians(90)) # 旋转(0, 0, 90度) + print(f"已将窗户组旋转到: (0°, 0°, 90°)") + + # 强制更新 + bpy.context.view_layer.update() + print("窗户组位移和旋转完成") + + except Exception as move_error: + print(f"移动和旋转窗户组时出现错误: {{move_error}}") + + else: + print("警告: 未找到主窗户对象或WindowPanelGenerator属性") + # 尝试查找其他可能的窗户属性 + for obj in bpy.data.objects: + if hasattr(obj, 'archimesh_window'): + print(f"找到archimesh_window属性在对象: {{obj.name}}") + + # 打印窗户组的所有子对象信息 + print("窗户组包含的对象:") + for obj in bpy.data.objects: + if obj.parent == window_group: + print(f" - {{obj.name}} (位置: {{obj.location}})") + + return True + else: + print("警告: 未找到Window_Group对象") + # 尝试查找其他可能的窗户对象 + window_objects = [obj for obj in bpy.data.objects if "Window" in obj.name] + if window_objects: + print("找到的窗户相关对象:") + for obj in window_objects: + print(f" - {{obj.name}} (类型: {{obj.type}})") + + # 尝试移动第一个找到的窗户对象 + first_window = window_objects[0] + # 计算窗户的位置 + room_length = {adjusted_length} + window_width = {window_width} + wall_distance = {wall_distance} + + window_y = room_length - (window_width / 2) - wall_distance + first_window.location = (0.0, window_y, 0.0) + print(f"已移动 {{first_window.name}} 到位置: (0.0, {{window_y}}, 0.0)") + return True + else: + print("未找到任何窗户相关对象") + return False + + else: + print("窗户创建失败") + return False + + except Exception as e: + print(f"创建窗户时出现错误: {{str(e)}}") + import traceback + traceback.print_exc() + return False + +def fix_window_glass_material(): + """修复窗户玻璃材质,将其改为高透BSDF""" + try: + print("开始修复窗户玻璃材质...") + + # 查找Window_Group下的Window对象 + window_group = None + for obj in bpy.data.objects: + if obj.name == "Window_Group": + window_group = obj + break + + if not window_group: + print("未找到Window_Group对象") + return False + + # 查找Window对象(Window_Group的子对象) + window_obj = None + for obj in bpy.data.objects: + if obj.name == "Window" and obj.parent == window_group: + window_obj = obj + break + + + if not window_obj: + print("未找到Window对象") + return False + + print(f"找到Window对象: {{window_obj.name}}") + + # 检查Window对象的材质 + if not window_obj.material_slots: + print("Window对象没有材质槽") + return False + + # 遍历所有材质槽 + for slot in window_obj.material_slots: + if slot.material and "Glass" in slot.material.name: + print(f"找到Glass材质: {{slot.material.name}}") + + # 启用节点编辑 + slot.material.use_nodes = True + + # 清除所有现有节点 + slot.material.node_tree.nodes.clear() + + # 创建半透BSDF节点 + translucent_bsdf = slot.material.node_tree.nodes.new(type='ShaderNodeBsdfTranslucent') + translucent_bsdf.location = (0, 0) + + # 设置半透BSDF参数 + translucent_bsdf.inputs['Color'].default_value = (0.95, 0.98, 1.0, 1.0) # 几乎无色 + # 半透BSDF节点没有Weight参数,只有Color参数 + + # 创建材质输出节点 + material_output = slot.material.node_tree.nodes.new(type='ShaderNodeOutputMaterial') + material_output.location = (300, 0) + + # 连接节点 + slot.material.node_tree.links.new( + translucent_bsdf.outputs['BSDF'], + material_output.inputs['Surface'] + ) + + # 设置材质混合模式为Alpha Blend + slot.material.blend_method = 'BLEND' + + print(f"已成功修改Glass材质为半透BSDF") + print(f" - 半透颜色: 几乎无色") + print(f" - 混合模式: Alpha Blend") + + return True + + print("未找到Glass材质") + return False + + except Exception as e: + print(f"修复玻璃材质时出现错误: {{str(e)}}") + import traceback + traceback.print_exc() + return False + +def setup_render_settings(): + """设置渲染参数(固定EEVEE渲染,优化速度)""" + scene = bpy.context.scene + + # 设置渲染引擎固定为EEVEE + scene.render.engine = 'BLENDER_EEVEE_NEXT' + print("已设置渲染引擎为: BLENDER_EEVEE_NEXT") + + # 启用Freestyle线条渲染 + scene.render.use_freestyle = True + print("已启用Freestyle线条渲染") + + # 配置Freestyle线条设置 + if hasattr(scene, 'view_layers'): + view_layer = scene.view_layers[0] # 获取第一个视图层 + view_layer.use_freestyle = True + + # 获取Freestyle线条设置 + freestyle = view_layer.freestyle_settings + + # 启用线条渲染 + freestyle.use_smoothness = True + freestyle.use_culling = True + + # 设置线条宽度 - 使用正确的API 没有生效 + + + # 设置世界环境 + if hasattr(scene, 'world') and scene.world: + # 启用世界节点 + scene.world.use_nodes = True + + # 清除现有节点 + scene.world.node_tree.nodes.clear() + + # 创建自发光节点 + emission_node = scene.world.node_tree.nodes.new(type='ShaderNodeEmission') + emission_node.location = (0, 0) + + # 设置HSV颜色:色相0,饱和度0,明度0.051,Alpha 1 + # 转换为RGB:HSV(0, 0, 0.051) = RGB(0.051, 0.051, 0.051) + emission_node.inputs['Color'].default_value = (0.051, 0.051, 0.051, 1.0) # 深灰色 + emission_node.inputs['Strength'].default_value = 8.0 # 强度8 + + # 创建世界输出节点 + world_output = scene.world.node_tree.nodes.new(type='ShaderNodeOutputWorld') + world_output.location = (300, 0) + + # 连接节点 + scene.world.node_tree.links.new( + emission_node.outputs['Emission'], + world_output.inputs['Surface'] + ) + + print("已设置世界环境为自发光") + print(f" - 颜色: HSV(0, 0, 0.051) = RGB(0.051, 0.051, 0.051)") + print(f" - 强度: 8.0") + + # 设置EEVEE采样(降低采样数提高速度) + try: + if hasattr(scene.eevee, 'taa_render_samples'): + scene.eevee.taa_render_samples = 32 # 从64降低到32 + print(f"已设置EEVEE渲染采样: {{scene.eevee.taa_render_samples}}") + elif hasattr(scene.eevee, 'taa_samples'): + scene.eevee.taa_samples = 32 # 从64降低到32 + print(f"已设置EEVEE采样: {{scene.eevee.taa_samples}}") + else: + print("警告: 无法找到EEVEE采样设置,使用默认值") + + # 启用屏幕空间反射(简化设置) + if hasattr(scene.eevee, 'use_ssr'): + scene.eevee.use_ssr = True + print("已启用屏幕空间反射") + + # 启用环境光遮蔽(简化设置) + if hasattr(scene.eevee, 'use_gtao'): + scene.eevee.use_gtao = True + print("已启用环境光遮蔽") + + # 启用透明渲染(必要) + if hasattr(scene.eevee, 'use_transparent'): + scene.eevee.use_transparent = True + print("已启用透明渲染") + + # 设置透明混合模式(必要) + if hasattr(scene.render, 'film_transparent'): + scene.render.film_transparent = True + print("已启用透明背景") + + except AttributeError as e: + print(f"EEVEE设置警告: {{e}}") + print("使用默认EEVEE渲染设置") + + # 设置分辨率 + scene.render.resolution_x = {request.render.resolution_x} + scene.render.resolution_y = {request.render.resolution_y} + scene.render.resolution_percentage = 100 + + # 设置输出格式 + scene.render.image_settings.file_format = 'PNG' + scene.render.image_settings.color_mode = 'RGBA' # 支持透明 + + # 设置输出路径(使用绝对路径) + scene.render.filepath = OUTPUT_FILE + + print(f"渲染设置完成,输出路径: {{scene.render.filepath}}") + print(f"分辨率: {{scene.render.resolution_x}}x{{scene.render.resolution_y}}") + return scene.render.filepath + +def setup_camera_and_lighting(): + """设置摄像机和照明(160W点光源 + 150W日光)""" + # 计算灯光高度(房间高度减一米) + room_height = {height} + light_height = room_height - 1.0 + + # 设置摄像机 + camera = None + if "Camera" in bpy.data.objects: + camera = bpy.data.objects["Camera"] + elif "Isometric Camera" in bpy.data.objects: + camera = bpy.data.objects["Isometric Camera"] + else: + # 创建新摄像机 + bpy.ops.object.camera_add(location={camera_pos}) + camera = bpy.context.active_object + camera.name = "Isometric Camera" + + # 设置摄像机位置和旋转 + camera.location = {camera_pos} + camera.rotation_euler = {camera_rot} + + # 设置为透视投影 + camera.data.type = 'PERSP' + camera.data.lens = 35.0 # 35mm焦距 + camera.data.sensor_width = 35.0 # 35mm传感器 + camera.data.sensor_fit = 'AUTO' # 自动适配 + + # 设置为场景的活动摄像机 + bpy.context.scene.camera = camera + print("摄像机设置完成") + print(f"相机位置: ({{camera.location.x}}, {{camera.location.y}}, {{camera.location.z}})") + + # 隐藏ISO Emission灯光(如果存在) + light_objects = ["ISO Emission Left", "ISO Emission Right"] + for obj_name in light_objects: + if obj_name in bpy.data.objects: + light_obj = bpy.data.objects[obj_name] + # 隐藏发光对象 + light_obj.hide_render = True + light_obj.hide_viewport = True + print(f"已隐藏 {{obj_name}}") + + # # 创建第一个160W点光源(房间中心) + # try: + # # 计算点光源位置(房间中心上方) + # room_length = {adjusted_length} # Y轴 + # room_width = {adjusted_width} # X轴 + + # # 第一个点光源位置:房间中心上方 + # light_x = 0 # X轴中心 + # light_y = 0 # Y轴中心 + # light_z = light_height + + # # 创建第一个点光源 + # bpy.ops.object.light_add( + # type='POINT', location=(light_x, light_y, light_z)) + # point_light1 = bpy.context.active_object + # point_light1.name = "Main Point Light" + # point_light1.data.energy = 160 # 160W + + # # 设置软衰减(简化设置) + # point_light1.data.shadow_soft_size = 0.5 # 0.5米半径,产生柔和阴影 + # point_light1.data.use_shadow = True # 启用阴影 + + # print(f"已创建第一个点光源(160W)") + # print(f"点光源位置: x={{light_x}}, y={{light_y}}, z={{light_z}}") + + # except Exception as e: + # print(f"创建第一个点光源时出错: {{e}}") + + + + print("照明设置完成") + #print("灯光类型: 点光源 + 日光") + print(f"阴影设置: 启用,柔和阴影") + #print(f"主灯光高度: {{light_height}}米 (房间高度减一米)") + +def render_scene(): + """渲染场景""" + print("开始渲染...") + print(f"输出路径: {{OUTPUT_FILE}}") + + # 确保输出目录存在 + output_dir = os.path.dirname(OUTPUT_FILE) + if not os.path.exists(output_dir): + os.makedirs(output_dir, exist_ok=True) + print(f"创建输出目录: {{output_dir}}") + + bpy.ops.render.render(write_still=True) + print(f"渲染完成!") + + # 验证文件是否创建 + if os.path.exists(OUTPUT_FILE): + file_size = os.path.getsize(OUTPUT_FILE) + print(f"输出文件已创建: {{OUTPUT_FILE}} (大小: {{file_size}} bytes)") + else: + print(f"警告: 输出文件未找到: {{OUTPUT_FILE}}") + # 列出输出目录中的文件 + if os.path.exists(output_dir): + files = os.listdir(output_dir) + print(f"输出目录中的文件: {{files}}") + + return OUTPUT_FILE + +def main(): + """主函数""" + print("=" * 60) + print("开始API渲染任务") + print("=" * 60) + + try: + # 1. 创建房间 + print("1. 创建等轴测房间...") + if not create_isometric_room(): + print("房间创建失败,停止执行") + return False + + # 2. 根据道具类型创建道具 + prop_type = PROP_TYPE + if prop_type == 1: # 窗户 + print("2. 创建archimesh落地窗...") + if not create_window_with_archimesh(): + print("落地窗创建失败,但继续执行") + elif prop_type == 2: # 拱门 + print("2. 拱门已通过isometric_room_gen插件创建") + elif prop_type == 3: # 门 + print("2. 创建archimesh门...") + if not create_door_with_archimesh(): + print("门创建失败,但继续执行") + else: + print("2. 无道具设置") + + # 3. 设置渲染参数 + print("3. 设置渲染参数...") + output_path = setup_render_settings() + + # 4. 设置摄像机和照明 + print("4. 设置摄像机和照明...") + setup_camera_and_lighting() + + # 5. 渲染场景 + print("5. 开始渲染...") + final_path = render_scene() + + print("=" * 60) + print("API渲染任务完成!") + print(f"输出文件: {{final_path}}") + print("=" * 60) + + return True + + except Exception as e: + print(f"执行过程中出现错误: {{str(e)}}") + import traceback + traceback.print_exc() + return False + +# 执行主函数 +if __name__ == "__main__": + main() +''' + + return script.strip(), output_file + + async def render_room(self, task_id: str, request: RenderRequest) -> str: + """ + 执行房间渲染 + + Args: + task_id: 任务ID + request: 渲染请求参数 + + Returns: + 输出文件路径 + """ + try: + # 确保输出目录存在 + self._ensure_output_dir() + + # 生成Blender脚本 + script_content, output_file = self._generate_blender_script( + task_id, request) + + # 创建临时脚本文件 + with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f: + f.write(script_content) + script_path = f.name + + try: + # 构建Blender命令 + cmd = [ + self.blender_path, + "--background", + "--disable-crash-handler", + "--python", script_path + ] + + logger.info(f"执行Blender命令: {' '.join(cmd)}") + logger.info(f"输出文件路径: {output_file}") + + # 执行Blender渲染 + process = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + + stdout, stderr = await asyncio.wait_for( + process.communicate(), + timeout=300 # 5分钟超时 + ) + + # 记录Blender输出 + if stdout: + logger.info(f"Blender stdout: {stdout.decode('utf-8')}") + if stderr: + logger.warning(f"Blender stderr: {stderr.decode('utf-8')}") + + # 检查执行结果 + if process.returncode == 0: + # 检查输出文件是否存在 + if os.path.exists(output_file): + file_size = os.path.getsize(output_file) + logger.info( + f"渲染成功: {output_file} (大小: {file_size} bytes)") + return output_file + else: + # 检查输出目录中的文件 + output_dir = os.path.dirname(output_file) + if os.path.exists(output_dir): + files = os.listdir(output_dir) + logger.error(f"输出文件不存在,但目录中有文件: {files}") + else: + logger.error(f"输出目录不存在: {output_dir}") + + # 尝试查找可能的输出文件 + possible_files = [] + for root, dirs, files in os.walk(output_dir): + for file in files: + if file.endswith('.png') and 'render' in file: + possible_files.append( + os.path.join(root, file)) + + if possible_files: + logger.info(f"找到可能的输出文件: {possible_files}") + return possible_files[0] + else: + raise Exception(f"渲染完成但输出文件不存在: {output_file}") + else: + error_msg = stderr.decode('utf-8') if stderr else "未知错误" + raise Exception( + f"Blender执行失败 (返回码: {process.returncode}): {error_msg}") + + finally: + # 清理临时脚本文件 + try: + os.unlink(script_path) + except: + pass + + except asyncio.TimeoutError: + raise Exception("渲染超时(5分钟)") + except Exception as e: + logger.error(f"渲染失败: {e}") + raise diff --git a/Isometquick-server/blender_service0716).py b/Isometquick-server/blender_service0716).py new file mode 100644 index 0000000..82fdeef --- /dev/null +++ b/Isometquick-server/blender_service0716).py @@ -0,0 +1,592 @@ +#!/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 diff --git a/Isometquick-server/config.py b/Isometquick-server/config.py new file mode 100644 index 0000000..df1fb16 --- /dev/null +++ b/Isometquick-server/config.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +配置文件 +""" + +import os +from typing import Optional + + +class Settings: + """应用设置""" + + # 服务配置 + HOST: str = "0.0.0.0" + PORT: int = 8003 + DEBUG: bool = False + + # Blender配置 + BLENDER_PATH: Optional[str] = os.getenv("BLENDER_PATH") + BLENDER_TIMEOUT: int = 300 # 5分钟 + + # 文件存储配置 + RENDER_OUTPUT_DIR: str = "/data/Isometquick/" + MAX_FILE_AGE_HOURS: int = 24 # 24小时后清理文件 + + # 日志配置 + LOG_LEVEL: str = "INFO" + LOG_FORMAT: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + + # 限制配置 + MAX_CONCURRENT_RENDERS: int = 3 + MAX_QUEUE_SIZE: int = 10 + + +settings = Settings() diff --git a/Isometquick-server/irg.sh b/Isometquick-server/irg.sh new file mode 100644 index 0000000..d5b089e --- /dev/null +++ b/Isometquick-server/irg.sh @@ -0,0 +1,363 @@ +#!/bin/bash +# irg.sh - Isometric Room Generator 一键服务管理脚本 + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# 打印函数 +info() { echo -e "${BLUE}[INFO]${NC} $1"; } +success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# 脚本目录 +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +cd "$SCRIPT_DIR" + +# 配置变量 +PID_FILE="$SCRIPT_DIR/irg.pid" +LOG_DIR="/data/Isometquick/logs" +LOG_FILE="$LOG_DIR/isometquick.log" +SERVICE_PORT=8003 + +# 创建日志目录 +create_log_dir() { + if [ ! -d "$LOG_DIR" ]; then + if sudo mkdir -p "$LOG_DIR" 2>/dev/null && sudo chown $USER:$USER "$LOG_DIR"; then + info "日志目录创建成功: $LOG_DIR" + else + # 如果无法创建/data目录,使用本地目录 + LOG_DIR="$SCRIPT_DIR/logs" + LOG_FILE="$LOG_DIR/isometquick.log" + mkdir -p "$LOG_DIR" + info "使用本地日志目录: $LOG_DIR" + fi + fi +} + +# 检查服务是否运行 +is_running() { + if [ -f "$PID_FILE" ]; then + PID=$(cat "$PID_FILE") + if ps -p "$PID" > /dev/null 2>&1; then + return 0 + else + rm -f "$PID_FILE" + fi + fi + return 1 +} + +# 清理占用端口的进程 +cleanup_port() { + info "🧹 清理端口 $SERVICE_PORT..." + + # 查找占用端口的进程 + PIDS=$(sudo lsof -t -i:$SERVICE_PORT 2>/dev/null || true) + + if [ -n "$PIDS" ]; then + info "发现占用端口的进程: $PIDS" + for PID in $PIDS; do + info "杀死进程: $PID" + sudo kill -9 "$PID" 2>/dev/null || true + done + sleep 1 + fi + + # 再次检查 + if sudo lsof -i:$SERVICE_PORT >/dev/null 2>&1; then + warning "端口 $SERVICE_PORT 仍被占用" + return 1 + else + success "端口 $SERVICE_PORT 已清理" + return 0 + fi +} + +# 安装依赖 +install() { + info "🔧 安装IRG服务依赖..." + + # 检查Python + if ! command -v python3 &> /dev/null; then + error "未找到Python3,请先安装Python 3.8+" + exit 1 + fi + + # 创建虚拟环境 + if [ ! -d "venv" ]; then + info "创建虚拟环境..." + python3 -m venv venv + fi + + # 激活虚拟环境并安装依赖 + . venv/bin/activate + pip install --upgrade pip -q + pip install -r requirements.txt -q + + # 创建输出目录 + if [ ! -d "/data/Isometquick" ]; then + if sudo mkdir -p /data/Isometquick 2>/dev/null && sudo chown $USER:$USER /data/Isometquick; then + success "输出目录创建成功: /data/Isometquick" + else + mkdir -p ./renders + success "输出目录: ./renders" + fi + fi + + # 创建日志目录 + create_log_dir + + success "IRG服务依赖安装完成" +} + +# 启动服务 +start() { + if is_running; then + PID=$(cat "$PID_FILE") + warning "IRG服务已在运行 (PID: $PID)" + return 0 + fi + + info "🚀 启动IRG服务..." + + # 创建日志目录 + create_log_dir + + # 清理占用的端口 + cleanup_port || { + error "无法清理端口 $SERVICE_PORT,请手动检查" + exit 1 + } + + # 确保虚拟环境存在 + if [ ! -d "venv" ]; then + install + fi + + # 启动服务 + nohup bash -c " + cd '$SCRIPT_DIR' + . venv/bin/activate + python start.py + " > "$LOG_FILE" 2>&1 & + + echo $! > "$PID_FILE" + sleep 2 + + if is_running; then + PID=$(cat "$PID_FILE") + success "IRG服务启动成功 (PID: $PID)" + success "API地址: http://localhost:$SERVICE_PORT" + success "API文档: http://localhost:$SERVICE_PORT/docs" + success "日志文件: $LOG_FILE" + else + error "IRG服务启动失败" + if [ -f "$LOG_FILE" ]; then + echo "错误日志:" + tail -10 "$LOG_FILE" + fi + exit 1 + fi +} + +# 停止服务 +stop() { + if ! is_running; then + warning "IRG服务未运行" + return 0 + fi + + PID=$(cat "$PID_FILE") + info "🛑 停止IRG服务 (PID: $PID)..." + + # 优雅停止 + kill -TERM "$PID" 2>/dev/null || true + + # 等待5秒 + for i in {1..5}; do + if ! ps -p "$PID" > /dev/null 2>&1; then + break + fi + sleep 1 + done + + # 强制停止 + if ps -p "$PID" > /dev/null 2>&1; then + kill -KILL "$PID" 2>/dev/null || true + fi + + rm -f "$PID_FILE" + + # 清理可能残留的端口占用 + cleanup_port + + success "IRG服务已停止" +} + +# 重启服务 +restart() { + info "🔄 重启IRG服务..." + stop + sleep 1 + start +} + +# 查看状态 +status() { + echo "========================================" + echo "📊 Isometric Room Generator 服务状态" + echo "========================================" + + if is_running; then + PID=$(cat "$PID_FILE") + MEMORY=$(ps -p "$PID" -o rss= 2>/dev/null | awk '{print int($1/1024)"MB"}' || echo "N/A") + CPU=$(ps -p "$PID" -o pcpu= 2>/dev/null | awk '{print $1"%"}' || echo "N/A") + + success "IRG服务正在运行" + echo " PID: $PID" + echo " 内存: $MEMORY" + echo " CPU: $CPU" + echo " 端口: $SERVICE_PORT" + echo " 日志: $LOG_FILE" + + # 检查端口 + if netstat -tlnp 2>/dev/null | grep -q ":$SERVICE_PORT"; then + success "端口 $SERVICE_PORT 正在监听" + else + warning "端口 $SERVICE_PORT 未监听" + fi + + # 健康检查 + if command -v curl &> /dev/null; then + if curl -s "http://localhost:$SERVICE_PORT/health" > /dev/null 2>&1; then + success "API健康检查通过" + else + warning "API健康检查失败" + fi + fi + else + warning "IRG服务未运行" + fi +} + +# 查看日志 +logs() { + if [ -f "$LOG_FILE" ]; then + if [ "$1" = "-f" ]; then + info "实时查看IRG服务日志 (Ctrl+C退出)..." + tail -f "$LOG_FILE" + else + info "显示最近20行IRG服务日志..." + tail -20 "$LOG_FILE" + echo "" + info "实时查看: $0 logs -f" + fi + else + warning "日志文件不存在: $LOG_FILE" + fi +} + +# 日志管理 +log_rotate() { + info "🔄 IRG服务日志轮转..." + + if [ -f "$LOG_FILE" ]; then + # 获取当前日期 + DATE=$(date +%Y%m%d_%H%M%S) + BACKUP_FILE="$LOG_DIR/isometquick_$DATE.log" + + # 备份当前日志 + mv "$LOG_FILE" "$BACKUP_FILE" + info "日志已备份到: $BACKUP_FILE" + + # 清理7天前的日志 + find "$LOG_DIR" -name "isometquick_*.log" -mtime +7 -delete 2>/dev/null || true + success "IRG服务日志轮转完成" + else + warning "没有找到日志文件" + fi +} + +# 显示帮助 +help() { + echo "========================================" + echo "🏠 Isometric Room Generator 服务管理脚本" + echo "========================================" + echo "" + echo "用法: $0 [命令]" + echo "" + echo "命令:" + echo " install 安装依赖" + echo " start 启动IRG服务" + echo " stop 停止IRG服务" + echo " restart 重启IRG服务" + echo " status 查看服务状态" + echo " logs 查看日志" + echo " logs -f 实时日志" + echo " rotate 日志轮转" + echo "" + echo "示例:" + echo " $0 start # 启动IRG服务" + echo " $0 restart # 重启IRG服务" + echo " $0 status # 查看服务状态" + echo " $0 logs # 查看日志" + echo " $0 rotate # 日志轮转" + echo "" + echo "首次使用请运行: $0 install" + echo "" +} + +# 主函数 +main() { + case "${1:-}" in + "install") + install + ;; + "start") + start + ;; + "stop") + stop + ;; + "restart") + restart + ;; + "status") + status + ;; + "logs") + logs "$2" + ;; + "rotate") + log_rotate + ;; + "help"|"-h"|"--help") + help + ;; + "") + # 默认:如果没有参数,显示状态或首次安装启动 + if [ -d "venv" ]; then + status + else + info "首次使用,开始安装IRG服务..." + install + start + fi + ;; + *) + error "未知命令: $1" + help + exit 1 + ;; + esac +} + +# 执行主函数 +main "$@" \ No newline at end of file diff --git a/Isometquick-server/main.py b/Isometquick-server/main.py new file mode 100644 index 0000000..7766423 --- /dev/null +++ b/Isometquick-server/main.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Isometquick Blender渲染微服务 +基于FastAPI的Blender等轴测房间渲染API服务 +""" + +from fastapi import FastAPI, HTTPException, BackgroundTasks +from fastapi.responses import JSONResponse, FileResponse +from pydantic import BaseModel, Field +from typing import Optional, Literal +import os +import asyncio +import uuid +from datetime import datetime +import logging + +from blender_service import BlenderRenderService +from models import RenderRequest, RenderResponse, RenderStatus + +# 配置日志 +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# 创建FastAPI应用 +app = FastAPI( + title="Isometquick Blender渲染服务", + description="基于Blender的等轴测房间渲染API微服务", + version="1.0.0", + docs_url="/docs", + redoc_url="/redoc" +) + +# 创建Blender服务实例 +blender_service = BlenderRenderService() + +# 渲染任务状态存储(生产环境应使用Redis等) +render_tasks = {} + + +@app.get("/") +async def root(): + """健康检查接口""" + return { + "service": "Isometquick Blender渲染服务", + "status": "running", + "version": "1.0.0", + "timestamp": datetime.now().isoformat() + } + + +@app.get("/health") +async def health_check(): + """服务健康检查""" + try: + # 检查Blender是否可用 + blender_available = blender_service.check_blender_available() + return { + "status": "healthy" if blender_available else "unhealthy", + "blender_available": blender_available, + "timestamp": datetime.now().isoformat() + } + except Exception as e: + logger.error(f"健康检查失败: {e}") + return JSONResponse( + status_code=500, + content={"status": "unhealthy", "error": str(e)} + ) + + +@app.post("/render", response_model=RenderResponse) +async def create_render_task( + request: RenderRequest, + background_tasks: BackgroundTasks +): + """ + 创建渲染任务 + """ + try: + # 生成任务ID + task_id = str(uuid.uuid4()) + + # 记录任务状态 + render_tasks[task_id] = { + "status": "pending", + "created_at": datetime.now().isoformat(), + "request": request.dict(), + "output_file": None, + "error": None + } + + # 添加后台渲染任务 + background_tasks.add_task( + execute_render_task, + task_id, + request + ) + + logger.info(f"创建渲染任务: {task_id}") + + return RenderResponse( + task_id=task_id, + status="pending", + message="渲染任务已创建,正在处理中" + ) + + except Exception as e: + logger.error(f"创建渲染任务失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/render/{task_id}", response_model=RenderStatus) +async def get_render_status(task_id: str): + """ + 获取渲染任务状态 + """ + if task_id not in render_tasks: + raise HTTPException(status_code=404, detail="任务不存在") + + task_info = render_tasks[task_id] + + return RenderStatus( + task_id=task_id, + status=task_info["status"], + created_at=task_info["created_at"], + output_file=task_info.get("output_file"), + error=task_info.get("error") + ) + + +@app.get("/render/{task_id}/download") +async def download_render_result(task_id: str): + """ + 下载渲染结果 + """ + if task_id not in render_tasks: + raise HTTPException(status_code=404, detail="任务不存在") + + task_info = render_tasks[task_id] + + if task_info["status"] != "completed": + raise HTTPException( + status_code=400, + detail=f"任务状态: {task_info['status']}, 无法下载" + ) + + output_file = task_info.get("output_file") + if not output_file or not os.path.exists(output_file): + raise HTTPException(status_code=404, detail="渲染文件不存在") + + return FileResponse( + path=output_file, + filename=f"render_{task_id}.png", + media_type="image/png" + ) + + +@app.delete("/render/{task_id}") +async def delete_render_task(task_id: str): + """ + 删除渲染任务和相关文件 + """ + if task_id not in render_tasks: + raise HTTPException(status_code=404, detail="任务不存在") + + task_info = render_tasks[task_id] + + # 删除输出文件 + output_file = task_info.get("output_file") + if output_file and os.path.exists(output_file): + try: + os.remove(output_file) + logger.info(f"删除文件: {output_file}") + except Exception as e: + logger.warning(f"删除文件失败: {e}") + + # 删除任务记录 + del render_tasks[task_id] + + return {"message": f"任务 {task_id} 已删除"} + + +@app.get("/tasks") +async def list_render_tasks(): + """ + 列出所有渲染任务 + """ + return { + "total": len(render_tasks), + "tasks": [ + { + "task_id": task_id, + "status": task_info["status"], + "created_at": task_info["created_at"] + } + for task_id, task_info in render_tasks.items() + ] + } + + +async def execute_render_task(task_id: str, request: RenderRequest): + """ + 执行渲染任务(后台任务) + """ + try: + # 更新任务状态 + render_tasks[task_id]["status"] = "processing" + logger.info(f"开始处理渲染任务: {task_id}") + + # 调用Blender服务执行渲染 + output_file = await blender_service.render_room(task_id, request) + + # 更新任务状态 + render_tasks[task_id].update({ + "status": "completed", + "output_file": output_file, + "completed_at": datetime.now().isoformat() + }) + + logger.info(f"渲染任务完成: {task_id}, 输出文件: {output_file}") + + except Exception as e: + # 更新错误状态 + render_tasks[task_id].update({ + "status": "failed", + "error": str(e), + "failed_at": datetime.now().isoformat() + }) + + logger.error(f"渲染任务失败: {task_id}, 错误: {e}") + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8003) diff --git a/Isometquick-server/models.py b/Isometquick-server/models.py new file mode 100644 index 0000000..4d45490 --- /dev/null +++ b/Isometquick-server/models.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Isometquick API数据模型 +定义请求和响应的数据结构 +""" + +from pydantic import BaseModel, Field +from typing import Optional, Literal +from datetime import datetime + + +class RoomDimensions(BaseModel): + """房间尺寸参数""" + length: float = Field(default=4.0, ge=1.0, le=20.0, description="房间长度(X轴)") + width: float = Field(default=4.0, ge=1.0, le=20.0, description="房间宽度(Y轴)") + height: float = Field(default=3.0, ge=1.0, le=10.0, description="房间高度(Z轴)") + prop_type: int = Field(default=0, ge=0, le=3, + description="道具类型: 0=无, 1=窗, 2=拱门, 3=门") + + +class CameraSettings(BaseModel): + """摄像机设置参数""" + height: float = Field(default=1.3, ge=0.5, le=5.0, + description="摄像机垂直高度(米)") + view_type: Literal[1, 2] = Field( + default=2, description="视图类型: 1=正视图, 2=侧视图") + rotation_angle: float = Field( + default=45.0, ge=0.0, le=360.0, description="摄像机旋转角度(度)") + + +class RenderSettings(BaseModel): + """渲染设置参数""" + resolution_x: int = Field(default=1080, ge=256, + le=4096, description="渲染宽度") + resolution_y: int = Field(default=2400, ge=256, + le=4096, description="渲染高度") + engine: Literal["workbench", "eevee", "cycles"] = Field( + default="workbench", description="渲染引擎") + + +class RenderRequest(BaseModel): + """渲染请求模型""" + room: RoomDimensions = Field( + default_factory=RoomDimensions, description="房间尺寸参数") + camera: CameraSettings = Field( + default_factory=CameraSettings, description="摄像机设置") + render: RenderSettings = Field( + default_factory=RenderSettings, description="渲染设置") + + class Config: + json_schema_extra = { + "example": { + "room": { + "length": 4.0, + "width": 4.0, + "height": 3.0, + "prop_type": 0 + }, + "camera": { + "height": 1.3, + "view_type": 2, + "rotation_angle": 45.0 + }, + "render": { + "resolution_x": 1080, + "resolution_y": 2400, + "engine": "workbench" + } + } + } + + +class RenderResponse(BaseModel): + """渲染响应模型""" + task_id: str = Field(description="任务唯一标识符") + status: str = Field(description="任务状态") + message: str = Field(description="响应消息") + created_at: Optional[datetime] = Field(default=None, description="创建时间") + + +class RenderStatus(BaseModel): + """渲染状态模型""" + task_id: str = Field(description="任务唯一标识符") + status: str = Field( + description="任务状态: pending/processing/completed/failed") + created_at: str = Field(description="创建时间") + completed_at: Optional[str] = Field(default=None, description="完成时间") + output_file: Optional[str] = Field(default=None, description="输出文件路径") + image_base64: Optional[str] = Field( + default=None, description="图片的base64编码") + error: Optional[str] = Field(default=None, description="错误信息") + + +class ApiResponse(BaseModel): + """通用API响应模型""" + success: bool = Field(description="操作是否成功") + message: str = Field(description="响应消息") + data: Optional[dict] = Field(default=None, description="响应数据") + timestamp: datetime = Field( + default_factory=datetime.now, description="响应时间") diff --git a/Isometquick-server/render_5a6dc259-e154-415f-99bc-faf8460035ae.png b/Isometquick-server/render_5a6dc259-e154-415f-99bc-faf8460035ae.png new file mode 100644 index 0000000..453434f Binary files /dev/null and b/Isometquick-server/render_5a6dc259-e154-415f-99bc-faf8460035ae.png differ diff --git a/Isometquick-server/render_a5b458b3-3f43-4f39-a9f3-5a39f04c22ed.png b/Isometquick-server/render_a5b458b3-3f43-4f39-a9f3-5a39f04c22ed.png new file mode 100644 index 0000000..55cbc7d Binary files /dev/null and b/Isometquick-server/render_a5b458b3-3f43-4f39-a9f3-5a39f04c22ed.png differ diff --git a/Isometquick-server/requirements.txt b/Isometquick-server/requirements.txt new file mode 100644 index 0000000..6a36eb4 --- /dev/null +++ b/Isometquick-server/requirements.txt @@ -0,0 +1,6 @@ +fastapi==0.104.1 +uvicorn==0.24.0 +pydantic==2.5.0 +python-multipart==0.0.6 +aiofiles==23.2.1 +requests==2.31.0 diff --git a/Isometquick-server/start.py b/Isometquick-server/start.py new file mode 100644 index 0000000..fab204a --- /dev/null +++ b/Isometquick-server/start.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +服务启动脚本 +""" + +import uvicorn +import logging +from config import settings + + +def setup_logging(): + """设置日志""" + logging.basicConfig( + level=getattr(logging, settings.LOG_LEVEL), + format=settings.LOG_FORMAT + ) + + +def main(): + """启动服务""" + setup_logging() + + logger = logging.getLogger(__name__) + logger.info("正在启动Isometquick Blender渲染服务...") + logger.info(f"服务地址: http://{settings.HOST}:{settings.PORT}") + logger.info(f"API文档: http://{settings.HOST}:{settings.PORT}/docs") + + uvicorn.run( + "main:app", + host=settings.HOST, + port=settings.PORT, + reload=settings.DEBUG, + log_level=settings.LOG_LEVEL.lower() + ) + + +if __name__ == "__main__": + main() diff --git a/Isometquick-server/test_client.py b/Isometquick-server/test_client.py new file mode 100644 index 0000000..ac259b2 --- /dev/null +++ b/Isometquick-server/test_client.py @@ -0,0 +1,341 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +API测试客户端 +用于测试Isometric Room Generator渲染服务 +""" + +import requests +import time +import json +import sys + + +class IRGClient: + """Isometric Room Generator API客户端""" + + def __init__(self, base_url: str = "http://localhost:8003"): + self.base_url = base_url.rstrip('/') + + def health_check(self): + """健康检查""" + try: + response = requests.get(f"{self.base_url}/health", timeout=10) + return response.json() + except Exception as e: + return {"error": str(e)} + + def create_render_task(self, request_data: dict): + """创建渲染任务""" + try: + response = requests.post( + f"{self.base_url}/render", + json=request_data, + timeout=30 + ) + response.raise_for_status() + return response.json() + except Exception as e: + return {"error": str(e)} + + def get_task_status(self, task_id: str): + """获取任务状态""" + try: + response = requests.get( + f"{self.base_url}/render/{task_id}", timeout=10) + response.raise_for_status() + return response.json() + except Exception as e: + return {"error": str(e)} + + def download_result(self, task_id: str, output_file: str): + """下载渲染结果""" + try: + response = requests.get( + f"{self.base_url}/render/{task_id}/download", + timeout=60 + ) + response.raise_for_status() + + with open(output_file, 'wb') as f: + f.write(response.content) + return {"success": True, "file": output_file} + except Exception as e: + return {"error": str(e)} + + def wait_for_completion(self, task_id: str, max_wait: int = 300): + """等待任务完成""" + start_time = time.time() + + while time.time() - start_time < max_wait: + status_result = self.get_task_status(task_id) + + if "error" in status_result: + return status_result + + status = status_result.get("status") + print(f"任务状态: {status}") + + if status == "completed": + return status_result + elif status == "failed": + return status_result + + time.sleep(2) + + return {"error": "任务超时"} + + +def test_basic_render(): + """测试基本渲染功能""" + print("=" * 60) + print("测试基本渲染功能") + print("=" * 60) + + client = IRGClient() + + # 1. 健康检查 + print("1. 健康检查...") + health = client.health_check() + print(f"健康状态: {json.dumps(health, indent=2, ensure_ascii=False)}") + + if not health.get("blender_available"): + print("❌ Blender不可用,请检查安装") + return + + # 2. 创建渲染任务 + print("\n2. 创建渲染任务...") + request_data = { + "room": { + "length": 7.0, + "width": 4.0, + "height": 3.0, + "prop_type": 0 # 0=无, 1=落地窗窗, 2=拱门, 3=门" + }, + "camera": { + "height": 1.3, + "view_type": 2, #正视图:1, 侧视图:2 + "rotation_angle": 45.0 + }, + "render": { + "resolution_x": 1080, + "resolution_y": 2400, + "engine": "eevee" # 固定为EEVEE + } + } + + create_result = client.create_render_task(request_data) + print(f"创建结果: {json.dumps(create_result, indent=2, ensure_ascii=False)}") + + if "error" in create_result: + print("❌ 任务创建失败") + return + + task_id = create_result.get("task_id") + if not task_id: + print("❌ 未获取到任务ID") + return + + # 3. 等待任务完成 + print(f"\n3. 等待任务完成 (ID: {task_id})...") + completion_result = client.wait_for_completion(task_id) + + if "error" in completion_result: + print(f"❌ 任务失败: {completion_result['error']}") + return + + if completion_result.get("status") == "completed": + print("✅ 任务完成!") + + # 4. 下载结果 + print("\n4. 下载渲染结果...") + output_file = f"test_render_{task_id}.png" + download_result = client.download_result(task_id, output_file) + + if "error" in download_result: + print(f"❌ 下载失败: {download_result['error']}") + else: + print(f"✅ 渲染结果已保存到: {output_file}") + else: + print(f"❌ 任务失败: {completion_result.get('error')}") + + +def test_different_views(): + """测试不同视图""" + print("=" * 60) + print("测试不同视图") + print("=" * 60) + + client = IRGClient() + + # 测试正视图和侧视图 + test_cases = [ + { + "name": "正视图", + "data": { + "room": {"length": 4.0, "width": 4.0, "height": 3.0, "prop_type": 0}, + "camera": {"height": 1.3, "view_type": 1, "rotation_angle": 0.0}, + "render": {"resolution_x": 800, "resolution_y": 600, "engine": "eevee"} + } + }, + { + "name": "侧视图-45度", + "data": { + "room": {"length": 4.0, "width": 4.0, "height": 3.0, "prop_type": 0}, + "camera": {"height": 1.3, "view_type": 2, "rotation_angle": 45.0}, + "render": {"resolution_x": 800, "resolution_y": 600, "engine": "eevee"} + } + }, + { + "name": "侧视图-30度", + "data": { + "room": {"length": 4.0, "width": 4.0, "height": 3.0, "prop_type": 0}, + "camera": {"height": 1.3, "view_type": 2, "rotation_angle": 30.0}, + "render": {"resolution_x": 800, "resolution_y": 600, "engine": "eevee"} + } + } + ] + + for i, test_case in enumerate(test_cases, 1): + print(f"\n{i}. 测试{test_case['name']}...") + + create_result = client.create_render_task(test_case['data']) + if "error" in create_result: + print(f"❌ 创建失败: {create_result['error']}") + continue + + task_id = create_result.get("task_id") + print(f"任务ID: {task_id}") + + # 快速检查(不等待完成) + time.sleep(1) + status = client.get_task_status(task_id) + print(f"当前状态: {status.get('status', 'unknown')}") + + +def test_prop_types(): + """测试不同道具类型""" + print("=" * 60) + print("测试不同道具类型") + print("=" * 60) + + client = IRGClient() + + # 测试不同道具类型 + test_cases = [ + { + "name": "无道具", + "data": { + "room": {"length": 5.0, "width": 4.0, "height": 3.0, "prop_type": 0}, + "camera": {"height": 1.3, "view_type": 2, "rotation_angle": 45.0}, + "render": {"resolution_x": 800, "resolution_y": 600, "engine": "eevee"} + } + }, + { + "name": "落地窗", + "data": { + "room": {"length": 5.0, "width": 4.0, "height": 3.0, "prop_type": 1}, + "camera": {"height": 1.3, "view_type": 2, "rotation_angle": 45.0}, + "render": {"resolution_x": 800, "resolution_y": 600, "engine": "eevee"} + } + }, + { + "name": "拱门", + "data": { + "room": {"length": 5.0, "width": 4.0, "height": 3.0, "prop_type": 2}, + "camera": {"height": 1.3, "view_type": 2, "rotation_angle": 45.0}, + "render": {"resolution_x": 800, "resolution_y": 600, "engine": "eevee"} + } + }, + { + "name": "门", + "data": { + "room": {"length": 5.0, "width": 4.0, "height": 3.0, "prop_type": 3}, + "camera": {"height": 1.3, "view_type": 2, "rotation_angle": 45.0}, + "render": {"resolution_x": 800, "resolution_y": 600, "engine": "eevee"} + } + } + ] + + for i, test_case in enumerate(test_cases, 1): + print(f"\n{i}. 测试{test_case['name']}...") + + create_result = client.create_render_task(test_case['data']) + if "error" in create_result: + print(f"❌ 创建失败: {create_result['error']}") + continue + + task_id = create_result.get("task_id") + print(f"任务ID: {task_id}") + + # 快速检查(不等待完成) + time.sleep(1) + status = client.get_task_status(task_id) + print(f"当前状态: {status.get('status', 'unknown')}") + + +def test_room_dimensions(): + """测试房间尺寸调整逻辑""" + print("=" * 60) + print("测试房间尺寸调整逻辑") + print("=" * 60) + + client = IRGClient() + + # 测试房间尺寸调整(width > length 的情况) + test_cases = [ + { + "name": "正常尺寸 (length >= width)", + "data": { + "room": {"length": 6.0, "width": 4.0, "height": 3.0, "prop_type": 0}, + "camera": {"height": 1.3, "view_type": 2, "rotation_angle": 45.0}, + "render": {"resolution_x": 800, "resolution_y": 600, "engine": "eevee"} + } + }, + { + "name": "需要调整 (width > length)", + "data": { + "room": {"length": 4.0, "width": 6.0, "height": 3.0, "prop_type": 0}, + "camera": {"height": 1.3, "view_type": 2, "rotation_angle": 45.0}, + "render": {"resolution_x": 800, "resolution_y": 600, "engine": "eevee"} + } + } + ] + + for i, test_case in enumerate(test_cases, 1): + print(f"\n{i}. 测试{test_case['name']}...") + + create_result = client.create_render_task(test_case['data']) + if "error" in create_result: + print(f"❌ 创建失败: {create_result['error']}") + continue + + task_id = create_result.get("task_id") + print(f"任务ID: {task_id}") + + # 快速检查(不等待完成) + time.sleep(1) + status = client.get_task_status(task_id) + print(f"当前状态: {status.get('status', 'unknown')}") + + +def main(): + """主函数""" + if len(sys.argv) > 1: + if sys.argv[1] == "views": + test_different_views() + elif sys.argv[1] == "props": + test_prop_types() + elif sys.argv[1] == "dimensions": + test_room_dimensions() + else: + print("用法: python test_client.py [views|props|dimensions]") + print(" views - 测试不同视图") + print(" props - 测试不同道具类型") + print(" dimensions - 测试房间尺寸调整") + else: + test_basic_render() + + +if __name__ == "__main__": + main() diff --git a/Isometquick-server/使用说明.md b/Isometquick-server/使用说明.md new file mode 100644 index 0000000..341267d --- /dev/null +++ b/Isometquick-server/使用说明.md @@ -0,0 +1,253 @@ +# Isometric Room Generator (IRG) 服务管理 - 使用说明 + +## 🚀 一键脚本 - irg.sh + +这是一个简化的一键服务管理脚本,整合了所有功能。 + +### 基本用法 + +```bash +# 进入项目目录 +cd Isometquick-server + +# 添加执行权限(Linux/Mac) +chmod +x irg.sh + +# 首次使用(自动安装并启动) +./irg.sh + +# 或者手动安装 +./irg.sh install +``` + +### 服务管理命令 + +```bash +# 启动服务 +./irg.sh start + +# 停止服务 +./irg.sh stop + +# 重启服务 +./irg.sh restart + +# 查看状态 +./irg.sh status + +# 查看日志 +./irg.sh logs + +# 实时查看日志 +./irg.sh logs -f + +# 日志轮转 +./irg.sh rotate + +# 显示帮助 +./irg.sh help +``` + +### 服务状态说明 + +**正常运行时会显示:** +- ✅ IRG服务正在运行 +- ✅ 端口 8003 正在监听 +- ✅ API健康检查通过 +- PID、内存、CPU使用情况 + +**服务地址:** +- API地址: http://localhost:8003 +- API文档: http://localhost:8003/docs +- 健康检查: http://localhost:8003/health + +### 文件说明 + +- `irg.pid` - 进程ID文件 +- `/data/Isometquick/logs/isometquick.log` - 服务日志文件 +- `venv/` - Python虚拟环境目录 +- `/data/Isometquick/` - 渲染输出目录 + +### 故障排除 + +1. **服务启动失败** + ```bash + ./irg.sh logs + ``` + 查看错误日志 + +2. **端口被占用** + ```bash + netstat -tlnp | grep 8003 + ``` + 检查端口占用情况 + +3. **虚拟环境问题** + ```bash + rm -rf venv + ./irg.sh install + ``` + 重新创建虚拟环境 + +4. **权限问题** + ```bash + sudo chown -R $USER:$USER /data/Isometquick/ + ``` + 修复输出目录权限 + +### 完整的部署流程 + +```bash +# 1. 进入项目目录 +cd Isometquick-server + +# 2. 首次部署(一键完成) +./irg.sh + +# 3. 检查状态 +./irg.sh status + +# 4. 测试API +curl http://localhost:8003/health +``` + +### 日常维护 + +```bash +# 重启服务 +./irg.sh restart + +# 查看运行状态 +./irg.sh status + +# 查看最新日志 +./irg.sh logs + +# 停止服务 +./irg.sh stop + +# 日志轮转(清理旧日志) +./irg.sh rotate +``` + +--- + +## 🧪 测试客户端 + +使用 `test_client.py` 测试API功能: + +```bash +# 基本渲染测试 +python test_client.py + +# 测试不同视图 +python test_client.py views + +# 测试不同道具类型 +python test_client.py props + +# 测试房间尺寸调整 +python test_client.py dimensions +``` + +### 道具类型说明 + +- `prop_type: 0` - 无道具 +- `prop_type: 1` - 落地窗(使用archimesh插件) +- `prop_type: 2` - 拱门(使用isometric_room_gen插件) +- `prop_type: 3` - 门(使用archimesh插件) + +### 视图类型说明 + +- `view_type: 1` - 正视图(相机位置:x=width/2, y=1, z=height) +- `view_type: 2` - 侧视图(相机位置:x=width-1, y=1, z=height) + +--- + +## 🔍 进程管理 + +### 查看进程 + +```bash +# 查看服务进程 +ps aux | grep -i irg + +# 查看Python进程 +ps aux | grep python | grep start.py + +# 查看端口占用 +netstat -tlnp | grep 8003 +``` + +### 手动停止进程 + +```bash +# 通过PID文件停止 +if [ -f "irg.pid" ]; then + kill $(cat irg.pid) + rm irg.pid +fi + +# 强制停止所有相关进程 +pkill -f "python.*start.py" +``` + +--- + +## 💡 使用建议 + +1. **推荐使用 `irg.sh`** - 功能完整且简单易用 +2. **首次使用直接运行** `./irg.sh` 会自动安装并启动 +3. **定期重启服务** 保持最佳性能 +4. **监控日志文件** 及时发现问题 +5. **备份配置文件** 避免意外丢失 +6. **使用道具功能** 创建更丰富的房间场景 + +--- + +## 🎯 快速上手 + +```bash +# 一键启动(推荐) +cd Isometquick-server && ./irg.sh + +# 检查状态 +./irg.sh status + +# 测试API +python test_client.py + +# 访问API文档 +# 浏览器打开: http://localhost:8003/docs +``` + +### 示例API请求 + +```python +import requests + +# 创建渲染任务 +request_data = { + "room": { + "length": 7.0, # 房间长度(Y轴) + "width": 4.0, # 房间宽度(X轴) + "height": 3.0, # 房间高度(Z轴) + "prop_type": 1 # 1=落地窗 + }, + "camera": { + "height": 1.3, # 相机高度 + "view_type": 2, # 2=侧视图 + "rotation_angle": 45.0 + }, + "render": { + "resolution_x": 1080, + "resolution_y": 2400, + "engine": "eevee" # 固定为EEVEE + } +} + +response = requests.post("http://localhost:8003/render", json=request_data) +print(response.json()) +``` + +就这么简单!🎉 \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..54ba9c0 --- /dev/null +++ b/__init__.py @@ -0,0 +1,1267 @@ +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import math +import bmesh +import bpy.utils.previews +import bpy + +bl_info = { + "name": "Isometric Room Gen", + "author": "SkdSam & Mr Steve", + "description": "Generate Isometric Rooms", + "blender": (3, 6, 0), + "version": (1, 0, 6), + "location": "", + "warning": "", + "doc_url": "https://superhivemarket.com/creators/skdsam", + "tracker_url": "", + "category": "3D View" +} + + +def string_to_int(value): + if value.isdigit(): + return int(value) + return 0 + + +def string_to_icon(value): + if value in bpy.types.UILayout.bl_rna.functions["prop"].parameters["icon"].enum_items.keys(): + return bpy.types.UILayout.bl_rna.functions["prop"].parameters["icon"].enum_items[value].value + return string_to_int(value) + + +addon_keymaps = {} +_icons = None + + +class SNA_PT_IRG_3542C(bpy.types.Panel): + bl_label = 'IRG' + bl_idname = 'SNA_PT_IRG_3542C' + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_context = '' + bl_category = 'IRG' + bl_order = 0 + bl_ui_units_x = 0 + + @classmethod + def poll(cls, context): + return not (False) + + def draw_header(self, context): + layout = self.layout + + def draw(self, context): + layout = self.layout + layout.prop(bpy.context.scene, 'sna_style', + text=bpy.context.scene.sna_style, icon_value=0, emboss=True, expand=True) + if bpy.context.scene.sna_style == "Square": + col_F2E32 = layout.column(heading='', align=False) + col_F2E32.alert = False + col_F2E32.enabled = True + col_F2E32.active = True + col_F2E32.use_property_split = False + col_F2E32.use_property_decorate = False + col_F2E32.scale_x = 1.0 + col_F2E32.scale_y = 1.0 + col_F2E32.alignment = 'Expand'.upper() + col_F2E32.operator_context = "INVOKE_DEFAULT" if True else "EXEC_DEFAULT" + row_8A9B7 = col_F2E32.row(heading='', align=True) + row_8A9B7.alert = False + row_8A9B7.enabled = True + row_8A9B7.active = True + row_8A9B7.use_property_split = False + row_8A9B7.use_property_decorate = False + row_8A9B7.scale_x = 1.0 + row_8A9B7.scale_y = 1.0 + row_8A9B7.alignment = 'Expand'.upper() + row_8A9B7.operator_context = "INVOKE_DEFAULT" if True else "EXEC_DEFAULT" + row_8A9B7.prop(bpy.context.scene, 'sna_room_settings', text='Room Settings ', icon_value=( + 11 if bpy.context.scene.sna_room_settings else 10), emboss=False, toggle=True) + row_8A9B7.prop(bpy.context.scene, 'sna_room_settings', text='', icon_value=string_to_icon( + 'MATCUBE'), emboss=bpy.context.scene.sna_room_settings, toggle=True) + col_AF92C = col_F2E32.column(heading='', align=False) + col_AF92C.alert = False + col_AF92C.enabled = True + col_AF92C.active = True + col_AF92C.use_property_split = False + col_AF92C.use_property_decorate = False + col_AF92C.scale_x = 1.0 + col_AF92C.scale_y = 1.0 + col_AF92C.alignment = 'Expand'.upper() + col_AF92C.operator_context = "INVOKE_DEFAULT" if True else "EXEC_DEFAULT" + if bpy.context.scene.sna_room_settings: + box_480DE = col_AF92C.box() + box_480DE.alert = False + box_480DE.enabled = True + box_480DE.active = True + box_480DE.use_property_split = False + box_480DE.use_property_decorate = False + box_480DE.alignment = 'Expand'.upper() + box_480DE.scale_x = 1.0 + box_480DE.scale_y = 1.0 + if not True: + box_480DE.operator_context = "EXEC_DEFAULT" + box_480DE.prop(bpy.context.scene, 'sna_room_width', + text='Room Width', icon_value=0, emboss=True) + box_480DE.prop(bpy.context.scene, 'sna_room_depth', + text='Room Depth', icon_value=0, emboss=True) + box_480DE.prop(bpy.context.scene, 'sna_room_height', + text='Room Height', icon_value=0, emboss=True) + box_480DE.prop(bpy.context.scene, 'sna_wall_thickness', + text='Wall Thickness', icon_value=0, emboss=True) + row_C816B = col_F2E32.row(heading='', align=True) + row_C816B.alert = False + row_C816B.enabled = True + row_C816B.active = True + row_C816B.use_property_split = False + row_C816B.use_property_decorate = False + row_C816B.scale_x = 1.0 + row_C816B.scale_y = 1.0 + row_C816B.alignment = 'Expand'.upper() + row_C816B.operator_context = "INVOKE_DEFAULT" if True else "EXEC_DEFAULT" + row_C816B.prop(bpy.context.scene, 'sna_winows_settings', text='Window Settings', icon_value=( + 11 if bpy.context.scene.sna_winows_settings else 10), emboss=False, toggle=True) + row_C816B.prop(bpy.context.scene, 'sna_winows_settings', text='', icon_value=string_to_icon( + 'MESH_PLANE'), emboss=bpy.context.scene.sna_winows_settings, toggle=True) + if bpy.context.scene.sna_winows_settings: + box_7092C = col_F2E32.box() + box_7092C.alert = False + box_7092C.enabled = True + box_7092C.active = True + box_7092C.use_property_split = False + box_7092C.use_property_decorate = False + box_7092C.alignment = 'Expand'.upper() + box_7092C.scale_x = 1.0 + box_7092C.scale_y = 1.0 + if not True: + box_7092C.operator_context = "EXEC_DEFAULT" + box_7092C.prop(bpy.context.scene, 'sna_windows_enum', + text='Window Placement', icon_value=0, emboss=True) + if (bpy.context.scene.sna_windows_enum != 'NONE'): + col_8CB93 = box_7092C.column(heading='', align=False) + col_8CB93.alert = False + col_8CB93.enabled = True + col_8CB93.active = True + col_8CB93.use_property_split = False + col_8CB93.use_property_decorate = False + col_8CB93.scale_x = 1.0 + col_8CB93.scale_y = 1.0 + col_8CB93.alignment = 'Expand'.upper() + col_8CB93.operator_context = "INVOKE_DEFAULT" if True else "EXEC_DEFAULT" + col_8CB93.prop(bpy.context.scene, 'sna_window_style', + text='Type', icon_value=0, emboss=True) + col_8CB93.prop(bpy.context.scene, 'sna_windows_count', + text='Window Count', icon_value=0, emboss=True) + col_8CB93.prop(bpy.context.scene, 'sna_windows_width', + text='Window Width %', icon_value=0, emboss=True) + col_8CB93.prop(bpy.context.scene, 'sna_windows_height', + text='Window Height %', icon_value=0, emboss=True) + + # 添加拱门设置 + row_arch = col_F2E32.row(heading='', align=True) + row_arch.alert = False + row_arch.enabled = True + row_arch.active = True + row_arch.use_property_split = False + row_arch.use_property_decorate = False + row_arch.scale_x = 1.0 + row_arch.scale_y = 1.0 + row_arch.alignment = 'Expand'.upper() + row_arch.operator_context = "INVOKE_DEFAULT" if True else "EXEC_DEFAULT" + row_arch.prop(bpy.context.scene, 'sna_arch_settings', text='Arch Settings ', icon_value=( + 11 if bpy.context.scene.sna_arch_settings else 10), emboss=False, toggle=True) + row_arch.prop(bpy.context.scene, 'sna_arch_settings', text='', icon_value=string_to_icon( + 'MESH_CUBE'), emboss=bpy.context.scene.sna_arch_settings, toggle=True) + + if bpy.context.scene.sna_arch_settings: + box_arch = col_F2E32.box() + box_arch.alert = False + box_arch.enabled = True + box_arch.active = True + box_arch.use_property_split = False + box_arch.use_property_decorate = False + box_arch.alignment = 'Expand'.upper() + box_arch.scale_x = 1.0 + box_arch.scale_y = 1.0 + if not True: + box_arch.operator_context = "EXEC_DEFAULT" + box_arch.prop(bpy.context.scene, 'sna_arch_placement', + text='Arch Placement', icon_value=0, emboss=True) + if bpy.context.scene.sna_arch_placement != 'NONE': + box_arch.prop(bpy.context.scene, 'sna_arch_width', + text='Arch Width', icon_value=0, emboss=True) + box_arch.prop(bpy.context.scene, 'sna_arch_height', + text='Arch Height', icon_value=0, emboss=True) + box_arch.prop(bpy.context.scene, 'sna_arch_thickness', + text='Arch Thickness', icon_value=0, emboss=True) + + col_F2E32.separator(factor=1.0000016689300537) + row_A6551 = col_F2E32.row(heading='', align=False) + row_A6551.alert = False + row_A6551.enabled = True + row_A6551.active = True + row_A6551.use_property_split = False + row_A6551.use_property_decorate = False + row_A6551.scale_x = 1.0 + row_A6551.scale_y = 2.440000057220459 + row_A6551.alignment = 'Expand'.upper() + row_A6551.operator_context = "INVOKE_DEFAULT" if True else "EXEC_DEFAULT" + op = row_A6551.operator('sna.gen_room_1803a', text='Generate Room', icon_value=string_to_icon( + 'SHAPEKEY_DATA'), emboss=True, depress=False) + elif bpy.context.scene.sna_style == "Round": + col_05349 = layout.column(heading='', align=False) + col_05349.alert = False + col_05349.enabled = True + col_05349.active = True + col_05349.use_property_split = False + col_05349.use_property_decorate = False + col_05349.scale_x = 1.0 + col_05349.scale_y = 1.0 + col_05349.alignment = 'Expand'.upper() + col_05349.operator_context = "INVOKE_DEFAULT" if True else "EXEC_DEFAULT" + row_31CFF = col_05349.row(heading='', align=True) + row_31CFF.alert = False + row_31CFF.enabled = True + row_31CFF.active = True + row_31CFF.use_property_split = False + row_31CFF.use_property_decorate = False + row_31CFF.scale_x = 1.0 + row_31CFF.scale_y = 1.0 + row_31CFF.alignment = 'Expand'.upper() + row_31CFF.operator_context = "INVOKE_DEFAULT" if True else "EXEC_DEFAULT" + row_31CFF.prop(bpy.context.scene, 'sna_round_room_settings', text='Room Settings ', icon_value=( + 11 if bpy.context.scene.sna_round_room_settings else 10), emboss=False, toggle=True) + row_31CFF.prop(bpy.context.scene, 'sna_round_room_settings', text='', icon_value=string_to_icon( + 'MATCUBE'), emboss=bpy.context.scene.sna_round_room_settings, toggle=True) + col_549D5 = col_05349.column(heading='', align=False) + col_549D5.alert = False + col_549D5.enabled = True + col_549D5.active = True + col_549D5.use_property_split = False + col_549D5.use_property_decorate = False + col_549D5.scale_x = 1.0 + col_549D5.scale_y = 1.0 + col_549D5.alignment = 'Expand'.upper() + col_549D5.operator_context = "INVOKE_DEFAULT" if True else "EXEC_DEFAULT" + if bpy.context.scene.sna_round_room_settings: + box_B3F1F = col_549D5.box() + box_B3F1F.alert = False + box_B3F1F.enabled = True + box_B3F1F.active = True + box_B3F1F.use_property_split = False + box_B3F1F.use_property_decorate = False + box_B3F1F.alignment = 'Expand'.upper() + box_B3F1F.scale_x = 1.0 + box_B3F1F.scale_y = 1.0 + if not True: + box_B3F1F.operator_context = "EXEC_DEFAULT" + box_B3F1F.prop(bpy.context.scene, 'sna_room_diameter_round', + text='Room Diameter', icon_value=0, emboss=True) + box_B3F1F.prop(bpy.context.scene, 'sna_room_floor_thickness_round', + text='Floor Depth', icon_value=0, emboss=True) + box_B3F1F.prop(bpy.context.scene, 'sna_room_height_round', + text='Room Height', icon_value=0, emboss=True) + box_B3F1F.prop(bpy.context.scene, 'sna_wall_thickness_round', + text='Wall Thickness', icon_value=0, emboss=True) + row_6F6A0 = col_05349.row(heading='', align=True) + row_6F6A0.alert = False + row_6F6A0.enabled = True + row_6F6A0.active = True + row_6F6A0.use_property_split = False + row_6F6A0.use_property_decorate = False + row_6F6A0.scale_x = 1.0 + row_6F6A0.scale_y = 1.0 + row_6F6A0.alignment = 'Expand'.upper() + row_6F6A0.operator_context = "INVOKE_DEFAULT" if True else "EXEC_DEFAULT" + row_6F6A0.prop(bpy.context.scene, 'sna_round_window_settings', text='Window Settings', icon_value=( + 11 if bpy.context.scene.sna_round_window_settings else 10), emboss=False, toggle=True) + row_6F6A0.prop(bpy.context.scene, 'sna_round_window_settings', text='', icon_value=string_to_icon( + 'MESH_PLANE'), emboss=bpy.context.scene.sna_round_window_settings, toggle=True) + if bpy.context.scene.sna_round_window_settings: + box_E2A99 = col_05349.box() + box_E2A99.alert = False + box_E2A99.enabled = True + box_E2A99.active = True + box_E2A99.use_property_split = False + box_E2A99.use_property_decorate = False + box_E2A99.alignment = 'Expand'.upper() + box_E2A99.scale_x = 1.0 + box_E2A99.scale_y = 1.0 + if not True: + box_E2A99.operator_context = "EXEC_DEFAULT" + box_E2A99.prop(bpy.context.scene, 'sna_window_style_round', + text='Window Placement', icon_value=0, emboss=True) + if (bpy.context.scene.sna_window_style_round != 'NONE'): + col_765D0 = box_E2A99.column(heading='', align=False) + col_765D0.alert = False + col_765D0.enabled = True + col_765D0.active = True + col_765D0.use_property_split = False + col_765D0.use_property_decorate = False + col_765D0.scale_x = 1.0 + col_765D0.scale_y = 1.0 + col_765D0.alignment = 'Expand'.upper() + col_765D0.operator_context = "INVOKE_DEFAULT" if True else "EXEC_DEFAULT" + col_765D0.prop(bpy.context.scene, 'sna_round_window_amount', + text='Window Count', icon_value=0, emboss=True) + col_765D0.prop(bpy.context.scene, 'sna_round_window_width', + text='Window Width %', icon_value=0, emboss=True) + col_765D0.prop(bpy.context.scene, 'sna_round_window_height', + text='Window Height %', icon_value=0, emboss=True) + col_05349.separator(factor=1.0000016689300537) + row_63B4A = col_05349.row(heading='', align=False) + row_63B4A.alert = False + row_63B4A.enabled = True + row_63B4A.active = True + row_63B4A.use_property_split = False + row_63B4A.use_property_decorate = False + row_63B4A.scale_x = 1.0 + row_63B4A.scale_y = 2.440000057220459 + row_63B4A.alignment = 'Expand'.upper() + row_63B4A.operator_context = "INVOKE_DEFAULT" if True else "EXEC_DEFAULT" + op = row_63B4A.operator('sna.gen_round_room_a43ca', text='Generate Room', icon_value=string_to_icon( + 'SHAPEKEY_DATA'), emboss=True, depress=False) + + +class SNA_OT_Gen_Room_1803A(bpy.types.Operator): + bl_idname = "sna.gen_room_1803a" + bl_label = "Gen room" + bl_description = "Create room" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + if bpy.app.version >= (3, 0, 0) and True: + cls.poll_message_set('') + return not False + + def execute(self, context): + room_width = bpy.context.scene.sna_room_width + room_depth = bpy.context.scene.sna_room_depth + room_height = bpy.context.scene.sna_room_height + wall_thickness = bpy.context.scene.sna_wall_thickness + window_placement = bpy.context.scene.sna_windows_enum + window_count = bpy.context.scene.sna_windows_count + window_width_pct = bpy.context.scene.sna_windows_width + window_height_pct = bpy.context.scene.sna_windows_height + window_type = bpy.context.scene.sna_window_style + import math + + # 确保大的值为room_depth,小的值为room_width + if room_width > room_depth: + # 交换值 + room_width, room_depth = room_depth, room_width + print(f"自动调整房间尺寸:宽度={room_width}m,深度={room_depth}m") + + # 在generate_room函数中,将拱门生成函数定义在调用之前 + def generate_room( + room_width, room_depth, room_height, wall_thickness, + window_placement, window_count, window_width_pct, window_height_pct, + window_type='SQUARE', window_divisions=2, + sash_bottom_pct=0.55): + """ + 1) Make floor & walls as separate cubes (scale applied) + 2) Carve windows from each wall (cutters & panes all apply scale) + 3) Boolean-union walls into the floor + 4) Join sash-frame bits, then weld stray verts + 5) Add ceiling (天花板) + 6) Add arch door (拱门) + """ + extra = 0.01 + inset = (wall_thickness - extra) / 2.0 + glass_thickness = extra + ceiling_thickness = 0.10 # 天花板厚度10cm + + # 拱门生成函数(定义在generate_room内部,但在使用之前) + def add_arch_to_room(room_obj, room_width, room_depth, room_height, wall_thickness): + """创建独立的拱门墙面""" + arch_placement = bpy.context.scene.sna_arch_placement + + # 如果选择"无",则不生成拱门 + if arch_placement == 'NONE': + print("未选择拱门位置,跳过拱门生成") + return + + # 根据规则计算拱门参数 + # 基础空间:5x3x2.8,拱门距背景墙1米,空间每加1米长,距离增加30cm + base_distance = 1.0 # 基础距离1米 + distance_increase = 0.3 # 每增加1米距离增加30cm + extra_length = room_depth - 5.0 # 超出基础5米的长度(不是3米) + arch_distance = base_distance + \ + (extra_length * distance_increase) + + # 门洞宽度:基础1.2米,空间宽度每加1米,门洞宽度+30cm + base_width = 1.2 # 基础宽度1.2米 + width_increase = 0.3 # 每增加1米宽度增加30cm + extra_width = room_width - 3.0 # 超出基础3米的宽度(不是5米) + arch_width = base_width + (extra_width * width_increase) + + # 门洞高度:根据层高确定 + arch_height = 2.4 # 默认2.4米 + if room_height >= 3.2: + arch_height = 2.7 + elif room_height >= 3.0: + arch_height = 2.6 + elif room_height >= 2.8: + arch_height = 2.4 + + # 拱门墙面厚度 + arch_wall_thickness = 0.10 # 10cm厚 + + print(f"拱门墙面参数计算:") + print(f" 房间尺寸: {room_width}x{room_depth}x{room_height}") + print(f" 拱门墙面距离背景墙: {arch_distance}m") + print(f" 拱门宽度: {arch_width}m") + print(f" 拱门高度: {arch_height}m") + print(f" 拱门墙面厚度: {arch_wall_thickness}m") + + # 创建拱门墙面(复制并移动选择的墙面) + arch_wall = create_arch_wall_by_copy( + room_width, room_depth, room_height, arch_wall_thickness, arch_placement, arch_distance) + + if arch_wall: + # 在拱门墙面上挖洞 + # 增加切割器厚度,确保能完全穿透拱门墙面 + arch_cutter = create_arch_cutter( + arch_width, arch_height, arch_wall_thickness + 0.05) # 增加厚度确保穿透 + + if arch_cutter: + # 设置拱门切割器位置(相对于拱门墙面) + if arch_placement == 'BACK': + # 在后墙前面创建拱门墙面,在墙面上挖洞 + # 拱门墙面位置:(0, room_depth - distance - wall_thickness, 0) + # 切割器应该在拱门墙面中心:(room_width/2, room_depth - distance - wall_thickness, 0) + arch_cutter.location = ( + room_width/2, # X居中 + room_depth - arch_distance - arch_wall_thickness + + arch_wall_thickness/2, # Y在拱门墙面中心,多加半个墙体厚度 + 0 # Z从地面开始 + ) + # 参考COPY4的旋转设置:绕Y轴旋转90度,使其垂直于墙面 + arch_cutter.rotation_euler = (0, 0, 0) + + # 使用布尔运算在拱门墙面上挖洞 + print( + f"开始布尔运算,拱门墙面: {arch_wall.name}, 切割器: {arch_cutter.name}") + print( + f"拱门墙面位置: {arch_wall.location}, 切割器位置: {arch_cutter.location}") + print( + f"拱门墙面尺寸: {arch_wall.dimensions}, 切割器尺寸: {arch_cutter.dimensions}") + + # 确保切割器完全穿透拱门墙面 + mod = arch_wall.modifiers.new("ArchCut", 'BOOLEAN') + mod.operation = 'DIFFERENCE' + mod.object = arch_cutter + bpy.context.view_layer.objects.active = arch_wall + bpy.ops.object.modifier_apply(modifier=mod.name) + + print(f"布尔运算完成") + + # 删除拱门切割器对象 + bpy.data.objects.remove(arch_cutter, do_unlink=True) + + print(f"拱门墙面已创建并挖洞,位置: {arch_placement}") + + def create_arch_wall_by_copy(room_width, room_depth, room_height, wall_thickness, placement, distance): + """通过复制墙面创建拱门墙面""" + # 创建墙面几何体 + bm = bmesh.new() + + # 房间外轮廓尺寸(包含墙体) + room_outer_width = room_width # 直接使用房间宽度,不加墙体厚度 + room_outer_depth = room_depth + wall_thickness + room_inner_height = room_height + + if placement == 'BACK': + # 复制后墙并前移 + # 后墙尺寸:宽度=房间宽度,高度=房间内高度,厚度=10cm + wall_width = room_outer_width + wall_height = room_inner_height + wall_depth = wall_thickness + + # 拱门墙面位置:在Y=distance处(距背景墙distance米) + # 创建墙面顶点(从原点开始,然后移动到正确位置) + verts = [ + (0, 0, 0), # 左下后(地面顶面) + (wall_width, 0, 0), # 右下后(地面顶面) + (wall_width, wall_depth, 0), # 右下前(地面顶面) + (0, wall_depth, 0), # 左下前(地面顶面) + (0, 0, room_inner_height), # 左上后(天花板底面) + (wall_width, 0, room_inner_height), # 右上后(天花板底面) + (wall_width, wall_depth, room_inner_height), # 右上前(天花板底面) + (0, wall_depth, room_inner_height) # 左上前(天花板底面) + ] + + # 添加顶点到bmesh + bm_verts = [bm.verts.new(v) for v in verts] + bm.verts.ensure_lookup_table() + + # 创建面 + faces = [ + [0, 1, 2, 3], # 底面 + [4, 5, 6, 7], # 顶面 + [0, 4, 7, 3], # 左面 + [1, 5, 6, 2], # 右面 + [3, 7, 6, 2], # 前面 + [0, 4, 5, 1] # 后面 + ] + + for face_verts in faces: + bm.faces.new([bm_verts[i] for i in face_verts]) + + # 创建网格对象 + mesh = bpy.data.meshes.new("ArchWallMesh") + bm.to_mesh(mesh) + bm.free() + + obj = bpy.data.objects.new("ArchWall", mesh) + bpy.context.collection.objects.link(obj) + + # 设置拱门墙面位置 + if placement == 'BACK': + # 背景墙位置:Y=room_depth + # 拱门墙面位置:Y=room_depth - distance + # 调整:向原点方向移动半个墙面厚度,使背面距背景墙前面distance米 + obj.location = (0, room_depth - distance - + wall_thickness, 0) + + return obj + + def create_arch_cutter(width, height, thickness, segments=32): + """创建拱门切割器(垂直XY面)""" + bm = bmesh.new() + + radius = width / 2 + wall_height = height - radius # 墙体部分高度 + + # 创建拱门轮廓点(在XZ平面上) + profile_verts = [] + + # 底部矩形部分 - 从左下角开始,逆时针,从地面以下开始,确保完全穿透 + profile_verts.extend([ + (-radius, 0, -thickness/2), # 左下(地面以下) + (radius, 0, -thickness/2), # 右下(地面以下) + (radius, 0, wall_height), # 右上(墙体顶部) + (-radius, 0, wall_height) # 左上(墙体顶部) + ]) + + # 顶部半圆部分 - 从右到左创建半圆顶点 + # 注意:这里需要从右到左,所以角度从0到π + for i in range(segments + 1): + angle = math.pi * i / segments # 从0到π + x = radius * math.cos(angle) + z = wall_height + radius * math.sin(angle) + profile_verts.append((x, 0, z)) + + # 拉伸创建3D几何体(沿Y轴拉伸) + extrude_profile_to_3d_vertical( + bm, profile_verts, thickness, radius, wall_height) + + # 创建网格对象 + mesh = bpy.data.meshes.new("ArchCutterMesh") + bm.to_mesh(mesh) + bm.free() + + obj = bpy.data.objects.new("ArchCutter", mesh) + bpy.context.collection.objects.link(obj) + + return obj + + def extrude_profile_to_3d_vertical(bm, profile_verts, thickness, radius, wall_height): + """将2D轮廓沿Y轴拉伸为3D几何体(垂直XY面)""" + # 创建前后面 + front_verts = [] + back_verts = [] + + for x, y, z in profile_verts: + front_verts.append(bm.verts.new((x, thickness/2, z))) + back_verts.append(bm.verts.new((x, -thickness/2, z))) + + bm.verts.ensure_lookup_table() + + # 创建前面 + bm.faces.new(front_verts) + + # 创建后面 + bm.faces.new(back_verts[::-1]) # 反转顺序以保持法向一致 + + # 创建侧面 - 但不创建矩形和半圆之间的连接面 + for i in range(len(profile_verts) - 1): + v1_front = front_verts[i] + v2_front = front_verts[i + 1] + v1_back = back_verts[i] + v2_back = back_verts[i + 1] + + # 检查是否是矩形和半圆之间的连接 + # 矩形顶部:Z = wall_height, X = ±radius + # 半圆底部:Z = wall_height, X = ±radius + # 如果两个顶点都在矩形顶部,则不创建面 + if (abs(v1_front.co.z - wall_height) < 0.001 and + abs(v2_front.co.z - wall_height) < 0.001 and + abs(v1_front.co.x) < radius + 0.001 and + abs(v2_front.co.x) < radius + 0.001): + continue # 跳过这个面 + + bm.faces.new([v1_front, v2_front, v2_back, v1_back]) + + # 连接首尾 + v1_front = front_verts[-1] + v2_front = front_verts[0] + v1_back = back_verts[-1] + v2_back = back_verts[0] + + bm.faces.new([v1_front, v2_front, v2_back, v1_back]) + + def create_semicircle_arch_geometry(width, height, thickness, segments=32): + """创建半圆拱门几何体(保留用于其他用途)""" + bm = bmesh.new() + + radius = width / 2 + wall_height = height - radius # 墙体部分高度 + + # 创建拱门轮廓点 + profile_verts = [] + + # 底部矩形部分 + profile_verts.extend([ + (-radius, 0, 0), (radius, 0, 0), + (radius, wall_height, 0), (-radius, wall_height, 0) + ]) + + # 顶部半圆部分 + for i in range(segments + 1): + angle = math.pi * i / segments + x = radius * math.cos(angle) + y = wall_height + radius * math.sin(angle) + profile_verts.append((x, y, 0)) + + # 拉伸创建3D几何体 + extrude_profile_to_3d(bm, profile_verts, thickness) + + # 创建网格对象 + mesh = bpy.data.meshes.new("SemicircleArchMesh") + bm.to_mesh(mesh) + bm.free() + + obj = bpy.data.objects.new("SemicircleArch", mesh) + bpy.context.collection.objects.link(obj) + + return obj + + def extrude_profile_to_3d(bm, profile_verts, thickness): + """将2D轮廓拉伸为3D几何体(保留用于其他用途)""" + # 创建前后面 + front_verts = [] + back_verts = [] + + for x, y, z in profile_verts: + front_verts.append(bm.verts.new((x, y, thickness/2))) + back_verts.append(bm.verts.new((x, y, -thickness/2))) + + bm.verts.ensure_lookup_table() + + # 创建前面 + bm.faces.new(front_verts) + + # 创建后面 + bm.faces.new(back_verts[::-1]) # 反转顺序以保持法向一致 + + # 创建侧面 + for i in range(len(profile_verts) - 1): + v1_front = front_verts[i] + v2_front = front_verts[i + 1] + v1_back = back_verts[i] + v2_back = back_verts[i + 1] + + bm.faces.new([v1_front, v2_front, v2_back, v1_back]) + + # 连接首尾 + v1_front = front_verts[-1] + v2_front = front_verts[0] + v1_back = back_verts[-1] + v2_back = back_verts[0] + + bm.faces.new([v1_front, v2_front, v2_back, v1_back]) + + # — Glass material setup — + def get_glass_mat(): + mat = bpy.data.materials.get("IRG_GlassMaterial") + if not mat: + mat = bpy.data.materials.new("IRG_GlassMaterial") + mat.use_nodes = True + nodes = mat.node_tree.nodes + links = mat.node_tree.links + for n in list(nodes): + nodes.remove(n) + out = nodes.new(type='ShaderNodeOutputMaterial') + mix = nodes.new(type='ShaderNodeMixShader') + fres = nodes.new(type='ShaderNodeFresnel') + refr = nodes.new(type='ShaderNodeBsdfRefraction') + gloss = nodes.new(type='ShaderNodeBsdfGlossy') + fres.inputs['IOR'].default_value = 1.5 + refr.inputs['IOR'].default_value = 1.5 + refr.inputs['Roughness'].default_value = 0.0 + gloss.inputs['Roughness'].default_value = 0.0 + fres.location = (-400, 200) + refr.location = (-200, 300) + gloss.location = (-200, 100) + mix.location = (0, 200) + out.location = (200, 200) + links.new(fres.outputs['Fac'], mix.inputs['Fac']) + links.new(refr.outputs['BSDF'], mix.inputs[1]) + links.new(gloss.outputs['BSDF'], mix.inputs[2]) + links.new(mix.outputs['Shader'], out.inputs['Surface']) + return mat + glass_mat = get_glass_mat() + + # 1) Create floor & walls, then apply scale + def make_cube(name, loc, scale): + bpy.ops.mesh.primitive_cube_add(location=loc) + o = bpy.context.active_object + o.name = name + o.scale = scale + bpy.ops.object.transform_apply( + location=False, rotation=False, scale=True) + return o + + # 房间内空尺寸(用户输入的尺寸) + room_inner_width = room_width + room_inner_depth = room_depth + room_inner_height = room_height + + # 房间外轮廓尺寸(包含墙体) + room_outer_width = room_inner_width + wall_thickness # 只有一边有墙 + room_outer_depth = room_inner_depth + wall_thickness # 只有一边有墙 + room_outer_height = room_inner_height + 2 * wall_thickness # 上下都有墙(地面+天花板) + + floor = make_cube("Floor", + # 地面顶面在Z=0,底面在Z=-wall_thickness + (room_outer_width/2, + room_outer_depth/2, -wall_thickness/2), + (room_outer_width/2, room_outer_depth/2, wall_thickness/2)) + + # 创建天花板 + ceiling = make_cube("Ceiling", + (room_outer_width/2, room_outer_depth/2, + # 天花板底面在Z=room_height,顶面在Z=room_height+wall_thickness + room_height + wall_thickness/2), + (room_outer_width/2, room_outer_depth/2, wall_thickness/2)) + + back_wall = make_cube("BackWall", + (room_outer_width/2, room_outer_depth - + wall_thickness/2, room_height/2), + (room_outer_width/2, wall_thickness/2, room_height/2)) + # 生成X=0位置的侧墙,垂直XZ面 + side_wall = make_cube("SideWall", + (wall_thickness/2, + room_outer_depth/2, room_height/2), + (wall_thickness/2, room_outer_depth/2, room_height/2)) + + # 2) Window-cut helpers with scale‐apply + def cube_cut(loc, sx, sy, sz): + bpy.ops.mesh.primitive_cube_add(location=loc) + o = bpy.context.active_object + o.scale = (sx, sy, sz) + bpy.ops.object.transform_apply( + location=False, rotation=False, scale=True) + return o + + def cyl_cut(loc, dia, depth, rot): + bpy.ops.mesh.primitive_cylinder_add( + vertices=64, radius=dia/2, depth=depth, + location=loc, rotation=rot + ) + o = bpy.context.active_object + # scale is baked into radius/depth above, so just apply + bpy.ops.object.transform_apply( + location=False, rotation=False, scale=True) + return o + + # carve windows out of a wall + def add_windows(wall, axis, wall_len): + frames = [] + glasses = [] + margin = wall_len * 0.1 + slot = (wall_len - 2*margin) / window_count + for i in range(window_count): + win_h = room_height * window_height_pct + z0 = room_height/2 + if axis == 'X': + x0 = margin + (i+0.5)*slot + y0 = room_depth - wall_thickness/2 + else: + y0 = margin + (i+0.5)*slot + x0 = room_width - wall_thickness/2 + win_w = slot * window_width_pct + pos = (x0, y0, z0) + depth_cut = wall_thickness + extra + # Boolean cutter + if window_type == 'ROUND': + dia = min(win_w, win_h) + rot = (math.pi/2, 0, 0) if axis == 'X' else ( + math.pi/2, 0, math.pi/2) + cutter = cyl_cut(pos, dia, depth_cut, rot) + else: + sx = win_w/2 if axis == 'X' else depth_cut/2 + sy = depth_cut/2 if axis == 'X' else win_w/2 + sz = win_h/2 + cutter = cube_cut(pos, sx, sy, sz) + # subtract cutter + mod = wall.modifiers.new(f"Win_{axis}_{i}", 'BOOLEAN') + mod.operation = 'DIFFERENCE' + mod.object = cutter + bpy.context.view_layer.objects.active = wall + bpy.ops.object.modifier_apply(modifier=mod.name) + bpy.data.objects.remove(cutter, do_unlink=True) + # add glass panes, name & apply scale + if window_type == 'ROUND': + glass_loc = ( + x0, y0-(wall_thickness/2-glass_thickness/2), z0 + ) if axis == 'X' else ( + x0-(wall_thickness/2-glass_thickness/2), y0, z0 + ) + g = cyl_cut(glass_loc, dia, glass_thickness, rot) + g.name = "IRG_IsoWindow" + g.data.materials.append(glass_mat) + glasses.append(g) + continue + # sash or single pane + if window_type == 'SASH' and window_divisions == 2: + ft = min(win_w, win_h)*0.08 + bottom_h = win_h * sash_bottom_pct + top_h = win_h - bottom_h - ft + # bottom pane + bot_cz = z0 - win_h/2 + bottom_h/2 + loc_b = (x0, y0-(wall_thickness/2-glass_thickness/2), bot_cz) \ + if axis == 'X' else \ + (x0-(wall_thickness/2-glass_thickness/2), y0, bot_cz) + g_bot = cube_cut(loc_b, + *((win_w/2, glass_thickness/2, bottom_h/2) + if axis == 'X' + else (glass_thickness/2, win_w/2, bottom_h/2))) + g_bot.name = "IRG_IsoWindow" + g_bot.data.materials.append(glass_mat) + glasses.append(g_bot) + # divider + z_div = z0 - win_h/2 + bottom_h + ft/2 + d = cube_cut((x0, y0, z_div), + *((win_w/2, depth_cut/2, ft/2) + if axis == 'X' + else (depth_cut/2, win_w/2, ft/2))) + frames.append(d) + # top pane + top_cz = z0 - win_h/2 + bottom_h + ft + top_h/2 + loc_t = (x0, y0-(wall_thickness/2-glass_thickness/2), top_cz) \ + if axis == 'X' else \ + (x0-(wall_thickness/2-glass_thickness/2), y0, top_cz) + g_top = cube_cut(loc_t, + *((win_w/2, glass_thickness/2, top_h/2) + if axis == 'X' + else (glass_thickness/2, win_w/2, top_h/2))) + g_top.name = "IRG_IsoWindow" + g_top.data.materials.append(glass_mat) + glasses.append(g_top) + else: + # single pane + loc_g = (x0, y0-(wall_thickness/2-glass_thickness/2), z0) \ + if axis == 'X' else \ + (x0-(wall_thickness/2-glass_thickness/2), y0, z0) + g = cube_cut(loc_g, + *((win_w/2, glass_thickness/2, win_h/2) + if axis == 'X' + else (glass_thickness/2, win_w/2, win_h/2))) + g.name = "IRG_IsoWindow" + g.data.materials.append(glass_mat) + glasses.append(g) + return frames, glasses + + # carve windows + frame_objs = [] + if window_placement in {'BACK', 'BOTH'}: + f, _ = add_windows(back_wall, 'X', room_width) + frame_objs += f + if window_placement in {'SIDE', 'BOTH'}: + # 改为'X'轴,因为对面墙也是X轴方向 + f, _ = add_windows(side_wall, 'X', room_width) + frame_objs += f + + # 3) 取消墙面与地面合并,保持独立对象 + # for wall in (back_wall, side_wall): # 注释掉墙面合并 + # mod = floor.modifiers.new(f"Union_{wall.name}", 'BOOLEAN') + # mod.operation = 'UNION' + # mod.object = wall + # bpy.context.view_layer.objects.active = floor + # bpy.ops.object.modifier_apply(modifier=mod.name) + # bpy.data.objects.remove(wall, do_unlink=True) + + # 4) 取消窗户框架合并,保持独立对象 + # bpy.ops.object.select_all(action='DESELECT') + # floor.select_set(True) + # for o in frame_objs: + # o.select_set(True) + # bpy.context.view_layer.objects.active = floor + # bpy.ops.object.join() + # floor.name = "IRG_IsoRoom" + + # 5) Final weld + normals (只对地面进行) + bpy.context.view_layer.objects.active = floor + bpy.ops.object.mode_set(mode='EDIT') + try: + bpy.ops.mesh.merge_by_distance(distance=0.001) + except AttributeError: + bpy.ops.mesh.remove_doubles( + threshold=0.001, use_unselected=False) + bpy.ops.mesh.normals_make_consistent(inside=False) + bpy.ops.object.mode_set(mode='OBJECT') + + # 6) 取消天花板与房间主体合并,保持独立对象 + # bpy.ops.object.select_all(action='DESELECT') + # floor.select_set(True) + # ceiling.select_set(True) + # bpy.context.view_layer.objects.active = floor + # bpy.ops.object.join() + # floor.name = "IRG_IsoRoom_WithCeiling" + + # 7) 添加拱门 + if bpy.context.scene.sna_arch_settings: + add_arch_to_room(floor, room_width, room_depth, + room_height, wall_thickness) + + # 8) 最终清理(只对地面进行) + bpy.context.view_layer.objects.active = floor + bpy.ops.object.mode_set(mode='EDIT') + try: + bpy.ops.mesh.merge_by_distance(distance=0.001) + except AttributeError: + bpy.ops.mesh.remove_doubles( + threshold=0.001, use_unselected=False) + bpy.ops.mesh.normals_make_consistent(inside=False) + bpy.ops.object.mode_set(mode='OBJECT') + + # 9) 将整个房间在X轴负方向上位移墙厚度的距离 + floor.location.x -= wall_thickness + back_wall.location.x -= wall_thickness + side_wall.location.x -= wall_thickness + ceiling.location.x -= wall_thickness + print(f"房间已向X轴负方向位移 {wall_thickness}m") + + generate_room(room_width, room_depth, room_height, + wall_thickness, window_placement, + window_count, window_width_pct, + window_height_pct, window_type, + window_divisions=2, sash_bottom_pct=0.55) + return {"FINISHED"} + + def invoke(self, context, event): + return self.execute(context) + + +class SNA_OT_Gen_Round_Room_A43Ca(bpy.types.Operator): + bl_idname = "sna.gen_round_room_a43ca" + bl_label = "Gen Round Room" + bl_description = "Create Round Iso Room" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + if bpy.app.version >= (3, 0, 0) and True: + cls.poll_message_set('') + return not False + + def execute(self, context): + diameter = bpy.context.scene.sna_room_diameter_round + floor_thickness = bpy.context.scene.sna_room_floor_thickness_round + wall_height = bpy.context.scene.sna_room_height_round + wall_thickness = bpy.context.scene.sna_wall_thickness_round + window_count = bpy.context.scene.sna_round_window_amount + window_width_pct = bpy.context.scene.sna_round_window_width + window_height_pct = bpy.context.scene.sna_round_window_height + window_type = bpy.context.scene.sna_window_style_round + + def get_glass_mat(): + mat = bpy.data.materials.get("IRG_GlassMaterial") + if not mat: + mat = bpy.data.materials.new("IRG_GlassMaterial") + mat.use_nodes = True + nodes = mat.node_tree.nodes + links = mat.node_tree.links + for n in list(nodes): + nodes.remove(n) + out = nodes.new('ShaderNodeOutputMaterial') + mix = nodes.new('ShaderNodeMixShader') + fres = nodes.new('ShaderNodeFresnel') + refr = nodes.new('ShaderNodeBsdfRefraction') + gloss = nodes.new('ShaderNodeBsdfGlossy') + fres.inputs['IOR'].default_value = 1.5 + refr.inputs['IOR'].default_value = 1.5 + refr.inputs['Roughness'].default_value = 0.0 + gloss.inputs['Roughness'].default_value = 0.0 + links.new(fres.outputs['Fac'], mix.inputs['Fac']) + links.new(refr.outputs['BSDF'], mix.inputs[1]) + links.new(gloss.outputs['BSDF'], mix.inputs[2]) + links.new(mix.outputs['Shader'], out.inputs['Surface']) + return mat + + def make_curved_glass(name, θ_center, arc_angle, radius, height, segments, z_offset, mat=None): + bm = bmesh.new() + verts_top = [] + verts_bottom = [] + for i in range(segments + 1): + θ = θ_center - arc_angle / 2 + i * (arc_angle / segments) + x = radius * math.cos(θ) + y = radius * math.sin(θ) + verts_top.append(bm.verts.new((x, y, z_offset + height / 2))) + verts_bottom.append(bm.verts.new( + (x, y, z_offset - height / 2))) + bm.verts.ensure_lookup_table() + for i in range(segments): + bm.faces.new( + (verts_top[i], verts_bottom[i], verts_bottom[i+1], verts_top[i+1])) + mesh = bpy.data.meshes.new(name) + bm.to_mesh(mesh) + bm.free() + obj = bpy.data.objects.new(name, mesh) + bpy.context.collection.objects.link(obj) + obj.location.z = height / 2 + if mat: + obj.data.materials.append(mat) + return obj + + def generate_round_room( + diameter, floor_thickness, wall_height, wall_thickness, + window_count, window_width_pct, window_height_pct, + window_type='SQUARE', sash_bottom_pct=0.55 + ): + extra = 0.01 + R_out = diameter / 2.0 + R_mid = R_out - wall_thickness / 2.0 + glass_off = extra / 2.0 + glass_mat = get_glass_mat() + + def make_cylinder(name, radius, depth, loc, rot=(0, 0, 0), verts=64): + bpy.ops.mesh.primitive_cylinder_add( + vertices=verts, radius=radius, depth=depth, location=loc, rotation=rot) + o = bpy.context.active_object + o.name = name + bpy.ops.object.transform_apply( + location=False, rotation=False, scale=True) + return o + + def make_cube(name, loc, half_extents, rot=(0, 0, 0)): + bpy.ops.mesh.primitive_cube_add(location=loc) + o = bpy.context.active_object + o.name = name + o.scale = half_extents + o.rotation_euler = rot + bpy.ops.object.transform_apply( + location=False, rotation=True, scale=True) + return o + floor = make_cylinder( + "Floor", R_out, floor_thickness, (0, 0, floor_thickness/2)) + wall = make_cylinder("WallOuter", R_out, + wall_height, (0, 0, wall_height/2)) + inner = make_cylinder( + "WallInner", R_out-wall_thickness, wall_height+extra, (0, 0, wall_height/2)) + mod = wall.modifiers.new("HollowWall", 'BOOLEAN') + mod.operation, mod.object = 'DIFFERENCE', inner + bpy.context.view_layer.objects.active = wall + bpy.ops.object.modifier_apply(modifier=mod.name) + bpy.data.objects.remove(inner, do_unlink=True) + cutter = make_cube( + "FrontCutter", (0, R_out, wall_height/2), (2*R_out, R_out, wall_height)) + mod = wall.modifiers.new("CutFront", 'BOOLEAN') + mod.operation, mod.object = 'DIFFERENCE', cutter + bpy.context.view_layer.objects.active = wall + bpy.ops.object.modifier_apply(modifier=mod.name) + bpy.data.objects.remove(cutter, do_unlink=True) + panes = [] + # Only generate windows if applicable + if window_type.upper() in {'SQUARE', 'SASH'} and window_count > 0: + start_ang, end_ang = math.pi, 2*math.pi + seg = (end_ang - start_ang) / window_count + for i in range(window_count): + θ = start_ang + (i+0.5)*seg + x, y = R_mid * math.cos(θ), R_mid * math.sin(θ) + z = wall_height / 2 + win_h = wall_height * window_height_pct + win_ang = seg * window_width_pct + chord = 2 * R_mid * math.sin(win_ang / 2) + cut = make_cube( + f"Cut_{i}", (x, y, z), (wall_thickness, chord/2 + extra, win_h/2 + extra), rot=(0, 0, θ)) + mod = wall.modifiers.new(f"WinBool_{i}", 'BOOLEAN') + mod.operation, mod.object = 'DIFFERENCE', cut + bpy.context.view_layer.objects.active = wall + bpy.ops.object.modifier_apply(modifier=mod.name) + bpy.data.objects.remove(cut, do_unlink=True) + objs = [] + if window_type.upper() == 'SASH': + ft = min(chord, win_h) * 0.08 + bot_h = win_h * sash_bottom_pct + top_h = win_h - bot_h - ft + bz = z - win_h/2 + bot = make_curved_glass( + f"IRG_IsoRoom_Window_SashBot_{i}", θ, win_ang, R_out - glass_off, bot_h, 16, bz, glass_mat) + objs.append(bot) + dz = bz + bot_h + bar = make_curved_glass( + f"IRG_IsoRoom_Window_SashBar_{i}", θ, win_ang, R_out - glass_off, ft, 16, dz, None) + objs.append(bar) + tz = dz + ft + top = make_curved_glass( + f"IRG_IsoRoom_Window_SashTop_{i}", θ, win_ang, R_out - glass_off, top_h, 16, tz, glass_mat) + objs.append(top) + if window_type.upper() == 'SQUARE': + pane = make_curved_glass( + f"IRG_IsoRoom_Window_Glass_{i}", θ, win_ang, R_out - glass_off, win_h, 16, z - win_h/2, glass_mat) + objs.append(pane) + panes.extend(objs) + mod = floor.modifiers.new("WallUnion", 'BOOLEAN') + mod.operation, mod.object = 'UNION', wall + bpy.context.view_layer.objects.active = floor + bpy.ops.object.modifier_apply(modifier=mod.name) + bpy.data.objects.remove(wall, do_unlink=True) + me = floor.data + bm = bmesh.new() + bm.from_mesh(me) + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001) + bm.normal_update() + bm.to_mesh(me) + bm.free() + floor.name = "IRG_IsoRoom_Round" + return floor, panes + generate_round_room( + diameter, + floor_thickness, + wall_height, + wall_thickness, + window_count, + window_width_pct, + window_height_pct, + window_type + ) + return {"FINISHED"} + + def invoke(self, context, event): + return self.execute(context) + + +def register(): + global _icons + _icons = bpy.utils.previews.new() + bpy.types.Scene.sna_room_width = bpy.props.FloatProperty( + name='Room Width', description='', default=1.0, subtype='NONE', unit='NONE', min=0.10000000149011612, step=3, precision=2) + bpy.types.Scene.sna_room_depth = bpy.props.FloatProperty( + name='Room Depth', description='', default=1.0, subtype='NONE', unit='NONE', min=0.10000000149011612, step=3, precision=2) + bpy.types.Scene.sna_room_height = bpy.props.FloatProperty( + name='Room Height', description='', default=1.0, subtype='NONE', unit='NONE', min=0.10000000149011612, step=3, precision=2) + bpy.types.Scene.sna_wall_thickness = bpy.props.FloatProperty( + name='Wall Thickness', description='', default=0.10000000149011612, subtype='NONE', unit='NONE', min=0.009999999776482582, step=3, precision=2) + bpy.types.Scene.sna_windows_enum = bpy.props.EnumProperty(name='Windows Enum', description='', items=[('NONE', 'NONE', 'None', 0, 0), ( + 'BACK', 'BACK', 'Back Wall Only', 0, 1), ('SIDE', 'SIDE', 'Side Wall Only', 0, 2), ('BOTH', 'BOTH', 'Both Walls', 0, 3)]) + bpy.types.Scene.sna_windows_count = bpy.props.IntProperty( + name='Windows Count', description='', default=1, subtype='NONE', min=1, max=5) + bpy.types.Scene.sna_windows_width = bpy.props.FloatProperty( + name='Windows Width', description='', default=0.25, subtype='NONE', unit='NONE', min=0.10000000149011612, max=1.0, step=3, precision=2) + bpy.types.Scene.sna_windows_height = bpy.props.FloatProperty( + name='Windows Height', description='', default=0.25, subtype='NONE', unit='NONE', min=0.10000000149011612, max=1.0, step=3, precision=2) + bpy.types.Scene.sna_window_style = bpy.props.EnumProperty(name='Window Style', description='', items=[( + 'SQUARE', 'SQUARE', 'SQUARE', 0, 0), ('ROUND', 'ROUND', 'ROUND', 0, 1), ('SASH', 'SASH', 'SASH', 0, 2)]) + bpy.types.Scene.sna_room_settings = bpy.props.BoolProperty( + name='Room Settings', description='', default=False) + bpy.types.Scene.sna_winows_settings = bpy.props.BoolProperty( + name='Winows settings', description='', default=False) + bpy.types.Scene.sna_style = bpy.props.EnumProperty(name='Style', description='', items=[( + 'Square', 'Square', 'Square Room', 0, 0), ('Round', 'Round', 'Round Room', 0, 1)]) + + # 添加拱门相关属性(移除前墙选项) + bpy.types.Scene.sna_arch_settings = bpy.props.BoolProperty( + name='Arch Settings', description='', default=False) + bpy.types.Scene.sna_arch_placement = bpy.props.EnumProperty( + name='Arch Placement', description='', + items=[('NONE', 'None', 'No Arch', 0, 0), + ('BACK', 'Back Wall', 'Back Wall', 0, 1)]) + bpy.types.Scene.sna_arch_width = bpy.props.FloatProperty( + name='Arch Width', description='', default=1.2, subtype='NONE', unit='NONE', min=0.5, max=3.0, step=3, precision=2) + bpy.types.Scene.sna_arch_height = bpy.props.FloatProperty( + name='Arch Height', description='', default=2.4, subtype='NONE', unit='NONE', min=1.5, max=4.0, step=3, precision=2) + bpy.types.Scene.sna_arch_thickness = bpy.props.FloatProperty( + name='Arch Thickness', description='', default=0.10, subtype='NONE', unit='NONE', min=0.05, max=0.5, step=3, precision=2) + + bpy.types.Scene.sna_round_room_settings = bpy.props.BoolProperty( + name='Round Room Settings', description='', default=False) + bpy.types.Scene.sna_round_window_settings = bpy.props.BoolProperty( + name='Round Window Settings', description='', default=False) + bpy.types.Scene.sna_room_diameter_round = bpy.props.FloatProperty( + name='Room diameter Round', description='', default=10.0, subtype='NONE', unit='NONE', min=0.10000000149011612, step=3, precision=2) + bpy.types.Scene.sna_room_floor_thickness_round = bpy.props.FloatProperty( + name='Room Floor thickness Round', description='', default=0.10000000149011612, subtype='NONE', unit='NONE', min=0.10000000149011612, step=3, precision=2) + bpy.types.Scene.sna_room_height_round = bpy.props.FloatProperty( + name='Room Height Round', description='', default=3.0, subtype='NONE', unit='NONE', min=0.10000000149011612, step=3, precision=2) + bpy.types.Scene.sna_wall_thickness_round = bpy.props.FloatProperty( + name='Wall Thickness round', description='', default=0.20000000298023224, subtype='NONE', unit='NONE', min=0.009999999776482582, step=3, precision=2) + bpy.types.Scene.sna_window_style_round = bpy.props.EnumProperty(name='Window Style Round', description='', items=[( + 'NONE', 'NONE', 'No Windows', 0, 0), ('SQUARE', 'SQUARE', 'Square Windows', 0, 1), ('SASH', 'SASH', 'Sash Windows', 0, 2)]) + bpy.types.Scene.sna_round_window_amount = bpy.props.IntProperty( + name='Round Window Amount', description='', default=6, subtype='NONE', min=1) + bpy.types.Scene.sna_round_window_width = bpy.props.FloatProperty( + name='Round Window Width', description='', default=0.699999988079071, subtype='NONE', unit='NONE', min=0.10000000149011612, max=1.0, step=3, precision=2) + bpy.types.Scene.sna_round_window_height = bpy.props.FloatProperty( + name='Round Window Height', description='', default=0.6000000238418579, subtype='NONE', unit='NONE', min=0.10000000149011612, max=1.0, step=3, precision=2) + bpy.utils.register_class(SNA_PT_IRG_3542C) + bpy.utils.register_class(SNA_OT_Gen_Room_1803A) + bpy.utils.register_class(SNA_OT_Gen_Round_Room_A43Ca) + + +def unregister(): + global _icons + bpy.utils.previews.remove(_icons) + wm = bpy.context.window_manager + kc = wm.keyconfigs.addon + for km, kmi in addon_keymaps.values(): + km.keymap_items.remove(kmi) + addon_keymaps.clear() + + # 清理拱门相关属性 + del bpy.types.Scene.sna_arch_thickness + del bpy.types.Scene.sna_arch_height + del bpy.types.Scene.sna_arch_width + del bpy.types.Scene.sna_arch_placement + del bpy.types.Scene.sna_arch_settings + + del bpy.types.Scene.sna_round_window_height + del bpy.types.Scene.sna_round_window_width + del bpy.types.Scene.sna_round_window_amount + del bpy.types.Scene.sna_window_style_round + del bpy.types.Scene.sna_wall_thickness_round + del bpy.types.Scene.sna_room_height_round + del bpy.types.Scene.sna_room_floor_thickness_round + del bpy.types.Scene.sna_room_diameter_round + del bpy.types.Scene.sna_round_window_settings + del bpy.types.Scene.sna_round_room_settings + del bpy.types.Scene.sna_style + del bpy.types.Scene.sna_winows_settings + del bpy.types.Scene.sna_room_settings + del bpy.types.Scene.sna_window_style + del bpy.types.Scene.sna_windows_height + del bpy.types.Scene.sna_windows_width + del bpy.types.Scene.sna_windows_count + del bpy.types.Scene.sna_windows_enum + del bpy.types.Scene.sna_wall_thickness + del bpy.types.Scene.sna_room_height + del bpy.types.Scene.sna_room_depth + del bpy.types.Scene.sna_room_width + bpy.utils.unregister_class(SNA_PT_IRG_3542C) + bpy.utils.unregister_class(SNA_OT_Gen_Room_1803A) + bpy.utils.unregister_class(SNA_OT_Gen_Round_Room_A43Ca) diff --git a/__pycache__/__init__.cpython-311.pyc b/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..ce7c8a0 Binary files /dev/null and b/__pycache__/__init__.cpython-311.pyc differ diff --git a/auto_room copy 2.py b/auto_room copy 2.py new file mode 100644 index 0000000..345ce04 --- /dev/null +++ b/auto_room copy 2.py @@ -0,0 +1,770 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Blender 4.2兼容版自动化脚本(白模版本) +自动创建等轴测房间并渲染白模 +""" + +import subprocess +import os +import sys + +# Blender可执行文件路径 +BLENDER_PATH = r"D:\Program Files\Blender Foundation\blender-4.2.11-windows-x64\blender.exe" + +# 简化版Blender脚本(只渲染白模) +BLENDER_SIMPLE_SCRIPT = ''' +import bpy +import bmesh +import os +import math +import time + +def disable_problematic_addons(): + """禁用可能导致问题的插件""" + try: + # 禁用可能冲突的插件 + problematic_addons = ['bl_tool'] + for addon in problematic_addons: + if addon in bpy.context.preferences.addons: + bpy.ops.preferences.addon_disable(module=addon) + print(f"已禁用插件: {addon}") + except: + pass + +def create_isometric_room(): + """创建等轴测房间,使用isometric_room_gen插件""" + try: + # 禁用问题插件 + #disable_problematic_addons() + + # 清除默认立方体 + if "Cube" in bpy.data.objects: + bpy.data.objects.remove(bpy.data.objects["Cube"], do_unlink=True) + + # 设置3D游标位置到原点 + bpy.context.scene.cursor.location = (0.0, 0.0, 0.0) + + # 检查isometric_room_gen插件 + if not hasattr(bpy.context.scene, 'sna_room_width'): + print("错误: isometric_room_gen插件未正确加载") + return False + + # 设置isometric_room_gen插件参数 + # 房间设置 + bpy.context.scene.sna_wall_thickness = 0.10 # 墙体厚度10cm + bpy.context.scene.sna_room_width = 5.0 # 房间宽度8米 + bpy.context.scene.sna_room_depth = 8.0 # 房间深度4米 + bpy.context.scene.sna_room_height = 3.0 # 房间高度3米 + + # 窗户设置 - 不设置窗户 + bpy.context.scene.sna_windows_enum = 'NONE' # 无窗户 + + # 设置房间类型为方形 + bpy.context.scene.sna_style = 'Square' + + # 找到3D视图区域 + view3d_area = None + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + view3d_area = area + break + + if view3d_area is None: + print("错误: 找不到3D视图区域") + return False + + # 临时切换到3D视图上下文 + with bpy.context.temp_override(area=view3d_area): + # 使用isometric_room_gen插件创建房间 + result = bpy.ops.sna.gen_room_1803a() + + if result == {'FINISHED'}: + print("等轴测房间创建完成!") + print(f"房间尺寸: {bpy.context.scene.sna_room_width}m x {bpy.context.scene.sna_room_depth}m x {bpy.context.scene.sna_room_height}m") + print(f"墙体厚度: {bpy.context.scene.sna_wall_thickness}m") + + # 将生成的房间在Z轴上向下移动墙体厚度的距离 + wall_thickness = bpy.context.scene.sna_wall_thickness + + # 查找生成的房间对象 + room_object = None + for obj in bpy.data.objects: + if obj.name == "IRG_IsoRoom_WithCeiling": + room_object = obj + break + + + return True + else: + print("房间创建失败") + return False + + except Exception as e: + print(f"创建房间时出现错误: {str(e)}") + import traceback + traceback.print_exc() + return False + +def create_window_with_archimesh(): + """使用archimesh插件创建落地窗""" + try: + print("开始创建archimesh落地窗...") + + # 检查archimesh插件是否可用 + if not hasattr(bpy.ops.mesh, 'archimesh_winpanel'): + print("错误: archimesh插件未正确加载") + print("请确保archimesh插件已安装并启用") + return False + + # 设置3D游标到原点 + bpy.context.scene.cursor.location = (0.0, 0.0, 0.0) + + # 找到3D视图区域 + view3d_area = None + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + view3d_area = area + break + + if view3d_area is None: + print("错误: 找不到3D视图区域") + return False + + # 临时切换到3D视图上下文 + with bpy.context.temp_override(area=view3d_area): + # 创建窗户 + result = bpy.ops.mesh.archimesh_winpanel() + + if result == {'FINISHED'}: + print("archimesh落地窗创建完成!") + + # 等待一帧以确保对象创建完成 + bpy.context.view_layer.update() + + # 找到刚创建的窗户组对象(Window_Group) + window_group = None + for obj in bpy.data.objects: + if obj.name == "Window_Group": + window_group = obj + break + + if window_group: + print(f"找到窗户组: {window_group.name}") + + # 查找主窗户对象(Window) + main_window_obj = None + for obj in bpy.data.objects: + if obj.name == "Window": + main_window_obj = obj + break + + if main_window_obj and hasattr(main_window_obj, 'WindowPanelGenerator'): + # 获取窗户属性 + window_props = main_window_obj.WindowPanelGenerator[0] + + print("设置窗户属性...") + print(f"找到主窗户对象: {main_window_obj.name}") + + # 设置窗户的基本属性 + window_props.gen = 4 # 4扇窗户(水平) + window_props.yuk = 1 # 1行(垂直) + window_props.kl1 = 5 # 外框厚度5cm + window_props.kl2 = 5 # 竖框厚度5cm + window_props.fk = 2 # 内框厚度2cm + + # 设置窗户尺寸 + window_props.gnx0 = 80 # 第1扇窗户宽度80cm + window_props.gnx1 = 80 # 第2扇窗户宽度80cm + window_props.gnx2 = 80 # 第3扇窗户宽度80cm + window_props.gnx3 = 80 # 第4扇窗户宽度80cm + window_props.gny0 = 250 # 窗户高度250cm(落地窗) + + # 设置窗户类型为平窗 + window_props.UST = '1' # 平窗 + window_props.r = 0 # 旋转角度0度 + + # 设置窗台 + window_props.mr = True # 启用窗台 + window_props.mr1 = 4 # 窗台高度4cm + window_props.mr2 = 4 # 窗台深度4cm + window_props.mr3 = 20 # 窗台宽度20cm + window_props.mr4 = 0 # 窗台延伸0cm + + # 设置材质 + window_props.mt1 = '1' # 外框材质PVC + window_props.mt2 = '1' # 内框材质塑料 + + # 设置窗户开启状态 + window_props.k00 = True # 第1扇窗户开启 + window_props.k01 = True # 第2扇窗户开启 + window_props.k02 = True # 第3扇窗户开启 + window_props.k03 = True # 第4扇窗户开启(如果有的话) + + print(f"窗户属性设置完成:") + print(f" - 水平扇数: {window_props.gen}") + print(f" - 垂直行数: {window_props.yuk}") + print(f" - 外框厚度: {window_props.kl1}cm") + print(f" - 竖框厚度: {window_props.kl2}cm") + print(f" - 内框厚度: {window_props.fk}cm") + print(f" - 窗户高度: {window_props.gny0}cm") + print(f" - 窗户类型: 平窗") + print(f" - 窗台: 启用") + print(f" - 窗户开启状态: k00={window_props.k00}, k01={window_props.k01}, k02={window_props.k02}, k03={window_props.k03}") + + + # 修复窗户玻璃材质 + print("修复窗户玻璃材质...") + fix_window_glass_material() + + # 等待更新完成 + bpy.context.view_layer.update() + + # 强制触发更新(如果需要) + try: + # 重新设置属性以触发更新 + current_gen = window_props.gen + current_gny0 = window_props.gny0 + + window_props.gen = 2 # 临时设置为其他值 + bpy.context.view_layer.update() + + window_props.gen = current_gen # 设置回目标值 + bpy.context.view_layer.update() + + print("已强制触发窗户更新") + + except Exception as update_error: + print(f"强制更新时出现错误: {update_error}") + + # 在修改落地窗全部属性后进行位移和旋转 + print("开始移动和旋转窗户组...") + try: + window_group1 = None + for obj in bpy.data.objects: + if obj.name == "Window_Group": + window_group1 = obj + break + # 设置位置 + window_group1.location = (0, 4.0, 0.0) + print(f"已将窗户组移动到位置: {window_group1.location}") + + # 设置旋转 + window_group1.rotation_euler = (math.radians(0), math.radians(0), math.radians(90)) # 旋转(0, 0, 90度) + print(f"已将窗户组旋转到: (0°, 0°, 90°)") + + # 强制更新 + bpy.context.view_layer.update() + print("窗户组位移和旋转完成") + + except Exception as move_error: + print(f"移动和旋转窗户组时出现错误: {move_error}") + + else: + print("警告: 未找到主窗户对象或WindowPanelGenerator属性") + # 尝试查找其他可能的窗户属性 + for obj in bpy.data.objects: + if hasattr(obj, 'archimesh_window'): + print(f"找到archimesh_window属性在对象: {obj.name}") + + # 打印窗户组的所有子对象信息 + print("窗户组包含的对象:") + for obj in bpy.data.objects: + if obj.parent == window_group: + print(f" - {obj.name} (位置: {obj.location})") + + return True + else: + print("警告: 未找到Window_Group对象") + # 尝试查找其他可能的窗户对象 + window_objects = [obj for obj in bpy.data.objects if "Window" in obj.name] + if window_objects: + print("找到的窗户相关对象:") + for obj in window_objects: + print(f" - {obj.name} (类型: {obj.type})") + + # 尝试移动第一个找到的窗户对象 + first_window = window_objects[0] + first_window.location = (0.0, 1.96, 0.0) + print(f"已移动 {first_window.name} 到位置: {first_window.location}") + return True + else: + print("未找到任何窗户相关对象") + return False + + + + else: + print("窗户创建失败") + return False + + except Exception as e: + print(f"创建窗户时出现错误: {str(e)}") + import traceback + traceback.print_exc() + return False + + +def fix_window_glass_material(): + """修复窗户玻璃材质,将其改为高透BSDF""" + try: + print("开始修复窗户玻璃材质...") + + # 查找Window_Group下的Window对象 + window_group = None + for obj in bpy.data.objects: + if obj.name == "Window_Group": + window_group = obj + break + + if not window_group: + print("未找到Window_Group对象") + return False + + # 查找Window对象(Window_Group的子对象) + window_obj = None + + for obj in bpy.data.objects: + if obj.name == "Window" and obj.parent == window_group: + window_obj = obj + break + + if not window_obj: + print("未找到Window对象") + return False + + print(f"找到Window对象: {window_obj.name}") + + # 检查Window对象的材质 + if not window_obj.material_slots: + print("Window对象没有材质槽") + return False + + # 遍历所有材质槽 + for slot in window_obj.material_slots: + if slot.material and "Glass" in slot.material.name: + print(f"找到Glass材质: {slot.material.name}") + + # 启用节点编辑 + slot.material.use_nodes = True + + # 清除所有现有节点 + slot.material.node_tree.nodes.clear() + + # 创建半透BSDF节点 + translucent_bsdf = slot.material.node_tree.nodes.new(type='ShaderNodeBsdfTranslucent') + translucent_bsdf.location = (0, 0) + + # 设置半透BSDF参数 + translucent_bsdf.inputs['Color'].default_value = (0.95, 0.98, 1.0, 1.0) # 几乎无色 + # 半透BSDF节点没有Weight参数,只有Color参数 + + # 创建材质输出节点 + material_output = slot.material.node_tree.nodes.new(type='ShaderNodeOutputMaterial') + material_output.location = (300, 0) + + # 连接节点 + slot.material.node_tree.links.new( + translucent_bsdf.outputs['BSDF'], + material_output.inputs['Surface'] + ) + + # 设置材质混合模式为Alpha Blend + slot.material.blend_method = 'BLEND' + + print(f"已成功修改Glass材质为半透BSDF") + print(f" - 半透颜色: 几乎无色") + print(f" - 混合模式: Alpha Blend") + + return True + + print("未找到Glass材质") + return False + + except Exception as e: + print(f"修复玻璃材质时出现错误: {str(e)}") + import traceback + traceback.print_exc() + return False + +def setup_render_settings(): + """设置渲染参数(EEVEE渲染,优化速度)""" + scene = bpy.context.scene + + # 设置渲染引擎为EEVEE + scene.render.engine = 'BLENDER_EEVEE_NEXT' + print("已设置渲染引擎为EEVEE") + + # 设置世界环境 + 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 = 7.0 # 强度7 + + # 创建世界输出节点 + 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" - 强度: 7.0") + + # 设置EEVEE采样(降低采样数提高速度) + try: + if hasattr(scene.eevee, 'taa_render_samples'): + scene.eevee.taa_render_samples = 32 # 从64降低到32 + print(f"已设置EEVEE渲染采样: {scene.eevee.taa_render_samples}") + elif hasattr(scene.eevee, 'taa_samples'): + scene.eevee.taa_samples = 32 # 从64降低到32 + print(f"已设置EEVEE采样: {scene.eevee.taa_samples}") + else: + print("警告: 无法找到EEVEE采样设置,使用默认值") + + # 启用屏幕空间反射(简化设置) + if hasattr(scene.eevee, 'use_ssr'): + scene.eevee.use_ssr = True + print("已启用屏幕空间反射") + + # 启用环境光遮蔽(简化设置) + if hasattr(scene.eevee, 'use_gtao'): + scene.eevee.use_gtao = True + print("已启用环境光遮蔽") + + # 启用透明渲染(必要) + if hasattr(scene.eevee, 'use_transparent'): + scene.eevee.use_transparent = True + print("已启用透明渲染") + + # 设置透明混合模式(必要) + if hasattr(scene.render, 'film_transparent'): + scene.render.film_transparent = True + print("已启用透明背景") + + except AttributeError as e: + print(f"EEVEE设置警告: {e}") + print("使用默认EEVEE渲染设置") + + # 设置分辨率 + scene.render.resolution_x = 1080 # 宽度:1080px + scene.render.resolution_y = 2400 # 高度:2400px + scene.render.resolution_percentage = 100 + + # 设置输出格式 + scene.render.image_settings.file_format = 'PNG' + scene.render.image_settings.color_mode = 'RGBA' # 支持透明 + + # 设置输出路径到桌面 + desktop_path = os.path.join(os.path.expanduser("~"), "Desktop") + timestamp = time.strftime("%m%d_%H%M%S") # 日期_时分秒格式 + filename = f"isometric_{timestamp}.png" + scene.render.filepath = os.path.join(desktop_path, filename) + + print(f"EEVEE渲染设置完成,输出路径: {scene.render.filepath}") + return scene.render.filepath + +def setup_camera_and_lighting(): + """设置摄像机和照明(160W点光源 + 150W日光)""" + # 计算灯光高度(房间高度减一米) + room_height = 3.0 + light_height = room_height - 1.0 + + # 设置摄像机 + camera = None + if "Camera" in bpy.data.objects: + camera = bpy.data.objects["Camera"] + elif "Isometric Camera" in bpy.data.objects: + camera = bpy.data.objects["Isometric Camera"] + else: + # 创建新摄像机 + bpy.ops.object.camera_add(location=(7, -7, 5)) + camera = bpy.context.active_object + camera.name = "Isometric Camera" + + # 设置摄像机位置和旋转 + # 房间宽度5.0米,相机位置调整为(房间宽度-1, 1, 1.3) + room_width = 5.0 + camera.location = (room_width - 1, 1, 1.3) # 相机位置:(4, 1, 1.3) + camera.rotation_euler = (math.radians(90), math.radians(0), math.radians(45)) # 相机旋转 + + # 设置为透视投影 + camera.data.type = 'PERSP' + camera.data.lens = 35.0 # 35mm焦距 + camera.data.sensor_width = 35.0 # 35mm传感器 + camera.data.sensor_fit = 'AUTO' # 自动适配 + + # 设置为场景的活动摄像机 + bpy.context.scene.camera = camera + print("摄像机设置完成") + print(f"相机位置: ({camera.location.x}, {camera.location.y}, {camera.location.z})") + + # 隐藏ISO Emission灯光(如果存在) + light_objects = ["ISO Emission Left", "ISO Emission Right"] + for obj_name in light_objects: + if obj_name in bpy.data.objects: + light_obj = bpy.data.objects[obj_name] + # 隐藏发光对象 + light_obj.hide_render = True + light_obj.hide_viewport = True + print(f"已隐藏 {obj_name}") + + # 创建第一个160W点光源(房间中心) + # try: + # # 计算点光源位置(房间中心上方) + # room_length = 4.0 # Y轴 + # room_width = 8.0 # X轴(4+4) + + # # 第一个点光源位置:房间中心上方 + # light_x = 0 # X轴中心 + # light_y = 0 # Y轴中心 + # light_z = light_height + + # # 创建第一个点光源 + # bpy.ops.object.light_add(type='POINT', location=(light_x, light_y, light_z)) + # point_light1 = bpy.context.active_object + # point_light1.name = "Main Point Light" + # point_light1.data.energy = 160 # 160W + + # # 设置软衰减(简化设置) + # point_light1.data.shadow_soft_size = 0.5 # 0.5米半径,产生柔和阴影 + # point_light1.data.use_shadow = True # 启用阴影 + + # print(f"已创建第一个点光源(160W)") + # print(f"点光源位置: x={light_x}, y={light_y}, z={light_z}") + + # except Exception as e: + # print(f"创建第一个点光源时出错: {e}") + + # 创建第二个150W日光(位置12, 7, 6) + try: + # 第二个光源位置 - 调整到窗户后方更远的位置 + light2_x = -6 + light2_y = 7 # 让日光更远 + light2_z = 6 # 提高高度 + + # 创建日光 + bpy.ops.object.light_add(type='SUN', location=(light2_x, light2_y, light2_z)) + sun_light = bpy.context.active_object + sun_light.name = "Sun Light" + sun_light.data.energy = 20 + + # 调整日光旋转角度,让光线更直接地照射窗户 + sun_light.rotation_euler = (math.radians(0), math.radians(-60), math.radians(0)) # 简化旋转角度 + + # 设置日光属性(简化设置) + sun_light.data.angle = 0.05 # 从0.1改为0.05,让阴影更锐利 + sun_light.data.use_shadow = True # 启用阴影 + + print(f"已创建日光(20W)") + print(f"日光位置: x={light2_x}, y={light2_y}, z={light2_z}") + print(f"日光旋转: x={-70}°, y={0}°, z={0}°") + + except Exception as e: + print(f"创建日光时出错: {e}") + + print("照明设置完成") + print("灯光类型: 点光源 + 日光") + print(f"衰减类型: 点光源软衰减 + 日光平行光") + print(f"阴影设置: 启用,柔和阴影") + print(f"主灯光高度: {light_height}米 (房间高度减一米)") + print(f"日光位置: (0, 10, 6)米,旋转: (-70°, 0°, 0°)") + +def render_scene(): + """渲染场景""" + print("开始EEVEE渲染...") + + # 执行渲染 + bpy.ops.render.render(write_still=True) + print(f"EEVEE渲染完成! 图片保存在: {bpy.context.scene.render.filepath}") + return bpy.context.scene.render.filepath + +def main(): + """主函数""" + print("=" * 60) + print("开始自动创建等轴测房间并EEVEE渲染") + print("=" * 60) + + try: + # 1. 创建房间 + print("1. 创建等轴测房间...") + if not create_isometric_room(): + print("房间创建失败,停止执行") + return False + + # 2. 创建落地窗 + print("2. 创建archimesh落地窗...") + if not create_window_with_archimesh(): + print("落地窗创建失败,但继续执行") + # 不中断执行,窗户创建失败不影响渲染 + + # 3. 设置渲染参数(EEVEE) + print("3. 设置EEVEE渲染参数...") + output_path = setup_render_settings() + + # 4. 设置摄像机和照明 + print("4. 设置摄像机和照明...") + setup_camera_and_lighting() + + # 5. 渲染场景 + print("5. 开始EEVEE渲染...") + final_path = render_scene() + + print("=" * 60) + print("EEVEE渲染完成!") + print(f"渲染图片已保存到桌面: {final_path}") + print("=" * 60) + + return True + + except Exception as e: + print(f"执行过程中出现错误: {str(e)}") + import traceback + traceback.print_exc() + return False + +# 执行主函数 +if __name__ == "__main__": + main() +''' + + +def check_blender_exists(): + """检查Blender是否存在于指定路径""" + if not os.path.exists(BLENDER_PATH): + print(f"错误: 在路径 '{BLENDER_PATH}' 找不到Blender") + print("请检查Blender安装路径是否正确") + return False + return True + + +def create_temp_script(): + """创建临时的Blender脚本文件""" + script_path = os.path.join(os.getcwd(), "temp_simple_blender_script.py") + with open(script_path, 'w', encoding='utf-8') as f: + f.write(BLENDER_SIMPLE_SCRIPT) + return script_path + + +def launch_blender_simple(): + """启动Blender并执行简化版自动化脚本""" + if not check_blender_exists(): + return False + + # 创建临时脚本文件 + script_path = create_temp_script() + + try: + print("正在启动Blender(白模版本)...") + print(f"Blender路径: {BLENDER_PATH}") + print("特点:") + print("- 不设置材质,使用默认白色材质") + print("- 快速渲染") + print("- 兼容Blender 4.2") + print("- 自动保存到桌面") + + # 构建命令行参数 + cmd = [ + BLENDER_PATH, + "--background", # 后台运行模式 + "--disable-crash-handler", # 禁用崩溃处理器 + "--python", script_path # 执行Python脚本 + ] + + print("\n开始执行白模渲染流程...") + + # 启动Blender并等待完成 + process = subprocess.run( + cmd, + capture_output=True, + text=True, + encoding='utf-8', + timeout=300 # 5分钟超时 + ) + + # 输出结果 + if process.stdout: + print("Blender输出:") + # 过滤掉重复的错误信息 + lines = process.stdout.split('\n') + filtered_lines = [] + for line in lines: + if not ("AttributeError: 'NoneType' object has no attribute 'idname'" in line or + "Error in bpy.app.handlers.depsgraph_update_post" in line): + filtered_lines.append(line) + print('\n'.join(filtered_lines)) + + if process.stderr and not "rotate_tool.py" in process.stderr: + print("重要错误信息:") + print(process.stderr) + + if process.returncode == 0: + print("\n✅ 白模渲染执行成功!") + print("白模图片应该已经保存到桌面") + return True + else: + print(f"\n❌ 执行失败,返回码: {process.returncode}") + return False + + except subprocess.TimeoutExpired: + print("\n⏰ 执行超时(5分钟),可能Blender仍在运行") + print("请检查桌面是否有生成的图片") + return False + except Exception as e: + print(f"启动Blender时出现错误: {str(e)}") + return False + + finally: + # 清理临时文件 + try: + if os.path.exists(script_path): + os.remove(script_path) + print(f"已清理临时文件: {script_path}") + except: + pass + + +def main(): + """主函数""" + print("=" * 70) + print("Blender 4.2兼容版自动化脚本 - 白模渲染") + print("=" * 70) + print("特点:") + print("✓ 移除所有材质设置") + print("✓ 使用默认白色材质") + print("✓ 快速渲染") + print("✓ 兼容Blender 4.2") + print("✓ 自动保存到桌面") + print("=" * 70) + + success = launch_blender_simple() + + if success: + print("\n🎉 白模渲染完成!") + print("请检查桌面上的渲染图片") + print("图片文件名格式: isometric_room_white_[时间戳].png") + else: + print("\n❌ 执行失败!") + print("可能的解决方案:") + print("1. 确保Isometquick插件已正确安装") + print("2. 禁用可能冲突的其他插件") + print("3. 检查磁盘空间是否足够") + + +if __name__ == "__main__": + main() diff --git a/auto_room copy.py b/auto_room copy.py new file mode 100644 index 0000000..7f4a17f --- /dev/null +++ b/auto_room copy.py @@ -0,0 +1,775 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Blender 4.2兼容版自动化脚本(白模版本) +自动创建等轴测房间并渲染白模 +""" + +import subprocess +import os +import sys + +# Blender可执行文件路径 +BLENDER_PATH = r"D:\Program Files\Blender Foundation\blender-4.2.11-windows-x64\blender.exe" + +# 简化版Blender脚本(只渲染白模) +BLENDER_SIMPLE_SCRIPT = ''' +import bpy +import bmesh +import os +import math +import time + +def disable_problematic_addons(): + """禁用可能导致问题的插件""" + try: + # 禁用可能冲突的插件 + problematic_addons = ['bl_tool'] + for addon in problematic_addons: + if addon in bpy.context.preferences.addons: + bpy.ops.preferences.addon_disable(module=addon) + print(f"已禁用插件: {addon}") + except: + pass + +def create_isometric_room(): + """创建等轴测房间,使用isometric_room_gen插件""" + try: + # 禁用问题插件 + #disable_problematic_addons() + + # 清除默认立方体 + if "Cube" in bpy.data.objects: + bpy.data.objects.remove(bpy.data.objects["Cube"], do_unlink=True) + + # 设置3D游标位置到原点 + bpy.context.scene.cursor.location = (0.0, 0.0, 0.0) + + # 检查isometric_room_gen插件 + if not hasattr(bpy.context.scene, 'sna_room_width'): + print("错误: isometric_room_gen插件未正确加载") + return False + + # 设置isometric_room_gen插件参数 + # 房间设置 + bpy.context.scene.sna_wall_thickness = 0.10 # 墙体厚度10cm + bpy.context.scene.sna_room_width = 5.0 # 房间宽度8米 + bpy.context.scene.sna_room_depth = 8.0 # 房间深度4米 + bpy.context.scene.sna_room_height = 3.0 # 房间高度3米 + + # 窗户设置 - 不设置窗户 + bpy.context.scene.sna_windows_enum = 'NONE' # 无窗户 + + # 设置房间类型为方形 + bpy.context.scene.sna_style = 'Square' + + # 找到3D视图区域 + view3d_area = None + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + view3d_area = area + break + + if view3d_area is None: + print("错误: 找不到3D视图区域") + return False + + # 临时切换到3D视图上下文 + with bpy.context.temp_override(area=view3d_area): + # 使用isometric_room_gen插件创建房间 + result = bpy.ops.sna.gen_room_1803a() + + if result == {'FINISHED'}: + print("等轴测房间创建完成!") + print(f"房间尺寸: {bpy.context.scene.sna_room_width}m x {bpy.context.scene.sna_room_depth}m x {bpy.context.scene.sna_room_height}m") + print(f"墙体厚度: {bpy.context.scene.sna_wall_thickness}m") + + # 将生成的房间在Z轴上向下移动墙体厚度的距离 + wall_thickness = bpy.context.scene.sna_wall_thickness + + # 查找生成的房间对象 + room_object = None + for obj in bpy.data.objects: + if obj.name == "IRG_IsoRoom_WithCeiling": + room_object = obj + break + + if room_object: + # 获取当前位置 + current_location = room_object.location.copy() + # 在Z轴上向下移动墙体厚度的距离 + room_object.location.z = current_location.z - wall_thickness + print(f"已将房间在Z轴上向下移动 {wall_thickness}m") + print(f"房间新位置: {room_object.location}") + else: + print("警告: 未找到生成的房间对象") + + return True + else: + print("房间创建失败") + return False + + except Exception as e: + print(f"创建房间时出现错误: {str(e)}") + import traceback + traceback.print_exc() + return False + +def create_window_with_archimesh(): + """使用archimesh插件创建落地窗""" + try: + print("开始创建archimesh落地窗...") + + # 检查archimesh插件是否可用 + if not hasattr(bpy.ops.mesh, 'archimesh_winpanel'): + print("错误: archimesh插件未正确加载") + print("请确保archimesh插件已安装并启用") + return False + + # 设置3D游标到原点 + bpy.context.scene.cursor.location = (0.0, 0.0, 0.0) + + # 找到3D视图区域 + view3d_area = None + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + view3d_area = area + break + + if view3d_area is None: + print("错误: 找不到3D视图区域") + return False + + # 临时切换到3D视图上下文 + with bpy.context.temp_override(area=view3d_area): + # 创建窗户 + result = bpy.ops.mesh.archimesh_winpanel() + + if result == {'FINISHED'}: + print("archimesh落地窗创建完成!") + + # 等待一帧以确保对象创建完成 + bpy.context.view_layer.update() + + # 找到刚创建的窗户组对象(Window_Group) + window_group = None + for obj in bpy.data.objects: + if obj.name == "Window_Group": + window_group = obj + break + + if window_group: + print(f"找到窗户组: {window_group.name}") + + # 查找主窗户对象(Window) + main_window_obj = None + for obj in bpy.data.objects: + if obj.name == "Window": + main_window_obj = obj + break + + if main_window_obj and hasattr(main_window_obj, 'WindowPanelGenerator'): + # 获取窗户属性 + window_props = main_window_obj.WindowPanelGenerator[0] + + print("设置窗户属性...") + print(f"找到主窗户对象: {main_window_obj.name}") + + # 设置窗户的基本属性 + window_props.gen = 4 # 4扇窗户(水平) + window_props.yuk = 1 # 1行(垂直) + window_props.kl1 = 5 # 外框厚度5cm + window_props.kl2 = 5 # 竖框厚度5cm + window_props.fk = 2 # 内框厚度2cm + + # 设置窗户尺寸 + window_props.gnx0 = 80 # 第1扇窗户宽度80cm + window_props.gnx1 = 80 # 第2扇窗户宽度80cm + window_props.gnx2 = 80 # 第3扇窗户宽度80cm + window_props.gnx3 = 80 # 第4扇窗户宽度80cm + window_props.gny0 = 250 # 窗户高度250cm(落地窗) + + # 设置窗户类型为平窗 + window_props.UST = '1' # 平窗 + window_props.r = 0 # 旋转角度0度 + + # 设置窗台 + window_props.mr = True # 启用窗台 + window_props.mr1 = 4 # 窗台高度4cm + window_props.mr2 = 4 # 窗台深度4cm + window_props.mr3 = 20 # 窗台宽度20cm + window_props.mr4 = 0 # 窗台延伸0cm + + # 设置材质 + window_props.mt1 = '1' # 外框材质PVC + window_props.mt2 = '1' # 内框材质塑料 + + # 设置窗户开启状态 + window_props.k00 = True # 第1扇窗户开启 + window_props.k01 = True # 第2扇窗户开启 + window_props.k02 = True # 第3扇窗户开启 + window_props.k03 = True # 第4扇窗户开启(如果有的话) + + print(f"窗户属性设置完成:") + print(f" - 水平扇数: {window_props.gen}") + print(f" - 垂直行数: {window_props.yuk}") + print(f" - 外框厚度: {window_props.kl1}cm") + print(f" - 竖框厚度: {window_props.kl2}cm") + print(f" - 内框厚度: {window_props.fk}cm") + print(f" - 窗户高度: {window_props.gny0}cm") + print(f" - 窗户类型: 平窗") + print(f" - 窗台: 启用") + print(f" - 窗户开启状态: k00={window_props.k00}, k01={window_props.k01}, k02={window_props.k02}, k03={window_props.k03}") + + + # 修复窗户玻璃材质 + print("修复窗户玻璃材质...") + fix_window_glass_material() + + # 等待更新完成 + bpy.context.view_layer.update() + + # 强制触发更新(如果需要) + try: + # 重新设置属性以触发更新 + current_gen = window_props.gen + current_gny0 = window_props.gny0 + + window_props.gen = 2 # 临时设置为其他值 + bpy.context.view_layer.update() + + window_props.gen = current_gen # 设置回目标值 + bpy.context.view_layer.update() + + print("已强制触发窗户更新") + + except Exception as update_error: + print(f"强制更新时出现错误: {update_error}") + + # 在修改落地窗全部属性后进行位移和旋转 + print("开始移动和旋转窗户组...") + try: + window_group1 = None + for obj in bpy.data.objects: + if obj.name == "Window_Group": + window_group1 = obj + break + # 设置位置 + window_group1.location = (5, 4.0, 0.0) + print(f"已将窗户组移动到位置: {window_group1.location}") + + # 设置旋转 + window_group1.rotation_euler = (math.radians(0), math.radians(0), math.radians(90)) # 旋转(0, 0, 90度) + print(f"已将窗户组旋转到: (0°, 0°, 90°)") + + # 强制更新 + bpy.context.view_layer.update() + print("窗户组位移和旋转完成") + + except Exception as move_error: + print(f"移动和旋转窗户组时出现错误: {move_error}") + + else: + print("警告: 未找到主窗户对象或WindowPanelGenerator属性") + # 尝试查找其他可能的窗户属性 + for obj in bpy.data.objects: + if hasattr(obj, 'archimesh_window'): + print(f"找到archimesh_window属性在对象: {obj.name}") + + # 打印窗户组的所有子对象信息 + print("窗户组包含的对象:") + for obj in bpy.data.objects: + if obj.parent == window_group: + print(f" - {obj.name} (位置: {obj.location})") + + return True + else: + print("警告: 未找到Window_Group对象") + # 尝试查找其他可能的窗户对象 + window_objects = [obj for obj in bpy.data.objects if "Window" in obj.name] + if window_objects: + print("找到的窗户相关对象:") + for obj in window_objects: + print(f" - {obj.name} (类型: {obj.type})") + + # 尝试移动第一个找到的窗户对象 + first_window = window_objects[0] + first_window.location = (0.0, 1.96, 0.0) + print(f"已移动 {first_window.name} 到位置: {first_window.location}") + return True + else: + print("未找到任何窗户相关对象") + return False + + + + else: + print("窗户创建失败") + return False + + except Exception as e: + print(f"创建窗户时出现错误: {str(e)}") + import traceback + traceback.print_exc() + return False + + +def fix_window_glass_material(): + """修复窗户玻璃材质,将其改为高透BSDF""" + try: + print("开始修复窗户玻璃材质...") + + # 查找Window_Group下的Window对象 + window_group = None + for obj in bpy.data.objects: + if obj.name == "Window_Group": + window_group = obj + break + + if not window_group: + print("未找到Window_Group对象") + return False + + # 查找Window对象(Window_Group的子对象) + window_obj = None + for obj in bpy.data.objects: + if obj.name == "Window" and obj.parent == window_group: + window_obj = obj + break + + if not window_obj: + print("未找到Window对象") + return False + + print(f"找到Window对象: {window_obj.name}") + + # 检查Window对象的材质 + if not window_obj.material_slots: + print("Window对象没有材质槽") + return False + + # 遍历所有材质槽 + for slot in window_obj.material_slots: + if slot.material and "Glass" in slot.material.name: + print(f"找到Glass材质: {slot.material.name}") + + # 启用节点编辑 + slot.material.use_nodes = True + + # 清除所有现有节点 + slot.material.node_tree.nodes.clear() + + # 创建半透BSDF节点 + translucent_bsdf = slot.material.node_tree.nodes.new(type='ShaderNodeBsdfTranslucent') + translucent_bsdf.location = (0, 0) + + # 设置半透BSDF参数 + translucent_bsdf.inputs['Color'].default_value = (0.95, 0.98, 1.0, 1.0) # 几乎无色 + # 半透BSDF节点没有Weight参数,只有Color参数 + + # 创建材质输出节点 + material_output = slot.material.node_tree.nodes.new(type='ShaderNodeOutputMaterial') + material_output.location = (300, 0) + + # 连接节点 + slot.material.node_tree.links.new( + translucent_bsdf.outputs['BSDF'], + material_output.inputs['Surface'] + ) + + # 设置材质混合模式为Alpha Blend + slot.material.blend_method = 'BLEND' + + print(f"已成功修改Glass材质为半透BSDF") + print(f" - 半透颜色: 几乎无色") + print(f" - 混合模式: Alpha Blend") + + return True + + print("未找到Glass材质") + return False + + except Exception as e: + print(f"修复玻璃材质时出现错误: {str(e)}") + import traceback + traceback.print_exc() + return False + +def setup_render_settings(): + """设置渲染参数(EEVEE渲染,优化速度)""" + scene = bpy.context.scene + + # 设置渲染引擎为EEVEE + scene.render.engine = 'BLENDER_EEVEE_NEXT' + print("已设置渲染引擎为EEVEE") + + # 设置世界环境 + 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 = 7.0 # 强度7 + + # 创建世界输出节点 + 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" - 强度: 7.0") + + # 设置EEVEE采样(降低采样数提高速度) + try: + if hasattr(scene.eevee, 'taa_render_samples'): + scene.eevee.taa_render_samples = 32 # 从64降低到32 + print(f"已设置EEVEE渲染采样: {scene.eevee.taa_render_samples}") + elif hasattr(scene.eevee, 'taa_samples'): + scene.eevee.taa_samples = 32 # 从64降低到32 + print(f"已设置EEVEE采样: {scene.eevee.taa_samples}") + else: + print("警告: 无法找到EEVEE采样设置,使用默认值") + + # 启用屏幕空间反射(简化设置) + if hasattr(scene.eevee, 'use_ssr'): + scene.eevee.use_ssr = True + print("已启用屏幕空间反射") + + # 启用环境光遮蔽(简化设置) + if hasattr(scene.eevee, 'use_gtao'): + scene.eevee.use_gtao = True + print("已启用环境光遮蔽") + + # 启用透明渲染(必要) + if hasattr(scene.eevee, 'use_transparent'): + scene.eevee.use_transparent = True + print("已启用透明渲染") + + # 设置透明混合模式(必要) + if hasattr(scene.render, 'film_transparent'): + scene.render.film_transparent = True + print("已启用透明背景") + + except AttributeError as e: + print(f"EEVEE设置警告: {e}") + print("使用默认EEVEE渲染设置") + + # 设置分辨率 + scene.render.resolution_x = 1080 # 宽度:1080px + scene.render.resolution_y = 2400 # 高度:2400px + scene.render.resolution_percentage = 100 + + # 设置输出格式 + scene.render.image_settings.file_format = 'PNG' + scene.render.image_settings.color_mode = 'RGBA' # 支持透明 + + # 设置输出路径到桌面 + desktop_path = os.path.join(os.path.expanduser("~"), "Desktop") + timestamp = time.strftime("%m%d_%H%M%S") # 日期_时分秒格式 + filename = f"isometric_{timestamp}.png" + scene.render.filepath = os.path.join(desktop_path, filename) + + print(f"EEVEE渲染设置完成,输出路径: {scene.render.filepath}") + return scene.render.filepath + +def setup_camera_and_lighting(): + """设置摄像机和照明(160W点光源 + 150W日光)""" + # 计算灯光高度(房间高度减一米) + room_height = 3.0 + light_height = room_height - 1.0 + + # 设置摄像机 + camera = None + if "Camera" in bpy.data.objects: + camera = bpy.data.objects["Camera"] + elif "Isometric Camera" in bpy.data.objects: + camera = bpy.data.objects["Isometric Camera"] + else: + # 创建新摄像机 + bpy.ops.object.camera_add(location=(7, -7, 5)) + camera = bpy.context.active_object + camera.name = "Isometric Camera" + + # 设置摄像机位置和旋转 + camera.location = (1, 1, 1.3) # 相机位置 + camera.rotation_euler = (math.radians(90), math.radians(0), math.radians(45)) # 相机旋转 + + # 设置为透视投影 + 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("摄像机设置完成") + + # 隐藏ISO Emission灯光(如果存在) + light_objects = ["ISO Emission Left", "ISO Emission Right"] + for obj_name in light_objects: + if obj_name in bpy.data.objects: + light_obj = bpy.data.objects[obj_name] + # 隐藏发光对象 + light_obj.hide_render = True + light_obj.hide_viewport = True + print(f"已隐藏 {obj_name}") + + # 创建第一个160W点光源(房间中心) + # try: + # # 计算点光源位置(房间中心上方) + # room_length = 4.0 # Y轴 + # room_width = 8.0 # X轴(4+4) + + # # 第一个点光源位置:房间中心上方 + # light_x = 0 # X轴中心 + # light_y = 0 # Y轴中心 + # light_z = light_height + + # # 创建第一个点光源 + # bpy.ops.object.light_add(type='POINT', location=(light_x, light_y, light_z)) + # point_light1 = bpy.context.active_object + # point_light1.name = "Main Point Light" + # point_light1.data.energy = 160 # 160W + + # # 设置软衰减(简化设置) + # point_light1.data.shadow_soft_size = 0.5 # 0.5米半径,产生柔和阴影 + # point_light1.data.use_shadow = True # 启用阴影 + + # print(f"已创建第一个点光源(160W)") + # print(f"点光源位置: x={light_x}, y={light_y}, z={light_z}") + + # except Exception as e: + # print(f"创建第一个点光源时出错: {e}") + + # 创建第二个150W日光(位置12, 7, 6) + try: + # 第二个光源位置 - 调整到窗户后方更远的位置 + light2_x = 12 + light2_y = 7 # 让日光更远 + light2_z = 6 # 提高高度 + + # 创建日光 + bpy.ops.object.light_add(type='SUN', location=(light2_x, light2_y, light2_z)) + sun_light = bpy.context.active_object + sun_light.name = "Sun Light" + sun_light.data.energy = 20 + + # 调整日光旋转角度,让光线更直接地照射窗户 + sun_light.rotation_euler = (math.radians(0), math.radians(60), math.radians(0)) # 简化旋转角度 + + # 设置日光属性(简化设置) + sun_light.data.angle = 0.05 # 从0.1改为0.05,让阴影更锐利 + sun_light.data.use_shadow = True # 启用阴影 + + print(f"已创建日光(20W)") + print(f"日光位置: x={light2_x}, y={light2_y}, z={light2_z}") + print(f"日光旋转: x={-70}°, y={0}°, z={0}°") + + except Exception as e: + print(f"创建日光时出错: {e}") + + print("照明设置完成") + print("灯光类型: 点光源 + 日光") + print(f"衰减类型: 点光源软衰减 + 日光平行光") + print(f"阴影设置: 启用,柔和阴影") + print(f"主灯光高度: {light_height}米 (房间高度减一米)") + print(f"日光位置: (0, 10, 6)米,旋转: (-70°, 0°, 0°)") + +def render_scene(): + """渲染场景""" + print("开始EEVEE渲染...") + + # 执行渲染 + bpy.ops.render.render(write_still=True) + print(f"EEVEE渲染完成! 图片保存在: {bpy.context.scene.render.filepath}") + return bpy.context.scene.render.filepath + +def main(): + """主函数""" + print("=" * 60) + print("开始自动创建等轴测房间并EEVEE渲染") + print("=" * 60) + + try: + # 1. 创建房间 + print("1. 创建等轴测房间...") + if not create_isometric_room(): + print("房间创建失败,停止执行") + return False + + # 2. 创建落地窗 + print("2. 创建archimesh落地窗...") + if not create_window_with_archimesh(): + print("落地窗创建失败,但继续执行") + # 不中断执行,窗户创建失败不影响渲染 + + # 3. 设置渲染参数(EEVEE) + print("3. 设置EEVEE渲染参数...") + output_path = setup_render_settings() + + # 4. 设置摄像机和照明 + print("4. 设置摄像机和照明...") + setup_camera_and_lighting() + + # 5. 渲染场景 + print("5. 开始EEVEE渲染...") + final_path = render_scene() + + print("=" * 60) + print("EEVEE渲染完成!") + print(f"渲染图片已保存到桌面: {final_path}") + print("=" * 60) + + return True + + except Exception as e: + print(f"执行过程中出现错误: {str(e)}") + import traceback + traceback.print_exc() + return False + +# 执行主函数 +if __name__ == "__main__": + main() +''' + + +def check_blender_exists(): + """检查Blender是否存在于指定路径""" + if not os.path.exists(BLENDER_PATH): + print(f"错误: 在路径 '{BLENDER_PATH}' 找不到Blender") + print("请检查Blender安装路径是否正确") + return False + return True + + +def create_temp_script(): + """创建临时的Blender脚本文件""" + script_path = os.path.join(os.getcwd(), "temp_simple_blender_script.py") + with open(script_path, 'w', encoding='utf-8') as f: + f.write(BLENDER_SIMPLE_SCRIPT) + return script_path + + +def launch_blender_simple(): + """启动Blender并执行简化版自动化脚本""" + if not check_blender_exists(): + return False + + # 创建临时脚本文件 + script_path = create_temp_script() + + try: + print("正在启动Blender(白模版本)...") + print(f"Blender路径: {BLENDER_PATH}") + print("特点:") + print("- 不设置材质,使用默认白色材质") + print("- 快速渲染") + print("- 兼容Blender 4.2") + print("- 自动保存到桌面") + + # 构建命令行参数 + cmd = [ + BLENDER_PATH, + "--background", # 后台运行模式 + "--disable-crash-handler", # 禁用崩溃处理器 + "--python", script_path # 执行Python脚本 + ] + + print("\n开始执行白模渲染流程...") + + # 启动Blender并等待完成 + process = subprocess.run( + cmd, + capture_output=True, + text=True, + encoding='utf-8', + timeout=300 # 5分钟超时 + ) + + # 输出结果 + if process.stdout: + print("Blender输出:") + # 过滤掉重复的错误信息 + lines = process.stdout.split('\n') + filtered_lines = [] + for line in lines: + if not ("AttributeError: 'NoneType' object has no attribute 'idname'" in line or + "Error in bpy.app.handlers.depsgraph_update_post" in line): + filtered_lines.append(line) + print('\n'.join(filtered_lines)) + + if process.stderr and not "rotate_tool.py" in process.stderr: + print("重要错误信息:") + print(process.stderr) + + if process.returncode == 0: + print("\n✅ 白模渲染执行成功!") + print("白模图片应该已经保存到桌面") + return True + else: + print(f"\n❌ 执行失败,返回码: {process.returncode}") + return False + + except subprocess.TimeoutExpired: + print("\n⏰ 执行超时(5分钟),可能Blender仍在运行") + print("请检查桌面是否有生成的图片") + return False + except Exception as e: + print(f"启动Blender时出现错误: {str(e)}") + return False + + finally: + # 清理临时文件 + try: + if os.path.exists(script_path): + os.remove(script_path) + print(f"已清理临时文件: {script_path}") + except: + pass + + +def main(): + """主函数""" + print("=" * 70) + print("Blender 4.2兼容版自动化脚本 - 白模渲染") + print("=" * 70) + print("特点:") + print("✓ 移除所有材质设置") + print("✓ 使用默认白色材质") + print("✓ 快速渲染") + print("✓ 兼容Blender 4.2") + print("✓ 自动保存到桌面") + print("=" * 70) + + success = launch_blender_simple() + + if success: + print("\n🎉 白模渲染完成!") + print("请检查桌面上的渲染图片") + print("图片文件名格式: isometric_room_white_[时间戳].png") + else: + print("\n❌ 执行失败!") + print("可能的解决方案:") + print("1. 确保Isometquick插件已正确安装") + print("2. 禁用可能冲突的其他插件") + print("3. 检查磁盘空间是否足够") + + +if __name__ == "__main__": + main() diff --git a/auto_room copy_window.py b/auto_room copy_window.py new file mode 100644 index 0000000..728a1fb --- /dev/null +++ b/auto_room copy_window.py @@ -0,0 +1,773 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Blender 4.2兼容版自动化脚本(白模版本) +自动创建等轴测房间并渲染白模 +""" + +import subprocess +import os +import sys + +# Blender可执行文件路径 +BLENDER_PATH = r"D:\Program Files\Blender Foundation\blender-4.2.11-windows-x64\blender.exe" + +# 简化版Blender脚本(只渲染白模) +BLENDER_SIMPLE_SCRIPT = ''' +import bpy +import bmesh +import os +import math +import time + +def disable_problematic_addons(): + """禁用可能导致问题的插件""" + try: + # 禁用可能冲突的插件 + problematic_addons = ['bl_tool'] + for addon in problematic_addons: + if addon in bpy.context.preferences.addons: + bpy.ops.preferences.addon_disable(module=addon) + print(f"已禁用插件: {addon}") + except: + pass + +def create_isometric_room(): + """创建等轴测房间,使用isometric_room_gen插件""" + try: + # 禁用问题插件 + #disable_problematic_addons() + + # 清除默认立方体 + if "Cube" in bpy.data.objects: + bpy.data.objects.remove(bpy.data.objects["Cube"], do_unlink=True) + + # 设置3D游标位置到原点 + bpy.context.scene.cursor.location = (0.0, 0.0, 0.0) + + # 检查isometric_room_gen插件 + if not hasattr(bpy.context.scene, 'sna_room_width'): + print("错误: isometric_room_gen插件未正确加载") + return False + + # 设置isometric_room_gen插件参数 + # 房间设置 + bpy.context.scene.sna_wall_thickness = 0.10 # 墙体厚度10cm + bpy.context.scene.sna_room_width = 5.0 # 房间宽度8米 + bpy.context.scene.sna_room_depth = 8.0 # 房间深度4米 + bpy.context.scene.sna_room_height = 3.0 # 房间高度3米 + + # 窗户设置 - 不设置窗户 + bpy.context.scene.sna_windows_enum = 'NONE' # 无窗户 + + # 设置房间类型为方形 + bpy.context.scene.sna_style = 'Square' + + # 找到3D视图区域 + view3d_area = None + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + view3d_area = area + break + + if view3d_area is None: + print("错误: 找不到3D视图区域") + return False + + # 临时切换到3D视图上下文 + with bpy.context.temp_override(area=view3d_area): + # 使用isometric_room_gen插件创建房间 + result = bpy.ops.sna.gen_room_1803a() + + if result == {'FINISHED'}: + print("等轴测房间创建完成!") + print(f"房间尺寸: {bpy.context.scene.sna_room_width}m x {bpy.context.scene.sna_room_depth}m x {bpy.context.scene.sna_room_height}m") + print(f"墙体厚度: {bpy.context.scene.sna_wall_thickness}m") + + # 将生成的房间在Z轴上向下移动墙体厚度的距离 + wall_thickness = bpy.context.scene.sna_wall_thickness + + # 查找生成的房间对象 + room_object = None + for obj in bpy.data.objects: + if obj.name == "IRG_IsoRoom_WithCeiling": + room_object = obj + break + + + return True + else: + print("房间创建失败") + return False + + except Exception as e: + print(f"创建房间时出现错误: {str(e)}") + import traceback + traceback.print_exc() + return False + +def create_window_with_archimesh(): + """使用archimesh插件创建落地窗""" + try: + print("开始创建archimesh落地窗...") + + # 检查archimesh插件是否可用 + if not hasattr(bpy.ops.mesh, 'archimesh_winpanel'): + print("错误: archimesh插件未正确加载") + print("请确保archimesh插件已安装并启用") + return False + + # 设置3D游标到原点 + bpy.context.scene.cursor.location = (0.0, 0.0, 0.0) + + # 找到3D视图区域 + view3d_area = None + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + view3d_area = area + break + + if view3d_area is None: + print("错误: 找不到3D视图区域") + return False + + # 临时切换到3D视图上下文 + with bpy.context.temp_override(area=view3d_area): + # 创建窗户 + result = bpy.ops.mesh.archimesh_winpanel() + + if result == {'FINISHED'}: + print("archimesh落地窗创建完成!") + + # 等待一帧以确保对象创建完成 + bpy.context.view_layer.update() + + # 找到刚创建的窗户组对象(Window_Group) + window_group = None + for obj in bpy.data.objects: + if obj.name == "Window_Group": + window_group = obj + break + + if window_group: + print(f"找到窗户组: {window_group.name}") + + # 查找主窗户对象(Window) + main_window_obj = None + for obj in bpy.data.objects: + if obj.name == "Window": + main_window_obj = obj + break + + if main_window_obj and hasattr(main_window_obj, 'WindowPanelGenerator'): + # 获取窗户属性 + window_props = main_window_obj.WindowPanelGenerator[0] + + print("设置窗户属性...") + print(f"找到主窗户对象: {main_window_obj.name}") + + # 设置窗户的基本属性 + window_props.gen = 4 # 4扇窗户(水平) + window_props.yuk = 1 # 1行(垂直) + window_props.kl1 = 5 # 外框厚度5cm + window_props.kl2 = 5 # 竖框厚度5cm + window_props.fk = 2 # 内框厚度2cm + + # 设置窗户尺寸 + window_props.gnx0 = 80 # 第1扇窗户宽度80cm + window_props.gnx1 = 80 # 第2扇窗户宽度80cm + window_props.gnx2 = 80 # 第3扇窗户宽度80cm + window_props.gnx3 = 80 # 第4扇窗户宽度80cm + window_props.gny0 = 250 # 窗户高度250cm(落地窗) + + # 设置窗户类型为平窗 + window_props.UST = '1' # 平窗 + window_props.r = 0 # 旋转角度0度 + + # 设置窗台 + window_props.mr = True # 启用窗台 + window_props.mr1 = 4 # 窗台高度4cm + window_props.mr2 = 4 # 窗台深度4cm + window_props.mr3 = 20 # 窗台宽度20cm + window_props.mr4 = 0 # 窗台延伸0cm + + # 设置材质 + window_props.mt1 = '1' # 外框材质PVC + window_props.mt2 = '1' # 内框材质塑料 + + # 设置窗户开启状态 + window_props.k00 = True # 第1扇窗户开启 + window_props.k01 = True # 第2扇窗户开启 + window_props.k02 = True # 第3扇窗户开启 + window_props.k03 = True # 第4扇窗户开启(如果有的话) + + print(f"窗户属性设置完成:") + print(f" - 水平扇数: {window_props.gen}") + print(f" - 垂直行数: {window_props.yuk}") + print(f" - 外框厚度: {window_props.kl1}cm") + print(f" - 竖框厚度: {window_props.kl2}cm") + print(f" - 内框厚度: {window_props.fk}cm") + print(f" - 窗户高度: {window_props.gny0}cm") + print(f" - 窗户类型: 平窗") + print(f" - 窗台: 启用") + print(f" - 窗户开启状态: k00={window_props.k00}, k01={window_props.k01}, k02={window_props.k02}, k03={window_props.k03}") + + + # 修复窗户玻璃材质 + print("修复窗户玻璃材质...") + fix_window_glass_material() + + # 等待更新完成 + bpy.context.view_layer.update() + + # 强制触发更新(如果需要) + try: + # 重新设置属性以触发更新 + current_gen = window_props.gen + current_gny0 = window_props.gny0 + + window_props.gen = 2 # 临时设置为其他值 + bpy.context.view_layer.update() + + window_props.gen = current_gen # 设置回目标值 + bpy.context.view_layer.update() + + print("已强制触发窗户更新") + + except Exception as update_error: + print(f"强制更新时出现错误: {update_error}") + + # 在修改落地窗全部属性后进行位移和旋转 + print("开始移动和旋转窗户组...") + try: + window_group1 = None + + for obj in bpy.data.objects: + if obj.name == "Window_Group": + window_group1 = obj + break + + # 设置位置 + window_group1.location = (0, 4.0, 0.0) + print(f"已将窗户组移动到位置: {window_group1.location}") + + # 设置旋转 + window_group1.rotation_euler = (math.radians(0), math.radians(0), math.radians(90)) # 旋转(0, 0, 90度) + print(f"已将窗户组旋转到: (0°, 0°, 90°)") + + # 强制更新 + bpy.context.view_layer.update() + print("窗户组位移和旋转完成") + + except Exception as move_error: + print(f"移动和旋转窗户组时出现错误: {move_error}") + + else: + print("警告: 未找到主窗户对象或WindowPanelGenerator属性") + # 尝试查找其他可能的窗户属性 + for obj in bpy.data.objects: + if hasattr(obj, 'archimesh_window'): + print(f"找到archimesh_window属性在对象: {obj.name}") + + # 打印窗户组的所有子对象信息 + print("窗户组包含的对象:") + for obj in bpy.data.objects: + if obj.parent == window_group: + print(f" - {obj.name} (位置: {obj.location})") + + + return True + else: + print("警告: 未找到Window_Group对象") + # 尝试查找其他可能的窗户对象 + window_objects = [obj for obj in bpy.data.objects if "Window" in obj.name] + if window_objects: + print("找到的窗户相关对象:") + for obj in window_objects: + print(f" - {obj.name} (类型: {obj.type})") + + # 尝试移动第一个找到的窗户对象 + first_window = window_objects[0] + first_window.location = (0.0, 1.96, 0.0) + print(f"已移动 {first_window.name} 到位置: {first_window.location}") + return True + else: + print("未找到任何窗户相关对象") + return False + + + + else: + print("窗户创建失败") + return False + + except Exception as e: + print(f"创建窗户时出现错误: {str(e)}") + import traceback + traceback.print_exc() + return False + + +def fix_window_glass_material(): + """修复窗户玻璃材质,将其改为高透BSDF""" + try: + print("开始修复窗户玻璃材质...") + + # 查找Window_Group下的Window对象 + window_group = None + for obj in bpy.data.objects: + if obj.name == "Window_Group": + window_group = obj + break + + if not window_group: + print("未找到Window_Group对象") + return False + + # 查找Window对象(Window_Group的子对象) + window_obj = None + for obj in bpy.data.objects: + if obj.name == "Window" and obj.parent == window_group: + window_obj = obj + break + + + if not window_obj: + print("未找到Window对象") + return False + + print(f"找到Window对象: {window_obj.name}") + + # 检查Window对象的材质 + if not window_obj.material_slots: + print("Window对象没有材质槽") + return False + + # 遍历所有材质槽 + for slot in window_obj.material_slots: + if slot.material and "Glass" in slot.material.name: + print(f"找到Glass材质: {slot.material.name}") + + # 启用节点编辑 + slot.material.use_nodes = True + + # 清除所有现有节点 + slot.material.node_tree.nodes.clear() + + # 创建半透BSDF节点 + translucent_bsdf = slot.material.node_tree.nodes.new(type='ShaderNodeBsdfTranslucent') + translucent_bsdf.location = (0, 0) + + # 设置半透BSDF参数 + translucent_bsdf.inputs['Color'].default_value = (0.95, 0.98, 1.0, 1.0) # 几乎无色 + # 半透BSDF节点没有Weight参数,只有Color参数 + + # 创建材质输出节点 + material_output = slot.material.node_tree.nodes.new(type='ShaderNodeOutputMaterial') + material_output.location = (300, 0) + + # 连接节点 + slot.material.node_tree.links.new( + translucent_bsdf.outputs['BSDF'], + material_output.inputs['Surface'] + ) + + # 设置材质混合模式为Alpha Blend + slot.material.blend_method = 'BLEND' + + print(f"已成功修改Glass材质为半透BSDF") + print(f" - 半透颜色: 几乎无色") + print(f" - 混合模式: Alpha Blend") + + return True + + print("未找到Glass材质") + return False + + except Exception as e: + print(f"修复玻璃材质时出现错误: {str(e)}") + import traceback + traceback.print_exc() + return False + +def setup_render_settings(): + """设置渲染参数(EEVEE渲染,优化速度)""" + scene = bpy.context.scene + + # 设置渲染引擎为EEVEE + scene.render.engine = 'BLENDER_EEVEE_NEXT' + print("已设置渲染引擎为EEVEE") + + # 设置世界环境 + 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 = 7.0 # 强度7 + + # 创建世界输出节点 + 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" - 强度: 7.0") + + # 设置EEVEE采样(降低采样数提高速度) + try: + if hasattr(scene.eevee, 'taa_render_samples'): + scene.eevee.taa_render_samples = 32 # 从64降低到32 + print(f"已设置EEVEE渲染采样: {scene.eevee.taa_render_samples}") + elif hasattr(scene.eevee, 'taa_samples'): + scene.eevee.taa_samples = 32 # 从64降低到32 + print(f"已设置EEVEE采样: {scene.eevee.taa_samples}") + else: + print("警告: 无法找到EEVEE采样设置,使用默认值") + + # 启用屏幕空间反射(简化设置) + if hasattr(scene.eevee, 'use_ssr'): + scene.eevee.use_ssr = True + print("已启用屏幕空间反射") + + # 启用环境光遮蔽(简化设置) + if hasattr(scene.eevee, 'use_gtao'): + scene.eevee.use_gtao = True + print("已启用环境光遮蔽") + + # 启用透明渲染(必要) + if hasattr(scene.eevee, 'use_transparent'): + scene.eevee.use_transparent = True + print("已启用透明渲染") + + # 设置透明混合模式(必要) + if hasattr(scene.render, 'film_transparent'): + scene.render.film_transparent = True + print("已启用透明背景") + + except AttributeError as e: + print(f"EEVEE设置警告: {e}") + print("使用默认EEVEE渲染设置") + + # 设置分辨率 + scene.render.resolution_x = 1080 # 宽度:1080px + scene.render.resolution_y = 2400 # 高度:2400px + scene.render.resolution_percentage = 100 + + # 设置输出格式 + scene.render.image_settings.file_format = 'PNG' + scene.render.image_settings.color_mode = 'RGBA' # 支持透明 + + # 设置输出路径到桌面 + desktop_path = os.path.join(os.path.expanduser("~"), "Desktop") + timestamp = time.strftime("%m%d_%H%M%S") # 日期_时分秒格式 + filename = f"isometric_{timestamp}.png" + scene.render.filepath = os.path.join(desktop_path, filename) + + print(f"EEVEE渲染设置完成,输出路径: {scene.render.filepath}") + return scene.render.filepath + +def setup_camera_and_lighting(): + """设置摄像机和照明(160W点光源 + 150W日光)""" + # 计算灯光高度(房间高度减一米) + room_height = 3.0 + light_height = room_height - 1.0 + + # 设置摄像机 + camera = None + if "Camera" in bpy.data.objects: + camera = bpy.data.objects["Camera"] + elif "Isometric Camera" in bpy.data.objects: + camera = bpy.data.objects["Isometric Camera"] + else: + # 创建新摄像机 + bpy.ops.object.camera_add(location=(7, -7, 5)) + camera = bpy.context.active_object + camera.name = "Isometric Camera" + + # 设置摄像机位置和旋转 + # 房间宽度5.0米,相机位置调整为(房间宽度-1, 1, 1.3) + room_width = 5.0 + camera.location = (room_width - 1, 1, 1.3) # 相机位置:(4, 1, 1.3) + camera.rotation_euler = (math.radians(90), math.radians(0), math.radians(45)) # 相机旋转 + + # 设置为透视投影 + camera.data.type = 'PERSP' + camera.data.lens = 35.0 # 35mm焦距 + camera.data.sensor_width = 35.0 # 35mm传感器 + camera.data.sensor_fit = 'AUTO' # 自动适配 + + # 设置为场景的活动摄像机 + bpy.context.scene.camera = camera + print("摄像机设置完成") + print(f"相机位置: ({camera.location.x}, {camera.location.y}, {camera.location.z})") + + # 隐藏ISO Emission灯光(如果存在) + light_objects = ["ISO Emission Left", "ISO Emission Right"] + for obj_name in light_objects: + if obj_name in bpy.data.objects: + light_obj = bpy.data.objects[obj_name] + # 隐藏发光对象 + light_obj.hide_render = True + light_obj.hide_viewport = True + print(f"已隐藏 {obj_name}") + + # 创建第一个160W点光源(房间中心) + # try: + # # 计算点光源位置(房间中心上方) + # room_length = 4.0 # Y轴 + # room_width = 8.0 # X轴(4+4) + + # # 第一个点光源位置:房间中心上方 + # light_x = 0 # X轴中心 + # light_y = 0 # Y轴中心 + # light_z = light_height + + # # 创建第一个点光源 + # bpy.ops.object.light_add(type='POINT', location=(light_x, light_y, light_z)) + # point_light1 = bpy.context.active_object + # point_light1.name = "Main Point Light" + # point_light1.data.energy = 160 # 160W + + # # 设置软衰减(简化设置) + # point_light1.data.shadow_soft_size = 0.5 # 0.5米半径,产生柔和阴影 + # point_light1.data.use_shadow = True # 启用阴影 + + # print(f"已创建第一个点光源(160W)") + # print(f"点光源位置: x={light_x}, y={light_y}, z={light_z}") + + # except Exception as e: + # print(f"创建第一个点光源时出错: {e}") + + # 创建第二个150W日光(位置12, 7, 6) + try: + # 第二个光源位置 - 调整到窗户后方更远的位置 + light2_x = -6 + light2_y = 7 # 让日光更远 + light2_z = 6 # 提高高度 + + # 创建日光 + bpy.ops.object.light_add(type='SUN', location=(light2_x, light2_y, light2_z)) + sun_light = bpy.context.active_object + sun_light.name = "Sun Light" + sun_light.data.energy = 20 + + # 调整日光旋转角度,让光线更直接地照射窗户 + sun_light.rotation_euler = (math.radians(0), math.radians(-60), math.radians(0)) # 简化旋转角度 + + # 设置日光属性(简化设置) + sun_light.data.angle = 0.05 # 从0.1改为0.05,让阴影更锐利 + sun_light.data.use_shadow = True # 启用阴影 + + print(f"已创建日光(20W)") + print(f"日光位置: x={light2_x}, y={light2_y}, z={light2_z}") + print(f"日光旋转: x={-70}°, y={0}°, z={0}°") + + except Exception as e: + print(f"创建日光时出错: {e}") + + print("照明设置完成") + print("灯光类型: 点光源 + 日光") + print(f"衰减类型: 点光源软衰减 + 日光平行光") + print(f"阴影设置: 启用,柔和阴影") + print(f"主灯光高度: {light_height}米 (房间高度减一米)") + print(f"日光位置: (0, 10, 6)米,旋转: (-70°, 0°, 0°)") + +def render_scene(): + """渲染场景""" + print("开始EEVEE渲染...") + + # 执行渲染 + bpy.ops.render.render(write_still=True) + print(f"EEVEE渲染完成! 图片保存在: {bpy.context.scene.render.filepath}") + return bpy.context.scene.render.filepath + +def main(): + """主函数""" + print("=" * 60) + print("开始自动创建等轴测房间并EEVEE渲染") + print("=" * 60) + + try: + # 1. 创建房间 + print("1. 创建等轴测房间...") + if not create_isometric_room(): + print("房间创建失败,停止执行") + return False + + # 2. 创建落地窗 + print("2. 创建archimesh落地窗...") + if not create_window_with_archimesh(): + print("落地窗创建失败,但继续执行") + # 不中断执行,窗户创建失败不影响渲染 + + # 3. 设置渲染参数(EEVEE) + print("3. 设置EEVEE渲染参数...") + output_path = setup_render_settings() + + # 4. 设置摄像机和照明 + print("4. 设置摄像机和照明...") + setup_camera_and_lighting() + + # 5. 渲染场景 + print("5. 开始EEVEE渲染...") + final_path = render_scene() + + print("=" * 60) + print("EEVEE渲染完成!") + print(f"渲染图片已保存到桌面: {final_path}") + print("=" * 60) + + return True + + except Exception as e: + print(f"执行过程中出现错误: {str(e)}") + import traceback + traceback.print_exc() + return False + +# 执行主函数 +if __name__ == "__main__": + main() +''' + + +def check_blender_exists(): + """检查Blender是否存在于指定路径""" + if not os.path.exists(BLENDER_PATH): + print(f"错误: 在路径 '{BLENDER_PATH}' 找不到Blender") + print("请检查Blender安装路径是否正确") + return False + return True + + +def create_temp_script(): + """创建临时的Blender脚本文件""" + script_path = os.path.join(os.getcwd(), "temp_simple_blender_script.py") + with open(script_path, 'w', encoding='utf-8') as f: + f.write(BLENDER_SIMPLE_SCRIPT) + return script_path + + +def launch_blender_simple(): + """启动Blender并执行简化版自动化脚本""" + if not check_blender_exists(): + return False + + # 创建临时脚本文件 + script_path = create_temp_script() + + try: + print("正在启动Blender(白模版本)...") + print(f"Blender路径: {BLENDER_PATH}") + print("特点:") + print("- 不设置材质,使用默认白色材质") + print("- 快速渲染") + print("- 兼容Blender 4.2") + print("- 自动保存到桌面") + + # 构建命令行参数 + cmd = [ + BLENDER_PATH, + "--background", # 后台运行模式 + "--disable-crash-handler", # 禁用崩溃处理器 + "--python", script_path # 执行Python脚本 + ] + + print("\n开始执行白模渲染流程...") + + # 启动Blender并等待完成 + process = subprocess.run( + cmd, + capture_output=True, + text=True, + encoding='utf-8', + timeout=300 # 5分钟超时 + ) + + # 输出结果 + if process.stdout: + print("Blender输出:") + # 过滤掉重复的错误信息 + lines = process.stdout.split('\n') + filtered_lines = [] + for line in lines: + if not ("AttributeError: 'NoneType' object has no attribute 'idname'" in line or + "Error in bpy.app.handlers.depsgraph_update_post" in line): + filtered_lines.append(line) + print('\n'.join(filtered_lines)) + + if process.stderr and not "rotate_tool.py" in process.stderr: + print("重要错误信息:") + print(process.stderr) + + if process.returncode == 0: + print("\n✅ 白模渲染执行成功!") + print("白模图片应该已经保存到桌面") + return True + else: + print(f"\n❌ 执行失败,返回码: {process.returncode}") + return False + + except subprocess.TimeoutExpired: + print("\n⏰ 执行超时(5分钟),可能Blender仍在运行") + print("请检查桌面是否有生成的图片") + return False + except Exception as e: + print(f"启动Blender时出现错误: {str(e)}") + return False + + finally: + # 清理临时文件 + try: + if os.path.exists(script_path): + os.remove(script_path) + print(f"已清理临时文件: {script_path}") + except: + pass + + +def main(): + """主函数""" + print("=" * 70) + print("Blender 4.2兼容版自动化脚本 - 白模渲染") + print("=" * 70) + print("特点:") + print("✓ 移除所有材质设置") + print("✓ 使用默认白色材质") + print("✓ 快速渲染") + print("✓ 兼容Blender 4.2") + print("✓ 自动保存到桌面") + print("=" * 70) + + success = launch_blender_simple() + + if success: + print("\n🎉 白模渲染完成!") + print("请检查桌面上的渲染图片") + print("图片文件名格式: isometric_room_white_[时间戳].png") + else: + print("\n❌ 执行失败!") + print("可能的解决方案:") + print("1. 确保Isometquick插件已正确安装") + print("2. 禁用可能冲突的其他插件") + print("3. 检查磁盘空间是否足够") + + +if __name__ == "__main__": + main() diff --git a/auto_room.py b/auto_room.py new file mode 100644 index 0000000..3a8ce55 --- /dev/null +++ b/auto_room.py @@ -0,0 +1,976 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Blender 4.2兼容版自动化脚本(白模版本) +自动创建等轴测房间并渲染白模 +""" + +import subprocess +import os +import sys + +# Blender可执行文件路径 +BLENDER_PATH = r"D:\Program Files\Blender Foundation\blender-4.2.11-windows-x64\blender.exe" + +# 简化版Blender脚本(只渲染白模) +BLENDER_SIMPLE_SCRIPT = ''' +import bpy +import bmesh +import os +import math +import time + +def disable_problematic_addons(): + """禁用可能导致问题的插件""" + try: + # 禁用可能冲突的插件 + problematic_addons = ['bl_tool'] + for addon in problematic_addons: + if addon in bpy.context.preferences.addons: + bpy.ops.preferences.addon_disable(module=addon) + print(f"已禁用插件: {addon}") + except: + pass + +def create_isometric_room(): + """创建等轴测房间,使用isometric_room_gen插件""" + try: + # 禁用问题插件 + #disable_problematic_addons() + + # 清除默认立方体 + if "Cube" in bpy.data.objects: + bpy.data.objects.remove(bpy.data.objects["Cube"], do_unlink=True) + + # 设置3D游标位置到原点 + bpy.context.scene.cursor.location = (0.0, 0.0, 0.0) + + # 检查isometric_room_gen插件 + if not hasattr(bpy.context.scene, 'sna_room_width'): + print("错误: isometric_room_gen插件未正确加载") + return False + + # 设置isometric_room_gen插件参数 + # 房间设置 + bpy.context.scene.sna_wall_thickness = 0.10 # 墙体厚度10cm + bpy.context.scene.sna_room_width = 5.0 # 房间宽度8米 + bpy.context.scene.sna_room_depth = 8.0 # 房间深度4米 + bpy.context.scene.sna_room_height = 3.0 # 房间高度3米 + + # 窗户设置 - 不设置窗户 + bpy.context.scene.sna_windows_enum = 'NONE' # 无窗户 + + # 设置房间类型为方形 + bpy.context.scene.sna_style = 'Square' + + # 设置拱门参数 + bpy.context.scene.sna_arch_settings = True # 启用拱门设置 + bpy.context.scene.sna_arch_placement = 'NONE' # 设置为后墙拱门BACK 不设置拱门NONE + bpy.context.scene.sna_arch_width = 1.2 # 拱门宽度1.2米 + bpy.context.scene.sna_arch_height = 2.4 # 拱门高度2.4米 + bpy.context.scene.sna_arch_thickness = 0.10 # 拱门厚度0.1米 + + # 找到3D视图区域 + view3d_area = None + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + view3d_area = area + break + + if view3d_area is None: + print("错误: 找不到3D视图区域") + return False + + # 临时切换到3D视图上下文 + with bpy.context.temp_override(area=view3d_area): + # 使用isometric_room_gen插件创建房间 + result = bpy.ops.sna.gen_room_1803a() + + if result == {'FINISHED'}: + print("等轴测房间创建完成!") + print(f"房间尺寸: {bpy.context.scene.sna_room_width}m x {bpy.context.scene.sna_room_depth}m x {bpy.context.scene.sna_room_height}m") + print(f"墙体厚度: {bpy.context.scene.sna_wall_thickness}m") + + # 将生成的房间在Z轴上向下移动墙体厚度的距离 + wall_thickness = bpy.context.scene.sna_wall_thickness + + # 查找生成的房间对象 + room_object = None + for obj in bpy.data.objects: + if obj.name == "IRG_IsoRoom_WithCeiling": + room_object = obj + break + + + return True + else: + print("房间创建失败") + return False + + except Exception as e: + print(f"创建房间时出现错误: {str(e)}") + import traceback + traceback.print_exc() + return False + + +def create_door_with_archimesh(): + """使用archimesh插件创建门""" + try: + print("开始创建archimesh门...") + + # 检查archimesh插件是否可用 + if not hasattr(bpy.ops.mesh, 'archimesh_door'): + print("错误: archimesh插件未正确加载") + print("请确保archimesh插件已安装并启用") + return False + + # 设置3D游标到原点 + bpy.context.scene.cursor.location = (0.0, 0.0, 0.0) + + # 找到3D视图区域 + view3d_area = None + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + view3d_area = area + break + + if view3d_area is None: + print("错误: 找不到3D视图区域") + return False + + # 临时切换到3D视图上下文 + with bpy.context.temp_override(area=view3d_area): + # 先创建门(使用默认参数) + result = bpy.ops.mesh.archimesh_door() + + if result == {'FINISHED'}: + print("archimesh门创建完成!") + + # 等待一帧以确保对象创建完成 + bpy.context.view_layer.update() + + # 找到刚创建的门组对象(Door_Group) + door_group = None + for obj in bpy.data.objects: + if obj.name == "Door_Group": + door_group = obj + break + + if door_group: + print(f"找到门组: {door_group.name}") + + # 查找主门对象(DoorFrame) + main_door_obj = None + for obj in bpy.data.objects: + if obj.name == "DoorFrame": + main_door_obj = obj + break + + if main_door_obj and hasattr(main_door_obj, 'DoorObjectGenerator'): + # 获取门属性 + door_props = main_door_obj.DoorObjectGenerator[0] + + print("设置门属性...") + print(f"找到主门对象: {main_door_obj.name}") + + # 先设置基本属性 + door_props.frame_width = 1.0 # 门框宽度 + door_props.frame_height = 2.1 # 门框高度 + door_props.frame_thick = 0.08 # 门框厚度 + door_props.openside = '1' # 右侧开门 + + # 设置门模型和把手(这些会触发update_object回调) + print("设置门模型为2...") + door_props.model = '2' # 门模型2 + + print("设置门把手为1...") + door_props.handle = '1' # 门把手1 + + print(f"门属性设置完成:") + print(f" - 门框宽度: {door_props.frame_width}") + print(f" - 门框高度: {door_props.frame_height}") + print(f" - 门框厚度: {door_props.frame_thick}") + print(f" - 开门方向: {door_props.openside}") + print(f" - 门模型: {door_props.model}") + print(f" - 门把手: {door_props.handle}") + + # 等待更新完成 + bpy.context.view_layer.update() + + # 验证设置是否生效 + print("验证门属性设置:") + print(f" - 当前门模型: {door_props.model}") + print(f" - 当前门把手: {door_props.handle}") + + # 强制触发更新(如果需要) + try: + # 重新设置属性以触发更新 + current_model = door_props.model + current_handle = door_props.handle + + door_props.model = '1' # 临时设置为其他值 + bpy.context.view_layer.update() + + door_props.model = current_model # 设置回目标值 + bpy.context.view_layer.update() + + print("已强制触发门模型更新") + + except Exception as update_error: + print(f"强制更新时出现错误: {update_error}") + + # 在修改门全部属性后进行位移和旋转 + print("开始移动和旋转门组...") + try: + door_group1 = None + for obj in bpy.data.objects: + if obj.name == "Door_Group": + door_group1 = obj + break + # 设置位置 + door_group1.location = (0.0, 6.0, 0.0) + print(f"已将门组移动到位置: {door_group1.location}") + + # 设置旋转 + door_group1.rotation_euler = (math.radians(0), math.radians(0), math.radians(90)) # 旋转(0, 0, 90度) + print(f"已将门组旋转到: (0°, 0°, 90°)") + + # 强制更新 + bpy.context.view_layer.update() + print("门组位移和旋转完成") + + except Exception as move_error: + print(f"移动和旋转门组时出现错误: {move_error}") + + else: + print("警告: 未找到主门对象或DoorObjectGenerator属性") + # 尝试查找其他可能的门属性 + for obj in bpy.data.objects: + if hasattr(obj, 'archimesh_door'): + print(f"找到archimesh_door属性在对象: {obj.name}") + + # 打印门组的所有子对象信息 + print("门组包含的对象:") + for obj in bpy.data.objects: + if obj.parent == door_group: + print(f" - {obj.name} (位置: {obj.location})") + + return True + else: + print("警告: 未找到Door_Group对象") + # 尝试查找其他可能的门对象 + door_objects = [obj for obj in bpy.data.objects if "Door" in obj.name] + if door_objects: + print("找到的门相关对象:") + for obj in door_objects: + print(f" - {obj.name} (类型: {obj.type})") + + # 尝试移动第一个找到的门对象 + first_door = door_objects[0] + first_door.location = (0.0, 2.0, 0.0) + print(f"已移动 {first_door.name} 到位置: {first_door.location}") + return True + else: + print("未找到任何门相关对象") + return False + else: + print("门创建失败") + return False + + except Exception as e: + print(f"创建门时出现错误: {str(e)}") + import traceback + traceback.print_exc() + return False + + +def create_window_with_archimesh(): + """使用archimesh插件创建落地窗""" + try: + print("开始创建archimesh落地窗...") + + # 检查archimesh插件是否可用 + if not hasattr(bpy.ops.mesh, 'archimesh_winpanel'): + print("错误: archimesh插件未正确加载") + print("请确保archimesh插件已安装并启用") + return False + + # 设置3D游标到原点 + bpy.context.scene.cursor.location = (0.0, 0.0, 0.0) + + # 找到3D视图区域 + view3d_area = None + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + view3d_area = area + break + + if view3d_area is None: + print("错误: 找不到3D视图区域") + return False + + # 临时切换到3D视图上下文 + with bpy.context.temp_override(area=view3d_area): + # 创建窗户 + result = bpy.ops.mesh.archimesh_winpanel() + + if result == {'FINISHED'}: + print("archimesh落地窗创建完成!") + + # 等待一帧以确保对象创建完成 + bpy.context.view_layer.update() + + # 找到刚创建的窗户组对象(Window_Group) + window_group = None + for obj in bpy.data.objects: + if obj.name == "Window_Group": + window_group = obj + break + + if window_group: + print(f"找到窗户组: {window_group.name}") + + # 查找主窗户对象(Window) + main_window_obj = None + for obj in bpy.data.objects: + if obj.name == "Window": + main_window_obj = obj + break + + if main_window_obj and hasattr(main_window_obj, 'WindowPanelGenerator'): + # 获取窗户属性 + window_props = main_window_obj.WindowPanelGenerator[0] + + print("设置窗户属性...") + print(f"找到主窗户对象: {main_window_obj.name}") + + # 设置窗户的基本属性 + window_props.gen = 4 # 4扇窗户(水平) + window_props.yuk = 1 # 1行(垂直) + window_props.kl1 = 5 # 外框厚度5cm + window_props.kl2 = 5 # 竖框厚度5cm + window_props.fk = 2 # 内框厚度2cm + + # 设置窗户尺寸 + window_props.gnx0 = 80 # 第1扇窗户宽度80cm + window_props.gnx1 = 80 # 第2扇窗户宽度80cm + window_props.gnx2 = 80 # 第3扇窗户宽度80cm + window_props.gnx3 = 80 # 第4扇窗户宽度80cm + window_props.gny0 = 250 # 窗户高度250cm(落地窗) + + # 设置窗户类型为平窗 + window_props.UST = '1' # 平窗 + window_props.r = 0 # 旋转角度0度 + + # 设置窗台 + window_props.mr = False # 启用窗台 + #window_props.mr1 = 4 # 窗台高度4cm + #window_props.mr2 = 4 # 窗台深度4cm + #window_props.mr3 = 20 # 窗台宽度20cm + #window_props.mr4 = 0 # 窗台延伸0cm + + # 设置材质 + window_props.mt1 = '1' # 外框材质PVC + window_props.mt2 = '1' # 内框材质塑料 + + # 设置窗户开启状态 + window_props.k00 = True # 第1扇窗户开启 + window_props.k01 = True # 第2扇窗户开启 + window_props.k02 = True # 第3扇窗户开启 + window_props.k03 = True # 第4扇窗户开启(如果有的话) + + print(f"窗户属性设置完成:") + print(f" - 水平扇数: {window_props.gen}") + print(f" - 垂直行数: {window_props.yuk}") + print(f" - 外框厚度: {window_props.kl1}cm") + print(f" - 竖框厚度: {window_props.kl2}cm") + print(f" - 内框厚度: {window_props.fk}cm") + print(f" - 窗户高度: {window_props.gny0}cm") + print(f" - 窗户类型: 平窗") + print(f" - 窗台: 启用") + print(f" - 窗户开启状态: k00={window_props.k00}, k01={window_props.k01}, k02={window_props.k02}, k03={window_props.k03}") + + + # 修复窗户玻璃材质 + print("修复窗户玻璃材质...") + fix_window_glass_material() + + # 等待更新完成 + bpy.context.view_layer.update() + + # 强制触发更新(如果需要) + try: + # 重新设置属性以触发更新 + current_gen = window_props.gen + current_gny0 = window_props.gny0 + + window_props.gen = 2 # 临时设置为其他值 + bpy.context.view_layer.update() + + window_props.gen = current_gen # 设置回目标值 + bpy.context.view_layer.update() + + print("已强制触发窗户更新") + + except Exception as update_error: + print(f"强制更新时出现错误: {update_error}") + + # 在修改落地窗全部属性后进行位移和旋转 + print("开始移动和旋转窗户组...") + try: + window_group1 = None + + for obj in bpy.data.objects: + if obj.name == "Window_Group": + window_group1 = obj + break + + # 设置位置 + window_group1.location = (0, 4.0, 0.0) + print(f"已将窗户组移动到位置: {window_group1.location}") + + # 设置旋转 + window_group1.rotation_euler = (math.radians(0), math.radians(0), math.radians(90)) # 旋转(0, 0, 90度) + print(f"已将窗户组旋转到: (0°, 0°, 90°)") + + # 强制更新 + bpy.context.view_layer.update() + print("窗户组位移和旋转完成") + + except Exception as move_error: + print(f"移动和旋转窗户组时出现错误: {move_error}") + + else: + print("警告: 未找到主窗户对象或WindowPanelGenerator属性") + # 尝试查找其他可能的窗户属性 + for obj in bpy.data.objects: + if hasattr(obj, 'archimesh_window'): + print(f"找到archimesh_window属性在对象: {obj.name}") + + # 打印窗户组的所有子对象信息 + print("窗户组包含的对象:") + for obj in bpy.data.objects: + if obj.parent == window_group: + print(f" - {obj.name} (位置: {obj.location})") + + + return True + else: + print("警告: 未找到Window_Group对象") + # 尝试查找其他可能的窗户对象 + window_objects = [obj for obj in bpy.data.objects if "Window" in obj.name] + if window_objects: + print("找到的窗户相关对象:") + for obj in window_objects: + print(f" - {obj.name} (类型: {obj.type})") + + # 尝试移动第一个找到的窗户对象 + first_window = window_objects[0] + first_window.location = (0.0, 1.96, 0.0) + print(f"已移动 {first_window.name} 到位置: {first_window.location}") + return True + else: + print("未找到任何窗户相关对象") + return False + + + + else: + print("窗户创建失败") + return False + + except Exception as e: + print(f"创建窗户时出现错误: {str(e)}") + import traceback + traceback.print_exc() + return False + + +def fix_window_glass_material(): + """修复窗户玻璃材质,将其改为高透BSDF""" + try: + print("开始修复窗户玻璃材质...") + + # 查找Window_Group下的Window对象 + window_group = None + for obj in bpy.data.objects: + if obj.name == "Window_Group": + window_group = obj + break + + if not window_group: + print("未找到Window_Group对象") + return False + + # 查找Window对象(Window_Group的子对象) + window_obj = None + for obj in bpy.data.objects: + if obj.name == "Window" and obj.parent == window_group: + window_obj = obj + break + + + if not window_obj: + print("未找到Window对象") + return False + + print(f"找到Window对象: {window_obj.name}") + + # 检查Window对象的材质 + if not window_obj.material_slots: + print("Window对象没有材质槽") + return False + + # 遍历所有材质槽 + for slot in window_obj.material_slots: + if slot.material and "Glass" in slot.material.name: + print(f"找到Glass材质: {slot.material.name}") + + # 启用节点编辑 + slot.material.use_nodes = True + + # 清除所有现有节点 + slot.material.node_tree.nodes.clear() + + # 创建半透BSDF节点 + translucent_bsdf = slot.material.node_tree.nodes.new(type='ShaderNodeBsdfTranslucent') + translucent_bsdf.location = (0, 0) + + # 设置半透BSDF参数 + translucent_bsdf.inputs['Color'].default_value = (0.95, 0.98, 1.0, 1.0) # 几乎无色 + # 半透BSDF节点没有Weight参数,只有Color参数 + + # 创建材质输出节点 + material_output = slot.material.node_tree.nodes.new(type='ShaderNodeOutputMaterial') + material_output.location = (300, 0) + + # 连接节点 + slot.material.node_tree.links.new( + translucent_bsdf.outputs['BSDF'], + material_output.inputs['Surface'] + ) + + # 设置材质混合模式为Alpha Blend + slot.material.blend_method = 'BLEND' + + print(f"已成功修改Glass材质为半透BSDF") + print(f" - 半透颜色: 几乎无色") + print(f" - 混合模式: Alpha Blend") + + return True + + print("未找到Glass材质") + return False + + except Exception as e: + print(f"修复玻璃材质时出现错误: {str(e)}") + import traceback + traceback.print_exc() + return False + +def setup_render_settings(): + """设置渲染参数(EEVEE渲染,优化速度)""" + scene = bpy.context.scene + + # 设置渲染引擎为EEVEE + scene.render.engine = 'BLENDER_EEVEE_NEXT' + print("已设置渲染引擎为EEVEE") + + # 启用Freestyle线条渲染 + scene.render.use_freestyle = True + print("已启用Freestyle线条渲染") + + # 配置Freestyle线条设置 + if hasattr(scene, 'view_layers'): + view_layer = scene.view_layers[0] # 获取第一个视图层 + view_layer.use_freestyle = True + + # 获取Freestyle线条设置 + freestyle = view_layer.freestyle_settings + + # 启用线条渲染 + freestyle.use_smoothness = True + freestyle.use_culling = True + + # 设置线条宽度 - 使用正确的API 没有生效 + + + # 设置世界环境 + if hasattr(scene, 'world') and scene.world: + # 启用世界节点 + scene.world.use_nodes = True + + # 清除现有节点 + scene.world.node_tree.nodes.clear() + + # 创建自发光节点 + emission_node = scene.world.node_tree.nodes.new(type='ShaderNodeEmission') + emission_node.location = (0, 0) + + # 设置HSV颜色:色相0,饱和度0,明度0.051,Alpha 1 + # 转换为RGB:HSV(0, 0, 0.051) = RGB(0.051, 0.051, 0.051) + emission_node.inputs['Color'].default_value = (0.051, 0.051, 0.051, 1.0) # 深灰色 + emission_node.inputs['Strength'].default_value = 8.0 # 强度8 + + # 创建世界输出节点 + world_output = scene.world.node_tree.nodes.new(type='ShaderNodeOutputWorld') + world_output.location = (300, 0) + + # 连接节点 + scene.world.node_tree.links.new( + emission_node.outputs['Emission'], + world_output.inputs['Surface'] + ) + + print("已设置世界环境为自发光") + print(f" - 颜色: HSV(0, 0, 0.051) = RGB(0.051, 0.051, 0.051)") + print(f" - 强度: 8.0") + + # 设置EEVEE采样(降低采样数提高速度) + try: + if hasattr(scene.eevee, 'taa_render_samples'): + scene.eevee.taa_render_samples = 32 # 从64降低到32 + print(f"已设置EEVEE渲染采样: {scene.eevee.taa_render_samples}") + elif hasattr(scene.eevee, 'taa_samples'): + scene.eevee.taa_samples = 32 # 从64降低到32 + print(f"已设置EEVEE采样: {scene.eevee.taa_samples}") + else: + print("警告: 无法找到EEVEE采样设置,使用默认值") + + # 启用屏幕空间反射(简化设置) + if hasattr(scene.eevee, 'use_ssr'): + scene.eevee.use_ssr = True + print("已启用屏幕空间反射") + + # 启用环境光遮蔽(简化设置) + if hasattr(scene.eevee, 'use_gtao'): + scene.eevee.use_gtao = True + print("已启用环境光遮蔽") + + # 启用透明渲染(必要) + if hasattr(scene.eevee, 'use_transparent'): + scene.eevee.use_transparent = True + print("已启用透明渲染") + + # 设置透明混合模式(必要) + if hasattr(scene.render, 'film_transparent'): + scene.render.film_transparent = True + print("已启用透明背景") + + except AttributeError as e: + print(f"EEVEE设置警告: {e}") + print("使用默认EEVEE渲染设置") + + # 设置分辨率 + scene.render.resolution_x = 1080 # 宽度:1080px + scene.render.resolution_y = 2400 # 高度:2400px + scene.render.resolution_percentage = 100 + + # 设置输出格式 + scene.render.image_settings.file_format = 'PNG' + scene.render.image_settings.color_mode = 'RGBA' # 支持透明 + + # 设置输出路径到桌面 + desktop_path = os.path.join(os.path.expanduser("~"), "Desktop") + timestamp = time.strftime("%m%d_%H%M%S") # 日期_时分秒格式 + filename = f"isometric_{timestamp}.png" + scene.render.filepath = os.path.join(desktop_path, filename) + + print(f"EEVEE渲染设置完成,输出路径: {scene.render.filepath}") + return scene.render.filepath + +def setup_camera_and_lighting(): + """设置摄像机和照明(160W点光源 + 150W日光)""" + # 计算灯光高度(房间高度减一米) + room_height = 3.0 + light_height = room_height - 1.0 + + # 设置摄像机 + camera = None + if "Camera" in bpy.data.objects: + camera = bpy.data.objects["Camera"] + elif "Isometric Camera" in bpy.data.objects: + camera = bpy.data.objects["Isometric Camera"] + else: + # 创建新摄像机 + bpy.ops.object.camera_add(location=(7, -7, 5)) + camera = bpy.context.active_object + camera.name = "Isometric Camera" + + # 设置摄像机位置和旋转 + # 房间宽度5.0米,相机位置调整为(房间宽度-1, 1, 1.3) + room_width = 5.0 + camera.location = (room_width - 1, 1, 1.3) # 相机位置:(4, 1, 1.3) + camera.rotation_euler = (math.radians(90), math.radians(0), math.radians(30)) # 相机旋转 + + # 设置为透视投影 + camera.data.type = 'PERSP' + camera.data.lens = 35.0 # 35mm焦距 + camera.data.sensor_width = 35.0 # 35mm传感器 + camera.data.sensor_fit = 'AUTO' # 自动适配 + + # 设置为场景的活动摄像机 + bpy.context.scene.camera = camera + print("摄像机设置完成") + print(f"相机位置: ({camera.location.x}, {camera.location.y}, {camera.location.z})") + + # 隐藏ISO Emission灯光(如果存在) + light_objects = ["ISO Emission Left", "ISO Emission Right"] + for obj_name in light_objects: + if obj_name in bpy.data.objects: + light_obj = bpy.data.objects[obj_name] + # 隐藏发光对象 + light_obj.hide_render = True + light_obj.hide_viewport = True + print(f"已隐藏 {obj_name}") + + # 创建第一个160W点光源(房间中心) + # try: + # # 计算点光源位置(房间中心上方) + # room_length = 4.0 # Y轴 + # room_width = 8.0 # X轴(4+4) + + # # 第一个点光源位置:房间中心上方 + # light_x = 0 # X轴中心 + # light_y = 0 # Y轴中心 + # light_z = light_height + + # # 创建第一个点光源 + # bpy.ops.object.light_add(type='POINT', location=(light_x, light_y, light_z)) + # point_light1 = bpy.context.active_object + # point_light1.name = "Main Point Light" + # point_light1.data.energy = 160 # 160W + + # # 设置软衰减(简化设置) + # point_light1.data.shadow_soft_size = 0.5 # 0.5米半径,产生柔和阴影 + # point_light1.data.use_shadow = True # 启用阴影 + + # print(f"已创建第一个点光源(160W)") + # print(f"点光源位置: x={light_x}, y={light_y}, z={light_z}") + + # except Exception as e: + # print(f"创建第一个点光源时出错: {e}") + + # 创建第二个150W日光(位置12, 7, 6) + # try: + # # 第二个光源位置 - 调整到窗户后方更远的位置 + # light2_x = -6 + # light2_y = 7 # 让日光更远 + # light2_z = 6 # 提高高度 + + # # 创建日光 + # bpy.ops.object.light_add(type='SUN', location=(light2_x, light2_y, light2_z)) + # sun_light = bpy.context.active_object + # sun_light.name = "Sun Light" + # sun_light.data.energy = 20 + + # # 调整日光旋转角度,让光线更直接地照射窗户 + # sun_light.rotation_euler = (math.radians(0), math.radians(-60), math.radians(0)) # 简化旋转角度 + + # # 设置日光属性(简化设置) + # sun_light.data.angle = 0.05 # 从0.1改为0.05,让阴影更锐利 + # sun_light.data.use_shadow = True # 启用阴影 + + # print(f"已创建日光(20W)") + # print(f"日光位置: x={light2_x}, y={light2_y}, z={light2_z}") + # print(f"日光旋转: x={-70}°, y={0}°, z={0}°") + + # except Exception as e: + # print(f"创建日光时出错: {e}") + + print("照明设置完成") + print("灯光类型: 点光源 + 日光") + print(f"衰减类型: 点光源软衰减 + 日光平行光") + print(f"阴影设置: 启用,柔和阴影") + print(f"主灯光高度: {light_height}米 (房间高度减一米)") + print(f"日光位置: (0, 10, 6)米,旋转: (-70°, 0°, 0°)") + +def render_scene(): + """渲染场景""" + print("开始EEVEE渲染...") + + # 执行渲染 + bpy.ops.render.render(write_still=True) + print(f"EEVEE渲染完成! 图片保存在: {bpy.context.scene.render.filepath}") + return bpy.context.scene.render.filepath + +def main(): + """主函数""" + print("=" * 60) + print("开始自动创建等轴测房间并EEVEE渲染") + print("=" * 60) + + try: + # 1. 创建房间 + print("1. 创建等轴测房间...") + if not create_isometric_room(): + print("房间创建失败,停止执行") + return False + + # 2. 创建落地窗 + print("2. 创建archimesh落地窗...") + if not create_window_with_archimesh(): + print("落地窗创建失败,但继续执行") + # 不中断执行,窗户创建失败不影响渲染 + # 2. 创建门 + print("2. 创建archimesh门...") + if not create_door_with_archimesh(): + print("落地窗创建失败,但继续执行") + # 不中断执行,窗户创建失败不影响渲染 + + # 3. 设置渲染参数(EEVEE) + print("3. 设置EEVEE渲染参数...") + output_path = setup_render_settings() + + # 4. 设置摄像机和照明 + print("4. 设置摄像机和照明...") + setup_camera_and_lighting() + + # 5. 渲染场景 + print("5. 开始EEVEE渲染...") + final_path = render_scene() + + print("=" * 60) + print("EEVEE渲染完成!") + print(f"渲染图片已保存到桌面: {final_path}") + print("=" * 60) + + return True + + except Exception as e: + print(f"执行过程中出现错误: {str(e)}") + import traceback + traceback.print_exc() + return False + +# 执行主函数 +if __name__ == "__main__": + main() +''' + + +def check_blender_exists(): + """检查Blender是否存在于指定路径""" + if not os.path.exists(BLENDER_PATH): + print(f"错误: 在路径 '{BLENDER_PATH}' 找不到Blender") + print("请检查Blender安装路径是否正确") + return False + return True + + +def create_temp_script(): + """创建临时的Blender脚本文件""" + script_path = os.path.join(os.getcwd(), "temp_simple_blender_script.py") + with open(script_path, 'w', encoding='utf-8') as f: + f.write(BLENDER_SIMPLE_SCRIPT) + return script_path + + +def launch_blender_simple(): + """启动Blender并执行简化版自动化脚本""" + if not check_blender_exists(): + return False + + # 创建临时脚本文件 + script_path = create_temp_script() + + try: + print("正在启动Blender(白模版本)...") + print(f"Blender路径: {BLENDER_PATH}") + print("特点:") + print("- 不设置材质,使用默认白色材质") + print("- 快速渲染") + print("- 兼容Blender 4.2") + print("- 自动保存到桌面") + + # 构建命令行参数 + cmd = [ + BLENDER_PATH, + "--background", # 后台运行模式 + "--disable-crash-handler", # 禁用崩溃处理器 + "--python", script_path # 执行Python脚本 + ] + + print("\n开始执行白模渲染流程...") + + # 启动Blender并等待完成 + process = subprocess.run( + cmd, + capture_output=True, + text=True, + encoding='utf-8', + timeout=300 # 5分钟超时 + ) + + # 输出结果 + if process.stdout: + print("Blender输出:") + # 过滤掉重复的错误信息 + lines = process.stdout.split('\n') + filtered_lines = [] + for line in lines: + if not ("AttributeError: 'NoneType' object has no attribute 'idname'" in line or + "Error in bpy.app.handlers.depsgraph_update_post" in line): + filtered_lines.append(line) + print('\n'.join(filtered_lines)) + + if process.stderr and not "rotate_tool.py" in process.stderr: + print("重要错误信息:") + print(process.stderr) + + if process.returncode == 0: + print("\n✅ 白模渲染执行成功!") + print("白模图片应该已经保存到桌面") + return True + else: + print(f"\n❌ 执行失败,返回码: {process.returncode}") + return False + + except subprocess.TimeoutExpired: + print("\n⏰ 执行超时(5分钟),可能Blender仍在运行") + print("请检查桌面是否有生成的图片") + return False + except Exception as e: + print(f"启动Blender时出现错误: {str(e)}") + return False + + finally: + # 清理临时文件 + try: + if os.path.exists(script_path): + os.remove(script_path) + print(f"已清理临时文件: {script_path}") + except: + pass + + +def main(): + """主函数""" + print("=" * 70) + print("Blender 4.2兼容版自动化脚本 - 白模渲染") + print("=" * 70) + print("特点:") + print("✓ 移除所有材质设置") + print("✓ 使用默认白色材质") + print("✓ 快速渲染") + print("✓ 兼容Blender 4.2") + print("✓ 自动保存到桌面") + print("=" * 70) + + success = launch_blender_simple() + + if success: + print("\n🎉 白模渲染完成!") + print("请检查桌面上的渲染图片") + print("图片文件名格式: isometric_room_white_[时间戳].png") + else: + print("\n❌ 执行失败!") + print("可能的解决方案:") + print("1. 确保Isometquick插件已正确安装") + print("2. 禁用可能冲突的其他插件") + print("3. 检查磁盘空间是否足够") + + +if __name__ == "__main__": + main() diff --git a/blender_manifest.toml b/blender_manifest.toml new file mode 100644 index 0000000..f2f3163 --- /dev/null +++ b/blender_manifest.toml @@ -0,0 +1,13 @@ +schema_version = "1.0.0" +id = "isometric_room_gen" +version = "1.0.6" +name = "Isometric Room Gen" +tagline = "Generate Isometric Rooms" +maintainer = "SkdSam & Mr Steve" +type = "add-on" +website = "https://superhivemarket.com/creators/skdsam" +tags = ["3D View"] +blender_version_min = "3.6.0" +license = [ +"SPDX:GPL-2.0-or-later", +] \ No newline at end of file