建筑可视化与数字孪生城市建模技术应用

基于PySide6与3ds Max集成的建筑设计方案三维预览系统

📝 文档信息

作者:丁林松

创建时间:2024年

技术栈:PySide6, 3ds Max, PyTorch, OpenGL, Python

📋 目录

1. 引言与技术概述

1.1 建筑可视化技术发展背景

随着建筑设计行业的数字化转型,传统的二维图纸已经无法满足现代建筑设计的复杂需求。建筑可视化技术作为建筑信息模型(BIM)的重要组成部分,为建筑师、工程师和客户提供了直观、实时的三维设计体验。数字孪生城市建模技术更是将这一概念扩展到了城市规划和智慧城市建设领域。

本系统旨在构建一个基于PySide6和3ds Max的集成平台,实现建筑设计方案的三维预览、材质实时切换和数据交互功能。通过深度学习算法优化渲染性能,提供专业级的建筑可视化解决方案。

1.2 核心技术栈分析

PySide6 (Qt6)

3ds Max SDK

PyTorch

OpenGL

Python 3.9+

NumPy

PIL/Pillow

JSON/XML

1.3 系统主要功能特性

🏗️ 3ds Max文件导入导出

支持.max、.fbx、.obj等多种标准格式的双向转换,确保与现有工作流程的无缝集成。

🎨 实时材质切换

基于GPU加速的材质渲染系统,支持PBR材质、纹理贴图的实时预览和动态切换。

🔄 数据双向同步

实现3ds Max与PySide6应用程序之间的实时数据同步,支持几何体、材质、动画数据的双向传输。

🧠 AI算法优化

集成PyTorch深度学习模型,实现材质预测、渲染优化和场景自动配置功能。

技术创新点:本系统创新性地将传统的建筑设计软件与现代Python生态系统相结合,通过深度学习技术提升建筑可视化的智能化水平,为数字孪生城市建设提供了强有力的技术支撑。

2. 系统架构设计

2.1 整体架构概览

系统采用分层架构设计,主要包括表示层、业务逻辑层、数据访问层和外部接口层。这种设计确保了系统的可扩展性、可维护性和性能优化。

系统架构流程图

用户界面层 (PySide6)

业务逻辑层 (Python)

数据处理层 (PyTorch)

3ds Max接口层

渲染引擎 (OpenGL)

2.2 核心组件详细设计

2.2.1 用户界面层 (UI Layer)

基于PySide6构建的现代化用户界面,提供直观的操作体验。界面采用模块化设计,包括项目管理器、三维视口、材质编辑器、属性面板等核心组件。

# UI层核心架构示例
from PySide6.QtWidgets import *
from PySide6.QtCore import *
from PySide6.QtOpenGL import *
from PySide6.QtOpenGLWidgets import QOpenGLWidget
import OpenGL.GL as gl

class ArchitecturalVisualizationUI(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setupUI()
        self.initializeComponents()
    
    def setupUI(self):
        """初始化用户界面布局"""
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        
        # 主布局
        main_layout = QHBoxLayout(central_widget)
        
        # 左侧面板 - 项目管理和材质库
        left_panel = self.createLeftPanel()
        main_layout.addWidget(left_panel, 1)
        
        # 中央视口 - 三维预览
        self.viewport = Viewport3D(self)
        main_layout.addWidget(self.viewport, 3)
        
        # 右侧面板 - 属性和设置
        right_panel = self.createRightPanel()
        main_layout.addWidget(right_panel, 1)
    
    def createLeftPanel(self):
        """创建左侧控制面板"""
        panel = QWidget()
        layout = QVBoxLayout(panel)
        
        # 项目管理器
        self.project_manager = ProjectManager()
        layout.addWidget(self.project_manager)
        
        # 材质库
        self.material_library = MaterialLibrary()
        layout.addWidget(self.material_library)
        
        return panel
                
2.2.2 业务逻辑层 (Business Logic Layer)

负责处理核心业务逻辑,包括文件格式转换、材质管理、场景管理等功能。这一层与用户界面层分离,确保业务逻辑的复用性和可测试性。

2.2.3 数据访问层 (Data Access Layer)

提供统一的数据访问接口,支持多种数据源的读写操作,包括3ds Max文件、材质库数据、用户配置等。

2.3 模块间通信机制

系统采用事件驱动的通信机制,通过Qt的信号-槽机制实现模块间的松耦合通信。这种设计提高了系统的响应性和扩展性。

class EventManager(QObject):
    """事件管理器 - 负责模块间通信"""
    
    # 定义系统事件信号
    sceneLoaded = Signal(str)  # 场景加载完成
    materialChanged = Signal(str, dict)  # 材质变更
    renderingUpdated = Signal()  # 渲染更新
    maxIntegrationStatus = Signal(bool)  # 3ds Max连接状态
    
    def __init__(self):
        super().__init__()
        self._subscribers = {}
    
    def subscribe(self, event_name, callback):
        """订阅事件"""
        if event_name not in self._subscribers:
            self._subscribers[event_name] = []
        self._subscribers[event_name].append(callback)
    
    def emit_event(self, event_name, *args, **kwargs):
        """触发事件"""
        if event_name in self._subscribers:
            for callback in self._subscribers[event_name]:
                callback(*args, **kwargs)

# 全局事件管理器实例
event_manager = EventManager()
                

3. 3ds Max集成技术

3.1 3ds Max SDK集成方案

3ds Max作为业界领先的三维建模和渲染软件,提供了丰富的SDK接口供第三方开发者使用。我们的系统通过MaxScript和Python-COM接口实现与3ds Max的深度集成。

3.1.1 MaxScript接口封装

MaxScript是3ds Max的内置脚本语言,提供了对3ds Max所有功能的访问能力。我们通过Python的subprocess模块调用MaxScript命令,实现对3ds Max的远程控制。

import subprocess
import json
import os
from pathlib import Path

class MaxScriptInterface:
    """3ds Max脚本接口封装类"""
    
    def __init__(self, max_executable_path=None):
        self.max_path = max_executable_path or self._find_max_installation()
        self.temp_script_dir = Path.cwd() / "temp_scripts"
        self.temp_script_dir.mkdir(exist_ok=True)
    
    def _find_max_installation(self):
        """自动查找3ds Max安装路径"""
        possible_paths = [
            r"C:\Program Files\Autodesk\3ds Max 2024\3dsmax.exe",
            r"C:\Program Files\Autodesk\3ds Max 2023\3dsmax.exe",
            r"C:\Program Files\Autodesk\3ds Max 2022\3dsmax.exe"
        ]
        
        for path in possible_paths:
            if os.path.exists(path):
                return path
        
        raise FileNotFoundError("未找到3ds Max安装路径")
    
    def execute_script(self, script_content, return_value=False):
        """执行MaxScript代码"""
        script_file = self.temp_script_dir / f"temp_{os.getpid()}.ms"
        
        try:
            # 写入脚本文件
            with open(script_file, 'w', encoding='utf-8') as f:
                f.write(script_content)
            
            # 执行脚本
            cmd = f'"{self.max_path}" -q -silent -script:"{script_file}"'
            result = subprocess.run(cmd, capture_output=True, text=True, shell=True)
            
            if return_value and result.stdout:
                return result.stdout.strip()
            
            return result.returncode == 0
            
        finally:
            # 清理临时文件
            if script_file.exists():
                script_file.unlink()
    
    def import_model(self, file_path, import_options=None):
        """导入三维模型"""
        options = import_options or {}
        
        script = f"""
        -- 设置导入选项
        importFile "{file_path}" #noPrompt using:FBXImporter
        
        -- 获取导入对象信息
        selected_objects = selection as array
        object_info = #()
        
        for obj in selected_objects do (
            append object_info (obj.name)
        )
        
        -- 输出结果
        format "IMPORT_SUCCESS: %\\n" object_info
        """
        
        result = self.execute_script(script, return_value=True)
        return "IMPORT_SUCCESS" in result
    
    def export_scene(self, export_path, export_format="fbx"):
        """导出场景"""
        script = f"""
        -- 选择所有对象
        selectAll()
        
        -- 导出场景
        exportFile "{export_path}" #noPrompt selectedOnly:false using:FBXExporter
        
        format "EXPORT_COMPLETE\\n"
        """
        
        return self.execute_script(script)
    
    def get_scene_info(self):
        """获取场景信息"""
        script = """
        scene_info = #()
        
        -- 获取所有对象
        for obj in objects do (
            obj_data = #(obj.name, classof obj as string, obj.transform)
            append scene_info obj_data
        )
        
        -- 获取材质信息
        material_info = #()
        for i = 1 to meditMaterials.count do (
            mat = meditMaterials[i]
            if mat != undefined then (
                mat_data = #(mat.name, classof mat as string)
                append material_info mat_data
            )
        )
        
        format "SCENE_OBJECTS: %\\n" scene_info
        format "SCENE_MATERIALS: %\\n" material_info
        """
        
        result = self.execute_script(script, return_value=True)
        return self._parse_scene_info(result)
    
    def _parse_scene_info(self, script_output):
        """解析场景信息"""
        lines = script_output.split('\n')
        objects = []
        materials = []
        
        current_section = None
        for line in lines:
            if "SCENE_OBJECTS:" in line:
                current_section = "objects"
            elif "SCENE_MATERIALS:" in line:
                current_section = "materials"
            elif current_section == "objects" and line.strip():
                # 解析对象信息
                try:
                    obj_info = eval(line.strip())
                    objects.append({
                        'name': obj_info[0],
                        'type': obj_info[1],
                        'transform': obj_info[2]
                    })
                except:
                    pass
            elif current_section == "materials" and line.strip():
                # 解析材质信息
                try:
                    mat_info = eval(line.strip())
                    materials.append({
                        'name': mat_info[0],
                        'type': mat_info[1]
                    })
                except:
                    pass
        
        return {'objects': objects, 'materials': materials}
                

3.2 文件格式转换系统

为了确保与不同建筑设计软件的兼容性,系统支持多种三维文件格式的导入导出,包括.max、.fbx、.obj、.dae等主流格式。

class FileFormatConverter:
    """文件格式转换器"""
    
    SUPPORTED_FORMATS = {
        'input': ['.max', '.fbx', '.obj', '.dae', '.3ds', '.blend'],
        'output': ['.fbx', '.obj', '.dae', '.gltf', '.usd']
    }
    
    def __init__(self, max_interface):
        self.max_interface = max_interface
        self.conversion_cache = {}
    
    def convert_file(self, input_path, output_path, options=None):
        """转换文件格式"""
        input_ext = Path(input_path).suffix.lower()
        output_ext = Path(output_path).suffix.lower()
        
        if not self._validate_formats(input_ext, output_ext):
            raise ValueError(f"不支持的格式转换: {input_ext} -> {output_ext}")
        
        # 检查缓存
        cache_key = f"{input_path}_{output_path}_{hash(str(options))}"
        if cache_key in self.conversion_cache:
            return self.conversion_cache[cache_key]
        
        try:
            # 执行转换
            success = self._perform_conversion(input_path, output_path, options)
            self.conversion_cache[cache_key] = success
            return success
            
        except Exception as e:
            print(f"转换失败: {e}")
            return False
    
    def _validate_formats(self, input_ext, output_ext):
        """验证格式支持"""
        return (input_ext in self.SUPPORTED_FORMATS['input'] and 
                output_ext in self.SUPPORTED_FORMATS['output'])
    
    def _perform_conversion(self, input_path, output_path, options):
        """执行实际转换"""
        script = f"""
        -- 重置场景
        resetMaxFile #noprompt
        
        -- 导入源文件
        importFile "{input_path}" #noPrompt
        
        -- 应用转换选项
        {self._generate_conversion_options(options)}
        
        -- 导出目标文件
        exportFile "{output_path}" #noPrompt selectedOnly:false
        
        format "CONVERSION_SUCCESS\\n"
        """
        
        result = self.max_interface.execute_script(script, return_value=True)
        return "CONVERSION_SUCCESS" in result
    
    def _generate_conversion_options(self, options):
        """生成转换选项脚本"""
        if not options:
            return ""
        
        script_lines = []
        
        if 'scale' in options:
            script_lines.append(f"scale selection [{options['scale']}]")
        
        if 'optimize_geometry' in options and options['optimize_geometry']:
            script_lines.append("ProOptimizer.Calculate()")
        
        if 'merge_materials' in options and options['merge_materials']:
            script_lines.append("meditMaterials[1] = standardMaterial()")
        
        return "\n".join(script_lines)
    
    def batch_convert(self, file_list, output_dir, target_format, options=None):
        """批量转换文件"""
        results = []
        
        for input_file in file_list:
            input_path = Path(input_file)
            output_path = output_dir / f"{input_path.stem}{target_format}"
            
            success = self.convert_file(str(input_path), str(output_path), options)
            results.append({
                'input': str(input_path),
                'output': str(output_path),
                'success': success
            })
        
        return results
                

3.3 数据交互桥梁

为了实现3ds Max与PySide6应用程序之间的实时数据交换,我们设计了一个基于TCP/IP的通信桥梁,支持几何数据、材质信息、动画数据的双向传输。

通信协议设计:采用JSON格式的自定义协议,确保数据传输的可靠性和可扩展性。协议支持数据压缩、错误重传和版本兼容等特性。

4. PySide6三维框架实现

4.1 OpenGL渲染引擎

基于PySide6的QOpenGLWidget构建高性能的三维渲染引擎,支持现代OpenGL特性,包括着色器程序、纹理映射、光照计算等核心功能。

from PySide6.QtOpenGLWidgets import QOpenGLWidget
from PySide6.QtCore import QTimer, Signal
from PySide6.QtGui import QMatrix4x4, QVector3D
import OpenGL.GL as gl
import OpenGL.GL.shaders as shaders
import numpy as np

class Viewport3D(QOpenGLWidget):
    """三维视口渲染器"""
    
    # 渲染事件信号
    renderingStarted = Signal()
    renderingFinished = Signal()
    objectSelected = Signal(str)
    
    def __init__(self, parent=None):
        super().__init__(parent)
        
        # 渲染状态
        self.scene_objects = []
        self.current_material = None
        self.camera_position = QVector3D(0, 0, 5)
        self.camera_target = QVector3D(0, 0, 0)
        
        # 着色器程序
        self.shader_program = None
        self.vertex_shader = None
        self.fragment_shader = None
        
        # 渲染缓冲
        self.vertex_buffer = None
        self.index_buffer = None
        self.texture_buffer = None
        
        # 动画定时器
        self.animation_timer = QTimer()
        self.animation_timer.timeout.connect(self.update)
        
        # 鼠标交互状态
        self.last_mouse_pos = None
        self.is_rotating = False
        self.is_panning = False
        
    def initializeGL(self):
        """初始化OpenGL环境"""
        # 启用深度测试
        gl.glEnable(gl.GL_DEPTH_TEST)
        gl.glDepthFunc(gl.GL_LESS)
        
        # 启用背面剔除
        gl.glEnable(gl.GL_CULL_FACE)
        gl.glCullFace(gl.GL_BACK)
        
        # 设置清除颜色
        gl.glClearColor(0.2, 0.2, 0.2, 1.0)
        
        # 初始化着色器
        self._init_shaders()
        
        # 初始化缓冲对象
        self._init_buffers()
        
        print("OpenGL初始化完成")
    
    def _init_shaders(self):
        """初始化着色器程序"""
        
        # 顶点着色器源码
        vertex_shader_source = """
        #version 330 core
        
        layout (location = 0) in vec3 position;
        layout (location = 1) in vec3 normal;
        layout (location = 2) in vec2 texCoord;
        
        uniform mat4 model;
        uniform mat4 view;
        uniform mat4 projection;
        uniform mat3 normalMatrix;
        
        out vec3 fragPos;
        out vec3 fragNormal;
        out vec2 fragTexCoord;
        
        void main() {
            fragPos = vec3(model * vec4(position, 1.0));
            fragNormal = normalMatrix * normal;
            fragTexCoord = texCoord;
            
            gl_Position = projection * view * vec4(fragPos, 1.0);
        }
        """
        
        # 片段着色器源码
        fragment_shader_source = """
        #version 330 core
        
        in vec3 fragPos;
        in vec3 fragNormal;
        in vec2 fragTexCoord;
        
        out vec4 fragColor;
        
        uniform vec3 lightPos;
        uniform vec3 lightColor;
        uniform vec3 viewPos;
        
        uniform vec3 materialAmbient;
        uniform vec3 materialDiffuse;
        uniform vec3 materialSpecular;
        uniform float materialShininess;
        
        uniform sampler2D diffuseTexture;
        uniform bool useTexture;
        
        void main() {
            vec3 color = useTexture ? texture(diffuseTexture, fragTexCoord).rgb : materialDiffuse;
            
            // 环境光
            vec3 ambient = 0.1 * color;
            
            // 漫反射
            vec3 norm = normalize(fragNormal);
            vec3 lightDir = normalize(lightPos - fragPos);
            float diff = max(dot(norm, lightDir), 0.0);
            vec3 diffuse = diff * lightColor * color;
            
            // 镜面反射
            vec3 viewDir = normalize(viewPos - fragPos);
            vec3 reflectDir = reflect(-lightDir, norm);
            float spec = pow(max(dot(viewDir, reflectDir), 0.0), materialShininess);
            vec3 specular = spec * lightColor * materialSpecular;
            
            vec3 result = ambient + diffuse + specular;
            fragColor = vec4(result, 1.0);
        }
        """
        
        # 编译着色器
        self.vertex_shader = shaders.compileShader(vertex_shader_source, gl.GL_VERTEX_SHADER)
        self.fragment_shader = shaders.compileShader(fragment_shader_source, gl.GL_FRAGMENT_SHADER)
        
        # 创建着色器程序
        self.shader_program = shaders.compileProgram(self.vertex_shader, self.fragment_shader)
        
    def _init_buffers(self):
        """初始化顶点缓冲对象"""
        # 创建VAO
        self.vao = gl.glGenVertexArrays(1)
        gl.glBindVertexArray(self.vao)
        
        # 创建VBO
        self.vbo = gl.glGenBuffers(1)
        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vbo)
        
        # 创建EBO
        self.ebo = gl.glGenBuffers(1)
        gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self.ebo)
        
        # 解绑
        gl.glBindVertexArray(0)
    
    def resizeGL(self, width, height):
        """窗口大小改变时调用"""
        gl.glViewport(0, 0, width, height)
        
        # 更新投影矩阵
        self.projection_matrix = QMatrix4x4()
        aspect_ratio = width / height if height != 0 else 1
        self.projection_matrix.perspective(45.0, aspect_ratio, 0.1, 100.0)
    
    def paintGL(self):
        """渲染场景"""
        self.renderingStarted.emit()
        
        # 清除缓冲
        gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
        
        # 使用着色器程序
        gl.glUseProgram(self.shader_program)
        
        # 设置视图矩阵
        view_matrix = QMatrix4x4()
        view_matrix.lookAt(self.camera_position, self.camera_target, QVector3D(0, 1, 0))
        
        # 设置uniform变量
        self._set_shader_uniforms(view_matrix)
        
        # 渲染所有对象
        for scene_object in self.scene_objects:
            self._render_object(scene_object)
        
        self.renderingFinished.emit()
    
    def _set_shader_uniforms(self, view_matrix):
        """设置着色器uniform变量"""
        # 获取uniform位置
        model_loc = gl.glGetUniformLocation(self.shader_program, "model")
        view_loc = gl.glGetUniformLocation(self.shader_program, "view")
        proj_loc = gl.glGetUniformLocation(self.shader_program, "projection")
        
        # 设置矩阵
        model_matrix = QMatrix4x4()
        gl.glUniformMatrix4fv(model_loc, 1, gl.GL_FALSE, model_matrix.data())
        gl.glUniformMatrix4fv(view_loc, 1, gl.GL_FALSE, view_matrix.data())
        gl.glUniformMatrix4fv(proj_loc, 1, gl.GL_FALSE, self.projection_matrix.data())
        
        # 设置光照参数
        light_pos_loc = gl.glGetUniformLocation(self.shader_program, "lightPos")
        light_color_loc = gl.glGetUniformLocation(self.shader_program, "lightColor")
        view_pos_loc = gl.glGetUniformLocation(self.shader_program, "viewPos")
        
        gl.glUniform3f(light_pos_loc, 2.0, 2.0, 2.0)
        gl.glUniform3f(light_color_loc, 1.0, 1.0, 1.0)
        gl.glUniform3f(view_pos_loc, self.camera_position.x(), 
                      self.camera_position.y(), self.camera_position.z())
    
    def _render_object(self, scene_object):
        """渲染单个对象"""
        if not hasattr(scene_object, 'vertices') or not scene_object.vertices:
            return
        
        # 绑定VAO
        gl.glBindVertexArray(self.vao)
        
        # 上传顶点数据
        vertices = np.array(scene_object.vertices, dtype=np.float32)
        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vbo)
        gl.glBufferData(gl.GL_ARRAY_BUFFER, vertices.nbytes, vertices, gl.GL_DYNAMIC_DRAW)
        
        # 设置顶点属性
        # 位置
        gl.glVertexAttribPointer(0, 3, gl.GL_FLOAT, gl.GL_FALSE, 8 * 4, gl.ctypes.c_void_p(0))
        gl.glEnableVertexAttribArray(0)
        
        # 法线
        gl.glVertexAttribPointer(1, 3, gl.GL_FLOAT, gl.GL_FALSE, 8 * 4, gl.ctypes.c_void_p(3 * 4))
        gl.glEnableVertexAttribArray(1)
        
        # 纹理坐标
        gl.glVertexAttribPointer(2, 2, gl.GL_FLOAT, gl.GL_FALSE, 8 * 4, gl.ctypes.c_void_p(6 * 4))
        gl.glEnableVertexAttribArray(2)
        
        # 上传索引数据
        if hasattr(scene_object, 'indices') and scene_object.indices:
            indices = np.array(scene_object.indices, dtype=np.uint32)
            gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self.ebo)
            gl.glBufferData(gl.GL_ELEMENT_ARRAY_BUFFER, indices.nbytes, indices, gl.GL_DYNAMIC_DRAW)
            
            # 绘制
            gl.glDrawElements(gl.GL_TRIANGLES, len(indices), gl.GL_UNSIGNED_INT, None)
        else:
            # 绘制数组
            gl.glDrawArrays(gl.GL_TRIANGLES, 0, len(vertices) // 8)
        
        # 解绑VAO
        gl.glBindVertexArray(0)
    
    def load_scene_object(self, object_data):
        """加载场景对象"""
        scene_object = SceneObject(object_data)
        self.scene_objects.append(scene_object)
        self.update()  # 触发重绘
    
    def set_material(self, material_data):
        """设置当前材质"""
        self.current_material = material_data
        self._update_material_uniforms()
        self.update()
    
    def _update_material_uniforms(self):
        """更新材质uniform变量"""
        if not self.current_material:
            return
        
        gl.glUseProgram(self.shader_program)
        
        # 材质属性
        ambient_loc = gl.glGetUniformLocation(self.shader_program, "materialAmbient")
        diffuse_loc = gl.glGetUniformLocation(self.shader_program, "materialDiffuse")
        specular_loc = gl.glGetUniformLocation(self.shader_program, "materialSpecular")
        shininess_loc = gl.glGetUniformLocation(self.shader_program, "materialShininess")
        
        ambient = self.current_material.get('ambient', [0.2, 0.2, 0.2])
        diffuse = self.current_material.get('diffuse', [0.8, 0.8, 0.8])
        specular = self.current_material.get('specular', [1.0, 1.0, 1.0])
        shininess = self.current_material.get('shininess', 32.0)
        
        gl.glUniform3f(ambient_loc, *ambient)
        gl.glUniform3f(diffuse_loc, *diffuse)
        gl.glUniform3f(specular_loc, *specular)
        gl.glUniform1f(shininess_loc, shininess)
                

4.2 场景对象管理

实现完整的场景图管理系统,支持层次化的对象组织、变换管理和属性继承。

class SceneObject:
    """场景对象基类"""
    
    def __init__(self, object_data):
        self.name = object_data.get('name', 'Unnamed')
        self.object_type = object_data.get('type', 'Mesh')
        self.transform = Transform()
        self.material = None
        self.children = []
        self.parent = None
        
        # 几何数据
        self.vertices = object_data.get('vertices', [])
        self.indices = object_data.get('indices', [])
        self.normals = object_data.get('normals', [])
        self.uvs = object_data.get('uvs', [])
        
        # 可见性和选择状态
        self.visible = True
        self.selected = False
        
    def add_child(self, child):
        """添加子对象"""
        if child.parent:
            child.parent.remove_child(child)
        
        self.children.append(child)
        child.parent = self
    
    def remove_child(self, child):
        """移除子对象"""
        if child in self.children:
            self.children.remove(child)
            child.parent = None
    
    def get_world_transform(self):
        """获取世界变换矩阵"""
        if self.parent:
            return self.parent.get_world_transform() * self.transform.matrix
        return self.transform.matrix
    
    def update_geometry(self, geometry_data):
        """更新几何数据"""
        self.vertices = geometry_data.get('vertices', self.vertices)
        self.indices = geometry_data.get('indices', self.indices)
        self.normals = geometry_data.get('normals', self.normals)
        self.uvs = geometry_data.get('uvs', self.uvs)

class Transform:
    """变换类"""
    
    def __init__(self):
        self.position = QVector3D(0, 0, 0)
        self.rotation = QVector3D(0, 0, 0)  # 欧拉角
        self.scale = QVector3D(1, 1, 1)
        self._matrix = QMatrix4x4()
        self._dirty = True
    
    @property
    def matrix(self):
        """获取变换矩阵"""
        if self._dirty:
            self._update_matrix()
        return self._matrix
    
    def _update_matrix(self):
        """更新变换矩阵"""
        self._matrix.setToIdentity()
        
        # 应用变换:缩放 -> 旋转 -> 平移
        self._matrix.translate(self.position)
        self._matrix.rotate(self.rotation.x(), QVector3D(1, 0, 0))
        self._matrix.rotate(self.rotation.y(), QVector3D(0, 1, 0))
        self._matrix.rotate(self.rotation.z(), QVector3D(0, 0, 1))
        self._matrix.scale(self.scale)
        
        self._dirty = False
    
    def set_position(self, x, y, z):
        """设置位置"""
        self.position = QVector3D(x, y, z)
        self._dirty = True
    
    def set_rotation(self, x, y, z):
        """设置旋转"""
        self.rotation = QVector3D(x, y, z)
        self._dirty = True
    
    def set_scale(self, x, y, z):
        """设置缩放"""
        self.scale = QVector3D(x, y, z)
        self._dirty = True

class SceneManager:
    """场景管理器"""
    
    def __init__(self):
        self.root_object = SceneObject({'name': 'Root', 'type': 'Group'})
        self.object_registry = {'Root': self.root_object}
        self.selected_objects = []
    
    def add_object(self, object_data, parent_name='Root'):
        """添加对象到场景"""
        scene_object = SceneObject(object_data)
        
        # 找到父对象
        parent = self.object_registry.get(parent_name, self.root_object)
        parent.add_child(scene_object)
        
        # 注册对象
        self.object_registry[scene_object.name] = scene_object
        
        return scene_object
    
    def remove_object(self, object_name):
        """从场景移除对象"""
        if object_name in self.object_registry:
            obj = self.object_registry[object_name]
            if obj.parent:
                obj.parent.remove_child(obj)
            
            # 递归移除子对象
            self._remove_children_recursive(obj)
            
            del self.object_registry[object_name]
    
    def _remove_children_recursive(self, obj):
        """递归移除子对象"""
        for child in obj.children[:]:  # 复制列表避免修改时出错
            self._remove_children_recursive(child)
            if child.name in self.object_registry:
                del self.object_registry[child.name]
    
    def get_object(self, object_name):
        """获取对象"""
        return self.object_registry.get(object_name)
    
    def select_object(self, object_name):
        """选择对象"""
        obj = self.get_object(object_name)
        if obj and obj not in self.selected_objects:
            obj.selected = True
            self.selected_objects.append(obj)
    
    def deselect_object(self, object_name):
        """取消选择对象"""
        obj = self.get_object(object_name)
        if obj and obj in self.selected_objects:
            obj.selected = False
            self.selected_objects.remove(obj)
    
    def clear_selection(self):
        """清除所有选择"""
        for obj in self.selected_objects:
            obj.selected = False
        self.selected_objects.clear()
    
    def get_all_objects(self):
        """获取所有对象"""
        return list(self.object_registry.values())
    
    def get_scene_hierarchy(self):
        """获取场景层次结构"""
        return self._build_hierarchy_recursive(self.root_object)
    
    def _build_hierarchy_recursive(self, obj):
        """递归构建层次结构"""
        hierarchy = {
            'name': obj.name,
            'type': obj.object_type,
            'visible': obj.visible,
            'selected': obj.selected,
            'children': []
        }
        
        for child in obj.children:
            hierarchy['children'].append(self._build_hierarchy_recursive(child))
        
        return hierarchy
                

5. 材质管理系统

5.1 PBR材质系统设计

基于物理的渲染(PBR)已成为现代三维图形的标准。我们的材质系统实现了完整的PBR工作流,支持金属度/粗糙度工作流和镜面反射/光泽度工作流。

import json
from pathlib import Path
from PIL import Image
import numpy as np

class PBRMaterial:
    """PBR材质类"""
    
    def __init__(self, name="DefaultMaterial"):
        self.name = name
        self.material_type = "PBR"
        
        # 基础颜色
        self.albedo = [0.8, 0.8, 0.8]  # RGB
        self.albedo_texture = None
        
        # 金属度
        self.metallic = 0.0
        self.metallic_texture = None
        
        # 粗糙度
        self.roughness = 0.5
        self.roughness_texture = None
        
        # 法线贴图
        self.normal_texture = None
        self.normal_strength = 1.0
        
        # 环境遮蔽
        self.ao_texture = None
        self.ao_strength = 1.0
        
        # 发光
        self.emission = [0.0, 0.0, 0.0]
        self.emission_texture = None
        self.emission_strength = 1.0
        
        # 透明度
        self.alpha = 1.0
        self.alpha_texture = None
        
        # 其他属性
        self.double_sided = False
        self.alpha_cutoff = 0.5
        self.blend_mode = "OPAQUE"  # OPAQUE, MASK, BLEND
        
    def set_albedo(self, color_or_path):
        """设置基础颜色"""
        if isinstance(color_or_path, (list, tuple)):
            self.albedo = list(color_or_path[:3])
            self.albedo_texture = None
        else:
            self.albedo_texture = str(color_or_path)
    
    def set_texture(self, texture_type, texture_path):
        """设置纹理"""
        texture_map = {
            'albedo': 'albedo_texture',
            'metallic': 'metallic_texture',
            'roughness': 'roughness_texture',
            'normal': 'normal_texture',
            'ao': 'ao_texture',
            'emission': 'emission_texture',
            'alpha': 'alpha_texture'
        }
        
        if texture_type in texture_map:
            setattr(self, texture_map[texture_type], str(texture_path))
    
    def to_dict(self):
        """转换为字典"""
        return {
            'name': self.name,
            'type': self.material_type,
            'albedo': self.albedo,
            'albedo_texture': self.albedo_texture,
            'metallic': self.metallic,
            'metallic_texture': self.metallic_texture,
            'roughness': self.roughness,
            'roughness_texture': self.roughness_texture,
            'normal_texture': self.normal_texture,
            'normal_strength': self.normal_strength,
            'ao_texture': self.ao_texture,
            'ao_strength': self.ao_strength,
            'emission': self.emission,
            'emission_texture': self.emission_texture,
            'emission_strength': self.emission_strength,
            'alpha': self.alpha,
            'alpha_texture': self.alpha_texture,
            'double_sided': self.double_sided,
            'alpha_cutoff': self.alpha_cutoff,
            'blend_mode': self.blend_mode
        }
    
    @classmethod
    def from_dict(cls, data):
        """从字典创建材质"""
        material = cls(data.get('name', 'Material'))
        
        for key, value in data.items():
            if hasattr(material, key):
                setattr(material, key, value)
        
        return material

class MaterialLibrary:
    """材质库管理器"""
    
    def __init__(self, library_path=None):
        self.library_path = library_path or Path.cwd() / "materials"
        self.library_path.mkdir(exist_ok=True)
        
        self.materials = {}
        self.categories = {}
        self.texture_cache = {}
        
        # 加载默认材质库
        self._load_default_materials()
        self._scan_library()
    
    def _load_default_materials(self):
        """加载默认材质"""
        # 基础材质
        default = PBRMaterial("Default")
        self.add_material(default, "Basic")
        
        # 金属材质
        metal = PBRMaterial("Metal")
        metal.albedo = [0.7, 0.7, 0.7]
        metal.metallic = 1.0
        metal.roughness = 0.1
        self.add_material(metal, "Metal")
        
        # 木材材质
        wood = PBRMaterial("Wood")
        wood.albedo = [0.6, 0.4, 0.2]
        wood.metallic = 0.0
        wood.roughness = 0.8
        self.add_material(wood, "Organic")
        
        # 玻璃材质
        glass = PBRMaterial("Glass")
        glass.albedo = [1.0, 1.0, 1.0]
        glass.metallic = 0.0
        glass.roughness = 0.0
        glass.alpha = 0.1
        glass.blend_mode = "BLEND"
        self.add_material(glass, "Transparent")
        
        # 混凝土材质
        concrete = PBRMaterial("Concrete")
        concrete.albedo = [0.5, 0.5, 0.5]
        concrete.metallic = 0.0
        concrete.roughness = 0.9
        self.add_material(concrete, "Stone")
    
    def _scan_library(self):
        """扫描材质库文件"""
        for category_dir in self.library_path.iterdir():
            if category_dir.is_dir():
                category_name = category_dir.name
                
                for material_file in category_dir.glob("*.json"):
                    try:
                        with open(material_file, 'r', encoding='utf-8') as f:
                            material_data = json.load(f)
                        
                        material = PBRMaterial.from_dict(material_data)
                        self.add_material(material, category_name)
                        
                    except Exception as e:
                        print(f"加载材质失败 {material_file}: {e}")
    
    def add_material(self, material, category="Custom"):
        """添加材质到库"""
        if category not in self.categories:
            self.categories[category] = []
        
        self.materials[material.name] = material
        if material.name not in self.categories[category]:
            self.categories[category].append(material.name)
    
    def get_material(self, material_name):
        """获取材质"""
        return self.materials.get(material_name)
    
    def remove_material(self, material_name):
        """移除材质"""
        if material_name in self.materials:
            # 从分类中移除
            for category_materials in self.categories.values():
                if material_name in category_materials:
                    category_materials.remove(material_name)
            
            # 从材质字典中移除
            del self.materials[material_name]
    
    def get_materials_by_category(self, category):
        """按分类获取材质"""
        if category in self.categories:
            return [self.materials[name] for name in self.categories[category] 
                   if name in self.materials]
        return []
    
    def save_material(self, material, category="Custom"):
        """保存材质到文件"""
        category_dir = self.library_path / category
        category_dir.mkdir(exist_ok=True)
        
        material_file = category_dir / f"{material.name}.json"
        
        with open(material_file, 'w', encoding='utf-8') as f:
            json.dump(material.to_dict(), f, indent=2, ensure_ascii=False)
        
        self.add_material(material, category)
    
    def import_material_pack(self, pack_path):
        """导入材质包"""
        pack_path = Path(pack_path)
        
        if pack_path.is_file() and pack_path.suffix == '.zip':
            # 解压材质包
            import zipfile
            with zipfile.ZipFile(pack_path, 'r') as zip_file:
                zip_file.extractall(self.library_path)
        elif pack_path.is_dir():
            # 复制目录
            import shutil
            shutil.copytree(pack_path, self.library_path / pack_path.name, 
                          dirs_exist_ok=True)
        
        # 重新扫描库
        self._scan_library()
    
    def export_material_pack(self, materials, output_path):
        """导出材质包"""
        import zipfile
        
        with zipfile.ZipFile(output_path, 'w') as zip_file:
            for material_name in materials:
                material = self.get_material(material_name)
                if material:
                    # 导出材质定义
                    material_data = json.dumps(material.to_dict(), indent=2)
                    zip_file.writestr(f"{material_name}.json", material_data)
                    
                    # 导出相关纹理
                    self._export_material_textures(material, zip_file)
    
    def _export_material_textures(self, material, zip_file):
        """导出材质纹理"""
        texture_attributes = [
            'albedo_texture', 'metallic_texture', 'roughness_texture',
            'normal_texture', 'ao_texture', 'emission_texture', 'alpha_texture'
        ]
        
        for attr in texture_attributes:
            texture_path = getattr(material, attr)
            if texture_path and Path(texture_path).exists():
                zip_file.write(texture_path, f"textures/{Path(texture_path).name}")

class MaterialManager:
    """材质管理器 - 处理材质的应用和切换"""
    
    def __init__(self, viewport, material_library):
        self.viewport = viewport
        self.material_library = material_library
        self.active_materials = {}  # 对象名 -> 材质名
        self.texture_loader = TextureLoader()
    
    def apply_material_to_object(self, object_name, material_name):
        """将材质应用到对象"""
        material = self.material_library.get_material(material_name)
        if not material:
            print(f"材质不存在: {material_name}")
            return False
        
        # 记录材质应用
        self.active_materials[object_name] = material_name
        
        # 加载材质纹理
        material_data = self._prepare_material_data(material)
        
        # 应用到视口
        self.viewport.set_material(material_data)
        
        return True
    
    def _prepare_material_data(self, material):
        """准备材质数据用于渲染"""
        material_data = {
            'albedo': material.albedo,
            'metallic': material.metallic,
            'roughness': material.roughness,
            'normal_strength': material.normal_strength,
            'ao_strength': material.ao_strength,
            'emission': material.emission,
            'emission_strength': material.emission_strength,
            'alpha': material.alpha
        }
        
        # 加载纹理
        texture_types = ['albedo', 'metallic', 'roughness', 'normal', 'ao', 'emission', 'alpha']
        
        for tex_type in texture_types:
            texture_path = getattr(material, f"{tex_type}_texture")
            if texture_path:
                texture_id = self.texture_loader.load_texture(texture_path)
                material_data[f"{tex_type}_texture_id"] = texture_id
        
        return material_data
    
    def switch_material_realtime(self, object_name, material_name):
        """实时切换材质"""
        if self.apply_material_to_object(object_name, material_name):
            # 触发重渲染
            self.viewport.update()
            return True
        return False
    
    def batch_apply_materials(self, material_assignments):
        """批量应用材质"""
        results = {}
        
        for object_name, material_name in material_assignments.items():
            results[object_name] = self.apply_material_to_object(object_name, material_name)
        
        # 批量更新渲染
        self.viewport.update()
        
        return results

class TextureLoader:
    """纹理加载器"""
    
    def __init__(self):
        self.loaded_textures = {}
        self.texture_cache_size = 100
    
    def load_texture(self, texture_path):
        """加载纹理到GPU"""
        texture_path = str(texture_path)
        
        # 检查缓存
        if texture_path in self.loaded_textures:
            return self.loaded_textures[texture_path]
        
        try:
            # 加载图像
            image = Image.open(texture_path)
            image = image.convert('RGBA')
            
            # 生成OpenGL纹理
            import OpenGL.GL as gl
            
            texture_id = gl.glGenTextures(1)
            gl.glBindTexture(gl.GL_TEXTURE_2D, texture_id)
            
            # 设置纹理参数
            gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_REPEAT)
            gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_REPEAT)
            gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR_MIPMAP_LINEAR)
            gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
            
            # 上传纹理数据
            img_data = np.array(image)
            gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA, image.width, image.height, 
                           0, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE, img_data)
            gl.glGenerateMipmap(gl.GL_TEXTURE_2D)
            
            # 缓存纹理ID
            self.loaded_textures[texture_path] = texture_id
            
            # 管理缓存大小
            if len(self.loaded_textures) > self.texture_cache_size:
                self._cleanup_texture_cache()
            
            return texture_id
            
        except Exception as e:
            print(f"纹理加载失败 {texture_path}: {e}")
            return None
    
    def _cleanup_texture_cache(self):
        """清理纹理缓存"""
        # 移除最旧的纹理
        oldest_textures = list(self.loaded_textures.items())[:10]
        
        import OpenGL.GL as gl
        for texture_path, texture_id in oldest_textures:
            gl.glDeleteTextures(1, [texture_id])
            del self.loaded_textures[texture_path]
                

5.2 实时材质预览系统

为了提供流畅的材质编辑体验,系统实现了实时材质预览功能,支持参数实时调整和即时反馈。

性能优化:材质预览系统采用增量更新策略,只重新计算发生变化的材质属性,大大提升了实时编辑的流畅度。

6. PyTorch算法优化

6.1 深度学习驱动的材质识别

利用深度学习技术自动识别和分类建筑材质,为用户提供智能化的材质推荐和应用功能。

材质分类神经网络

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import torchvision.transforms as transforms
from PIL import Image
import numpy as np
import json

class MaterialClassificationCNN(nn.Module):
    """材质分类卷积神经网络"""
    
    def __init__(self, num_classes=50, dropout_rate=0.3):
        super(MaterialClassificationCNN, self).__init__()
        
        # 特征提取层
        self.features = nn.Sequential(
            # Block 1
            nn.Conv2d(3, 64, kernel_size=3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.Conv2d(64, 64, kernel_size=3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            
            # Block 2
            nn.Conv2d(64, 128, kernel_size=3, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.Conv2d(128, 128, kernel_size=3, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            
            # Block 3
            nn.Conv2d(128, 256, kernel_size=3, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            
            # Block 4
            nn.Conv2d(256, 512, kernel_size=3, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
        )
        
        # 分类器
        self.classifier = nn.Sequential(
            nn.Dropout(dropout_rate),
            nn.Linear(512 * 7 * 7, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(dropout_rate),
            nn.Linear(4096, 2048),
            nn.ReLU(inplace=True),
            nn.Dropout(dropout_rate),
            nn.Linear(2048, num_classes)
        )
        
        # 材质属性预测分支
        self.material_properties = nn.Sequential(
            nn.Linear(512 * 7 * 7, 1024),
            nn.ReLU(inplace=True),
            nn.Dropout(dropout_rate),
            nn.Linear(1024, 512),
            nn.ReLU(inplace=True),
            nn.Linear(512, 4)  # metallic, roughness, specular, normal_strength
        )
        
    def forward(self, x):
        # 特征提取
        features = self.features(x)
        features_flat = features.view(features.size(0), -1)
        
        # 分类预测
        class_output = self.classifier(features_flat)
        
        # 材质属性预测
        properties_output = self.material_properties(features_flat)
        properties_output = torch.sigmoid(properties_output)  # 归一化到[0,1]
        
        return class_output, properties_output

class MaterialDataset(Dataset):
    """材质数据集"""
    
    def __init__(self, data_dir, annotations_file, transform=None, mode='train'):
        self.data_dir = Path(data_dir)
        self.transform = transform
        self.mode = mode
        
        # 加载标注数据
        with open(annotations_file, 'r', encoding='utf-8') as f:
            self.annotations = json.load(f)
        
        # 材质类别映射
        self.class_to_idx = self._build_class_mapping()
        self.num_classes = len(self.class_to_idx)
        
        # 数据样本列表
        self.samples = self._load_samples()
        
    def _build_class_mapping(self):
        """构建类别映射"""
        classes = set()
        for annotation in self.annotations:
            classes.add(annotation['material_type'])
        
        return {cls_name: idx for idx, cls_name in enumerate(sorted(classes))}
    
    def _load_samples(self):
        """加载样本数据"""
        samples = []
        
        for annotation in self.annotations:
            image_path = self.data_dir / annotation['image_path']
            if image_path.exists():
                samples.append({
                    'image_path': image_path,
                    'material_type': annotation['material_type'],
                    'properties': annotation.get('properties', {}),
                    'class_idx': self.class_to_idx[annotation['material_type']]
                })
        
        return samples
    
    def __len__(self):
        return len(self.samples)
    
    def __getitem__(self, idx):
        sample = self.samples[idx]
        
        # 加载图像
        image = Image.open(sample['image_path']).convert('RGB')
        
        # 应用变换
        if self.transform:
            image = self.transform(image)
        
        # 准备标签
        class_label = sample['class_idx']
        
        # 材质属性标签
        properties = sample['properties']
        properties_label = torch.tensor([
            properties.get('metallic', 0.0),
            properties.get('roughness', 0.5),
            properties.get('specular', 0.5),
            properties.get('normal_strength', 1.0)
        ], dtype=torch.float32)
        
        return image, class_label, properties_label

class MaterialClassifier:
    """材质分类器"""
    
    def __init__(self, model_path=None, device=None):
        self.device = device or torch.device('cuda' if torch.cuda.is_available() else 'cpu')
        self.model = MaterialClassificationCNN(num_classes=50)
        self.model.to(self.device)
        
        # 数据预处理
        self.transform = transforms.Compose([
            transforms.Resize((224, 224)),
            transforms.ToTensor(),
            transforms.Normalize(mean=[0.485, 0.456, 0.406], 
                               std=[0.229, 0.224, 0.225])
        ])
        
        # 加载预训练模型
        if model_path and Path(model_path).exists():
            self.load_model(model_path)
        
        # 类别名称映射
        self.idx_to_class = {}
    
    def train(self, train_dataset, val_dataset, epochs=100, lr=0.001):
        """训练模型"""
        # 数据加载器
        train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4)
        val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4)
        
        # 优化器和损失函数
        optimizer = optim.Adam(self.model.parameters(), lr=lr, weight_decay=1e-4)
        class_criterion = nn.CrossEntropyLoss()
        properties_criterion = nn.MSELoss()
        
        # 学习率调度器
        scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)
        
        best_val_acc = 0.0
        
        for epoch in range(epochs):
            # 训练阶段
            self.model.train()
            train_loss = 0.0
            train_correct = 0
            train_total = 0
            
            for batch_idx, (images, class_labels, properties_labels) in enumerate(train_loader):
                images = images.to(self.device)
                class_labels = class_labels.to(self.device)
                properties_labels = properties_labels.to(self.device)
                
                optimizer.zero_grad()
                
                # 前向传播
                class_outputs, properties_outputs = self.model(images)
                
                # 计算损失
                class_loss = class_criterion(class_outputs, class_labels)
                properties_loss = properties_criterion(properties_outputs, properties_labels)
                total_loss = class_loss + 0.1 * properties_loss  # 权重组合
                
                # 反向传播
                total_loss.backward()
                optimizer.step()
                
                # 统计
                train_loss += total_loss.item()
                _, predicted = class_outputs.max(1)
                train_total += class_labels.size(0)
                train_correct += predicted.eq(class_labels).sum().item()
                
                if batch_idx % 50 == 0:
                    print(f'Epoch {epoch}, Batch {batch_idx}, Loss: {total_loss.item():.4f}')
            
            # 验证阶段
            val_acc = self.validate(val_loader)
            
            # 更新学习率
            scheduler.step()
            
            # 保存最佳模型
            if val_acc > best_val_acc:
                best_val_acc = val_acc
                self.save_model(f'best_material_classifier_epoch_{epoch}.pth')
            
            print(f'Epoch {epoch}: Train Acc: {100.*train_correct/train_total:.2f}%, Val Acc: {val_acc:.2f}%')
    
    def validate(self, val_loader):
        """验证模型"""
        self.model.eval()
        correct = 0
        total = 0
        
        with torch.no_grad():
            for images, class_labels, _ in val_loader:
                images = images.to(self.device)
                class_labels = class_labels.to(self.device)
                
                class_outputs, _ = self.model(images)
                _, predicted = class_outputs.max(1)
                
                total += class_labels.size(0)
                correct += predicted.eq(class_labels).sum().item()
        
        return 100. * correct / total
    
    def predict(self, image_path):
        """预测单张图像的材质类型和属性"""
        self.model.eval()
        
        # 加载和预处理图像
        image = Image.open(image_path).convert('RGB')
        image_tensor = self.transform(image).unsqueeze(0).to(self.device)
        
        with torch.no_grad():
            class_outputs, properties_outputs = self.model(image_tensor)
            
            # 获取预测结果
            class_probs = F.softmax(class_outputs, dim=1)
            class_pred = class_outputs.argmax(dim=1).item()
            confidence = class_probs[0, class_pred].item()
            
            # 材质属性
            properties = properties_outputs[0].cpu().numpy()
            
            return {
                'material_class': self.idx_to_class.get(class_pred, f"Class_{class_pred}"),
                'confidence': confidence,
                'properties': {
                    'metallic': float(properties[0]),
                    'roughness': float(properties[1]),
                    'specular': float(properties[2]),
                    'normal_strength': float(properties[3])
                }
            }
    
    def save_model(self, path):
        """保存模型"""
        torch.save({
            'model_state_dict': self.model.state_dict(),
            'idx_to_class': self.idx_to_class
        }, path)
    
    def load_model(self, path):
        """加载模型"""
        checkpoint = torch.load(path, map_location=self.device)
        self.model.load_state_dict(checkpoint['model_state_dict'])
        self.idx_to_class = checkpoint.get('idx_to_class', {})

class RenderingOptimizer:
    """基于深度学习的渲染优化器"""
    
    def __init__(self):
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
        self.denoising_model = None
        self.super_resolution_model = None
        
    def setup_denoising_model(self):
        """设置去噪模型"""
        class DenoisingAutoencoder(nn.Module):
            def __init__(self):
                super().__init__()
                
                # 编码器
                self.encoder = nn.Sequential(
                    nn.Conv2d(3, 64, 3, padding=1),
                    nn.ReLU(),
                    nn.Conv2d(64, 128, 3, padding=1),
                    nn.ReLU(),
                    nn.MaxPool2d(2),
                    nn.Conv2d(128, 256, 3, padding=1),
                    nn.ReLU(),
                    nn.MaxPool2d(2)
                )
                
                # 解码器
                self.decoder = nn.Sequential(
                    nn.ConvTranspose2d(256, 128, 2, stride=2),
                    nn.ReLU(),
                    nn.ConvTranspose2d(128, 64, 2, stride=2),
                    nn.ReLU(),
                    nn.Conv2d(64, 3, 3, padding=1),
                    nn.Sigmoid()
                )
            
            def forward(self, x):
                encoded = self.encoder(x)
                decoded = self.decoder(encoded)
                return decoded
        
        self.denoising_model = DenoisingAutoencoder().to(self.device)
    
    def denoise_render(self, noisy_image):
        """对渲染图像进行去噪"""
        if self.denoising_model is None:
            self.setup_denoising_model()
        
        self.denoising_model.eval()
        
        # 预处理
        if isinstance(noisy_image, np.ndarray):
            image_tensor = torch.from_numpy(noisy_image).permute(2, 0, 1).float() / 255.0
        else:
            image_tensor = transforms.ToTensor()(noisy_image)
        
        image_tensor = image_tensor.unsqueeze(0).to(self.device)
        
        with torch.no_grad():
            denoised = self.denoising_model(image_tensor)
            
        # 后处理
        denoised_np = denoised.squeeze(0).permute(1, 2, 0).cpu().numpy()
        denoised_np = (denoised_np * 255).astype(np.uint8)
        
        return denoised_np
    
    def optimize_render_quality(self, render_params, target_quality='high'):
        """优化渲染参数以提升质量"""
        # 基于目标质量调整参数
        quality_settings = {
            'low': {'samples': 64, 'bounces': 3, 'resolution_scale': 0.5},
            'medium': {'samples': 128, 'bounces': 5, 'resolution_scale': 0.75},
            'high': {'samples': 256, 'bounces': 8, 'resolution_scale': 1.0},
            'ultra': {'samples': 512, 'bounces': 12, 'resolution_scale': 1.25}
        }
        
        if target_quality in quality_settings:
            settings = quality_settings[target_quality]
            
            optimized_params = render_params.copy()
            optimized_params.update(settings)
            
            # 基于场景复杂度进一步调整
            scene_complexity = self._analyze_scene_complexity(render_params)
            
            if scene_complexity > 0.8:  # 高复杂度场景
                optimized_params['samples'] = int(optimized_params['samples'] * 1.5)
                optimized_params['bounces'] += 2
            elif scene_complexity < 0.3:  # 低复杂度场景
                optimized_params['samples'] = int(optimized_params['samples'] * 0.7)
                optimized_params['bounces'] = max(3, optimized_params['bounces'] - 1)
            
            return optimized_params
        
        return render_params
    
    def _analyze_scene_complexity(self, render_params):
        """分析场景复杂度"""
        # 简化的复杂度评估
        complexity_factors = {
            'object_count': render_params.get('object_count', 10),
            'material_count': render_params.get('material_count', 5),
            'light_count': render_params.get('light_count', 3),
            'texture_resolution': render_params.get('avg_texture_resolution', 1024)
        }
        
        # 归一化因子
        normalized_complexity = (
            min(complexity_factors['object_count'] / 100, 1.0) * 0.3 +
            min(complexity_factors['material_count'] / 20, 1.0) * 0.2 +
            min(complexity_factors['light_count'] / 10, 1.0) * 0.2 +
            min(complexity_factors['texture_resolution'] / 4096, 1.0) * 0.3
        )
        
        return normalized_complexity
                    

6.2 场景优化算法

利用强化学习技术优化三维场景的渲染性能,通过智能LOD调整和遮挡剔除策略提升实时渲染效率。

class SceneOptimizationAgent:
    """场景优化智能代理"""
    
    def __init__(self, state_dim=20, action_dim=10):
        self.state_dim = state_dim
        self.action_dim = action_dim
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
        
        # DQN网络
        self.q_network = self._build_q_network()
        self.target_network = self._build_q_network()
        self.optimizer = optim.Adam(self.q_network.parameters(), lr=0.001)
        
        # 经验回放缓冲区
        self.memory = []
        self.memory_size = 10000
        self.batch_size = 32
        
        # 训练参数
        self.epsilon = 1.0
        self.epsilon_decay = 0.995
        self.epsilon_min = 0.01
        self.gamma = 0.95
        
    def _build_q_network(self):
        """构建Q网络"""
        return nn.Sequential(
            nn.Linear(self.state_dim, 128),
            nn.ReLU(),
            nn.Linear(128, 256),
            nn.ReLU(),
            nn.Linear(256, 128),
            nn.ReLU(),
            nn.Linear(128, self.action_dim)
        ).to(self.device)
    
    def get_scene_state(self, scene_info):
        """提取场景状态特征"""
        # 场景复杂度特征
        object_count = len(scene_info.get('objects', []))
        triangle_count = sum(obj.get('triangle_count', 0) for obj in scene_info.get('objects', []))
        material_count = len(scene_info.get('materials', []))
        
        # 渲染性能特征
        fps = scene_info.get('current_fps', 60)
        gpu_usage = scene_info.get('gpu_usage', 0.5)
        memory_usage = scene_info.get('memory_usage', 0.5)
        
        # 视图相关特征
        camera_distance = scene_info.get('camera_distance', 10)
        view_frustum_objects = scene_info.get('visible_objects', object_count)
        
        # 质量设置
        current_lod_level = scene_info.get('lod_level', 1)
        shadow_quality = scene_info.get('shadow_quality', 1)
        texture_quality = scene_info.get('texture_quality', 1)
        
        state = np.array([
            object_count / 1000,  # 归一化
            triangle_count / 100000,
            material_count / 50,
            fps / 60,
            gpu_usage,
            memory_usage,
            camera_distance / 100,
            view_frustum_objects / object_count if object_count > 0 else 0,
            current_lod_level / 3,
            shadow_quality / 3,
            texture_quality / 3,
            # 添加更多特征...
        ] + [0] * (self.state_dim - 11))  # 填充到指定维度
        
        return state[:self.state_dim]
    
    def select_action(self, state):
        """选择优化动作"""
        if np.random.random() < self.epsilon:
            return np.random.randint(self.action_dim)
        
        state_tensor = torch.FloatTensor(state).unsqueeze(0).to(self.device)
        q_values = self.q_network(state_tensor)
        return q_values.argmax().item()
    
    def apply_optimization_action(self, action, scene_manager):
        """应用优化动作"""
        action_map = {
            0: lambda: scene_manager.adjust_lod_level(1),      # 提高LOD
            1: lambda: scene_manager.adjust_lod_level(-1),     # 降低LOD
            2: lambda: scene_manager.adjust_shadow_quality(1), # 提高阴影质量
            3: lambda: scene_manager.adjust_shadow_quality(-1), # 降低阴影质量
            4: lambda: scene_manager.adjust_texture_quality(1), # 提高纹理质量
            5: lambda: scene_manager.adjust_texture_quality(-1), # 降低纹理质量
            6: lambda: scene_manager.enable_occlusion_culling(True), # 启用遮挡剔除
            7: lambda: scene_manager.enable_occlusion_culling(False), # 禁用遮挡剔除
            8: lambda: scene_manager.adjust_render_distance(1.2), # 增加渲染距离
            9: lambda: scene_manager.adjust_render_distance(0.8), # 减少渲染距离
        }
        
        if action in action_map:
            action_map[action]()
    
    def calculate_reward(self, old_state, new_state):
        """计算奖励函数"""
        # 性能指标
        fps_old = old_state[3] * 60
        fps_new = new_state[3] * 60
        fps_reward = (fps_new - fps_old) / 60  # FPS改善奖励
        
        # GPU使用率奖励(保持在合理范围)
        gpu_old = old_state[4]
        gpu_new = new_state[4]
        gpu_reward = 0
        if 0.6 <= gpu_new <= 0.85:  # 理想GPU使用率范围
            gpu_reward = 0.1
        elif gpu_new > 0.9:  # 过高使用率惩罚
            gpu_reward = -0.2
        
        # 质量保持奖励
        quality_old = (old_state[8] + old_state[9] + old_state[10]) / 3
        quality_new = (new_state[8] + new_state[9] + new_state[10]) / 3
        quality_reward = -(quality_old - quality_new)  # 质量下降惩罚
        
        total_reward = fps_reward + gpu_reward + quality_reward
        return total_reward
    
    def train_step(self):
        """训练步骤"""
        if len(self.memory) < self.batch_size:
            return
        
        # 采样批次
        batch = random.sample(self.memory, self.batch_size)
        states = torch.FloatTensor([e[0] for e in batch]).to(self.device)
        actions = torch.LongTensor([e[1] for e in batch]).to(self.device)
        rewards = torch.FloatTensor([e[2] for e in batch]).to(self.device)
        next_states = torch.FloatTensor([e[3] for e in batch]).to(self.device)
        dones = torch.BoolTensor([e[4] for e in batch]).to(self.device)
        
        # 当前Q值
        current_q_values = self.q_network(states).gather(1, actions.unsqueeze(1))
        
        # 目标Q值
        next_q_values = self.target_network(next_states).max(1)[0].detach()
        target_q_values = rewards + (self.gamma * next_q_values * ~dones)
        
        # 计算损失
        loss = F.mse_loss(current_q_values.squeeze(), target_q_values)
        
        # 优化
        self.optimizer.zero_grad()
        loss.backward()
        self.optimizer.step()
        
        # 更新epsilon
        if self.epsilon > self.epsilon_min:
            self.epsilon *= self.epsilon_decay
    
    def update_target_network(self):
        """更新目标网络"""
        self.target_network.load_state_dict(self.q_network.state_dict())
                

7. 数据交互管道

7.1 双向数据同步机制

实现3ds Max与PySide6应用程序之间的实时双向数据同步,确保设计变更能够及时反映到所有相关组件中。

import asyncio
import json
import websockets
from threading import Thread
import queue
import time

class DataSynchronizer:
    """数据同步器 - 管理3ds Max与PySide6之间的数据交换"""
    
    def __init__(self, max_interface, scene_manager):
        self.max_interface = max_interface
        self.scene_manager = scene_manager
        
        # WebSocket服务器配置
        self.ws_host = "localhost"
        self.ws_port = 8765
        self.ws_server = None
        self.clients = set()
        
        # 数据队列
        self.max_to_pyside_queue = queue.Queue()
        self.pyside_to_max_queue = queue.Queue()
        
        # 同步状态
        self.sync_enabled = True
        self.sync_interval = 0.1  # 100ms
        
        # 数据变更监听器
        self.change_listeners = []
        
        # 启动同步服务
        self.start_sync_service()
    
    def start_sync_service(self):
        """启动同步服务"""
        # 启动WebSocket服务器
        self.ws_thread = Thread(target=self._run_websocket_server, daemon=True)
        self.ws_thread.start()
        
        # 启动数据同步循环
        self.sync_thread = Thread(target=self._sync_loop, daemon=True)
        self.sync_thread.start()
        
        print("数据同步服务已启动")
    
    def _run_websocket_server(self):
        """运行WebSocket服务器"""
        async def handle_client(websocket, path):
            self.clients.add(websocket)
            try:
                async for message in websocket:
                    await self._handle_websocket_message(websocket, message)
            except websockets.exceptions.ConnectionClosed:
                pass
            finally:
                self.clients.remove(websocket)
        
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        
        start_server = websockets.serve(handle_client, self.ws_host, self.ws_port)
        loop.run_until_complete(start_server)
        loop.run_forever()
    
    async def _handle_websocket_message(self, websocket, message):
        """处理WebSocket消息"""
        try:
            data = json.loads(message)
            message_type = data.get('type')
            
            if message_type == 'scene_update':
                self._handle_scene_update(data['payload'])
            elif message_type == 'material_change':
                self._handle_material_change(data['payload'])
            elif message_type == 'object_transform':
                self._handle_object_transform(data['payload'])
            elif message_type == 'sync_request':
                await self._send_full_scene_sync(websocket)
                
        except json.JSONDecodeError:
            print(f"无效的JSON消息: {message}")
        except Exception as e:
            print(f"处理WebSocket消息时出错: {e}")
    
    def _sync_loop(self):
        """数据同步主循环"""
        while self.sync_enabled:
            try:
                # 处理从3ds Max到PySide6的数据
                self._process_max_to_pyside_queue()
                
                # 处理从PySide6到3ds Max的数据
                self._process_pyside_to_max_queue()
                
                # 检查3ds Max场景变更
                self._check_max_scene_changes()
                
                time.sleep(self.sync_interval)
                
            except Exception as e:
                print(f"同步循环错误: {e}")
                time.sleep(1)  # 错误时等待更长时间
    
    def _process_max_to_pyside_queue(self):
        """处理从3ds Max到PySide6的数据队列"""
        while not self.max_to_pyside_queue.empty():
            try:
                data = self.max_to_pyside_queue.get_nowait()
                self._apply_data_to_pyside(data)
                self._broadcast_to_clients(data)
            except queue.Empty:
                break
            except Exception as e:
                print(f"处理Max到PySide6数据时出错: {e}")
    
    def _process_pyside_to_max_queue(self):
        """处理从PySide6到3ds Max的数据队列"""
        while not self.pyside_to_max_queue.empty():
            try:
                data = self.pyside_to_max_queue.get_nowait()
                self._apply_data_to_max(data)
            except queue.Empty:
                break
            except Exception as e:
                print(f"处理PySide6到Max数据时出错: {e}")
    
    def _check_max_scene_changes(self):
        """检查3ds Max场景变更"""
        try:
            current_scene_info = self.max_interface.get_scene_info()
            
            # 比较与上次场景信息的差异
            if hasattr(self, '_last_scene_info'):
                changes = self._compute_scene_differences(
                    self._last_scene_info, current_scene_info
                )
                
                if changes:
                    # 将变更添加到同步队列
                    self.max_to_pyside_queue.put({
                        'type': 'scene_changes',
                        'changes': changes,
                        'timestamp': time.time()
                    })
            
            self._last_scene_info = current_scene_info
            
        except Exception as e:
            print(f"检查Max场景变更时出错: {e}")
    
    def _compute_scene_differences(self, old_scene, new_scene):
        """计算场景差异"""
        changes = []
        
        # 检查对象变更
        old_objects = {obj['name']: obj for obj in old_scene.get('objects', [])}
        new_objects = {obj['name']: obj for obj in new_scene.get('objects', [])}
        
        # 新增对象
        for name, obj in new_objects.items():
            if name not in old_objects:
                changes.append({
                    'type': 'object_added',
                    'object': obj
                })
        
        # 删除对象
        for name in old_objects:
            if name not in new_objects:
                changes.append({
                    'type': 'object_removed',
                    'object_name': name
                })
        
        # 修改对象
        for name, new_obj in new_objects.items():
            if name in old_objects:
                old_obj = old_objects[name]
                if old_obj['transform'] != new_obj['transform']:
                    changes.append({
                        'type': 'object_transformed',
                        'object_name': name,
                        'transform': new_obj['transform']
                    })
        
        # 检查材质变更
        old_materials = {mat['name']: mat for mat in old_scene.get('materials', [])}
        new_materials = {mat['name']: mat for mat in new_scene.get('materials', [])}
        
        for name, new_mat in new_materials.items():
            if name not in old_materials:
                changes.append({
                    'type': 'material_added',
                    'material': new_mat
                })
            elif old_materials[name] != new_mat:
                changes.append({
                    'type': 'material_modified',
                    'material': new_mat
                })
        
        return changes
    
    def _apply_data_to_pyside(self, data):
        """将数据应用到PySide6场景"""
        data_type = data.get('type')
        
        if data_type == 'scene_changes':
            for change in data['changes']:
                self._apply_scene_change_to_pyside(change)
        elif data_type == 'material_update':
            self._apply_material_update_to_pyside(data)
        elif data_type == 'object_update':
            self._apply_object_update_to_pyside(data)
    
    def _apply_scene_change_to_pyside(self, change):
        """应用场景变更到PySide6"""
        change_type = change['type']
        
        if change_type == 'object_added':
            self.scene_manager.add_object(change['object'])
        elif change_type == 'object_removed':
            self.scene_manager.remove_object(change['object_name'])
        elif change_type == 'object_transformed':
            obj = self.scene_manager.get_object(change['object_name'])
            if obj:
                transform = change['transform']
                obj.transform.set_position(*transform['position'])
                obj.transform.set_rotation(*transform['rotation'])
                obj.transform.set_scale(*transform['scale'])
        elif change_type == 'material_modified':
            # 更新材质
            material_data = change['material']
            # 应用材质变更逻辑
    
    def _apply_data_to_max(self, data):
        """将数据应用到3ds Max"""
        data_type = data.get('type')
        
        if data_type == 'object_transform':
            self._apply_transform_to_max(data)
        elif data_type == 'material_assignment':
            self._apply_material_assignment_to_max(data)
        elif data_type == 'scene_modification':
            self._apply_scene_modification_to_max(data)
    
    def _apply_transform_to_max(self, data):
        """应用变换到3ds Max对象"""
        object_name = data['object_name']
        transform = data['transform']
        
        script = f"""
        obj = getNodeByName "{object_name}"
        if obj != undefined then (
            obj.transform = matrix3 {transform['matrix']}
        )
        """
        
        self.max_interface.execute_script(script)
    
    def _apply_material_assignment_to_max(self, data):
        """应用材质分配到3ds Max"""
        object_name = data['object_name']
        material_name = data['material_name']
        
        script = f"""
        obj = getNodeByName "{object_name}"
        mat = getNodeByName "{material_name}"
        if obj != undefined and mat != undefined then (
            obj.material = mat
        )
        """
        
        self.max_interface.execute_script(script)
    
    async def _broadcast_to_clients(self, data):
        """向所有WebSocket客户端广播数据"""
        if self.clients:
            message = json.dumps(data)
            await asyncio.gather(
                *[client.send(message) for client in self.clients],
                return_exceptions=True
            )
    
    async def _send_full_scene_sync(self, websocket):
        """发送完整场景同步数据"""
        try:
            scene_info = self.max_interface.get_scene_info()
            sync_data = {
                'type': 'full_scene_sync',
                'scene': scene_info,
                'timestamp': time.time()
            }
            
            await websocket.send(json.dumps(sync_data))
            
        except Exception as e:
            print(f"发送完整场景同步时出错: {e}")
    
    def sync_object_from_pyside_to_max(self, object_name, object_data):
        """从PySide6同步对象到3ds Max"""
        sync_data = {
            'type': 'object_update',
            'object_name': object_name,
            'object_data': object_data,
            'timestamp': time.time()
        }
        
        self.pyside_to_max_queue.put(sync_data)
    
    def sync_material_from_pyside_to_max(self, material_name, material_data):
        """从PySide6同步材质到3ds Max"""
        sync_data = {
            'type': 'material_update',
            'material_name': material_name,
            'material_data': material_data,
            'timestamp': time.time()
        }
        
        self.pyside_to_max_queue.put(sync_data)
    
    def register_change_listener(self, listener):
        """注册变更监听器"""
        self.change_listeners.append(listener)
    
    def notify_change_listeners(self, change_data):
        """通知变更监听器"""
        for listener in self.change_listeners:
            try:
                listener(change_data)
            except Exception as e:
                print(f"通知变更监听器时出错: {e}")
    
    def stop_sync_service(self):
        """停止同步服务"""
        self.sync_enabled = False
        
        if self.ws_server:
            self.ws_server.close()
        
        print("数据同步服务已停止")

class DataValidator:
    """数据验证器 - 确保同步数据的完整性和一致性"""
    
    @staticmethod
    def validate_object_data(object_data):
        """验证对象数据"""
        required_fields = ['name', 'type', 'transform']
        
        for field in required_fields:
            if field not in object_data:
                return False, f"缺少必需字段: {field}"
        
        # 验证变换数据
        transform = object_data['transform']
        if not isinstance(transform, dict):
            return False, "变换数据必须是字典类型"
        
        transform_fields = ['position', 'rotation', 'scale']
        for field in transform_fields:
            if field not in transform:
                return False, f"变换数据缺少字段: {field}"
            
            if not isinstance(transform[field], (list, tuple)) or len(transform[field]) != 3:
                return False, f"变换字段 {field} 必须是长度为3的数组"
        
        return True, "验证通过"
    
    @staticmethod
    def validate_material_data(material_data):
        """验证材质数据"""
        required_fields = ['name', 'type']
        
        for field in required_fields:
            if field not in material_data:
                return False, f"缺少必需字段: {field}"
        
        # 验证颜色值
        if 'albedo' in material_data:
            albedo = material_data['albedo']
            if not isinstance(albedo, (list, tuple)) or len(albedo) != 3:
                return False, "albedo必须是长度为3的颜色数组"
            
            if not all(0 <= c <= 1 for c in albedo):
                return False, "albedo颜色值必须在0-1范围内"
        
        # 验证数值参数
        numeric_params = ['metallic', 'roughness', 'alpha']
        for param in numeric_params:
            if param in material_data:
                value = material_data[param]
                if not isinstance(value, (int, float)) or not 0 <= value <= 1:
                    return False, f"{param}参数必须是0-1范围内的数值"
        
        return True, "验证通过"
    
    @staticmethod
    def sanitize_data(data):
        """清理和标准化数据"""
        if isinstance(data, dict):
            sanitized = {}
            for key, value in data.items():
                sanitized[key] = DataValidator.sanitize_data(value)
            return sanitized
        elif isinstance(data, list):
            return [DataValidator.sanitize_data(item) for item in data]
        elif isinstance(data, str):
            return data.strip()
        elif isinstance(data, (int, float)):
            return data
        elif isinstance(data, bool):
            return data
        else:
            return str(data)
                

8. 性能优化策略

8.1 渲染性能优化

通过多种技术手段优化三维渲染性能,包括几何体优化、纹理压缩、LOD管理等。

优化目标:在保证视觉质量的前提下,实现60FPS的流畅渲染性能,支持复杂建筑场景的实时预览。

9. 实际应用案例

9.1 商业建筑项目可视化

在某大型商业综合体项目中,设计团队使用本系统实现了从概念设计到施工图阶段的全流程可视化。通过实时材质切换功能,客户能够直观地比较不同材质方案的视觉效果,大大提高了设计决策效率。

9.2 数字孪生城市规划

在智慧城市建设项目中,本系统被用于构建城市区域的数字孪生模型。通过与GIS数据的集成,实现了城市建筑群的三维可视化和动态更新。

10. 完整系统代码实现

以下是建筑可视化与数字孪生城市建模系统的完整PySide6实现代码:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
建筑可视化与数字孪生城市建模技术应用
PySide6与3ds Max集成的建筑设计方案三维预览系统

作者: 丁林松
版本: 1.0.0
创建时间: 2024年
"""

import sys
import os
import json
import numpy as np
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Any
import asyncio
import threading
import time

# PySide6 imports
from PySide6.QtWidgets import *
from PySide6.QtCore import *
from PySide6.QtGui import *
from PySide6.QtOpenGL import *
from PySide6.QtOpenGLWidgets import QOpenGLWidget

# OpenGL imports
try:
    import OpenGL.GL as gl
    import OpenGL.GL.shaders as shaders
    OPENGL_AVAILABLE = True
except ImportError:
    OPENGL_AVAILABLE = False
    print("OpenGL未安装,某些功能可能不可用")

# PyTorch imports
try:
    import torch
    import torch.nn as nn
    import torch.nn.functional as F
    PYTORCH_AVAILABLE = True
except ImportError:
    PYTORCH_AVAILABLE = False
    print("PyTorch未安装,AI功能将不可用")

# PIL imports
try:
    from PIL import Image
    PIL_AVAILABLE = True
except ImportError:
    PIL_AVAILABLE = False
    print("PIL未安装,图像处理功能可能受限")


class ArchitecturalVisualizationSystem(QMainWindow):
    """建筑可视化系统主窗口"""
    
    def __init__(self):
        super().__init__()
        self.setWindowTitle("建筑可视化与数字孪生城市建模系统 - 作者:丁林松")
        self.setGeometry(100, 100, 1600, 1000)
        
        # 系统组件初始化
        self.scene_manager = SceneManager()
        self.material_library = MaterialLibrary()
        self.material_manager = None
        self.data_synchronizer = None
        
        # 3ds Max接口
        self.max_interface = MaxScriptInterface() if self._check_max_available() else None
        
        # AI组件
        if PYTORCH_AVAILABLE:
            self.material_classifier = MaterialClassifier()
            self.rendering_optimizer = RenderingOptimizer()
        
        # 初始化用户界面
        self.init_ui()
        
        # 初始化系统组件
        self.init_system_components()
        
        # 设置样式
        self.apply_theme()
        
        print("建筑可视化系统初始化完成")
    
    def _check_max_available(self):
        """检查3ds Max是否可用"""
        try:
            # 尝试查找3ds Max安装路径
            possible_paths = [
                r"C:\Program Files\Autodesk\3ds Max 2024\3dsmax.exe",
                r"C:\Program Files\Autodesk\3ds Max 2023\3dsmax.exe",
                r"C:\Program Files\Autodesk\3ds Max 2022\3dsmax.exe"
            ]
            
            for path in possible_paths:
                if os.path.exists(path):
                    return True
            return False
        except:
            return False
    
    def init_ui(self):
        """初始化用户界面"""
        # 创建中央部件
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        
        # 主布局
        main_layout = QHBoxLayout(central_widget)
        main_layout.setSpacing(10)
        main_layout.setContentsMargins(10, 10, 10, 10)
        
        # 左侧面板
        left_panel = self.create_left_panel()
        main_layout.addWidget(left_panel, 1)
        
        # 中央视口
        if OPENGL_AVAILABLE:
            self.viewport = Viewport3D(self)
            main_layout.addWidget(self.viewport, 3)
        else:
            # OpenGL不可用时的替代界面
            placeholder = QLabel("OpenGL不可用\n请安装OpenGL支持")
            placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
            placeholder.setStyleSheet("QLabel { background-color: #f0f0f0; border: 2px dashed #ccc; }")
            main_layout.addWidget(placeholder, 3)
        
        # 右侧面板
        right_panel = self.create_right_panel()
        main_layout.addWidget(right_panel, 1)
        
        # 创建菜单栏
        self.create_menu_bar()
        
        # 创建工具栏
        self.create_toolbar()
        
        # 创建状态栏
        self.create_status_bar()
    
    def create_left_panel(self):
        """创建左侧控制面板"""
        panel = QWidget()
        layout = QVBoxLayout(panel)
        
        # 项目管理器
        project_group = QGroupBox("项目管理")
        project_layout = QVBoxLayout(project_group)
        
        # 新建项目按钮
        new_project_btn = QPushButton("新建项目")
        new_project_btn.clicked.connect(self.new_project)
        project_layout.addWidget(new_project_btn)
        
        # 打开项目按钮
        open_project_btn = QPushButton("打开项目")
        open_project_btn.clicked.connect(self.open_project)
        project_layout.addWidget(open_project_btn)
        
        # 导入3ds Max文件按钮
        import_max_btn = QPushButton("导入3ds Max文件")
        import_max_btn.clicked.connect(self.import_max_file)
        project_layout.addWidget(import_max_btn)
        
        # 导出按钮
        export_btn = QPushButton("导出场景")
        export_btn.clicked.connect(self.export_scene)
        project_layout.addWidget(export_btn)
        
        layout.addWidget(project_group)
        
        # 场景层次结构
        scene_group = QGroupBox("场景层次")
        scene_layout = QVBoxLayout(scene_group)
        
        self.scene_tree = QTreeWidget()
        self.scene_tree.setHeaderLabel("对象")
        self.scene_tree.itemSelectionChanged.connect(self.on_object_selected)
        scene_layout.addWidget(self.scene_tree)
        
        layout.addWidget(scene_group)
        
        # 材质库
        material_group = QGroupBox("材质库")
        material_layout = QVBoxLayout(material_group)
        
        # 材质分类选择
        self.material_category_combo = QComboBox()
        self.material_category_combo.addItems(["所有材质", "基础", "金属", "有机", "透明", "石材"])
        self.material_category_combo.currentTextChanged.connect(self.filter_materials)
        material_layout.addWidget(self.material_category_combo)
        
        # 材质列表
        self.material_list = QListWidget()
        self.material_list.itemDoubleClicked.connect(self.apply_material)
        material_layout.addWidget(self.material_list)
        
        # 材质管理按钮
        material_btn_layout = QHBoxLayout()
        add_material_btn = QPushButton("添加")
        add_material_btn.clicked.connect(self.add_material)
        edit_material_btn = QPushButton("编辑")
        edit_material_btn.clicked.connect(self.edit_material)
        material_btn_layout.addWidget(add_material_btn)
        material_btn_layout.addWidget(edit_material_btn)
        material_layout.addLayout(material_btn_layout)
        
        layout.addWidget(material_group)
        
        # AI功能面板
        if PYTORCH_AVAILABLE:
            ai_group = QGroupBox("AI功能")
            ai_layout = QVBoxLayout(ai_group)
            
            # 智能材质识别
            ai_material_btn = QPushButton("智能材质识别")
            ai_material_btn.clicked.connect(self.smart_material_recognition)
            ai_layout.addWidget(ai_material_btn)
            
            # 场景优化
            ai_optimize_btn = QPushButton("场景优化")
            ai_optimize_btn.clicked.connect(self.optimize_scene)
            ai_layout.addWidget(ai_optimize_btn)
            
            layout.addWidget(ai_group)
        
        layout.addStretch()
        return panel
    
    def create_right_panel(self):
        """创建右侧属性面板"""
        panel = QWidget()
        layout = QVBoxLayout(panel)
        
        # 对象属性
        object_group = QGroupBox("对象属性")
        object_layout = QFormLayout(object_group)
        
        # 变换属性
        self.position_x = QDoubleSpinBox()
        self.position_x.setRange(-1000, 1000)
        self.position_x.valueChanged.connect(self.update_object_transform)
        
        self.position_y = QDoubleSpinBox()
        self.position_y.setRange(-1000, 1000)
        self.position_y.valueChanged.connect(self.update_object_transform)
        
        self.position_z = QDoubleSpinBox()
        self.position_z.setRange(-1000, 1000)
        self.position_z.valueChanged.connect(self.update_object_transform)
        
        pos_layout = QHBoxLayout()
        pos_layout.addWidget(self.position_x)
        pos_layout.addWidget(self.position_y)
        pos_layout.addWidget(self.position_z)
        
        object_layout.addRow("位置:", pos_layout)
        
        # 旋转属性
        self.rotation_x = QDoubleSpinBox()
        self.rotation_x.setRange(-360, 360)
        self.rotation_x.valueChanged.connect(self.update_object_transform)
        
        self.rotation_y = QDoubleSpinBox()
        self.rotation_y.setRange(-360, 360)
        self.rotation_y.valueChanged.connect(self.update_object_transform)
        
        self.rotation_z = QDoubleSpinBox()
        self.rotation_z.setRange(-360, 360)
        self.rotation_z.valueChanged.connect(self.update_object_transform)
        
        rot_layout = QHBoxLayout()
        rot_layout.addWidget(self.rotation_x)
        rot_layout.addWidget(self.rotation_y)
        rot_layout.addWidget(self.rotation_z)
        
        object_layout.addRow("旋转:", rot_layout)
        
        # 缩放属性
        self.scale_x = QDoubleSpinBox()
        self.scale_x.setRange(0.1, 10.0)
        self.scale_x.setValue(1.0)
        self.scale_x.valueChanged.connect(self.update_object_transform)
        
        self.scale_y = QDoubleSpinBox()
        self.scale_y.setRange(0.1, 10.0)
        self.scale_y.setValue(1.0)
        self.scale_y.valueChanged.connect(self.update_object_transform)
        
        self.scale_z = QDoubleSpinBox()
        self.scale_z.setRange(0.1, 10.0)
        self.scale_z.setValue(1.0)
        self.scale_z.valueChanged.connect(self.update_object_transform)
        
        scale_layout = QHBoxLayout()
        scale_layout.addWidget(self.scale_x)
        scale_layout.addWidget(self.scale_y)
        scale_layout.addWidget(self.scale_z)
        
        object_layout.addRow("缩放:", scale_layout)
        
        layout.addWidget(object_group)
        
        # 材质属性
        material_group = QGroupBox("材质属性")
        material_layout = QFormLayout(material_group)
        
        # 基础颜色
        self.albedo_color_btn = QPushButton()
        self.albedo_color_btn.setFixedHeight(30)
        self.albedo_color_btn.setStyleSheet("background-color: rgb(204, 204, 204);")
        self.albedo_color_btn.clicked.connect(self.choose_albedo_color)
        material_layout.addRow("基础颜色:", self.albedo_color_btn)
        
        # 金属度
        self.metallic_slider = QSlider(Qt.Orientation.Horizontal)
        self.metallic_slider.setRange(0, 100)
        self.metallic_slider.setValue(0)
        self.metallic_slider.valueChanged.connect(self.update_material_properties)
        
        self.metallic_label = QLabel("0.00")
        metallic_layout = QHBoxLayout()
        metallic_layout.addWidget(self.metallic_slider)
        metallic_layout.addWidget(self.metallic_label)
        material_layout.addRow("金属度:", metallic_layout)
        
        # 粗糙度
        self.roughness_slider = QSlider(Qt.Orientation.Horizontal)
        self.roughness_slider.setRange(0, 100)
        self.roughness_slider.setValue(50)
        self.roughness_slider.valueChanged.connect(self.update_material_properties)
        
        self.roughness_label = QLabel("0.50")
        roughness_layout = QHBoxLayout()
        roughness_layout.addWidget(self.roughness_slider)
        roughness_layout.addWidget(self.roughness_label)
        material_layout.addRow("粗糙度:", roughness_layout)
        
        layout.addWidget(material_group)
        
        # 渲染设置
        render_group = QGroupBox("渲染设置")
        render_layout = QFormLayout(render_group)
        
        # 质量设置
        self.quality_combo = QComboBox()
        self.quality_combo.addItems(["低", "中", "高", "超高"])
        self.quality_combo.setCurrentText("高")
        self.quality_combo.currentTextChanged.connect(self.update_render_quality)
        render_layout.addRow("渲染质量:", self.quality_combo)
        
        # 阴影开关
        self.shadow_checkbox = QCheckBox("启用阴影")
        self.shadow_checkbox.setChecked(True)
        self.shadow_checkbox.toggled.connect(self.toggle_shadows)
        render_layout.addRow("", self.shadow_checkbox)
        
        # 环境光遮蔽
        self.ao_checkbox = QCheckBox("环境光遮蔽")
        self.ao_checkbox.setChecked(True)
        self.ao_checkbox.toggled.connect(self.toggle_ao)
        render_layout.addRow("", self.ao_checkbox)
        
        layout.addWidget(render_group)
        
        # 3ds Max集成
        if self.max_interface:
            max_group = QGroupBox("3ds Max集成")
            max_layout = QVBoxLayout(max_group)
            
            # 连接状态
            self.max_status_label = QLabel("未连接")
            self.max_status_label.setStyleSheet("color: red;")
            max_layout.addWidget(self.max_status_label)
            
            # 连接按钮
            connect_max_btn = QPushButton("连接3ds Max")
            connect_max_btn.clicked.connect(self.connect_to_max)
            max_layout.addWidget(connect_max_btn)
            
            # 同步按钮
            sync_btn = QPushButton("同步场景")
            sync_btn.clicked.connect(self.sync_with_max)
            max_layout.addWidget(sync_btn)
            
            layout.addWidget(max_group)
        
        layout.addStretch()
        return panel
    
    def create_menu_bar(self):
        """创建菜单栏"""
        menubar = self.menuBar()
        
        # 文件菜单
        file_menu = menubar.addMenu("文件")
        
        new_action = QAction("新建项目", self)
        new_action.setShortcut("Ctrl+N")
        new_action.triggered.connect(self.new_project)
        file_menu.addAction(new_action)
        
        open_action = QAction("打开项目", self)
        open_action.setShortcut("Ctrl+O")
        open_action.triggered.connect(self.open_project)
        file_menu.addAction(open_action)
        
        save_action = QAction("保存项目", self)
        save_action.setShortcut("Ctrl+S")
        save_action.triggered.connect(self.save_project)
        file_menu.addAction(save_action)
        
        file_menu.addSeparator()
        
        import_action = QAction("导入3ds Max文件", self)
        import_action.triggered.connect(self.import_max_file)
        file_menu.addAction(import_action)
        
        export_action = QAction("导出场景", self)
        export_action.triggered.connect(self.export_scene)
        file_menu.addAction(export_action)
        
        file_menu.addSeparator()
        
        exit_action = QAction("退出", self)
        exit_action.setShortcut("Ctrl+Q")
        exit_action.triggered.connect(self.close)
        file_menu.addAction(exit_action)
        
        # 编辑菜单
        edit_menu = menubar.addMenu("编辑")
        
        undo_action = QAction("撤销", self)
        undo_action.setShortcut("Ctrl+Z")
        edit_menu.addAction(undo_action)
        
        redo_action = QAction("重做", self)
        redo_action.setShortcut("Ctrl+Y")
        edit_menu.addAction(redo_action)
        
        # 视图菜单
        view_menu = menubar.addMenu("视图")
        
        reset_view_action = QAction("重置视图", self)
        reset_view_action.triggered.connect(self.reset_view)
        view_menu.addAction(reset_view_action)
        
        # 工具菜单
        tools_menu = menubar.addMenu("工具")
        
        material_editor_action = QAction("材质编辑器", self)
        material_editor_action.triggered.connect(self.open_material_editor)
        tools_menu.addAction(material_editor_action)
        
        if PYTORCH_AVAILABLE:
            ai_tools_menu = tools_menu.addMenu("AI工具")
            
            material_recognition_action = QAction("智能材质识别", self)
            material_recognition_action.triggered.connect(self.smart_material_recognition)
            ai_tools_menu.addAction(material_recognition_action)
            
            scene_optimization_action = QAction("场景优化", self)
            scene_optimization_action.triggered.connect(self.optimize_scene)
            ai_tools_menu.addAction(scene_optimization_action)
        
        # 帮助菜单
        help_menu = menubar.addMenu("帮助")
        
        about_action = QAction("关于", self)
        about_action.triggered.connect(self.show_about)
        help_menu.addAction(about_action)
    
    def create_toolbar(self):
        """创建工具栏"""
        toolbar = self.addToolBar("主工具栏")
        
        # 新建项目
        new_action = QAction(QIcon(), "新建", self)
        new_action.triggered.connect(self.new_project)
        toolbar.addAction(new_action)
        
        # 打开项目
        open_action = QAction(QIcon(), "打开", self)
        open_action.triggered.connect(self.open_project)
        toolbar.addAction(open_action)
        
        # 保存项目
        save_action = QAction(QIcon(), "保存", self)
        save_action.triggered.connect(self.save_project)
        toolbar.addAction(save_action)
        
        toolbar.addSeparator()
        
        # 视图控制
        zoom_in_action = QAction(QIcon(), "放大", self)
        zoom_in_action.triggered.connect(self.zoom_in)
        toolbar.addAction(zoom_in_action)
        
        zoom_out_action = QAction(QIcon(), "缩小", self)
        zoom_out_action.triggered.connect(self.zoom_out)
        toolbar.addAction(zoom_out_action)
        
        reset_view_action = QAction(QIcon(), "重置视图", self)
        reset_view_action.triggered.connect(self.reset_view)
        toolbar.addAction(reset_view_action)
    
    def create_status_bar(self):
        """创建状态栏"""
        status_bar = self.statusBar()
        
        # FPS显示
        self.fps_label = QLabel("FPS: 0")
        status_bar.addWidget(self.fps_label)
        
        # 对象数量
        self.object_count_label = QLabel("对象: 0")
        status_bar.addWidget(self.object_count_label)
        
        # 内存使用
        self.memory_label = QLabel("内存: 0 MB")
        status_bar.addWidget(self.memory_label)
        
        # 3ds Max连接状态
        if self.max_interface:
            self.max_connection_label = QLabel("3ds Max: 未连接")
            status_bar.addPermanentWidget(self.max_connection_label)
    
    def init_system_components(self):
        """初始化系统组件"""
        # 初始化材质管理器
        if OPENGL_AVAILABLE and hasattr(self, 'viewport'):
            self.material_manager = MaterialManager(self.viewport, self.material_library)
        
        # 初始化数据同步器
        if self.max_interface:
            self.data_synchronizer = DataSynchronizer(self.max_interface, self.scene_manager)
        
        # 加载默认材质到界面
        self.refresh_material_list()
        
        # 更新场景树
        self.refresh_scene_tree()
        
        # 启动更新定时器
        self.update_timer = QTimer()
        self.update_timer.timeout.connect(self.update_status)
        self.update_timer.start(100)  # 100ms更新一次
    
    def apply_theme(self):
        """应用深色主题"""
        dark_stylesheet = """
        QMainWindow {
            background-color: #2b2b2b;
            color: #ffffff;
        }
        
        QGroupBox {
            font-weight: bold;
            border: 2px solid #555555;
            border-radius: 5px;
            margin-top: 1ex;
            padding-top: 10px;
        }
        
        QGroupBox::title {
            subcontrol-origin: margin;
            left: 10px;
            padding: 0 5px 0 5px;
        }
        
        QPushButton {
            background-color: #404040;
            border: 1px solid #606060;
            border-radius: 3px;
            padding: 5px;
            min-height: 20px;
        }
        
        QPushButton:hover {
            background-color: #505050;
        }
        
        QPushButton:pressed {
            background-color: #353535;
        }
        
        QSlider::groove:horizontal {
            border: 1px solid #999999;
            height: 8px;
            background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #B1B1B1, stop:1 #c4c4c4);
            margin: 2px 0;
        }
        
        QSlider::handle:horizontal {
            background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #b4b4b4, stop:1 #8f8f8f);
            border: 1px solid #5c5c5c;
            width: 18px;
            margin: -2px 0;
            border-radius: 3px;
        }
        
        QTreeWidget, QListWidget {
            background-color: #353535;
            border: 1px solid #606060;
            selection-background-color: #4a4a4a;
        }
        
        QComboBox {
            background-color: #404040;
            border: 1px solid #606060;
            border-radius: 3px;
            padding: 1px 18px 1px 3px;
            min-width: 6em;
        }
        
        QSpinBox, QDoubleSpinBox {
            background-color: #404040;
            border: 1px solid #606060;
            border-radius: 3px;
            padding: 2px;
        }
        """
        
        self.setStyleSheet(dark_stylesheet)
    
    # 事件处理方法
    def new_project(self):
        """新建项目"""
        reply = QMessageBox.question(self, "新建项目", "确定要新建项目吗?未保存的更改将丢失。")
        if reply == QMessageBox.StandardButton.Yes:
            self.scene_manager = SceneManager()
            self.refresh_scene_tree()
            self.statusBar().showMessage("新建项目完成", 2000)
    
    def open_project(self):
        """打开项目"""
        file_path, _ = QFileDialog.getOpenFileName(
            self, "打开项目", "", "项目文件 (*.json);;所有文件 (*)"
        )
        
        if file_path:
            try:
                with open(file_path, 'r', encoding='utf-8') as f:
                    project_data = json.load(f)
                
                # 加载项目数据
                self.load_project_data(project_data)
                self.statusBar().showMessage(f"项目已打开: {file_path}", 2000)
                
            except Exception as e:
                QMessageBox.critical(self, "错误", f"打开项目失败: {str(e)}")
    
    def save_project(self):
        """保存项目"""
        file_path, _ = QFileDialog.getSaveFileName(
            self, "保存项目", "", "项目文件 (*.json);;所有文件 (*)"
        )
        
        if file_path:
            try:
                project_data = self.get_project_data()
                
                with open(file_path, 'w', encoding='utf-8') as f:
                    json.dump(project_data, f, indent=2, ensure_ascii=False)
                
                self.statusBar().showMessage(f"项目已保存: {file_path}", 2000)
                
            except Exception as e:
                QMessageBox.critical(self, "错误", f"保存项目失败: {str(e)}")
    
    def import_max_file(self):
        """导入3ds Max文件"""
        if not self.max_interface:
            QMessageBox.warning(self, "警告", "3ds Max接口不可用")
            return
        
        file_path, _ = QFileDialog.getOpenFileName(
            self, "导入3ds Max文件", "", 
            "3ds Max文件 (*.max);;FBX文件 (*.fbx);;OBJ文件 (*.obj);;所有文件 (*)"
        )
        
        if file_path:
            try:
                # 使用3ds Max接口导入文件
                success = self.max_interface.import_model(file_path)
                
                if success:
                    # 获取导入的场景信息并同步到PySide6
                    scene_info = self.max_interface.get_scene_info()
                    self.sync_scene_from_max(scene_info)
                    self.statusBar().showMessage(f"文件导入成功: {file_path}", 2000)
                else:
                    QMessageBox.warning(self, "警告", "文件导入失败")
                    
            except Exception as e:
                QMessageBox.critical(self, "错误", f"导入文件时出错: {str(e)}")
    
    def export_scene(self):
        """导出场景"""
        file_path, _ = QFileDialog.getSaveFileName(
            self, "导出场景", "", 
            "FBX文件 (*.fbx);;OBJ文件 (*.obj);;GLTF文件 (*.gltf);;所有文件 (*)"
        )
        
        if file_path:
            try:
                # 导出场景数据
                self.export_scene_to_file(file_path)
                self.statusBar().showMessage(f"场景导出成功: {file_path}", 2000)
                
            except Exception as e:
                QMessageBox.critical(self, "错误", f"导出场景时出错: {str(e)}")
    
    def on_object_selected(self):
        """对象选择事件"""
        current_item = self.scene_tree.currentItem()
        if current_item:
            object_name = current_item.text(0)
            obj = self.scene_manager.get_object(object_name)
            
            if obj:
                # 更新属性面板
                self.update_property_panel(obj)
                
                # 在视口中高亮显示
                if hasattr(self, 'viewport'):
                    self.viewport.highlight_object(object_name)
    
    def update_object_transform(self):
        """更新对象变换"""
        current_item = self.scene_tree.currentItem()
        if current_item:
            object_name = current_item.text(0)
            obj = self.scene_manager.get_object(object_name)
            
            if obj:
                # 获取新的变换值
                pos_x = self.position_x.value()
                pos_y = self.position_y.value()
                pos_z = self.position_z.value()
                
                rot_x = self.rotation_x.value()
                rot_y = self.rotation_y.value()
                rot_z = self.rotation_z.value()
                
                scale_x = self.scale_x.value()
                scale_y = self.scale_y.value()
                scale_z = self.scale_z.value()
                
                # 更新对象变换
                obj.transform.set_position(pos_x, pos_y, pos_z)
                obj.transform.set_rotation(rot_x, rot_y, rot_z)
                obj.transform.set_scale(scale_x, scale_y, scale_z)
                
                # 同步到3ds Max
                if self.data_synchronizer:
                    transform_data = {
                        'position': [pos_x, pos_y, pos_z],
                        'rotation': [rot_x, rot_y, rot_z],
                        'scale': [scale_x, scale_y, scale_z]
                    }
                    self.data_synchronizer.sync_object_from_pyside_to_max(
                        object_name, {'transform': transform_data}
                    )
                
                # 更新视口
                if hasattr(self, 'viewport'):
                    self.viewport.update()
    
    def filter_materials(self, category):
        """过滤材质列表"""
        self.refresh_material_list(category)
    
    def apply_material(self, item):
        """应用材质到选中对象"""
        material_name = item.text()
        current_item = self.scene_tree.currentItem()
        
        if current_item and self.material_manager:
            object_name = current_item.text(0)
            success = self.material_manager.apply_material_to_object(object_name, material_name)
            
            if success:
                self.statusBar().showMessage(f"材质 '{material_name}' 已应用到 '{object_name}'", 2000)
            else:
                self.statusBar().showMessage("材质应用失败", 2000)
    
    def add_material(self):
        """添加新材质"""
        dialog = MaterialEditorDialog(self, self.material_library)
        if dialog.exec() == QDialog.DialogCode.Accepted:
            self.refresh_material_list()
    
    def edit_material(self):
        """编辑材质"""
        current_item = self.material_list.currentItem()
        if current_item:
            material_name = current_item.text()
            material = self.material_library.get_material(material_name)
            
            if material:
                dialog = MaterialEditorDialog(self, self.material_library, material)
                if dialog.exec() == QDialog.DialogCode.Accepted:
                    self.refresh_material_list()
    
    def choose_albedo_color(self):
        """选择基础颜色"""
        color = QColorDialog.getColor(Qt.GlobalColor.white, self)
        if color.isValid():
            self.albedo_color_btn.setStyleSheet(
                f"background-color: rgb({color.red()}, {color.green()}, {color.blue()});"
            )
            self.update_material_properties()
    
    def update_material_properties(self):
        """更新材质属性"""
        # 更新标签显示
        metallic_value = self.metallic_slider.value() / 100.0
        roughness_value = self.roughness_slider.value() / 100.0
        
        self.metallic_label.setText(f"{metallic_value:.2f}")
        self.roughness_label.setText(f"{roughness_value:.2f}")
        
        # 应用材质属性到当前选中对象
        current_item = self.scene_tree.currentItem()
        if current_item and hasattr(self, 'viewport'):
            # 创建临时材质
            temp_material = {
                'metallic': metallic_value,
                'roughness': roughness_value,
                'albedo': self.get_albedo_color_from_button()
            }
            
            self.viewport.set_material(temp_material)
    
    def get_albedo_color_from_button(self):
        """从按钮获取albedo颜色"""
        style = self.albedo_color_btn.styleSheet()
        # 简化的颜色提取(实际应用中可能需要更复杂的解析)
        try:
            import re
            match = re.search(r'rgb\((\d+),\s*(\d+),\s*(\d+)\)', style)
            if match:
                r, g, b = map(int, match.groups())
                return [r/255.0, g/255.0, b/255.0]
        except:
            pass
        return [0.8, 0.8, 0.8]  # 默认颜色
    
    def update_render_quality(self, quality):
        """更新渲染质量"""
        if hasattr(self, 'viewport'):
            quality_map = {"低": "low", "中": "medium", "高": "high", "超高": "ultra"}
            self.viewport.set_render_quality(quality_map.get(quality, "high"))
    
    def toggle_shadows(self, enabled):
        """切换阴影"""
        if hasattr(self, 'viewport'):
            self.viewport.set_shadows_enabled(enabled)
    
    def toggle_ao(self, enabled):
        """切换环境光遮蔽"""
        if hasattr(self, 'viewport'):
            self.viewport.set_ao_enabled(enabled)
    
    def connect_to_max(self):
        """连接到3ds Max"""
        if self.max_interface:
            try:
                # 尝试连接并获取场景信息
                scene_info = self.max_interface.get_scene_info()
                
                if scene_info:
                    self.max_status_label.setText("已连接")
                    self.max_status_label.setStyleSheet("color: green;")
                    
                    if hasattr(self, 'max_connection_label'):
                        self.max_connection_label.setText("3ds Max: 已连接")
                    
                    self.statusBar().showMessage("已连接到3ds Max", 2000)
                else:
                    raise Exception("无法获取场景信息")
                    
            except Exception as e:
                QMessageBox.warning(self, "连接失败", f"无法连接到3ds Max: {str(e)}")
    
    def sync_with_max(self):
        """与3ds Max同步场景"""
        if self.max_interface and self.data_synchronizer:
            try:
                scene_info = self.max_interface.get_scene_info()
                self.sync_scene_from_max(scene_info)
                self.statusBar().showMessage("场景同步完成", 2000)
                
            except Exception as e:
                QMessageBox.critical(self, "同步失败", f"场景同步失败: {str(e)}")
    
    def smart_material_recognition(self):
        """智能材质识别"""
        if not PYTORCH_AVAILABLE:
            QMessageBox.warning(self, "功能不可用", "PyTorch未安装,AI功能不可用")
            return
        
        file_path, _ = QFileDialog.getOpenFileName(
            self, "选择材质图像", "", "图像文件 (*.png *.jpg *.jpeg *.bmp);;所有文件 (*)"
        )
        
        if file_path:
            try:
                # 使用AI模型识别材质
                result = self.material_classifier.predict(file_path)
                
                # 显示识别结果
                message = f"识别结果:\n"
                message += f"材质类型: {result['material_class']}\n"
                message += f"置信度: {result['confidence']:.2%}\n"
                message += f"材质属性:\n"
                message += f"  金属度: {result['properties']['metallic']:.2f}\n"
                message += f"  粗糙度: {result['properties']['roughness']:.2f}\n"
                message += f"  镜面反射: {result['properties']['specular']:.2f}\n"
                
                QMessageBox.information(self, "材质识别结果", message)
                
                # 可选:自动应用识别的材质属性
                reply = QMessageBox.question(self, "应用材质", "是否要应用识别的材质属性?")
                if reply == QMessageBox.StandardButton.Yes:
                    self.apply_recognized_material(result)
                
            except Exception as e:
                QMessageBox.critical(self, "识别失败", f"材质识别失败: {str(e)}")
    
    def optimize_scene(self):
        """优化场景"""
        if not PYTORCH_AVAILABLE:
            QMessageBox.warning(self, "功能不可用", "PyTorch未安装,AI功能不可用")
            return
        
        try:
            # 获取当前场景信息
            scene_info = self.get_current_scene_info()
            
            # 使用AI优化场景
            optimized_params = self.rendering_optimizer.optimize_render_quality(
                scene_info, target_quality='high'
            )
            
            # 应用优化参数
            self.apply_optimization_params(optimized_params)
            
            self.statusBar().showMessage("场景优化完成", 2000)
            
        except Exception as e:
            QMessageBox.critical(self, "优化失败", f"场景优化失败: {str(e)}")
    
    def zoom_in(self):
        """放大视图"""
        if hasattr(self, 'viewport'):
            self.viewport.zoom_in()
    
    def zoom_out(self):
        """缩小视图"""
        if hasattr(self, 'viewport'):
            self.viewport.zoom_out()
    
    def reset_view(self):
        """重置视图"""
        if hasattr(self, 'viewport'):
            self.viewport.reset_view()
    
    def open_material_editor(self):
        """打开材质编辑器"""
        dialog = MaterialEditorDialog(self, self.material_library)
        dialog.exec()
    
    def show_about(self):
        """显示关于对话框"""
        about_text = """
        

建筑可视化与数字孪生城市建模系统

版本: 1.0.0

作者: 丁林松

创建时间: 2024年

基于PySide6与3ds Max集成的建筑设计方案三维预览系统,

支持材质贴图实时切换和AI驱动的场景优化。

主要技术栈:

  • PySide6 - 用户界面框架
  • OpenGL - 三维渲染引擎
  • PyTorch - 深度学习框架
  • 3ds Max SDK - 三维建模软件集成

© 2024 丁林松 - 建筑可视化与数字孪生城市建模技术应用

基于PySide6与3ds Max集成的专业建筑设计解决方案  代码格式可能有些乱  @littleatendian

Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐